Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Repository Guidelines

## Project Structure & Module Organization
The Flipper Zero TPMS app is written in C and keeps its entry point in `tpms_app.c`, with the public interface in `tpms_app_i.h`. Interaction flows are assembled in `scenes/`, rendering logic in `views/`, and protocol decoders in `protocols/`. Shared helpers for radio presets and event data sit in `helpers/`, while deployable assets live under `images/`. `application.fam` defines the build metadata (appid `tpms`, category Sub-GHz); keep it in sync whenever you expose new scenes or resources.

## Build, Test, and Development Commands
- `./fbt fap_tpms` (run from a cloned `flipperzero-firmware` tree with this repo under `applications_user/tpms`) builds the FAP and drops it into `build/f7-firmware-D/apps`.
- `./fbt launch_app APPSRC=flipperzero-tpms` launches the debug build on a connected Flipper or emulator for quick iteration.
- `./fbt fap_dist APPSRC=flipperzero-tpms` creates a distributable `tpms.fap` archive in `dist/fap` for release builds.

## Coding Style & Naming Conventions
- Follow the established 4-space indentation, K&R braces, and `snake_case` function/file names prefixed with `tpms_`; reserve `PascalCase` for structs and enums such as `TPMSApp`.
- Group system/Furi includes before project headers, keep them alphabetized, and prefer `static` helpers when the symbol is local to a file.
- Document non-obvious radio timing or preset choices with a short comment; favor existing logging patterns over new macros.

## Testing Guidelines
There is no automated suite yet, so rely on hardware verification. After flashing via `./fbt fap_tpms` and loading the package with qFlipper, confirm menu navigation, sensor discovery, and relearn timing against supported sensors. Record manual smoke results in the pull request, including firmware hash and sensor IDs. When adding protocol decoders, attach concise RF logs or test notes that let reviewers understand the scenario you validated.

For receiver validation, do not stop at the first successful decode. Confirm repeated reception of an already-known sensor ID and verify that `Auto` behaves correctly on hardware without an external CC1101 attached.

## Commit & Pull Request Guidelines
Recent history favors concise, imperative subjects (e.g. `Update tpms for latest firmware version`); keep them ≤72 characters and scope each commit to one logical change. For pull requests, provide a short summary, testing notes, related issues/discussions, and screenshots or GIFs when UI states change. Request maintainer review prior to merge and ensure you build against the same `flipperzero-firmware` revision referenced in the PR description.

## Configuration Tips
Add new sensors by extending `protocols/` and documenting any radio preset changes directly in the decoder code so testers can replicate results. Re-run `./fbt fap_tpms` after asset changes to verify the `tpms_10px.png` icon and other resources remain within Flipper size/format limits.

## Path Hygiene
Prefer repo-relative paths in docs, scripts, comments, and answers unless an absolute path is required for a device-specific or host-specific operation. Avoid centering instructions around local workspace paths when a repository-relative path communicates the same thing.

Do not leave `picocom`, `minicom`, or similar serial sessions open after a check. Close them before launches, uploads, or additional device verification so the USB CDC path does not get wedged by your own session.
9 changes: 9 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@

