Skip to content

Commit 418d132

Browse files
committed
ESC lights/beeps, baro cache, and UI sync
Introduce ESC status light modes and motor beep queuing, add cached barometer/altitude/vertical-speed getters, and reduce cross-task I2C/CAN contention. Key changes: - esc.h / esc.cpp: add EscStatusLightMode enum and APIs to request status lights and queue motor beeps; implement LED patterns, motor-tone building, and sync logic to send outputs only when ESC is connected. - altimeter.h / altimeter.cpp: add cached getters (altitude, baro temp, vertical speed), compute vertical speed from buffer, seed and retry BMP3xx init to avoid transient I2C failures, and update cache on reads to allow safe cross-task access. - alert_display.h / alert_display.cpp: track ESC/BMS-specific active alert counts and expose getEscBmsAlertCounts() used to influence ESC lights. - main.cpp: prefer cached getters in non-I2C tasks, restructure changeDeviceState to minimize mutex hold time and side effects outside critical section, use status LED constants, queue ESC beeps on arm/disarm, avoid direct CAN throttle writes from disarm, and use non-blocking queue overwrite for throttle/cruise updates. - utilities.h: rename LED macros to STATUS_LED_* for controller status semantics. - buzzer.cpp / audioTask: increase default melody duration and add short silence between notes for separation. - system_monitors.cpp: re-enable Baro_Temp monitor using cached getter to avoid I2C access from monitors. - platformio.ini / extra_script.py / .claude/settings.local.json: adjust project src_dir and build flags, update a lib dep commit hash, clean up extra_script usage, and add a couple shell checks for CI. Overall these changes improve robustness against I2C/CAN races, provide cross-task-safe sensor values, and add ESC visual/audio feedback tied to alert state.
1 parent 37e9292 commit 418d132

13 files changed

Lines changed: 404 additions & 113 deletions

.claude/settings.local.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
"Bash(cpplint:*)",
77
"Bash(grep:*)",
88
"Bash(pio test:*)",
9-
"Bash(pio run:*)"
9+
"Bash(pio run:*)",
10+
"Bash(pio --version)",
11+
"Bash(python -c \"import platformio; print\\(platformio.__file__\\)\")"
1012
],
1113
"deny": []
1214
}

extra_script.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,6 @@
22
import subprocess
33

44
Import("env")
5-
folder = env.GetProjectOption("custom_src_folder")
6-
7-
# Generic
8-
env.Replace(
9-
PROJECT_SRC_DIR="$PROJECT_DIR/src/" + folder
10-
)
115

126
def get_git_revision_short_hash():
137
try:

inc/sp140/alert_display.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ void updateAlertCounterDisplay(const AlertCounts& counts);
7373
// Helper for monitors/UI logger to push alert events
7474
void sendAlertEvent(SensorID id, AlertLevel level);
7575

