Skip to content

Add support for 14 additional TPMS protocols - #3003

Draft
jLynx wants to merge 1 commit into
portapack-mayhem:nextfrom
jLynx:tpms_protocols
Draft

Add support for 14 additional TPMS protocols#3003
jLynx wants to merge 1 commit into
portapack-mayhem:nextfrom
jLynx:tpms_protocols

Conversation

@jLynx

@jLynx jLynx commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

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:

  1. Toyota - 72-bit FSK with CRC-8, pressure in PSI
  2. Ford - 64-bit FSK Manchester with checksum
  3. Citroen - 80-bit FSK Manchester with XOR checksum
  4. Renault - 72-bit FSK Manchester with CRC-8
  5. Hyundai VDO - 80-bit FSK Manchester with CRC-8
  6. Nissan - 37-bit FSK Manchester
  7. Abarth 124 Spider / VDO TG1C - 72-bit FSK Manchester with XOR checksum
  8. Jansite - 56-bit FSK Manchester (aftermarket)
  9. Jansite Solar - 88-bit FSK Manchester with CRC-16 (aftermarket)
  10. Kia - 138-bit FSK Manchester with CRC-8
  11. Elantra 2012 / TRW - 64-bit FSK Manchester with CRC-8
  12. PMV-107J (Toyota) - 66-bit differential Manchester (decoder stub only)
  13. Renault 0435R - 72-bit FSK Manchester with XOR checksum
  14. AVE - 64-bit differential Manchester (decoder stub only)

Changes

Receiver (tpmsrx)

  • Added type name mappings for all new protocols in type_name() function
  • Protocols are automatically detected when receiving signals

Transmitter (tpmstx)

  • Added all new protocol types to the packet type dropdown (expanded from 10 to 12 character width)
  • Configured signal type mapping (most use FSK_19k2_Schrader)
  • Added temperature display support for applicable protocols
  • Updated packet type validation range to include new types
  • Shortened some display names to fit UI constraints (e.g., "Hyundai-VDO" → "Hyundai", "Jansite-Solar" → "Jansite-S")

Core Decoder (tpms_packet.cpp)

  • Implemented 14 new decoder functions with protocol-specific:
  • Bit field extraction and parsing
  • Checksum/CRC validation (CRC-8, CRC-16, XOR checksums)
  • Pressure unit conversion (PSI → kPa where needed)
  • Temperature offset correction
  • Sanity checks for data validity
  • Added fallback decoder chain in reading() to try all decoders when signal type is unknown
  • Each decoder returns Optional<Reading> with empty result if validation fails

Notes

  • PMV-107J and AVE protocols use differential Manchester encoding which requires special decoder handling not yet implemented - these return empty results for now
  • All pressure values are normalized to kPa internally
  • Temperature readings include manufacturer-specific offsets (typically -30°C to -56°C)
  • Most protocols include CRC or checksum validation to ensure data integrity
  • The decoder chain attempts all protocols when signal type cannot be determined from modulation alone

Testing

These decoders should be tested with real TPMS sensors from the respective manufacturers to verify:

  • Correct ID extraction
  • Accurate pressure readings
  • Accurate temperature readings (where supported)
  • Proper checksum/CRC validation

@jLynx jLynx changed the title Add support for additional TPMS reading types and decoding methods Add support for 14 additional TPMS protocols Feb 18, 2026
@gullradriel
gullradriel requested a review from Copilot February 19, 2026 19:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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::Type with 14 new protocol types and added corresponding Packet::reading_*() decoder entry points.
  • Implemented new decode routines in tpms_packet.cpp and 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;

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
bytes[8] = crc;

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch here

Comment on lines +248 to +260
// 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 {};

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 350 to 454
} 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;
}

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
return Reading{
Reading::Type::Toyota,
(uint32_t)id,
Pressure{static_cast<int>(pressure_raw * 0.25 - 7.0) * 7}, // Convert to kPa

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +287 to +300
// 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 {};
}

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()).

Copilot uses AI. Check for mistakes.
Comment on lines +310 to +329
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)}};
}

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +146
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;

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
const auto temp_raw = reader_.read(56, 8);
const auto battery = reader_.read(64, 8);
const auto checksum = reader_.read(72, 8);

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
(void)state;
(void)repeat;
(void)battery;
(void)checksum;

Copilot uses AI. Check for mistakes.
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

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
const auto id = reader_.read(24, 24); // Little-endian in original
const auto id = reader_.read(24, 24);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants