diff --git a/firmware/application/external/tpmsrx/tpms_app.cpp b/firmware/application/external/tpmsrx/tpms_app.cpp index c75ba385cc..9e43b82448 100644 --- a/firmware/application/external/tpmsrx/tpms_app.cpp +++ b/firmware/application/external/tpmsrx/tpms_app.cpp @@ -56,6 +56,34 @@ std::string type_name(tpms::Reading::Type type) { return "Schrader"; case tpms::Reading::Type::GMC_96: return "GMC_96"; + case tpms::Reading::Type::Toyota: + return "Toyota"; + case tpms::Reading::Type::Ford: + return "Ford"; + case tpms::Reading::Type::Citroen: + return "Citroen"; + case tpms::Reading::Type::Renault: + return "Renault"; + case tpms::Reading::Type::Hyundai_VDO: + return "Hyundai-VDO"; + case tpms::Reading::Type::Nissan: + return "Nissan"; + case tpms::Reading::Type::Abarth124: + return "Abarth124"; + case tpms::Reading::Type::Jansite: + return "Jansite"; + case tpms::Reading::Type::Jansite_Solar: + return "Jansite-Solar"; + case tpms::Reading::Type::Kia: + return "Kia"; + case tpms::Reading::Type::Elantra2012: + return "Elantra2012"; + case tpms::Reading::Type::PMV107J: + return "PMV-107J"; + case tpms::Reading::Type::Renault_0435R: + return "Renault-0435R"; + case tpms::Reading::Type::AVE: + return "AVE"; default: return "Unknown"; } diff --git a/firmware/application/external/tpmstx/tpms_tx_app.cpp b/firmware/application/external/tpmstx/tpms_tx_app.cpp index 5315dc73a0..d9e3b740a0 100644 --- a/firmware/application/external/tpmstx/tpms_tx_app.cpp +++ b/firmware/application/external/tpmstx/tpms_tx_app.cpp @@ -49,6 +49,20 @@ void TPMSTXView::update_signal_type_from_packet() { case tpms::Reading::Type::FLM_64: case tpms::Reading::Type::FLM_72: case tpms::Reading::Type::FLM_80: + case tpms::Reading::Type::Toyota: + case tpms::Reading::Type::Ford: + case tpms::Reading::Type::Citroen: + case tpms::Reading::Type::Renault: + case tpms::Reading::Type::Hyundai_VDO: + case tpms::Reading::Type::Nissan: + case tpms::Reading::Type::Abarth124: + case tpms::Reading::Type::Jansite: + case tpms::Reading::Type::Jansite_Solar: + case tpms::Reading::Type::Kia: + case tpms::Reading::Type::Elantra2012: + case tpms::Reading::Type::PMV107J: + case tpms::Reading::Type::Renault_0435R: + case tpms::Reading::Type::AVE: signal_type_ = tpms::SignalType::FSK_19k2_Schrader; break; case tpms::Reading::Type::GMC_96: @@ -103,7 +117,20 @@ void TPMSTXView::update_packet_display() { if (packet_type_ == tpms::Reading::Type::GMC_96 || packet_type_ == tpms::Reading::Type::FLM_64 || packet_type_ == tpms::Reading::Type::FLM_72 || - packet_type_ == tpms::Reading::Type::FLM_80) { + packet_type_ == tpms::Reading::Type::FLM_80 || + packet_type_ == tpms::Reading::Type::Toyota || + packet_type_ == tpms::Reading::Type::Ford || + packet_type_ == tpms::Reading::Type::Citroen || + packet_type_ == tpms::Reading::Type::Renault || + packet_type_ == tpms::Reading::Type::Hyundai_VDO || + packet_type_ == tpms::Reading::Type::Abarth124 || + packet_type_ == tpms::Reading::Type::Jansite || + packet_type_ == tpms::Reading::Type::Jansite_Solar || + packet_type_ == tpms::Reading::Type::Kia || + packet_type_ == tpms::Reading::Type::Elantra2012 || + packet_type_ == tpms::Reading::Type::PMV107J || + packet_type_ == tpms::Reading::Type::Renault_0435R || + packet_type_ == tpms::Reading::Type::AVE) { status += " " + to_string_dec_int(temperature_c_) + "C"; } else { status += " (no temp)"; @@ -724,7 +751,7 @@ TPMSTXView::TPMSTXView(NavigationView& nav) if (!type_str.empty()) { int type = std::stoi(type_str); if (type >= static_cast(tpms::Reading::Type::FLM_64) && - type <= static_cast(tpms::Reading::Type::GMC_96)) { + type <= static_cast(tpms::Reading::Type::AVE)) { packet_type_ = static_cast(type); options_packet_type.set_by_value(type); } diff --git a/firmware/application/external/tpmstx/tpms_tx_app.hpp b/firmware/application/external/tpmstx/tpms_tx_app.hpp index 69d0d10e1e..925dde4fbe 100644 --- a/firmware/application/external/tpmstx/tpms_tx_app.hpp +++ b/firmware/application/external/tpmstx/tpms_tx_app.hpp @@ -136,13 +136,27 @@ class TPMSTXView : public View { OptionsField options_packet_type{ {6 * 8, 1 * 16}, - 10, + 12, { {"Schrader", (int32_t)tpms::Reading::Type::Schrader}, {"FLM_64", (int32_t)tpms::Reading::Type::FLM_64}, {"FLM_72", (int32_t)tpms::Reading::Type::FLM_72}, {"FLM_80", (int32_t)tpms::Reading::Type::FLM_80}, {"GMC_96", (int32_t)tpms::Reading::Type::GMC_96}, + {"Toyota", (int32_t)tpms::Reading::Type::Toyota}, + {"Ford", (int32_t)tpms::Reading::Type::Ford}, + {"Citroen", (int32_t)tpms::Reading::Type::Citroen}, + {"Renault", (int32_t)tpms::Reading::Type::Renault}, + {"Hyundai", (int32_t)tpms::Reading::Type::Hyundai_VDO}, + {"Nissan", (int32_t)tpms::Reading::Type::Nissan}, + {"Abarth", (int32_t)tpms::Reading::Type::Abarth124}, + {"Jansite", (int32_t)tpms::Reading::Type::Jansite}, + {"Jansite-S", (int32_t)tpms::Reading::Type::Jansite_Solar}, + {"Kia", (int32_t)tpms::Reading::Type::Kia}, + {"Elantra", (int32_t)tpms::Reading::Type::Elantra2012}, + {"PMV107J", (int32_t)tpms::Reading::Type::PMV107J}, + {"Renault-R", (int32_t)tpms::Reading::Type::Renault_0435R}, + {"AVE", (int32_t)tpms::Reading::Type::AVE}, }}; OptionsField options_pressure{ diff --git a/firmware/common/tpms_packet.cpp b/firmware/common/tpms_packet.cpp index 8098b13caf..3559739699 100644 --- a/firmware/common/tpms_packet.cpp +++ b/firmware/common/tpms_packet.cpp @@ -127,6 +127,455 @@ Optional Packet::reading_ook_8k4_schrader() const { } } +Optional 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; + + // Verify pressure consistency + if (pressure_raw != pressure2) { + return {}; + } + + return Reading{ + Reading::Type::Toyota, + (uint32_t)id, + Pressure{static_cast(pressure_raw * 0.25 - 7.0) * 7}, // Convert to kPa + Temperature{static_cast(temp_raw - 40)}}; +} + +Optional Packet::reading_ford() const { + /* + * Ford TPMS - FSK Manchester + * 64 bits with simple checksum + * Pressure in (raw * 0.25) PSI + * Temperature in (raw - 56) C + */ + const auto id = reader_.read(0, 32); + const auto pressure_raw = ((reader_.read(48, 1) << 8) | reader_.read(32, 8)); + const auto temp_raw = reader_.read(40, 8); + const auto flags = reader_.read(48, 8); + const auto checksum = reader_.read(56, 8); + + // Verify checksum + uint8_t sum = 0; + for (size_t i = 0; i < 7; i++) { + sum += reader_.read(i * 8, 8); + } + if ((sum & 0xff) != checksum) { + return {}; + } + + // Temperature valid if bit 7 is not set + Optional temperature{}; + if ((temp_raw & 0x80) == 0) { + temperature = Temperature{static_cast(temp_raw & 0x7f) - 56}; + } + + return Reading{ + Reading::Type::Ford, + (uint32_t)id, + Pressure{static_cast(pressure_raw * 0.25 * 7)}, // Convert to kPa + temperature, + Flags{static_cast(flags)}}; +} + +Optional Packet::reading_citroen() const { + /* + * Citroen TPMS - FSK Manchester + * 80 bits with XOR checksum + * Pressure in (raw * 1.364) kPa + * Temperature in (raw - 50) C + */ + const auto state = reader_.read(0, 8); + const auto id = reader_.read(8, 32); + const auto flags = reader_.read(40, 4); + const auto repeat = reader_.read(44, 4); + const auto pressure_raw = reader_.read(48, 8); + const auto temp_raw = reader_.read(56, 8); + const auto battery = reader_.read(64, 8); + const auto checksum = reader_.read(72, 8); + + // Sanity checks + if (pressure_raw == 0 || temp_raw == 0) { + return {}; + } + + // Verify XOR checksum (bytes 1-9 XOR = 0) + uint8_t xor_sum = 0; + for (size_t i = 1; i < 10; i++) { + xor_sum ^= reader_.read(i * 8, 8); + } + if (xor_sum != 0) { + return {}; + } + + return Reading{ + Reading::Type::Citroen, + (uint32_t)id, + Pressure{static_cast(pressure_raw * 1.364)}, + Temperature{static_cast(temp_raw - 50)}, + Flags{static_cast(flags)}}; +} + +Optional Packet::reading_renault() const { + /* + * Renault TPMS - FSK Manchester + * 72 bits with CRC-8 + * Pressure in (raw * 0.75) kPa + * Temperature in (raw - 30) C + */ + 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 + const auto unknown = reader_.read(48, 16); + const auto crc = reader_.read(64, 8); + + // 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 {}; + } + + return Reading{ + Reading::Type::Renault, + (uint32_t)id, + Pressure{static_cast(pressure_raw * 0.75)}, + Temperature{static_cast(temp_raw - 30)}, + Flags{static_cast(flags)}}; +} + +Optional Packet::reading_hyundai_vdo() const { + /* + * Hyundai VDO TPMS - FSK Manchester + * 80 bits with CRC-8 + * Pressure in (raw * 1.375) kPa + * Temperature in (raw - 50) C + */ + const auto state = reader_.read(0, 8); + const auto id = reader_.read(8, 32); + const auto flags = reader_.read(40, 4); + const auto repeat = reader_.read(44, 4); + const auto pressure_raw = reader_.read(48, 8); + const auto temp_raw = reader_.read(56, 8); + const auto battery = reader_.read(64, 8); + const auto crc = reader_.read(72, 8); + + // 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 {}; + } + + return Reading{ + Reading::Type::Hyundai_VDO, + (uint32_t)id, + Pressure{static_cast(pressure_raw * 1.375)}, + Temperature{static_cast(temp_raw - 50)}, + Flags{static_cast(flags)}}; +} + +Optional 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(pressure_raw / 4.0 * 7)}, // Convert PSI to kPa + {}, + Flags{static_cast(mode)}}; +} + +Optional Packet::reading_abarth124() const { + /* + * Abarth 124 Spider / VDO TG1C - FSK Manchester + * 72 bits with XOR checksum + * Pressure in (raw * 1.38) kPa + * Temperature in (raw - 50) C + */ + const auto id = reader_.read(0, 32); + const auto flags = reader_.read(32, 8); + const auto pressure_raw = reader_.read(40, 8); + const auto temp_raw = reader_.read(48, 8); + const auto status = reader_.read(56, 8); + const auto checksum = reader_.read(64, 8); + + // Verify XOR checksum + uint8_t xor_sum = 0; + for (size_t i = 0; i < 9; i++) { + xor_sum ^= reader_.read(i * 8, 8); + } + if (xor_sum != 0) { + return {}; + } + + return Reading{ + Reading::Type::Abarth124, + (uint32_t)id, + Pressure{static_cast(pressure_raw * 1.38)}, + Temperature{static_cast(temp_raw - 50)}, + Flags{static_cast(flags)}}; +} + +Optional Packet::reading_jansite() const { + /* + * Jansite TPMS - FSK Manchester + * 56 bits (no reliable checksum) + * Pressure in (raw * 1.7) kPa + * Temperature in (raw - 50) C + */ + const auto id = ((reader_.read(0, 8) << 20) | + (reader_.read(8, 8) << 12) | + (reader_.read(16, 8) << 4) | + (reader_.read(24, 4))); + const auto flags = reader_.read(28, 4); + const auto pressure_raw = reader_.read(32, 8); + const auto temp_raw = reader_.read(40, 8); + + return Reading{ + Reading::Type::Jansite, + (uint32_t)id, + Pressure{static_cast(pressure_raw * 1.7)}, + Temperature{static_cast(temp_raw - 50)}, + Flags{static_cast(flags)}}; +} + +Optional Packet::reading_jansite_solar() const { + /* + * Jansite Solar TPMS - FSK Manchester + * 88 bits with CRC-16 + * Pressure in (raw * 1.6) kPa + * Temperature in (raw - 55) C + */ + const auto sync = reader_.read(0, 16); + if (sync != 0xdd33) { + return {}; + } + + const auto id = ((reader_.read(16, 8) << 16) | + (reader_.read(24, 8) << 8) | + reader_.read(32, 8)); + const auto flags = reader_.read(40, 8); + const auto temp_raw = reader_.read(48, 8); + const auto pressure_raw = reader_.read(56, 8); + const auto unknown = reader_.read(64, 8); + const auto crc = reader_.read(72, 16); + + // Verify CRC-16/BUYPASS + uint8_t bytes[9]; + for (size_t i = 0; i < 9; i++) { + bytes[i] = reader_.read(i * 8 + 16, 8); // Skip sync word + } + + uint16_t crc_calc = 0; + for (size_t i = 0; i < 7; i++) { // CRC over first 7 bytes + crc_calc = crc_calc ^ (bytes[i] << 8); + for (int j = 0; j < 8; j++) { + if (crc_calc & 0x8000) { + crc_calc = (crc_calc << 1) ^ 0x8005; + } else { + crc_calc = crc_calc << 1; + } + } + } + + if (crc_calc != crc) { + return {}; + } + + return Reading{ + Reading::Type::Jansite_Solar, + (uint32_t)id, + Pressure{static_cast(pressure_raw * 1.6)}, + Temperature{static_cast(temp_raw - 55)}, + Flags{static_cast(flags)}}; +} + +Optional Packet::reading_kia() const { + /* + * Kia TPMS - FSK Manchester + * 138 bits (after 16-bit preamble) with CRC-8 + * Pressure in (raw / 5.0) PSI + * Temperature in (raw - 50) C + */ + const auto unknown1 = reader_.read(0, 4); + const auto pressure_raw = ((reader_.read(4, 4) << 4) | reader_.read(8, 4)); + const auto temp_raw = ((reader_.read(12, 4) << 4) | reader_.read(16, 4)); + const auto id = ((reader_.read(20, 4) << 28) | + (reader_.read(24, 8) << 20) | + (reader_.read(32, 8) << 12) | + (reader_.read(40, 8) << 4) | + reader_.read(48, 4)); + const auto unknown2 = ((reader_.read(52, 4) << 8) | reader_.read(56, 8)); + const auto crc = reader_.read(64, 8) & 0xF8; // Last 3 bits are padding + + // Verify CRC-8 + uint8_t bytes[8]; + for (size_t i = 0; i < 8; i++) { + bytes[i] = reader_.read(i * 8, 8); + } + bytes[8] = crc; + + CRC<8> crc_calc{0x07, 0x76}; + for (size_t i = 0; i < 8; i++) { + crc_calc.process_byte(bytes[i]); + } + + if (crc_calc.checksum() != crc) { + return {}; + } + + return Reading{ + Reading::Type::Kia, + (uint32_t)id, + Pressure{static_cast(pressure_raw / 5.0 * 7)}, // Convert PSI to kPa + Temperature{static_cast(temp_raw - 50)}, + Flags{static_cast(unknown1)}}; +} + +Optional Packet::reading_elantra2012() const { + /* + * Elantra 2012 / TRW - FSK Manchester + * 64 bits with CRC-8 + * Pressure in (raw + 60) kPa + * Temperature in (raw - 50) C + */ + const auto pressure_raw = reader_.read(0, 8); + const auto temp_raw = reader_.read(8, 8); + const auto id = reader_.read(16, 32); + const auto flags = reader_.read(48, 8); + const auto crc = reader_.read(56, 8); + + // Verify CRC-8 + uint8_t bytes[7]; + for (size_t i = 0; i < 7; i++) { + bytes[i] = reader_.read(i * 8, 8); + } + + CRC<8> crc_calc{0x07, 0x00}; + for (size_t i = 0; i < 7; i++) { + crc_calc.process_byte(bytes[i]); + } + + if (crc_calc.checksum() != crc) { + return {}; + } + + return Reading{ + Reading::Type::Elantra2012, + (uint32_t)id, + Pressure{static_cast(pressure_raw + 60)}, + Temperature{static_cast(temp_raw - 50)}, + Flags{static_cast(flags)}}; +} + +Optional Packet::reading_pmv107j() const { + /* + * PMV-107J (Toyota) - FSK Differential Manchester + * 66 bits with CRC-8 + * Pressure in (raw - 40) * 2.48 kPa + * Temperature in (raw - 40) C + */ + // This uses differential Manchester which requires special handling + // For now, return empty as it needs different decoder + return {}; +} + +Optional Packet::reading_renault_0435r() const { + /* + * Renault 0435R - FSK Manchester + * 72 bits with XOR checksum + * Pressure in (raw / 0.75) kPa + * Temperature in (raw - 50) C + */ + const auto id = ((reader_.read(0, 8) << 16) | + (reader_.read(8, 8) << 8) | + reader_.read(16, 8)); + const auto flags = reader_.read(24, 8); + const auto pressure_raw = reader_.read(32, 8); + const auto temp_raw = reader_.read(40, 8); + const auto accel = reader_.read(48, 8); + const auto checksum = reader_.read(56, 8); + const auto tick = reader_.read(64, 8); + + // Verify XOR checksum + uint8_t xor_sum = 0; + for (size_t i = 0; i < 9; i++) { + xor_sum ^= reader_.read(i * 8, 8); + } + if (xor_sum != 0) { + return {}; + } + + // Sanity check tick + const auto has_tick = (tick >> 7) & 1; + const auto tick_val = tick & 0x7F; + if (tick != 0 && (!has_tick || tick_val > 30)) { + return {}; + } + + return Reading{ + Reading::Type::Renault_0435R, + (uint32_t)id, + Pressure{static_cast(pressure_raw / 0.75)}, + Temperature{static_cast(temp_raw - 50)}, + Flags{static_cast(flags)}}; +} + +Optional Packet::reading_ave() const { + /* + * AVE TPMS - FSK Differential Manchester + * 64 bits with CRC-8 (poly 0x31, init 0xff) + * Pressure varies by mode (default: raw * 2.352 kPa) + * Temperature in (raw - 50) C + */ + // This uses differential Manchester which requires special handling + // For now, return empty as it needs different decoder + return {}; +} + Optional Packet::reading() const { switch (signal_type()) { case SignalType::FSK_19k2_Schrader: @@ -136,6 +585,50 @@ Optional Packet::reading() const { case SignalType::OOK_8k4_Schrader: return reading_ook_8k4_schrader(); default: + // Try all new decoders + { + auto result = reading_toyota(); + if (result.is_valid()) return result; + + result = reading_ford(); + if (result.is_valid()) return result; + + result = reading_citroen(); + if (result.is_valid()) return result; + + result = reading_renault(); + if (result.is_valid()) return result; + + result = reading_hyundai_vdo(); + if (result.is_valid()) return result; + + result = reading_nissan(); + if (result.is_valid()) return result; + + result = reading_abarth124(); + if (result.is_valid()) return result; + + result = reading_jansite(); + if (result.is_valid()) return result; + + result = reading_jansite_solar(); + if (result.is_valid()) return result; + + result = reading_kia(); + if (result.is_valid()) return result; + + result = reading_elantra2012(); + if (result.is_valid()) return result; + + result = reading_pmv107j(); + if (result.is_valid()) return result; + + result = reading_renault_0435r(); + if (result.is_valid()) return result; + + result = reading_ave(); + if (result.is_valid()) return result; + } return {}; } } diff --git a/firmware/common/tpms_packet.hpp b/firmware/common/tpms_packet.hpp index cba59a7aca..702a7c4d8d 100644 --- a/firmware/common/tpms_packet.hpp +++ b/firmware/common/tpms_packet.hpp @@ -78,6 +78,20 @@ class Reading { FLM_80 = 3, Schrader = 4, GMC_96 = 5, + Toyota = 6, + Ford = 7, + Citroen = 8, + Renault = 9, + Hyundai_VDO = 10, + Nissan = 11, + Abarth124 = 12, + Jansite = 13, + Jansite_Solar = 14, + Kia = 15, + Elantra2012 = 16, + PMV107J = 17, + Renault_0435R = 18, + AVE = 19, }; constexpr Reading() @@ -163,6 +177,21 @@ class Packet { Optional reading_ook_8k192_schrader() const; Optional reading_ook_8k4_schrader() const; + Optional reading_toyota() const; + Optional reading_ford() const; + Optional reading_citroen() const; + Optional reading_renault() const; + Optional reading_hyundai_vdo() const; + Optional reading_nissan() const; + Optional reading_abarth124() const; + Optional reading_jansite() const; + Optional reading_jansite_solar() const; + Optional reading_kia() const; + Optional reading_elantra2012() const; + Optional reading_pmv107j() const; + Optional reading_renault_0435r() const; + Optional reading_ave() const; + size_t crc_valid_length() const; };