Repository navigation
fix(v1): a Feetech arm's calibration is the file Robot(id=) names, recorded as LeRobot records it; inf/nan hz refused; GLFW renders on macOS - #4803
Conversation
…ead of killing the process strands-labs#4789 moved every render to the one 'strands-gl' thread. On macOS GLFW makes its window on the main thread only, so with MUJOCO_GL=glfw the first render, even one asked for on the main thread, ended the process with SIGTRAP: a child that renders a 64x48 camera exits -5, where the CGL default renders (48, 64, 3). On macOS, when MuJoCo's GLContext is its glfw one, a render now runs on the thread that asks. CGL, EGL and OSMesa keep the one GL thread, and MUJOCO_GL=disable still says "rendering unavailable". This is GLFW as it was before strands-labs#4789 (measured at 269d54c): a main- thread render works, a background start() after it renders too, and a first render from a worker thread still dies with SIGTRAP, which is GLFW's rule.
…bration.py, beside the paths it reads FeetechDriver._load_calibration built the list of candidate files, took the first that exists and refused a missing calibration=<path>; calibration_candidates() wrapped lerobot_calibration_candidates for it. That search is LeRobot's file naming, which drivers/calibration.py already holds (lerobot_calibration_path, lerobot_calibration_candidates), so it moves there as the private _find_lerobot_calibration(robot_type, robot_id, path) -> (found, looked), which feetech/driver.py imports the way description.py imports _resolve_asset. FeetechDriver.calibration_candidates(), whose only caller this was, goes. Nothing else changes: the same file loads, a missing path raises the same FileNotFoundError, and calibration_looked lists the same paths. feetech/driver.py goes from 231 to 224 lines, which leaves room under the 250-line rule for the calibration fixes that follow and for strands-labs#4795's read retry in the same file.
…bration file
Robot never handed its id to the driver, so a Feetech arm looked for the type-named file or the only
file in the directory. With my_arm.json and other_arm.json under so_follower/, Robot("so101",
port=, id="my_arm") loaded no calibration: act({"shoulder_pan": 1.0}) wrote Goal_Position 2700 (raw
full-turn counts) where my_arm.json maps it to 2581. With only other_arm.json there, it loaded that
other arm's file (2681). make_driver() now takes a keyword-only robot_id, which Robot(id=) fills, and
passes it to a driver class that takes one (Feetech), and with an id the search never guesses the
only file in a directory. Without id=, nothing changes. Measured on the suite's FakeSerial.
So an id with no file of its own now loads none, even where dev took the only file there: beside
only LeRobot's tutorial so_follower/my_awesome_follower_arm.json, Robot(id="left") sends
shoulder_pan's lower limit as 796 raw counts, where that file puts it at 1024. The search
(_find_lerobot_calibration) now warns then, naming the files in the directories it searched and
suggesting calibration=<path> or the id LeRobot calibrated the arm with. With nothing on disk it says
nothing, as before.
…cord over a file it only guessed calibrate() always saved to default_calibration_path() (so101_follower/so101_follower.json), while the servos held the new homing offsets. On the suite's FakeSerial, with so_follower/my_arm.json loaded for id "my_arm": calibrate() wrote homing 301 to the servos and saved so101_follower/so101_follower.json; the next session loaded my_arm.json again (homing 0, range 1024..3072), and shoulder_pan's lower limit went to encoder 1325, where the new calibration puts it at 2148. calibrate() now saves over the file it loaded (named for the id, or given as calibration=<path>), else to <lerobot_type>/<id>.json, and the next session's lower limit is encoder 2148. Without an id the search also loads the only *.json in a directory, which may be another arm's. A save over that file would give the other arm this one's calibration, and a save beside it, to the type-named file a connect without an id reads first, would hide it from every such connect. So for a guessed file _find_lerobot_calibration returns no save target, and calibrate(interactive=True, save=True) raises RuntimeError before torque goes off, naming the file and asking for id= or calibration=. With only arm A's so_follower/arm_a.json on disk, arm B connected without an id loads it; B's calibrate() now sends no bus frame and leaves arm_a.json the only file, and arm A, with or without its id, still sends its lower limit to encoder 1024. On dev, B's next connect read arm_a.json against B's new offsets (1325). Connected as id "my_arm", the arm whose file that was saves over it, and the next connect reads 2148 with or without the id. Acceptance check 9 reads the file the driver loaded (calibration_source), not the save path: with LeRobot's so_follower/my_arm.json it skipped, and now it passes on the fake bus.
…g only after the middle pose Interactive calibrate() turned torque off and zeroed every servo's Homing_Offset before its first prompt, and replaced an arm's calibration without asking. On the suite's FakeSerial, with so_follower/my_arm.json loaded (offsets 120): before the first answer, a Torque_Enable=0 frame and 6 Homing_Offset writes; Enter recorded a new calibration; Ctrl-C at that prompt left the servos at offset 0 while the file says 120. The pinned LeRobot asks first when a calibration exists (so_follower.py:139-147 at 53feaef3) and zeroes homing after the middle-pose Enter (:156-157). Now, after strands-labs#4789's no-terminal guard, a loaded calibration is offered first ("Enter keeps it, c re-records"): Enter returns it with nothing written to the bus or the file, where LeRobot's Enter writes the kept calibration to the servos (:146). Torque goes off only after c, and the offsets are zeroed only after the middle pose is confirmed, so a Ctrl-C at either prompt leaves the servos holding what the file says (120). With save=True, a file loaded only as a guess is refused after c, still before torque goes off. A Ctrl-C during the sweep, after the new offsets are written, still leaves them in the servos beside the old file, as on dev.
… instead of sweeping it LeRobot's so_follower never sweeps wrist_roll: it asks the operator to move every joint except wrist_roll, and records 0..4095 for it (so_follower.py:160-168 at the pinned 53feaef3). calibrate() swept every joint. On the suite's FakeSerial, a sweep that left wrist_roll still, as LeRobot asks, recorded 2047..2047 for it: the next read() divided by zero, and wrist_roll's upper limit went out as goal 2047. A sweep that turned it 200 counts each way recorded 1847..2247, and that limit went out as 2247. Now that calibrate() saves over the file it loaded, either range would also replace the 0..4095 in LeRobot's own so_follower/my_arm.json. calibrate() now leaves wrist_roll out of the sweep and gives it 0..4095, and its prompt says so: both sweeps record 0..4095, read() is finite, and the upper limit goes out as 4095. The homing offset is still set at the middle pose for every joint, as LeRobot's set_half_turn_homings() does. wrist_roll is named in the driver, as LeRobot names it; so100 and so101, the registry's two feetech arms, both have one. tests/test_drivers.py gains SweepSerial, a fake bus on which every joint but the still ones moves 200 counts each way.
…nge, and puts back the servos' offsets calibrate() kept whatever the sweep recorded. A joint the operator never moved came out with range_min == range_max, and a read() through that calibration divides by zero. On the suite's FakeSerial, with so_follower/my_arm.json loaded for id "my_arm" and a sweep in which the gripper never moves, calibrate() returned a gripper range of 2047..2047 and the same session's next read() raised ZeroDivisionError; now that the save goes over the file it loaded, the next connect would read that range too. A sweep with no reading at all (sweep_seconds=0) recorded range_min 4096 and range_max 0, and shoulder_pan's lower limit then went out as goal 4096 and its upper limit as 0. LeRobot refuses such a sweep of the joints it sweeps, every one but wrist_roll (motors_bus.py:848-850 at the pinned 53feaef3, "Some motors have the same min and max values"). calibrate() now raises ValueError naming those joints before it builds the calibration: the file and the driver's calibration stay as they were (gripper 1024..3072), and the next read() is finite. wrist_roll, which the previous commit gives the full turn, is never among them. By then the servos hold the new homing offsets, while the driver keeps the calibration it loaded. With the new offsets left there, shoulder_pan's upper limit went to encoder 3373, 301 counts past my_arm.json's stop at 3072 (homing 0, range 1024..3072), in the same session and at the next connect (measured with the write-back below taken out). LeRobot's refusal leaves its new offsets too, but set_half_turn_homings() has emptied the bus calibration first (reset_calibration(), motors_bus.py:791 and :773), so no normalized read or write goes out after it (:855-856, :884-885), and its next connect sees the mismatch and asks again (feetech.py:228-245, so_follower.py:122-126). So calibrate() reads the servos' offsets just before it zeroes them, 6 more READ frames, and writes them back before it raises, 18 more WRITE frames: the servos then hold what they held before (0 with that my_arm.json), and the upper limit lands on encoder 3072 in the same session and at the next connect. A Ctrl-C during the sweep still leaves the new offsets, as on dev. The new test loads a file recorded at offset 120 onto servos holding 120 and checks the file, the driver's calibration and the servos' offsets after the refusal. The homing clamp's limit variable is folded into its line: feetech/driver.py is 239 lines, and 250 with strands-labs#4795's read retry merged. tests/test_drivers.py's calibrate test now sweeps on SweepSerial, 200 counts each way, instead of holding still, and pins the range that records (1847..2247).
…ouches the bus
run() and the state stream checked hz <= 0; inf and nan pass both, and json.loads returns both for
Infinity and NaN. At hz=inf a tick lasts 0 s and at nan every wait is max(0.0, nan) = 0.0, so
nothing sleeps. On the suite's FakeSerial, on a shared machine, a hardware run(hz=inf, steps=50)
wrote its 51 goal and torque frames in 0.011 s (hz=50: 1.006 s), and subscribe("state", hz=inf)
read the bus 3621 times in 0.2 s (hz=50: 10). Both now refuse anything but a finite number > 0:
run(hz=) before bind, so a bad hz loads no checkpoint, and the stream before its thread starts.
The loop's _rate() keeps its own check: it only sees an hz run() has checked, or a rate the policy
declares, which bind() already refuses unless it is finite and > 0 (strands-labs#4786).
…que on write() turned torque on first and only then built the goals, so the rad_to_counts check strands-labs#4789 added refused a NaN after Torque_Enable=1 had gone to all six servos: on the suite's FakeSerial, d.write([nan, 0, 0, 0, 0, 0]) raised "non-finite goal nan" with one torque-enable SYNC_WRITE sent and torque left on. Robot.act never gets here with a NaN (coerce refuses it); a direct body.write does. The goals are now built first: the same call writes nothing and torque stays off, and so does a target vector of the wrong length (zip's ValueError, also after the torque write before).
yinsong1986
left a comment
There was a problem hiding this comment.
Summary
Four fixes to the v1 Feetech/run surface, each with the fault reproduced on a fake bus first. Robot(id=) now reaches FeetechDriver as robot_id (via make_driver, only for a driver class whose signature takes it), so it names so_follower/<id>.json and never loads another arm's file; with an id and no file of its own it loads none and warns naming the files present. calibrate() saves over the file it loaded (else <type>/<id>.json), refuses with save=True to record over or beside a file it only guessed (no id, the one *.json in the directory), asks before torque goes off when a calibration is loaded, zeroes homing only after the middle pose is confirmed, gives wrist_roll the full turn as LeRobot does, and refuses a sweep that leaves a joint with lo >= hi - restoring the servos' prior homing offsets and leaving the loaded calibration in place, where dev wrote a 2047..2047 range that made read() divide by zero. run(hz=) and subscribe("state", hz=) reject inf/nan with not 0 < hz < inf, and FeetechDriver.write() builds the goal words before enabling torque so a NaN or wrong-length target leaves torque as it was. on_gl_thread runs inline when MUJOCO_GL=glfw on Darwin, which restores the pre-#4789 behaviour for the one platform where the dedicated GL thread trapped. Public signatures are only widened (make_driver(robot_id=)), the on-disk file format is unchanged, and the one behaviour change that can bite an existing caller (id= now selects the calibration file) is surfaced by a warning that names the fix rather than silently.
What's good
- Every refusal is ordered before the side effect it guards: the guessed-file check precedes
torque(False), the hz check precedesbind(), the NaN check precedesTorque_Enable, and the homing read-back happens before the first homing write. tests/test_feetech_calibration.pyasserts on bus frames and files, not driver internals, and the PR body records the red run of the new tests against dev.- The Darwin-only GLFW branch is pinned by a platform-agnostic routing test plus a
MUJOCO_GL=disablechild, so the macOS behaviour is graded on Linux CI without a GLFW window.
Verification suggestions
MUJOCO_GL=egl pytest -q tests/test_feetech_calibration.py tests/test_non_finite.py tests/test_drivers.py tests/test_render_thread.py(76 pass here; the rest oftests/is green apart from the two pre-existingtests/recordingchild-process SIGSEGVs the PR body already lists).- On a Mac with glfw installed, the
e_gl.shprobe from the PR body should printexit 0, printed '(48, 64, 3)'forMUJOCO_GL=glfw; dev printsexit 133. - The author's open question (should a fresh
calibrate()save toso_follower/<id>.jsonas LeRobot 0.6 does, rather thanso101_follower/<id>.json) is a maintainer call; the current choice keeps dev's path and is covered by thenone-loadedtest case.
Fix to parts 2, 3, 5 and 5.5 of #4749 · roadmap #3818
In one paragraph
On macOS,
MUJOCO_GL=glfwrenders on #4789'sstrands-glended the process (SIGTRAP; GLFW windows need the main thread); now they stay on the caller's thread.Robot(id=)never reached the Feetech driver; now it names the LeRobot file; an id without one loads none, warning beside other files.calibrate()saved where the next connect does not look; now it saves over the file it loaded, but withsave=True(the default) records nothing over a guess (no id, the only file there, maybe another arm's). As in LeRobot, it asks before torque goes off, zeroes offsets after the middle pose, giveswrist_rollthe full turn, and refuses a sweep leaving a joint without a range; the refusal puts the servos' offsets back. Athz=infornan,run()andsubscribe("state")ran unpaced;FeetechDriver.write()turned torque on before refusing a NaN. Each now refuses first.Measured
Fake Feetech bus; dev cc15d9a.
Robot(id="my_arm")beside another arm's file:shoulder_pan1.0 rad; next connect aftercalibrate(), lower limit (e_calibration.py)Robot(id="left")beside only LeRobot's tutorial file:shoulder_pan's lower limit (e_calibration.py)calibrate()on arm A's file (e_calibration.py)calibrate(), a file loaded (e_calibration.py)wrist_rollstill; turned (e_calibration.py)read();shoulder_pan's upper limit (stop 3072), same session; next connect (e_calibration.py)ValueError, nothing saved, offsets back: finite; 3072; 3072e_no_sweep.py)ValueError, offsets backrun(hz=inf);subscribe("state", hz=inf); NaN toFeetechDriver.write()(e_rates_write.py)ValueError, nothing sentNot in this PR
GLFW off the main thread still traps. A Ctrl-C after the middle pose or a failed save leaves new offsets under the old file, 301 counts off at the next connect, which, unlike LeRobot's, never compares them (
e_sweep.py,k_failed_save.py).wrist_roll's 0..4095, as in LeRobot's files, maps onto the description's ±2.79 rad: the real wrist turns 65° where the twin shows 57°, and 180° at its limit (k_wrist_roll_map.py,k_wrist_twin.py).Simulation.run(hz=)checks no rate.Questions
Should a fresh
calibrate()save toso_follower/<id>.json, and Enter write the kept file to the servos, as LeRobot 0.6 does?Evidence
macOS 26.7.1, Python 3.12.10, MuJoCo 3.16.0, LeRobot at the pinned 53feaef3. "dev" is cc15d9a's
strands_robots/andtests/fromgit archive, run from that root; each script runs from the repo root unlessit takes the root as an argument.
HF_LEROBOT_CALIBRATIONalways points at a temp dir. The machine was shared,so timings are only orders of magnitude.
GLFW.
e_gl.sh, the render in a child process perMUJOCO_GL(133 is 128 + SIGTRAP):The other callers of the GL thread,
python run_child.py glfw python gl_child.py <root> <mode>, which starts thechild with
PYTHONPATHunset and a 60 s cap; the table shows the child'sreturncodefrom Python'ssubprocess(-5: SIGTRAP):
main: a main-thread render(48, 64, 3),GLContextfrommujoco.glfwtask: that, then a backgroundstart()donein 3 stepsfirst-off-main: a first render on a worker threadscene-off-main: a main-thread render, then a worker adds a robotWith
MUJOCO_GLunset (CGL),scene-off-mainreturns 0 on all three, its second main-thread render(48, 64, 3).Where
scene-off-maintraps on this PR:backend.load()(backend.py:92) clears the renderers throughon_gl_thread, which runs inline on the worker that added the robot, andRenderer.__del__reachesglfw.destroy_windowthere.MUJOCO_GL=glfw python gl_trap_stack.py <root>, paths shortened:No GLFW child is in the suite: when this Mac's session went idle on 2026-10-18 (
pmset -g assertions:UserIsActive 0), a plainglfw.init()hung and such a test timed out on every commit. The routing test pins thedecision on every platform, and a
MUJOCO_GL=disablechild pins "rendering unavailable".Calibration.
e_calibration.py:This PR's question reads
so101 has a calibration (<root>/robots/so_follower/my_arm.json). Enter keeps it, c re-records (torque goes off). In thestill gripperlinesmy_arm.json's stops are encoder 1024 and 3072:dev's next connect maps that file onto the new offsets (3373); this PR's refusal puts the old offsets back
(
servos hold 0), and both sessions send the upper limit to 3072.In the
Robot(id='left')lines LeRobot's tutorial file,my_awesome_follower_arm.json, is the only one there,with
shoulder_pan's stops at 1024 and 3072 (homing 0). Dev never passes the id, so it loads that file and sendsthe lower limit, -1.92 rad, as goal 1024. This PR's id names
left.json, which is not there: it loads none, warns,and maps the full turn (2048 is 0 rad), so the same limit goes out as goal 796, 228 counts (20°) past that file's
stop. The warning names the file and asks for
calibration=<path>or the id LeRobot calibrated the arm with. Thefake bus enforces no position limit: what a servo holding LeRobot's
Min_Position_Limit(itswrite_calibration(),feetech.py:268-273) does with goal 796 was not measured.A sweep with no reading,
e_no_sweep.py:A Ctrl-C after the middle pose, at the sweep's prompt and inside the sweep,
e_sweep.py(python e_sweep.pyondev, which asks no question;
python e_sweep.py chere):A save that fails after a full sweep (
my_arm.jsonmade read-only),python k_failed_save.py c '' '':LeRobot at the pinned 53feaef3, a code reading of
robots/so_follower/so_follower.py,motors/feetech/feetech.pyand
motors/motors_bus.py:calibrate()asks first when a calibration exists (so_follower.py:139-147), andEnter writes the kept one to the servos (
:146,write_calibration():Homing_Offset,Min_Position_LimitandMax_Position_Limit,feetech.py:268-273); homing is set only after the middle-pose Enter, for every motor(
:156-157,set_half_turn_homings(), which first callsreset_calibration(),motors_bus.py:791; that emptiesthe bus calibration,
:773, so after a refused sweep every normalized read or write raises "has no calibrationregistered",
:855-856and:884-885);wrist_rollis left out of the sweep and recorded as 0..4095(
so_follower.py:160-168);record_ranges_of_motion()raisesValueError("Some motors have the same min and max values: ...")for themotors it sweeps (
motors_bus.py:848-850); andconnect()runscalibrate()again (so_follower.py:122-126)when
is_calibratedis false, that is when the servos' offsets or ranges differ from the file's, or no fileloaded (
feetech.py:228-245).Acceptance check 9 on the fake bus,
zsh acceptance_calibrate.sh <root> <scratch>, which writes a LeRobot-formatrobots/so_follower/my_arm.jsonand runs the calibrate test with this plugin beside it:wrist_roll's full turn. LeRobot recordswrist_rollas 0..4095 (so_follower.py:167-168), and so does thisPR's
calibrate(); the driver maps that range onto the description's limits, ±2.7925 rad. After this PR'scalibrate()with the middle pose at encoder 2348 (homing 301),python k_wrist_roll_map.py:Goal 2781 is 734 counts, 64.5°, from the middle (2047), where the description's angle is 57.3°; at the limit the
goal is 4095, 180° from the middle, where the description says 160°. At 2.4 rad the fake bus, which models
Present = Actual - Homing mod 4096, puts the encoder at 12, -205° from the middle pose, the other way around; what a
real STS3215 does with a goal past 4095 minus its offset was not measured. A file LeRobot made maps the same way on
dev and on this PR, and the twin shows the description's angle:
python k_wrist_twin.py, a realRobot(id="my_arm")with its twin over a LeRobot-shapedso_follower/my_arm.json:Dev's own
calibrate()recorded the swept range instead (2047..2047 or 1847..2247 in thewrist_rolllinesabove).
Rates and the driver's write.
e_rates_write.py:python -c "import json; print(json.loads('{\"hz\": Infinity, \"h2\": NaN}'))"prints{'hz': inf, 'h2': nan}.Simulation.run, whose lock-step loop this PR does not touch,python lockstep_hz.py <root>:Tests and checks (this PR on dev cc15d9a).
pytest -rf tests/test_render_thread.py tests/test_feetech_calibration.py tests/test_non_finite.py tests/test_drivers.py::test_calibrate_writes_lerobot_json, this PR'stests/against dev'sstrands_robots/:25 failed, 10 passed, each failure for its reason: the GLFW render went to
strands-gl; an id loadedNone(2)or another arm's file (2); the save went to
so101_follower/so101_follower.json(4); a guessed file wasrecorded instead of refused (
DID NOT RAISE RuntimeError); dev re-recorded instead of asking; homing ended[0]*6,[1]*6and[0]*6instead of 120;wrist_rollrecorded 2047..2047 and 1847..2247 instead of0..4095;
DID NOT RAISE ValueError(2) and (4, the rates); the Torque_EnableSYNC_WRITEwent out (3). The 10that pass: the existing render test, five non-GLFW routing cases, the
disablechild, the no-id andnothing-on-disk cases, and
test_calibrate_writes_lerobot_json, which now sweeps 200 counts each way. This PR:35 passed. On this PR's package without the offset write-back, both refusal cases fail with
assert [1, 1, 1, 1, 1, 1] == [120, 120, 120, 120, 120, 120].test_layout,test_drivers,test_run,test_device,test_operator_gate,test_render_thread,test_public_surfaceandtest_device_safety(132, 132, 138, 143,147, 149, 151, 155 and 158 passed).
ruff checkandruff format --check(strands_robots tests tests_integ):All checks passed!,190 files already formatted;mypy strands_robots:Success: no issues found in 123 source files;lint-imports:Contracts: 8 kept, 0 broken.pytest -q -p no:cacheprovider --timeout=300from the root,MUJOCO_GLunset: 678 passed, 32 skipped in 166 s;every skip is an environment one (LeRobot not in this venv 10, Isaac 16, Newton 5, the opt-in live model 1).
tests/policy,test_policy_factory,test_units_lerobot_modes,test_units_lerobot_calibrated,in the LeRobot venv): 34 passed.
tests/recordingthere: 2 failed, 30 passed on dev and on this PR, the same two(
test_a_top_level_script_records_two_cameras,test_a_second_run_in_a_new_process_resumes_the_same_directory:assert -11 == 0, their child process ends on signal 11).9999-calibration-recorded-as-lerobot-records-it.mdwith
python scripts/assemble_changelog.py --check --fragment-dir <a dir holding only it>: one problem,9999 is a placeholder, none in its heading or body; on dev itself--checkfails on fix(v1): a faulted run puts the body in its declared on_disconnect state #4793's0000-fragment(
0 is a placeholder).feetech/driver.py231 → 239 lines,drivers/calibration.py59 → 88,drivers/driver.py103 → 107,_gl.py68 → 78;robot.py(248),run/loop.py(202) anddevice/events.py(93) keep theirs. Package+90 / -39, tests +335 / -8 (with
tests_integ/), docs +3 / -3.git merge-tree --write-treeof this PR with each open head at 2026-10-18 21:03Z, thentests/test_layout.pyonthe merged tree (
git archive): clean, and 4 passed, with fix(v1): while the e-stop is latched, a faulted run, a watchdog lapse and hold() write nothing to the body #4794 fe59b12, fix(v1): a corrupted feetech status packet is read again, not the end of a run or an e-stop's hold #4795 0d37932, fix(v1): a tag on dev publishes 1.0.0rc1, and only the version the package reports #4796 2805bfa, fix(v1): one read and one write per tick, no burst after a stall, one clock, one step #479706e97cf, fix(v1): a wrist camera, per-body randomization, Perturb, one rebuild per edit block #4790 b4b0da9, feat(v1): AWS IoT Core natively: shadows, rules, X.509 and claim, an rpc path per link #4784 73f8102, fix(v1): an agent's call reaches the robot it names, and its run is the robot's one task #4798 b72cb68, fix(v1): a failed on_disconnect is retried, and the watchdog thread survives it #4799 1e0a3a3 and fix(v1): a twin never pushes a simulated arm or mirrors a NaN or huge reading; close() waits for the step in progress #4800 14b2bb1. No
strands_robotsfile goes over 250 lines;
feetech/driver.pyis 250 merged with fix(v1): a corrupted feetech status packet is read again, not the end of a run or an e-stop's hold #4795, else 239. On those nine treestest_drivers,test_feetech_calibration,test_non_finite,test_device_safety,test_operator_gate,test_render_thread,test_runandtest_devicegive 153, 152, 149, 149, 149, 149, 150, 164 and 149 passed, 149 on this PR alone and156 merged with fix(v1): while the e-stop is latched, a faulted run, a watchdog lapse and hold() write nothing to the body #4794, fix(v1): a corrupted feetech status packet is read again, not the end of a run or an e-stop's hold #4795 and fix(v1): a tag on dev publishes 1.0.0rc1, and only the version the package reports #4796 in sequence (
feetech/driver.py250 there too). Once, on an earlierversion of this branch at a load average near 66, one test failed on that three-way tree, not captured; that
failure did not recur.
_gl.pyrewriteson_gl_threadaround a_GLThread, against this PR's GLFW change (2 hunks), plus 1 hunk intest_render_thread.py; v1: the acceptance suite, the 1.0 definition #4780 6a87744 andv1 part 12: the docs #4781 f70a53a conflict in the same two files, v1 part 12: the docs #4781 also in
docs/hardware.mdanddocs/simulation.md(1 each).Refs #3818