TinyML Arduino Serial Tests: Separate Data Transfer Completion from Inference Success
The board acknowledges every serial input chunk, but the automated TinyML test still fails or times out. Transfer completion and inference acceptance are separate stages. Treating an acknowledgment as a prediction result hides whether the problem is framing, application input handling or the final output matcher.
The protocol examined here belongs to the pinned, archived TensorFlow Arduino examples. It is a development test interface, not a general production transport or a current recommendation to deploy the archived library.
Establish test mode and the negotiated chunk size
The protocol guide requires newline-terminated commands, data lines and responses. !TEST enters test mode; its response reports the maximum binary bytes that may be encoded into each base64 line.
That limit applies to decoded data. Base64 text is longer, so do not use the reported number as an arbitrary text-line limit. Respect the type and line sizing used by the receiver, and keep diagnostic output distinguishable from protocol responses. In test mode the module consumes serial input, so another command interpreter cannot assume it owns those same bytes.
Convert between wire bytes and callback elements
The receiver implementation parses !DATA length in bytes, then converts its internal total to data-type elements. For signed 16-bit audio, one element occupies two bytes. InputBuffer.length, offset and total consequently describe samples at the callback boundary.
For example, 16000 int16 samples require a declared length of 32000 bytes. A 96-byte decoded chunk contains 48 samples: the first callback offset is zero and the next is 48. The corresponding !DATA_ACK value is 96, the decoded byte count for that chunk, not the cumulative transfer total.
Validate total and chunk lengths against the selected element size on the sender. The examined parser checks each decoded chunk's divisibility, but its command-total conversion divides without a matching divisibility rejection. Do not depend on it to diagnose every malformed declaration cleanly.

Preserve the callback’s actual completion contract
The callback API provides a typed data pointer and progress fields. The receiver decodes into a local buffer before invoking the callback. Consume or copy that chunk during the call; retaining its pointer for later inference is not a valid ownership transfer.
A callback returning false aborts the transfer. With a successful handler, the receiver acknowledges the chunk and emits !OK DATA once the declared element total is reached. That success describes the transfer path, not the model's label or score. Also ensure the application supplies its handler when receiving data; transport bookkeeping is not proof that a model input was populated correctly.
Read the host runner’s final verdict separately
The host script first checks chunk acknowledgments and the final data response. Only afterward does it wait for the configured inference-output pattern and evaluate the configured expression.
Its matcher uses a full-line regular-expression match. Adding a log prefix or changing the score format can therefore produce an inference-result timeout even when the model ran. Inspect the raw completed line before increasing a timeout or changing the model. Conversely, a matching line can still fail the numerical or label expression.
Keep three outcomes explicit: transfer rejected, expected inference output absent, and output present but assertion false. The script's expression evaluator uses Python eval; treat configuration as trusted code, despite its helper's reassuring name. Do not load arbitrary third-party test expressions.
A useful regression fixture exercises a valid transfer, a wrong data type, a rejected callback, a malformed chunk and a deliberately failing result assertion. These cases test different boundaries. None alone validates live sensor acquisition, and no serial test or inference was executed for this article.