Add a Sensirion SCD41 CO2 gadget example - #13
Merged
Conversation
The gadget protocol is not published as a spec, and the vendor's Android app is a Flutter build, so there is no Java to decompile. The UUIDs and byte layouts here come from Sensirion's own firmware instead: arduino-ble-gadget for the services, advertisement header and download packets, arduino-upt-core for the sample layouts and the decode functions. The 21-entry layout table is generated from BLEProtocol.cpp rather than transcribed, because a wrong offset in one of those entries decodes as a plausible reading rather than as an error. There are two paths to live values. The gadget broadcasts temperature, humidity and CO2 in its manufacturer-specific advertisement data, so scan and monitor need no connection at all. Connecting and subscribing updates faster, so live does that instead. The download is driven by subscribing to the packet characteristic, which means the subscription is the request and a sample limit has to be written first. The header is identified by its sequence number of zero rather than by arrival order, because subscribing can surface a value cached from an earlier transfer. The gadget has no clock, so timestamps are reconstructed from the host clock and the age the header reports for the newest sample; readout is oldest-first, which fixes the direction. A production MyCO2 diverges from the reference firmware more than its shared service UUIDs suggest. Reading each characteristic's 0x2901 user-description descriptor off the device names them directly, which is how the following were established rather than guessed: It advertises T_RH_CO2_ALT but omits the two trailing reserved bytes, so the sample is shorter than the declared stride. Slots are decoded individually; requiring a full-width sample discarded every value. Requested Samples is 16-bit and rejects a four-byte write with Invalid Length. A limit now falls back to 16 bits, and is skipped entirely when downloading everything, since zero already means everything. The settings service holds LED Brightness and Ping Minion, not the Wi-Fi credentials and alternative device name the arduino build puts there. Renaming goes through GAP Device Name, which is advertised as writable but rejects the write with NotAuthorized on an unencrypted link, so characteristic flags alone do not tell you whether a write will be accepted. Bonding would be required and the device does not accept pairing: this library and bluetoothctl both time out with no SMP response, so the name cannot be changed over BLE at all. The vendor app keeps its own label instead -- its schema carries a shouldFetchNameFromGadget flag, which only makes sense for a local rename -- so the command explains that rather than suggesting a pairing that cannot succeed. 0x2A26 is Firmware Revision, not Manufacturer Name, which is 0x2A29. LED brightness is exposed read-only. The characteristic accepts both a 1-byte and a 4-byte write without error but reads back unchanged either way, so its write encoding is unconfirmed and a setter would silently do nothing. Changing the logging interval erases the gadget's ring buffer, so that command reports how many samples would be lost and requires --yes. Tests round-trip every layout through the firmware's encoding functions and decode captured MyCO2 advertisement and live-value payloads. Forced recalibration is unverified against hardware: it rewrites the sensor's calibration, and doing that against a reference the sensor is not actually sitting in would skew it. Signed-off-by: Joel Winarske <[email protected]>
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.
Adds a Sensirion SCD41 CO2 gadget example: advertisement decoding, live
monitoring, history download with CSV export and terminal plots, logging
interval, device info, and SCD4x forced recalibration.
Two new files under
example/, following the existing flat convention:sensirion_gadget.dartholds the wire protocol,scd41_co2_sensor.dartis theCLI.
test/sensirion_gadget_test.dartcovers the protocol.Where the protocol came from
It is not published as a spec, and the vendor's Android app is a Flutter build,
so there is no Java to decompile. The UUIDs and byte layouts come from
Sensirion's own firmware instead:
arduino-ble-gadgetfor the services,advertisement header and download packets,
arduino-upt-corefor the samplelayouts and decode functions. The 21-entry layout table is generated from
BLEProtocol.cpprather than transcribed, because a wrong offset in one ofthose entries decodes as a plausible reading rather than as an error.
Two paths to live values
The gadget broadcasts temperature, humidity and CO2 in its manufacturer-specific
advertisement data, so
scanandmonitorneed no connection at all.liveconnects and subscribes instead, which updates faster.
History download
Driven by subscribing to the packet characteristic, which means the subscription
is the request and a sample limit has to be written first. The header is
identified by its sequence number of zero rather than by arrival order, because
subscribing can surface a value cached from an earlier transfer. The gadget has
no clock, so timestamps are reconstructed from the host clock and the age the
header reports for the newest sample; readout is oldest-first, which fixes the
direction.
Changing the logging interval erases the gadget's ring buffer, so that command
reports how many samples would be lost and requires
--yes.Verified against a production MyCO2
Its GATT table diverges from the reference firmware more than the shared service
UUIDs suggest. Reading each characteristic's
0x2901user-descriptiondescriptor off the device names them directly, which is how the following were
established rather than guessed:
T_RH_CO2_ALTbut omits the two trailing reserved bytes, so thesample is shorter than the declared stride. Slots are decoded individually;
requiring a full-width sample discarded every value.
Invalid Length. A limit falls back to 16 bits, and is skipped entirely whendownloading everything, since zero already means everything.
credentials and alternative device name the arduino build puts there.
0x2A26is Firmware Revision, not Manufacturer Name, which is0x2A29.Download verified end to end at 25/25 samples, and
--max 10returns exactlythe ten newest, matching the firmware's
startReadOut(_head - n)semantics.Two things deliberately not shipped
LED brightness is read-only. The characteristic accepts both a 1-byte and a
4-byte write without error but reads back unchanged as
ff 00 00 00either way,so its write encoding is unconfirmed and a setter would silently do nothing.
Rename is reported as unavailable rather than attempted. GAP Device Name is
advertised as writable but rejects the write with
NotAuthorizedon anunencrypted link. Bonding would be required, and the device does not accept
pairing: this library and
bluetoothctlboth time out with no SMP response, andthe agent never receives a request. The vendor app keeps its own label instead --
its schema carries a
shouldFetchNameFromGadgetflag, which only makes sensefor a local rename.
Forced recalibration is unverified against hardware. It rewrites the
sensor's calibration, and doing that against a reference the sensor is not
actually sitting in would skew it. Argument validation and service detection are
tested; the write itself is not.
Testing
dart analyzeclean,dart formatclean, 36 tests passing. Protocol testsround-trip every layout through the firmware's encoding functions and decode
captured MyCO2 advertisement and live-value payloads.
Noted separately
The existing
example/pair_device.dartnever registers an agent, despite theREADME describing it as driving the pairing agent, so it times out against any
device that needs one; it also pairs before connecting, which fails with
Page Timeouton a cold LE device. Not touched here.