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.
|
**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.
|
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
|
return
|
||||||
try:
|
try:
|
||||||
import subprocess
|
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)
|
timeout=5, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -71,15 +71,15 @@ def guarded(fn):
|
|||||||
try:
|
try:
|
||||||
return fn()
|
return fn()
|
||||||
except SwitchTripped as e:
|
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'{e}.\nThe motors were stopped at once. Check the arm, belts and carriage, then run '
|
||||||
f'`play` again; it homes first.')
|
f'`play` again; it homes first.')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except GrblError as e:
|
except GrblError as e:
|
||||||
alert('Sand table stopped', str(e))
|
alert('Gyre stopped', str(e))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except OSError as e: # pyserial's SerialException is an OSError: the USB link dropped
|
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'{e}\nThe Uno restarts when it is plugged back in, so the table stops where it is. '
|
||||||
f'Run `play` again; it homes first.')
|
f'Run `play` again; it homes first.')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from .grbl import RT_HOLD, RT_JOG_CANCEL, GrblError
|
|||||||
|
|
||||||
PAGE = r"""<!doctype html>
|
PAGE = r"""<!doctype html>
|
||||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
<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>
|
<style>
|
||||||
:root{--bg:#f6f4ef;--card:#fff;--ink:#222;--mute:#666;--line:#ddd;--acc:#2f6f5e;--stop:#c0392b}
|
: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}}
|
@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)}
|
.err{color:var(--stop)}
|
||||||
small{color:var(--mute)}
|
small{color:var(--mute)}
|
||||||
</style></head><body><main>
|
</style></head><body><main>
|
||||||
<h1>Sand table: jog & calibrate</h1>
|
<h1>Gyre: jog & calibrate</h1>
|
||||||
<div class="card"><div class="stat">
|
<div class="card"><div class="stat">
|
||||||
<div><span>State</span><b id="state">-</b></div>
|
<div><span>State</span><b id="state">-</b></div>
|
||||||
<div><span>Rho (mm)</span><b id="rho">-</b></div>
|
<div><span>Rho (mm)</span><b id="rho">-</b></div>
|
||||||
|
|||||||
@@ -251,17 +251,17 @@ class Player:
|
|||||||
self.p.homed = False
|
self.p.homed = False
|
||||||
self.message = f'Stopped: {e}. Check the arm, belt and carriage, then play again (it homes first).'
|
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}')
|
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
|
except OSError as e: # the USB cable came out: the panel reconnects
|
||||||
self.p.lost(e)
|
self.p.lost(e)
|
||||||
self.message = 'Stopped: the USB connection to the Uno was lost. Plug it back in; the page reconnects by itself.'
|
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}')
|
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:
|
except GrblError as e:
|
||||||
self.p.homed = False
|
self.p.homed = False
|
||||||
self.message = f'Stopped: {e}'
|
self.message = f'Stopped: {e}'
|
||||||
log('ERROR ' + self.message + f' | status {c.status.state} wpos={c.status.wpos} errors={c.errors[-3:]}')
|
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
|
except Exception as e: # a bug: stop safely and say so
|
||||||
import traceback
|
import traceback
|
||||||
self.p.homed = False
|
self.p.homed = False
|
||||||
@@ -271,7 +271,7 @@ class Player:
|
|||||||
c.stop()
|
c.stop()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self.alert('Sand table stopped', self.message)
|
self.alert('Gyre stopped', self.message)
|
||||||
finally:
|
finally:
|
||||||
c.guard = None
|
c.guard = None
|
||||||
c.abort = False
|
c.abort = False
|
||||||
@@ -324,7 +324,7 @@ class Player:
|
|||||||
# ---------------------------------------------------------------- page
|
# ---------------------------------------------------------------- page
|
||||||
PAGE = r"""<!doctype html>
|
PAGE = r"""<!doctype html>
|
||||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
<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>
|
<style>
|
||||||
:root{--bg:#f4f1ea;--card:#fffdf8;--ink:#2a2622;--mute:#6f675d;--line:#e2dbcf;--acc:#2f6f5e;--acc-ink:#fff;--stop:#c0392b;--bar:#e9e2d4}
|
: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}}
|
@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}}
|
@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>
|
</style></head><body><main>
|
||||||
<header>
|
<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>
|
<span class="pill" id="mach">connecting…</span>
|
||||||
<button id="homeBtn" onclick="post('home')">Home</button>
|
<button id="homeBtn" onclick="post('home')">Home</button>
|
||||||
<button class="stop" onclick="post('stop')">STOP</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)
|
lib = library or Library(cfg)
|
||||||
player = Player(panel, cfg, lib, alert=alert)
|
player = Player(panel, cfg, lib, alert=alert)
|
||||||
threading.Thread(target=lib.warm, daemon=True).start()
|
threading.Thread(target=lib.warm, daemon=True).start()
|
||||||
jog_page = JOG_PAGE.replace('<h1>Sand table: jog & calibrate</h1>',
|
jog_page = JOG_PAGE.replace('<h1>Gyre: jog & calibrate</h1>',
|
||||||
'<p><a href="/">← Library</a></p><h1>Sand table: jog & calibrate</h1>')
|
'<p><a href="/">← Library</a></p><h1>Gyre: jog & calibrate</h1>')
|
||||||
|
|
||||||
def busy():
|
def busy():
|
||||||
if not panel.connected:
|
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 = ThreadingHTTPServer((host, port), H)
|
||||||
srv.daemon_threads = True
|
srv.daemon_threads = True
|
||||||
url = f'http://{"127.0.0.1" if host in ("0.0.0.0", "") else host}:{port}'
|
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', ''):
|
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.')
|
print(' Also open to other devices on your network, with no password: anyone on it can drive the table.')
|
||||||
if open_browser:
|
if open_browser:
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ class TestHttp(unittest.TestCase):
|
|||||||
return json.loads(r.read())
|
return json.loads(r.read())
|
||||||
|
|
||||||
def test_pages_and_play(self):
|
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'))
|
self.assertIn(b'jog & calibrate', self.get('/jog'))
|
||||||
lib = json.loads(self.get('/api/library'))
|
lib = json.loads(self.get('/api/library'))
|
||||||
self.assertEqual(len(lib), 2)
|
self.assertEqual(len(lib), 2)
|
||||||
|
|||||||
Reference in new issue
Block a user