mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
Milestone 2: factory-true motion tuning, bench-verified
Extract the factory motion profile from the _RESOURCES pulse streams (new puls_profile.py): 700/590 mm/s2 X/Y accel on v2.6.0 firmware, 202 mm/s travel peak at the 28160 Hz travel tick, PIC currents X 135/33 and Y 22/5 run/hold, decay mode 1. Add the bench_m2.py round-trip jog + feed-hold suite (all green 2026-08-02: sustained 200 mm/s, exact returns, clean hold/resume, zero underruns) and update BRINGUP.md: board moved to a fixed lease at 172.16.1.97, analog config now applied by the sink itself, $RST=$ note for stale stored settings.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Milestone-2 motion-quality bench: factory-true rates/accels over TCP.
|
||||
|
||||
Runs a bounded, return-to-start jog sequence against grblHAL on the board
|
||||
(default 172.16.1.97:23) and reports peak feed reached, state transitions,
|
||||
and final position drift. Every move is relative and round-trip, so the
|
||||
head ends where it started; the laser stays latched (motion-only backend).
|
||||
|
||||
Sequence: sanity jogs (X, Y, 40 mm out/back at 2400 mm/min), max-rate X
|
||||
out/back (60 mm at F12000 - peaks ~200 mm/s mid-move), diagonal out/back,
|
||||
then a G1 move with a feed-hold/resume in the middle.
|
||||
"""
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
HOST = sys.argv[1] if len(sys.argv) > 1 else '172.16.1.97'
|
||||
PORT = 23
|
||||
|
||||
|
||||
class Grbl:
|
||||
def __init__(self, host, port):
|
||||
self.s = socket.create_connection((host, port), timeout=5)
|
||||
self.s.settimeout(0.2)
|
||||
self.buf = b''
|
||||
time.sleep(0.5)
|
||||
self.drain()
|
||||
|
||||
def drain(self):
|
||||
try:
|
||||
while True:
|
||||
d = self.s.recv(4096)
|
||||
if not d:
|
||||
break
|
||||
self.buf += d
|
||||
except socket.timeout:
|
||||
pass
|
||||
out, self.buf = self.buf, b''
|
||||
return out.decode('ascii', 'replace')
|
||||
|
||||
def cmd(self, line, timeout=5.0):
|
||||
"""Send a line, wait for ok/error."""
|
||||
self.s.sendall(line.encode() + b'\n')
|
||||
deadline = time.time() + timeout
|
||||
text = ''
|
||||
while time.time() < deadline:
|
||||
text += self.drain()
|
||||
if 'ok' in text or 'error' in text:
|
||||
return text.strip()
|
||||
time.sleep(0.02)
|
||||
return '(timeout) ' + text.strip()
|
||||
|
||||
def status(self):
|
||||
"""Realtime ? query -> raw <...> report or ''."""
|
||||
self.s.sendall(b'?')
|
||||
t = time.time() + 1.0
|
||||
text = ''
|
||||
while time.time() < t:
|
||||
text += self.drain()
|
||||
if '>' in text:
|
||||
break
|
||||
time.sleep(0.02)
|
||||
if '<' in text and '>' in text:
|
||||
return text[text.rfind('<'):text.rfind('>') + 1]
|
||||
return ''
|
||||
|
||||
def wait_idle(self, timeout=30.0, poll=0.05):
|
||||
"""Poll until Idle; return (peak_feed_mm_min, states_seen)."""
|
||||
peak = 0.0
|
||||
states = []
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
st = self.status()
|
||||
if st:
|
||||
state = st[1:].split('|')[0]
|
||||
if not states or states[-1] != state:
|
||||
states.append(state)
|
||||
for f in st.split('|'):
|
||||
if f.startswith('FS:'):
|
||||
peak = max(peak, float(f[3:].split(',')[0]))
|
||||
if state.startswith('Idle'):
|
||||
return peak, states
|
||||
time.sleep(poll)
|
||||
return peak, states + ['TIMEOUT']
|
||||
|
||||
def rt(self, ch):
|
||||
self.s.sendall(ch)
|
||||
|
||||
|
||||
def main():
|
||||
g = Grbl(HOST, PORT)
|
||||
st = g.status()
|
||||
print('connect: %s' % st)
|
||||
if 'Alarm' in st:
|
||||
print('unlock: %s' % g.cmd('$X'))
|
||||
|
||||
for name, out, back, feed in (
|
||||
('X sanity 40mm', '$J=G91X40F2400', '$J=G91X-40F2400', 2400),
|
||||
('Y sanity 40mm', '$J=G91Y40F2400', '$J=G91Y-40F2400', 2400),
|
||||
('X max-rate 60mm', '$J=G91X60F12000', '$J=G91X-60F12000', 12000),
|
||||
('diag 40mm', '$J=G91X40Y40F8000', '$J=G91X-40Y-40F8000', 8000),
|
||||
):
|
||||
for jog in (out, back):
|
||||
r = g.cmd(jog)
|
||||
if 'error' in r:
|
||||
print('%s: JOG REFUSED: %s' % (name, r))
|
||||
return 1
|
||||
peak, states = g.wait_idle()
|
||||
print('%s %s: peak %.0f mm/min, states %s'
|
||||
% (name, 'out' if jog == out else 'back', peak, states))
|
||||
|
||||
# Feed-hold mid-move: G1 at 600 mm/min takes 3 s for 30 mm.
|
||||
print('hold test: %s' % g.cmd('G91'))
|
||||
g.cmd('G1X30F600', timeout=0.5) # ok arrives while moving
|
||||
time.sleep(1.0)
|
||||
g.rt(b'!')
|
||||
time.sleep(0.8)
|
||||
st = g.status()
|
||||
print('after !: %s' % st)
|
||||
held = 'Hold' in st
|
||||
g.rt(b'~')
|
||||
peak, states = g.wait_idle()
|
||||
print('after ~: states %s' % states)
|
||||
g.cmd('G1X-30F2400', timeout=0.5) # return to start
|
||||
g.wait_idle()
|
||||
print('hold worked: %s' % held)
|
||||
|
||||
print('final: %s' % g.status())
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user