|
11 | 11 |
|
12 | 12 | #define TELEMETRY_TIMEOUT_MS 50 // Threshold to determine stale ESC telemetry in ms |
13 | 13 |
|
14 | | -static CanardAdapter adapter; |
15 | | -static uint8_t memory_pool[1024] __attribute__((aligned(8))); |
16 | | -static SineEsc esc(adapter); |
17 | | -static unsigned long lastSuccessfulCommTimeMs = 0; // Store millis() time of last successful ESC comm |
18 | | -// Flag set by requestEscHardwareInfo() (may be called from BLE task), |
19 | | -// consumed safely inside readESCTelemetry() on the throttle task. |
20 | | -static volatile bool s_hwInfoRequested = false; |
21 | | - |
22 | | -// ESC runtime accumulation — unwraps the uint16 time_10ms counter (~10.9 min period) |
23 | | -// Unsigned subtraction naturally handles wrap: (uint16_t)(current - last) is correct even across rollover. |
24 | | -static uint16_t sEscLastTime10ms = 0; |
25 | | -static uint32_t sEscAccumulatedRuntimeMs = 0; |
26 | | -static bool sEscFirstUpdate = true; |
27 | | - |
28 | | - |
29 | | -STR_ESC_TELEMETRY_140 escTelemetryData = { |
30 | | - .escState = TelemetryState::NOT_CONNECTED, |
31 | | - .running_error = 0, |
| 14 | +static CanardAdapter adapter; |
| 15 | +static uint8_t memory_pool[1024] __attribute__((aligned(8))); |
| 16 | +static SineEsc esc(adapter); |
| 17 | +static unsigned long lastSuccessfulCommTimeMs = 0; // Store millis() time of last successful ESC comm |
| 18 | +// Flag set by requestEscHardwareInfo() (may be called from BLE task), |
| 19 | +// consumed safely inside readESCTelemetry() on the throttle task. |
| 20 | +static volatile bool s_hwInfoRequested = false; |
| 21 | + |
| 22 | +enum class PendingEscTone : uint8_t { |
| 23 | + NONE = 0, |
| 24 | + ARM, |
| 25 | + DISARM, |
| 26 | +}; |
| 27 | + |
| 28 | +static volatile EscStatusLightMode sRequestedStatusLightMode = |
| 29 | + EscStatusLightMode::OFF; |
| 30 | +static EscStatusLightMode sLastSentStatusLightMode = EscStatusLightMode::OFF; |
| 31 | +static unsigned long sLastStatusLightSendMs = 0; |
| 32 | +static bool sHaveSentStatusLight = false; |
| 33 | +static volatile PendingEscTone sPendingEscTone = PendingEscTone::NONE; |
| 34 | + |
| 35 | +// ESC runtime accumulation — unwraps the uint16 time_10ms counter (~10.9 min period) |
| 36 | +// Unsigned subtraction naturally handles wrap: (uint16_t)(current - last) is correct even across rollover. |
| 37 | +static uint16_t sEscLastTime10ms = 0; |
| 38 | +static uint32_t sEscAccumulatedRuntimeMs = 0; |
| 39 | +static bool sEscFirstUpdate = true; |
| 40 | + |
| 41 | +namespace { |
| 42 | + |
| 43 | +constexpr uint8_t kEscToneVolumePct = 50; |
| 44 | +constexpr uint8_t kEscToneDuration10ms = 10; |
| 45 | + |
| 46 | +void buildEscMotorTone(uint8_t* out, PendingEscTone tone) { |
| 47 | + if (tone == PendingEscTone::ARM) { |
| 48 | + SineEsc::makeBeepEntry(&out[0], 3, kEscToneDuration10ms, kEscToneVolumePct); |
| 49 | + SineEsc::makeBeepEntry(&out[3], 6, kEscToneDuration10ms, kEscToneVolumePct); |
| 50 | + } else { |
| 51 | + SineEsc::makeBeepEntry(&out[0], 6, kEscToneDuration10ms, kEscToneVolumePct); |
| 52 | + SineEsc::makeBeepEntry(&out[3], 3, kEscToneDuration10ms, kEscToneVolumePct); |
| 53 | + } |
| 54 | +} |
| 55 | + |
| 56 | +unsigned long escStatusLightRefreshMs(EscStatusLightMode mode) { |
| 57 | + switch (mode) { |
| 58 | + case EscStatusLightMode::FLIGHT: |
| 59 | + return 1700; |
| 60 | + case EscStatusLightMode::READY: |
| 61 | + case EscStatusLightMode::CAUTION: |
| 62 | + return 1000; |
| 63 | + case EscStatusLightMode::OFF: |
| 64 | + default: |
| 65 | + return 0; |
| 66 | + } |
| 67 | +} |
| 68 | + |
| 69 | +void sendEscStatusLight(EscStatusLightMode mode) { |
| 70 | + switch (mode) { |
| 71 | + case EscStatusLightMode::READY: { |
| 72 | + const uint16_t pattern[] = { |
| 73 | + SineEsc::makeLedControlEntry(SineEsc::LED_GREEN_BREATH, 20), |
| 74 | + }; |
| 75 | + esc.setLedControl(pattern, 1); |
| 76 | + break; |
| 77 | + } |
| 78 | + case EscStatusLightMode::FLIGHT: { |
| 79 | + const uint16_t pattern[] = { |
| 80 | + SineEsc::makeLedControlEntry(SineEsc::LED_GREEN, 1), |
| 81 | + SineEsc::makeLedControlEntry(SineEsc::LED_OFF, 2), |
| 82 | + SineEsc::makeLedControlEntry(SineEsc::LED_GREEN, 1), |
| 83 | + SineEsc::makeLedControlEntry(SineEsc::LED_OFF, 30), |
| 84 | + }; |
| 85 | + esc.setLedControl(pattern, 4); |
| 86 | + break; |
| 87 | + } |
| 88 | + case EscStatusLightMode::CAUTION: { |
| 89 | + const uint16_t pattern[] = { |
| 90 | + SineEsc::makeLedControlEntry(SineEsc::LED_YELLOW_BREATH, 14), |
| 91 | + }; |
| 92 | + esc.setLedControl(pattern, 1); |
| 93 | + break; |
| 94 | + } |
| 95 | + case EscStatusLightMode::OFF: |
| 96 | + default: { |
| 97 | + const uint16_t pattern[] = { |
| 98 | + SineEsc::makeLedControlEntry(SineEsc::LED_OFF, 20), |
| 99 | + }; |
| 100 | + esc.setLedControl(pattern, 1); |
| 101 | + break; |
| 102 | + } |
| 103 | + } |
| 104 | +} |
| 105 | + |
| 106 | +void syncEscOutputs() { |
| 107 | + const bool escConnected = |
| 108 | + escTwaiInitialized && |
| 109 | + escTelemetryData.escState == TelemetryState::CONNECTED; |
| 110 | + |
| 111 | + if (!escConnected) { |
| 112 | + sPendingEscTone = PendingEscTone::NONE; |
| 113 | + sHaveSentStatusLight = false; |
| 114 | + sLastSentStatusLightMode = EscStatusLightMode::OFF; |
| 115 | + sLastStatusLightSendMs = 0; |
| 116 | + return; |
| 117 | + } |
| 118 | + |
| 119 | + const PendingEscTone pendingTone = sPendingEscTone; |
| 120 | + if (pendingTone != PendingEscTone::NONE) { |
| 121 | + uint8_t beepData[6]; |
| 122 | + buildEscMotorTone(beepData, pendingTone); |
| 123 | + esc.setMotorSound(beepData, 2); |
| 124 | + sPendingEscTone = PendingEscTone::NONE; |
| 125 | + } |
| 126 | + |
| 127 | + const EscStatusLightMode requestedMode = sRequestedStatusLightMode; |
| 128 | + const unsigned long now = millis(); |
| 129 | + const bool needsRefresh = |
| 130 | + sHaveSentStatusLight && |
| 131 | + escStatusLightRefreshMs(requestedMode) > 0 && |
| 132 | + (now - sLastStatusLightSendMs) >= escStatusLightRefreshMs(requestedMode); |
| 133 | + |
| 134 | + if (!sHaveSentStatusLight || requestedMode != sLastSentStatusLightMode || |
| 135 | + needsRefresh) { |
| 136 | + sendEscStatusLight(requestedMode); |
| 137 | + sLastSentStatusLightMode = requestedMode; |
| 138 | + sLastStatusLightSendMs = now; |
| 139 | + sHaveSentStatusLight = true; |
| 140 | + } |
| 141 | +} |
| 142 | + |
| 143 | +} // namespace |
| 144 | + |
| 145 | + |
| 146 | +STR_ESC_TELEMETRY_140 escTelemetryData = { |
| 147 | + .escState = TelemetryState::NOT_CONNECTED, |
| 148 | + .running_error = 0, |
32 | 149 | .selfcheck_error = 0 |
33 | 150 | }; |
34 | 151 |
|
@@ -174,16 +291,17 @@ void readESCTelemetry() { |
174 | 291 | } |
175 | 292 |
|
176 | 293 | // Populate static hardware info whenever available (comes in once after boot) |
177 | | - if (model.hasGetHardwareInfoResponse) { |
178 | | - const sine_esc_GetHwInfoResponse *hw = &model.getHardwareInfoResponse; |
179 | | - escTelemetryData.hardware_id = hw->hardware_id; |
180 | | - escTelemetryData.fw_version = hw->app_version; |
181 | | - escTelemetryData.bootloader_version = hw->bootloader_version; |
182 | | - memcpy(escTelemetryData.sn_code, hw->sn_code, sizeof(escTelemetryData.sn_code)); |
183 | | - } |
184 | | - |
185 | | - adapter.processTxRxOnce(); // Process CAN messages |
186 | | -} |
| 294 | + if (model.hasGetHardwareInfoResponse) { |
| 295 | + const sine_esc_GetHwInfoResponse *hw = &model.getHardwareInfoResponse; |
| 296 | + escTelemetryData.hardware_id = hw->hardware_id; |
| 297 | + escTelemetryData.fw_version = hw->app_version; |
| 298 | + escTelemetryData.bootloader_version = hw->bootloader_version; |
| 299 | + memcpy(escTelemetryData.sn_code, hw->sn_code, sizeof(escTelemetryData.sn_code)); |
| 300 | + } |
| 301 | + |
| 302 | + syncEscOutputs(); |
| 303 | + adapter.processTxRxOnce(); // Process CAN messages |
| 304 | +} |
187 | 305 |
|
188 | 306 | /** |
189 | 307 | * Request ESC hardware info (HW ID, FW version, bootloader version, serial number). |
@@ -263,87 +381,27 @@ bool setupTWAI() { |
263 | 381 | * @param ledData Array of packed LED entries (use SineEsc::makeLedControlEntry) |
264 | 382 | * @param ledNum Number of entries (max 10) |
265 | 383 | */ |
266 | | -void setESCLedControl(const uint16_t *ledData, uint8_t ledNum) { |
267 | | - if (!escTwaiInitialized) return; |
268 | | - esc.setLedControl(ledData, ledNum); |
269 | | -} |
270 | | - |
271 | | -/** |
272 | | - * Send a motor beep sequence to the ESC |
273 | | - * @param beepData Raw byte array of 3-byte entries (tone, duration_10ms, volume) |
274 | | - * @param beepDataLen Length of beepData in bytes (entries * 3) |
275 | | - * @param beepNum Number of beep entries |
276 | | - */ |
277 | | -void setESCMotorSound(const uint8_t *beepData, uint8_t beepDataLen, uint8_t beepNum) { |
278 | | - if (!escTwaiInitialized) return; |
279 | | - esc.setMotorSound(beepData, beepDataLen, beepNum); |
280 | | -} |
281 | | - |
282 | | -// Helper to pack a beep entry into 3 bytes in a buffer |
283 | | -// tone: 0-9, duration: in 10ms units, volume: 0-100 |
284 | | -static void packBeepEntry(uint8_t *buf, uint8_t tone, uint8_t duration10ms, uint8_t volume) { |
285 | | - buf[0] = tone; |
286 | | - buf[1] = duration10ms; |
287 | | - buf[2] = volume; |
288 | | -} |
289 | | - |
290 | | -/** |
291 | | - * Play ascending arm beep on ESC motor (matches hand controller: C7 -> E7) |
292 | | - * Uses two tones at 100ms each, ascending pitch |
293 | | - */ |
294 | | -void escMotorBeepArm() { |
295 | | - uint8_t beepData[6]; |
296 | | - packBeepEntry(&beepData[0], 3, 10, 50); // lower tone, 100ms, 50% vol |
297 | | - packBeepEntry(&beepData[3], 6, 10, 50); // higher tone, 100ms, 50% vol |
298 | | - setESCMotorSound(beepData, 6, 2); |
299 | | - USBSerial.println("ESC: arm beep sent"); |
300 | | -} |
301 | | - |
302 | | -/** |
303 | | - * Play descending disarm beep on ESC motor (matches hand controller: E7 -> C7) |
304 | | - * Uses two tones at 100ms each, descending pitch |
305 | | - */ |
306 | | -void escMotorBeepDisarm() { |
307 | | - uint8_t beepData[6]; |
308 | | - packBeepEntry(&beepData[0], 6, 10, 50); // higher tone, 100ms, 50% vol |
309 | | - packBeepEntry(&beepData[3], 3, 10, 50); // lower tone, 100ms, 50% vol |
310 | | - setESCMotorSound(beepData, 6, 2); |
311 | | - USBSerial.println("ESC: disarm beep sent"); |
312 | | -} |
313 | | - |
314 | | -// FreeRTOS task: aviation strobe pattern |
315 | | -// Quick red pulse, quick green pulse, then off for ~1.5s, repeat |
316 | | -static void escLedStrobeTask(void *pvParameters) { |
317 | | - // Wait for ESC connection before starting |
318 | | - vTaskDelay(pdMS_TO_TICKS(3000)); |
319 | | - |
320 | | - const uint16_t pattern[] = { |
321 | | - SineEsc::makeLedControlEntry(SineEsc::LED_RED, 1), // red flash 50ms |
322 | | - SineEsc::makeLedControlEntry(SineEsc::LED_OFF, 2), // gap 100ms |
323 | | - SineEsc::makeLedControlEntry(SineEsc::LED_GREEN, 1), // green flash 50ms |
324 | | - SineEsc::makeLedControlEntry(SineEsc::LED_OFF, 30), // off 1500ms |
325 | | - }; |
326 | | - |
327 | | - USBSerial.println("ESC LED aviation strobe started"); |
328 | | - |
329 | | - for (;;) { |
330 | | - setESCLedControl(pattern, 4); |
331 | | - USBSerial.println("ESC LED: sent aviation strobe"); |
332 | | - // Wait for full cycle (50 + 100 + 50 + 1500 = 1700ms) before resending |
333 | | - vTaskDelay(pdMS_TO_TICKS(1700)); |
334 | | - } |
335 | | -} |
336 | | - |
337 | | -static TaskHandle_t escLedStrobeTaskHandle = NULL; |
338 | | - |
339 | | -/** |
340 | | - * Start the ESC LED strobe test pattern |
341 | | - * Creates a FreeRTOS task that repeatedly sends the strobe sequence |
342 | | - */ |
343 | | -void startESCLedStrobeTest() { |
344 | | - if (escLedStrobeTaskHandle != NULL) return; // already running |
345 | | - xTaskCreate(escLedStrobeTask, "ESCLedTest", 2048, NULL, 1, &escLedStrobeTaskHandle); |
346 | | -} |
| 384 | +void setESCLedControl(const uint16_t *ledData, uint8_t ledNum) { |
| 385 | + if (!escTwaiInitialized) return; |
| 386 | + esc.setLedControl(ledData, ledNum); |
| 387 | +} |
| 388 | + |
| 389 | +void requestEscStatusLightMode(EscStatusLightMode mode) { |
| 390 | + sRequestedStatusLightMode = mode; |
| 391 | +} |
| 392 | + |
| 393 | +void setESCMotorSound(const uint8_t *beepData, uint8_t beepNum) { |
| 394 | + if (!escTwaiInitialized) return; |
| 395 | + esc.setMotorSound(beepData, beepNum); |
| 396 | +} |
| 397 | + |
| 398 | +void queueEscMotorBeepArm() { |
| 399 | + sPendingEscTone = PendingEscTone::ARM; |
| 400 | +} |
| 401 | + |
| 402 | +void queueEscMotorBeepDisarm() { |
| 403 | + sPendingEscTone = PendingEscTone::DISARM; |
| 404 | +} |
347 | 405 |
|
348 | 406 | /** |
349 | 407 | * Debug function to dump ESC throttle response data to serial |
|
0 commit comments