diff --git a/src/A2DP.ino b/src/A2DP.ino index 671696a..4d458bf 100644 --- a/src/A2DP.ino +++ b/src/A2DP.ino @@ -7,6 +7,7 @@ volatile bool md_album_recvd=0, md_artist_recvd=0, md_title_recvd=0; // updates the buffers void avrc_metadata_callback(uint8_t md_type, const uint8_t *data2) { // fills the song title buffer with data, updates text_lenght with the amount of chars + xSemaphoreTake(CAN_MsgSemaphore, portMAX_DELAY); // take the semaphore as a way to prevent the buffers being accessed elsewhere switch(md_type){ case 0x1: memset(title_buffer, 0, sizeof(title_buffer)); snprintf(title_buffer, sizeof(title_buffer), "%s", data2); @@ -25,6 +26,7 @@ void avrc_metadata_callback(uint8_t md_type, const uint8_t *data2) { // fills t break; default: break; } + xSemaphoreGive(CAN_MsgSemaphore); if(md_title_recvd && md_artist_recvd && md_album_recvd){ DIS_forceUpdate=1; // lets the main loop() know that there's a new song title in the buffer md_title_recvd=0; @@ -69,8 +71,8 @@ void a2dp_init(){ a2dp_sink.start("EHU32"); // setting up bluetooth audio sink a2dp_started=1; DEBUG_PRINTLN("A2DP: Started!"); - disp_mode=0; // set display mode to audio metadata on boot - writeTextToDisplay(1, "EHU32 v0.9.1 started!", "Bluetooth on", "Waiting for connection..."); + disp_mode=-1; // set display mode to audio metadata on boot + writeTextToDisplay(1, "EHU32 v0.9.2 started!", "Bluetooth on", "Waiting for connection..."); } // handles events such as connecion/disconnection and audio play/pause @@ -78,27 +80,31 @@ void A2DP_EventHandler(){ if(ehu_started && !a2dp_started){ // this enables bluetooth A2DP service only after the radio is started a2dp_init(); } - - if(audio_state_changed && bt_connected){ // mute external DAC when not playing; bt_connected ensures no "Connected, paused" is displayed, seems that the audio_state_changed callback comes late - if(bt_audio_playing){ - digitalWrite(PCM_MUTE_CTL, HIGH); - DIS_forceUpdate=1; // force reprinting of audio metadata when the music is playing - } else { - digitalWrite(PCM_MUTE_CTL, LOW); - writeTextToDisplay(1, "Bluetooth connected", "", "Paused"); - } - audio_state_changed=0; - } if(bt_state_changed){ // mute external DAC when not playing if(bt_connected){ a2dp_sink.set_volume(127); // workaround to ensure max volume being applied on successful connection writeTextToDisplay(1, "Bluetooth connected", "", (char*)a2dp_sink.get_peer_name()); + if(disp_mode==-1) disp_mode=0; } else { writeTextToDisplay(1, "Bluetooth disconnected", "", ""); + if(disp_mode==0) disp_mode=-1; } bt_state_changed=0; } + + if(audio_state_changed && bt_connected){ // mute external DAC when not playing; bt_connected ensures no "Connected, paused" is displayed, seems that the audio_state_changed callback comes late + if(bt_audio_playing){ + digitalWrite(PCM_MUTE_CTL, HIGH); + DIS_forceUpdate=1; // force reprinting of audio metadata when the music is playing + if(disp_mode==-1) disp_mode=0; + } else { + digitalWrite(PCM_MUTE_CTL, LOW); + writeTextToDisplay(1, "Bluetooth connected", "", "Paused"); + if(disp_mode==0) disp_mode=-1; + } + audio_state_changed=0; + } } // ID 0x501 DB3 0x18 indicates imminent shutdown of the radio and display; disconnect from source diff --git a/src/CAN.ino b/src/CAN.ino index 5b70907..69a683e 100644 --- a/src/CAN.ino +++ b/src/CAN.ino @@ -53,7 +53,7 @@ void canReceiveTask(void *pvParameters){ if(disp_mode!=-1){ // don't bother checking the data if there's no need to update the display if(Recvd_CAN_MSG.data[0]==0x10 && (Recvd_CAN_MSG.data[1]<0x40 || (Recvd_CAN_MSG.data[1]<0x4F && Recvd_CAN_MSG.data[2]==0x50))){ // we check if the total payload of radio's message is small, if yes assume it's an Aux message DEBUG_PRINTLN("CAN: Received display update, trying to block"); - twai_transmit(&Msg_PreventDisplayUpdate, pdMS_TO_TICKS(10)); // radio blocking msg has to be transmitted ASAP, which is why we skip the queue + twai_transmit(&Msg_PreventDisplayUpdate, pdMS_TO_TICKS(30)); // radio blocking msg has to be transmitted ASAP, which is why we skip the queue twai_read_alerts(&alerts_triggered, pdMS_TO_TICKS(10)); // read stats if(alerts_triggered & TWAI_ALERT_TX_SUCCESS){ CAN_flowCtlFail=0; @@ -62,7 +62,7 @@ void canReceiveTask(void *pvParameters){ CAN_flowCtlFail=1; DEBUG_PRINTLN("CAN: Blocking failed!"); } - if(disp_mode==0) vTaskResume(canDisplayTaskHandle); // only retransmit the msg for audio metadata mode + if(disp_mode==0 || disp_mode==2) vTaskResume(canDisplayTaskHandle); // only retransmit the msg for audio metadata mode and single line coolant, since these don't update frequently } } } @@ -136,7 +136,7 @@ void canProcessTask(void *pvParameters){ break; } case 0x208: { // AC panel button event - if((RxMsg.data[0]==0x0) && (RxMsg.data[1]==0x17) && (RxMsg.data[2]<0x02)){ // FIXME! + if((RxMsg.data[0]==0x0) && (RxMsg.data[1]==0x17) && (RxMsg.data[2]<0x01)){ // FIXME! vTaskResume(canAirConMacroTaskHandle); // start AC macro } break; @@ -150,13 +150,19 @@ void canProcessTask(void *pvParameters){ break; } case 0x546: { // display measurement blocks (used as a fallback) - switch(RxMsg.data[0]){ // measurement block ID -> update data which the message is referencing + if(disp_mode==1 || disp_mode==2) xSemaphoreTake(CAN_MsgSemaphore, portMAX_DELAY); + DEBUG_PRINT("CAN: Got measurements from DIS: "); + switch(RxMsg.data[0]){ // measurement block ID -> update data which the message is referencing, I may implement more cases in the future which is why switch is there case 0x0B: { // 0x0B references coolant temps + DEBUG_PRINT("coolant\n"); int CAN_data_coolant=RxMsg.data[5]-40; snprintf(voltage_buffer, sizeof(voltage_buffer), "No additional data available"); snprintf(coolant_buffer, sizeof(coolant_buffer), "Coolant temp: %d%c%cC ", CAN_data_coolant, 0xC2, 0xB0); //snprintf(speed_buffer, sizeof(speed_buffer), "ECC not present"); // -> speed received as part of the 0x4E8 msg CAN_coolant_recvd=1; + #ifdef DEBUG + CAN_speed_recvd=1; + #endif break; } default: break; @@ -166,15 +172,19 @@ void canProcessTask(void *pvParameters){ CAN_coolant_recvd=0; CAN_new_dataSet_recvd=1; } + if(disp_mode==1 || disp_mode==2) xSemaphoreGive(CAN_MsgSemaphore); // let the message processing continue break; } case 0x548: { // AC measurement blocks + if(disp_mode==1 || disp_mode==2) xSemaphoreTake(CAN_MsgSemaphore, portMAX_DELAY); // if we're in body data mode, take the semaphore to prevent the buffer being modified while the display message is being compiled + DEBUG_PRINT("CAN: Got measurements from ECC: "); switch(RxMsg.data[0]){ // measurement block ID -> update data which the message is referencing case 0x07: { // 0x10 references battery voltage CAN_data_voltage=RxMsg.data[2]; CAN_data_voltage/=10; snprintf(voltage_buffer, sizeof(voltage_buffer), "Voltage: %.1f V ", CAN_data_voltage); CAN_voltage_recvd=1; + DEBUG_PRINT("battery voltage\n"); break; } case 0x10: { // 0x10 references coolant temps @@ -183,6 +193,7 @@ void canProcessTask(void *pvParameters){ CAN_data_coolant/=10; snprintf(coolant_buffer, sizeof(coolant_buffer), "Coolant temp: %.1f%c%cC ", CAN_data_coolant, 0xC2, 0xB0); CAN_coolant_recvd=1; + DEBUG_PRINT("coolant\n"); break; } case 0x11: { // 0x11 references RPMs and speed @@ -190,6 +201,7 @@ void canProcessTask(void *pvParameters){ CAN_data_speed=RxMsg.data[4]; snprintf(speed_buffer, sizeof(speed_buffer), "%d km/h %d RPM ", CAN_data_speed, CAN_data_rpm); CAN_speed_recvd=1; + DEBUG_PRINT("speed and RPMs\n"); break; } default: break; @@ -200,14 +212,17 @@ void canProcessTask(void *pvParameters){ CAN_speed_recvd=0; CAN_new_dataSet_recvd=1; } + if(disp_mode==1 || disp_mode==2) xSemaphoreGive(CAN_MsgSemaphore); // let the message processing continue break; } - case 0x4E8: { // this provides speed and RPMs right from the bus - if(disp_mode==1 && !ECC_present){ + case 0x4E8: { // this provides speed and RPMs right from the bus, only for disp_mode==1 + if((disp_mode==1) && !ECC_present){ + if(disp_mode==1) xSemaphoreTake(CAN_MsgSemaphore, portMAX_DELAY); CAN_data_rpm=(RxMsg.data[2]<<8 | RxMsg.data[3]); CAN_data_speed=RxMsg.data[4]; snprintf(speed_buffer, sizeof(speed_buffer), "%d km/h %d RPM ", CAN_data_speed, CAN_data_rpm); CAN_speed_recvd=1; + if(disp_mode==1) xSemaphoreGive(CAN_MsgSemaphore); // let the message processing continue } break; } @@ -219,6 +234,7 @@ void canProcessTask(void *pvParameters){ a2dp_sink.reconnect(); ehu_started=1; } + xTaskNotifyGive(canWatchdogTaskHandle); // reset the watchdog break; } case 0x6C8: { // if any ECC module message is received, assume ECC is available to request measurement data from @@ -330,9 +346,11 @@ void CANsimTask(void *pvParameters){ } case 'd': { Serial.print("CURRENT FLAGS CAN: "); - Serial.printf("CAN_MessageReady=%d CAN_prevTxFail=%d, DIS_forceUpdate=%d, ECC_present=%d \n", CAN_MessageReady, CAN_prevTxFail, DIS_forceUpdate, ECC_present); + Serial.printf("CAN_MessageReady=%d CAN_prevTxFail=%d, DIS_forceUpdate=%d, ECC_present=%d, ehu_started=%d \n", CAN_MessageReady, CAN_prevTxFail, DIS_forceUpdate, ECC_present, ehu_started); Serial.print("CURRENT FLAGS BODY: "); Serial.printf("CAN_voltage_recvd=%d CAN_coolant_recvd=%d, CAN_speed_recvd=%d, CAN_new_dataSet_recvd=%d \n", CAN_voltage_recvd, CAN_coolant_recvd, CAN_speed_recvd, CAN_new_dataSet_recvd); + Serial.print("TIME AND STUFF: "); + Serial.printf("last_millis_req=%lu last_millis_disp=%lu, millis=%lu \n", last_millis_req, last_millis_disp, millis()); Serial.printf("CanMsgSemaphore state: %d \n", checkMutexState()); Serial.printf("TxQueue items: %d, RxQueue items: %d \n", uxQueueMessagesWaiting(canTxQueue), uxQueueMessagesWaiting(canRxQueue)); break; @@ -345,13 +363,13 @@ void CANsimTask(void *pvParameters){ } #endif -// this task waits for a flow control packet from the display +// this task implements ISO 15765-2 (multi-packet transmission over CAN frames) in a crude, but hopefully robust way in order to send frames to the display void canDisplayTask(void *pvParameters){ while(1){ - if(xSemaphoreTake(CAN_MsgSemaphore, pdMS_TO_TICKS(100))==pdTRUE){ // if the buffer is being accessed, block indefinitely + if(xSemaphoreTake(CAN_MsgSemaphore, portMAX_DELAY)==pdTRUE){ // if the buffer is being accessed, block indefinitely if(CAN_flowCtlFail){ // failed transmitting flow control before the display resulting in an error frame, wait for a bit before sending it again, skip queue vTaskDelay(pdMS_TO_TICKS(1)); - twai_transmit(&Msg_PreventDisplayUpdate, pdMS_TO_TICKS(10)); // hope for the best and send flow control again + twai_transmit(&Msg_PreventDisplayUpdate, pdMS_TO_TICKS(10)); // hope for the best and send flow control again, skip queue CAN_flowCtlFail=0; vTaskDelay(pdMS_TO_TICKS(20)); } @@ -362,9 +380,9 @@ void canDisplayTask(void *pvParameters){ sendMultiPacket(); } //xEventGroupWaitBits(CAN_Events, CAN_MessageReady, pdFALSE, pdFALSE, portMAX_DELAY); // this waits until CAN_MessageReady is set by the transmit function (only in case of a successful TX) - if(CAN_MessageReady && !CAN_prevTxFail){ // possibly not needed anymore? + if(CAN_MessageReady && !CAN_prevTxFail){ // possibly not needed anymore? nope, still needed until I'm competent enough to implement EventGroups DEBUG_PRINTLN("CAN: Now waiting for 2C1..."); - xTaskNotifyWait(0, 0, NULL, portMAX_DELAY); // waiting for a notification from the canProcessTask once a flow control frame is received + xTaskNotifyWait(0, 0, NULL, pdMS_TO_TICKS(100)); sendMultiPacketData(); xTaskNotifyStateClear(NULL); } else { @@ -372,14 +390,14 @@ void canDisplayTask(void *pvParameters){ } xSemaphoreGive(CAN_MsgSemaphore); // release the semaphore } - vTaskSuspend(NULL); + vTaskSuspend(NULL); // have the display task stop itself } } // this task provides asynchronous simulation of button presses on the AC panel, quickly toggling AC void canAirConMacroTask(void *pvParameters){ while(1){ - vTaskDelay(100); + vTaskDelay(500); // initial delay has to be extended, in some cases 100ms was not enough to have the AC menu appear on the screen, resulting in the inputs being dropped and often entering the vent config instead xQueueSend(canTxQueue, &Msg_ACmacro_down, portMAX_DELAY); vTaskDelay(100); xQueueSend(canTxQueue, &Msg_ACmacro_press, portMAX_DELAY); @@ -451,6 +469,7 @@ void sendMultiPacketData(){ // should only be executed after the display ackno // function to queue a frame requesting measurement blocks void requestMeasurementBlocks(){ + DEBUG_PRINTLN("CAN: Requesting measurements..."); if(ECC_present){ // request measurement blocks from the climate control module xQueueSend(canTxQueue, &Msg_MeasurementRequestECC, portMAX_DELAY); } else { @@ -464,13 +483,14 @@ void canActionEhuButton0(){ // do not use for CD30! it does not have a " void canActionEhuButton1(){ // regular audio metadata mode if(disp_mode!=0){ - disp_mode=0; + if(bt_audio_playing) disp_mode=0; // we have to check whether the music is playing, else we the buffered song title just stays there DIS_forceUpdate=1; } } void canActionEhuButton2(){ // printing speed+rpm, coolant and voltage from measurement blocks if(disp_mode!=1){ + CAN_new_dataSet_recvd=0; disp_mode=1; disp_mode_changed=1; DEBUG_PRINTLN("DISP_MODE: Switching to vehicle data..."); @@ -479,6 +499,7 @@ void canActionEhuButton2(){ // printing speed+rpm, coolant and voltage f void canActionEhuButton3(){ if(disp_mode!=2){ + CAN_new_dataSet_recvd=0; disp_mode=2; disp_mode_changed=1; DEBUG_PRINTLN("DISP_MODE: Switching to 1-line coolant..."); diff --git a/src/EHU32.ino b/src/EHU32.ino index 6833715..f7319da 100644 --- a/src/EHU32.ino +++ b/src/EHU32.ino @@ -2,15 +2,18 @@ #include "BluetoothA2DPSink.h" #include "driver/twai.h" #include +// defining DEBUG enables Serial I/O for simulating button presses or faking measurement blocks through a separate RTOS task +//#define DEBUG + +#ifndef DEBUG #include #include +#endif // internal CAN queue constants #define QUEUE_LENGTH 100 #define MSG_SIZE sizeof(twai_message_t) -// defining DEBUG enables Serial I/O for simulating button presses or faking measurement blocks through a separate RTOS task -//#define DEBUG #ifdef DEBUG #define DEBUG_SERIAL(X) Serial.begin(X) #define DEBUG_PRINT(X) Serial.print(X) @@ -26,14 +29,14 @@ // pin definitions const int PCM_MUTE_CTL=23; // this pin controls PCM5102s soft-mute function // RTOS stuff -TaskHandle_t canReceiveTaskHandle, canDisplayTaskHandle, canProcessTaskHandle, canTransmitTaskHandle, canAirConMacroTaskHandle; +TaskHandle_t canReceiveTaskHandle, canDisplayTaskHandle, canProcessTaskHandle, canTransmitTaskHandle, canWatchdogTaskHandle, canAirConMacroTaskHandle; QueueHandle_t canRxQueue, canTxQueue; SemaphoreHandle_t CAN_MsgSemaphore=NULL; // TWAI driver stuff uint32_t alerts_triggered; twai_status_info_t status_info; // CAN related flags -volatile bool DIS_forceUpdate=0, CAN_MessageReady=0, CAN_prevTxFail=0, CAN_flowCtlFail=0, CAN_speed_recvd=0, CAN_coolant_recvd=0, CAN_voltage_recvd=0, CAN_new_dataSet_recvd=0, disp_mode_changed=0; +volatile bool DIS_forceUpdate=0, CAN_MessageReady=0, CAN_prevTxFail=0, CAN_flowCtlFail=0, CAN_speed_recvd=0, CAN_coolant_recvd=0, CAN_voltage_recvd=0, CAN_new_dataSet_recvd=0, CAN_measurements_requested=0, disp_mode_changed=0; // measurement related flags volatile bool ECC_present=0; // global bluetooth flags @@ -47,12 +50,13 @@ char coolant_buffer[32], speed_buffer[32], voltage_buffer[32]; // display mode 0 -> song metadata and general status messages, 1 -> body data, 2 -> single-line body data, -1 -> prevent screen updates int disp_mode=-1; // time to compare against -unsigned int last_millis=0, last_millis_notif=0, last_millis_inline=0; +unsigned long last_millis=0, last_millis_req=0, last_millis_disp=0; void canReceiveTask(void* pvParameters); void canTransmitTask(void* pvParameters); void canProcessTask(void* pvParameters); void canDisplayTask(void* pvParameters); +void canWatchdogTask(void* pvParameters); void canAirConMacroTask(void* pvParameters); void OTAhandleTask(void* pvParameters); void prepareMultiPacket(int bytes_processed, char* buffer_to_read); @@ -60,7 +64,7 @@ int processDisplayMessage(char* upper_line_buffer, char* middle_line_buffer, cha void sendMultiPacket(); void sendMultiPacketData(); -void setup() { +void setup(){ pinMode(PCM_MUTE_CTL, OUTPUT); digitalWrite(PCM_MUTE_CTL, HIGH); vTaskDelay(pdMS_TO_TICKS(100)); @@ -75,6 +79,7 @@ void setup() { xTaskCreate(canTransmitTask, "CANbusTransmitTask", 4096, NULL, 2, &canTransmitTaskHandle); xTaskCreate(canProcessTask, "CANbusMessageProcessor", 8192, NULL, 3, &canProcessTaskHandle); xTaskCreate(canDisplayTask, "DisplayUpdateTask", 8192, NULL, 3, &canDisplayTaskHandle); + xTaskCreatePinnedToCore(canWatchdogTask, "WatchdogTask", 2048, NULL, 20, &canWatchdogTaskHandle, 0); vTaskSuspend(canDisplayTaskHandle); #ifdef DEBUG xTaskCreate(CANsimTask, "CANbusSimulateEvents", 2048, NULL, 4, NULL); // allows to simulate button presses through serial @@ -83,15 +88,37 @@ void setup() { vTaskSuspend(canAirConMacroTaskHandle); // Aircon macro task exists solely to execute simulated button presses asynchronously, as such it is only started when needed } +// this task monitors radio messages and resets the program if the radio goes to sleep or CAN dies +void canWatchdogTask(void *pvParameters){ + static BaseType_t notifResult; + while(1){ + if(ehu_started){ + notifResult=xTaskNotifyWait(0, 0, NULL, pdMS_TO_TICKS(15000)); // wait for a notification that display packet from the radio unit has been received + if(notifResult==pdFAIL){ // if the notification has not been received in the specified timeframe (radio sends its display messages each 5s, specified timeout of 15s for safety) we assume the radio is off + DEBUG_PRINTLN("WATCHDOG: Triggering software reset..."); + vTaskDelay(pdMS_TO_TICKS(100)); + a2dp_shutdown(); // this or disp_mode=-1? + } else { + DEBUG_PRINTLN("WATCHDOG: Reset successful."); + } + } + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} + // processes data based on the current value of disp_mode or prints one-off messages by specifying the data in arguments; message is then transmitted right away void writeTextToDisplay(bool disp_mode_override=0, char* up_line_text=nullptr, char* mid_line_text=nullptr, char* low_line_text=nullptr){ // disp_mode_override exists as a simple way to print one-off messages (like board status, errors and such) xSemaphoreTake(CAN_MsgSemaphore, portMAX_DELAY); // take the semaphore as a way to prevent any transmission when the message structure is being written if(!disp_mode_override){ if(disp_mode==0 && (album_buffer[0]!='\0' || title_buffer[0]!='\0' || artist_buffer[0]!='\0')){ prepareMultiPacket(processDisplayMessage(album_buffer, title_buffer, artist_buffer), DisplayMsg); // prepare a 3-line message (audio Title, Album and Artist) - } - if(disp_mode==1){ - prepareMultiPacket(processDisplayMessage(coolant_buffer, speed_buffer, voltage_buffer), DisplayMsg); // vehicle data buffer + } else { + if(disp_mode==1){ + prepareMultiPacket(processDisplayMessage(coolant_buffer, speed_buffer, voltage_buffer), DisplayMsg); // vehicle data buffer + } + if(disp_mode==2){ + prepareMultiPacket(processDisplayMessage(nullptr, coolant_buffer, nullptr), DisplayMsg); // vehicle data buffer + } } } else { // overriding buffers prepareMultiPacket(processDisplayMessage(up_line_text, mid_line_text, low_line_text), DisplayMsg); @@ -111,28 +138,33 @@ void loop(){ vTaskSuspend(canTransmitTaskHandle); vTaskSuspend(canProcessTaskHandle); vTaskSuspend(canDisplayTaskHandle); + vTaskSuspend(canWatchdogTaskHandle); // so I added the watchdog but forgot to suspend it when starting OTA. result? Couldn't update it inside the car and had to take the radio unit out to do it manually + #ifndef DEBUG OTA_Handle(); + #endif } if(disp_mode==1 && ehu_started){ // if running in measurement block mode, check time and if enough time has elapsed ask for new data - if((last_millis+250)