Sensors: entries can now name AirQualityMetrics fields (pm25_standard,
pm100_standard, particle counts, co2, formaldehyde, VOC/NOx index, ...)
as well as EnvironmentMetrics fields.
- CommandSensorFields gains an AirQualityMetrics table and messageFor(),
which says which telemetry message a Type belongs to. No field name
appears in both. pm_status_flags is left out: it is a status bitmask,
not a measurement.
- Each telemetry module owns one CommandSensor that runs only the entries
for its message, so no command runs twice. The air-quality module
registers its instance once (its i2cScanFinished() runs twice) and
first, so it is read last and wins a shared field.
- Air-quality readings follow the air-quality module's own enable switch
and interval. The boot warning now counts unread entries per module.
- --check's info line and config-dist.yaml say which setting each kind
of type needs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011C7D9dE4aUYBrGdpPyHs25
CommandSensor ran its commands from getMetrics(), on the main loop that
also drives the radio. Each expired sensor could stall the loop for up to
its 2 s timeout, and on Windows, where popen() has no timeout, a command
that never exits froze the node.
- A worker thread re-runs each command every 30 s and stores the result.
getMetrics() only reads the latest results under a mutex, so it never
waits on a command.
- The worker never calls LOG_* (portduino's log buffer is not
thread-safe). It records why a read failed, and getMetrics() logs each
new result on the main thread, as NativeDiscovery does.
- execCommand() returns an ExecResult instead of logging. exec() logs
timeouts and start failures for its main-thread callers (HostMetrics).
- The sensor copies its config at init and joins the worker on
destruction.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011C7D9dE4aUYBrGdpPyHs25
- Read the command sensor after the INA219/260/3221, MAX17048 and RAK
readers in getEnvironmentTelemetry(). Those ran after the sensors list
and overwrote a configured voltage or current command.
- Drop a reading whose scaled value is not finite (value * Scale can
overflow), so inf is never sent and never reaches an integer cast.
- A non-finite Scale (.inf, .nan) falls back to 1; --check warns.
- An empty `Sensors:` is skipped like any other empty section. A normal
boot rejected it as "not a list" while --check called it clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011C7D9dE4aUYBrGdpPyHs25
Add a Sensors: list to config.yaml. Each entry names an EnvironmentMetrics
field and a shell command that prints one number:
Sensors:
- Type: temperature
Command: cat /sys/bus/iio/devices/iio:device0/in_temp_input
Scale: 0.001
- CommandSensor plugs into EnvironmentTelemetryModule like the T1000x
sensor (no I2C device). Readings are cached for 30s, since getMetrics()
also runs for screen refreshes and phone requests. Output that is not
exactly one finite number is logged and the field is left unset.
Integer fields are rounded and clamped.
- Type matches the protobuf field name, case-insensitively. An unknown
Type or missing Command stops meshtasticd, as Security.AdminKeys does.
Entries append across config.d files.
- exec() now has a timeout: commands run in their own process group,
stdin is /dev/null, and the whole group is killed at the deadline, so
a hung script stalls the main loop for a bounded time. Command sensors
get 2s; HostMetrics and other exec() callers get 10s. Output is capped
at 4 KiB. Windows keeps popen() without a timeout.
- --check validates the section (type, command, scale, unknown keys,
duplicate types) and notes that sensors need environment measurement
enabled. Boot logs a warning when sensors are configured but
environment measurement is off. The section does not enable telemetry
itself, because enabling it also broadcasts to the mesh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011C7D9dE4aUYBrGdpPyHs25