Name it Gyre - Sand Art Table
README, build guide, software README, web page header and title, jog page, desktop alerts and the start-up message now say Gyre. The repo, the Python package and its commands keep their names (sandTable, python3 -m sandtable), so links and setups keep working. New web page screenshot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e7a4f67a96
commit
97ebae93b2
9 files changed
+22
-22
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Build guide
|
||||
# Gyre - build guide
|
||||
|
||||
This guide takes you from a pile of parts to a table drawing in sand. It follows the order that works: case first, then the electronics, the arm on the bench, the arm into the table, a test run without sand, and finally the floor, sand and glass.
|
||||
This guide takes you from a pile of parts to Gyre, a sand art table, drawing in sand. It follows the order that works: case first, then the electronics, the arm on the bench, the arm into the table, a test run without sand, and finally the floor, sand and glass.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# sandTable
|
||||
# Gyre - Sand Art Table
|
||||
|
||||
A 12" kinetic sand table that draws patterns in sand. A steel ball rolls under glass, pulled by a magnet on a polar arm underneath. The case is about 12.5" across and 5" tall. The parts are 3D-printed for a Bambu P1S and designed in SolidCraft.
|
||||
Gyre is a 12" kinetic sand table that draws patterns in sand. A steel ball rolls under glass, pulled by a magnet on a polar arm underneath. The case is about 12.5" across and 5" tall. The parts are 3D-printed for a Bambu P1S and designed in SolidCraft.
|
||||
|
||||
**Building one? Start with [BUILD.md](BUILD.md):** parts to buy, what to print, assembly in order, wiring, the first run and troubleshooting.
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Sand table software
|
||||
# Gyre software
|
||||
|
||||
A small Python program that drives the table from your computer over USB. It has a web page with a library of designs, plays `.thr` pattern files (yours or the built-in ones), and erases the sand with a spiral between patterns.
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 306 KiB |
@@ -1 +1 @@
|
||||
"""Sand table pattern player: .thr files and built-in patterns -> GRBL / FluidNC over USB."""
|
||||
"""Gyre sand art table: pattern player for .thr files and built-in patterns -> GRBL / FluidNC over USB."""
|
||||
@@ -52,7 +52,7 @@ def alert(title, msg):
|
||||
return
|
||||
try:
|
||||
import subprocess
|
||||
subprocess.run(['notify-send', '-u', 'critical', '-a', 'Sand table', title, msg],
|
||||
subprocess.run(['notify-send', '-u', 'critical', '-a', 'Gyre', title, msg],
|
||||
timeout=5, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -71,15 +71,15 @@ def guarded(fn):
|
||||
try:
|
||||
return fn()
|
||||
except SwitchTripped as e:
|
||||
alert('Sand table stopped: home switch pressed',
|
||||
alert('Gyre stopped: home switch pressed',
|
||||
f'{e}.\nThe motors were stopped at once. Check the arm, belts and carriage, then run '
|
||||
f'`play` again; it homes first.')
|
||||
sys.exit(1)
|
||||
except GrblError as e:
|
||||
alert('Sand table stopped', str(e))
|
||||
alert('Gyre stopped', str(e))
|
||||
sys.exit(1)
|
||||
except OSError as e: # pyserial's SerialException is an OSError: the USB link dropped
|
||||
alert('Sand table: USB connection lost',
|
||||
alert('Gyre: USB connection lost',
|
||||
f'{e}\nThe Uno restarts when it is plugged back in, so the table stops where it is. '
|
||||
f'Run `play` again; it homes first.')
|
||||
sys.exit(1)
|
||||
|
||||
@@ -13,7 +13,7 @@ from .grbl import RT_HOLD, RT_JOG_CANCEL, GrblError
|
||||
|
||||
PAGE = r"""<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Sand Table Jog</title>
|
||||
<title>Gyre - Jog</title>
|
||||
<style>
|
||||
:root{--bg:#f6f4ef;--card:#fff;--ink:#222;--mute:#666;--line:#ddd;--acc:#2f6f5e;--stop:#c0392b}
|
||||
@media (prefers-color-scheme:dark){:root{--bg:#1b1c1e;--card:#26282b;--ink:#eee;--mute:#aaa;--line:#3a3d41;--acc:#5fb49c}}
|
||||
@@ -36,7 +36,7 @@ input{font:inherit;width:90px;padding:8px;border-radius:6px;border:1px solid var
|
||||
.err{color:var(--stop)}
|
||||
small{color:var(--mute)}
|
||||
</style></head><body><main>
|
||||
<h1>Sand table: jog & calibrate</h1>
|
||||
<h1>Gyre: jog & calibrate</h1>
|
||||
<div class="card"><div class="stat">
|
||||
<div><span>State</span><b id="state">-</b></div>
|
||||
<div><span>Rho (mm)</span><b id="rho">-</b></div>
|
||||
|
||||
@@ -251,17 +251,17 @@ class Player:
|
||||
self.p.homed = False
|
||||
self.message = f'Stopped: {e}. Check the arm, belt and carriage, then play again (it homes first).'
|
||||
log('ERROR ' + self.message + f' | status {c.status.state} pins={c.status.pins!r} wpos={c.status.wpos}')
|
||||
self.alert('Sand table stopped: home switch pressed', self.message)
|
||||
self.alert('Gyre stopped: home switch pressed', self.message)
|
||||
except OSError as e: # the USB cable came out: the panel reconnects
|
||||
self.p.lost(e)
|
||||
self.message = 'Stopped: the USB connection to the Uno was lost. Plug it back in; the page reconnects by itself.'
|
||||
log(f'ERROR USB lost: {e!r}')
|
||||
self.alert('Sand table: USB connection lost', self.message)
|
||||
self.alert('Gyre: USB connection lost', self.message)
|
||||
except GrblError as e:
|
||||
self.p.homed = False
|
||||
self.message = f'Stopped: {e}'
|
||||
log('ERROR ' + self.message + f' | status {c.status.state} wpos={c.status.wpos} errors={c.errors[-3:]}')
|
||||
self.alert('Sand table stopped', self.message)
|
||||
self.alert('Gyre stopped', self.message)
|
||||
except Exception as e: # a bug: stop safely and say so
|
||||
import traceback
|
||||
self.p.homed = False
|
||||
@@ -271,7 +271,7 @@ class Player:
|
||||
c.stop()
|
||||
except Exception:
|
||||
pass
|
||||
self.alert('Sand table stopped', self.message)
|
||||
self.alert('Gyre stopped', self.message)
|
||||
finally:
|
||||
c.guard = None
|
||||
c.abort = False
|
||||
@@ -324,7 +324,7 @@ class Player:
|
||||
# ---------------------------------------------------------------- page
|
||||
PAGE = r"""<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Sand Table</title>
|
||||
<title>Gyre - Sand Art Table</title>
|
||||
<style>
|
||||
:root{--bg:#f4f1ea;--card:#fffdf8;--ink:#2a2622;--mute:#6f675d;--line:#e2dbcf;--acc:#2f6f5e;--acc-ink:#fff;--stop:#c0392b;--bar:#e9e2d4}
|
||||
@media (prefers-color-scheme:dark){:root{--bg:#18191b;--card:#232528;--ink:#ece8e1;--mute:#a59e94;--line:#36393d;--acc:#5fb49c;--acc-ink:#0d1f1a;--bar:#2e3135}}
|
||||
@@ -369,7 +369,7 @@ a{color:var(--acc)}
|
||||
@media (max-width:560px){#now img{width:72px;height:72px} #now .info{flex-basis:150px} #now .row{width:100%} #now .row button{flex:1} h1{font-size:18px}}
|
||||
</style></head><body><main>
|
||||
<header>
|
||||
<h1>Sand Table</h1>
|
||||
<h1>Gyre <span style="font-weight:400;color:var(--mute);font-size:15px">sand art table</span></h1>
|
||||
<span class="pill" id="mach">connecting…</span>
|
||||
<button id="homeBtn" onclick="post('home')">Home</button>
|
||||
<button class="stop" onclick="post('stop')">STOP</button>
|
||||
@@ -511,8 +511,8 @@ def serve(ctrl, cfg, host='127.0.0.1', port=8765, open_browser=True, open_path='
|
||||
lib = library or Library(cfg)
|
||||
player = Player(panel, cfg, lib, alert=alert)
|
||||
threading.Thread(target=lib.warm, daemon=True).start()
|
||||
jog_page = JOG_PAGE.replace('<h1>Sand table: jog & calibrate</h1>',
|
||||
'<p><a href="/">← Library</a></p><h1>Sand table: jog & calibrate</h1>')
|
||||
jog_page = JOG_PAGE.replace('<h1>Gyre: jog & calibrate</h1>',
|
||||
'<p><a href="/">← Library</a></p><h1>Gyre: jog & calibrate</h1>')
|
||||
|
||||
def busy():
|
||||
if not panel.connected:
|
||||
@@ -614,7 +614,7 @@ def serve(ctrl, cfg, host='127.0.0.1', port=8765, open_browser=True, open_path='
|
||||
srv = ThreadingHTTPServer((host, port), H)
|
||||
srv.daemon_threads = True
|
||||
url = f'http://{"127.0.0.1" if host in ("0.0.0.0", "") else host}:{port}'
|
||||
print(f'Sand table page at {url}{open_path} (Ctrl-C here to quit)', flush=True)
|
||||
print(f'Gyre page at {url}{open_path} (Ctrl-C here to quit)', flush=True)
|
||||
if host in ('0.0.0.0', ''):
|
||||
print(' Also open to other devices on your network, with no password: anyone on it can drive the table.')
|
||||
if open_browser:
|
||||
|
||||
@@ -249,7 +249,7 @@ class TestHttp(unittest.TestCase):
|
||||
return json.loads(r.read())
|
||||
|
||||
def test_pages_and_play(self):
|
||||
self.assertIn(b'Sand Table', self.get('/'))
|
||||
self.assertIn(b'Gyre', self.get('/'))
|
||||
self.assertIn(b'jog & calibrate', self.get('/jog'))
|
||||
lib = json.loads(self.get('/api/library'))
|
||||
self.assertEqual(len(lib), 2)
|
||||
|
||||
Reference in new issue
Block a user