## Features
- Read [TPMS](https://en.wikipedia.org/wiki/Tire-pressure_monitoring_system) sensors
- Decode Toyota PMV-C210/C215 sensors
- Filter received frames by minimum RSSI
- Select radio path explicitly: `Auto`, `Internal`, or `External`
- Relearn by 125kHz signal

#### Supported sensors
* Schrader GG4
* Toyota PMV-C210 / PMV-C215
* Abarth 124 (soon)

## How to use
Expand All @@ -19,6 +23,11 @@ If sensor is supported and correct frequency and modulation was set, an item wit

Pressing OK displays temperature and pressure.

Receiver configuration is available from the main receive screen:
- `Modulation` now includes the Toyota `PMV-C2x` preset
- `Min RSSI` can be used to ignore weak frames
- `Radio` can force `Internal` or `External` hardware instead of relying on `Auto`

![input](tpms.gif)

Feel free to contribute via PR or report issue
Expand Down
3 changes: 2 additions & 1 deletion application.fam
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ App(
targets=["f7"],
entry_point="tpms_app",
requires=["gui"],
sources=["*.c", "!tools"],
stack_size=4 * 1024,
order=50,
fap_icon="tpms_10px.png",
fap_category="Sub-GHz",
fap_icon_assets="images",
fap_version="0.1",
fap_version="0.3",
fap_description="Receive data from vehicle Tyre Pressure sensors (TPMS)",
)
56 changes: 43 additions & 13 deletions helpers/radio_device_loader.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ static void radio_device_loader_power_off() {
if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg();
}

static bool radio_device_loader_begin_external(const SubGhzDevice* radio_device) {
furi_assert(radio_device);

radio_device_loader_power_on();
if(subghz_devices_begin(radio_device)) {
return true;
}

radio_device_loader_power_off();
return false;
}

bool radio_device_loader_is_connect_external(const char* name) {
bool is_connect = false;
bool is_otg_enabled = furi_hal_power_is_otg_enabled();
Expand All @@ -38,21 +50,39 @@ bool radio_device_loader_is_connect_external(const char* name) {
const SubGhzDevice* radio_device_loader_set(
const SubGhzDevice* current_radio_device,
SubGhzRadioDeviceType radio_device_type) {
const SubGhzDevice* radio_device;
const SubGhzDevice* internal_radio_device =
subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
const SubGhzDevice* external_radio_device =
subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME);

if(radio_device_type == SubGhzRadioDeviceTypeExternalCC1101 &&
radio_device_loader_is_connect_external(SUBGHZ_DEVICE_CC1101_EXT_NAME)) {
radio_device_loader_power_on();
radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME);
subghz_devices_begin(radio_device);
} else if(current_radio_device == NULL) {
radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
} else {
radio_device_loader_end(current_radio_device);
radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
bool prefer_external = (radio_device_type == SubGhzRadioDeviceTypeExternalCC1101);
if(prefer_external) {
prefer_external = radio_device_loader_is_connect_external(SUBGHZ_DEVICE_CC1101_EXT_NAME);
}

return radio_device;
const SubGhzDevice* target_radio_device = internal_radio_device;
bool use_external = false;

if(prefer_external && external_radio_device) {
if(current_radio_device && current_radio_device != internal_radio_device) {
subghz_devices_end(current_radio_device);
current_radio_device = NULL;
}

use_external = radio_device_loader_begin_external(external_radio_device);
if(use_external) {
target_radio_device = external_radio_device;
}
}

if(!use_external) {
if(current_radio_device && current_radio_device != internal_radio_device) {
subghz_devices_end(current_radio_device);
}
radio_device_loader_power_off();
}

return target_radio_device;
}

bool radio_device_loader_is_external(const SubGhzDevice* radio_device) {
Expand All @@ -66,4 +96,4 @@ void radio_device_loader_end(const SubGhzDevice* radio_device) {
if(radio_device != subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME)) {
subghz_devices_end(radio_device);
}
}
}
3 changes: 2 additions & 1 deletion helpers/radio_device_loader.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

/** SubGhzRadioDeviceType */
typedef enum {
SubGhzRadioDeviceTypeAuto,
SubGhzRadioDeviceTypeInternal,
SubGhzRadioDeviceTypeExternalCC1101,
} SubGhzRadioDeviceType;
Expand All @@ -14,4 +15,4 @@ const SubGhzDevice* radio_device_loader_set(

bool radio_device_loader_is_external(const SubGhzDevice* radio_device);

void radio_device_loader_end(const SubGhzDevice* radio_device);
void radio_device_loader_end(const SubGhzDevice* radio_device);
118 changes: 118 additions & 0 deletions helpers/tpms_preset.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#include "tpms_preset.h"

#include <string.h>

static uint8_t tpms_preset_toyota_pmvc2x_data[] = {
0x02, 0x0D, 0x0B, 0x06, 0x08, 0x32, 0x07, 0x04, 0x14, 0x00, 0x13, 0x02,
0x12, 0x04, 0x11, 0x93, 0x10, 0x59, 0x15, 0x41, 0x18, 0x18, 0x19, 0x16,
0x1D, 0x91, 0x1C, 0x00, 0x1B, 0x07, 0x20, 0xFB, 0x22, 0x10, 0x21, 0x56,
0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

static const TPMSPresetDefinition tpms_preset_definitions[] = {
{
.name = TPMS_PRESET_TOYOTA_PMVC2X_NAME,
.short_name = "FM",
.radio_preset = FuriHalSubGhzPresetCustom,
.custom_data = tpms_preset_toyota_pmvc2x_data,
.custom_data_size = sizeof(tpms_preset_toyota_pmvc2x_data),
},
{
.name = "AM270",
.short_name = "AM",
.radio_preset = FuriHalSubGhzPresetOok270Async,
},
{
.name = "AM650",
.short_name = "AM",
.radio_preset = FuriHalSubGhzPresetOok650Async,
},
{
.name = "FM238",
.short_name = "FM",
.radio_preset = FuriHalSubGhzPreset2FSKDev238Async,
},
{
.name = "FM12K",
.short_name = "FM",
.radio_preset = FuriHalSubGhzPreset2FSKDev12KAsync,
},
{
.name = "FM476",
.short_name = "FM",
.radio_preset = FuriHalSubGhzPreset2FSKDev476Async,
},
};

size_t tpms_preset_count(void) {
return COUNT_OF(tpms_preset_definitions);
}

const TPMSPresetDefinition* tpms_preset_get_by_index(size_t index) {
if(index >= tpms_preset_count()) {
return NULL;
}

return &tpms_preset_definitions[index];
}

const TPMSPresetDefinition* tpms_preset_get_by_name(const char* name) {
if(name == NULL) {
return NULL;
}

for(size_t index = 0; index < tpms_preset_count(); index++) {
const TPMSPresetDefinition* definition = &tpms_preset_definitions[index];
if(strcmp(definition->name, name) == 0) {
return definition;
}
}

return NULL;
}

const TPMSPresetDefinition* tpms_preset_get_default(void) {
return &tpms_preset_definitions[0];
}

const char* tpms_preset_get_short_name(const char* name) {
const TPMSPresetDefinition* definition = tpms_preset_get_by_name(name);
return definition ? definition->short_name : "CU";
}

FuriHalSubGhzPreset tpms_preset_get_radio_preset(const char* name) {
const TPMSPresetDefinition* definition = tpms_preset_get_by_name(name);
return definition ? definition->radio_preset : FuriHalSubGhzPresetCustom;
}

uint32_t tpms_preset_get_default_frequency(void) {
return TPMS_PRESET_TOYOTA_PMVC2X_FREQUENCY;
}

bool tpms_preset_init_radio(
SubGhzRadioPreset* preset,
const char* preset_name,
uint32_t frequency,
uint8_t* preset_data,
size_t preset_data_size) {
furi_assert(preset);
furi_assert(preset_name);

const TPMSPresetDefinition* definition = tpms_preset_get_by_name(preset_name);

furi_string_set(preset->name, definition ? definition->name : preset_name);
preset->frequency = frequency;

if(definition && definition->radio_preset == FuriHalSubGhzPresetCustom) {
preset->data = (uint8_t*)definition->custom_data;
preset->data_size = definition->custom_data_size;
} else if(definition) {
preset->data = NULL;
preset->data_size = 0;
} else {
preset->data = preset_data;
preset->data_size = preset_data_size;
}

return true;
}
29 changes: 29 additions & 0 deletions helpers/tpms_preset.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#pragma once

#include <lib/subghz/devices/preset.h>
#include <lib/subghz/types.h>

typedef struct {
const char* name;
const char* short_name;
FuriHalSubGhzPreset radio_preset;
const uint8_t* custom_data;
size_t custom_data_size;
} TPMSPresetDefinition;

#define TPMS_PRESET_TOYOTA_PMVC2X_NAME "Toyota PMV-C2x"
#define TPMS_PRESET_TOYOTA_PMVC2X_FREQUENCY 433920000U

size_t tpms_preset_count(void);
const TPMSPresetDefinition* tpms_preset_get_by_index(size_t index);
const TPMSPresetDefinition* tpms_preset_get_by_name(const char* name);
const TPMSPresetDefinition* tpms_preset_get_default(void);
const char* tpms_preset_get_short_name(const char* name);
FuriHalSubGhzPreset tpms_preset_get_radio_preset(const char* name);
uint32_t tpms_preset_get_default_frequency(void);
bool tpms_preset_init_radio(
SubGhzRadioPreset* preset,
const char* preset_name,
uint32_t frequency,
uint8_t* preset_data,
size_t preset_data_size);
7 changes: 5 additions & 2 deletions helpers/tpms_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

#include <furi.h>
#include <furi_hal.h>
#include <limits.h>

#define TPMS_VERSION_APP "0.1"
#define TPMS_VERSION_APP "0.3"
#define TPMS_DEVELOPED "wosk"
#define TPMS_GITHUB "https://github.com/beewosk/flipperzero-tpms"

Expand Down Expand Up @@ -55,4 +56,6 @@ typedef enum {
typedef enum {
TPMSRelearnTypeCommon,
//TPMSRelearnAnotherOEM,
} TPMSRelearnType;
} TPMSRelearnType;

#define TPMS_RSSI_FILTER_OFF INT16_MIN
1 change: 1 addition & 0 deletions protocols/protocol_items.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const SubGhzProtocol* tpms_protocol_registry_items[] = {
&tpms_protocol_schrader_gg4,
&tpms_protocol_toyota_pmvc2x,
};

const SubGhzProtocolRegistry tpms_protocol_registry = {
Expand Down
1 change: 1 addition & 0 deletions protocols/protocol_items.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
#include "../tpms_app_i.h"

#include "schrader_gg4.h"
#include "toyota_pmvc2x.h"

extern const SubGhzProtocolRegistry tpms_protocol_registry;
Loading