askrejans/sensors-to-mqtt
An application that reads data from a wide range of sensors (I2C, GPIO) locally and over tcp bridge and publishes it to an MQTT broker. Includes an interactive terminal UI with live charts, G-force visualisation, and a systemd-ready daemon mode.
sensors-to-mqtt
Rust application that reads data from a wide range of sensors (I2C, GPIO, serial) and publishes it to an MQTT broker. Sensors can be connected locally (Linux hardware interfaces) or remotely over TCP via an io-to-net bridge — enabling monitoring from any platform including macOS and Windows. Includes an interactive terminal UI with live charts, G-force visualisation, and a systemd-ready daemon mode.
Part of the to-mqtt ecosystem — see also gps-to-mqtt and speeduino-to-mqtt.
Key Capabilities
- Broad sensor support — I2C environmental, power, motion, light, and ADC sensors; GPIO digital inputs; particulate matter (PM2.5/PM10); a synthetic test sensor (no hardware required)
- TCP bridge support — connect to any sensor remotely via an io-to-net bridge; all drivers work cross-platform over TCP
- Real-time MQTT publishing with automatic reconnection and QoS configuration
- 1-D Kalman filter on numeric fields with configurable noise/process variance and dead-zone suppression
- Interactive terminal UI — tabbed per-sensor views, live sparkline charts, G-meter canvas, keyboard navigation
- Daemon mode — auto-detected when stdout is not a TTY; structured JSON logs, systemd-compatible
- TOML configuration with environment-variable overrides (
SENSORS_TO_MQTT__*)
Supported Sensors
| Driver | Bus | Chip | Measurements |
|---|---|---|---|
mpu6500 |
I2C / TCP | InvenSense MPU-6500 | Acceleration (g), gyroscope (°/s), tilt/lean/bank angles |
bmp280 |
I2C / TCP | Bosch BMP280 | Temperature (°C), pressure (hPa), altitude (m) |
bme280 |
I2C / TCP | Bosch BME280 | Temperature (°C), pressure (hPa), humidity (%), altitude (m) |
sht31 |
I2C / TCP | Sensirion SHT31 | Temperature (°C), humidity (%) |
bh1750 |
I2C / TCP | ROHM BH1750 | Ambient light (lux), light category |
ina219 |
I2C / TCP | TI INA219 | Bus voltage (V), shunt voltage (mV), current (A), power (W), state-of-charge (%) |
ads1115 |
I2C / TCP | TI ADS1115 | 4-channel 16-bit ADC — configurable per-channel gain, sample rate, and linear scaling |
gpio_button |
GPIO / TCP | — | State (0/1), press count, press duration; software debounce |
sds011 |
Serial / TCP | Nova Fitness SDS011 | PM2.5 (μg/m³), PM10 (μg/m³), AQI (EPA) |
synthetic |
— | — | 15 simulated fields (g-force, gyro, temperature, pressure, humidity, battery, RPM, speed, throttle); sine/sawtooth waveforms |
I2C / TCP — local hardware on Linux, or remote via TCP bridge on any platform.
GPS and ECU (Speeduino) are handled by dedicated sibling projects.
Installation
Debian / Ubuntu
# Add g86racing APT repository
curl -fsSL https://g86racing.com/packages/deb/g86racing-archive-keyring.gpg \
| sudo tee /usr/share/keyrings/g86racing-archive-keyring.gpg > /dev/null
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/g86racing-archive-keyring.gpg] \
https://g86racing.com/packages/deb stable main" \
| sudo tee /etc/apt/sources.list.d/g86racing.list
sudo apt-get update
sudo apt-get install sensors-to-mqttFedora / RHEL / AlmaLinux / Rocky Linux
# Add g86racing RPM repository
sudo tee /etc/yum.repos.d/g86racing.repo <<'EOF'
[g86racing]
name=g86racing packages
baseurl=https://g86racing.com/packages/rpm
enabled=1
gpgcheck=0
EOF
sudo dnf install sensors-to-mqttmacOS (Homebrew)
brew tap g86racing/g86racing
brew install sensors-to-mqttOn macOS only TCP bridge sensors are available (no local I2C/GPIO hardware). Use io-to-net on the device with the physical sensors.
Windows
Not supported — I2C and GPIO sensors require Linux hardware. Use TCP bridge mode from a Linux/macOS machine to access remote sensors.
Docker
# Pull and run
docker pull ghcr.io/askrejans/sensors-to-mqtt:latest
# Or use docker-compose (edit docker-compose.yml for your sensor devices)
docker compose up -dQuick Start
Requirements
- Rust 1.82+ (
rustup.rs) - Local sensors (Linux): I2C enabled (
raspi-config→ Interface Options → I2C), user in thei2cgroup - Remote sensors (all platforms): io-to-net bridge running on the device with the sensors
Build and run (native)
cargo build --release
./target/release/sensors-to-mqtt --config config.tomlIf stdout is a terminal the TUI launches automatically. Pipe or redirect stdout to suppress the TUI and get structured logs instead (daemon mode).
Synthetic sensor (no hardware)
# config.toml
[[sensors]]
name = "Synthetic IMU"
driver = "synthetic"
enabled = true
[sensors.connection]
type = "i2c" # ignored by the synthetic driver — any value is accepted
device = "/dev/i2c-1"
address = 0x00Configuration Reference
Configuration is loaded in this priority order (highest wins):
SENSORS_TO_MQTT__*environment variables (double-underscore separator)- File specified with
--config <path> ./config.toml/etc/sensors-to-mqtt/config.toml- Built-in defaults
Top-level
log_level = "info" # trace | debug | info | warn | error
log_json = false # emit JSON log lines (useful in daemon mode)
tui_refresh_rate_ms = 100 # TUI redraw intervalMQTT
[mqtt]
enabled = true
host = "localhost"
port = 1883
base_topic = "/SENSORS"
client_id = "sensors-to-mqtt"
keep_alive_secs = 20
# username = "user"
# password = "pass"Sensor entries
Each [[sensors]] block configures one sensor instance.
[[sensors]]
name = "My Sensor" # used as MQTT sub-topic and TUI tab name
driver = "<driver>" # see table above
enabled = true # set false to skip without removing the block
[sensors.connection]
# see connection types below
[sensors.settings]
# driver-specific settings (all optional)Connection types
I2C (Linux only — direct hardware)
[sensors.connection]
type = "i2c"
device = "/dev/i2c-1" # default
address = 0x68 # 7-bit hex addressTCP (all platforms — connects to an io-to-net bridge)
[sensors.connection]
type = "tcp"
host = "192.168.88.58" # IP of the io-to-net bridge device
port = 9002 # port configured on the bridge
address = 0x68 # I2C device address (required for I2C sensors)
# omit for serial-over-TCP sensors (e.g. sds011)
framing = true # set true when the bridge sends length-prefixed frames
# (2-byte big-endian length header before each response)
# default: false (raw byte stream)GPIO (Linux only — sysfs)
[sensors.connection]
type = "gpio"
pin = 17 # BCM pin number
active_low = false # true = LOW means pressed/active
debounce_ms = 50 # software debounce windowSerial (Linux / macOS — USB or UART)
[sensors.connection]
type = "serial"
port = "/dev/ttyUSB0" # or /dev/ttyS0, /dev/ttyAMA0, COM3, etc.
baud_rate = 9600Sensor Configuration Examples
MPU-6500 IMU — local I2C
[[sensors]]
name = "Front IMU"
driver = "mpu6500"
[sensors.connection]
type = "i2c"
address = 0x68MPU-6500 IMU — remote TCP bridge
[[sensors]]
name = "Front IMU"
driver = "mpu6500"
[sensors.connection]
type = "tcp"
host = "192.168.88.58"
port = 9002
framing = true # required when bridge uses frame_mode = "length_prefix"
# address is not needed — the bridge handles I2C addressing internallyBridge config note: set
read_len = 14andread_reg = 0x3Bin the io-to-net bridge.
The driver reads all six axes in a single 14-byte burst (ACCEL_XYZ + TEMP + GYRO_XYZ).
Client writes are ignored by the bridge whenread_only = true.
BME280 — temperature, pressure, humidity
[[sensors]]
name = "Cabin Climate"
driver = "bme280"
[sensors.connection]
type = "tcp"
host = "192.168.88.58"
port = 9003
address = 0x76SHT31 — temperature and humidity
[[sensors]]
name = "Engine Bay Temp"
driver = "sht31"
[sensors.connection]
type = "i2c"
address = 0x44BMP280 — temperature and pressure
[[sensors]]
name = "Intake Pressure"
driver = "bmp280"
[sensors.connection]
type = "i2c"
address = 0x77BH1750 — ambient light
[[sensors]]
name = "Dashboard Light"
driver = "bh1750"
[sensors.connection]
type = "i2c"
address = 0x23 # 0x23 (ADDR=GND) or 0x5C (ADDR=VCC)INA219 — current, voltage, power
[[sensors]]
name = "Battery Monitor"
driver = "ina219"
[sensors.connection]
type = "tcp"
host = "192.168.88.58"
port = 9004
address = 0x40
[sensors.settings]
shunt_ohms = 0.1 # shunt resistor value in ohms
max_current_a = 3.2 # determines current resolution
battery_min_v = 11.0 # for state-of-charge estimation
battery_max_v = 14.4ADS1115 — 4-channel ADC (analog sensors)
Each channel has an independent linear scale: value = (volts - offset) * scale.
[[sensors]]
name = "Analog Inputs"
driver = "ads1115"
[sensors.connection]
type = "i2c"
address = 0x48 # 0x48–0x4B depending on ADDR pin
[sensors.settings]
gain = 4.096 # PGA full-scale range (V): 6.144|4.096|2.048|1.024|0.512|0.256
sample_rate = 128 # SPS: 8|16|32|64|128|250|475|860
[[sensors.settings.channels]]
index = 0
label = "Throttle"
unit = "%"
scale = 25.6 # maps 0–3.9 V → 0–100 %
offset = 0.0
[[sensors.settings.channels]]
index = 1
label = "Coolant Temp"
unit = "°C"
scale = 100.0
offset = -1.0GPIO button / switch — local
[[sensors]]
name = "Brake Light Switch"
driver = "gpio_button"
[sensors.connection]
type = "gpio"
pin = 17
active_low = false
debounce_ms = 20GPIO button / switch — remote TCP
[[sensors]]
name = "Brake Light Switch"
driver = "gpio_button"
[sensors.connection]
type = "tcp"
host = "192.168.88.58"
port = 9005Published fields: state (0.0 / 1.0), press_count, press_duration_ms.
SDS011 — PM2.5 / PM10 air quality
Supports two transports.
USB-serial — plug the SDS011 in via USB (appears as /dev/ttyUSB0).
If permission is denied: sudo usermod -aG dialout $USER.
[[sensors]]
name = "Air Quality"
driver = "sds011"
[sensors.connection]
type = "serial"
port = "/dev/ttyUSB0"
baud_rate = 9600 # always 9600 for SDS011Serial-over-IP (raw TCP) — e.g. an io-to-net bridge, ESP8266 / ESP32, or a serial device server.
[[sensors]]
name = "Air Quality (remote)"
driver = "sds011"
[sensors.connection]
type = "tcp"
host = "192.168.1.42" # address of the bridge
port = 8880 # raw-TCP port configured on the bridgePublished fields: pm2_5 (μg/m³), pm10 (μg/m³), aqi_pm2_5, aqi_pm10 (US EPA index).
Synthetic test sensor
[[sensors]]
name = "Synthetic"
driver = "synthetic"
[sensors.connection]
type = "i2c"
address = 0x00
[sensors.settings]
rate_hz = 50 # simulated sample rate
speed = 1.0 # waveform speed multiplier
noise = 0.02 # noise amplitudeTCP Bridge Setup (io-to-net)
To use sensors remotely, run io-to-net on the device that has the sensors physically connected (e.g. a Raspberry Pi):
# Install and configure io-to-net on the sensor host
# Then point sensors-to-mqtt at it from any machineEach sensor gets its own TCP port in the bridge config. The I2C address field in sensors-to-mqtt tells the bridge which device on the bus to talk to.
CLI Flags
sensors-to-mqtt [OPTIONS]
-c, --config <PATH> Config file (default: ./config.toml)
--no-mqtt Disable MQTT publishing (TUI-only mode)
--log-level Override log level (trace|debug|info|warn|error)
-h, --help Show help
TUI Keyboard Shortcuts
| Key | Action |
|---|---|
← / → or h / l |
Switch sensor tab |
↑ / ↓ or k / j |
Scroll data list |
r |
Recalibrate active sensor |
e |
Toggle sensor enabled/disabled |
q / Esc |
Quit |
? |
Toggle help overlay |
MQTT Topics
Topics follow the pattern <base_topic>/IMU/<sensor_name>/<subtopic>.
With base_topic = "/SENSORS" and name = "Front IMU":
| Topic | Payload | Description |
|---|---|---|
/SENSORS/IMU/Front IMU/INFO |
{"sensor":"…","timestamp":"…"} |
Heartbeat on every reading |
/SENSORS/IMU/Front IMU/FILTERED |
{"timestamp":"…","accel_x":…,…} |
Kalman-filtered motion fields |
/SENSORS/IMU/Front IMU/DERIVED |
{"timestamp":"…","g_force_x":…,…} |
Derived fields (G-force, tilt, etc.) |
Payloads are JSON objects. Numeric values are f64.
Systemd Service
The installed package creates a sensors system user and installs a hardened unit file.
# Enable and start
sudo systemctl enable --now sensors-to-mqtt
# View logs
sudo journalctl -u sensors-to-mqtt -f
# Check status
sudo systemctl status sensors-to-mqttCopy the example config and edit for your sensors:
sudo cp /etc/sensors-to-mqtt/settings.toml.example /etc/sensors-to-mqtt/settings.toml
sudo nano /etc/sensors-to-mqtt/settings.toml
sudo systemctl restart sensors-to-mqttI2C / GPIO permissions
The service user sensors is automatically added to the i2c, dialout, and gpio groups at install time. If you add sensors after installation, ensure the device group matches:
# Verify groups
id sensors
# Add manually if needed
sudo usermod -aG i2c sensors
sudo systemctl restart sensors-to-mqttDocker
# Build image
docker build -t sensors-to-mqtt .
# Run with docker-compose (edit docker-compose.yml first)
docker compose up -d
# View logs
docker compose logs -f sensors-to-mqttFor I2C sensors, uncomment the devices and group_add sections in docker-compose.yml and set the correct i2c group GID:
getent group i2c | cut -d: -f3 # get GIDBuilding Packages
Requires cross for Linux cross-compilation:
cargo install crossBuild all platforms:
./scripts/build_packages.shBuild specific targets:
./scripts/build_packages.sh --platform linux --arch arm64 --type deb
./scripts/build_packages.sh --platform linux --type rpm
./scripts/build_packages.sh --platform mac
./scripts/build_packages.sh --platform linux --arch x64 --no-crossPackages are written to ./release/<version>/.
Environment Variables
Any config key can be overridden with an environment variable using double-underscore as the nesting separator:
SENSORS_TO_MQTT__MQTT__HOST=192.168.1.10
SENSORS_TO_MQTT__MQTT__PORT=1884
SENSORS_TO_MQTT__LOG_LEVEL=debugAdding a New Sensor Driver
- Create
src/sensors/i2c/<driver>.rs(orsrc/sensors/gpio/<driver>.rs,src/sensors/serial/<driver>.rs) - Implement the
Sensortrait —init(),read(),field_descriptors() - For I2C drivers, use
open_i2c(cfg, default_address)fromcrate::transportto get aBox<dyn I2cBus>— this gives local I2C on Linux and TCP on all platforms automatically - Add
pub mod <driver>;in the appropriatemod.rs - Add a match arm in
src/sensors/registry.rs - Write inline unit tests in the driver file
The TUI renders fields automatically based on the VizType in each FieldDescriptor:
VizType |
Rendering |
|---|---|
Value |
Text value |
Numeric { unit } |
Value with unit suffix |
GForce |
G-meter canvas + sparkline |
AngularRate |
Sparkline (°/s) |
Angle |
Sparkline (°) |
Development
# Run all tests
cargo test
# Run with debug logging
RUST_LOG=debug cargo run
# Check for errors without building
cargo checkLicense
MIT — see LICENSE.
