Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
"allow": [
"Bash(pio)",
"Bash(pio run)",
"Bash(cpplint:*)"
"Bash(cpplint:*)",
"Bash(pio test:*)"
],
"deny": []
}
}
}
14 changes: 14 additions & 0 deletions inc/sp140/ble/bms_packet_codec.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#ifndef INC_SP140_BLE_BMS_PACKET_CODEC_H_
#define INC_SP140_BLE_BMS_PACKET_CODEC_H_

#include <cstdint>

#include "sp140/structs.h"

BLE_BMS_Telemetry_V1 buildBMSPackedTelemetryV1(
const STR_BMS_TELEMETRY_140& telemetry, uint8_t bms_id);

BLE_BMS_Extended_Telemetry_V1 buildBMSExtendedTelemetryV1(
const STR_BMS_TELEMETRY_140& telemetry, uint8_t bms_id);

#endif // INC_SP140_BLE_BMS_PACKET_CODEC_H_
1 change: 1 addition & 0 deletions inc/sp140/ble/bms_service.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ void initBmsBleService(NimBLEServer* server);
// Binary packed telemetry update (V1 protocol)
// bms_id: 0-3 for multi-BMS support
void updateBMSPackedTelemetry(const STR_BMS_TELEMETRY_140& telemetry, uint8_t bms_id = 0);
void updateBMSExtendedTelemetry(const STR_BMS_TELEMETRY_140& telemetry, uint8_t bms_id = 0);

