Guide Software Intermediate 30 min

Atmospheric Sensing Board - Custom Sensor Integration

Guide for adding custom I²C sensors to CubeSat v1 ASB firmware using init, read, validate, and $ASB UART packet patterns.

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:

  1. Declare the driver instance, a presence flag, and fields in the shared data struct.
  2. Initialize in setup() via a dedicated init* function.
  3. Re-initialize periodically in loop() if the sensor was disconnected or failed validation.
  4. Read in readSensors() only when the presence flag is true; clear the flag on invalid readings.
  5. Transmit scaled integer values in transmitPackets() inside the $ASB packet 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:

SensorOn failure
SHT4xisnan on temperature or humidity → sht4_ok = false
BMP180isnan or temperature outside -40…85 °C → bmp_ok = false
SCD4xCO2 outside 400–5000 ppm → fallback to eCO2 or 410
SPS30Negative 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 indexSourceEncoding
1data.t1 (SHT4x °C)(int)((t1 + 40) * 100)
2data.t2 (BMP180 °C)(int)((t2 + 40) * 100)
3data.rh (%RH)(int)(rh * 10)
4data.p1 (Pa)(int)(p1 / 10)
5data.pa1 (m altitude)(int)(pa1)
6data.aqiinteger
7data.tvocinteger
8data.eco2integer
9data.co2integer

Packet 4 ($ASB,4,...) — particulate matter (only if sps30_ready).

To add a custom sensor without breaking the host:

  1. Preferred: Define a new packet ID (e.g. $ASB,5,...) with a documented field order and scaling, and only emit it when your *_ok flag is true.
  2. 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:

  1. Temperature offset: +40 °C bias so negative temps encode as non-negative centidegrees.
  2. PM mass: mc_2p5 * 100 for 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:

  1. trySPS30Start() — probe bus, configure fan cleaning, start measurement.
  2. sps30_ready — false until SPS30_WARMUP_MS (8 s) after a successful start.
  3. SPS30_PROBE_INTERVAL_MS — retry probe every 5 s if never started.
  4. 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

TaskLocation in sketch
#include + instanceTop of file
data fieldsstruct { ... } 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 + validatereadSensors()
Encode + sendtransmitPackets() (new or extended $ASB id)
Human debugprintDebugData() 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.

Questions about this resource?

Submit a support ticket and reference this page — our team will see the resource context in the admin queue.

  • Confirmation email with your ticket reference
  • Typical response within 1–2 business days
  • Links to docs, modules, and resources you reference

Before you submit

  1. Search troubleshooting and the resource library.
  2. Note your product, module ID, or resource name.
  3. Include error messages, firmware version, and steps to reproduce.

Email: [email protected]