Custom Sensor Integration
This guide explains how to add a new sensor to the CubeSat v1 ASB firmware using the same patterns as ASB_SHT4x.ino. The sketch is a minimal telemetry node: it initializes sensors on I²C, reads them once per second, and transmits compact $ASB packets over UART.
Overview
The firmware follows a consistent lifecycle for each sensor:
- Declare the driver instance, a presence flag, and fields in the shared
datastruct. - Initialize in
setup()via a dedicatedinit*function. - Re-initialize periodically in
loop()if the sensor was disconnected or failed validation. - Read in
readSensors()only when the presence flag is true; clear the flag on invalid readings. - Transmit scaled integer values in
transmitPackets()inside the$ASBpacket format.
Optional debug output is controlled by DEBUG_OUTPUT and does not affect the wire protocol.
Step 1: Add the library and instance
At the top of ASB_SHT4x.ino, include your sensor library and create a global instance, matching existing sensors:
#include "YourSensorLibrary.h"
YourSensorClass mySensor;If the sensor uses the shared I²C bus, it uses the same Wire object initialized in setup() with Wire.begin(). Pass &Wire to the constructor if your library requires it (see DFRobot_SCD4X).
Step 2: Extend the data struct
All telemetry values consumed by the host are stored in one struct:
struct {
float t1, t2, rh, p1, pa1;
uint32_t aqi, tvoc, eco2;
uint16_t co2;
} data;Add fields for your sensor’s readings (use types that match precision and range). Example for a light sensor:
float lux;Keep naming consistent with how values will be encoded in packets (see Step 6).
Step 3: Add a presence flag and init function
Each integrated sensor has a bool *_ok flag so disconnects and bad reads can be detected without blocking the rest of the stack:
bool my_sensor_ok = false;Add a small initializer, following initSHT4x() / initBMP():
static void initMySensor() {
if (mySensor.begin()) {
// Optional: configure mode, precision, compensation, etc.
my_sensor_ok = true;
}
}Call it from setup() after Wire.begin():
initMySensor();ENS160-style startup: Some devices need a reset mode, delay, then standard mode (initENS160()).
SCD4x-style compensation: You can read another sensor first (e.g. altitude from BMP) and pass it into the new driver before starting periodic measurement (initSCD4X()).
Step 4: Re-init on disconnect in loop()
Every 10 seconds the firmware retries initialization for any sensor whose *_ok flag is false:
#define SENSOR_REINIT_INTERVAL_MS 10000
if ((now - sensor_reinit_time) >= SENSOR_REINIT_INTERVAL_MS) {
sensor_reinit_time = now;
if (!sht4_ok) initSHT4x();
if (!bmp_ok) initBMP();
// ...
}Add your sensor the same way:
if (!my_sensor_ok) initMySensor();This allows hot-plug recovery without resetting the MCU.
Step 5: Read in readSensors()
readSensors() runs once per second (driven by the 1000 ms gate in loop()). Pattern for a typical sensor:
if (my_sensor_ok) {
data.lux = mySensor.readLux();
if (isnan(data.lux) || data.lux < 0.0f) {
my_sensor_ok = false;
}
}Validation examples from the reference sketch:
| Sensor | On failure |
| SHT4x | isnan on temperature or humidity → sht4_ok = false |
| BMP180 | isnan or temperature outside -40…85 °C → bmp_ok = false |
| SCD4x | CO2 outside 400–5000 ppm → fallback to eCO2 or 410 |
| SPS30 | Negative return from driver → sps30_ready = false, reset warm-up state |
Only clear *_ok when you are confident the device or bus is unhealthy; transient “data not ready” states are often handled by skipping an update (see ENS160 checkDataStatus() and SCD4x getDataReadyStatus()).
Step 6: Add telemetry to transmitPackets()
Packets are ASCII, comma-separated, with a trailing ,*:
$ASB,<packet_id>,<field1>,<field2>,...,*Packet 3 ($ASB,3,...) — main environmental bundle (fixed field order):
| Field index | Source | Encoding |
| 1 | data.t1 (SHT4x °C) | (int)((t1 + 40) * 100) |
| 2 | data.t2 (BMP180 °C) | (int)((t2 + 40) * 100) |
| 3 | data.rh (%RH) | (int)(rh * 10) |
| 4 | data.p1 (Pa) | (int)(p1 / 10) |
| 5 | data.pa1 (m altitude) | (int)(pa1) |
| 6 | data.aqi | integer |
| 7 | data.tvoc | integer |
| 8 | data.eco2 | integer |
| 9 | data.co2 | integer |
Packet 4 ($ASB,4,...) — particulate matter (only if sps30_ready).
To add a custom sensor without breaking the host:
- Preferred: Define a new packet ID (e.g.
$ASB,5,...) with a documented field order and scaling, and only emit it when your*_okflag is true. - Alternative: If the ground station already expects spare fields in packet 3, append fields before
,*and update host parsing in lockstep.
Scaling must be reversible on the host. Examples from the sketch:
- Temperature offset: +40 °C bias so negative temps encode as non-negative centidegrees.
- PM mass:
mc_2p5 * 100for 0.01 µg/m³ resolution.
UART is enabled only during transmission:
UCSR0B |= _BV(TXEN0);
// Serial.print ...
delay(100);
UCSR0B &= ~_BV(TXEN0);Keep transmitPackets() fast; heavy work belongs in readSensors().
Step 7: Optional debug output
Set #define DEBUG_OUTPUT true to print human-readable values in printDebugData() before packets are sent. Add Serial.print lines for your fields there; this does not change the $ASB protocol.
Special case: slow or warm-up sensors (SPS30)
The SPS30 does not follow the simple *_ok + periodic re-init pattern alone. It uses:
trySPS30Start()— probe bus, configure fan cleaning, start measurement.sps30_ready— false untilSPS30_WARMUP_MS(8 s) after a successful start.SPS30_PROBE_INTERVAL_MS— retry probe every 5 s if never started.- Separate read path in
readSensors()with driver return codes clearing readiness.
If your sensor needs seconds of warm-up or a start command before valid data, copy this state machine instead of only using init* + readSensors().
Checklist
| Task | Location in sketch |
#include + instance | Top of file |
data fields | struct { ... } data |
bool *_ok + init*() | Near other sensor flags / inits |
init*() in setup() | After Wire.begin() |
Re-init in loop() | SENSOR_REINIT_INTERVAL_MS block |
| Read + validate | readSensors() |
| Encode + send | transmitPackets() (new or extended $ASB id) |
| Human debug | printDebugData() if needed |
Minimal template
// --- declarations ---
#include "YourSensorLibrary.h"
YourSensorClass mySensor;
bool my_sensor_ok = false;
// add to struct: float my_value;
static void initMySensor() {
if (mySensor.begin()) {
my_sensor_ok = true;
}
}
// setup(): initMySensor();
// loop() re-init:
// if (!my_sensor_ok) initMySensor();
// readSensors():
// if (my_sensor_ok) {
// data.my_value = mySensor.read();
// if (isnan(data.my_value)) my_sensor_ok = false;
// }
// transmitPackets() — example new packet:
// if (my_sensor_ok) {
// Serial.print("$ASB,5,");
// Serial.print((int)(data.my_value * 100));
// Serial.println(",*");
// }Host compatibility
Any new packet ID or changed field order must be documented for whoever parses ASB telemetry (flight computer, logger, or ground station). The reference firmware assumes 9600 baud serial and 1 Hz packet rate. Coordinate scaling and packet IDs with the rest of the CubeSat v1 stack before flight.