From db78b1f2e61d5a8adb92fd66d5871c723144fcab Mon Sep 17 00:00:00 2001 From: Christopher Koch Date: Sun, 1 Nov 2026 21:25:10 -0500 Subject: [PATCH] Start Gyre from an icon: launch / quit / install-desktop - python3 -m sandtable install-desktop puts a Gyre icon in the app grid and on the desktop (freedesktop .desktop entry with a "Quit Gyre" action). - The icon runs `launch`: starts the web page in the background (no terminal, own session, log in .cache/server.log) if it isn't running, then opens it. A dialog shows the reason if it can't start. - Quit link on the page (local computer only) and `quit`: stop any design, motors off, close the page. - Gyre icon (sandtable/static/gyre.svg), also the page's tab icon. - Simulator takes several lines per tick, so streaming tests aren't slowed. Co-Authored-By: Claude Opus 5.5 --- BUILD.md | 10 ++- software/README.md | 10 +-- software/sandtable/cli.py | 7 ++ software/sandtable/launcher.py | 112 +++++++++++++++++++++++++++++ software/sandtable/static/gyre.svg | 15 ++++ software/sandtable/ui.py | 1 + software/sandtable/web.py | 30 +++++++- software/tests/fakegrbl.py | 3 +- software/tests/test_web.py | 20 ++++++ 9 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 software/sandtable/launcher.py create mode 100644 software/sandtable/static/gyre.svg diff --git a/BUILD.md b/BUILD.md index 79455c2..30d598d 100644 --- a/BUILD.md +++ b/BUILD.md @@ -247,17 +247,15 @@ The full details are in [`software/README.md`](software/README.md). In short, fr 5. **Glass:** lay it on the sand ring. The two finger notches in the rim help lift it out. ## 14. Everyday use -From the `software` folder, run: -``` -python3 -m sandtable web -``` -This opens a page in your browser with the design library: +Once, from the `software` folder, run `python3 -m sandtable install-desktop`. It adds a **Gyre** icon to your app grid and desktop (Linux). On GNOME, right-click the desktop icon once and choose **Allow Launching**. + +From then on, click the Gyre icon. It opens a page in your browser with the design library: - click a design to draw it; it homes and erases the sand first; - **Play all** runs the whole list, shuffled and repeating; - when the designs finish (or you press Stop), the motors switch off and the page shows "Motors off". The next design homes first; - **Upload** takes `.thr` files (from [sandify.org](https://sandify.org)) and `.svg` line drawings. -Leave the terminal open while the table runs. See [`software/README.md`](software/README.md) for everything else. +**Quit**, at the bottom of the page, closes Gyre. See [`software/README.md`](software/README.md) for everything else, and [`pi/README.md`](pi/README.md) to run the table from a Raspberry Pi Zero 2 W with no computer at all. ## 15. Troubleshooting | Symptom | Likely cause | Fix | diff --git a/software/README.md b/software/README.md index bbeb81b..815e3e9 100644 --- a/software/README.md +++ b/software/README.md @@ -10,10 +10,12 @@ A small Python program that drives the table from your computer over USB. It has **Status:** running on the table since 2026-11-01. On a new build, do the first-run steps below in order. ## Web page (everyday use) -``` -python3 -m sandtable web -``` -This opens http://127.0.0.1:8765 in your browser. The table stays on the USB cable and the page is the remote control, so leave the program running in its terminal (Ctrl-C there quits). +**Start it from the Gyre icon** in your app grid or on the desktop. It starts Gyre in the background, with no terminal, and opens http://127.0.0.1:8765 in your browser. Clicking it again while Gyre runs just opens the page. +- **Close it:** use **Quit** at the bottom of the page, or right-click the icon and choose **Quit Gyre**. Either way any design stops and the motors switch off first. +- **One-time setup (Linux):** `python3 -m sandtable install-desktop` adds the icon to the app grid and the desktop. On GNOME, right-click the desktop icon once and choose **Allow Launching**. To add it to the dock, right-click it in the app grid and choose **Pin to Dash**. +- **If it can't start,** a pop-up shows the reason. The log is `.cache/server.log`. +- **From a terminal:** `python3 -m sandtable web` does the same in the foreground (Ctrl-C quits). +- **On a Raspberry Pi** inside the table, there's no computer to start at all: see `../pi/README.md`. ![web page](previews/web-page.png) diff --git a/software/sandtable/cli.py b/software/sandtable/cli.py index 46f0256..ee771a2 100644 --- a/software/sandtable/cli.py +++ b/software/sandtable/cli.py @@ -452,6 +452,9 @@ def main(argv=None): sub = ap.add_subparsers(dest='cmd', required=True) sub.add_parser('ports', help='list serial ports') sub.add_parser('info', help='connect and show the controller and its settings') + sub.add_parser('launch', help='start the web page in the background if needed and open it (the Gyre icon)') + sub.add_parser('quit', help='close the web page started by launch (stops any design, motors off)') + sub.add_parser('install-desktop', help='put a Gyre icon in the app grid and on the desktop (Linux)') for name, text in (('web', 'the web page: a library of designs to play, plus the jog panel'), ('ui', 'the same web page, opened at the jog panel: home, jog, calibrate')): u = sub.add_parser(name, help=text) @@ -483,5 +486,9 @@ def main(argv=None): m.add_argument('--no-home', action='store_true') args = ap.parse_args(argv) cfg = config.load(args.config) + if args.cmd in ('launch', 'quit', 'install-desktop'): # no USB connection needed here + from . import launcher + sys.exit({'launch': launcher.launch, 'quit': launcher.quit_gyre, + 'install-desktop': lambda: launcher.install_desktop() and 0}[args.cmd]()) {'ports': cmd_ports, 'info': cmd_info, 'flash': cmd_flash, 'ui': cmd_web, 'web': cmd_web, 'setup': cmd_setup, 'test': cmd_test, 'home': cmd_home, 'play': cmd_play, 'gen': cmd_gen, 'erase': cmd_erase, 'move': cmd_move}[args.cmd](cfg, args) diff --git a/software/sandtable/launcher.py b/software/sandtable/launcher.py new file mode 100644 index 0000000..892cac1 --- /dev/null +++ b/software/sandtable/launcher.py @@ -0,0 +1,112 @@ +"""Start Gyre from a desktop icon: run the web page in the background (no terminal window) and +open it in the browser. Used by the Gyre icon (`install-desktop` puts it in the app grid and on +the desktop): + python3 -m sandtable launch start if needed, then open the page + python3 -m sandtable quit stop any design, motors off, close Gyre +""" +import json +import os +import shutil +import subprocess +import sys +import time +import urllib.request +import webbrowser + +SOFTWARE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +ICON = os.path.join(SOFTWARE, 'sandtable', 'static', 'gyre.svg') +LOG = os.path.join(SOFTWARE, '.cache', 'server.log') + + +def status(port): + """The running Gyre page's status, or None if nothing (or something else) answers.""" + try: + with urllib.request.urlopen(f'http://127.0.0.1:{port}/api/status', timeout=1.5) as r: + d = json.load(r) + return d if 'machine' in d else None + except Exception: + return None + + +def say(title, msg, error=False): + """Tell the person at the desktop: a dialog if possible, else a notification.""" + print(f'{title}: {msg}', flush=True) + if shutil.which('zenity'): + subprocess.run(['zenity', '--error' if error else '--info', '--title', title, '--width', '420', + '--text', msg], stderr=subprocess.DEVNULL) + elif shutil.which('notify-send'): + subprocess.run(['notify-send', '-a', 'Gyre', title, msg]) + + +def launch(port=8765): + if status(port) is None: + os.makedirs(os.path.dirname(LOG), exist_ok=True) + log = open(LOG, 'a') + log.write(time.strftime('\n=== %Y-%m-%d %H:%M:%S starting from the Gyre icon\n')) + log.flush() + subprocess.Popen([sys.executable, '-m', 'sandtable', 'web', '--no-browser', '--http-port', str(port)], + cwd=SOFTWARE, stdout=log, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, + start_new_session=True) # keeps running after this launcher exits + t_end = time.time() + 25 + while time.time() < t_end and status(port) is None: + time.sleep(0.4) + if status(port) is None: + with open(LOG) as f: + tail = ''.join(f.readlines()[-12:]) + say('Gyre could not start', f'The page did not come up. The last lines of its log:\n\n{tail}', error=True) + return 1 + webbrowser.open(f'http://127.0.0.1:{port}/') + return 0 + + +def quit_gyre(port=8765): + try: + req = urllib.request.Request(f'http://127.0.0.1:{port}/api/quit', data=b'{}', method='POST', + headers={'Content-Type': 'application/json'}) + with urllib.request.urlopen(req, timeout=40) as r: + ok = json.load(r).get('ok') + except Exception: + ok = False + if not ok: + say('Gyre', 'Gyre is not running.') + return 0 + + +def install_desktop(): + """Put a Gyre icon in the app grid and on the desktop (Linux desktops that follow the + freedesktop.org standard: GNOME, KDE, Cinnamon, Xfce...).""" + entry = f"""[Desktop Entry] +Type=Application +Name=Gyre +GenericName=Sand art table +Comment=Draw designs on the Gyre sand table +Exec={sys.executable} -m sandtable launch +Path={SOFTWARE} +Icon={ICON} +Terminal=false +Categories=Utility; +StartupNotify=false +Actions=quit; + +[Desktop Action quit] +Name=Quit Gyre +Exec={sys.executable} -m sandtable quit +""" + apps = os.path.expanduser('~/.local/share/applications') + os.makedirs(apps, exist_ok=True) + places = [os.path.join(apps, 'gyre.desktop')] + desktop = subprocess.run(['xdg-user-dir', 'DESKTOP'], capture_output=True, text=True).stdout.strip() \ + if shutil.which('xdg-user-dir') else os.path.expanduser('~/Desktop') + if desktop and os.path.isdir(desktop) and desktop != os.path.expanduser('~'): + places.append(os.path.join(desktop, 'Gyre.desktop')) + for p in places: + with open(p, 'w') as f: + f.write(entry) + os.chmod(p, 0o755) + if shutil.which('gio'): # GNOME: allow launching from the desktop + subprocess.run(['gio', 'set', p, 'metadata::trusted', 'true'], stderr=subprocess.DEVNULL) + if shutil.which('update-desktop-database'): + subprocess.run(['update-desktop-database', apps], stderr=subprocess.DEVNULL) + for p in places: + print(' wrote', p) + return places diff --git a/software/sandtable/static/gyre.svg b/software/sandtable/static/gyre.svg new file mode 100644 index 0000000..3179f3a --- /dev/null +++ b/software/sandtable/static/gyre.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/software/sandtable/ui.py b/software/sandtable/ui.py index a7ba79e..0a55fd7 100644 --- a/software/sandtable/ui.py +++ b/software/sandtable/ui.py @@ -14,6 +14,7 @@ from .grbl import RT_HOLD, RT_JOG_CANCEL, GrblError PAGE = r""" Gyre - Jog +