firmware: add XIAO C6 sensor node
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# XIAO ESP32-C6 Sensor Node
|
||||
|
||||
This Arduino sketch reads a BNO086 IMU and MAX-M10S GPS and publishes versioned, newline-delimited JSON over the Seeed RS485 expansion board. It produces vehicle telemetry only; Cargo remains the controller and `/api/v1` authority.
|
||||
|
||||
| Function | XIAO pin |
|
||||
| --- | --- |
|
||||
| RS485 DE / TX / RX | D2 / D4 / D5 |
|
||||
| BNO086 SDA / SCL | D7 / D8 |
|
||||
| GPS TX to XIAO RX | D9 |
|
||||
| GPS RX from XIAO TX | D10 |
|
||||
|
||||
Power both sensors from 3.3V with shared ground. Required Arduino libraries are Adafruit BNO08x, TinyGPSPlus, and ArduinoJson 6.x. Enable **USB CDC On Boot** for diagnostics. GPS uses hardware UART0 and RS485 uses hardware UART1.
|
||||
|
||||
The node sends `sensor_status` at 115200 baud every 200 ms. See `docs/SENSOR_NODE_PROTOCOL.md`. The initial firmware is transmit-only; add a dashboard receiver after bench-testing the node, RS485 polarity, and sensor axes.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#include <TinyGPSPlus.h>
|
||||
#include "gps.h"
|
||||
#include "sensor_node_config.h"
|
||||
namespace { TinyGPSPlus parser; }
|
||||
|
||||
void initGps(HardwareSerial& serial) { serial.begin(GPS_BAUD, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); }
|
||||
void updateGps(HardwareSerial& serial, GpsState& s) {
|
||||
while (serial.available()) { s.lastByteMs = millis(); parser.encode(serial.read()); }
|
||||
s.online = s.lastByteMs && millis() - s.lastByteMs <= SENSOR_STALE_MS;
|
||||
s.fix = parser.location.isValid() && parser.location.age() <= SENSOR_STALE_MS;
|
||||
s.satellites = parser.satellites.isValid() ? parser.satellites.value() : 0;
|
||||
if (parser.location.isValid()) { s.latitude = parser.location.lat(); s.longitude = parser.location.lng(); }
|
||||
if (parser.altitude.isValid()) s.altitudeMeters = parser.altitude.meters();
|
||||
if (parser.speed.isValid()) s.speedKph = parser.speed.kmph();
|
||||
if (parser.course.isValid()) s.courseDegrees = parser.course.deg();
|
||||
if (parser.date.isValid()) { s.year = parser.date.year(); s.month = parser.date.month(); s.day = parser.date.day(); }
|
||||
if (parser.time.isValid()) { s.hour = parser.time.hour(); s.minute = parser.time.minute(); s.second = parser.time.second(); }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
#include <HardwareSerial.h>
|
||||
#include "sensor_state.h"
|
||||
void initGps(HardwareSerial& serial);
|
||||
void updateGps(HardwareSerial& serial, GpsState& state);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#include <Adafruit_BNO08x.h>
|
||||
#include <Wire.h>
|
||||
#include <math.h>
|
||||
#include "imu.h"
|
||||
#include "sensor_node_config.h"
|
||||
|
||||
namespace {
|
||||
Adafruit_BNO08x bno086(-1);
|
||||
sh2_SensorValue_t value;
|
||||
bool initialized = false;
|
||||
float degrees(float r) { return r * 180.0f / PI; }
|
||||
void updateEuler(ImuState& s) {
|
||||
float sinr = 2 * (s.real * s.i + s.j * s.k);
|
||||
float cosr = 1 - 2 * (s.i * s.i + s.j * s.j);
|
||||
s.rollDegrees = degrees(atan2f(sinr, cosr));
|
||||
float sinp = 2 * (s.real * s.j - s.k * s.i);
|
||||
s.pitchDegrees = degrees(fabsf(sinp) >= 1 ? copysignf(PI / 2, sinp) : asinf(sinp));
|
||||
float siny = 2 * (s.real * s.k + s.i * s.j);
|
||||
float cosy = 1 - 2 * (s.j * s.j + s.k * s.k);
|
||||
s.yawDegrees = degrees(atan2f(siny, cosy));
|
||||
}
|
||||
}
|
||||
|
||||
bool initImu() {
|
||||
Wire.begin(BNO086_SDA_PIN, BNO086_SCL_PIN);
|
||||
initialized = bno086.begin_I2C(BNO086_I2C_ADDRESS, &Wire);
|
||||
if (initialized) initialized = bno086.enableReport(SH2_ROTATION_VECTOR, IMU_REPORT_INTERVAL_US);
|
||||
return initialized;
|
||||
}
|
||||
|
||||
void updateImu(ImuState& s) {
|
||||
s.online = initialized;
|
||||
if (!initialized || !bno086.getSensorEvent(&value) || value.sensorId != SH2_ROTATION_VECTOR) return;
|
||||
s.i = value.un.rotationVector.i; s.j = value.un.rotationVector.j;
|
||||
s.k = value.un.rotationVector.k; s.real = value.un.rotationVector.real;
|
||||
s.accuracy = value.status; s.valid = true; s.lastUpdateMs = millis();
|
||||
updateEuler(s);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
#include "sensor_state.h"
|
||||
bool initImu();
|
||||
void updateImu(ImuState& state);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#include <ArduinoJson.h>
|
||||
#include "rs485_transport.h"
|
||||
#include "sensor_node_config.h"
|
||||
|
||||
void initRs485(HardwareSerial& serial) {
|
||||
pinMode(RS485_DE_PIN, OUTPUT); digitalWrite(RS485_DE_PIN, LOW);
|
||||
serial.begin(RS485_BAUD, SERIAL_8N1, RS485_RX_PIN, RS485_TX_PIN);
|
||||
}
|
||||
|
||||
void publishSensorStatus(HardwareSerial& serial, const ImuState& imu, const GpsState& gps) {
|
||||
StaticJsonDocument<1024> doc;
|
||||
doc["type"] = "sensor_status"; doc["schema_version"] = SENSOR_NODE_SCHEMA_VERSION;
|
||||
doc["node_id"] = SENSOR_NODE_NAME; doc["firmware_version"] = SENSOR_NODE_FIRMWARE_VERSION;
|
||||
doc["uptime_ms"] = millis();
|
||||
JsonObject i = doc.createNestedObject("imu");
|
||||
i["online"] = imu.online; i["valid"] = imu.valid && millis() - imu.lastUpdateMs <= SENSOR_STALE_MS;
|
||||
i["accuracy"] = imu.accuracy;
|
||||
JsonObject q = i.createNestedObject("quaternion"); q["i"] = imu.i; q["j"] = imu.j; q["k"] = imu.k; q["real"] = imu.real;
|
||||
i["yaw_degrees"] = imu.yawDegrees; i["pitch_degrees"] = imu.pitchDegrees; i["roll_degrees"] = imu.rollDegrees;
|
||||
JsonObject g = doc.createNestedObject("gps");
|
||||
g["online"] = gps.online; g["fix"] = gps.fix; g["satellites"] = gps.satellites;
|
||||
g["latitude"] = gps.latitude; g["longitude"] = gps.longitude; g["altitude_meters"] = gps.altitudeMeters;
|
||||
g["speed_kph"] = gps.speedKph; g["course_degrees"] = gps.courseDegrees;
|
||||
JsonObject utc = g.createNestedObject("utc"); utc["year"] = gps.year; utc["month"] = gps.month; utc["day"] = gps.day;
|
||||
utc["hour"] = gps.hour; utc["minute"] = gps.minute; utc["second"] = gps.second;
|
||||
digitalWrite(RS485_DE_PIN, HIGH); delayMicroseconds(20);
|
||||
serializeJson(doc, serial); serial.write('\n'); serial.flush();
|
||||
digitalWrite(RS485_DE_PIN, LOW);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
#include <HardwareSerial.h>
|
||||
#include "sensor_state.h"
|
||||
void initRs485(HardwareSerial& serial);
|
||||
void publishSensorStatus(HardwareSerial& serial, const ImuState& imu, const GpsState& gps);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
#define SENSOR_NODE_NAME "xiao_c6_sensor_node"
|
||||
#define SENSOR_NODE_FIRMWARE_VERSION "0.1.0"
|
||||
#define SENSOR_NODE_SCHEMA_VERSION 1
|
||||
#define RS485_DE_PIN D2
|
||||
#define RS485_TX_PIN D4
|
||||
#define RS485_RX_PIN D5
|
||||
#define BNO086_SDA_PIN D7
|
||||
#define BNO086_SCL_PIN D8
|
||||
#define GPS_RX_PIN D9
|
||||
#define GPS_TX_PIN D10
|
||||
#define BNO086_I2C_ADDRESS 0x4A
|
||||
#define GPS_BAUD 9600
|
||||
#define RS485_BAUD 115200
|
||||
#define IMU_REPORT_INTERVAL_US 20000
|
||||
#define TELEMETRY_INTERVAL_MS 200
|
||||
#define SENSOR_STALE_MS 2000
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
struct ImuState {
|
||||
bool online = false, valid = false;
|
||||
uint8_t accuracy = 0;
|
||||
float i = 0, j = 0, k = 0, real = 1;
|
||||
float yawDegrees = 0, pitchDegrees = 0, rollDegrees = 0;
|
||||
unsigned long lastUpdateMs = 0;
|
||||
};
|
||||
|
||||
struct GpsState {
|
||||
bool online = false, fix = false;
|
||||
uint32_t satellites = 0;
|
||||
double latitude = 0, longitude = 0, altitudeMeters = 0;
|
||||
double speedKph = 0, courseDegrees = 0;
|
||||
uint16_t year = 0;
|
||||
uint8_t month = 0, day = 0, hour = 0, minute = 0, second = 0;
|
||||
unsigned long lastByteMs = 0;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#include <Arduino.h>
|
||||
#include "gps.h"
|
||||
#include "imu.h"
|
||||
#include "rs485_transport.h"
|
||||
#include "sensor_node_config.h"
|
||||
|
||||
HardwareSerial gpsSerial(0), rs485Serial(1);
|
||||
ImuState imuState;
|
||||
GpsState gpsState;
|
||||
unsigned long lastTelemetryMs = 0;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200); delay(250);
|
||||
Serial.println("XIAO ESP32-C6 sensor node booting");
|
||||
initGps(gpsSerial); initRs485(rs485Serial);
|
||||
imuState.online = initImu();
|
||||
Serial.println(imuState.online ? "BNO086 ready" : "BNO086 not detected");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
updateGps(gpsSerial, gpsState); updateImu(imuState);
|
||||
unsigned long now = millis();
|
||||
if (now - lastTelemetryMs >= TELEMETRY_INTERVAL_MS) {
|
||||
publishSensorStatus(rs485Serial, imuState, gpsState); lastTelemetryMs = now;
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user