mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
Add the gfcloud full-cloud controller daemon
gfcloud runs the machine under the Glowforge web service (the factory cloud experience: the app drives homing, framing, printing). Its init service starts it only when controller_mode = cloud - mutually exclusive with grblHAL, which already stands down for that mode - so it owns /dev/glowforge exclusively. Persistent GFUIService loop with a clean SIGTERM shutdown that safes the hardware; the init script mirrors grblhal.init. The image installs gfcloud and python3-ffmachine.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
### BEGIN INIT INFO
|
||||
# Provides: gfcloud
|
||||
# Required-Start: $local_fs $network
|
||||
# Required-Stop:
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: Glowforge web-service controller (full cloud mode)
|
||||
### END INIT INFO
|
||||
|
||||
DAEMON=/usr/sbin/gfcloud.py
|
||||
PIDFILE=/var/run/gfcloud.pid
|
||||
CONF=/data/forgefirm.conf
|
||||
|
||||
# Boot-time controller-mode dispatch, mirror of grblhal.init: this daemon
|
||||
# runs the machine under the Glowforge web service. It starts only when
|
||||
# controller_mode = cloud (which keeps grblHAL down); anything else - grbl,
|
||||
# unset, or no config file - leaves it stopped.
|
||||
controller_mode() {
|
||||
[ -r $CONF ] || { echo grbl; return; }
|
||||
m=$(sed -n 's/^[ \t]*controller_mode[ \t]*=[ \t]*\([a-z]*\).*/\1/p' $CONF | tail -n 1)
|
||||
echo "${m:-grbl}"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
if [ "$(controller_mode)" != "cloud" ]; then
|
||||
echo "gfcloud not started (controller_mode != cloud)"
|
||||
exit 0
|
||||
fi
|
||||
echo "Starting gfcloud"
|
||||
start-stop-daemon -S -q -p $PIDFILE -m -b -x $DAEMON
|
||||
;;
|
||||
stop)
|
||||
echo "Stopping gfcloud"
|
||||
start-stop-daemon -K -q -p $PIDFILE
|
||||
rm -f $PIDFILE
|
||||
;;
|
||||
restart)
|
||||
$0 stop
|
||||
sleep 2
|
||||
$0 start
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/python3
|
||||
"""
|
||||
gfcloud - full Glowforge web-service controller for ForgeFIRM.
|
||||
|
||||
Runs the machine under the Glowforge web service (the factory cloud
|
||||
experience): the phone/web app drives homing, framing, and printing.
|
||||
Started by the gfcloud init service when controller_mode = cloud in
|
||||
/data/forgefirm.conf, which keeps grblHAL down so this daemon owns
|
||||
/dev/glowforge exclusively.
|
||||
|
||||
Reconnects (fresh single-use ws_token) and 401 re-auth are handled in
|
||||
gfutilities. On SIGTERM the service loop stops and the machine is shut
|
||||
down safe (laser latched, steppers disabled, deadman released).
|
||||
|
||||
(C) Copyright 2026
|
||||
Scott Wiederhold, s.e.wiederhold@gmail.com
|
||||
SPDX-License-Identifier: MIT
|
||||
"""
|
||||
import argparse
|
||||
import logging
|
||||
import shutil
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from gfutilities.configuration import parse, get_cfg, log_level
|
||||
from gfutilities import GFUIService
|
||||
|
||||
import ffmachine
|
||||
|
||||
CONF = '/data/etc/gfhome.conf'
|
||||
CONF_SAMPLE = '/etc/gfhome.conf.sample'
|
||||
|
||||
logging.basicConfig(format='(%(levelname)s) %(module)s:%(funcName)s %(message)s')
|
||||
logger = logging.getLogger('openglow')
|
||||
|
||||
|
||||
def load_config(path: str) -> bool:
|
||||
if path == CONF and not Path(CONF).is_file() and Path(CONF_SAMPLE).is_file():
|
||||
Path(CONF).parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(CONF_SAMPLE, CONF)
|
||||
if not Path(path).is_file():
|
||||
logger.error('config file %s not found', path)
|
||||
return False
|
||||
parse(path)
|
||||
if not get_cfg('SERVICE.SERVER_URL'):
|
||||
logger.error('config %s has no SERVICE section', path)
|
||||
return False
|
||||
if get_cfg('LOGGING.FILE'):
|
||||
Path(get_cfg('LOGGING.FILE')).parent.mkdir(parents=True, exist_ok=True)
|
||||
fh = logging.FileHandler(get_cfg('LOGGING.FILE'))
|
||||
fh.setLevel(log_level(get_cfg('LOGGING.LEVEL')))
|
||||
fh.setFormatter(logging.Formatter(
|
||||
'%(asctime)s (%(levelname)s) %(module)s:%(funcName)s %(message)s'))
|
||||
logger.addHandler(fh)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description='ForgeFIRM full Glowforge cloud controller')
|
||||
ap.add_argument('-c', '--config', default=CONF, help='config file (default %s)' % CONF)
|
||||
args = ap.parse_args()
|
||||
|
||||
if not load_config(args.config):
|
||||
return 1
|
||||
|
||||
ffmachine.apply_identity_overrides()
|
||||
|
||||
# Machine() reads the OCOTP identity and head info; it fails cleanly if
|
||||
# grblHAL still holds /dev/glowforge (controller_mode must be cloud).
|
||||
try:
|
||||
machine = ffmachine.build_machine()
|
||||
except Exception:
|
||||
logger.exception('machine init failed (is grblHAL still running? '
|
||||
'controller_mode must be cloud)')
|
||||
return 1
|
||||
|
||||
service = GFUIService(machine)
|
||||
|
||||
def _shutdown(*_):
|
||||
logger.info('shutdown requested')
|
||||
service.request_stop()
|
||||
|
||||
signal.signal(signal.SIGTERM, _shutdown)
|
||||
signal.signal(signal.SIGINT, _shutdown)
|
||||
|
||||
# Run for the life of the daemon. connect() can fail if the network or
|
||||
# service is briefly unavailable (e.g. at boot); retry until stopped.
|
||||
# Once connected, run() stays up across WS drops (gfutilities reconnects
|
||||
# with a fresh token) and returns only when a stop is requested, having
|
||||
# shut the machine down safe.
|
||||
while not service.stop:
|
||||
if service.connect():
|
||||
service.run()
|
||||
break
|
||||
logger.error('connect failed; retrying in 10s')
|
||||
for _ in range(100):
|
||||
if service.stop:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
logger.info('gfcloud exit')
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,29 @@
|
||||
DESCRIPTION = "Full Glowforge web-service controller daemon for ForgeFIRM (cloud mode)"
|
||||
HOMEPAGE = "https://github.com/ScottW514/forgefirm"
|
||||
|
||||
LICENSE = "MIT"
|
||||
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
|
||||
|
||||
PV = "0.1.0"
|
||||
|
||||
SRC_URI = " \
|
||||
file://gfcloud.py \
|
||||
file://gfcloud.init \
|
||||
"
|
||||
|
||||
S = "${WORKDIR}"
|
||||
|
||||
inherit update-rc.d
|
||||
|
||||
INITSCRIPT_NAME = "gfcloud"
|
||||
INITSCRIPT_PARAMS = "defaults 92"
|
||||
|
||||
do_install() {
|
||||
install -Dm 0755 ${WORKDIR}/gfcloud.py ${D}${sbindir}/gfcloud.py
|
||||
install -d ${D}${sysconfdir}/init.d
|
||||
install -m 0755 ${WORKDIR}/gfcloud.init ${D}${sysconfdir}/init.d/gfcloud
|
||||
}
|
||||
|
||||
# Shares the Glowforge web-service config with gfhome (SERVICE section,
|
||||
# camera captures via forgectrl); pulls the shared machine glue.
|
||||
RDEPENDS:${PN} += "python3-core python3-ffmachine python3-gfhardware python3-gfutilities"
|
||||
Reference in New Issue
Block a user