Objective and use case
What you’ll build: A Jetson-based edge node that reads BME680 (temp/humidity/pressure/gas) via I2C and PMS5003 (PM1.0/PM2.5/PM10) via UART, then publishes a JSON message over MQTT every 5 seconds. Includes timestamping, simple baselining for VOC proxy, and reconnect logic to ensure reliable delivery.
Why it matters / Use cases
- Facility monitoring: Track PM2.5 and VOC trends in workshops/server rooms; auto-trigger ventilation when PM2.5 > 35 µg/m³ for 3 consecutive samples (typical LAN actuation latency < 500 ms).
- Home automation: Publish to a local broker for Home Assistant; turn on a purifier when BME680 gas resistance drops > 30% from baseline or PM2.5 spikes > 25 µg/m³ for > 10 s.
- Classroom/office safety: Alert on rising VOC proxy and sustained PM2.5 > 12 µg/m³; indicate inadequate ventilation during occupancy windows.
- Edge analytics: Keep operating offline with local aggregation; bridge MQTT to the cloud when connectivity returns, preserving timestamps.
- Rapid prototyping: Reserve GPU for future TensorRT add-ons (e.g., smoke/anomaly detection) running 30–60 FPS at < 20% GPU on modern Jetson while telemetry remains unaffected.
Expected outcome
- Stable MQTT publish every 5 s (jitter ±100 ms) with QoS 1 under a topic like env/air/telemetry; JSON payload ~180–300 bytes.
- Sensor pipeline latency < 250 ms per cycle (PMS5003 frame parse < 20 ms; BME680 sample 100–200 ms); local broker round-trip < 10 ms on LAN.
- Resource use on Jetson: < 5% CPU, ~0% GPU for sensing-only; optional TensorRT add-on runs concurrently at 30–60 FPS with < 20% GPU and < 5 ms added latency to the MQTT path.
- Simple alerts on a secondary topic (e.g., env/air/alerts) when thresholds are exceeded; 24 h test shows > 99% successful publishes with automatic reconnect/backoff.
Audience: IoT/embedded engineers, makers, facilities teams; Level: Intermediate
Architecture/flow: BME680 (I2C) + PMS5003 (UART) → Python polling loop (Adafruit/Bosch libs) with VOC baselining → JSON pack → paho-mqtt publish to local Mosquitto → consumers (Home Assistant/Node-RED/cloud via MQTT bridge); runs as a systemd service with offline buffering and watchdog; optional GStreamer+TensorRT pipeline on a separate thread for future AI.
Prerequisites
- OS and SDK: NVIDIA Jetson Nano 2GB Developer Kit running JetPack (L4T) on Ubuntu.
- Network: Jetson reachable over LAN/Wi‑Fi with internet access for package downloads.
- Permissions: A user with sudo rights.
Verify your JetPack, kernel and NVIDIA packages:
cat /etc/nv_tegra_release
uname -a
dpkg -l | grep -E 'nvidia|tensorrt'
Optional helper (if installed):
jetson_release -v
Materials (with exact model)
- NVIDIA Jetson Nano 2GB Developer Kit
- Adafruit BME680 – Temperature, Humidity, Pressure and Gas Sensor Breakout (default I2C address 0x77)
- Plantower PMS5003 laser dust sensor with cable
- Breadboard/jumper wires (female–female for Jetson 40‑pin header)
- USB-C power supply for Jetson (recommended ≥ 3A)
- Optional: CSI camera (Raspberry Pi Camera v2) for camera pipeline test
Setup/Connection
Hardware wiring (textual)
- BME680 (I2C):
- Power the BME680 from Jetson 3.3V.
- Connect SDA to Jetson SDA1 (GPIO2, pin 3) and SCL to SCL1 (GPIO3, pin 5).
-
Keep wiring short; use common ground.
-
PMS5003 (UART + 5V power):
- The PMS5003 requires 5V supply (typically 100 mA–200 mA while sampling).
- Use Jetson 5V pin to power the sensor; connect GND to Jetson ground.
- Connect PMS5003 TX to Jetson RX (pin 10), and PMS5003 RX to Jetson TX (pin 8). Logic levels are 3.3V TTL and are compatible.
- Leave SET and RESET pins unconnected (or tie SET high for continuous measurement, which is the default for many breakouts).
Pin mapping summary:
| Device signal | PMS5003 pin | BME680 pin | Jetson Nano 40-pin | Jetson pin number | Notes |
|---|---|---|---|---|---|
| 5V | 1 | — | 5V | 2 (or 4) | PMS5003 power |
| 3.3V | — | VIN | 3.3V | 1 | BME680 power |
| GND | 2 | GND | GND | 6 (or 9,14,20,25,30,34,39) | Common ground |
| UART TX (to Jetson RX) | 3 | — | UART RXD | 10 | PMS5003 → Jetson |
| UART RX (from Jetson TX) | 4 | — | UART TXD | 8 | Not strictly required for reading; leave connected or unused |
| I2C SDA | — | SDA | I2C1 SDA | 3 | BME680 SDA |
| I2C SCL | — | SCL | I2C1 SCL | 5 | BME680 SCL |
| SET | 5 | — | — | — | Optional (low=standby) |
| RESET | 6 | — | — | — | Optional |
Notes:
– Ensure PMS5003 receives 5V from a stable supply; the Jetson 5V pin is okay if your overall budget allows it. If unsure, use an external 5V supply that shares ground with the Jetson.
– BME680 default I2C address is 0x77 (Adafruit board). If you strap SDO to GND it becomes 0x76.
Enable and check buses
- I2C is enabled by default on Jetson Nano 2GB for the 40‑pin header. Confirm the BME680 presence:
sudo apt update
sudo apt install -y i2c-tools python3-smbus
sudo usermod -aG i2c,dialout $USER
newgrp i2c
i2cdetect -y -r 1
# Expect to see 0x77 (or 0x76 if SDO grounded)
- Confirm PMS5003 UART is present:
ls -l /dev/ttyTHS1
# Configure and peek raw bytes (should see non-zero data):
sudo stty -F /dev/ttyTHS1 9600 cs8 -cstopb -parenb
sudo head -c 64 /dev/ttyTHS1 | hexdump -C
You should occasionally observe frames beginning with 0x42 0x4D.
Full Code
Create a Python script that:
– Reads the BME680 over I2C using Adafruit CircuitPython drivers.
– Reads PMS5003 frames over /dev/ttyTHS1 at 9600 8N1, with checksum verification.
– Derives a simple VOC_index (proxy) from BME680 gas resistance, normalized to a running baseline.
– Publishes JSON messages to a local MQTT broker.
Save as ~/env-air-quality-mqtt/env_air_mqtt.py:
#!/usr/bin/env python3
import os
import time
import json
import logging
import socket
from datetime import datetime
import serial
import board
import adafruit_bme680
import paho.mqtt.client as mqtt
# ---- User configuration (env overrides) ----
MQTT_HOST = os.getenv("MQTT_HOST", "127.0.0.1")
MQTT_PORT = int(os.getenv("MQTT_PORT", "1883"))
MQTT_USER = os.getenv("MQTT_USER", "")
MQTT_PASS = os.getenv("MQTT_PASS", "")
MQTT_BASE = os.getenv("MQTT_BASE", "jetsonnano/env")
PUBLISH_PERIOD = float(os.getenv("PUBLISH_PERIOD", "5.0")) # seconds
BME680_ADDR = int(os.getenv("BME680_ADDR", "0x77"), 16) # 0x77 or 0x76
PMS_TTY = os.getenv("PMS_TTY", "/dev/ttyTHS1")
# VOC proxy index calibration
VOC_WARMUP_S = int(os.getenv("VOC_WARMUP_S", "60")) # gather baseline for N seconds
VOC_MIN = float(os.getenv("VOC_MIN", "0.0"))
VOC_MAX = float(os.getenv("VOC_MAX", "500.0"))
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
def init_bme680(addr=BME680_ADDR):
i2c = board.I2C() # uses Jetson's I2C-1 (pins 3/5)
sensor = adafruit_bme680.Adafruit_BME680_I2C(i2c, address=addr)
# Optional: adjust heater profile for gas; defaults are fine for proxy index
sensor.sea_level_pressure = 1013.25
logging.info("BME680 ready at 0x%02X", addr)
return sensor
def init_pms5003(port=PMS_TTY):
ser = serial.Serial(port=port, baudrate=9600, bytesize=8,
parity=serial.PARITY_NONE, stopbits=1, timeout=2)
logging.info("PMS5003 serial open on %s", port)
return ser
def read_pms5003(ser):
"""
Robust frame reader with checksum.
Plantower PMS5003 frame structure:
Header: 0x42 0x4D
Length: 2 bytes (big-endian), usually 28
Payload: 28 bytes
Checksum: 2 bytes (big-endian), sum of header+length+payload
Atmospheric PM values:
pm1.0_atm = payload[6:8]
pm2.5_atm = payload[8:10]
pm10_atm = payload[10:12]
Returns dictionary with pm1_0, pm2_5, pm10 (µg/m³).
"""
# find header
while True:
b = ser.read(1)
if b != b'\x42':
continue
b2 = ser.read(1)
if b2 != b'\x4d':
continue
frame = ser.read(32) # read enough to include len(2) + payload(28) + checksum(2)
if len(frame) < 32:
continue
length = (frame[0] << 8) | frame[1]
if length != 28:
continue
payload = frame[2:2+28]
checksum_rx = (frame[30] << 8) | frame[31]
checksum_calc = 0x42 + 0x4D + sum(frame[0:30]) # 2 len bytes + 28 payload bytes
if (checksum_calc & 0xFFFF) != checksum_rx:
logging.warning("PMS checksum mismatch")
continue
pm1_atm = (payload[6] << 8) | payload[7]
pm25_atm = (payload[8] << 8) | payload[9]
pm10_atm = (payload[10] << 8) | payload[11]
return {
"pm1_0": pm1_atm,
"pm2_5": pm25_atm,
"pm10": pm10_atm
}
def connect_mqtt():
client = mqtt.Client(client_id=f"jetsonnano-{socket.gethostname()}")
if MQTT_USER:
client.username_pw_set(MQTT_USER, MQTT_PASS)
client.connect(MQTT_HOST, MQTT_PORT, keepalive=60)
return client
class VOCIndex:
"""
Simple VOC proxy index based on BME680 gas resistance (ohms).
Not a replacement for Bosch BSEC IAQ; used for trend/relative indication.
Index (0..500): higher is better (cleaner air).
"""
def __init__(self, warmup_s=60):
self.started = time.time()
self.warmup_s = warmup_s
self.baseline = None
self.min_r = None
self.max_r = None
def update(self, gas_res_ohm):
# track min/max during warmup to establish dynamic range
if self.min_r is None or gas_res_ohm < self.min_r:
self.min_r = gas_res_ohm
if self.max_r is None or gas_res_ohm > self.max_r:
self.max_r = gas_res_ohm
if (time.time() - self.started) < self.warmup_s:
self.baseline = gas_res_ohm if self.baseline is None else (0.9 * self.baseline + 0.1 * gas_res_ohm)
return None # still warming up
# Normalize current reading between observed min/max
span = max(1.0, (self.max_r - self.min_r))
norm = (gas_res_ohm - self.min_r) / span # 0..1 scale
# Scale to 0..500, higher = better (more resistance indicates fewer VOCs)
idx = 500.0 * norm
# Clamp
return max(VOC_MIN, min(VOC_MAX, idx))
def main():
bme = init_bme680(BME680_ADDR)
pms = init_pms5003(PMS_TTY)
voc = VOCIndex(warmup_s=VOC_WARMUP_S)
mqttc = connect_mqtt()
logging.info("MQTT connected to %s:%d base=%s", MQTT_HOST, MQTT_PORT, MQTT_BASE)
while True:
try:
# BME680 read
temp_c = float(bme.temperature)
humidity = float(bme.relative_humidity)
pressure_hpa = float(bme.pressure)
gas_ohm = float(bme.gas) # raw gas resistance in ohms
gas_kohm = gas_ohm / 1000.0
# PMS5003 read
pms_values = read_pms5003(pms)
pm1 = int(pms_values["pm1_0"])
pm25 = int(pms_values["pm2_5"])
pm10 = int(pms_values["pm10"])
# VOC proxy
voc_idx = voc.update(gas_ohm)
ts = datetime.utcnow().isoformat() + "Z"
payload = {
"timestamp": ts,
"metrics": {
"temperature_c": round(temp_c, 2),
"humidity_percent": round(humidity, 2),
"pressure_hpa": round(pressure_hpa, 2),
"gas_kohm": round(gas_kohm, 2),
"voc_index": None if voc_idx is None else round(voc_idx, 1),
"pm1_0_ugm3": pm1,
"pm2_5_ugm3": pm25,
"pm10_ugm3": pm10
},
"device": {
"model": "NVIDIA Jetson Nano 2GB Developer Kit + Adafruit BME680 + Plantower PMS5003",
"host": socket.gethostname()
}
}
# Publish combined metrics and per-sensor topics
mqttc.publish(f"{MQTT_BASE}/metrics", json.dumps(payload), qos=1, retain=False)
mqttc.publish(f"{MQTT_BASE}/pm", json.dumps({"timestamp": ts, "pm1_0": pm1, "pm2_5": pm25, "pm10": pm10}), qos=0)
mqttc.publish(f"{MQTT_BASE}/bme680", json.dumps({"timestamp": ts,
"temperature_c": round(temp_c, 2),
"humidity_percent": round(humidity, 2),
"pressure_hpa": round(pressure_hpa, 2),
"gas_kohm": round(gas_kohm, 2),
"voc_index": None if voc_idx is None else round(voc_idx, 1)}), qos=0)
logging.info("PUB %s/metrics %s", MQTT_BASE, payload["metrics"])
except Exception as e:
logging.exception("Read/Publish error: %s", e)
time.sleep(PUBLISH_PERIOD)
if __name__ == "__main__":
main()
Minimal subscriber (optional) for debugging; save as ~/env-air-quality-mqtt/mqtt_sub.py:
#!/usr/bin/env python3
import sys
import paho.mqtt.client as mqtt
TOPIC = sys.argv[1] if len(sys.argv) > 1 else "jetsonnano/env/#"
HOST = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1"
def on_message(client, userdata, msg):
print(f"[{msg.topic}] {msg.payload.decode('utf-8', 'ignore')}")
c = mqtt.Client()
c.connect(HOST, 1883, 60)
c.on_message = on_message
c.subscribe(TOPIC, qos=0)
c.loop_forever()
Topic layout (for reference):
– jetsonnano/env/metrics: Combined JSON payload with all metrics.
– jetsonnano/env/pm: PM-only JSON.
– jetsonnano/env/bme680: BME680-only JSON.
Build/Flash/Run commands
No flashing; we install dependencies, configure power/perf, and run.
1) Install packages and Python libraries:
sudo apt update
sudo apt install -y python3-pip python3-venv python3-smbus i2c-tools \
mosquitto mosquitto-clients git \
python3-serial
pip3 install --user adafruit-blinka adafruit-circuitpython-bme680 paho-mqtt
2) Create project directory and copy code:
mkdir -p ~/env-air-quality-mqtt
nano ~/env-air-quality-mqtt/env_air_mqtt.py
# (paste the main script)
chmod +x ~/env-air-quality-mqtt/env_air_mqtt.py
nano ~/env-air-quality-mqtt/mqtt_sub.py
# (paste the subscriber)
chmod +x ~/env-air-quality-mqtt/mqtt_sub.py
3) Start/restart a local MQTT broker:
sudo systemctl enable mosquitto
sudo systemctl restart mosquitto
sudo systemctl status mosquitto --no-pager
4) Performance/power setup for AI test (optional but recommended for TensorRT demo):
– Query current power mode:
sudo nvpmodel -q
- Set MAXN (mode 0) and lock clocks (watch thermals; 2GB Nano needs good airflow):
sudo nvpmodel -m 0
sudo jetson_clocks
5) Run the air-quality publisher:
# Optionally set env vars; default MQTT_HOST is 127.0.0.1
export MQTT_HOST=127.0.0.1
export MQTT_BASE=jetsonnano/env
python3 ~/env-air-quality-mqtt/env_air_mqtt.py
6) Verify messages via mosquitto_sub (from another terminal):
mosquitto_sub -h 127.0.0.1 -t 'jetsonnano/env/#' -v
You should see JSON messages every ~5 seconds.
TensorRT + ONNX demo (A-path)
We’ll use TensorRT’s trtexec on a small ONNX model (ResNet50 v1-7) to verify GPU acceleration.
mkdir -p ~/models && cd ~/models
wget -O resnet50-v1-7.onnx https://github.com/onnx/models/raw/main/vision/classification/resnet/model/resnet50-v1-7.onnx
# Build FP16 engine; on Nano there is no DLA, so GPU only.
# Input name in this model is 'data'; shape 1x3x224x224.
sudo /usr/src/tensorrt/bin/trtexec \
--onnx=./resnet50-v1-7.onnx \
--saveEngine=./resnet50_fp16.plan \
--shapes=data:1x3x224x224 \
--fp16 --workspace=1024
# Run timed inference (avg across runs)
sudo /usr/src/tensorrt/bin/trtexec \
--loadEngine=./resnet50_fp16.plan \
--separateProfileRun --avgRuns=100
Collect the reported Throughput (FPS) and Average latency (ms). Use tegrastats concurrently to observe GPU/EMC utilization:
sudo tegrastats
Expected: On Nano 2GB MAXN, ResNet50 FP16 should report tens of FPS; latency typically around 15–30 ms for batch=1 (varies by JetPack and clocking).
Camera pipeline check (CSI)
If you have a CSI camera attached, verify nvarguscamerasrc:
# 5-second test to fakesink (no display)
gst-launch-1.0 -e nvarguscamerasrc num-buffers=150 ! \
nvvidconv ! 'video/x-raw,format=BGRx,width=640,height=480,framerate=30/1' ! \
fakesink sync=false
This ensures camera stack is healthy for future vision use cases.
Step-by-step Validation
1) Platform validation
cat /etc/nv_tegra_release
uname -a
dpkg -l | grep -E 'nvidia|tensorrt'
You should see JetPack L4T release, kernel aarch64, and NVIDIA/TensorRT packages present.
2) Bus-level checks
– I2C: The BME680 should answer at 0x77 or 0x76:
i2cdetect -y -r 1
- UART: Observe PMS5003 raw bytes:
sudo stty -F /dev/ttyTHS1 9600 cs8 -cstopb -parenb
sudo dd if=/dev/ttyTHS1 bs=1 count=64 2>/dev/null | hexdump -C
# Expect leading bytes '42 4d' at frame boundaries
3) Run the publisher and subscribe
Terminal A (publisher):
python3 ~/env-air-quality-mqtt/env_air_mqtt.py
Expected logs (example):
- PUB jetsonnano/env/metrics {‘temperature_c’: 24.18, ‘humidity_percent’: 41.22, ‘pressure_hpa’: 1009.35, ‘gas_kohm’: 15.77, ‘voc_index’: None, ‘pm1_0_ugm3’: 4, ‘pm2_5_ugm3’: 7, ‘pm10_ugm3’: 10}
- After ~60s, voc_index should become a number (e.g., 340.0). If you place alcohol near the sensor inlet, gas_kohm drops and voc_index decreases.
Terminal B (subscriber):
mosquitto_sub -h 127.0.0.1 -t 'jetsonnano/env/#' -v
Sample output:
-
jetsonnano/env/metrics {«timestamp»:»2025-06-01T12:00:01.234Z»,»metrics»:{«temperature_c»:24.18,»humidity_percent»:41.22,»pressure_hpa»:1009.35,»gas_kohm»:15.77,»voc_index»:null,»pm1_0_ugm3″:4,»pm2_5_ugm3″:7,»pm10_ugm3″:10},»device»:{«model»:»NVIDIA Jetson Nano 2GB Developer Kit + Adafruit BME680 + Plantower PMS5003″,»host»:»nano2gb»}}
-
Check delivery rate: over 5 minutes you should see about 60 messages with consistent intervals (~5 s).
4) System metrics during run
– Run tegrastats concurrently to verify CPU/GPU usage and memory:
sudo tegrastats
With only the sensor publisher, GPU should be idle; CPU usage low. With TensorRT test running, observe GPU frequency and load.
5) TensorRT metrics
– After building and running the engine with trtexec, capture the lines:
– Throughput: N qps
– Average latency: X ms
– In MAXN mode, expect Throughput > 50 FPS for FP16 ResNet50 (variability expected by JetPack version and thermals).
6) Camera pipeline
– Running the gst-launch pipeline should end cleanly after 150 buffers; no errors.
Troubleshooting
- BME680 not detected (no 0x77/0x76 on i2cdetect)
- Recheck 3.3V and GND wiring; ensure SDA to pin 3, SCL to pin 5.
- Confirm no pull-ups conflict; Adafruit board includes pull-ups, which is fine.
-
If address differs, set BME680_ADDR=0x76 in the environment and rerun.
-
PMS5003 no data or checksum errors
- Confirm PMS5003 is powered by 5V and GND common with Jetson.
- Ensure PMS5003 TX → Jetson RX (pin 10), and PMS5003 RX → Jetson TX (pin 8).
- Use stty 9600 8N1 and hexdump to confirm presence of 0x42 0x4D frames.
-
Some breakouts require SET pulled high (internal pull-up usually present); if in doubt, tie SET to 5V via the breakout lead.
-
Permission denied on /dev/ttyTHS1 or I2C
-
Add your user to dialout and i2c groups, then re-login:
- sudo usermod -aG dialout,i2c $USER; newgrp dialout
-
MQTT connection refused
- Ensure mosquitto is running:
- systemctl status mosquitto
- If using credentials, set MQTT_USER/MQTT_PASS and configure mosquitto accordingly.
-
Check firewall: sudo ufw status (allow 1883/tcp on localhost or LAN as needed).
-
Python package import errors
- Ensure pip3 installed and PATH includes ~/.local/bin.
-
Try: pip3 install –user –upgrade pip; then reinstall adafruit-blinka, adafruit-circuitpython-bme680, paho-mqtt.
-
Jetson performance throttling
- In MAXN mode with jetson_clocks, Nano 2GB can run hot. Ensure good airflow.
-
If trtexec performance drops over time, check for thermal throttling in tegrastats and consider a heatsink/fan.
-
trtexec shape or input name errors
- If the command reports an invalid input name, try removing –shapes and let TensorRT infer, or query the model inputs with:
- python3 -c «import onnx; m=onnx.load(‘resnet50-v1-7.onnx’); print([i.name for i in m.graph.input])»
Improvements
- MQTT hardening
- Enable TLS on Mosquitto, create user/password, and set cafile/cert paths in your publisher using mqtt.Client.tls_set.
-
Use retained messages for last-known metrics.
-
Better IAQ estimation
-
Use Bosch BSEC library for BME680 to obtain compensated IAQ/eCO2/TVOC indices instead of a proxy VOC index. Publish these with timestamps and version tags.
-
Data storage and dashboards
- Bridge MQTT to InfluxDB or TimescaleDB; build dashboards in Grafana.
-
Use Home Assistant MQTT auto-discovery to integrate sensors as entities.
-
Reliability and deployment
- Create a systemd service that restarts the publisher on failure and at boot.
-
Use Docker to containerize the publisher with a slim Python base.
-
Edge AI integration
- Extend the pipeline to fuse sensor data with a camera-based smoke/flame detection model using TensorRT or DeepStream.
- Add anomaly detection (e.g., 1D autoencoder on time series) running on the GPU for early alerts.
Checklist
- Hardware
- BME680 wired to 3.3V, GND, SDA (pin 3), SCL (pin 5).
- PMS5003 powered at 5V, GND shared; TX→Jetson RX (pin 10), RX→Jetson TX (pin 8).
-
Optional CSI camera connected for pipeline validation.
-
Software
- JetPack verified via /etc/nv_tegra_release.
- i2c-tools, python3-smbus, python3-serial, mosquitto, mosquitto-clients installed.
-
Python libs adafruit-blinka, adafruit-circuitpython-bme680, paho-mqtt installed.
-
MQTT
- Mosquitto service active.
-
mosquitto_sub receives messages on jetsonnano/env/# every ~5 seconds.
-
Sensors
- i2cdetect shows 0x77 (or 0x76).
-
Raw PMS5003 frames visible via hexdump; script logs no checksum errors.
-
Performance
- MAXN mode set (optional): sudo nvpmodel -m 0; sudo jetson_clocks.
- trtexec builds and runs FP16 ResNet50 with reported FPS/latency.
-
tegrastats shows expected resource usage.
-
Camera
-
gst-launch nvarguscamerasrc pipeline runs without errors (if camera present).
-
Cleanup/revert
- Revert power mode when done:
- sudo nvpmodel -m 1
- Reboot to fully clear jetson_clocks, or if previously stored:
- sudo jetson_clocks –restore
- Stop publisher with Ctrl+C; disable mosquitto if not needed:
- sudo systemctl stop mosquitto
By completing the above, you will have a fully functional “env-air-quality-mqtt” node using NVIDIA Jetson Nano 2GB Developer Kit + Adafruit BME680 + Plantower PMS5003, validated end-to-end with quantitative performance metrics and reproducible commands.
Find this product and/or books on this topic on Amazon
As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.




