Add support for 14 additional TPMS protocols - #3003
Conversation
There was a problem hiding this comment.
Pull request overview
Adds new TPMS protocol identifiers across the firmware and introduces protocol-specific decode logic in the common TPMS packet decoder, with corresponding UI updates in both the receiver (tpmsrx) and transmitter (tpmstx).
Changes:
- Expanded
tpms::Reading::Typewith 14 new protocol types and added correspondingPacket::reading_*()decoder entry points. - Implemented new decode routines in
tpms_packet.cppand added a fallback decoder chain for unknown signal types. - Updated tpmsrx protocol name mapping and expanded tpmstx packet-type dropdown / validation to include the new types.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| firmware/common/tpms_packet.hpp | Adds new Reading::Type enum values and declares new decoder methods. |
| firmware/common/tpms_packet.cpp | Implements the new decoders and adds a fallback decoder chain in Packet::reading(). |
| firmware/application/external/tpmstx/tpms_tx_app.hpp | Adds new protocol types to the TX packet-type dropdown and widens the field. |
| firmware/application/external/tpmstx/tpms_tx_app.cpp | Maps new protocol types to an FSK signal type, updates UI display rules, and expands type validation range. |
| firmware/application/external/tpmsrx/tpms_app.cpp | Adds human-readable type-name mappings for the new protocols. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (size_t i = 0; i < 8; i++) { | ||
| bytes[i] = reader_.read(i * 8, 8); | ||
| } | ||
| bytes[8] = crc; |
There was a problem hiding this comment.
In reading_kia(), bytes is declared as uint8_t bytes[8]; but later bytes[8] = crc; writes past the end of the array (out-of-bounds). Either remove that assignment (it’s unused) or size the array appropriately and adjust the CRC calculation/verification accordingly.
| bytes[8] = crc; |
| // Verify CRC-8 | ||
| uint8_t bytes[8]; | ||
| for (size_t i = 0; i < 8; i++) { | ||
| bytes[i] = reader_.read(i * 8, 8); | ||
| } | ||
|
|
||
| CRC<8> crc_calc{0x07, 0x00}; | ||
| for (size_t i = 0; i < 8; i++) { | ||
| crc_calc.process_byte(bytes[i]); | ||
| } | ||
|
|
||
| if (crc_calc.checksum() != crc) { | ||
| return {}; |
There was a problem hiding this comment.
CRC verification in reading_renault() looks incorrect: you read crc from byte 7, then you compute crc_calc over all 8 bytes (including the CRC byte) and compare crc_calc.checksum() to crc. If the intended convention is a residual check (as in crc_valid_length()), the computed checksum should be compared to 0; otherwise, compute the CRC only over the data bytes (excluding the CRC byte) and compare to crc.
| } else if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) { | ||
| // FSK 19.2k for FLM variants | ||
| // Data is Manchester encoded: each data bit becomes two FSK symbols | ||
| // Preamble: 14 x '01' + '10' = 30 bits (matches RX pattern exactly) | ||
| // Payload: 160 Manchester-encoded bits (80 data bits max) | ||
|
|
||
| symbol_rate = 19200; | ||
|
|
||
| // Build preamble: 14 pairs of "01" followed by "10" | ||
| for (int i = 0; i < 14; i++) { | ||
| binary_string += "01"; | ||
| } | ||
| binary_string += "10"; | ||
|
|
||
| std::array<uint8_t, 20> data_bytes = {0}; // 160 bits = 20 bytes max | ||
| size_t num_data_bits = 0; | ||
|
|
||
| if (packet_type_ == tpms::Reading::Type::FLM_64) { | ||
| // FLM_64: 64 bits (8 bytes) | ||
| // Bytes 0-3: ID (32 bits) | ||
| // Byte 4: Pressure (8 bits) | ||
| // Byte 5: Temperature (8 bits, LSB 7 bits used) | ||
| // Byte 6: Padding | ||
| // Byte 7: Sum checksum of bytes 0-6 (low 8 bits) | ||
|
|
||
| num_data_bits = 64; | ||
|
|
||
| data_bytes[0] = (transponder_id_ >> 24) & 0xFF; | ||
| data_bytes[1] = (transponder_id_ >> 16) & 0xFF; | ||
| data_bytes[2] = (transponder_id_ >> 8) & 0xFF; | ||
| data_bytes[3] = transponder_id_ & 0xFF; | ||
| // Clamp pressure to prevent overflow: max 340 kPa (49.3 PSI) | ||
| uint16_t pressure_clamped_flm64 = (pressure_kpa_ > 340) ? 340 : pressure_kpa_; | ||
| data_bytes[4] = (pressure_clamped_flm64 * 3 / 4); | ||
| data_bytes[5] = ((temperature_c_ + 56) & 0x7F); // LSB 7 bits only | ||
| data_bytes[6] = 0x00; // Padding | ||
|
|
||
| // Addition checksum over bytes 0-6 | ||
| uint32_t checksum = 0; | ||
| for (int i = 0; i < 7; i++) { | ||
| checksum += data_bytes[i]; | ||
| } | ||
| data_bytes[7] = checksum & 0xFF; | ||
|
|
||
| } else if (packet_type_ == tpms::Reading::Type::FLM_72) { | ||
| // FLM_72: 72 bits (9 bytes) | ||
| // Bytes 0-3: ID (32 bits) | ||
| // Byte 4: Padding | ||
| // Byte 5: Pressure (8 bits) | ||
| // Byte 6: Temperature (8 bits) | ||
| // Bytes 7-8: CRC field - RX processes ALL 9 bytes and expects CRC result = 0 | ||
|
|
||
| num_data_bits = 72; | ||
|
|
||
| data_bytes[0] = (transponder_id_ >> 24) & 0xFF; | ||
| data_bytes[1] = (transponder_id_ >> 16) & 0xFF; | ||
| data_bytes[2] = (transponder_id_ >> 8) & 0xFF; | ||
| data_bytes[3] = transponder_id_ & 0xFF; | ||
| data_bytes[4] = 0x00; // Padding | ||
| // Clamp pressure to prevent overflow: max 340 kPa (49.3 PSI) | ||
| uint16_t pressure_clamped_flm = (pressure_kpa_ > 340) ? 340 : pressure_kpa_; | ||
| data_bytes[5] = (pressure_clamped_flm * 3 / 4); | ||
| data_bytes[6] = (temperature_c_ + 56) & 0xFF; | ||
| data_bytes[7] = 0x00; // Set to 0 initially | ||
|
|
||
| // Calculate CRC over bytes 0-7, place result in byte 8 | ||
| // When RX calculates CRC over bytes 0-8, it should get residual 0 | ||
| CRC<8> crc{0x01, 0x00}; | ||
| for (int i = 0; i < 8; i++) { | ||
| crc.process_byte(data_bytes[i]); | ||
| } | ||
| data_bytes[8] = crc.checksum() & 0xFF; | ||
|
|
||
| } else if (packet_type_ == tpms::Reading::Type::FLM_80) { | ||
| // FLM_80: 80 bits (10 bytes) | ||
| // Byte 0: Padding | ||
| // Bytes 1-4: ID (32 bits) | ||
| // Byte 5: Padding | ||
| // Byte 6: Pressure (8 bits) | ||
| // Byte 7: Temperature (8 bits) | ||
| // Bytes 8-9: CRC field - RX processes bytes 1-9 and expects CRC result = 0 | ||
|
|
||
| num_data_bits = 80; | ||
|
|
||
| data_bytes[0] = 0x00; // Padding (not included in CRC) | ||
| data_bytes[1] = (transponder_id_ >> 24) & 0xFF; | ||
| data_bytes[2] = (transponder_id_ >> 16) & 0xFF; | ||
| data_bytes[3] = (transponder_id_ >> 8) & 0xFF; | ||
| data_bytes[4] = transponder_id_ & 0xFF; | ||
| data_bytes[5] = 0x00; // Padding | ||
| // Clamp pressure to prevent overflow: max 340 kPa (49.3 PSI) | ||
| uint16_t pressure_clamped_flm80 = (pressure_kpa_ > 340) ? 340 : pressure_kpa_; | ||
| data_bytes[6] = (pressure_clamped_flm80 * 3 / 4); | ||
| data_bytes[7] = (temperature_c_ + 56) & 0xFF; | ||
| data_bytes[8] = 0x00; // Padding/Reserved | ||
| data_bytes[9] = 0x00; // CRC byte | ||
|
|
||
| // Calculate CRC over bytes 1-8 (all bytes except 0 and 9) | ||
| // When RX calculates CRC over bytes 1-9, it should get residual 0 | ||
| CRC<8> crc{0x01, 0x00}; | ||
| for (int i = 1; i <= 8; i++) { | ||
| crc.process_byte(data_bytes[i]); | ||
| } | ||
| data_bytes[9] = crc.checksum() & 0xFF; | ||
| } |
There was a problem hiding this comment.
TPMSTXView::encode_and_transmit() only builds FSK payloads for FLM_64/72/80. However update_signal_type_from_packet() maps the newly added protocol types (Toyota/Ford/…/AVE) to FSK_19k2_Schrader, so selecting any of those types will currently transmit just the preamble plus an all-zero padded payload. Either implement packet builders for the new types or prevent selecting/transmitting them until supported (e.g., map to an “unsupported” signal type and show an error).
| return Reading{ | ||
| Reading::Type::Toyota, | ||
| (uint32_t)id, | ||
| Pressure{static_cast<int>(pressure_raw * 0.25 - 7.0) * 7}, // Convert to kPa |
There was a problem hiding this comment.
Toyota pressure conversion currently truncates before converting to kPa: static_cast<int>(pressure_raw * 0.25 - 7.0) * 7 drops fractional PSI. Also * 7 is a rough PSI→kPa conversion; elsewhere (e.g. TX UI) PSI conversion uses * 6895 / 1000. Consider computing kPa with the more accurate factor and only converting to int once at the end.
| Pressure{static_cast<int>(pressure_raw * 0.25 - 7.0) * 7}, // Convert to kPa | |
| Pressure{static_cast<int>((pressure_raw * 0.25 - 7.0) * 6895.0 / 1000.0)}, // Convert to kPa |
| // Verify CRC-8 with poly 0x07 init 0xaa | ||
| uint8_t bytes[9]; | ||
| for (size_t i = 0; i < 9; i++) { | ||
| bytes[i] = reader_.read(i * 8, 8); | ||
| } | ||
|
|
||
| CRC<8> crc_calc{0x07, 0xaa}; | ||
| for (size_t i = 0; i < 9; i++) { | ||
| crc_calc.process_byte(bytes[i]); | ||
| } | ||
|
|
||
| if (crc_calc.checksum() != crc) { | ||
| return {}; | ||
| } |
There was a problem hiding this comment.
CRC verification in reading_hyundai_vdo() appears to have the same issue as reading_renault(): the CRC is read from the last byte, but the calculation processes all 9 bytes (including that CRC byte) and then compares the checksum to the CRC byte. Either compute CRC over the data bytes only and compare to crc, or include the CRC byte and check for a residual of 0 (consistent with crc_valid_length()).
| Optional<Reading> Packet::reading_nissan() const { | ||
| /* | ||
| * Nissan TPMS - FSK Manchester | ||
| * 37 bits | ||
| * Pressure in (raw / 4.0) PSI | ||
| */ | ||
| const auto mode = reader_.read(0, 3); | ||
| const auto id = ((reader_.read(3, 5) << 19) | | ||
| (reader_.read(8, 8) << 11) | | ||
| (reader_.read(16, 8) << 3) | | ||
| reader_.read(24, 3)); | ||
| const auto pressure_raw = ((reader_.read(27, 5) << 3) | reader_.read(32, 3)); | ||
|
|
||
| return Reading{ | ||
| Reading::Type::Nissan, | ||
| (uint32_t)id, | ||
| Pressure{static_cast<int>(pressure_raw / 4.0 * 7)}, // Convert PSI to kPa | ||
| {}, | ||
| Flags{static_cast<Flags>(mode)}}; | ||
| } |
There was a problem hiding this comment.
reading_nissan() always returns a Reading without any length/sanity/checksum validation. In the new fallback chain in Packet::reading() this would cause essentially any “unknown” packet to be accepted as Nissan, preventing later decoders from running. Add a strict validation (expected bit length, reserved bits/sync/mode constraints, plausibility checks) and return empty on failure.
| Optional<Reading> Packet::reading_toyota() const { | ||
| /* | ||
| * Toyota TPMS - FSK Differential Manchester | ||
| * 72 bits data with CRC-8 | ||
| * Pressure in (raw * 0.25 - 7) PSI | ||
| * Temperature in (raw - 40) C | ||
| */ | ||
| const auto length = crc_valid_length(); | ||
| if (length != 72) { | ||
| return {}; | ||
| } | ||
|
|
||
| const auto id = reader_.read(0, 32); | ||
| const auto status = (reader_.read(32, 1) << 7) | reader_.read(39, 7); | ||
| const auto pressure_raw = (reader_.read(33, 7) << 1) | reader_.read(40, 1); | ||
| const auto temp_raw = (reader_.read(41, 7) << 1) | reader_.read(48, 1); | ||
| const auto pressure2 = reader_.read(56, 8) ^ 0xff; |
There was a problem hiding this comment.
reading_toyota() is documented as “FSK Differential Manchester”, but tpms::Packet uses ManchesterDecoder/FieldReader<ManchesterDecoder,…> for all readings. If Toyota truly uses differential Manchester, this decoder will not work as written (you likely need BiphaseMDecoder, similar to sonde::Packet). If Toyota is not differential Manchester, the comment should be corrected to avoid misleading future work.
| const auto temp_raw = reader_.read(56, 8); | ||
| const auto battery = reader_.read(64, 8); | ||
| const auto checksum = reader_.read(72, 8); | ||
|
|
There was a problem hiding this comment.
Several new decoder functions introduce unused local variables (e.g. state, repeat, battery, checksum in reading_citroen()), which will trigger -Wall -Wextra unused-variable warnings. Either remove these locals or explicitly mark them unused (e.g., cast to void) if they’re kept for documentation/future use.
| (void)state; | |
| (void)repeat; | |
| (void)battery; | |
| (void)checksum; |
| const auto flags = reader_.read(0, 6); | ||
| const auto pressure_raw = (reader_.read(6, 2) << 8) | reader_.read(8, 8); | ||
| const auto temp_raw = reader_.read(16, 8); | ||
| const auto id = reader_.read(24, 24); // Little-endian in original |
There was a problem hiding this comment.
In reading_renault(), the ID field is read as reader_.read(24, 24) but the inline comment says it’s little-endian in the original. Either implement the endian conversion (byte-swap the 24-bit value) or update/remove the comment so the implementation and documentation match.
| const auto id = reader_.read(24, 24); // Little-endian in original | |
| const auto id = reader_.read(24, 24); |
This PR adds support for 14 new Tire Pressure Monitoring System (TPMS) protocols to both the receiver (tpmsrx) and transmitter (tpmstx) applications, based on decoder logic from rtl_433.
New TPMS Protocols
The following vehicle manufacturer and aftermarket TPMS protocols are now supported:
Changes
Receiver (tpmsrx)
type_name()functionTransmitter (tpmstx)
Core Decoder (tpms_packet.cpp)
reading()to try all decoders when signal type is unknownOptional<Reading>with empty result if validation failsNotes
Testing
These decoders should be tested with real TPMS sensors from the respective manufacturers to verify: