Conversation
The Shimmer Bluetooth protocol answers a command it will not carry out with a NACK (0xFE) rather than an acknowledgment. The API had no concept of it, so process_single_input_event() fell through to _process_resp_from_queue(), which either raised queue.Empty because no response was pending or raised ValueError on the unexpected response code. Either exception escapes ShimmerBluetooth._run_readloop, which catches only ReadAbort. The reader thread then dies while the device still looks connected, and because RequestCompletion.wait() and RequestResponse.wait() block on an Event with no timeout, that thread's death leaves the caller - and every later caller - blocked for good. A refusal is a normal part of the protocol and says nothing about the health of the link. The firmware refuses every command except the SD sync command and an acknowledgment while SD sync is enabled, which covers the whole connect sequence; it also refuses any set command while the device is sensing, a sync mode mismatch, an out-of-range InfoMem or calibration write, and a handful of commands it accepts but has never implemented. _process_nack() takes the pending entry off the acknowledgment queue and marks it refused rather than moving it to the response queue, since a refused command never produces a response. Waiting callers get a CommandRefused exception instead of blocking, and the reader thread carries on, so a later command still runs. CommandRefused is exported from the package so callers can catch it. has_completed() and has_result() report True after a refusal, because the request is finished either way; was_refused() distinguishes the two, and wait() raises. Two tests cover a refused command and a refusal followed by a command that succeeds. Note that the Bluetooth test module cannot be collected on Windows - pyshimmer.test_util imports termios - so these were written against the existing PTY-based fixtures and verified locally through an equivalent in-memory fake, with the unpatched tree checked first to confirm it does fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Collaborator
|
Wow, thank you for the code. It looks really nice! |
Collaborator
|
And you're right, the test cases use the Linux PTY pseudoterminal package. I don't know if there exists something that's cross-platform, but on Linux it works quite well to emulate a duplex serial connection. mock_creator.write_to_master(b"\xff\xfe") |
This branch has not been deployed
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.
The Shimmer Bluetooth protocol answers a command it will not carry out with a NACK
(
0xFE) instead of an acknowledgment.pyshimmerhas no concept of it, and theconsequence is more serious than a missing feature: one refusal kills the reader
thread and every waiting call blocks forever.
What happens today
process_single_input_event()dispatches on the first byte:A
0xFEreaches theelse, and_process_resp_from_queue()either raisesqueue.Emptyfromget_nowait()when no response is pending, or raisesValueErrorbecause the response code does not match.
Either exception escapes
ShimmerBluetooth._run_readloop, which catches onlyReadAbort. The reader thread dies while the device still looks connected. And sinceRequestCompletion.wait()andRequestResponse.wait()block on anEventwith notimeout, that thread's death leaves the current caller — and every later caller —
blocked indefinitely.
I confirmed the failure against an unpatched tree before writing the fix; it raises
queue.Empty.When the device refuses
A refusal is a normal part of the protocol and says nothing about the health of the
link. The firmware refuses:
enabled — this covers the entire connect sequence, so a sync-enabled device is
unusable rather than merely awkward
What this changes
_process_nack()takes the pending entry off the acknowledgment queue and marks itrefused, rather than moving it to the response queue — a refused command never produces
a response. Waiting callers get a
CommandRefusedexception instead of blocking, andthe reader thread carries on, so a later command still runs.
CommandRefusedis exported from the package so callers can catch it.has_completed()andhas_result()reportTrueafter a refusal, because the requestis finished either way;
was_refused()distinguishes the two, andwait()raises.BluetoothSerial.read_nack()mirrors the existingread_ack().This does change the contract of
wait(), which previously could only return. I thinkthat is the right trade: the behaviour it replaces is an unbounded block after the
reader thread has already died, so any defined outcome is an improvement, and raising
is the natural way to say "the device declined this".
Tests
Two new cases in
TestBluetoothRequestHandler, written against the existingPTY fixtures:
test_enqueue_command_refused— a refused command finishes as refused, carries noresult, and both
wait()calls raise.test_enqueue_refused_then_next_command_still_runs— a refusal followed by a commandthat is acknowledged and answered, so the refusal does not stall the queue.
One caveat worth stating plainly: I could not run these locally. The Bluetooth test
module cannot be collected on Windows, because
pyshimmer.test_utilimportstermios.I verified the same three scenarios — refusal, refusal-then-success, and an ACK path
left unaffected — through an equivalent in-memory fake serial, and checked that the
unpatched tree fails that harness first so it was not vacuously passing. CI here is the
first run of the PTY versions, so please do look at the result.
blackreports no changes.