Skip to content

Conversation

@error414
Copy link
Contributor

@error414 error414 commented Jan 27, 2026

User description


Recreated PR.
Original PR was closed after branch was deleted/recreated.
Last commit preserved: 28b6ed8
:(

Note: this PR needs updates in INAV Configurator

Global changes

  • added sensor scheduler, for each sensor is possible to set low and high refresh rate, low refresh rate is used if value of sensor is changed
  • rewritten smartport and crsf telemetry

SmarPort

  • old legacy sensors have been removed, for ROLL and PITCh were used sensors for TEMP .. etc..
  • new list of telemetry sensors is
  • legacy fueID sensors was preserved, setting smartportFuelUnit as well
  • it's possible to amend next sensors to sensor ID 0x51XX which is reserved for DYI sensor, in ethos must be added manualy, but it's possible use them in lua scripts

Ethos DIY sensors, how to add

it's native Ethos functionality, no needed to use any extra LUA script
20251128_184214

Ethos sensors

20251128_184307

CRSF

  • new setting crsf_telemetry_mode, can be set to native (uses CRSF native telemetry), or to CUSTOM, for CUSTOM telemetry sensors are sensors transfered via CRSF frame 0x88, in the transmitter must run lua background script which translates custom sensors to native EdgeTX sensors, or it's possible to parse 0x88 frame directly in LUA script of third sides.
  • new settings crsf_telemetry_link_rate and crsf_telemetry_link_ratio it's if they match with ELRS settings, but it's not mandatory.
  • list of NATIVE sensors
  • list of CUSTOM sensros
  • example how could background lua script looks like snztest.lua
  • for 30 CUSTOMCRSF telemetry sensors is nice to set ELRS telemetry at least to 1500bauds

CSFR CUSTOMtelemetry sensors

20251127_181058

CSFR NATIVE telemetry sensors

20251127_181517

For considering

  • I would prefer to keep as possible sensors for smartport and custom CRSF sensors as same as possible
  • second question if we need extra settings for each sensor, ON/OFF

Need to do


PR Type

Enhancement


Description

This description is generated by an AI tool. It may have inaccuracies

  • Refactored CRSF and SmartPort telemetry systems with significant architectural improvements
  • Added sensor scheduler allowing configurable low and high refresh rates based on sensor value changes
  • SmartPort changes:
    • Removed legacy sensors that were repurposed (ROLL, PITCH using TEMP sensor IDs)
    • Implemented new standardized telemetry sensor list
    • Preserved legacy fuel ID sensors with smartportFuelUnit setting
    • Added support for DIY sensors using reserved sensor ID 0x51XX for custom implementations
    • DIY sensors can be manually added in Ethos and used in Lua scripts
  • CRSF changes:
    • Added new crsf_telemetry_mode setting supporting NATIVE (CRSF native telemetry) and CUSTOM modes
    • CUSTOM mode transfers sensors via CRSF frame 0x88, requiring Lua background script translation in transmitter
    • Added crsf_telemetry_link_rate and crsf_telemetry_link_ratio settings for ELRS synchronization
    • Implemented separate lists for NATIVE and CUSTOM telemetry sensors
    • Recommended ELRS telemetry rate of at least 1500 bauds for 30 CUSTOM CRSF sensors
  • Maintains consistency between SmartPort and CUSTOM CRSF sensor implementations where possible
  • Requires corresponding updates in INAV Configurator, OpenTX-Telemetry-Widget, and ETHOS-Telemetry-Dashboard

Diagram Walkthrough

flowchart LR
  A["Sensor Scheduler"] --> B["SmartPort Telemetry"]
  A --> C["CRSF Telemetry"]
  B --> D["New Sensor List"]
  B --> E["DIY Sensors 0x51XX"]
  C --> F["NATIVE Mode"]
  C --> G["CUSTOM Mode 0x88"]
  F --> H["Native Sensors"]
  G --> I["Custom Sensors + Lua Script"]
Loading

File Walkthrough

Relevant files

* Telemetry scheduler for CRSF and SmartPort
@github-actions
Copy link

Branch Targeting Suggestion

You've targeted the master branch with this PR. Please consider if a version branch might be more appropriate:

  • maintenance-9.x - If your change is backward-compatible and won't create compatibility issues between INAV firmware and Configurator 9.x versions. This will allow your PR to be included in the next 9.x release.

  • maintenance-10.x - If your change introduces compatibility requirements between firmware and configurator that would break 9.x compatibility. This is for PRs which will be included in INAV 10.x

If master is the correct target for this change, no action is needed.


This is an automated suggestion to help route contributions to the appropriate branch.

@qodo-code-review
Copy link
Contributor

ⓘ Your approaching your monthly quota for Qodo. Upgrade your plan

PR Compliance Guide 🔍

All compliance sections have been disabled in the configurations.

Comment on lines +292 to +316
void crsfSensorEncodeEscRpm(telemetrySensor_t *sensor, sbuf_t *buf)
{
UNUSED(sensor);
uint8_t motorCount = MAX(getMotorCount(), 1); //must send at least one motor, to avoid CRSF frame shifting
motorCount = MIN(getMotorCount(), CRSF_PAYLOAD_SIZE_MAX / 3); // 3 bytes per RPM value
motorCount = MIN(motorCount, MAX_SUPPORTED_MOTORS); // ensure we don't exceed available ESC telemetry data

for (uint8_t i = 0; i < motorCount; i++) {
const escSensorData_t *escState = getEscTelemetry(i);
crsfSerialize24BE(buf, escState->rpm & 0xFFFFFF);
}
return frameSize;
}

void crsfSensorEncodeEscTemp(telemetrySensor_t *sensor, sbuf_t *buf)
{
UNUSED(sensor);
uint8_t motorCount = MAX(getMotorCount(), 1); //must send at least one motor, to avoid CRSF frame shifting
motorCount = MIN(getMotorCount(), CRSF_PAYLOAD_SIZE_MAX / 3); // 3 bytes per RPM value
motorCount = MIN(motorCount, MAX_SUPPORTED_MOTORS); // ensure we don't exceed available ESC telemetry data

for (uint8_t i = 0; i < motorCount; i++) {
const escSensorData_t *escState = getEscTelemetry(i);
crsfSerialize16BE(buf, escState->temperature & 0xFFFF);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Add a null check for the escState pointer in crsfSensorEncodeEscRpm and crsfSensorEncodeEscTemp to prevent a potential crash before dereferencing it. [possible issue, importance: 8]

Suggested change
void crsfSensorEncodeEscRpm(telemetrySensor_t *sensor, sbuf_t *buf)
{
UNUSED(sensor);
uint8_t motorCount = MAX(getMotorCount(), 1); //must send at least one motor, to avoid CRSF frame shifting
motorCount = MIN(getMotorCount(), CRSF_PAYLOAD_SIZE_MAX / 3); // 3 bytes per RPM value
motorCount = MIN(motorCount, MAX_SUPPORTED_MOTORS); // ensure we don't exceed available ESC telemetry data
for (uint8_t i = 0; i < motorCount; i++) {
const escSensorData_t *escState = getEscTelemetry(i);
crsfSerialize24BE(buf, escState->rpm & 0xFFFFFF);
}
return frameSize;
}
void crsfSensorEncodeEscTemp(telemetrySensor_t *sensor, sbuf_t *buf)
{
UNUSED(sensor);
uint8_t motorCount = MAX(getMotorCount(), 1); //must send at least one motor, to avoid CRSF frame shifting
motorCount = MIN(getMotorCount(), CRSF_PAYLOAD_SIZE_MAX / 3); // 3 bytes per RPM value
motorCount = MIN(motorCount, MAX_SUPPORTED_MOTORS); // ensure we don't exceed available ESC telemetry data
for (uint8_t i = 0; i < motorCount; i++) {
const escSensorData_t *escState = getEscTelemetry(i);
crsfSerialize16BE(buf, escState->temperature & 0xFFFF);
}
}
void crsfSensorEncodeEscRpm(telemetrySensor_t *sensor, sbuf_t *buf)
{
UNUSED(sensor);
uint8_t motorCount = MAX(getMotorCount(), 1); //must send at least one motor, to avoid CRSF frame shifting
motorCount = MIN(getMotorCount(), CRSF_PAYLOAD_SIZE_MAX / 3); // 3 bytes per RPM value
motorCount = MIN(motorCount, MAX_SUPPORTED_MOTORS); // ensure we don't exceed available ESC telemetry data
for (uint8_t i = 0; i < motorCount; i++) {
const escSensorData_t *escState = getEscTelemetry(i);
crsfSerialize24BE(buf, escState ? (escState->rpm & 0xFFFFFF) : 0);
}
}
void crsfSensorEncodeEscTemp(telemetrySensor_t *sensor, sbuf_t *buf)
{
UNUSED(sensor);
uint8_t motorCount = MAX(getMotorCount(), 1); //must send at least one motor, to avoid CRSF frame shifting
motorCount = MIN(getMotorCount(), CRSF_PAYLOAD_SIZE_MAX / 3); // 3 bytes per RPM value
motorCount = MIN(motorCount, MAX_SUPPORTED_MOTORS); // ensure we don't exceed available ESC telemetry data
for (uint8_t i = 0; i < motorCount; i++) {
const escSensorData_t *escState = getEscTelemetry(i);
crsfSerialize16BE(buf, escState ? (escState->temperature & 0xFFFF) : 0);
}
}

Comment on lines +156 to +176
static void smartPortSensorEncodeLat(__unused telemetrySensor_t *sensor, smartPortPayload_t *payload)
{
uint32_t lat = (uint32_t)(abs(gpsSol.llh.lat) * 6) / 100; //danger zone, may overflow for latitudes > 3579139.2 degrees, so convert int from abs function to uint32_t

if (gpsSol.llh.lat < 0) {
lat |= BIT(30);
}

payload->data = lat;
}

static void smartPortSensorEncodeLon(__unused telemetrySensor_t *sensor, smartPortPayload_t *payload)
{
uint32_t lon = (uint32_t)(abs(gpsSol.llh.lon) * 6) / 100; //danger zone, may overflow for longitudes > 3579139.2 degrees , so convert int from abs function to uint32_t

if (gpsSol.llh.lon < 0) {
lon |= BIT(30);
}

payload->data = lon | BIT(31);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Use floating-point arithmetic for GPS coordinate calculations in smartPortSensorEncodeLat and smartPortSensorEncodeLon to avoid precision loss. [general, importance: 7]

Suggested change
static void smartPortSensorEncodeLat(__unused telemetrySensor_t *sensor, smartPortPayload_t *payload)
{
uint32_t lat = (uint32_t)(abs(gpsSol.llh.lat) * 6) / 100; //danger zone, may overflow for latitudes > 3579139.2 degrees, so convert int from abs function to uint32_t
if (gpsSol.llh.lat < 0) {
lat |= BIT(30);
}
payload->data = lat;
}
static void smartPortSensorEncodeLon(__unused telemetrySensor_t *sensor, smartPortPayload_t *payload)
{
uint32_t lon = (uint32_t)(abs(gpsSol.llh.lon) * 6) / 100; //danger zone, may overflow for longitudes > 3579139.2 degrees , so convert int from abs function to uint32_t
if (gpsSol.llh.lon < 0) {
lon |= BIT(30);
}
payload->data = lon | BIT(31);
}
static void smartPortSensorEncodeLat(__unused telemetrySensor_t *sensor, smartPortPayload_t *payload)
{
uint32_t lat = (uint32_t)(fabsf(gpsSol.llh.lat) * 0.06f);
if (gpsSol.llh.lat < 0) {
lat |= BIT(30);
}
payload->data = lat;
}
static void smartPortSensorEncodeLon(__unused telemetrySensor_t *sensor, smartPortPayload_t *payload)
{
uint32_t lon = (uint32_t)(fabsf(gpsSol.llh.lon) * 0.06f);
if (gpsSol.llh.lon < 0) {
lon |= BIT(30);
}
payload->data = lon | BIT(31);
}

Comment on lines +277 to +293
static void smartPortSensorEncodeLat(__unused telemetrySensor_t *sensor, smartPortPayload_t *payload)
{
uint32_t tmpui = abs(gpsSol.llh.lon); // now we have unsigned value and one bit to spare
tmpui = (tmpui + tmpui / 2) / 25 | 0x80000000; // 6/100 = 1.5/25, division by power of 2 is fast
if (gpsSol.llh.lon < 0) tmpui |= 0x40000000;

payload->data = tmpui;
}

static void smartPortSensorEncodeLon(__unused telemetrySensor_t *sensor, smartPortPayload_t *payload)
{
uint32_t tmpui = abs(gpsSol.llh.lat); // now we have unsigned value and one bit to spare
tmpui = (tmpui + tmpui / 2) / 25; // 6/100 = 1.5/25, division by power of 2 is fast
if (gpsSol.llh.lat < 0) tmpui |= 0x40000000;

payload->data = tmpui;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Swap the use of gpsSol.llh.lon and gpsSol.llh.lat in the smartPortSensorEncodeLat and smartPortSensorEncodeLon functions to correctly encode GPS coordinates. [possible issue, importance: 9]

Suggested change
static void smartPortSensorEncodeLat(__unused telemetrySensor_t *sensor, smartPortPayload_t *payload)
{
uint32_t tmpui = abs(gpsSol.llh.lon); // now we have unsigned value and one bit to spare
tmpui = (tmpui + tmpui / 2) / 25 | 0x80000000; // 6/100 = 1.5/25, division by power of 2 is fast
if (gpsSol.llh.lon < 0) tmpui |= 0x40000000;
payload->data = tmpui;
}
static void smartPortSensorEncodeLon(__unused telemetrySensor_t *sensor, smartPortPayload_t *payload)
{
uint32_t tmpui = abs(gpsSol.llh.lat); // now we have unsigned value and one bit to spare
tmpui = (tmpui + tmpui / 2) / 25; // 6/100 = 1.5/25, division by power of 2 is fast
if (gpsSol.llh.lat < 0) tmpui |= 0x40000000;
payload->data = tmpui;
}
static void smartPortSensorEncodeLat(__unused telemetrySensor_t *sensor, smartPortPayload_t *payload)
{
uint32_t tmpui = abs(gpsSol.llh.lat); // now we have unsigned value and one bit to spare
tmpui = (tmpui + tmpui / 2) / 25 | 0x80000000; // 6/100 = 1.5/25, division by power of 2 is fast
if (gpsSol.llh.lat < 0) tmpui |= 0x40000000;
payload->data = tmpui;
}
static void smartPortSensorEncodeLon(__unused telemetrySensor_t *sensor, smartPortPayload_t *payload)
{
uint32_t tmpui = abs(gpsSol.llh.lon); // now we have unsigned value and one bit to spare
tmpui = (tmpui + tmpui / 2) / 25; // 6/100 = 1.5/25, division by power of 2 is fast
if (gpsSol.llh.lon < 0) tmpui |= 0x40000000;
payload->data = tmpui;
}

Comment on lines +133 to +134
case TELEM_GPS_AZIMUTH:
return ((GPS_directionToHome < 0 ? GPS_directionToHome + 360 : GPS_directionToHome) + 180) % 360;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Remove the addition of 180 degrees from the TELEM_GPS_AZIMUTH calculation to report the correct direction to home. [possible issue, importance: 8]

Suggested change
case TELEM_GPS_AZIMUTH:
return ((GPS_directionToHome < 0 ? GPS_directionToHome + 360 : GPS_directionToHome) + 180) % 360;
case TELEM_GPS_AZIMUTH:
return GPS_directionToHome < 0 ? GPS_directionToHome + 360 : GPS_directionToHome;

Comment on lines +285 to +305
void telemetryScheduleUpdate(timeUs_t currentTime)
{
timeDelta_t delta = cmpTimeUs(currentTime, sch.update_time);

for (int i = 0; i < sch.sensor_count; i++) {
telemetrySensor_t * sensor = &sch.sensors[i];
if (sensor->active) {
int value = telemetrySensorValue(sensor->sensor_id);
if (sensor->ratio_den)
value = value * sensor->ratio_num / sensor->ratio_den;
sensor->update |= (value != sensor->value);
sensor->value = value;

const int interval = (sensor->update) ? sensor->fast_interval : sensor->slow_interval;
sensor->bucket += delta * 1000 / interval;
sensor->bucket = constrain(sensor->bucket, sch.min_level, sch.max_level);
}
}

sch.update_time = currentTime;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: In telemetryScheduleUpdate, add a check to ensure interval is greater than zero before performing division to prevent a potential division-by-zero crash. [possible issue, importance: 8]

Suggested change
void telemetryScheduleUpdate(timeUs_t currentTime)
{
timeDelta_t delta = cmpTimeUs(currentTime, sch.update_time);
for (int i = 0; i < sch.sensor_count; i++) {
telemetrySensor_t * sensor = &sch.sensors[i];
if (sensor->active) {
int value = telemetrySensorValue(sensor->sensor_id);
if (sensor->ratio_den)
value = value * sensor->ratio_num / sensor->ratio_den;
sensor->update |= (value != sensor->value);
sensor->value = value;
const int interval = (sensor->update) ? sensor->fast_interval : sensor->slow_interval;
sensor->bucket += delta * 1000 / interval;
sensor->bucket = constrain(sensor->bucket, sch.min_level, sch.max_level);
}
}
sch.update_time = currentTime;
}
void telemetryScheduleUpdate(timeUs_t currentTime)
{
timeDelta_t delta = cmpTimeUs(currentTime, sch.update_time);
for (int i = 0; i < sch.sensor_count; i++) {
telemetrySensor_t * sensor = &sch.sensors[i];
if (sensor->active) {
int value = telemetrySensorValue(sensor->sensor_id);
if (sensor->ratio_den)
value = value * sensor->ratio_num / sensor->ratio_den;
sensor->update |= (value != sensor->value);
sensor->value = value;
const int interval = (sensor->update) ? sensor->fast_interval : sensor->slow_interval;
if (interval > 0) {
sensor->bucket += delta * 1000 / interval;
}
sensor->bucket = constrain(sensor->bucket, sch.min_level, sch.max_level);
}
}
sch.update_time = currentTime;
}

Comment on lines +374 to +375
TLM_SENSOR(LEGACY_LAT , 0x0800, 200, 200, 0, 0, 0, Lat),
TLM_SENSOR(LEGACY_LON , 0x0800, 200, 200, 0, 0, 0, Lon),
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Assign a unique app_id to LEGACY_LON to prevent a collision with LEGACY_LAT in the SmartPort sensor definitions. [possible issue, importance: 9]

Suggested change
TLM_SENSOR(LEGACY_LAT , 0x0800, 200, 200, 0, 0, 0, Lat),
TLM_SENSOR(LEGACY_LON , 0x0800, 200, 200, 0, 0, 0, Lon),
TLM_SENSOR(LEGACY_LAT , 0x0800, 200, 200, 0, 0, 0, Lat),
TLM_SENSOR(LEGACY_LON , 0x0810, 200, 200, 0, 0, 0, Lon),

Comment on lines +215 to +222
static void smartPortSensorEncodeFuel(__unused telemetrySensor_t *sensor, smartPortPayload_t *payload)
{
if (telemetryConfig()->smartportFuelUnit == SMARTPORT_FUEL_UNIT_PERCENT) {
payload->data = calculateBatteryPercentage(); // Show remaining battery % if smartport_fuel_percent=ON
} else if (isAmperageConfigured()) {
payload->data = telemetryConfig()->smartportFuelUnit == SMARTPORT_FUEL_UNIT_MAH ? getMAhDrawn() : getMWhDrawn();
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: In smartPortSensorEncodeFuel, add an else block to initialize payload->data to 0, preventing it from being uninitialized if no fuel reporting method is configured. [general, importance: 6]

Suggested change
static void smartPortSensorEncodeFuel(__unused telemetrySensor_t *sensor, smartPortPayload_t *payload)
{
if (telemetryConfig()->smartportFuelUnit == SMARTPORT_FUEL_UNIT_PERCENT) {
payload->data = calculateBatteryPercentage(); // Show remaining battery % if smartport_fuel_percent=ON
} else if (isAmperageConfigured()) {
payload->data = telemetryConfig()->smartportFuelUnit == SMARTPORT_FUEL_UNIT_MAH ? getMAhDrawn() : getMWhDrawn();
}
}
static void smartPortSensorEncodeFuel(__unused telemetrySensor_t *sensor, smartPortPayload_t *payload)
{
if (telemetryConfig()->smartportFuelUnit == SMARTPORT_FUEL_UNIT_PERCENT) {
payload->data = calculateBatteryPercentage();
} else if (isAmperageConfigured()) {
payload->data = telemetryConfig()->smartportFuelUnit == SMARTPORT_FUEL_UNIT_MAH ? getMAhDrawn() : getMWhDrawn();
} else {
payload->data = 0;
}
}

const float rate = telemetryConfig()->crsf_telemetry_link_rate;
const float ratio = telemetryConfig()->crsf_telemetry_link_ratio;

crsfTelemetryRateQuanta = rate / (ratio * 1000000);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Add validation to prevent divide-by-zero when ratio is zero. Check that ratio > 0 before performing the division, and provide a fallback value or early return if invalid. [Learned best practice, importance: 6]

Suggested change
crsfTelemetryRateQuanta = rate / (ratio * 1000000);
if (ratio > 0) {
crsfTelemetryRateQuanta = rate / (ratio * 1000000);
} else {
crsfTelemetryRateQuanta = 0;
}

@error414 error414 marked this pull request as draft January 27, 2026 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant