Repository navigation
fix(calibration): avoid concurrent motor bus reads during status polling - #132
Merged
nicolas-rabault merged 1 commit intoOct 9, 2026
Conversation
nicolas-rabault
self-requested a review
October 9, 2026 08:15
nicolas-rabault
approved these changes
Oct 9, 2026
nicolas-rabault
left a comment
Member
There was a problem hiding this comment.
Perfectly executed PR.
Thank you @Sjeongheon .
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #121
Problem
During range-of-motion recording, LeLab could access the same motor bus from two different threads at the same time.
The calibration worker continuously calls:
to record the minimum and maximum position of each joint.
At the same time, the calibration page polls
/calibration-statusevery 200 ms. Before this change,get_status()also calledsync_read("Present_Position").This meant that the recording loop and the HTTP status request could both try to use the same serial bus at the same time.
On an SO-101, the unpatched code reproduced errors from both paths during calibration:
The errors were intermittent, but they occurred repeatedly during calibration, including when the motors were moving very little or not moving.
This matters for calibration because if the recording loop fails to read a position at the moment a joint reaches its physical minimum or maximum, that sample cannot be included in the recorded range.
Root cause
The calibration worker and
get_status()were both readingPresent_Positionfrom the same motor bus.The frontend status request runs separately from the calibration worker, so the two
sync_read()calls could overlap.This matches the failure described in #121, where valid servo replies were observed on the wire but the host-side packet handling failed when two threads used the same serial connection concurrently.
Fix
This change makes the calibration worker the only code path that reads motor positions during range recording.
The recording loop still reads
Present_Positionand updates_minsand_maxesexactly as before. After a successful read, it also stores the latestmin,max, andcurrentvalues instatus.recorded_ranges.get_status()no longer accesses the motor bus. It only returns the values already recorded by the calibration worker.The cached range data is updated while holding
_status_lock.get_status()also returns a copied status object while holding that lock, so the calibration worker cannot change the nestedrecorded_rangesdictionary while FastAPI is serializing the response.If a motor read started while recording was active but finishes after calibration has already moved to another state, its result is not written back into the status cache.
The frontend API and response format are unchanged.
Calibration finalization
Before this change, the calibration status could still be
"recording"while_complete_calibration()was writing calibration registers. A simultaneous/calibration-statusrequest could therefore start another motor read whilewrite_calibration()was using the same bus.Because
get_status()no longer performs motor I/O, that overlap is also removed.This PR does not change
write_calibration()itself. It does not add register read-back, retries for lost replies, or recovery from partially written calibration values. Those are separate motor-communication concerns in LeRobot rather than part of the LeLab status-polling race fixed here.Related LeRobot work
This PR changes how LeLab uses the motor bus; it does not replace or modify the underlying motor communication implementation.
Those PRs work at different layers. This change only ensures that LeLab's calibration page does not create a second motor-bus reader through status polling.
Testing
Automated tests
tests/test_calibrate.pynow checks that:get_status()does not call the motor bus while calibration is recording;Local checks:
Hardware
Tested with SO-101 leader and follower arms.
With the unpatched version, the packet errors described above were repeatedly reproduced during calibration.
With this change, five consecutive calibration runs completed without observing the corresponding
Incorrect status packet!orThere is no status packet!warnings. The calibration page continued to update the current position and recorded minimum/maximum values normally.I also tested repeated status requests while calibration was recording.
The browser was left open, so the normal UI polling of
/calibration-statuscontinued every 200 ms. At the same time, a separate script sent another 200 requests to the same endpoint at approximately the same interval.All 200 additional requests returned successfully with:
This intentionally created duplicate status polling: the normal UI requests and a second stream of requests from the test script were hitting the same endpoint at the same time.
The timing numbers from this script are not intended to measure the execution time of
get_status()alone, because they also include the local HTTP request and response, FastAPI request handling, JSON serialization, and JSON parsing.The useful result here is that additional status requests continued to return normally while calibration was recording, without causing the extra motor-bus reads and packet warnings seen with the previous implementation.