├── EHU32_wiring.pdf ├── LICENSE ├── src ├── OTA.ino ├── A2DP.ino ├── TextHandler.ino ├── EHU32.ino └── CAN.ino └── README.md /EHU32_wiring.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PNKP237/EHU32/HEAD/EHU32_wiring.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 PNKP237 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/OTA.ino: -------------------------------------------------------------------------------- 1 | const char* ssid = "EHU32-OTA"; 2 | const char* password = "ehu32updater"; 3 | volatile bool OTA_running=0, OTA_finished=0, OTA_progressing=0; 4 | #ifndef DEBUG 5 | // initialize OTA functionality as a way to update firmware; this disables A2DP functionality! 6 | void OTA_start(){ 7 | //twai_stop(); 8 | a2dp_sink.end(true); 9 | vTaskDelay(pdMS_TO_TICKS(500)); 10 | if (!WiFi.softAP(ssid, password)) { 11 | vTaskDelay(pdMS_TO_TICKS(1000)); 12 | ESP.restart(); 13 | } else { 14 | IPAddress myIP = WiFi.softAPIP(); 15 | ArduinoOTA 16 | .setMdnsEnabled(false) 17 | .setRebootOnSuccess(true) 18 | .onStart([]() { 19 | String type; 20 | if (ArduinoOTA.getCommand() == U_FLASH) 21 | type = "sketch"; 22 | else 23 | type = "filesystem"; 24 | OTA_progressing=1; 25 | }) 26 | .onProgress([](unsigned int progress, unsigned int total) { // gives visual updates on the 27 | //Serial.printf("Progress: %u%%\r", (progress / (total / 100))); 28 | if(((progress / (total / 100))%10)==0){ 29 | unsigned int progress_val=progress / (total / 100); 30 | char progress_text[32]; 31 | snprintf(progress_text, sizeof(progress_text), "Updating... %d%%", progress_val); 32 | writeTextToDisplay(1, nullptr, progress_text, nullptr); 33 | } 34 | }) 35 | .onError([](ota_error_t error) { 36 | char err_reason[32]; 37 | switch(error){ 38 | case OTA_AUTH_ERROR:{ 39 | snprintf(err_reason, sizeof(err_reason), "Not authenticated"); 40 | break; 41 | } 42 | case OTA_BEGIN_ERROR:{ 43 | snprintf(err_reason, sizeof(err_reason), "Error starting"); 44 | break; 45 | } 46 | case OTA_CONNECT_ERROR:{ 47 | snprintf(err_reason, sizeof(err_reason), "Connection problem"); 48 | break; 49 | } 50 | case OTA_RECEIVE_ERROR:{ 51 | snprintf(err_reason, sizeof(err_reason), "Error receiving"); 52 | break; 53 | } 54 | case OTA_END_ERROR:{ 55 | snprintf(err_reason, sizeof(err_reason), "Couldn't apply update"); 56 | break; 57 | } 58 | default: break; 59 | } 60 | writeTextToDisplay(1, "Error updating", err_reason, "Resetting..."); 61 | vTaskDelay(pdMS_TO_TICKS(3000)); 62 | ESP.restart(); 63 | }) 64 | .onEnd([]() { 65 | prefs_clear(); // reset settings, ensures any new setup changes/optimizations will be useful 66 | OTA_finished=1; 67 | OTA_progressing=0; 68 | }); 69 | ArduinoOTA.begin(); 70 | OTA_running=1; 71 | } 72 | } 73 | 74 | void OTA_Handle(){ 75 | unsigned long time_started=0; 76 | while(1){ 77 | while(!checkFlag(OTA_begin)){ 78 | vTaskDelay(1000); 79 | } 80 | if(!OTA_running){ 81 | OTA_start(); 82 | time_started=millis(); 83 | } 84 | ArduinoOTA.handle(); 85 | if(!OTA_progressing){ // timeout after 10 minutes of no OTA start 86 | if((time_started+600000) 2 | I2SStream i2s; 3 | BluetoothA2DPSink a2dp_sink(i2s); 4 | A2DPNoVolumeControl noVolumeControl; 5 | 6 | // updates the buffers 7 | 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 8 | xSemaphoreTake(BufferSemaphore, portMAX_DELAY); // take the semaphore as a way to prevent the buffers being accessed elsewhere 9 | switch(md_type){ 10 | case 0x1: memset(title_buffer, 0, sizeof(title_buffer)); 11 | snprintf(title_buffer, sizeof(title_buffer), "%s", data2); 12 | //DEBUG_PRINTF("\nA2DP: Received title: \"%s\"", data2); 13 | setFlag(md_title_recvd); 14 | break; 15 | case 0x2: memset(artist_buffer, 0, sizeof(artist_buffer)); 16 | snprintf(artist_buffer, sizeof(artist_buffer), "%s", data2); 17 | //DEBUG_PRINTF("\nA2DP: Received artist: \"%s\"", data2); 18 | setFlag(md_artist_recvd); 19 | break; 20 | case 0x4: memset(album_buffer, 0, sizeof(album_buffer)); 21 | snprintf(album_buffer, sizeof(album_buffer), "%s", data2); 22 | //DEBUG_PRINTF("\nA2DP: Received album: \"%s\"", data2); 23 | setFlag(md_album_recvd); 24 | break; 25 | default: break; 26 | } 27 | xSemaphoreGive(BufferSemaphore); 28 | if(checkFlag(md_title_recvd) && checkFlag(md_artist_recvd) && checkFlag(md_album_recvd)){ 29 | setFlag(DIS_forceUpdate); // lets the eventHandler task know that there's new data to be written to the display 30 | clearFlag(md_title_recvd); 31 | clearFlag(md_artist_recvd); 32 | clearFlag(md_album_recvd); 33 | } 34 | } 35 | 36 | // a2dp bt connection callback 37 | void a2dp_connection_state_changed(esp_a2d_connection_state_t state, void *ptr){ // callback for bluetooth connection state change 38 | if(state==2){ // state=0 -> disconnected, state=1 -> connecting, state=2 -> connected 39 | setFlag(bt_connected); 40 | } else { 41 | clearFlag(bt_connected); 42 | } 43 | setFlag(bt_state_changed); 44 | } 45 | 46 | // a2dp audio state callback 47 | void a2dp_audio_state_changed(esp_a2d_audio_state_t state, void *ptr){ // callback for audio playing/stopped 48 | if(state==2){ // state=1 -> stopped, state=2 -> playing 49 | setFlag(bt_audio_playing); 50 | } else { 51 | clearFlag(bt_audio_playing); 52 | } 53 | setFlag(audio_state_changed); 54 | } 55 | 56 | // start A2DP audio service 57 | void a2dp_init(){ 58 | auto i2s_conf=i2s.defaultConfig(); 59 | i2s_conf.pin_bck=26; 60 | i2s_conf.pin_ws=25; 61 | i2s_conf.pin_data=22; 62 | i2s.begin(i2s_conf); 63 | a2dp_sink.set_avrc_metadata_callback(avrc_metadata_callback); 64 | a2dp_sink.set_avrc_metadata_attribute_mask(ESP_AVRC_MD_ATTR_TITLE | ESP_AVRC_MD_ATTR_ARTIST | ESP_AVRC_MD_ATTR_ALBUM); 65 | a2dp_sink.set_on_connection_state_changed(a2dp_connection_state_changed); 66 | a2dp_sink.set_on_audio_state_changed(a2dp_audio_state_changed); 67 | a2dp_sink.set_volume_control(&noVolumeControl); 68 | a2dp_sink.set_reconnect_delay(500); 69 | a2dp_sink.set_auto_reconnect(true, 2000); 70 | 71 | a2dp_sink.start("EHU32"); // setting up bluetooth audio sink 72 | setFlag(a2dp_started); 73 | DEBUG_PRINTLN("A2DP: Started!"); 74 | disp_mode=0; // set display mode to audio metadata on boot 75 | writeTextToDisplay(1, "EHU32 v0.9.5", "Bluetooth on", "Waiting for connection..."); 76 | } 77 | 78 | // handles events such as connecion/disconnection and audio play/pause 79 | void A2DP_EventHandler(){ 80 | if(checkFlag(ehu_started) && !checkFlag(a2dp_started)){ // this enables bluetooth A2DP service only after the radio is started 81 | a2dp_init(); 82 | } 83 | 84 | if(checkFlag(DIS_forceUpdate) && disp_mode==0 && checkFlag(CAN_allowAutoRefresh) && checkFlag(bt_audio_playing)){ // handles data processing for A2DP AVRC data events 85 | writeTextToDisplay(); 86 | } 87 | 88 | if(checkFlag(bt_state_changed) && disp_mode==0){ // mute external DAC when not playing 89 | if(checkFlag(bt_connected)){ 90 | a2dp_sink.set_volume(127); // workaround to ensure max volume being applied on successful connection 91 | writeTextToDisplay(1, "", "Bluetooth connected", (char*)a2dp_sink.get_peer_name()); 92 | } else { 93 | writeTextToDisplay(1, "", "Bluetooth disconnected", ""); 94 | } 95 | clearFlag(bt_state_changed); 96 | } 97 | 98 | if(checkFlag(audio_state_changed) && checkFlag(bt_connected) && disp_mode==0){ // mute external DAC when not playing; bt_connected ensures no "Connected, paused" is displayed, seems that the audio_state_changed callback comes late 99 | if(checkFlag(bt_audio_playing)){ 100 | digitalWrite(PCM_MUTE_CTL, HIGH); 101 | setFlag(DIS_forceUpdate); // force reprinting of audio metadata when the music is playing 102 | } else { 103 | digitalWrite(PCM_MUTE_CTL, LOW); 104 | writeTextToDisplay(1, "Bluetooth connected", "Paused", ""); 105 | } 106 | clearFlag(audio_state_changed); 107 | } 108 | } 109 | 110 | // ID 0x501 DB3 0x18 indicates imminent shutdown of the radio and display; disconnect from source 111 | void a2dp_shutdown(){ 112 | vTaskSuspend(canMessageDecoderTaskHandle); 113 | //vTaskSuspend(canWatchdogTaskHandle); 114 | ESP.restart(); // very crude workaround until I find a better way to deal with reconnection problems after end() is called 115 | delay(1000); 116 | a2dp_sink.disconnect(); 117 | a2dp_sink.end(); 118 | clearFlag(ehu_started); // so it is possible to restart and reconnect the source afterwards in the rare case radio is shutdown but ESP32 is still powered up 119 | clearFlag(a2dp_started); // while extremely unlikely to happen in the vehicle, this comes handy for debugging on my desk setup 120 | DEBUG_PRINTLN("CAN: EHU went down! Disconnecting A2DP."); 121 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **EHU32** 2 | 3 | EHU32 brings bluetooth audio to your Opel/Vauxhall vehicle, integrating with the onboard display and radio. 4 | 5 | Compatible with vehicles equipped with CID/GID/BID/TID display units, additionally giving you the option to view live diagnostic data. Supports headunits such as CD30/CD30MP3/CD40USB/CDC40Opera/CD70Navi/DVD90Navi, as long as you've got an **Aux input**. 6 | 7 | **EHU32 is non-invasive** and does not require modification to the existing hardware in your car - it can be connected to the OBD-II diagnostic port and radio unit's Aux input. 8 | 9 | **Simple schematic, small bill of materials and inexpensive, widely available components** are what makes EHU32 a great addition to your Astra H/Corsa D/Vectra C/Zafira B/Meriva A. 10 | 11 | ## Features 12 | - **Bluetooth (A2DP) audio**, data is output to an external I2S DAC, such as PCM5102 13 | - **control connected audio source** (play/pause, previous/next track) using buttons on the **steering wheel** 14 | - shows you what is currently playing on the center console display in **Aux mode** 15 | * prints **Artist**, **Track title** and **Album**, just like regular CD playback would 16 | * this is the default mode, otherwise accessible by long pressing "1" 17 | - **automatically reconnects** to your phone when the radio is started 18 | - alternatively, EHU32 can also display live data, such as vehicle speed, RPMs, coolant temperature and battery voltage 19 | * accessible by long pressing "2" on the radio panel 20 | * for single line displays, "3" prints just the coolant temperature 21 | * note that this mode will overwrite anything on the screen, even in FM radio mode or during CD playback 22 | * disable printing to the screen by holding "9" (or hold it for the total of 5 seconds to reset the entire board, that also clears the settings) 23 | - allows toggling AC with just a **single long-press of the AC selector knob** if it's held for at least half a second 24 | 25 | EHU32 can be updated over-the-air, holding "8" enables the wifi hotspot (password ehu32updater). To leave the OTA mode, press "8" for 5 seconds. 26 | 27 | ## How it looks 28 | Demo videos: 29 | 30 | [![Click here to watch EHU32 demo on YouTube](https://img.youtube.com/vi/CZvhz3yvV1g/0.jpg)](https://www.youtube.com/watch?v=CZvhz3yvV1g) [![Click here to watch EHU32 demo on YouTube](https://img.youtube.com/vi/cj5L4aGAB5w/0.jpg)](https://www.youtube.com/watch?v=cj5L4aGAB5w) 31 | 32 | Here's another, extended demo showing EHU32 in action: [https://www.youtube.com/watch?v=8fi7kX9ci_o](https://www.youtube.com/watch?v=8fi7kX9ci_o) 33 | 34 | ![VID_20250507_180309 mp4_snapshot_04 09 045](https://github.com/user-attachments/assets/ea93fcec-3e86-4963-869a-c7194ca0c965) 35 | 36 | ![IMG_20240217_172706](https://github.com/PNKP237/EHU32/assets/153071841/46e31e0d-70b7-423b-9a04-b4522eb96506) 37 | 38 | ![VID_20240224_174250 mp4_snapshot_00 11 305](https://github.com/PNKP237/EHU32/assets/153071841/030defa7-99e6-42d9-bbc5-f6a6a656e597) 39 | 40 | Video showing measurement data displayed in real time (warning, contains music!) https://www.youtube.com/watch?v=uxLYr1c_TJA 41 | 42 | ## How it works and general usage tips 43 | While this project aims to make the experience as seamless as possible, there are some shortcomings that have to be addressed: 44 | - First start (or hard resetting) will take up to 30-40 seconds, because EHU32 will attempt to test your vehicle's display capability and other modules your vehicle is equipped with in order to ensure high level of compatibility. **Please turn on your headunit (engine/ignition is not necessary) and wait patiently** until the startup message is shown! 45 | - Bluetooth is ONLY enabled once EHU32 detects the radio talking to the display over CAN bus 46 | - The audio source volume has to be set to maximum in order to avoid unnecessary noise (unless the audio is clearly distorted and is clipping). Adjust the volume as usual, using the radio's volume control knob or steering wheel buttons. 47 | - EHU32 scans the messages and is looking for "Aux", but once you switch off of Aux mode, there might be a delay before the screen is updated to FM radio mode 48 | - If you have a CD30/CD40 headunit, press "SOUND" twice if you want to adjust bass/treble/balance etc. This is a necessary evil because EHU32 can only block messages ahead of time, way before it knows what are they saying. Nevertheless, it still detects that Aux is no longer displayed hence the sound menu will show up on the second button press 49 | - In case your android device experiences issues with music playback (skipping, crackling and such), go into bluetooth settings, choose EHU32 from the list and disable **"Keep volume consistent"** 50 | 51 | If you came here looking for inspiration I'd recommend checking out the [wiki page](https://github.com/PNKP237/EHU32/wiki). I have documented some basics that might come in handy when developing your own addons for these vehicles. 52 | 53 | ## Building it yourself 54 | Required hardware: 55 | - ESP32 module (preferably an official Espressif-made module) with an antenna (IPX) connector; 56 | - any IPX antenna - you can use one recovered from an old, broken down notebook or buy one from any website; 57 | - PCM5102A DAC module (with configurable jumpers on the bottom, **make sure to configure them accordingly**); 58 | - any CAN transceiver module - MCP2551 (VCC 5V), TDA104x/TDA1050 (VCC 5V), SN65HVD23x (VCC 3.3V). 59 | 60 | Refer to [EHU32_wiring.pdf](https://github.com/PNKP237/EHU32/blob/main/EHU32_wiring.pdf) for a guide on how to wire up the modules together. 61 | 62 | Please note to configure the PCM5102A module correctly with the jumpers on the bottom. 63 | 64 | The MS-CAN bus can be accessed through the diagnostic port (pins 3 and 11 respectively for CAN-H and CAN-L), the headunit, the display, the climate control panel or the factory bluetooth hands-free module. 65 | 66 | You can install these modules inside the factory headunit: 67 | - for CD30MP3 (Delphi-Grundig) refer to [this post](https://github.com/PNKP237/EHU32/issues/3#issuecomment-2121866276); 68 | - for CD70Navi refer to [this wiki article](https://github.com/PNKP237/EHU32/wiki/Hardware-modification-%E2%80%90-EHU32-installation-in-CD70-Navi). 69 | 70 | Some ESP32 boards have been found to cause problems with audio playback over I2S (mainly exhibited with iPhones and Huawei phones), while it's difficult to suggest an ESP32 board that's confirmed to work, the ones with antenna connectors are often fine. **Look for boards with Espressif etched on the RF shield!** 71 | 72 | Note that this should be soldered directly in the radio unit as the OBD-II port only provides unswitched 12V. Powering it from a 5V car charger also works. 73 | Do not connect headphones to the DAC module, its output is supposed to only be connected to amplifier input - in case of this project either the AUX socket of radio's internal AUX input. 74 | 75 | ## Compilation notes 76 | Please use version **2.0.17 of ESP32 arduino core**. More recent versions don't seem stable enough, at least in my limited testing. 77 | Tested with ESP32-A2DP v1.8.7 and arduino-audio-tools v1.1.1. 78 | 79 | TWAI driver written by ESP as part of their ESP-IDF framework isn't perfect. To ensure everything works properly you'll need to modify "**sdkconfig**" which is located in %USERPROFILE%\AppData\Local\Arduino15\packages\esp32\hardware\esp32\version\tools\sdk\esp32\ 80 | 81 | Under "TWAI configuration" section enable **CONFIG_TWAI_ISR_IN_IRAM** and modify **CONFIG_TWAI_ERRATA_FIX_TX_INTR_LOST** so the errata fix is not applied. The entire **TWAI configuration** section should look like this: 82 | ``` 83 | # 84 | # TWAI configuration 85 | # 86 | CONFIG_TWAI_ISR_IN_IRAM=y 87 | CONFIG_TWAI_ERRATA_FIX_BUS_OFF_REC=y 88 | CONFIG_TWAI_ERRATA_FIX_TX_INTR_LOST=n 89 | CONFIG_TWAI_ERRATA_FIX_RX_FRAME_INVALID=y 90 | CONFIG_TWAI_ERRATA_FIX_RX_FIFO_CORRUPT=y 91 | # CONFIG_TWAI_ERRATA_FIX_LISTEN_ONLY_DOM is not set 92 | # end of TWAI configuration 93 | ``` 94 | In Arduino IDE set the following: Events on core 0, Arduino on core 1, partition scheme - Minimal SPIFFS. 95 | 96 | ### Credits 97 | Depends on Arduino ESP32-A2DP and arduino-audio-tools libraries by pschatzmann: [https://github.com/pschatzmann/ESP32-A2DP](https://github.com/pschatzmann/ESP32-A2DP) [https://github.com/pschatzmann/arduino-audio-tools](https://github.com/pschatzmann/arduino-audio-tools) 98 | 99 | Reverse engineering of the vehicles various messages was done by JJToB: [https://github.com/JJToB/Car-CAN-Message-DB](https://github.com/JJToB/Car-CAN-Message-DB) 100 | 101 | This project comes with absolutely no warranty of any kind, I'm not responsible for your car going up in flames. 102 | -------------------------------------------------------------------------------- /src/TextHandler.ino: -------------------------------------------------------------------------------- 1 | // below is data required to be included in every line - text formatting is based on those 2 | const char DIS_leftadjusted[14]={0x00,0x1B,0x00,0x5B,0x00,0x66,0x00,0x53,0x00,0x5F,0x00,0x67,0x00,0x6D}, DIS_smallfont[14]={0x00,0x1B,0x00,0x5B,0x00,0x66,0x00,0x53,0x00,0x5F,0x00,0x64,0x00,0x6D}, DIS_centered[8]={0x00, 0x1B, 0x00, 0x5B, 0x00, 0x63, 0x00, 0x6D}, DIS_rightadjusted[8]={0x00, 0x1B, 0x00, 0x5B, 0x00, 0x72, 0x00, 0x6D}; 3 | 4 | // converts an UTF-8 buffer to UTF-16, filters out unsupported chars, returns the amount of chars processed 5 | unsigned int utf8_to_utf16(const char* utf8_buffer, char* utf16_buffer){ 6 | unsigned int utf16_bytecount=0; 7 | while (*utf8_buffer!='\0'){ 8 | uint32_t charint=0; 9 | if ((*utf8_buffer&0x80)==0x00){ 10 | charint=*utf8_buffer&0x7F; 11 | utf8_buffer++; 12 | } 13 | else if ((*utf8_buffer&0xE0)==0xC0){ 14 | charint=(*utf8_buffer & 0x1F)<<6; 15 | charint|=(*(utf8_buffer+1)&0x3F); 16 | utf8_buffer+=2; 17 | } 18 | else if ((*utf8_buffer&0xF0)==0xE0){ 19 | charint=(*utf8_buffer&0x0F)<<12; 20 | charint|=(*(utf8_buffer+1)&0x3F)<<6; 21 | charint|=(*(utf8_buffer+2)&0x3F); 22 | utf8_buffer += 3; 23 | } 24 | else if ((*utf8_buffer&0xF8)==0xF0){ 25 | charint=(*utf8_buffer&0x07)<<18; 26 | charint|=(*(utf8_buffer+1)&0x3F)<<12; 27 | charint|=(*(utf8_buffer+2)&0x3F)<<6; 28 | charint|=(*(utf8_buffer+3)&0x3F); 29 | utf8_buffer+=4; 30 | } 31 | else { 32 | return utf16_bytecount/2; 33 | } 34 | // only process supported chars, latin and extended latin works, cyrillic does not 35 | if ((charint>=0x0000&&charint<=0x024F) || (charint>=0x1E00 && charint<=0x2C6F)){ 36 | if (charint>=0x10000) { 37 | charint-=0x10000; 38 | utf16_buffer[utf16_bytecount++]=static_cast((charint>>10)+0xD8); 39 | utf16_buffer[utf16_bytecount++]=static_cast((charint>>2)&0xFF); 40 | utf16_buffer[utf16_bytecount++]=static_cast(0xDC|((charint>>10)&0x03)); 41 | utf16_buffer[utf16_bytecount++]=static_cast((charint&0x03)<<6); 42 | } 43 | else { 44 | utf16_buffer[utf16_bytecount++]=static_cast((charint>>8)&0xFF); 45 | utf16_buffer[utf16_bytecount++]=static_cast(charint&0xFF); 46 | } 47 | } 48 | } 49 | return utf16_bytecount/2; // amount of chars processed 50 | } 51 | 52 | // converts UTF-8 strings from arguments to real UTF-16, then compiles a full display message with formatting; returns total bytes written as part of message payload 53 | int processDisplayMessage(char* upper_line_buffer, char* middle_line_buffer, char* lower_line_buffer){ 54 | static char utf16_middle_line[256], utf16_lower_line[256], utf16_upper_line[256]; 55 | int upper_line_buffer_length=0, middle_line_buffer_length=0, lower_line_buffer_length=0; 56 | if(upper_line_buffer!=nullptr){ // converting UTF-8 strings to UTF-16 and calculating string lengths to keep track of processed data 57 | upper_line_buffer_length=utf8_to_utf16(upper_line_buffer, utf16_upper_line); 58 | } 59 | if(middle_line_buffer!=nullptr){ 60 | middle_line_buffer_length=utf8_to_utf16(middle_line_buffer, utf16_middle_line); 61 | if(middle_line_buffer_length==0 || (middle_line_buffer_length==1 && utf16_middle_line[1]==0x20)){ // -> empty line (or unsupported chars) 62 | snprintf(middle_line_buffer, 8, "Playing"); // if the middle line was to be blank you can at least tell that there's audio being played 63 | middle_line_buffer_length=utf8_to_utf16(middle_line_buffer, utf16_middle_line); // do this once again for the new string 64 | } 65 | } 66 | if(lower_line_buffer!=nullptr){ 67 | lower_line_buffer_length=utf8_to_utf16(lower_line_buffer, utf16_lower_line); 68 | } 69 | 70 | #ifdef DEBUG_STRINGS // debug stuff 71 | Serial.printf("\nTitle length: %d", middle_line_buffer_length); 72 | Serial.printf("\nAlbum length: %d", upper_line_buffer_length); 73 | Serial.printf("\nArtist length: %d", lower_line_buffer_length); 74 | Serial.println("\nTitle buffer in UTF-8:"); 75 | for(int i=0;i1){ // if the middle line data is just a space, don't apply formatting - saves 2 frames of data 114 | memcpy(DisplayMsg+last_byte_written+1, DIS_leftadjusted, sizeof(DIS_leftadjusted)); 115 | last_byte_written+=sizeof(DIS_leftadjusted); 116 | DisplayMsg[6]=sizeof(DIS_leftadjusted)/2; 117 | } 118 | memcpy(DisplayMsg+last_byte_written+1, utf16_middle_line, middle_line_buffer_length*2); 119 | last_byte_written+=(middle_line_buffer_length*2); 120 | 121 | DisplayMsg[6]+=middle_line_buffer_length; // this is static, char count = title+(formatting/2) 122 | 123 | int album_count_pos=10; 124 | // ALBUM FIELD 125 | last_byte_written++; 126 | DisplayMsg[last_byte_written]=0x11; // specifying "album" field (upper line) 127 | last_byte_written++; 128 | album_count_pos=last_byte_written; 129 | if(upper_line_buffer_length>=1){ // if the upper line data is just a space, don't apply formatting - saves 2 frames of data 130 | memcpy(DisplayMsg+last_byte_written+1, DIS_smallfont, sizeof(DIS_smallfont)); 131 | last_byte_written+=sizeof(DIS_smallfont); 132 | DisplayMsg[album_count_pos]=sizeof(DIS_smallfont)/2; 133 | } 134 | memcpy(DisplayMsg+last_byte_written+1, utf16_upper_line, upper_line_buffer_length*2); 135 | last_byte_written+=(upper_line_buffer_length*2); 136 | DisplayMsg[album_count_pos]+=upper_line_buffer_length; 137 | 138 | int artist_count_pos=album_count_pos; 139 | // ARTIST FIELD 140 | last_byte_written++; 141 | DisplayMsg[last_byte_written]=0x12; // specifying "artist" field (lower line) 142 | last_byte_written++; 143 | artist_count_pos=last_byte_written; 144 | if(lower_line_buffer_length>=1){ // if the lower line data is just a space, don't apply formatting - saves 2 frames of data 145 | memcpy(DisplayMsg+last_byte_written+1, DIS_smallfont, sizeof(DIS_smallfont)); 146 | last_byte_written+=sizeof(DIS_smallfont); 147 | DisplayMsg[artist_count_pos]=sizeof(DIS_smallfont)/2; 148 | } 149 | memcpy(DisplayMsg+last_byte_written+1, utf16_lower_line, lower_line_buffer_length*2); 150 | last_byte_written+=(lower_line_buffer_length*2); 151 | DisplayMsg[artist_count_pos]+=lower_line_buffer_length; 152 | 153 | if((last_byte_written+1)%7==0){ // if the amount of bytes were to result in a full packet (ie no unused bytes), add a char to overflow into the next packet 154 | DisplayMsg[artist_count_pos]+=1; // workaround because if the packets are full the display would ignore the message. This is explained on the EHU32 wiki 155 | DisplayMsg[last_byte_written+1]=0x00; DisplayMsg[last_byte_written+2]=0x20; 156 | last_byte_written+=2; 157 | } 158 | if(last_byte_written>254){ // message size can't be larger than 255 bytes, as the character specifying total payload is an 8 bit value 159 | last_byte_written=254; // we can send that data though, it will just be ignored, no damage is done 160 | } 161 | DisplayMsg[0]=last_byte_written+1; // TOTAL PAYLOAD SIZE based on how many bytes have been written 162 | DisplayMsg[3]=DisplayMsg[0]-3; // payload size written as part of the 4000 command 163 | return last_byte_written+1; // return the total message size 164 | } -------------------------------------------------------------------------------- /src/EHU32.ino: -------------------------------------------------------------------------------- 1 | #include "AudioTools.h" 2 | #include "BluetoothA2DPSink.h" 3 | #include "esp_sleep.h" 4 | #include "driver/twai.h" 5 | #include 6 | 7 | // defining DEBUG enables Serial I/O for simulating button presses or faking measurement blocks through a separate RTOS task 8 | //#define DEBUG 9 | 10 | #ifndef DEBUG 11 | #include 12 | #include 13 | #include 14 | #endif 15 | 16 | #ifdef DEBUG 17 | #define DEBUG_SERIAL(X) Serial.begin(X) 18 | #define DEBUG_PRINT(X) Serial.print(X) 19 | #define DEBUG_PRINTLN(X) Serial.println(X) 20 | #define DEBUG_PRINTF(...) Serial.printf(__VA_ARGS__) 21 | #else 22 | #define DEBUG_SERIAL(X) 23 | #define DEBUG_PRINT(X) 24 | #define DEBUG_PRINTLN(X) 25 | #define DEBUG_PRINTF(...) 26 | #endif 27 | 28 | // defining available flags in the eventGroup 29 | #define DIS_forceUpdate (1 << 0) // call for the eventHandler to process the text buffers and instantly transmit the new message 30 | #define CAN_MessageReady (1 << 1) 31 | #define CAN_prevTxFail (1 << 2) 32 | #define CAN_abortMultiPacket (1 << 3) 33 | #define CAN_flowCtlFail (1 << 4) 34 | #define CAN_speed_recvd (1 << 5) 35 | #define CAN_coolant_recvd (1 << 6) 36 | #define CAN_new_dataSet_recvd (1 << 7) 37 | #define CAN_voltage_recvd (1 << 8) 38 | #define CAN_measurements_requested (1 << 9) 39 | #define disp_mode_changed (1 << 10) 40 | #define CAN_allowAutoRefresh (1 << 11) // otherwise means "Aux" has been detected 41 | #define ECC_present (1 << 12) 42 | #define ehu_started (1 << 13) 43 | #define a2dp_started (1 << 14) 44 | #define bt_connected (1 << 15) 45 | #define bt_state_changed (1 << 16) 46 | #define bt_audio_playing (1 << 17) 47 | #define audio_state_changed (1 << 18) 48 | #define md_album_recvd (1 << 19) 49 | #define md_artist_recvd (1 << 20) 50 | #define md_title_recvd (1 << 21) 51 | #define OTA_begin (1 << 22) 52 | #define OTA_abort (1 << 23) 53 | 54 | // pin definitions 55 | const int PCM_MUTE_CTL=23, PCM_ENABLE=27; // D23 controls PCM5102s soft-mute function, D27 enables PCM5102s power 56 | // RTOS stuff 57 | TaskHandle_t canReceiveTaskHandle, canDisplayTaskHandle, canProcessTaskHandle, canTransmitTaskHandle, canWatchdogTaskHandle, canAirConMacroTaskHandle, canMessageDecoderTaskHandle, eventHandlerTaskHandle; 58 | QueueHandle_t canRxQueue, canTxQueue, canDispQueue; 59 | SemaphoreHandle_t CAN_MsgSemaphore=NULL, BufferSemaphore=NULL; 60 | EventGroupHandle_t eventGroup; 61 | // TWAI driver stuff 62 | uint32_t alerts_triggered; 63 | twai_status_info_t status_info; 64 | uint32_t displayMsgIdentifier=0; 65 | // data buffers 66 | char DisplayMsg[1024], CAN_MsgArray[128][8], title_buffer[64], artist_buffer[64], album_buffer[64]; 67 | char coolant_buffer[32], speed_buffer[32], voltage_buffer[32]; 68 | // display mode 0 -> song metadata and general status messages, 1 -> body data, 2 -> single-line body data, -1 -> prevent screen updates 69 | volatile int disp_mode=-1; 70 | // time to compare against 71 | unsigned long last_millis=0, last_millis_req=0, last_millis_disp=0, last_millis_aux=0; 72 | // body data 73 | bool vehicle_ECC_present, vehicle_UHP_present; 74 | 75 | void canReceiveTask(void* pvParameters); 76 | void canTransmitTask(void* pvParameters); 77 | void canProcessTask(void* pvParameters); 78 | void canDisplayTask(void* pvParameters); 79 | void canWatchdogTask(void* pvParameters); 80 | void canAirConMacroTask(void* pvParameters); 81 | void OTAhandleTask(void* pvParameters); 82 | void prepareMultiPacket(int bytes_processed, char* buffer_to_read); 83 | int processDisplayMessage(char* upper_line_buffer, char* middle_line_buffer, char* lower_line_buffer); 84 | 85 | void setup(){ 86 | esp_sleep_enable_ext0_wakeup(GPIO_NUM_4, 0); // this will wake the ESP32 up if there's CAN activity 87 | pinMode(PCM_MUTE_CTL, OUTPUT); 88 | pinMode(PCM_ENABLE, OUTPUT); // control PCM5102 power setting 89 | digitalWrite(PCM_MUTE_CTL, HIGH); 90 | digitalWrite(PCM_ENABLE, HIGH); 91 | DEBUG_SERIAL(921600); // serial comms for debug 92 | 93 | twai_init(); // sets up everything CAN-bus related 94 | twai_message_t testMsg; 95 | if(twai_receive(&testMsg, pdMS_TO_TICKS(100))!=ESP_OK){ // if there's no activity on the bus, assume the vehicle is off, go to sleep and wake up after 5 seconds 96 | DEBUG_PRINTLN("CAN inactive. Back to sleep!"); 97 | #ifdef DEBUG 98 | vTaskDelay(pdMS_TO_TICKS(10)); // wait for a bit for the buffer to be transmitted when debugging 99 | #endif 100 | esp_deep_sleep_start(); // enter deep sleep 101 | } 102 | 103 | digitalWrite(PCM_ENABLE, LOW); // enable PCM5102 and wake SN65HVD230 up from standby (active low in case of KF50BD) 104 | 105 | Preferences settings; 106 | settings.begin("my-app", false); 107 | if(!settings.isKey("setupcomplete")){ 108 | DEBUG_PRINTLN("CAN SETUP: Key does not exist! Creating keys"); 109 | settings.clear(); 110 | settings.putBool("setupcomplete", 0); 111 | settings.putBool("uhppresent", 0); 112 | settings.putBool("eccpresent", 0); 113 | settings.putBool("vectra", 0); 114 | settings.putUInt("identifier", 0); 115 | } 116 | bool init_setupComplete=settings.getBool("setupcomplete", 0); // prefs init 117 | if(!init_setupComplete){ // this should only be executed on first boot 118 | while(twai_receive(&testMsg, portMAX_DELAY)!=ESP_OK && testMsg.identifier!=0x6C1 && (testMsg.data[2]!=0x40 || testMsg.data[2]!=0xC0)) {} // wait for the initial 0x6C1 c0/40 then start the timer 119 | unsigned long millis_init_start=millis(); // got 0x6C1, assume radio started now 120 | bool init_usedCANids[16]; // represents 0x6C0 to 0x6CF 121 | while(twai_receive(&testMsg, portMAX_DELAY)==ESP_OK && (millis_init_start+20000>millis())){ // keep receiving all msgs and log everything from 0x6C0 to 0x6CF for 10 secs 122 | if((testMsg.identifier & 0xFF0)==0x6C0 && !init_usedCANids[testMsg.identifier-0x6c0]){ // allows 0x6C0 to 0x6CF 123 | init_usedCANids[testMsg.identifier-0x6c0]=1; // if got a hit, mark it the ID as in use 124 | if(testMsg.identifier==0x6C7){ 125 | settings.putBool("uhppresent", 1); 126 | vehicle_UHP_present=1; 127 | } 128 | if(testMsg.identifier==0x6C8){ // ECC doesn't start until the key is at ignition so that's relatively pointless for now 129 | settings.putBool("eccpresent", 1); 130 | vehicle_ECC_present=1; 131 | } 132 | DEBUG_PRINTF("CAN SETUP: Marking 0x%03X as a CAN ID in use\n", testMsg.identifier); 133 | } 134 | } 135 | // finally, perform some test ISO 15765-2 first frame transmissions to see which CAN IDs the display will respond to. Avoid used IDs. 136 | // if the display does not respond to the unused IDs, check if 0x6C8 is present and use that, in the end just use 0x6C1 with additional logic for overwrite attempts 137 | DEBUG_PRINT("CAN SETUP: Attempting to test display responses to tested identifiers: "); 138 | twai_message_t testMsgTx={ .identifier=0x6C0, .data_length_code=8, .data={0x10, 0xA7, 0x40, 0x00, 0xA4, 0x03, 0x10, 0x13}}; 139 | unsigned long millis_transmitted; 140 | for(int i=0; i<16 && displayMsgIdentifier==0; i++){ 141 | if(init_usedCANids[i]) i++; // skip IDs in active use 142 | testMsgTx.identifier=(0x6C0+i); 143 | DEBUG_PRINTF("0x%03X... ", testMsgTx.identifier); 144 | twai_transmit(&testMsgTx, pdMS_TO_TICKS(300)); 145 | millis_transmitted=millis(); 146 | while((millis_transmitted+1000>millis()) && displayMsgIdentifier==0){ // break out early if the display message identifier has been found 147 | if(twai_receive(&testMsg, pdMS_TO_TICKS(300))==ESP_OK){ 148 | if(testMsg.identifier==(testMsgTx.identifier-0x400) && testMsg.data[0]==0x30){ // received a flow control frame, meaning the display has responded to a first frame (ids 0x2C0 to 0x2CF) and accepted the transmission (db0 0x30) 149 | displayMsgIdentifier=testMsgTx.identifier; // save that ID 150 | DEBUG_PRINTF("got a response on 0x%03X!", testMsg.identifier); 151 | } 152 | } 153 | } 154 | vTaskDelay(pdMS_TO_TICKS(100)); 155 | } 156 | if(displayMsgIdentifier==0){ 157 | if(init_usedCANids[8]==1){ 158 | displayMsgIdentifier=0x6C8; 159 | DEBUG_PRINTLN("\nCAN SETUP: Unable to find a valid unused CAN ID, but detected ECC -> using 0x6C8"); 160 | } else { 161 | displayMsgIdentifier=0x6C1; 162 | DEBUG_PRINTLN("\nCAN SETUP: Unable to find a valid unused CAN ID. Falling back to stock -> using 0x6C1"); 163 | } 164 | } 165 | DEBUG_PRINTLN("\nCAN SETUP: Saving the display message identifier to flash..."); 166 | settings.putUInt("identifier", displayMsgIdentifier); // saving that data to flash to use on next boot 167 | settings.putBool("setupcomplete", 1); 168 | } else { 169 | displayMsgIdentifier=settings.getUInt("identifier", 0); 170 | DEBUG_PRINTF("CAN SETUP: Get the display identifier from flash -> 0x%03X\n", displayMsgIdentifier); 171 | vehicle_ECC_present=settings.getBool("eccpresent", 0); 172 | vehicle_UHP_present=settings.getBool("uhppresent", 0); 173 | } 174 | if(displayMsgIdentifier==0){ 175 | DEBUG_PRINTLN("CAN SETUP: identifier can't be 0, rerunning CAN setup..."); 176 | settings.putBool("setupcomplete", 0); 177 | settings.end(); 178 | vTaskDelay(pdMS_TO_TICKS(100)); 179 | ESP.restart(); 180 | } else { 181 | settings.end(); 182 | } 183 | 184 | CAN_MsgSemaphore=xSemaphoreCreateMutex(); // as stuff is done asynchronously, we need to make sure that the message will not be transmitted when its being written to 185 | BufferSemaphore=xSemaphoreCreateMutex(); // CAN_MsgSemaphore is used when encoding the message and transmitting it, while BufferSemaphore is used when acquiring new data or encoding the message 186 | canRxQueue=xQueueCreate(100, sizeof(twai_message_t)); // internal EHU32 queue for messages to be handled by the canProcessTask 187 | canTxQueue=xQueueCreate(100, sizeof(twai_message_t)); // internal EHU32 queue for messages to be transmitted 188 | canDispQueue=xQueueCreate(255, sizeof(uint8_t)); // queue used for handling of raw ISO 15765-2 data that's meant for the display (Aux string detection) 189 | eventGroup=xEventGroupCreate(); // just one eventGroup for now 190 | 191 | // FreeRTOS tasks 192 | xTaskCreatePinnedToCore(canReceiveTask, "CANbusReceiveTask", 4096, NULL, 1, &canReceiveTaskHandle, 1); 193 | xTaskCreatePinnedToCore(canTransmitTask, "CANbusTransmitTask", 4096, NULL, 1, &canTransmitTaskHandle, 0); 194 | xTaskCreatePinnedToCore(canProcessTask, "CANbusMessageProcessor", 8192, NULL, 2, &canProcessTaskHandle, 0); 195 | xTaskCreatePinnedToCore(canDisplayTask, "DisplayUpdateTask", 8192, NULL, 1, &canDisplayTaskHandle, 1); 196 | vTaskSuspend(canDisplayTaskHandle); 197 | xTaskCreatePinnedToCore(canWatchdogTask, "WatchdogTask", 2048, NULL, tskIDLE_PRIORITY, &canWatchdogTaskHandle, 0); 198 | xTaskCreatePinnedToCore(canMessageDecoder, "MessageDecoder", 2048, NULL, tskIDLE_PRIORITY, &canMessageDecoderTaskHandle, 0); 199 | vTaskSuspend(canMessageDecoderTaskHandle); 200 | #ifdef DEBUG 201 | xTaskCreate(CANsimTask, "CANbusSimulateEvents", 2048, NULL, 21, NULL); // allows to simulate button presses through serial 202 | #endif 203 | xTaskCreatePinnedToCore(canAirConMacroTask, "AirConMacroTask", 2048, NULL, 10, &canAirConMacroTaskHandle, 0); 204 | vTaskSuspend(canAirConMacroTaskHandle); // Aircon macro task exists solely to execute simulated button presses asynchronously, as such it is only started when needed 205 | xTaskCreatePinnedToCore(eventHandlerTask, "eventHandler", 8192, NULL, 4, &eventHandlerTaskHandle, 1); 206 | } 207 | 208 | // this task monitors radio messages and resets the program if the radio goes to sleep or CAN dies 209 | void canWatchdogTask(void *pvParameters){ 210 | static BaseType_t notifResult; 211 | while(1){ 212 | notifResult=xTaskNotifyWait(0, 0, NULL, pdMS_TO_TICKS(15000)); // wait for a notification that display packet from the radio unit has been received 213 | 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 214 | DEBUG_PRINTLN("WATCHDOG: Triggering software reset..."); 215 | vTaskDelay(pdMS_TO_TICKS(100)); 216 | a2dp_shutdown(); // this or disp_mode=-1? 217 | } else { 218 | DEBUG_PRINTLN("WATCHDOG: Reset successful."); 219 | xTaskNotifyStateClear(NULL); 220 | } 221 | vTaskDelay(pdMS_TO_TICKS(1000)); 222 | } 223 | } 224 | 225 | // reads settings from preferences 226 | bool getPreferencesBool(const char* key){ 227 | Preferences settings; 228 | bool result; 229 | settings.begin("my-app", true); 230 | result=settings.getBool(key, 0); 231 | settings.end(); 232 | return result; 233 | } 234 | 235 | // writes settings to preferences 236 | void setPreferencesBool(const char* key, bool value){ 237 | Preferences settings; 238 | settings.begin("my-app", false); 239 | settings.putBool(key, value); 240 | settings.end(); 241 | } 242 | 243 | // below functions are used to simplify interaction with freeRTOS eventGroups 244 | void setFlag(uint32_t bit){ 245 | xEventGroupSetBits(eventGroup, bit); 246 | } 247 | 248 | // clears an event bit 249 | void clearFlag(uint32_t bit){ 250 | xEventGroupClearBits(eventGroup, bit); 251 | } 252 | 253 | // waits for an event bit to be set, blocking indefinitely if 2nd argument not provided 254 | void waitForFlag(uint32_t bit, TickType_t ticksToWait=portMAX_DELAY){ 255 | xEventGroupWaitBits(eventGroup, bit, pdFALSE, pdTRUE, ticksToWait); 256 | } 257 | 258 | // Check if a specific event bit is set (without blocking) 259 | bool checkFlag(uint32_t bit){ 260 | EventBits_t bits=xEventGroupGetBits(eventGroup); 261 | return (bits&bit)!=0; 262 | } 263 | 264 | // used to clear saved settings and go through the setup again on next reboot 265 | void prefs_clear(){ 266 | Preferences settings; 267 | settings.begin("my-app", false); 268 | settings.clear(); 269 | settings.end(); 270 | } 271 | 272 | // 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 273 | // it acts as a bridge between UTF-8 text data and the resulting CAN messages meant to be transmitted to the display 274 | 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) 275 | DEBUG_PRINTLN("EVENTS: Refreshing buffer..."); 276 | xSemaphoreTake(CAN_MsgSemaphore, portMAX_DELAY); // take the semaphore as a way to prevent any transmission when the message structure is being written 277 | xSemaphoreTake(BufferSemaphore, portMAX_DELAY); // we take both semaphores, since this task specifically interacts with both the internal data buffers and the CAN message buffer 278 | if(!disp_mode_override){ 279 | if(disp_mode==0 && (album_buffer[0]!='\0' || title_buffer[0]!='\0' || artist_buffer[0]!='\0')){ // audio metadata mode 280 | prepareMultiPacket(processDisplayMessage(album_buffer, title_buffer, artist_buffer), DisplayMsg); // prepare a 3-line message (audio Title, Album and Artist) 281 | } else { 282 | if(disp_mode==1){ // vehicle data mode (3-line) 283 | prepareMultiPacket(processDisplayMessage(coolant_buffer, speed_buffer, voltage_buffer), DisplayMsg); // vehicle data buffer 284 | } 285 | if(disp_mode==2){ // coolant mode (1-line) 286 | prepareMultiPacket(processDisplayMessage(nullptr, coolant_buffer, nullptr), DisplayMsg); // vehicle data buffer (single line) 287 | } 288 | } 289 | } else { // overriding buffers 290 | prepareMultiPacket(processDisplayMessage(up_line_text, mid_line_text, low_line_text), DisplayMsg); 291 | } 292 | xSemaphoreGive(CAN_MsgSemaphore); 293 | xSemaphoreGive(BufferSemaphore); // releasing semaphores 294 | vTaskResume(canDisplayTaskHandle); // buffer has been updated, transmit 295 | clearFlag(DIS_forceUpdate); 296 | } 297 | 298 | // this task handles events and output to display in context of events, such as new data in buffers or A2DP events 299 | void eventHandlerTask(void *pvParameters){ 300 | while(1){ 301 | if(checkFlag(OTA_begin)){ 302 | disp_mode=0; 303 | writeTextToDisplay(1, "Bluetooth off", "OTA Started", "Waiting for connection..."); 304 | vTaskDelay(1000); 305 | 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 306 | #ifndef DEBUG 307 | OTA_Handle(); 308 | #endif 309 | } 310 | 311 | if(disp_mode==1 && checkFlag(ehu_started)){ // if running in measurement block mode, check time and if enough time has elapsed ask for new data 312 | if(checkFlag(disp_mode_changed)){ 313 | clearFlag(disp_mode_changed); 314 | writeTextToDisplay(1, nullptr, "No data yet...", nullptr); // print a status message that will stay if display/ecc are not responding 315 | } 316 | if((last_millis_req+400) REINSTALLING"); 347 | vTaskSuspend(canReceiveTaskHandle); 348 | vTaskSuspend(canTransmitTaskHandle); 349 | vTaskSuspend(canProcessTaskHandle); 350 | vTaskSuspend(canDisplayTaskHandle); 351 | vTaskSuspend(canWatchdogTaskHandle); 352 | //twai_initiate_recovery(); // twai_initiate_recovery(); leads to hard crashes - it's something that ESP-IDF need to fix 353 | twai_stop(); 354 | if(twai_driver_uninstall()==ESP_OK){ 355 | DEBUG_PRINTLN("CAN: TWAI DRIVER UNINSTALL OK"); 356 | } else { 357 | DEBUG_PRINTLN("CAN: TWAI DRIVER UNINSTALL FAIL!!! Rebooting..."); // total fail - just reboot at this point 358 | vTaskDelay(pdMS_TO_TICKS(100)); 359 | ESP.restart(); 360 | } 361 | vTaskDelay(100); 362 | twai_init(); 363 | vTaskDelay(100); 364 | vTaskResume(canReceiveTaskHandle); 365 | vTaskResume(canTransmitTaskHandle); 366 | vTaskResume(canProcessTaskHandle); 367 | vTaskResume(canDisplayTaskHandle); 368 | vTaskResume(canWatchdogTaskHandle); 369 | } 370 | 371 | A2DP_EventHandler(); // process bluetooth and audio flags set by A2DP callbacks 372 | vTaskDelay(10); 373 | } 374 | } 375 | 376 | // loop will do nothing 377 | void loop(){ 378 | vTaskDelay(pdMS_TO_TICKS(1000)); 379 | } -------------------------------------------------------------------------------- /src/CAN.ino: -------------------------------------------------------------------------------- 1 | #include "driver/twai.h" 2 | 3 | void OTAhandleTask(void* pvParameters); 4 | 5 | // CAN-related variables 6 | volatile uint8_t canISO_frameSpacing=0; // simple implementation of ISO 15765-2 variable frame spacing, based on flow control frames by the receiving node 7 | 8 | // defining static CAN frames for simulation 9 | const twai_message_t simulate_scroll_up={ .identifier=0x201, .data_length_code=3, .data={0x08, 0x6A, 0x01}}, 10 | simulate_scroll_down={ .identifier=0x201, .data_length_code=3, .data={0x08, 0x6A, 0xFF}}, 11 | simulate_scroll_press={ .identifier=0x206, .data_length_code=3, .data={0x01, 0x84, 0x0}}, 12 | simulate_scroll_release={ .identifier=0x206, .data_length_code=3, .data={0x0, 0x84, 0x02}}, 13 | Msg_ACmacro_down={ .identifier=0x208, .data_length_code=3, .data={0x08, 0x16, 0x01}}, 14 | Msg_ACmacro_up={ .identifier=0x208, .data_length_code=3, .data={0x08, 0x16, 0xFF}}, 15 | Msg_ACmacro_press={ .identifier=0x208, .data_length_code=3, .data={0x01, 0x17, 0x0}}, 16 | Msg_ACmacro_release={ .identifier=0x208, .data_length_code=3, .data={0x0, 0x17, 0x02}}, 17 | Msg_MeasurementRequestDIS={ .identifier=0x246, .data_length_code=7, .data={0x06, 0xAA, 0x01, 0x01, 0x0B, 0x0E, 0x13}}, 18 | Msg_MeasurementRequestECC={ .identifier=0x248, .data_length_code=7, .data={0x06, 0xAA, 0x01, 0x01, 0x07, 0x10, 0x11}}, 19 | Msg_VoltageRequestDIS={ .identifier=0x246, .data_length_code=5, .data={0x04, 0xAA, 0x01, 0x01, 0x13}}, 20 | Msg_CoolantRequestDIS={ .identifier=0x246, .data_length_code=5, .data={0x04, 0xAA, 0x01, 0x01, 0x0B}}, 21 | Msg_CoolantRequestECC={ .identifier=0x248, .data_length_code=5, .data={0x04, 0xAA, 0x01, 0x01, 0x10}}; 22 | 23 | // can't initialize the values of the union inside the twai_message_t type struct, which is why it's defined here, then the transmit task sets the .ss flag 24 | twai_message_t Msg_PreventDisplayUpdate={ .identifier=0x2C1, .data_length_code=8, .data={0x30, 0x0, 0x7F, 0, 0, 0, 0, 0}}, 25 | Msg_AbortTransmission={ .identifier=0x2C1, .data_length_code=8, .data={0x32, 0x0, 0, 0, 0, 0, 0, 0}}; // can have unforseen consequences such as resets! use as last resort 26 | 27 | // initializing CAN communication 28 | void twai_init(){ 29 | twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(GPIO_NUM_5, GPIO_NUM_4, TWAI_MODE_NORMAL); // CAN bus set up 30 | g_config.rx_queue_len=40; 31 | g_config.tx_queue_len=5; 32 | g_config.intr_flags=(ESP_INTR_FLAG_NMI & ESP_INTR_FLAG_IRAM); // run the TWAI driver at the highest possible priority 33 | twai_timing_config_t t_config = {.brp = 42, .tseg_1 = 15, .tseg_2 = 4, .sjw = 3, .triple_sampling = false}; // set CAN prescalers and time quanta for 95kbit 34 | twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); 35 | DEBUG_PRINT("\nCAN/TWAI SETUP => "); 36 | if(twai_driver_install(&g_config, &t_config, &f_config) == ESP_OK) { 37 | DEBUG_PRINT("DRV_INSTALL: OK "); 38 | } else { 39 | DEBUG_PRINT("DRV_INST: FAIL "); 40 | } 41 | if (twai_start() == ESP_OK) { 42 | DEBUG_PRINT("DRV_START: OK "); 43 | } else { 44 | DEBUG_PRINT("DRV_START: FAIL "); 45 | } 46 | uint32_t alerts_to_enable=TWAI_ALERT_TX_SUCCESS; 47 | if(twai_reconfigure_alerts(alerts_to_enable, NULL) == ESP_OK){ 48 | DEBUG_PRINTLN("ALERTS: OK \n"); 49 | } else { 50 | DEBUG_PRINTLN("ALERTS: FAIL \n"); 51 | } 52 | } 53 | 54 | // this task only reads CAN messages, filters them and enqueues them to be decoded ansynchronously. 0x6C1 is a special case, as the radio message has to be blocked ASAP 55 | void canReceiveTask(void *pvParameters){ 56 | static twai_message_t Recvd_CAN_MSG, DummyFirstFrame={ .identifier=0x6C1, .data_length_code=8, .data={0x10, 0xA7, 0x50, 0x00, 0xA4, 0x03, 0x10, 0x13}}; 57 | uint32_t alerts_FlowCtl; // separate buffer for the flow control alerts, prevents race conditions with the other task which also uses alerts 58 | bool allowDisplayBlocking=0, firstAckReceived=0, overwriteAttemped=0; 59 | uint32_t flowCtlUsed=(displayMsgIdentifier-0x400); // set from memory, if using 0x6C0 flow control will be 0x2C0 etc. 60 | Msg_PreventDisplayUpdate.extd=0; Msg_PreventDisplayUpdate.ss=1; Msg_PreventDisplayUpdate.self=0; Msg_PreventDisplayUpdate.rtr=0; 61 | Msg_AbortTransmission.extd=0; Msg_AbortTransmission.ss=1; Msg_AbortTransmission.self=0; Msg_AbortTransmission.rtr=0; 62 | while(1){ 63 | allowDisplayBlocking=checkFlag(CAN_allowAutoRefresh); // checking earlier improves performance, there's very little time to send that message, otherwise we get error frames (because of the same ID) 64 | if(twai_receive(&Recvd_CAN_MSG, portMAX_DELAY)==ESP_OK){ 65 | switch(Recvd_CAN_MSG.identifier){ 66 | case 0x6C1: { 67 | if(disp_mode!=-1){ // don't bother checking the data if there's no need to update the display 68 | if(Recvd_CAN_MSG.data[0]==0x10 && (Recvd_CAN_MSG.data[2]==0x40 || Recvd_CAN_MSG.data[2]==0xC0 || (Recvd_CAN_MSG.data[2]==0x50 && Recvd_CAN_MSG.data[1]==0x4A)) && Recvd_CAN_MSG.data[5]==0x03 && (disp_mode!=0 || allowDisplayBlocking)){ // another task processes the data since we can't do that here 69 | twai_transmit(&Msg_PreventDisplayUpdate, pdMS_TO_TICKS(30)); // radio blocking msg has to be transmitted ASAP, which is why we skip the queue 70 | DEBUG_PRINTLN("CAN: Received display update, trying to block"); 71 | twai_read_alerts(&alerts_FlowCtl, pdMS_TO_TICKS(10)); // read stats to a local alert buffer 72 | if(alerts_FlowCtl & TWAI_ALERT_TX_SUCCESS){ 73 | clearFlag(CAN_flowCtlFail); 74 | DEBUG_PRINTLN("CAN: Blocked successfully"); 75 | } else { 76 | setFlag(CAN_flowCtlFail); // lets the display task know that we failed blocking the display TX and as such the display task shall wait 77 | DEBUG_PRINTLN("CAN: Blocking failed!"); 78 | } 79 | overwriteAttemped=1; // if the display message retransmission was intended to mask the radio's message 80 | if(eTaskGetState(canDisplayTaskHandle)!=eSuspended){ 81 | if(Recvd_CAN_MSG.data[0]==0x10) setFlag(CAN_abortMultiPacket); // let the transmission task know that the radio has transmissed a new first frame -> any ongoing transmission is no longer valid 82 | } 83 | vTaskResume(canDisplayTaskHandle); // only retransmit the msg for audio metadata mode and single line coolant, since these don't update frequently 84 | } 85 | } 86 | } 87 | case 0x201: 88 | case 0x206: 89 | case 0x208: 90 | case 0x501: 91 | case 0x546: 92 | case 0x548: 93 | case 0x4E8: 94 | case 0x6C8: 95 | xQueueSend(canRxQueue, &Recvd_CAN_MSG, portMAX_DELAY); // queue the message contents to be read at a later time 96 | break; 97 | case 0x2C1:{ // this attempts to invalidate the radio's display call with identifier 0x6C1 98 | if(flowCtlUsed==0x2C1){ // old/backup logic for radio messages on 0x6C1 99 | if(firstAckReceived || !overwriteAttemped){ // disregard the first flow control frame meant for the radio unit ONLY if it was a result of retransmission to mask the radio's display update 100 | waitForFlag(CAN_MessageReady, pdMS_TO_TICKS(20)); // this is blocking a lot of stuff so gotta find a sweet spot for how long to block for 101 | if(Recvd_CAN_MSG.data[0]==0x30){ 102 | xTaskNotifyGive(canDisplayTaskHandle); // let the display update task know that the data is ready to be transmitted 103 | xQueueSend(canRxQueue, &Recvd_CAN_MSG, portMAX_DELAY); 104 | if(firstAckReceived) firstAckReceived=0; // reset it to be ready for the next one 105 | if(overwriteAttemped) overwriteAttemped=0; 106 | } 107 | } else { 108 | firstAckReceived=1; // flow control not meant for EHU32, set the switch and wait for the second one 109 | } 110 | } 111 | if(overwriteAttemped && flowCtlUsed!=0x2C1){ 112 | twai_transmit(&DummyFirstFrame, pdMS_TO_TICKS(100)); // transmit a dummy frame ASAP to invalidate previous display call 113 | DEBUG_PRINTLN("CAN: Attempting to invalidate radio's screen call..."); 114 | overwriteAttemped=0; 115 | } 116 | break; 117 | } 118 | default: break; 119 | } 120 | if(Recvd_CAN_MSG.identifier==flowCtlUsed && Recvd_CAN_MSG.identifier!=0x2C1 && Recvd_CAN_MSG.data[0]==0x30){ // can't be a switch case because it might be dynamic 121 | xTaskNotifyGive(canDisplayTaskHandle); 122 | } 123 | } 124 | } 125 | } 126 | 127 | // this task processes filtered CAN frames read from canRxQueue 128 | void canProcessTask(void *pvParameters){ 129 | static twai_message_t RxMsg; 130 | bool badVoltage_VectraC_bypass=getPreferencesBool("vectra"); // read from the preferences to check if the car is a vectra 131 | unsigned long millis_EccKnobPressed; 132 | while(1){ 133 | xQueueReceive(canRxQueue, &RxMsg, portMAX_DELAY); // receives data from the internal queue 134 | switch(RxMsg.identifier){ 135 | case 0x201: { // radio button decoder 136 | bool btn_state=RxMsg.data[0]; 137 | unsigned int btn_ms_held=(RxMsg.data[2]*100); 138 | switch(RxMsg.data[1]){ 139 | case 0x30: canActionEhuButton0(btn_state, btn_ms_held); // CD30 has no '0' button! 140 | break; 141 | case 0x31: canActionEhuButton1(btn_state, btn_ms_held); 142 | break; 143 | case 0x32: canActionEhuButton2(btn_state, btn_ms_held); 144 | break; 145 | case 0x33: canActionEhuButton3(btn_state, btn_ms_held); 146 | break; 147 | case 0x34: canActionEhuButton4(btn_state, btn_ms_held); 148 | break; 149 | case 0x35: canActionEhuButton5(btn_state, btn_ms_held); 150 | break; 151 | case 0x36: canActionEhuButton6(btn_state, btn_ms_held); 152 | break; 153 | case 0x37: canActionEhuButton7(btn_state, btn_ms_held); 154 | break; 155 | case 0x38: canActionEhuButton8(btn_state, btn_ms_held); 156 | break; 157 | case 0x39: canActionEhuButton9(btn_state, btn_ms_held); 158 | break; 159 | default: break; 160 | } 161 | break; 162 | } 163 | case 0x206: { // decodes steering wheel buttons 164 | if(checkFlag(bt_connected) && RxMsg.data[0]==0x0 && checkFlag(CAN_allowAutoRefresh)){ // makes sure "Aux" is displayed, otherwise forward/next buttons will have no effect 165 | switch(RxMsg.data[1]){ 166 | case 0x81:{ 167 | if(!vehicle_UHP_present){ // only enable the play/pause functionality for vehicles without UHP otherwise it could conflict with the factory bluetooth hands-free 168 | if(checkFlag(bt_audio_playing)){ // upper left button (box with waves) 169 | a2dp_sink.pause(); 170 | } else { 171 | a2dp_sink.play(); 172 | } 173 | } 174 | break; 175 | } 176 | case 0x91:{ 177 | a2dp_sink.next(); // upper right button (arrow up) 178 | break; 179 | } 180 | case 0x92:{ 181 | a2dp_sink.previous(); // lower right button (arrow down) 182 | break; 183 | } 184 | default: break; 185 | } 186 | } 187 | break; 188 | } 189 | case 0x208: { // AC panel button event 190 | if(eTaskGetState(canAirConMacroTaskHandle)==eSuspended){ 191 | if(RxMsg.data[0]==0x01 && RxMsg.data[1]==0x17 && RxMsg.data[2]==0x0){ // button pressed, start save timestamp 192 | millis_EccKnobPressed=millis(); 193 | } else { 194 | if(RxMsg.data[0]==0x0 && RxMsg.data[1]==0x17 && (RxMsg.data[2]==0x0 || RxMsg.data[2]>=0x05)){ 195 | if(RxMsg.data[2]>=0x05){ // properly implemented ECC button counting (such as Vectra C) does not require stupid workarounds 196 | vTaskResume(canAirConMacroTaskHandle); 197 | } else { 198 | if((millis_EccKnobPressed+400)<=millis()){ // late Astra/Corsa/Zafira ECCs don't follow the standard, as these only report the initial press and release (saying it was released after being held for 0ms lol) 199 | vTaskResume(canAirConMacroTaskHandle); // so we count the time on our own, and if the time elapsed 200 | } 201 | } 202 | } 203 | } 204 | } // should be an else here to implement something to stop the macro task, can't be bothered to implement this now 205 | break; 206 | } 207 | case 0x2C1: { 208 | if(RxMsg.data[2]!=0 && canISO_frameSpacing!=RxMsg.data[2]) canISO_frameSpacing=RxMsg.data[2]; // adjust ISO 15765-2 frame spacing delay only if the receiving node calls for it 209 | break; 210 | } 211 | case 0x501: { // CD30MP3 goes to sleep -> disable bluetooth connectivity 212 | if(checkFlag(a2dp_started) && RxMsg.data[3]==0x18){ 213 | a2dp_shutdown(); 214 | } 215 | break; 216 | } 217 | case 0x546: { // display measurement blocks (used as a fallback or for ) 218 | if(disp_mode==1 || disp_mode==2){ 219 | xSemaphoreTake(BufferSemaphore, portMAX_DELAY); 220 | DEBUG_PRINT("CAN: Got measurements from DIS: "); 221 | 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 222 | case 0x0B: { // 0x0B references coolant temps 223 | DEBUG_PRINT("coolant\n"); 224 | int CAN_data_coolant=RxMsg.data[5]-40; 225 | snprintf(voltage_buffer, sizeof(voltage_buffer), " "); 226 | snprintf(coolant_buffer, sizeof(coolant_buffer), "Coolant temp: %d%c%cC ", CAN_data_coolant, 0xC2, 0xB0); 227 | setFlag(CAN_coolant_recvd); 228 | break; 229 | } 230 | case 0x0E: { 231 | DEBUG_PRINT("speed\n"); 232 | int CAN_data_speed=(RxMsg.data[2]<<8 | RxMsg.data[3]); 233 | CAN_data_speed/=128; 234 | snprintf(speed_buffer, sizeof(speed_buffer), "%d km/h ", CAN_data_speed); 235 | setFlag(CAN_speed_recvd); 236 | break; 237 | } 238 | case 0x13: { // reading voltage from display, research courtesy of @xymetox 239 | DEBUG_PRINT("battery voltage\n"); 240 | float CAN_data_voltage=RxMsg.data[6]; 241 | CAN_data_voltage/=10; 242 | snprintf(voltage_buffer, sizeof(voltage_buffer), "Voltage: %.1f V ", CAN_data_voltage); 243 | setFlag(CAN_voltage_recvd); 244 | break; 245 | } 246 | default: break; 247 | } 248 | if(checkFlag(CAN_voltage_recvd) && checkFlag(CAN_coolant_recvd) && checkFlag(CAN_speed_recvd)){ 249 | clearFlag(CAN_voltage_recvd); 250 | clearFlag(CAN_coolant_recvd); 251 | clearFlag(CAN_speed_recvd); 252 | setFlag(CAN_new_dataSet_recvd); 253 | } 254 | xSemaphoreGive(BufferSemaphore); // let the message processing continue 255 | } 256 | break; 257 | } 258 | case 0x548: { // AC measurement blocks 259 | if(disp_mode==1 || disp_mode==2) xSemaphoreTake(BufferSemaphore, 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 260 | DEBUG_PRINT("CAN: Got measurements from ECC: "); 261 | switch(RxMsg.data[0]){ // measurement block ID -> update data which the message is referencing 262 | case 0x07: { // 0x07 references battery voltage 263 | if(!badVoltage_VectraC_bypass){ 264 | float CAN_data_voltage=RxMsg.data[2]; 265 | CAN_data_voltage/=10; 266 | if(CAN_data_voltage>9 && CAN_data_voltage<16){ 267 | snprintf(voltage_buffer, sizeof(voltage_buffer), "Voltage: %.1f V ", CAN_data_voltage); 268 | } else { // we get erroneous readings, as such we'll switch to reading from display on the next measurement request 269 | badVoltage_VectraC_bypass=1; 270 | setPreferencesBool("vectra", 1); 271 | } 272 | setFlag(CAN_voltage_recvd); 273 | DEBUG_PRINT("battery voltage\n"); 274 | } else { 275 | xQueueSend(canTxQueue, &Msg_VoltageRequestDIS, pdMS_TO_TICKS(100)); // request just the voltage from Vectra's display because ECC voltage reading is erratic compared to Astra/Zafira 276 | } 277 | break; 278 | } 279 | case 0x10: { // 0x10 references coolant temps 280 | unsigned short raw_coolant=(RxMsg.data[3]<<8 | RxMsg.data[4]); 281 | float CAN_data_coolant=raw_coolant; 282 | CAN_data_coolant/=10; 283 | snprintf(coolant_buffer, sizeof(coolant_buffer), "Coolant temp: %.1f%c%cC ", CAN_data_coolant, 0xC2, 0xB0); 284 | setFlag(CAN_coolant_recvd); 285 | DEBUG_PRINT("coolant\n"); 286 | break; 287 | } 288 | case 0x11: { // 0x11 references RPMs and speed 289 | int CAN_data_rpm=(RxMsg.data[1]<<8 | RxMsg.data[2]); 290 | int CAN_data_speed=RxMsg.data[4]; 291 | snprintf(speed_buffer, sizeof(speed_buffer), "%d km/h %d RPM ", CAN_data_speed, CAN_data_rpm); 292 | setFlag(CAN_speed_recvd); 293 | DEBUG_PRINT("speed and RPMs\n"); 294 | break; 295 | } 296 | default: break; 297 | } 298 | if(checkFlag(CAN_voltage_recvd) && checkFlag(CAN_coolant_recvd) && checkFlag(CAN_speed_recvd)){ 299 | clearFlag(CAN_voltage_recvd); 300 | clearFlag(CAN_coolant_recvd); 301 | clearFlag(CAN_speed_recvd); 302 | setFlag(CAN_new_dataSet_recvd); 303 | } 304 | if(disp_mode==1 || disp_mode==2) xSemaphoreGive(BufferSemaphore); // let the message processing continue 305 | break; 306 | } 307 | case 0x6C1: { // radio requests a display update 308 | if(!checkFlag(a2dp_started)){ 309 | setFlag(ehu_started); // start the bluetooth A2DP service after first radio display call 310 | disp_mode=0; 311 | vTaskResume(canMessageDecoderTaskHandle); // begin decoding data from the display 312 | } else if(checkFlag(a2dp_started) && !checkFlag(ehu_started)){ 313 | a2dp_sink.reconnect(); 314 | setFlag(ehu_started); 315 | } 316 | if(disp_mode==0){ // queue that data for decoding by another task 317 | for(int i=1; i<=7; i++){ 318 | xQueueSend(canDispQueue, &RxMsg.data[i], portMAX_DELAY); // send a continuous byte stream 319 | } 320 | } 321 | xTaskNotifyGive(canWatchdogTaskHandle); // reset the watchdog 322 | break; 323 | } 324 | case 0x6C8: { 325 | if(!checkFlag(ECC_present)) setFlag(ECC_present); // adjust ISO 15765-2 frame spacing delay only if the receiving node calls for it 326 | break; 327 | } 328 | default: break; 329 | } 330 | } 331 | } 332 | 333 | // this task receives CAN messages from canTxQueue and transmits them asynchronously 334 | void canTransmitTask(void *pvParameters){ 335 | static twai_message_t TxMessage; 336 | int alert_result; 337 | while(1){ 338 | xQueueReceive(canTxQueue, &TxMessage, portMAX_DELAY); 339 | TxMessage.extd=0; 340 | TxMessage.rtr=0; 341 | TxMessage.ss=0; 342 | TxMessage.self=0; 343 | //DEBUG_PRINTF("%03X # %02X %02X %02X %02X %02X %02X %02X %02X", TxMessage.identifier, TxMessage.data[0], TxMessage.data[1], TxMessage.data[2], TxMessage.data[3], TxMessage.data[4], TxMessage.data[5], TxMessage.data[6], TxMessage.data[7]); 344 | if(twai_transmit(&TxMessage, pdMS_TO_TICKS(50))==ESP_OK) { 345 | //DEBUG_PRINT(" Q:OK "); 346 | } else { 347 | //DEBUG_PRINT("Q:FAIL "); 348 | setFlag(CAN_prevTxFail); 349 | if(TxMessage.identifier==displayMsgIdentifier && (TxMessage.data[0]==0x10 || TxMessage.data[0]==0x11)) setFlag(CAN_abortMultiPacket); 350 | } 351 | alert_result=twai_read_alerts(&alerts_triggered, pdMS_TO_TICKS(10)); // read stats 352 | if(alert_result==ESP_OK){ 353 | //DEBUG_PRINT("AR:OK "); 354 | if(alerts_triggered & TWAI_ALERT_TX_SUCCESS){ 355 | if(TxMessage.identifier==displayMsgIdentifier && (TxMessage.data[0]==0x10 || TxMessage.data[0]==0x11)) setFlag(CAN_MessageReady); // let the display task know that the first frame has been transmitted and we're expecting flow control (0x2C1) frame now 356 | //DEBUG_PRINTLN("TX:OK "); 357 | } else { 358 | DEBUG_PRINTLN("TX:FAIL "); 359 | setFlag(CAN_prevTxFail); 360 | } 361 | } else { 362 | setFlag(CAN_prevTxFail); 363 | if(TxMessage.identifier==displayMsgIdentifier && (TxMessage.data[0]==0x10 || TxMessage.data[0]==0x11)) setFlag(CAN_abortMultiPacket); 364 | DEBUG_PRINT("AR:FAIL:"); 365 | if(alert_result==ESP_ERR_INVALID_ARG){ 366 | DEBUG_PRINTLN("INV_ARG"); 367 | } 368 | if(alert_result==ESP_ERR_INVALID_STATE){ 369 | DEBUG_PRINTLN("INV_STATE"); 370 | } 371 | if(alert_result==ESP_ERR_TIMEOUT){ 372 | DEBUG_PRINTLN("TIMEOUT"); 373 | } 374 | } 375 | } 376 | } 377 | 378 | #ifdef DEBUG 379 | char* split_text[3]; 380 | char usage_stats[512]; 381 | 382 | bool checkMutexState(){ 383 | if(xSemaphoreTake(CAN_MsgSemaphore, pdMS_TO_TICKS(1))==pdTRUE){ 384 | xSemaphoreGive(CAN_MsgSemaphore); 385 | return 0; 386 | } else { 387 | return 1; 388 | } 389 | } 390 | 391 | void CANsimTask(void *pvParameters){ 392 | while(1){ 393 | if(Serial.available()>0){ 394 | Serial.print("SERIAL: Action - "); 395 | char serial_input=Serial.read(); 396 | switch(serial_input){ 397 | case '2': Serial.print("UP\n"); 398 | xQueueSend(canTxQueue, &simulate_scroll_up, portMAX_DELAY); 399 | break; 400 | case '8': Serial.print("DOWN\n"); 401 | xQueueSend(canTxQueue, &simulate_scroll_down, portMAX_DELAY); 402 | break; 403 | case '6': Serial.print("UP\n"); 404 | xQueueSend(canTxQueue, &simulate_scroll_up, portMAX_DELAY); 405 | break; 406 | case '4': Serial.print("DOWN\n"); 407 | xQueueSend(canTxQueue, &simulate_scroll_down, portMAX_DELAY); 408 | break; 409 | case '5': Serial.print("PRESS\n"); 410 | xQueueSend(canTxQueue, &simulate_scroll_press, portMAX_DELAY); 411 | vTaskDelay(pdMS_TO_TICKS(100)); 412 | xQueueSend(canTxQueue, &simulate_scroll_release, portMAX_DELAY); 413 | break; 414 | case 'd': { 415 | Serial.print("CURRENT FLAGS CAN: "); 416 | Serial.printf("CAN_MessageReady=%d CAN_prevTxFail=%d, DIS_forceUpdate=%d, ECC_present=%d, ehu_started=%d \n", checkFlag(CAN_MessageReady), checkFlag(CAN_prevTxFail), checkFlag(DIS_forceUpdate), checkFlag(ECC_present), checkFlag(ehu_started)); 417 | Serial.print("CURRENT FLAGS BODY: "); 418 | Serial.printf("CAN_voltage_recvd=%d CAN_coolant_recvd=%d, CAN_speed_recvd=%d, CAN_new_dataSet_recvd=%d \n", checkFlag(CAN_voltage_recvd), checkFlag(CAN_coolant_recvd), checkFlag(CAN_speed_recvd), checkFlag(CAN_new_dataSet_recvd)); 419 | Serial.print("TIME AND STUFF: "); 420 | Serial.printf("last_millis_req=%lu last_millis_disp=%lu, millis=%lu \n", last_millis_req, last_millis_disp, millis()); 421 | Serial.printf("CanMsgSemaphore state: %d \n", checkMutexState()); 422 | Serial.printf("TxQueue items: %d, RxQueue items: %d \n", uxQueueMessagesWaiting(canTxQueue), uxQueueMessagesWaiting(canRxQueue)); 423 | break; 424 | } 425 | case 'T': { // print arbitrary text from serial to the display, max char count for each line is 35 426 | char inputBuffer[256]; 427 | int bytesRead=Serial.readBytesUntil('\n', inputBuffer, 256); 428 | inputBuffer[bytesRead]='\0'; 429 | serialStringSplitter(inputBuffer); 430 | disp_mode=0; 431 | writeTextToDisplay(1, split_text[0], split_text[1], split_text[2]); 432 | break; 433 | } 434 | case 'C': { 435 | prefs_clear(); 436 | break; 437 | } 438 | default: break; 439 | } 440 | } 441 | vTaskDelay(100 / portTICK_PERIOD_MS); 442 | } 443 | } 444 | 445 | void serialStringSplitter(char* input){ 446 | char* text_in; 447 | int text_count=0; 448 | text_in=strtok(input, "|"); 449 | while(text_in!=NULL && text_count<3){ 450 | split_text[text_count]=text_in; 451 | text_count++; 452 | text_in=strtok(NULL, "|"); 453 | } 454 | while(text_count<3){ 455 | split_text[text_count]=""; 456 | text_count++; 457 | } 458 | } 459 | #endif 460 | 461 | // 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 462 | void canDisplayTask(void *pvParameters){ 463 | static twai_message_t MsgToTx; 464 | MsgToTx.identifier=displayMsgIdentifier; 465 | MsgToTx.data_length_code=8; 466 | uint32_t notifResult; 467 | bool retryTx=0; 468 | while(1){ 469 | retryTx=0; 470 | if(xSemaphoreTake(CAN_MsgSemaphore, portMAX_DELAY)==pdTRUE){ // if the buffer is being accessed, block indefinitely 471 | if(checkFlag(CAN_flowCtlFail)){ 472 | vTaskDelay(pdMS_TO_TICKS(300)); // since we failed at flow control, wait for the radio to finish its business 473 | } 474 | clearFlag(CAN_prevTxFail); 475 | clearFlag(CAN_abortMultiPacket); // new transmission, we clear these 476 | memcpy(MsgToTx.data, CAN_MsgArray[0], 8); 477 | xQueueSend(canTxQueue, &MsgToTx, portMAX_DELAY); 478 | DEBUG_PRINTLN("CAN: Now waiting for flow control frame..."); 479 | if(xTaskNotifyWait(0, 0, NULL, portMAX_DELAY)==pdPASS){ // blocking execution until flow control from display arrives 480 | DEBUG_PRINTLN("CAN: Got flow control! Sending consecutive frames..."); 481 | for(int i=1;i<64 && (CAN_MsgArray[i][0]!=0x00 && !checkFlag(CAN_prevTxFail) && !checkFlag(CAN_abortMultiPacket));i++){ // this loop will stop sending data once the next packet doesn't contain a label 482 | memcpy(MsgToTx.data, CAN_MsgArray[i], 8); 483 | xQueueSend(canTxQueue, &MsgToTx, portMAX_DELAY); 484 | vTaskDelay(pdMS_TO_TICKS(canISO_frameSpacing)); // receiving node can request a variable frame spacing time, we take it into account here, so far I've seen BID request 2ms while GID/CID request 0ms (no delay) 485 | } 486 | clearFlag(CAN_MessageReady); // clear this as fast as possible once we're done sending 487 | if(checkFlag(CAN_prevTxFail) || checkFlag(CAN_abortMultiPacket)){ 488 | retryTx=1; // "queue up" to restart this task since something went wrong 489 | clearFlag(CAN_prevTxFail); 490 | } 491 | xTaskNotifyStateClear(NULL); 492 | } else { // fail 493 | DEBUG_PRINTLN("CAN: Flow control frame has not been received in time, aborting"); 494 | clearFlag(CAN_MessageReady); 495 | retryTx=1; 496 | } 497 | xSemaphoreGive(CAN_MsgSemaphore); // release the semaphore 498 | } 499 | if(!retryTx) vTaskSuspend(NULL); // have the display task stop itself only if there is no need to retransmit, else run once again 500 | } 501 | } 502 | 503 | // this task provides asynchronous simulation of button presses on the AC panel, quickly toggling AC 504 | void canAirConMacroTask(void *pvParameters){ 505 | while(1){ 506 | vTaskDelay(pdMS_TO_TICKS(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 507 | xQueueSend(canTxQueue, &Msg_ACmacro_down, portMAX_DELAY); 508 | vTaskDelay(pdMS_TO_TICKS(100)); 509 | xQueueSend(canTxQueue, &Msg_ACmacro_press, portMAX_DELAY); 510 | vTaskDelay(pdMS_TO_TICKS(100)); 511 | xQueueSend(canTxQueue, &Msg_ACmacro_release, portMAX_DELAY); 512 | vTaskDelay(pdMS_TO_TICKS(100)); 513 | xQueueSend(canTxQueue, &Msg_ACmacro_up, portMAX_DELAY); 514 | vTaskDelay(pdMS_TO_TICKS(100)); 515 | xQueueSend(canTxQueue, &Msg_ACmacro_up, portMAX_DELAY); 516 | vTaskDelay(pdMS_TO_TICKS(100)); 517 | xQueueSend(canTxQueue, &Msg_ACmacro_press, portMAX_DELAY); 518 | vTaskDelay(pdMS_TO_TICKS(100)); 519 | xQueueSend(canTxQueue, &Msg_ACmacro_release, portMAX_DELAY); 520 | vTaskSuspend(NULL); 521 | } 522 | } 523 | 524 | // this task monitors raw data contained within messages sent by the radio and looks for Aux string being printed to the display; rejects "Aux" in views such as "Audio Source" screen (CD70/DVD90) 525 | void canMessageDecoder(void *pvParameters){ 526 | uint8_t rxDisplay; 527 | const uint8_t AuxPattern[8]={0x00, 0x6D, 0x00, 0x41, 0x00, 0x75, 0x00, 0x78}; // snippet of data to look for, allows for robust detection of "Aux" on all kinds of headunits 528 | int patternIndex=0; 529 | bool patternFound=0; 530 | int currentIndex[6] = {0}; 531 | const char patterns[6][17] = { // this is a crutch for CD30/CD40 "SOUND" menu, required to be able to adjust fader/balance/bass/treble, otherwise EHU32 will block it from showing up 532 | {0, 0x6D, 0, 0x41, 0, 0x75, 0, 0x78}, // formatted Aux (left or center aligned). Weird formatting because the data is in UTF-16 533 | {0x46, 0, 0x61, 0, 0x64, 0, 0x65, 0, 0x72}, // Fader 534 | {0x42, 0, 0x61, 0, 0x6c, 0, 0x61, 0, 0x6e, 0, 0x63, 0, 0x65}, // Balance 535 | {0x42, 0, 0x61, 0, 0x73, 0, 0x73}, // Bass 536 | {0x54, 0, 0x72, 0, 0x65, 0, 0x62, 0, 0x6c, 0, 0x65}, // Treble 537 | {0x53, 0, 0x6f, 0, 0x75, 0, 0x6e, 0, 0x64, 0, 0x20, 0, 0x4f, 0, 0x66, 0, 0x66} // Sound Off 538 | }; 539 | const char patternLengths[6] = {8, 9, 13, 7, 11, 17}; 540 | while(1){ 541 | if(xQueueReceive(canDispQueue, &rxDisplay, portMAX_DELAY)==pdTRUE){ // wait for new data queued by the ProcessTask 542 | for(int i=0;i<6; i++){ 543 | if(rxDisplay==patterns[i][currentIndex[i]]){ 544 | currentIndex[i]++; 545 | if(currentIndex[i]==patternLengths[i]){ 546 | switch(i){ 547 | case 0:{ // formatting+Aux detected 548 | patternFound=1; 549 | last_millis_aux=millis(); // keep track of when was the last time Aux has been seen 550 | DEBUG_PRINTLN("CAN Decode: Found Aux string!"); 551 | break; 552 | } 553 | case 1: // either Fader, Balance, Bass, Treble or Sound Off 554 | case 2: 555 | case 3: 556 | case 4: 557 | case 5: patternFound=0; 558 | clearFlag(CAN_allowAutoRefresh); // we let the following message(s) through 559 | } 560 | for(int j=0; j<6; j++){ 561 | currentIndex[j]=0; 562 | } 563 | break; 564 | } 565 | } else { 566 | currentIndex[i] = 0; // no match, start anew 567 | if (rxDisplay == patterns[i][0]) { 568 | currentIndex[i] = 1; 569 | } 570 | } 571 | } 572 | } 573 | if(checkFlag(CAN_allowAutoRefresh) && !patternFound && (last_millis_aux+6000 stop auto-updating the display 575 | DEBUG_PRINTLN("CAN Decode: Disabling display autorefresh..."); 576 | } else { 577 | if(patternFound && !checkFlag(CAN_allowAutoRefresh)){ 578 | setFlag(CAN_allowAutoRefresh); 579 | setFlag(DIS_forceUpdate); // gotta force a buffer update here anyway since the metadata might be outdated (wouldn't wanna reprint old audio metadata right?) 580 | DEBUG_PRINTLN("CAN Decode: Enabling display autorefresh..."); 581 | } 582 | patternFound=0; 583 | } 584 | vTaskDelay(pdMS_TO_TICKS(2)); 585 | } 586 | } 587 | 588 | // loads formatted UTF16 data into CAN_MsgArray and labels the messages; needs to know how many bytes to load into the array; afterwards this array is ready to be transmitted with sendMultiPacket() 589 | void prepareMultiPacket(int bytesProcessed, char* buffer_to_read){ // longer CAN messages are split into appropriately labeled (the so called PCI) packets, starting with 0x21 up to 0x2F, then rolling back to 0x20 590 | int packetCount=bytesProcessed/7, bytesToProcess=bytesProcessed%7; 591 | unsigned char frameIndex=0x20; 592 | for(int i=0; i0){ // if there are bytes left to be processed but are not enough for a complete message, process them now 603 | CAN_MsgArray[packetCount][0]=frameIndex; 604 | memcpy(&CAN_MsgArray[packetCount][1], &buffer_to_read[packetCount*7], bytesToProcess); 605 | packetCount++; 606 | } 607 | CAN_MsgArray[packetCount+1][0]=0x0; // remove the next frame label if there was any, as such it will not be transmitted 608 | } 609 | 610 | // function to queue a frame requesting measurement blocks 611 | void requestMeasurementBlocks(){ 612 | DEBUG_PRINT("CAN: Requesting measurements from "); 613 | if(checkFlag(ECC_present)){ // request measurement blocks from the climate control module 614 | DEBUG_PRINTLN("climate control..."); 615 | xQueueSend(canTxQueue, &Msg_MeasurementRequestECC, portMAX_DELAY); 616 | } else { 617 | DEBUG_PRINTLN("display..."); 618 | xQueueSend(canTxQueue, &Msg_MeasurementRequestDIS, portMAX_DELAY); // fallback if ECC is not present, then we read reduced data from the display 619 | } 620 | } 621 | 622 | // function to queue a frame requesting just the coolant data 623 | void requestCoolantTemperature(){ 624 | DEBUG_PRINT("CAN: Requesting coolant temperature from "); 625 | if(checkFlag(ECC_present)){ // request measurement blocks from the climate control module 626 | DEBUG_PRINTLN("climate control..."); 627 | xQueueSend(canTxQueue, &Msg_CoolantRequestECC, portMAX_DELAY); 628 | } else { 629 | DEBUG_PRINTLN("display..."); 630 | xQueueSend(canTxQueue, &Msg_CoolantRequestDIS, portMAX_DELAY); // fallback if ECC is not present, then we read reduced data from the display 631 | } 632 | } 633 | 634 | // below functions are used to add additional functionality to longpresses on the radio panel 635 | void canActionEhuButton0(bool btn_state, unsigned int btn_ms_held){ // do not use for CD30! it does not have a "0" button 636 | } 637 | 638 | // regular audio metadata mode 639 | void canActionEhuButton1(bool btn_state, unsigned int btn_ms_held){ 640 | if(disp_mode!=0 && btn_ms_held>=500){ 641 | disp_mode=0; // we have to check whether the music is playing, else the buffered song title just stays there 642 | setFlag(DIS_forceUpdate); 643 | } 644 | } 645 | 646 | // measurement mode type 1, printing speed+rpm, coolant and voltage from measurement blocks 647 | void canActionEhuButton2(bool btn_state, unsigned int btn_ms_held){ 648 | if(disp_mode!=1 && btn_ms_held>=500){ 649 | clearFlag(CAN_new_dataSet_recvd); 650 | disp_mode=1; 651 | setFlag(disp_mode_changed); 652 | DEBUG_PRINTLN("DISP_MODE: Switching to vehicle data..."); 653 | } 654 | } 655 | 656 | // measurement mode type 2, printing coolant from measurement blocks 657 | void canActionEhuButton3(bool btn_state, unsigned int btn_ms_held){ 658 | if(disp_mode!=2 && btn_ms_held>=500){ 659 | clearFlag(CAN_new_dataSet_recvd); 660 | disp_mode=2; 661 | setFlag(disp_mode_changed); 662 | DEBUG_PRINTLN("DISP_MODE: Switching to 1-line coolant..."); 663 | } 664 | } 665 | 666 | // no action 667 | void canActionEhuButton4(bool btn_state, unsigned int btn_ms_held){ 668 | } 669 | 670 | // no action 671 | void canActionEhuButton5(bool btn_state, unsigned int btn_ms_held){ 672 | } 673 | 674 | // no action 675 | void canActionEhuButton6(bool btn_state, unsigned int btn_ms_held){ 676 | } 677 | 678 | // no action 679 | void canActionEhuButton7(bool btn_state, unsigned int btn_ms_held){ 680 | } 681 | 682 | // Start OTA to allow updating over Wi-Fi 683 | void canActionEhuButton8(bool btn_state, unsigned int btn_ms_held){ 684 | if(!checkFlag(OTA_begin) && btn_ms_held>=1000){ 685 | setFlag(OTA_begin); 686 | } else { 687 | if(btn_ms_held>=5000) setFlag(OTA_abort); // allows to break out of OTA mode 688 | } 689 | } 690 | 691 | // holding the button for half a second disables EHU32 influencing the screen in any way, holding it for 5 whole seconds clears any saved settings and hard resets the ESP32 692 | void canActionEhuButton9(bool btn_state, unsigned int btn_ms_held){ 693 | if(disp_mode!=-1 && btn_ms_held>=500){ 694 | disp_mode=-1; 695 | DEBUG_PRINTLN("Screen updates disabled"); 696 | } 697 | if(btn_ms_held>=5000){ // this will perform a full reset, including clearing settings, so the next boot will take some time 698 | if(!checkFlag(OTA_begin)){ 699 | vTaskDelay(pdMS_TO_TICKS(1000)); 700 | prefs_clear(); 701 | vTaskDelay(pdMS_TO_TICKS(1000)); 702 | ESP.restart(); 703 | } 704 | } 705 | } --------------------------------------------------------------------------------