From 69442ca0dcd59cc09a916afac2d11c853bcd07d8 Mon Sep 17 00:00:00 2001 From: Mark Nolan Date: Mon, 21 Sep 2026 09:52:58 +0100 Subject: [PATCH] DEV-982: handle NACK from a Shimmer3/3R instead of timing out The protocol has had a NACK (0xFE) reply for a long time; this API never learned about it. A 0xFE fell through to the switch default in both branches of ReadData(): while streaming that logged "Misaligned ByteStream Detected" and discarded the packet in progress, and otherwise it was dropped silently. Either way the reply the caller was waiting for never arrived, ReadByte() carried on raising TimeoutException, and once StreamTimeOutCount passed 10 the API raised "Connection lost" and disconnected - about ten seconds after a refusal the device had already reported cleanly. A refused command was indistinguishable from a dead link, and the log actively misdirected. This is the same defect as Shimmer-Java-Android-API#293, found by checking the sibling implementations of the wire format after that one. The timing and the mechanism differ - Java tore the link down after two seconds via its ACK timer - but the cause is identical: only ACK was tested for. The firmware refuses more than it used to. Every command except SET_SD_SYNC_COMMAND and ACK is refused while SD sync is enabled (ShimBt_isCmdAllowedWhileSdSyncing), which includes the whole connect sequence, so a sync-enabled Shimmer cannot be connected to at all. Also any SET while sensing, a sync-mode mismatch, an out-of-range InfoMem or calibration write, and several commands the dispatcher accepts but that were never implemented. ProcessNackFromCommand() is protected virtual so an application can surface the refusal; by default it logs and raises a notification event. It clears mWaitingForStartStreamingACK, which a refused START_STREAMING would otherwise leave set for the rest of the session, and consumes the CRC bytes the firmware appends to the NACK packet when CRC mode is on - those would otherwise be read as the next packet header. None of the three SDBT_switch overrides looked at 0xFE, so adding an explicit case ahead of the default changes nothing else. ShimmerCaptureXamarin carries a second, diverged copy of ShimmerBluetooth.cs with the same defect. It is deliberately left alone: nothing has touched it since July 2017, neither CI workflow builds it, and Xamarin is end of life, so a change there could not be verified by anything. Worth fixing if that sample is ever revived. ShimmerAPI builds clean; the ten warnings are pre-existing and in files this does not touch. Co-Authored-By: Claude Opus 5 --- ShimmerAPI/ShimmerAPI/ShimmerBluetooth.cs | 52 +++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/ShimmerAPI/ShimmerAPI/ShimmerBluetooth.cs b/ShimmerAPI/ShimmerAPI/ShimmerBluetooth.cs index 55e4bd2..a625c6d 100644 --- a/ShimmerAPI/ShimmerAPI/ShimmerBluetooth.cs +++ b/ShimmerAPI/ShimmerAPI/ShimmerBluetooth.cs @@ -466,6 +466,7 @@ public enum PacketTypeShimmer2 : byte //Note that most packet SET_MAG_SAMPLING_RATE_COMMAND = 0x3A, MAG_SAMPLING_RATE_RESPONSE = 0x3B, GET_MAG_SAMPLING_RATE_COMMAND = 0x3C, + NACK_COMMAND = 0xFE, ACK_COMMAND = 0xFF }; @@ -1238,6 +1239,51 @@ void ConnectTimerElapsed(object sender, ElapsedEventArgs e) TimerConnect.Stop(); } + /// + /// The Shimmer answered a command with NACK (0xFE) rather than ACK: it has + /// refused that command, which is not the same as the link being gone. + /// + /// + /// Before this existed a 0xFE fell through to the switch default. While + /// streaming that logged "Misaligned ByteStream Detected" and discarded the + /// packet in progress; otherwise it was dropped silently. Either way the + /// reply the caller was waiting for never arrived, ReadByte() carried on + /// timing out, and once StreamTimeOutCount passed 10 the API raised + /// "Connection lost" and disconnected - roughly ten seconds after a refusal + /// the device had already reported cleanly. + /// + /// The firmware refuses more than it used to: every command except + /// SET_SD_SYNC_COMMAND and ACK while SD sync is enabled, any SET while the + /// device is sensing, a sync-mode mismatch, an out-of-range InfoMem or + /// calibration write, and several commands it accepts but never implemented. + /// + /// Override to surface the refusal to the application; the notification + /// raised below is the default. + /// + protected virtual void ProcessNackFromCommand() + { + System.Console.WriteLine("NACK Received - the Shimmer refused the command"); + + /* The NACK is the whole response packet, so the CRC bytes the firmware + * appended to it are still in the stream and would otherwise be read as + * the next packet header. */ + if (BluetoothCRCMode != BTCRCMode.OFF) + { + for (int k = 0; k < (int)BluetoothCRCMode; k++) + { + ReadByte(); + } + } + + /* A refused START_STREAMING never reaches SHIMMER_STATE_STREAMING, so + * clear the wait rather than leaving it set for the rest of the session. */ + mWaitingForStartStreamingACK = false; + + CustomEventArgs newEventArgs = new CustomEventArgs( + (int)ShimmerIdentifier.MSG_IDENTIFIER_NOTIFICATION_MESSAGE, "Command refused by the Shimmer"); + OnNewEvent(newEventArgs); + } + public void ReadData() { List buffer = new List(); @@ -1355,6 +1401,9 @@ public void ReadData() buffer.Clear(); } break; + case (byte)PacketTypeShimmer2.NACK_COMMAND: + ProcessNackFromCommand(); + break; case (byte)PacketTypeShimmer2.ACK_COMMAND: //Since the ack always proceeds the instreamcmd if (StreamingACKReceived) @@ -1554,6 +1603,9 @@ public void ReadData() case (byte)PacketTypeShimmer3.INTERNAL_EXP_POWER_ENABLE_RESPONSE: SetInternalExpPower(ReadByte()); break; + case (byte)PacketTypeShimmer2.NACK_COMMAND: + ProcessNackFromCommand(); + break; case (byte)PacketTypeShimmer2.ACK_COMMAND: System.Console.WriteLine("ACK Received"); if (mWaitingForStartStreamingACK)