@@ -16,6 +16,126 @@ static uint8_t memory_pool[1024] __attribute__((aligned(8)));
1616static SineEsc esc (adapter);
1717static unsigned long lastSuccessfulCommTimeMs = 0 ; // Store millis() time of last successful ESC comm
1818
19+ enum class PendingEscTone : uint8_t {
20+ NONE = 0 ,
21+ ARM,
22+ DISARM,
23+ };
24+
25+ static volatile EscStatusLightMode sRequestedStatusLightMode =
26+ EscStatusLightMode::OFF;
27+ static EscStatusLightMode sLastSentStatusLightMode = EscStatusLightMode::OFF;
28+ static unsigned long sLastStatusLightSendMs = 0 ;
29+ static bool sHaveSentStatusLight = false ;
30+ static volatile PendingEscTone sPendingEscTone = PendingEscTone::NONE;
31+
32+ namespace {
33+
34+ constexpr uint8_t kEscToneLow = 3 ;
35+ constexpr uint8_t kEscToneHigh = 6 ;
36+ constexpr uint8_t kEscToneVolumePct = 80 ;
37+ constexpr uint8_t kEscToneDuration10ms = 10 ;
38+
39+ // Caller must pass ARM or DISARM (never NONE).
40+ void buildEscMotorTone (uint8_t * out, PendingEscTone tone) {
41+ if (tone == PendingEscTone::ARM) {
42+ SineEsc::makeBeepEntry (&out[0 ], kEscToneLow , kEscToneDuration10ms , kEscToneVolumePct );
43+ SineEsc::makeBeepEntry (&out[3 ], kEscToneHigh , kEscToneDuration10ms , kEscToneVolumePct );
44+ } else {
45+ SineEsc::makeBeepEntry (&out[0 ], kEscToneHigh , kEscToneDuration10ms , kEscToneVolumePct );
46+ SineEsc::makeBeepEntry (&out[3 ], kEscToneLow , kEscToneDuration10ms , kEscToneVolumePct );
47+ }
48+ }
49+
50+ unsigned long escStatusLightRefreshMs (EscStatusLightMode mode) {
51+ switch (mode) {
52+ case EscStatusLightMode::FLIGHT:
53+ return 1700 ;
54+ case EscStatusLightMode::READY:
55+ case EscStatusLightMode::CAUTION:
56+ return 1000 ;
57+ case EscStatusLightMode::OFF:
58+ default :
59+ return 0 ;
60+ }
61+ }
62+
63+ void sendEscStatusLight (EscStatusLightMode mode) {
64+ switch (mode) {
65+ case EscStatusLightMode::READY: {
66+ const uint16_t pattern[] = {
67+ SineEsc::makeLedControlEntry (SineEsc::LED_GREEN_BREATH, 20 ),
68+ };
69+ esc.setLedControl (pattern, 1 );
70+ break ;
71+ }
72+ case EscStatusLightMode::FLIGHT: {
73+ const uint16_t pattern[] = {
74+ SineEsc::makeLedControlEntry (SineEsc::LED_GREEN, 1 ),
75+ SineEsc::makeLedControlEntry (SineEsc::LED_OFF, 2 ),
76+ SineEsc::makeLedControlEntry (SineEsc::LED_GREEN, 1 ),
77+ SineEsc::makeLedControlEntry (SineEsc::LED_OFF, 30 ),
78+ };
79+ esc.setLedControl (pattern, 4 );
80+ break ;
81+ }
82+ case EscStatusLightMode::CAUTION: {
83+ const uint16_t pattern[] = {
84+ SineEsc::makeLedControlEntry (SineEsc::LED_YELLOW_BREATH, 20 ),
85+ };
86+ esc.setLedControl (pattern, 1 );
87+ break ;
88+ }
89+ case EscStatusLightMode::OFF:
90+ default : {
91+ const uint16_t pattern[] = {
92+ SineEsc::makeLedControlEntry (SineEsc::LED_OFF, 20 ),
93+ };
94+ esc.setLedControl (pattern, 1 );
95+ break ;
96+ }
97+ }
98+ }
99+
100+ void syncEscOutputs () {
101+ const bool escConnected =
102+ escTwaiInitialized &&
103+ escTelemetryData.escState == TelemetryState::CONNECTED;
104+
105+ if (!escConnected) {
106+ sPendingEscTone = PendingEscTone::NONE;
107+ sHaveSentStatusLight = false ;
108+ sLastSentStatusLightMode = EscStatusLightMode::OFF;
109+ sLastStatusLightSendMs = 0 ;
110+ return ;
111+ }
112+
113+ const PendingEscTone pendingTone = sPendingEscTone ;
114+ if (pendingTone != PendingEscTone::NONE) {
115+ uint8_t beepData[6 ];
116+ buildEscMotorTone (beepData, pendingTone);
117+ esc.setMotorSound (beepData, 2 );
118+ sPendingEscTone = PendingEscTone::NONE;
119+ }
120+
121+ const EscStatusLightMode requestedMode = sRequestedStatusLightMode ;
122+ const unsigned long now = millis ();
123+ const bool needsRefresh =
124+ sHaveSentStatusLight &&
125+ escStatusLightRefreshMs (requestedMode) > 0 &&
126+ (now - sLastStatusLightSendMs ) >= escStatusLightRefreshMs (requestedMode);
127+
128+ if (!sHaveSentStatusLight || requestedMode != sLastSentStatusLightMode ||
129+ needsRefresh) {
130+ sendEscStatusLight (requestedMode);
131+ sLastSentStatusLightMode = requestedMode;
132+ sLastStatusLightSendMs = now;
133+ sHaveSentStatusLight = true ;
134+ }
135+ }
136+
137+ } // namespace
138+
19139
20140STR_ESC_TELEMETRY_140 escTelemetryData = {
21141 .escState = TelemetryState::NOT_CONNECTED,
@@ -95,15 +215,15 @@ void readESCTelemetry() {
95215 escTelemetryData.mos_temp = res->mos_temp / 10 .0f ;
96216 escTelemetryData.cap_temp = res->cap_temp / 10 .0f ;
97217 escTelemetryData.mcu_temp = res->mcu_temp / 10 .0f ;
98- // Filter motor temp - only update if sensor is connected (valid range: -20°C to 140°C)
99- // Disconnected sensor reads ~149°C (thermistor pulled high)
100- float rawMotorTemp = res->motor_temp / 10 .0f ;
101- if (isMotorTempValidC (rawMotorTemp)) {
102- escTelemetryData.motor_temp = rawMotorTemp;
103- } else {
104- // Store invalid motor temp as NaN. Downstream consumers can skip on isnan().
105- escTelemetryData.motor_temp = NAN;
106- }
218+ // Filter motor temp - only update if sensor is connected (valid range: -20°C to 140°C)
219+ // Disconnected sensor reads ~149°C (thermistor pulled high)
220+ float rawMotorTemp = res->motor_temp / 10 .0f ;
221+ if (isMotorTempValidC (rawMotorTemp)) {
222+ escTelemetryData.motor_temp = rawMotorTemp;
223+ } else {
224+ // Store invalid motor temp as NaN. Downstream consumers can skip on isnan().
225+ escTelemetryData.motor_temp = NAN;
226+ }
107227 escTelemetryData.eRPM = res->speed ;
108228 escTelemetryData.inPWM = res->recv_pwm / 10 .0f ;
109229 watts = escTelemetryData.amps * escTelemetryData.volts ;
@@ -138,6 +258,7 @@ void readESCTelemetry() {
138258 }
139259 }
140260
261+ syncEscOutputs ();
141262 adapter.processTxRxOnce (); // Process CAN messages
142263}
143264
@@ -204,6 +325,18 @@ bool setupTWAI() {
204325 return true ;
205326}
206327
328+ void requestEscStatusLightMode (EscStatusLightMode mode) {
329+ sRequestedStatusLightMode = mode;
330+ }
331+
332+ void queueEscMotorBeepArm () {
333+ sPendingEscTone = PendingEscTone::ARM;
334+ }
335+
336+ void queueEscMotorBeepDisarm () {
337+ sPendingEscTone = PendingEscTone::DISARM;
338+ }
339+
207340/* *
208341 * Debug function to dump ESC throttle response data to serial
209342 * @param res Pointer to the throttle response structure from ESC
@@ -301,10 +434,10 @@ double mapDouble(double x, double in_min, double in_max, double out_min, double
301434 return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
302435}
303436
304- /* *
305- * Decode running error bitmask into human-readable string
306- * @param errorCode 16-bit running error code from ESC
307- * @return String containing decoded error messages
437+ /* *
438+ * Decode running error bitmask into human-readable string
439+ * @param errorCode 16-bit running error code from ESC
440+ * @return String containing decoded error messages
308441 */
309442String decodeRunningError (uint16_t errorCode) {
310443 if (errorCode == 0 ) {
0 commit comments