76+
// Current active alert counts limited to ESC and BMS categories.
77+
AlertCounts getEscBmsAlertCounts();
78+
7679
// ILogger sink that pushes events to the alert queue (for UI)
7780
struct AlertUILogger : ILogger {
7881
void log(SensorID id, AlertLevel lvl, float v) override {

inc/sp140/altimeter.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,10 @@ float getBaroTemperature();
2929
// Get the pressure in hPa
3030
float getBaroPressure();
3131

32+
// Cached getters — safe to call from any task (atomic float reads on Xtensa).
33+
// Populated by the designated I2C reader (uiTask via getAltitude()).
34+
float getCachedAltitude();
35+
float getCachedBaroTemperature();
36+
float getCachedVerticalSpeed();
37+
3238
#endif // INC_SP140_ALTIMETER_H_

inc/sp140/esc.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ inline bool isMotorTempValidC(float tempC) {
1515
#include <SineEsc.h>
1616
#include <CanardAdapter.h>
1717

18+
enum class EscStatusLightMode : uint8_t {
19+
OFF = 0,
20+
DISARMED_NOMINAL,
21+
DISARMED_WARNING,
22+
DISARMED_CRITICAL,
23+
FLIGHT_NOMINAL,
24+
FLIGHT_WARNING,
25+
FLIGHT_CRITICAL,
26+
};
27+
1828
void initESC();
1929
void setESCThrottle(int throttlePWM);
2030
void readESCTelemetry();
@@ -58,6 +68,13 @@ bool hasMotorIDet2Error(uint16_t errorCode);
5868
bool hasSwHwIncompatError(uint16_t errorCode);
5969
bool hasBootloaderBadError(uint16_t errorCode);
6070

71+
// ESC LED control
72+
void requestEscStatusLightMode(EscStatusLightMode mode);
73+
74+
// ESC motor beep
75+
void queueEscMotorBeepArm();
76+
void queueEscMotorBeepDisarm();
77+
6178
// for debugging
6279
void dumpThrottleResponse(const sine_esc_SetThrottleSettings2Response *res);
6380
void dumpESCMessages(); // dumps all messages to USBSerial

inc/sp140/utilities.h

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
1-
#ifndef INC_SP140_UTILITIES_H_
2-
#define INC_SP140_UTILITIES_H_
3-
4-
#include <Arduino.h>
5-
6-
// Function to get unique chip ID
7-
String chipId();
1+
#ifndef INC_SP140_UTILITIES_H_
2+
#define INC_SP140_UTILITIES_H_
83

9-
// Definitions for main rainbow colors in WRGB format for NeoPixel.
10-
// The 32-bit color value is WRGB. W (White) is ignored for RGB pixels.
11-
// The next bytes are R (Red), G (Green), and B (Blue).
12-
// For example, YELLOW is 0x00FFFF00, with FF for Red and Green, and 00 for Blue.
4+
#include <Arduino.h>
135

14-
#define LED_RED 0x00FF0000
15-
#define LED_ORANGE 0x00FF7F00
16-
#define LED_YELLOW 0x00FFFF00
17-
#define LED_GREEN 0x0000FF00
18-
#define LED_BLUE 0x000000FF
19-
#define LED_INDIGO 0x004B0082
20-
#define LED_VIOLET 0x008000FF
6+
// Function to get unique chip ID
7+
String chipId();
8+
9+
// Controller status LED colors in WRGB format for NeoPixel.
10+
#define STATUS_LED_RED 0x00FF0000
11+
#define STATUS_LED_YELLOW 0x00FFFF00
12+
#define STATUS_LED_GREEN 0x0000FF00
2113

2214
#endif // INC_SP140_UTILITIES_H_

platformio.ini

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
[platformio]
1212
lib_dir = libraries
1313
include_dir = inc
14+
src_dir = src/sp140
1415
default_envs =
1516
OpenPPG-CESP32S3-CAN-SP140
1617

@@ -27,7 +28,6 @@ framework = arduino, espidf
2728
board_build.f_cpu = 240000000L
2829
board_build.flash_mode = qio
2930
board_build.partitions = default_8MB.csv
30-
custom_src_folder = sp140 ; Used by extra_script.py to rewrite PROJECT_SRC_DIR
3131
; If esptool.py complains about missing intelhex, install into the tool package:
3232
; ~/.platformio/penv/bin/python -m pip install intelhex -t ~/.platformio/packages/tool-esptoolpy
3333
extra_scripts =
@@ -42,6 +42,7 @@ build_flags =
4242
-D CORE_DEBUG_LEVEL=2
4343
-D CONFIG_ARDUINO_LOOP_STACK_SIZE=8192
4444
-Wno-error=format
45+
-Wno-error=int-in-bool-context
4546
;-D BLE_PAIR_ON_BOOT
4647
build_type = debug
4748
debug_speed = 12000
@@ -58,7 +59,7 @@ lib_deps =
5859
adafruit/Adafruit CAN@0.2.3
5960
adafruit/Adafruit MCP2515@0.2.1
6061
https://github.com/rlogiacco/CircularBuffer@1.4.0
61-
https://github.com/openppg/SINE-ESC-CAN#8caa93996b5d000fe10ca5265bd1c472dfdf885b
62+
https://github.com/openppg/SINE-ESC-CAN#2ab56a4e5b52e4456317c8ee3e3d802b232c6148
6263
https://github.com/openppg/ANT-BMS-CAN#fd54852bc6f1c9608e37af9ca7c13ea4135c095b
6364
lvgl/lvgl@^9.5.0
6465
h2zero/NimBLE-Arduino@^2.3.9
@@ -105,9 +106,9 @@ build_flags =
105106
-std=c++17
106107
build_src_filter =
107108
-<*>
108-
+<sp140/lvgl/lvgl_main_screen.cpp>
109-
+<sp140/lvgl/lvgl_updates.cpp>
110-
+<sp140/lvgl/lvgl_alerts.cpp>
109+
+<lvgl/lvgl_main_screen.cpp>
110+
+<lvgl/lvgl_updates.cpp>
111+
+<lvgl/lvgl_alerts.cpp>
111112
test_filter = test_screenshots
112113
lib_deps =
113114
lvgl/lvgl@^9.5.0

src/sp140/alert_display.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ static std::map<SensorID, AlertLevel> g_currentLevels;
1818
static AlertCounts g_currentCounts = {0, 0};
1919
static AlertCounts g_previousCounts = {0, 0}; // Track previous counts for transitions
2020
static uint32_t g_epoch = 0;
21+
static AlertCounts g_escBmsCounts = {0, 0};
2122

2223
// Rotation state for display - separate lists for warnings and criticals
2324
static std::vector<SensorID> g_critList;
@@ -31,6 +32,12 @@ static void alertAggregationTask(void* parameter);
3132
static void recalcCountsAndPublish();
3233
static void handleAlertVibration(const AlertCounts& newCounts, const AlertCounts& previousCounts);
3334

35+
static bool isEscOrBmsSensor(SensorID id) {
36+
return id >= SensorID::ESC_MOS_Temp && id <= SensorID::BMS_CAN_Init_Failure;
37+
}
38+
39+
AlertCounts getEscBmsAlertCounts() { return g_escBmsCounts; }
40+
3441
// ------------ Public helpers -------------
3542
void initAlertDisplay() {
3643
// Create queues – small, non-blocking
@@ -131,25 +138,33 @@ static void alertAggregationTask(void* parameter) {
131138
static void recalcCountsAndPublish() {
132139
AlertCounts counts{0, 0};
133140
std::vector<SensorID> newCritList;
141+
AlertCounts escBmsCounts{0, 0};
134142
std::vector<SensorID> newWarnList;
135143
for (const auto& kv : g_currentLevels) {
136144
switch (kv.second) {
137145
case AlertLevel::WARN_LOW:
138146
case AlertLevel::WARN_HIGH:
139147
newWarnList.push_back(kv.first);
140148
counts.warningCount++;
149+
if (isEscOrBmsSensor(kv.first)) {
150+
escBmsCounts.warningCount++;
151+
}
141152
break;
142153
case AlertLevel::CRIT_LOW:
143154
case AlertLevel::CRIT_HIGH:
144155
newCritList.push_back(kv.first);
145156
counts.criticalCount++;
157+
if (isEscOrBmsSensor(kv.first)) {
158+
escBmsCounts.criticalCount++;
159+
}
146160
break;
147161
default:
148162
break;
149163
}
150164
}
151165

152166
// Handle vibration alerts based on count changes
167+
g_escBmsCounts = escBmsCounts;
153168
handleAlertVibration(counts, g_previousCounts);
154169
g_previousCounts = counts;
155170

src/sp140/altimeter.cpp

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,28 @@ struct AltitudeReading {
1616
// Buffer to store altitude readings with timestamps
1717
CircularBuffer<AltitudeReading, VARIO_BUFFER_SIZE> altitudeBuffer;
1818

19+
// Cached values — written by the designated I2C reader (uiTask via getAltitude),
20+
// safe to read from any task (32-bit float reads are atomic on Xtensa).
21+
static float cachedAltitude = 0.0f;
22+
static float cachedBaroTemperature = __FLT_MIN__;
23+
static float cachedVerticalSpeed = 0.0f;
24+
25+
// Recompute vertical speed from the altitude buffer. Must be called from the
26+
// same task that pushes to altitudeBuffer (uiTask) to avoid CircularBuffer races.
27+
static float computeVerticalSpeed() {
28+
if (altitudeBuffer.size() < 2) {
29+
return 0.0f;
30+
}
31+
const AltitudeReading& oldest = altitudeBuffer.first();
32+
const AltitudeReading& newest = altitudeBuffer.last();
33+
float timeDiff = (newest.timestamp - oldest.timestamp) / 1000.0f;
34+
if (timeDiff <= 0) {
35+
return 0.0f;
36+
}
37+
float verticalSpeed = (newest.altitude - oldest.altitude) / timeDiff;
38+
return constrain(verticalSpeed, -MAX_VERTICAL_SPEED, MAX_VERTICAL_SPEED);
39+
}
40+
1941
float getAltitude(const STR_DEVICE_DATA_140_V1& deviceData) {
2042
if (bmpPresent && i2cMutex != nullptr) {
2143
if (xSemaphoreTake(i2cMutex, pdMS_TO_TICKS(20)) == pdTRUE) {
@@ -27,6 +49,10 @@ float getAltitude(const STR_DEVICE_DATA_140_V1& deviceData) {
2749
AltitudeReading reading = {relativeAltitude, millis()};
2850
altitudeBuffer.push(reading);
2951

52+
// Update caches (atomic 32-bit float writes on Xtensa)
53+
cachedAltitude = relativeAltitude;
54+
cachedVerticalSpeed = computeVerticalSpeed();
55+
3056
return relativeAltitude;
3157
}
3258
}
@@ -72,12 +98,18 @@ float getBaroTemperature() {
7298
if (xSemaphoreTake(i2cMutex, pdMS_TO_TICKS(20)) == pdTRUE) {
7399
float temp = bmp.readTemperature();
74100
xSemaphoreGive(i2cMutex);
101+
cachedBaroTemperature = temp; // Update cache for cross-task reads
75102
return temp;
76103
}
77104
}
78105
return __FLT_MIN__;
79106
}
80107

108+
// Cached getters — safe to call from any task (atomic float reads on Xtensa).
109+
float getCachedAltitude() { return cachedAltitude; }
110+
float getCachedBaroTemperature() { return cachedBaroTemperature; }
111+
float getCachedVerticalSpeed() { return cachedVerticalSpeed; }
112+
81113
// Get the pressure in hPa
82114
float getBaroPressure() {
83115
if (bmpPresent && i2cMutex != nullptr) {
@@ -97,13 +129,37 @@ bool setupAltimeter() {
97129
// pull down pin 40 to high to set the address
98130
pinMode(40, OUTPUT);
99131
digitalWrite(40, HIGH);
100-
if (!bmp.begin_I2C(BMP3XX_DEFAULT_ADDRESS, &Wire)) return false;
132+
133+
// Give the BMP3xx time to finish powering up after cold boot.
134+
// Without this, the first begin_I2C() occasionally fails before the
135+
// sensor is ready, leaving bmpPresent=false for the whole session and
136+
// tripping the Baro_Init_Failure alert.
137+
delay(20);
138+
139+
// Retry a few times — most failures here are transient I2C timing races.
140+
const int kInitAttempts = 4;
141+
bool ok = false;
142+
for (int i = 1; i <= kInitAttempts; i++) {
143+
if (bmp.begin_I2C(BMP3XX_DEFAULT_ADDRESS, &Wire)) {
144+
ok = true;
145+
if (i > 1) {
146+
USBSerial.printf("BMP3xx initialized on attempt %d\n", i);
147+
}
148+
break;
149+
}
150+
USBSerial.printf("BMP3xx init attempt %d/%d failed\n", i, kInitAttempts);
151+
delay(50);
152+
}
153+
if (!ok) return false;
101154

102155
bmp.setOutputDataRate(BMP3_ODR_25_HZ);
103156
bmp.setTemperatureOversampling(BMP3_OVERSAMPLING_2X);
104157
bmp.setPressureOversampling(BMP3_OVERSAMPLING_4X);
105158
bmp.setIIRFilterCoeff(BMP3_IIR_FILTER_COEFF_15);
106159
bmp.readPressure(); // throw away first reading
160+
// Seed the temperature cache so monitors and telemetry don't see __FLT_MIN__
161+
// until the first periodic read populates it.
162+
cachedBaroTemperature = bmp.readTemperature();
107163
bmpPresent = true;
108164
return true;
109165
}

src/sp140/buzzer.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ bool playMelody(uint16_t melody[], int siz) {
7979
MelodyRequest request = {
8080
.notes = melodyBuffer,
8181
.size = (uint8_t)std::min(siz, 32),
82-
.duration = 100 // Default duration
82+
.duration = 200 // Default duration
8383
};
8484

8585
// Send to queue with timeout

0 commit comments

Comments
 (0)