Acoustic Readout¶
Hekatron Genius Plus X smoke detectors expose their internal diagnostic snapshot via an acoustic signal that is emitted on demand. The signal is centered around 4.45 kHz and carries a complete dump of the device's identity, lifetime statistics, fault flags, and (if a radio module is fitted) the radio module's identity, configuration, and link status. Hekatron markets this mechanism as SmartSonic. This page documents the modulation, framing, and payload format as reverse-engineered from the official Android service app (the SmartSonic Tuner package) and re-implemented in TypeScript at interface/src/lib/audio/tuner-pipeline.ts.
Disclaimer
The information here is the result of reverse-engineering and observation. It is internally consistent and matches every test vector the project has captured, but it has not been validated against any official specification. Field meanings, especially within bitmasks, may be incomplete or partially mislabelled. Use at your own risk.
Signal Characteristics¶
| Parameter | Value |
|---|---|
| Carrier (NCO center) frequency | 4,449.1 Hz |
| Modulation | Continuous-Phase FSK (CPFSK), space/mark = ±250 Hz |
| Bit rate | ~ 84 bit/s (≈ 11.92 ms / bit) |
| Required ADC sample rate | ≥ 11 kHz; 44.1 kHz used by the reference decoder |
| Forward error correction | Hamming(8,4) per nibble (one-bit correction per 4 data bits) |
| Frame check | CRC-16/IBM (poly 0x8005, init 0xFFFF) over payload |
| Frame structure | [length:1][payload:0..39][crc:2 LE] |
| Typical session length | ~7–9 s (≈0.29 s sync preamble + ~6.1 s payload at 84 bit/s + silence padding) |
The signal lives entirely in the audible band — there is no actual ultrasound — which is why a phone, laptop, or tablet microphone is sufficient to capture it.
Demodulation Pipeline¶
The reference decoder is implemented as a streaming TypeScript pipeline that runs in the browser. Each new audio sample passes through the chain below; the chain is pure DSP and does not use an FFT.
| # | Stage | Input → Output |
|---|---|---|
| 1 | NCO Heterodyne | 44.1 kHz PCM → complex I/Q, baseband ±250 Hz |
| 2 | IIR Band-Pass | I/Q → noise-filtered I/Q (2 cascaded biquad stages) |
| 3 | Decimate ×8 | 44.1 kHz I/Q → 5,512.5 Hz I/Q |
| 4 | Phase Discriminator | I/Q → instantaneous frequency error (arctan-derivative) |
| 5 | Median + IIR Smoothing | Noisy frequency error → smoothed frequency error |
| 6 | Sync Correlator | Smoothed signal → sync-locked / searching (SYNC1 or SYNC2 pattern) |
| 7 | Bit Correlator | Locked stream → raw bits (64-sample template, dual-polarity) |
| 8 | Hamming(8,4) Decode | 8-bit codewords → data nibbles (1-bit error correction per nibble) |
| 9 | Frame Assembler | Decoded bytes → [length][payload][CRC-16/IBM] |
| 10 | Payload Parser | Frame bytes → TunerData fields |
1. Heterodyning¶
Each input sample is mixed with a digitally-generated complex carrier at 4449.1 Hz, producing a baseband I/Q pair around DC. This is what shifts the FSK deviation from [4199.1 Hz, 4699.1 Hz] to [−250 Hz, +250 Hz].
2. IIR Band-Pass + Decimation¶
Two cascaded biquad-style IIR stages remove out-of-band noise and DC. After filtering, the I/Q stream is decimated by 8, giving an effective working rate of 5,512.5 Hz. The filter coefficients are hard-coded (IIR_A1/B1, IIR_A2/B2, IIR_A_SMOOTH/B_SMOOTH in tuner-pipeline.ts).
3. Phase Discriminator¶
An arctan-derivative discriminator turns I/Q into an instantaneous frequency error:
phaseErr = (ΔQ · I_prev − ΔI · Q_prev) / (I² + Q²)
The output is clamped to ±400 (the CLAMP constant) and median-filtered (window 5) before being correlated against the bit and sync templates.
4. Sync Detection¶
Two alternating sync preambles are searched for in parallel:
- SYNC1 — 1,576 decimated samples, segment-encoded in
buildSyncPattern1() - SYNC2 — 1,578 decimated samples, segment-encoded in
buildSyncPattern2()
A correlation magnitude exceeding SYNC_THRESHOLD = 300,000 (and exceeding the opposite-polarity correlation) declares lock. The decoder reports a sync quality percentage:
quality = round(100 · maxCorr / (CLAMP · sync_length))
Sub-sync sequences (SubSync1g/h/i, SubSync2g/h/i) provide fine timing alignment within each pattern set.
5. Bit Recovery¶
After sync, the decoder slides a 64-sample bit template
[ 0×3, −1×26, 0×6, +1×26, 0×3 ]
across the discriminator output. The template is correlated against both polarities; the higher-magnitude polarity wins, yielding one bit. Symbol timing is refined by accumulating 16 consecutive bit correlations and taking a weighted median of their offsets.
6. Hamming(8,4) Byte Decoding¶
Each transmitted byte is two Hamming(8,4) codewords (low nibble first, then high nibble). The codebook is the 16-entry CODEBOOK = [0, 135, 153, 30, 170, 45, 51, 180, 75, 204, 210, 85, 225, 102, 120, 255]. Decoding selects the codebook entry with the smallest Hamming distance from the received 8-bit codeword, allowing 1-bit error correction per nibble. There is no rejection on Hamming distance — any closest-codeword wins.
7. Frame Assembly¶
The byte stream is fed into a three-state machine:
| State | Action |
|---|---|
LENGTH | Read one byte. If ≥ 40, abort with error. Otherwise expect that many payload bytes. |
DATA | Buffer payload bytes. |
CHECKSUM | Read two bytes (low-byte first), compute CRC-16/IBM over the buffered payload, compare. Mismatch → error. |
A successful frame is handed to the payload parser. There is no retransmission and no multi-frame agreement; one good frame ends the session.
Payload Format¶
All current-generation Genius Plus X devices emit protocol version 5. A complete readout (with radio module) is 32 bytes; a smoke detector without a radio module emits 20 bytes (bytes 0–19 only).
Compact view (32-byte payload)¶
Byte | 0 | 1-4 | 5 | 6 | 7 | 8 | 9-10 | 11-12 | 13-14 | 15-16 | 17-18 | 19 |
Hex | 05 | XX XX XX XX | XX | XX | XX | XX | XX XX | XX XX | XX XX | XX XX | XX XX | XX |
Field| ver| SD-Serial | typ| del| alm| 3m | lAlm | prdAge| store | lSlf | wrnty | st |
Byte | 20 | 21-24 | 25-28 | 29 | 30 | 31 |
Hex | XX | XX XX XX XX | XX XX XX XX | XX | XX | XX |
Field| rs | RM-Serial | Line-ID | line| sw | int |
Detailed Field Reference¶
| Offset | Length | Field | Type | Description |
|---|---|---|---|---|
| 0 | 1 | protocolVersion | u8 | Currently observed value: 0x05. The decoder does not validate this byte but stores it on the resulting device record (readoutProtocolVersion). |
| 1–4 | 4 | serialNumber | u32 BE | Smoke detector serial number, big-endian. |
| 5 [3:0] | ½ | productType | u4 | 0 = Genius H, 1 = Genius Hx, 2 = Genius Plus, 3 = Genius Plus X. |
| 5 [7:4] | ½ | radioProductType | u4 | 0 = no FM module, 1 = FM Basis, 2 = FM Pro, 3 = FM MCP, 4 = FM Basis X, 5 = FM Pro X. |
| 6 | 1 | deinstallationCount | u8 | Number of times the detector has been removed from its mounting plate. |
| 7 | 1 | alarmCount | u8 | Lifetime alarm count. |
| 8 | 1 | alarmCountLast3Months | u8 | Alarm count in the trailing 90-day window. |
| 9–10 | 2 | lastAlarmOffset | u16 LE | Days from production to the most recent alarm. 0xFFFF = never alarmed. |
| 11–12 | 2 | productionAge | u16 LE | Days elapsed since the production date. The decoder uses the current wall-clock time as the reference point (see Date Reconstruction). |
| 13–14 | 2 | hoursInStorageMode | u16 LE | Cumulative hours the detector has spent in transport / storage mode. |
| 15–16 | 2 | lastSelftestOffset | u16 LE | Days from production to the most recent self-test. 0xFFFF = no self-test recorded. |
| 17–18 | 2 | warrantyFlagsRaw | u16 LE | Bitmask of warranty-voiding conditions. See Warranty Flags. |
| 19 | 1 | (status byte) | u8 | Smoke detector fault and drift state. See Status Byte. |
| 20 | 1 | radioStateMask | u8 | Radio module link & fault flags. See Radio State Mask. Only present if the radio module is fitted. |
| 21–24 | 4 | radioSerialNumber | u32 BE | Radio module serial number, big-endian. 0 = no radio module. |
| 25–28 | 4 | lineId | u32 BE | Alarm line identifier the detector is commissioned to. 0 = unassigned, 0xFFFFFFFF = broadcast/global. |
| 29 [7:4] | ½ | lineCharacter | u4 → char | Maps 0..9 to 'A'..'J'. Identifies the alarm-line letter. |
| 29 [3:0] | ½ | lineNumber | u4 | Alarm-line numeric suffix (0..15). |
| 30 | 1 | radioSwitchMask | u8 | DIP-switch / configuration bits. See Radio Switch Mask. |
| 31 | 1 | radioInterference | u8 | Interference level. The UI scales values > 0 by 1/10 and renders the result as a percentage (so byte 25 → 2.5%). |
Status Byte (byte 19)¶
| Bit | Mask | Field | Meaning when set |
|---|---|---|---|
| 0 | 0x01 | batteryLowFault | Battery voltage below the low-battery threshold. |
| 1 | 0x02 | deviceFault | Generic internal hardware fault. |
| 2 | 0x04 | radioNetworkFault | Radio mesh / link integrity issue (mirrored from the radio module). |
| 3–6 | 0x78 | driftState | Smoke chamber drift indicator. Observed values: 0 = OK, 2 = warning, 4 = defect. Other values are unspecified. |
| 7 | 0x80 | dirtForecastNegative | Predictive dust accumulation will cause failure before end of warranty. |
Warranty Flags (bytes 17–18, LE)¶
If warrantyFlagsRaw == 0, the warranty is fully intact and the parser surfaces a synthetic flag WarrantyPossible. Otherwise, each set bit corresponds to a specific voiding reason:
| Bit | Mnemonic | Trigger |
|---|---|---|
| 0 | MaxDirty | Maximum allowed contamination exceeded. |
| 1 | OutOfTemp | Operated outside the rated ambient temperature range. |
| 2 | DetectorTooOld | Calendar age exceeds the warranty period. |
| 3 | StorageTimeExceeded | Spent too long in storage mode before activation. |
| 4 | ActivationTimeExceeded | Time since activation has exceeded the limit. |
| 5 | TooManyEvents | Total event counter exhausted. |
| 6 | TooManyAlarms | Real-alarm counter exhausted. |
| 7 | TooManyFaults | Internal fault counter exhausted. |
| 8 | TooManySelfTests | Self-test counter exhausted. |
| 9 | TooManyRadioFaults | Radio module fault counter exhausted. |
| 10 | TooManyRadioOutOfOrderEvents | Radio out-of-order events counter exhausted. |
| 11 | RadioInstallationTooOld | Radio module installation age exceeded. |
| 12 | TooMuchRadioActivity | Radio activity counter exhausted. |
| 13 | TooMuchRadioInterference | Cumulative interference counter exhausted. |
| 14 | TooManyRadioTxEvents | Radio TX event counter exhausted. |
| 15 | TooManyRadioRxEvents | Radio RX event counter exhausted. |
Radio State Mask (byte 20)¶
| Bit | Mask | Mnemonic | Meaning when set |
|---|---|---|---|
| 0 | 0x01 | FmFault | Radio module reports a generic fault. |
| 1 | 0x02 | TransmissionRangeTest | A range test is currently active. |
| 2 | 0x04 | Selftest | The radio module is performing a self-test. |
| 3 | 0x08 | FmBatteryLowFault | Radio module battery low. |
| 4 | 0x10 | RemoteBattLow | Battery-low reported by a remote (linked) device. |
| 5 | 0x20 | RemoteError | Generic error reported by a remote device. |
| 6 | 0x40 | RadioLinkError | Radio link layer error (packet loss / supervision miss). |
| 7 | 0x80 | RemoteAlarm | Alarm triggered remotely (collective alarm propagation). |
Radio Switch Mask (byte 30)¶
These bits mirror the DIP-switch configuration on the FM Basis X module. Bits 0 and 1 are reserved.
| Bit | Mask | Mnemonic | Function |
|---|---|---|---|
| 2 | 0x04 | ReducedTransmittingPower | TX output is reduced (battery saving / regulatory). |
| 3 | 0x08 | RadioLinkSupervision | Periodic link-supervision heartbeats are enabled. |
| 4 | 0x10 | ReceiveCollectiveAlarm | Module forwards collective alarms received from peers. |
| 5 | 0x20 | SendCollectiveAlarm | Module emits collective alarms to peers. |
| 6 | 0x40 | SuppressAlarms | Local alarm output is suppressed. |
| 7 | 0x80 | SuppressWarnings | Local warning chirps are suppressed. |
Date Reconstruction¶
The payload contains no absolute timestamps. All dates are reconstructed at decode time from productionAge and the offsets, using the current wall-clock time as anchor:
now = Date.now()
productionDate = now − productionAge days
lastAlarm = now − (productionAge − lastAlarmOffset) days (if offset ≠ 0xFFFF)
lastSelftest = now − (productionAge − lastSelftestOffset) days (if offset ≠ 0xFFFF)
Clock drift
Because the reconstruction anchors on the receiving device's clock, every reconstructed date drifts by exactly one calendar day for each day that passes between two readouts of the same detector. The relative spacing between dates is preserved; the absolute values are only as accurate as the receiver's system time on the day of the readout.
Test Vectors¶
A set of hand-crafted WAV files lives under tests/ and exercises every status / fault / warranty flag combination:
| File | Purpose |
|---|---|
sd-complete-healthy.wav | Reference healthy detector with radio module. |
sd-battery-low.wav | batteryLowFault set. |
sd-device-fault.wav | deviceFault set. |
sd-battery-low-and-device-fault.wav | Both faults set. |
sd-drift-warning.wav | driftState = 2. |
sd-drift-defect.wav | driftState = 4. |
sd-dirt-forecast-negative.wav | dirtForecastNegative set. |
sd-warranty-voided.wav | Selected warranty flag bits set. |
sd-multiple-faults-with-radio.wav | Multiple SD + RM faults combined. |
sd-no-line-id.wav | lineId = 0x00000000 (unassigned / no specific line configured). |
sd-no-radio.wav | TC-EC-05 — radioProductType = 0, 20-byte payload (no radio module bytes). Parser sets hasRadio = false. |
The companion script generate-smartsonic-wav.mjs regenerates all fixtures from in-line test specs and can be extended to produce custom payloads. It implements the full forward chain — Hamming(8,4) encode → CRC-16/IBM append → CPFSK modulate at 44.1 kHz, 16-bit mono — and is the authoritative reference for verifying the decoder against new corner cases.