#endif // INC_SP140_BLE_BMS_SERVICE_H_
29 changes: 28 additions & 1 deletion inc/sp140/structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ typedef struct {
float inPWM;
float outPWM;
uint8_t statusFlag;
word checksum;
uint16_t checksum;
unsigned long lastUpdateMs; // Timestamp of last telemetry update
TelemetryState escState; // Current connection state
uint16_t running_error; // Runtime error bitmask
Expand Down Expand Up @@ -67,6 +67,7 @@ struct UnifiedBatteryData {
};

#define BMS_CELLS_NUM 24 // Maximum number of cells supported
#define BMS_TEMPERATURE_SENSORS_NUM 6 // MOS, Balance, T1-T4

// BMS telemetry data
typedef struct {
Expand Down Expand Up @@ -138,6 +139,29 @@ typedef struct {
uint32_t lastUpdateMs; // Timestamp of last update
} BLE_BMS_Telemetry_V1;

// Binary packed Extended BMS telemetry for BLE transmission
// Contains summary fields + per-cell voltages + per-probe temperatures.
// Min/max/differential are omitted — clients compute from the arrays.
typedef struct {
uint8_t version; // Protocol version (1)
uint8_t bms_id; // BMS identifier (0-3 for multi-BMS)
uint8_t connection_state; // TelemetryState enum value
float soc; // State of charge (%)
float battery_voltage; // Total battery voltage (V)
float battery_current; // Battery current (A)
float power; // Power (kW)
uint8_t battery_fail_level; // Battery failure status
uint8_t is_charge_mos; // Charge MOSFET state (0/1)
uint8_t is_discharge_mos; // Discharge MOSFET state (0/1)
uint8_t is_charging; // Charging state (0/1)
uint32_t battery_cycle; // Battery cycle count
float energy_cycle; // Energy per cycle (kWh)
uint32_t lastUpdateMs; // Timestamp of last update
float cell_voltages[BMS_CELLS_NUM]; // Per-cell voltages (V)
// Temperature ordering: [mos, balance, t1, t2, t3, t4]
float temperatures[BMS_TEMPERATURE_SENSORS_NUM]; // Probe temperatures (deg C)
} BLE_BMS_Extended_Telemetry_V1;

// Binary packed ESC telemetry for BLE transmission (~46 bytes)
typedef struct {
uint8_t version; // Protocol version (1)
Expand Down Expand Up @@ -167,6 +191,9 @@ typedef struct {
uint32_t uptime_ms; // Time since boot (ms)
} BLE_Controller_Telemetry_V1;

static_assert(sizeof(BLE_BMS_Extended_Telemetry_V1) <= 182,
"Extended BMS packet exceeds BLE payload budget for MTU 185");

#pragma pack(pop)

#endif // INC_SP140_STRUCTS_H_
76 changes: 76 additions & 0 deletions src/sp140/ble/bms_packet_codec.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#include "sp140/ble/bms_packet_codec.h"

namespace {

constexpr uint8_t kBmsTelemetryVersion = 1;

#if defined(ARDUINO_ARCH_ESP32)
static_assert(sizeof(unsigned long) == 4,
"ESP32 build expects 32-bit unsigned long for telemetry timestamps");
#endif

void fillTemperatureArray(
const STR_BMS_TELEMETRY_140& telemetry,
float temperatures[BMS_TEMPERATURE_SENSORS_NUM]) {
// Order is fixed for client parsing: [mos, balance, t1, t2, t3, t4]
temperatures[0] = telemetry.mos_temperature;
temperatures[1] = telemetry.balance_temperature;
temperatures[2] = telemetry.t1_temperature;
temperatures[3] = telemetry.t2_temperature;
temperatures[4] = telemetry.t3_temperature;
temperatures[5] = telemetry.t4_temperature;
}

} // namespace

BLE_BMS_Telemetry_V1 buildBMSPackedTelemetryV1(
const STR_BMS_TELEMETRY_140& telemetry, uint8_t bms_id) {
BLE_BMS_Telemetry_V1 packet = {};
packet.version = kBmsTelemetryVersion;
packet.bms_id = bms_id;
packet.connection_state = static_cast<uint8_t>(telemetry.bmsState);
packet.soc = telemetry.soc;
packet.battery_voltage = telemetry.battery_voltage;
packet.battery_current = telemetry.battery_current;
packet.power = telemetry.power;
packet.highest_cell_voltage = telemetry.highest_cell_voltage;
packet.lowest_cell_voltage = telemetry.lowest_cell_voltage;
packet.highest_temperature = telemetry.highest_temperature;
packet.lowest_temperature = telemetry.lowest_temperature;
packet.voltage_differential = telemetry.voltage_differential;
packet.battery_fail_level = telemetry.battery_fail_level;
packet.is_charge_mos = telemetry.is_charge_mos ? 1 : 0;
packet.is_discharge_mos = telemetry.is_discharge_mos ? 1 : 0;
packet.is_charging = telemetry.is_charging ? 1 : 0;
packet.battery_cycle = telemetry.battery_cycle;
packet.energy_cycle = telemetry.energy_cycle;
packet.lastUpdateMs = static_cast<uint32_t>(telemetry.lastUpdateMs);

return packet;
}

BLE_BMS_Extended_Telemetry_V1 buildBMSExtendedTelemetryV1(
const STR_BMS_TELEMETRY_140& telemetry, uint8_t bms_id) {
BLE_BMS_Extended_Telemetry_V1 packet = {};
packet.version = kBmsTelemetryVersion;
packet.bms_id = bms_id;
packet.connection_state = static_cast<uint8_t>(telemetry.bmsState);
packet.soc = telemetry.soc;
packet.battery_voltage = telemetry.battery_voltage;
packet.battery_current = telemetry.battery_current;
packet.power = telemetry.power;
packet.battery_fail_level = telemetry.battery_fail_level;
packet.is_charge_mos = telemetry.is_charge_mos ? 1 : 0;
packet.is_discharge_mos = telemetry.is_discharge_mos ? 1 : 0;
packet.is_charging = telemetry.is_charging ? 1 : 0;
packet.battery_cycle = telemetry.battery_cycle;
packet.energy_cycle = telemetry.energy_cycle;
packet.lastUpdateMs = static_cast<uint32_t>(telemetry.lastUpdateMs);

for (uint8_t i = 0; i < BMS_CELLS_NUM; ++i) {
packet.cell_voltages[i] = telemetry.cell_voltages[i];
}
fillTemperatureArray(telemetry, packet.temperatures);

return packet;
}
56 changes: 33 additions & 23 deletions src/sp140/ble/bms_service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

#include "sp140/ble.h"
#include "sp140/ble/ble_ids.h"
#include "sp140/ble/bms_packet_codec.h"

namespace {

NimBLEService* pBmsService = nullptr;

// Binary packed telemetry characteristic (V1)
NimBLECharacteristic* pBMSPackedTelemetry = nullptr;
NimBLECharacteristic* pBMSExtendedTelemetry = nullptr;

} // namespace

Expand All @@ -27,13 +29,24 @@ void initBmsBleService(NimBLEServer* server) {
NimBLEUUID(BMS_PACKED_TELEMETRY_UUID),
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY);

// Initialize packed telemetry with zeros
BLE_BMS_Telemetry_V1 initialPacket = {};
initialPacket.version = 1;
pBMSExtendedTelemetry = pBmsService->createCharacteristic(
NimBLEUUID(BMS_EXTENDED_TELEMETRY_UUID),
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY);

// Initialize telemetry characteristics with a disconnected baseline.
STR_BMS_TELEMETRY_140 initialTelemetry = {};
initialTelemetry.bmsState = TelemetryState::NOT_CONNECTED;
BLE_BMS_Telemetry_V1 initialPacket = buildBMSPackedTelemetryV1(initialTelemetry, 0);
pBMSPackedTelemetry->setValue(
reinterpret_cast<uint8_t*>(&initialPacket),
sizeof(BLE_BMS_Telemetry_V1));

BLE_BMS_Extended_Telemetry_V1 initialExtendedPacket =
buildBMSExtendedTelemetryV1(initialTelemetry, 0);
pBMSExtendedTelemetry->setValue(
reinterpret_cast<uint8_t*>(&initialExtendedPacket),
sizeof(BLE_BMS_Extended_Telemetry_V1));

pBmsService->start();
}

Expand All @@ -43,26 +56,7 @@ void updateBMSPackedTelemetry(const STR_BMS_TELEMETRY_140& telemetry, uint8_t bm
return;
}

BLE_BMS_Telemetry_V1 packet;
packet.version = 1;
packet.bms_id = bms_id;
packet.connection_state = static_cast<uint8_t>(telemetry.bmsState);
packet.soc = telemetry.soc;
packet.battery_voltage = telemetry.battery_voltage;
packet.battery_current = telemetry.battery_current;
packet.power = telemetry.power;
packet.highest_cell_voltage = telemetry.highest_cell_voltage;
packet.lowest_cell_voltage = telemetry.lowest_cell_voltage;
packet.highest_temperature = telemetry.highest_temperature;
packet.lowest_temperature = telemetry.lowest_temperature;
packet.voltage_differential = telemetry.voltage_differential;
packet.battery_fail_level = telemetry.battery_fail_level;
packet.is_charge_mos = telemetry.is_charge_mos ? 1 : 0;
packet.is_discharge_mos = telemetry.is_discharge_mos ? 1 : 0;
packet.is_charging = telemetry.is_charging ? 1 : 0;
packet.battery_cycle = telemetry.battery_cycle;
packet.energy_cycle = telemetry.energy_cycle;
packet.lastUpdateMs = static_cast<uint32_t>(telemetry.lastUpdateMs);
BLE_BMS_Telemetry_V1 packet = buildBMSPackedTelemetryV1(telemetry, bms_id);

pBMSPackedTelemetry->setValue(
reinterpret_cast<uint8_t*>(&packet),
Expand All @@ -72,3 +66,19 @@ void updateBMSPackedTelemetry(const STR_BMS_TELEMETRY_140& telemetry, uint8_t bm
pBMSPackedTelemetry->notify();
}
}

void updateBMSExtendedTelemetry(const STR_BMS_TELEMETRY_140& telemetry, uint8_t bms_id) {
if (pBmsService == nullptr || pBMSExtendedTelemetry == nullptr) {
return;
}

BLE_BMS_Extended_Telemetry_V1 packet =
buildBMSExtendedTelemetryV1(telemetry, bms_id);
pBMSExtendedTelemetry->setValue(
reinterpret_cast<uint8_t*>(&packet),
sizeof(BLE_BMS_Extended_Telemetry_V1));

if (deviceConnected) {
pBMSExtendedTelemetry->notify();
}
}
15 changes: 12 additions & 3 deletions src/sp140/sp140.ino
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,8 @@ void throttleTask(void *pvParameters) {

void updateBLETask(void *pvParameters) {
STR_BMS_TELEMETRY_140 newBmsTelemetry;
static unsigned long lastExtendedUpdateMs = 0;
const unsigned long EXTENDED_UPDATE_INTERVAL_MS = 500; // 2Hz

while (true) {
// Add error checking for queue
Expand All @@ -393,6 +395,12 @@ void updateBLETask(void *pvParameters) {
// Update packed binary telemetry (always enabled)
updateBMSPackedTelemetry(newBmsTelemetry, 0); // bms_id=0 for primary BMS

const unsigned long now = millis();
if (now - lastExtendedUpdateMs >= EXTENDED_UPDATE_INTERVAL_MS) {
updateBMSExtendedTelemetry(newBmsTelemetry, 0); // bms_id=0 for primary BMS
lastExtendedUpdateMs = now;
}

// Update controller telemetry at same 10Hz rate as BMS
// Gather sensor data from barometer and ESP32
float altitude = getAltitude(deviceData);
Expand Down Expand Up @@ -534,14 +542,15 @@ void bmsTask(void *pvParameters) {
}
}

if (bmsTelemetryQueue != NULL) {
xQueueOverwrite(bmsTelemetryQueue, &bmsTelemetryData);
}

if (bmsTelemetryData.bmsState == TelemetryState::CONNECTED) {
unifiedBatteryData.volts = bmsTelemetryData.battery_voltage;
unifiedBatteryData.amps = bmsTelemetryData.battery_current;
unifiedBatteryData.soc = bmsTelemetryData.soc;
unifiedBatteryData.power = bmsTelemetryData.power;
if (bmsTelemetryQueue != NULL) {
xQueueOverwrite(bmsTelemetryQueue, &bmsTelemetryData);
}
} else if (escTelemetryData.escState == TelemetryState::CONNECTED) {
unifiedBatteryData.volts = escTelemetryData.volts;
unifiedBatteryData.amps = escTelemetryData.amps;
Expand Down
Loading