arduino: TSV telemetry protocol with mock RPM/gear
- null-terminated TSV frame: V_bat, IMU (9 fields), RPM, gear - mock RPM ramps 800-8000, gear derived from RPM bands - voltage calibration offset - PROTOCOL.md documents wire format Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
56
arduino/PROTOCOL.md
Normal file
56
arduino/PROTOCOL.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Arduino-Pi Communication Protocol
|
||||||
|
|
||||||
|
Telemetry protocol for Arduino → Pi communication over UART at 115200 baud.
|
||||||
|
|
||||||
|
## Design Rationale
|
||||||
|
|
||||||
|
- **ASCII-based**: Human-debuggable, digits are self-bounding (no accidental header spoofing)
|
||||||
|
- **TSV format**: Tab delimiter, predictable field count, trivial to parse, survives floating decimals
|
||||||
|
- **Null-terminated**: `\0` (0x00) is unambiguous end-of-line, avoids CRLF headaches
|
||||||
|
- **10Hz rate**: ~50 bytes/line × 10Hz = 500 B/s, well under 115200 baud capacity (~4% utilization)
|
||||||
|
|
||||||
|
## Telemetry Frame (Arduino → Pi)
|
||||||
|
|
||||||
|
```
|
||||||
|
field0\tfield1\tfield2\t...\tfieldN\0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
| Index | Name | Unit | Description |
|
||||||
|
|-------|--------|---------|--------------------------------|
|
||||||
|
| 0 | V_bat | V | Battery voltage |
|
||||||
|
| 1 | Ax | g | Acceleration X |
|
||||||
|
| 2 | Ay | g | Acceleration Y |
|
||||||
|
| 3 | Az | g | Acceleration Z |
|
||||||
|
| 4 | Gx | deg/s | Angular velocity X |
|
||||||
|
| 5 | Gy | deg/s | Angular velocity Y |
|
||||||
|
| 6 | Gz | deg/s | Angular velocity Z |
|
||||||
|
| 7 | Roll | deg | Euler angle roll |
|
||||||
|
| 8 | Pitch | deg | Euler angle pitch |
|
||||||
|
| 9 | Yaw | deg | Euler angle yaw |
|
||||||
|
| 10 | RPM | RPM | Engine RPM |
|
||||||
|
| 11 | Gear | - | Gear position (0=N, 1-6) |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```
|
||||||
|
12.45\t0.02\t-0.01\t1.00\t0.50\t-0.25\t0.10\t2.35\t-1.20\t45.80\t3500\t3\0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stale Data Handling
|
||||||
|
|
||||||
|
When IMU data is stale, empty fields are sent to preserve field count:
|
||||||
|
```
|
||||||
|
12.45\t\t\t\t\t\t\t\t\t\0
|
||||||
|
```
|
||||||
|
Backend parses empty fields as null/NaN.
|
||||||
|
|
||||||
|
## Commands (Pi → Arduino)
|
||||||
|
|
||||||
|
TBD: Command structure for configuration, calibration triggers, etc.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Protocol changes should bump a version field or use a different frame header.
|
||||||
|
Currently unversioned (v0 / development).
|
||||||
@@ -40,33 +40,16 @@ Install via Arduino Library Manager:
|
|||||||
|
|
||||||
## Protocol
|
## Protocol
|
||||||
|
|
||||||
Simple text lines, one per sensor reading:
|
TSV (tab-separated), null-terminated frames at 10Hz. See [PROTOCOL.md](PROTOCOL.md) for full specification.
|
||||||
```
|
|
||||||
V_bat: 12.45
|
|
||||||
Ax: 0.02
|
|
||||||
Ay: -0.01
|
|
||||||
Az: 1.00
|
|
||||||
Gx: 0.50
|
|
||||||
Gy: -0.25
|
|
||||||
Gz: 0.10
|
|
||||||
Roll: 2.35
|
|
||||||
Pitch: -1.20
|
|
||||||
Yaw: 45.80
|
|
||||||
```
|
|
||||||
|
|
||||||
If IMU data is stale (no valid packets for 200ms):
|
|
||||||
```
|
|
||||||
IMU: STALE
|
|
||||||
```
|
|
||||||
|
|
||||||
Commands from Pi are echoed back:
|
|
||||||
```
|
|
||||||
ACK: PING
|
|
||||||
```
|
|
||||||
|
|
||||||
## Planned
|
## Planned
|
||||||
|
|
||||||
- RPM sensing (pulse counting from ignition coil)
|
- RPM sensing (pulse counting from ignition coil)
|
||||||
- Engine temperature (thermocouple/NTC)
|
|
||||||
- Gear position indicator
|
- Gear position indicator
|
||||||
|
|
||||||
|
### Not planned
|
||||||
|
|
||||||
|
- Engine temperature (thermocouple/NTC)
|
||||||
|
*Borderline do not want to do, simple but not really useful*
|
||||||
- Turn signal / high beam status
|
- Turn signal / high beam status
|
||||||
|
*No need to do something the dash already does*
|
||||||
|
|||||||
@@ -39,6 +39,49 @@ bool comms_update() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void comms_send_telemetry(float voltage, const ImuData& imu, bool imu_valid, int rpm, int gear) {
|
||||||
|
// Field 0: voltage
|
||||||
|
Serial.print(voltage, 2);
|
||||||
|
Serial.write('\t');
|
||||||
|
|
||||||
|
if (imu_valid) {
|
||||||
|
// Fields 1-3: acceleration
|
||||||
|
Serial.print(imu.ax, 2);
|
||||||
|
Serial.write('\t');
|
||||||
|
Serial.print(imu.ay, 2);
|
||||||
|
Serial.write('\t');
|
||||||
|
Serial.print(imu.az, 2);
|
||||||
|
Serial.write('\t');
|
||||||
|
|
||||||
|
// Fields 4-6: angular velocity
|
||||||
|
Serial.print(imu.gx, 2);
|
||||||
|
Serial.write('\t');
|
||||||
|
Serial.print(imu.gy, 2);
|
||||||
|
Serial.write('\t');
|
||||||
|
Serial.print(imu.gz, 2);
|
||||||
|
Serial.write('\t');
|
||||||
|
|
||||||
|
// Fields 7-9: euler angles
|
||||||
|
Serial.print(imu.roll, 2);
|
||||||
|
Serial.write('\t');
|
||||||
|
Serial.print(imu.pitch, 2);
|
||||||
|
Serial.write('\t');
|
||||||
|
Serial.print(imu.yaw, 2);
|
||||||
|
} else {
|
||||||
|
// Empty fields for stale IMU (9 tabs for 9 empty fields)
|
||||||
|
Serial.print(F("\t\t\t\t\t\t\t\t"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields 10-11: RPM and gear
|
||||||
|
Serial.write('\t');
|
||||||
|
Serial.print(rpm);
|
||||||
|
Serial.write('\t');
|
||||||
|
Serial.print(gear);
|
||||||
|
|
||||||
|
// Null terminator (no newline)
|
||||||
|
Serial.write('\0');
|
||||||
|
}
|
||||||
|
|
||||||
void comms_send(const char* key, float value, int decimals) {
|
void comms_send(const char* key, float value, int decimals) {
|
||||||
Serial.print(key);
|
Serial.print(key);
|
||||||
Serial.print(": ");
|
Serial.print(": ");
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#define COMMS_H
|
#define COMMS_H
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include "imu.h"
|
||||||
|
|
||||||
// Initialize Pi serial communication (call in setup)
|
// Initialize Pi serial communication (call in setup)
|
||||||
void comms_init();
|
void comms_init();
|
||||||
@@ -10,7 +11,12 @@ void comms_init();
|
|||||||
// Returns true if a complete command was received
|
// Returns true if a complete command was received
|
||||||
bool comms_update();
|
bool comms_update();
|
||||||
|
|
||||||
// Send telemetry line to Pi
|
// Send complete telemetry frame (TSV format, null-terminated)
|
||||||
|
// Format: V_bat\tAx\tAy\tAz\tGx\tGy\tGz\tRoll\tPitch\tYaw\tRPM\tGear\0
|
||||||
|
// If imu_valid is false, IMU fields are empty (but tabs preserved)
|
||||||
|
void comms_send_telemetry(float voltage, const ImuData& imu, bool imu_valid, int rpm, int gear);
|
||||||
|
|
||||||
|
// Send key:value line (for debug/ACK, newline-terminated)
|
||||||
void comms_send(const char* key, float value, int decimals = 2);
|
void comms_send(const char* key, float value, int decimals = 2);
|
||||||
void comms_send(const char* key, int value);
|
void comms_send(const char* key, int value);
|
||||||
void comms_send(const char* key, const char* value);
|
void comms_send(const char* key, const char* value);
|
||||||
|
|||||||
19
arduino/main/gear.cpp
Normal file
19
arduino/main/gear.cpp
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#include "gear.h"
|
||||||
|
|
||||||
|
// Mock gear: derived from RPM bands
|
||||||
|
// Real sensor would read position switch
|
||||||
|
|
||||||
|
void gear_init() {
|
||||||
|
// Nothing to init for mock
|
||||||
|
}
|
||||||
|
|
||||||
|
int gear_get(int rpm) {
|
||||||
|
// Simulate gear based on RPM
|
||||||
|
// N < 1000, 1st < 2500, 2nd < 4000, 3rd < 5500, 4th < 7000, 5th+
|
||||||
|
if (rpm < 1000) return 0; // Neutral
|
||||||
|
if (rpm < 2500) return 1;
|
||||||
|
if (rpm < 4000) return 2;
|
||||||
|
if (rpm < 5500) return 3;
|
||||||
|
if (rpm < 7000) return 4;
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
7
arduino/main/gear.h
Normal file
7
arduino/main/gear.h
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#ifndef GEAR_H
|
||||||
|
#define GEAR_H
|
||||||
|
|
||||||
|
void gear_init();
|
||||||
|
int gear_get(int rpm); // Returns gear 0-6 (0=neutral)
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
#include "voltage.h"
|
#include "voltage.h"
|
||||||
#include "imu.h"
|
#include "imu.h"
|
||||||
|
#include "rpm.h"
|
||||||
|
#include "gear.h"
|
||||||
#include "comms.h"
|
#include "comms.h"
|
||||||
|
|
||||||
// Timing
|
// Timing
|
||||||
@@ -21,6 +23,10 @@ void setup() {
|
|||||||
imu_init(); // AltSoftSerial on pins 8(RX)/9(TX)
|
imu_init(); // AltSoftSerial on pins 8(RX)/9(TX)
|
||||||
Serial.println(F("[INIT] imu ok"));
|
Serial.println(F("[INIT] imu ok"));
|
||||||
|
|
||||||
|
rpm_init();
|
||||||
|
gear_init();
|
||||||
|
Serial.println(F("[INIT] rpm/gear ok"));
|
||||||
|
|
||||||
// Let IMU warm up a bit before calibrating
|
// Let IMU warm up a bit before calibrating
|
||||||
// (WT61 needs a moment to stabilize after power-on)
|
// (WT61 needs a moment to stabilize after power-on)
|
||||||
delay(500);
|
delay(500);
|
||||||
@@ -36,6 +42,9 @@ void loop() {
|
|||||||
// Always poll IMU - it's streaming at 20Hz
|
// Always poll IMU - it's streaming at 20Hz
|
||||||
imu_update();
|
imu_update();
|
||||||
|
|
||||||
|
// Update mock RPM (ramping)
|
||||||
|
rpm_update();
|
||||||
|
|
||||||
// Process any commands from Pi
|
// Process any commands from Pi
|
||||||
if (comms_update()) {
|
if (comms_update()) {
|
||||||
const char* cmd = comms_get_command();
|
const char* cmd = comms_get_command();
|
||||||
@@ -63,28 +72,12 @@ void loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void sendTelemetry() {
|
void sendTelemetry() {
|
||||||
// Battery voltage
|
// Send all telemetry in a single TSV frame
|
||||||
comms_send("V_bat", voltage_read());
|
float voltage = voltage_read();
|
||||||
|
const ImuData& imu = imu_get_data();
|
||||||
|
bool imu_valid = imu_is_fresh();
|
||||||
|
int rpm = rpm_get();
|
||||||
|
int gear = gear_get(rpm);
|
||||||
|
|
||||||
// IMU data (only if we have fresh data)
|
comms_send_telemetry(voltage, imu, imu_valid, rpm, gear);
|
||||||
if (imu_is_fresh()) {
|
|
||||||
const ImuData& imu = imu_get_data();
|
|
||||||
|
|
||||||
// Acceleration (g)
|
|
||||||
comms_send("Ax", imu.ax);
|
|
||||||
comms_send("Ay", imu.ay);
|
|
||||||
comms_send("Az", imu.az);
|
|
||||||
|
|
||||||
// Angular velocity (deg/s)
|
|
||||||
comms_send("Gx", imu.gx);
|
|
||||||
comms_send("Gy", imu.gy);
|
|
||||||
comms_send("Gz", imu.gz);
|
|
||||||
|
|
||||||
// Euler angles (degrees)
|
|
||||||
comms_send("Roll", imu.roll);
|
|
||||||
comms_send("Pitch", imu.pitch);
|
|
||||||
comms_send("Yaw", imu.yaw);
|
|
||||||
} else {
|
|
||||||
comms_send("IMU", "STALE");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
19
arduino/main/rpm.cpp
Normal file
19
arduino/main/rpm.cpp
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#include "rpm.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// Mock RPM: ramps up/down between idle and redline
|
||||||
|
static int _rpm = 800;
|
||||||
|
|
||||||
|
void rpm_init() {
|
||||||
|
_rpm = 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
void rpm_update() {
|
||||||
|
// ~100ms per call at 10Hz = takes ~7s to sweep range
|
||||||
|
_rpm += 10;
|
||||||
|
if (_rpm >= 8000) { _rpm = 800;}
|
||||||
|
}
|
||||||
|
|
||||||
|
int rpm_get() {
|
||||||
|
return _rpm;
|
||||||
|
}
|
||||||
8
arduino/main/rpm.h
Normal file
8
arduino/main/rpm.h
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef RPM_H
|
||||||
|
#define RPM_H
|
||||||
|
|
||||||
|
void rpm_init();
|
||||||
|
void rpm_update(); // Call in loop
|
||||||
|
int rpm_get(); // Returns current RPM (0 if invalid)
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -10,6 +10,7 @@ static const int PIN_VBAT = A0;
|
|||||||
static const float DIVIDER_RATIO = 47.0 / (100.0 + 47.0); // ~0.3197
|
static const float DIVIDER_RATIO = 47.0 / (100.0 + 47.0); // ~0.3197
|
||||||
static const float ADC_REF = 5.0;
|
static const float ADC_REF = 5.0;
|
||||||
static const int ADC_MAX = 1023;
|
static const int ADC_MAX = 1023;
|
||||||
|
static const float OFFSET = 0.2; // calib
|
||||||
|
|
||||||
void voltage_init() {
|
void voltage_init() {
|
||||||
// analogRead doesn't need explicit pinMode, but here for future config
|
// analogRead doesn't need explicit pinMode, but here for future config
|
||||||
@@ -23,5 +24,5 @@ int voltage_read_raw() {
|
|||||||
float voltage_read() {
|
float voltage_read() {
|
||||||
int raw = voltage_read_raw();
|
int raw = voltage_read_raw();
|
||||||
float vDivider = (raw / (float)ADC_MAX) * ADC_REF;
|
float vDivider = (raw / (float)ADC_MAX) * ADC_REF;
|
||||||
return vDivider / DIVIDER_RATIO;
|
return vDivider / DIVIDER_RATIO + OFFSET;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user