Automotive CAN Bus Hacking Tool (yesweCAN)

Hallo, ich starte noch ein neues Lernprojekt, diesmal zum Thema “CAN Bus”. Mein Ziel ist es einen Adapter zu bauen der über die OBD-Schnittstelle meines Autos Kontakt mit dem CAN-Bus (mein Fahrzeug hat 3 davon) aufnimmt und die Daten per Funk an einen PC senden kann um sie dort mittels Software auszuwerten.

Wie immer einen herzlichen Dank an das Forum, die Betreiber und Super-Nette Community hier dafür das ich mein Projekt hier ausbreiten darf. Den Sourcecode lege ich natürlich öffentlich zugänglich in meinem Github Repository ab GitHub - igittigitt/yesweCAN

Wie immer zunächst ein paar fachliche Anforderungen

  • OBD-Adapter
    • Als Basis-Chip soll ein ESP32 sein (mein aktueller Liebling, nur deswegen)
    • Der Adapter soll wenigstens über 2 CAN-Bus Schnittstellen verfügen, bzw. zwischen diesen umschalten können (HS-CAN, MS-CAN, ggf. MM-CAN)
    • Der Adapter soll im Fahrzeug verbleiben können, auch während der Fahrt, also über eigene Schutz- und Stromsparfunktionen verfügen (Unterspannungsschutz, Entladeschutz der Batterie, Load-Dump Transienten-Schutz beim Motorstart, Fremdstart, etc.)
    • Schreibzugriffe in den CAN-Bus müssen von einer externen Steuersoftware “genehmigt” werden, ansonsten nur passiver Sniffer-Mode
    • Der Adapter soll Daten auf eine SD-Card loggen können um Langzeitaufzeichnungen zu ermöglichen. Idealerweise eine Möglichkeit diese Daten auch per WLAN zu übertragen um nicht jedesmal die SD entnehmen zu müssen.
    • Übertragung der Daten per Funk an einen Remote-PC/Tablet zur Visualisierung und Auswertung in einem Standard-Format (z.B. GVRET oder Lawicel)
    • Eine OTA Update-Möglichkeit um nicht jedesmal den Adapter an einem PC anschließen zu müssen wenn ich meine Software optimieren möchte.
  • Software
    • Als Auswertungssoftware favorisiere ich “Savvycan”

Auswahl des ESP32

Über den ESP Product Selector habe ich geschaut welche ESP Varianten wenigstens einen, besser 2 CAN-Controller besitzen. CAN heißt übrigens im ESP-Terminologie “TWAI” (Two-Wire Automotive Interface). Da viel meine Wahl schnell auf den ESP32-C6, dieser hat laut Datenblatt “Two TWAI® controllers, compatible with ISO 11898-1 (CAN Specification 2.0)”. Der ist perfekt für meine Zwecke und ich habe bereits einige von diesen ESPs auf einem fertigen “Super-Mini” Board hier, die auch noch sehr günstig sind. Der C6 bietet auch noch genügend andere Schnittstellen wie SPI für eine SD-Card, hat WLAN und Bluetooth (BLE5) und auch etwas RAM (512 KB) für die Zwischenverarbeitung. Darüber hinaus bleiben noch ein paar Schnittstellen für eine Bordspannungsüberwachung per ADC und GPIOs für Taster/LEDs zur Steuerung am Adapter selbst.

Gehäuse

Zunächst baue ich mal einen Prototypen auf, dann schaue ich mal wie ich das mit dem Gehäuse regele. Ich habe noch ein paar ELM-Adapter Gehäuse, die haben aber das Problem das sie gerade eingesteckt werden und somit rausstehen. Ich denke da eher an eine geschlossene Box mit einem Kabel und einem OBD-Stecker, idealerweise einem gewinkelten.

Hier mal die wichtigsten Signale am OBD-Port (für mein Fahrzeug):

In meinem Fall ist Pin 16 DAUERPLUS, direkt mit der Batterie verbunden. Das kann in anderen Fahrzeugen anders geregelt sein, möglicherweise wirklich ein Zünd-Plus. Damit könnte man dann im Stand keine Frames mehr sniffen und bräuchte eine alternative Stromversorgung über einen LiPo-Akku damit es noch eine Weile klappt.

Stromversorgung

Die ist im KFZ über OBD üblicherweise 12V (naja, irgendwas um die 12V, also 6V bis 15V) und oftmals alles andere als sauber. Für den ESP32 brauche ich 3,3V bzw. 5V wenn ich den Onboard-LDO nutze. Für den Rest der Schaltung könnte ich ggf. noch 5V benötigen (für SD-Card, CAN-Transceiver, etc.). Im KFZ sollte zum einen alles möglichst wenig Strom verbrauchen, vor allem aber in einen Sleep gehen wenn es nicht arbeiten soll sonst ist die Batterie bald am Ende. Unter 6V steigen die meisten KFZ-Module mit Brown-Out eh aus, also benötige ich nur einen Buck-Converter, aber Automotive-Tauglich auf 5V, welcher auch einen Sleep-Mode hat, bzw. in einen solchen geschaltet werden kann, bzw. einen extrem geringen Strombedarf im Leerlauf (Quiescent Current).

Die Steuerung des Sleep muss im ESP erfolgen und der sollte auch die Peripherie an/ausschalten. Das ist ein bewährtes Power-Management-Pattern bei Automotive Modulen das die SBC die anderen Baugruppen steuert.

Für die Dimensionierung eines Spannungsreglers ist der Gesamtstromverbrauch entscheidend.

  • EPS32 => Im Aktiv-Modus ~ 60 mA, bei WiFi ~ 200 mA (Peak bis 400 mA)
  • CAN-Transceiver => Im Aktiv-Modus ~ 50 mA
  • SD-Card => ca. 50 mA (Peak bis 200 mA)
  • Hühnerfutter (LEDs) => 25 mA

Der Regler sollte also ~ 250 mA dauerhaft und 700 mA im Peak verkraften und dabei nicht all zu heiß werden (niedriger V-Drop). Da denke ich an einen 5V/1A Regler. Um sich gegen die Bordspannungsspitzen zu behaupten und eine gewisse EMV zu gewährleisten sollte man eine TVS-Diode, einen Eingangs LC-Filter haben. Auch ein Verpolschutz mittels Schottky-Diode ist angebracht.

Für so etwas sollte man eigentlich Automotive-Regler einsetzen wie aber dafür müsste man ein eigenes Board routen und dazu habe ich nicht die Lust. Daher musste ich auf etwas zurückgreifen was es fertig gibt und da bin ich bei diesem hier gelandet:

Für knapp 6€ ist das ein guter Regler für KFZ-Umgebungen, da er einen hohen Eingangsspannungsbereich (3,8V bis 32V) hat, genügend Strom liefern kann (bis zu 3,5 A bei 5V) bei gleichzeitig hohem Wirkungsgrad. Zudem hat er einen extrem niedrigen Ruhestrom von nur 22µA, erzeugt selbst wenig EMI, hat div. Schutzschaltungen integriert wie:
Unterspannungsabschaltung, Spitzenstrombegrenzung, Thermische Abschaltung und verfügt über einen Enable-Pin für eine echte Vollabschaltung.

Dazu dann von +12V kommend erst die Drossel (47µH, 1A) und nachgeschaltet in Sperrrichtung gegen GND eine TVS-Diode, welche knapp über der maximalen Bordspannung liegt, sowas wie eine “P6KE16CA” (16V). Danach dann eine SS14 oder 1N4007 in Flußrichtung als Verpolschutz (oder besser, einen Mosfet IRLML6344 in high-side Konfiguration, da dieser praktisch keinen Spannungsabfall erzeugt). Dahinter könnte man noch nen 10-22µF/25V Kondensator für die LC-Filterung setzen.

CAN-Bus Transceiver

Hier reden wir über sog. LOW-Speed Transceiver (< 1 MBit/s), also klassisches CAN, die sind in großer Vielfalt und billig zu bekommen. Häufige Vertreter sind der MCP2551, der TJA1050 und SN65HVD230.

Das Problem hier ist ein wenig der ESP32 mit seiner 3,3V Architektur. Ein CAN-Bus benötigt für saubere Pegel wenigstens 5V auf der CAN-Seite. Einige haben keine getrennte Spannungsversorgung für die IO-Logik wodurch sie dann dort TTL Pegel mit 5V erzeugen/erwarten, was für den ESP problematisch werden könnte. TX-Seitig vom ESP liegt der 3,3V Pegel noch in der Toleranz eines 5V CAN-Transceivers, aber RX-Seitig sollte man wohl besser einen Pegelwandler einsetzen um den ESP-Eingang nicht zu überlasten. Also entweder einen 5V CAN-Transceiver mit Pegelwandler, oder einen mit getrennter Vio und Vcan und doppelter Versorgung einsetzen.

Ich arbeite gern mit den NXP TJA-Transceivern, aber das spielt keine Rolle. Hier ein TJA1050 (5V !) auf einem Board für wenige Cent:

Der TJA1050 ist auf 5V ausgelegt. Leider besitzt er keine Wake-Up Funktion, D.H. man kann darüber den ESP nicht per GPIO aus dem Sleep holen. Der TJA1051 ist Pin-Kompatibel, legt den Transceiver aber schlafen wenn auf dem CAN nichts mehr los ist und kann bei CAN Aktivität ein Wake-Up Signal auf RX erzeugen. Den werde ich mal ausprobieren.

(!) ACHTUNG: Auf diesem Board sieht man ober rechts gut einen 120 Ohm Widerstand. Das ist der CAN Terminator. Den sollte man runterlöten wenn man damit ans Fahrzeug will, zumindest wenn man sich nicht an ein Gateway sondern direkt an den CAN ankoppelt. Diesen braucht man ggf. nur für eigene Projekte auf dem Labortisch. Macht vielleicht Sinn dafür einen Jumper einzubauen, der den bei Bedarf zuschaltet/trennt. Ich habe jetzt einfach den Widerstand runtergelötet.

Verbindung vom ESP32-C6 zum CAN-Transceiver

Es gibt zum glück keine festen Ports, man kann praktisch jeden GPIO mit dem/den CAN, äh “TWAI” Controllern verbinden. Ich nutze für die ersten Tests einfach mal GPIO5 zum senden (TX) und GPIO4 zum empfangen (RX). Wie immer gilt sich vor der Wahl eines GPIOs über deren sonstige Funktionen/Belegungen im Board zu informieren.

Die Software

Hier wird es wie immer interessant :slight_smile: Ich versuche mich mal Stück für Stück anzunähern…

Also fange ich mit einem Minimalbeispiel an. Dabei möchte ich einfach nur eine CAN-Botschaft empfangen (Sniffen) können. Die TWAI Funktionen erhält man mit folgendem Include:

#include <driver/twai.h>

Zunächst legt man fest welche Pins genutzt werden sollen und welchen CAN-Modus man wünscht (nur zuhören ohne ACK oder normal). Man kann es sich etwas einfacher machen indem man das Makro “TWAI_GENERAL_CONFIG_DEFAULT()” nutzt, welches andere, relevante Werte setzt und man sich auf das wesentliche konzentrieren kann. Ich mach das erstmal so:

twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(
    GPIO_NUM_5,             // TX GPIO
    GPIO_NUM_4,             // RX GPIO
    TWAI_MODE_LISTEN_ONLY   // Operation mode
);

Anschließend legt man noch die Timing-Parameter für den CAN Bus fest. Auch hier behelfe ich mir mit einem geeigneten Makro bei dem ich nur die Geschwindigkeit wählen muss:

twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS(); // 500 kbit/s

Als letztes noch den Filter festlegen. Da ich alles sehen will nehme ich einfach:

twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();

Nun installiert man den Treiber mit diesen Setups:

twai_driver_install(&g_config, &t_config, &f_config);

und ist startklar:

twai_start();

Danach werden eingehende Meldungen in die Receive-Buffer geschrieben die man wie Queues abfragen kann:


twai_message_t msg;
if (twai_receive(&msg, timeout) == ESP_OK) {
    // valid CAN frame having msg.identifier and msg.data_length_code in msg.data
} else {
    // something was wrong!
}

Der “timeout” ist klassisch in Ticks und “0” ergibt keine Blockade beim lesen.

Daraus habe ich mal dieses Testprogramm erstellt, welches einfach auf neue Botschaften pollt:

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/twai.h"
#include "esp_log.h"

#define TX_GPIO GPIO_NUM_5
#define RX_GPIO GPIO_NUM_4
static const char *TAG = "CAN_TEST";

void app_main(void)
{
    twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(TX_GPIO, RX_GPIO, TWAI_MODE_LISTEN_ONLY);
    twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS();
    twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();

    ESP_ERROR_CHECK(twai_driver_install(&g_config, &t_config, &f_config));
    ESP_ERROR_CHECK(twai_start());

    ESP_LOGI(TAG, "CAN Receiver started...");

    while (1) {
        twai_message_t msg;
        if (twai_receive(&msg, 0) == ESP_OK) {
            printf("ID: 0x%03lX  DLC: %d  DATA: ", msg.identifier, msg.data_length_code);
            for (int i = 0; i < msg.data_length_code; i++) {
                printf("%02X ", msg.data[i]);
            }
            printf("\n");
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

Als Test-Setup habe ich den Tranceiver an einen CAN-Bus angeschlossen auf den ich mittels eines anderen Tools jede Sekunde eine Botschaft sende. Das Ergebnis erscheint dann so:

I (228) CAN_TEST: CAN Receiver started...
ID: 0x123  DLC: 8  DATA: DE AD BE EF 01 02 03 04 
ID: 0x123  DLC: 8  DATA: DE AD BE EF 01 02 03 04
ID: 0x123  DLC: 8  DATA: DE AD BE EF 01 02 03 04 

PRIMA!

4 „Gefällt mir“

Frames per Funk an PC übertragen

Welches Funkprotokoll?

Mögliche Übertragungsprotokolle (auch im Hinblick auf späteres OTA) wären:

  • ESP-NOW => Proprietäres Format von Espressif auf Wifi-Basis. Ideal für kurze Nachrichten (bis 250 Bytes), kann aber nur mit anderen ESP wieder empfangen werden.
  • Kurze Reichweite, geringe Übertragungskapazität/Datendurchsatz.
  • BLE => Standard-Format, kurze Reichweite, geringer Datendurchsatz, OTA möglich aber kompliziert
  • WiFi => Standard-Format, Stabil, native OTA Unterstützung, große Reichweite, hoher Datendurchsatz, kann von Savvycan verwendet werden

Meine Wahl fiel auf WiFi. Mein Tool verbindet sich also mit meinem Haus-WLAN, erhält eine IP und kann dann an einem Websocket lauschen. Hier kann sich z.B. Savvycan hin verbinden und Daten austauschen. Für die Verbindung zu einer Smart-Device App (Handy/Tablet) wäre Bluetooth evtl. besser da sonst mein Tool auch AP spielen müsste. Evtl. mache ich mal beides mit Umschalter oder so, aber aktuell erstmal WiFi. Die Paketübertragung könnte nun UDP oder TCP sein.

Aus der Erfahrung im ersten Lernprojekt versuche ich gleich in Tasks und Queues zu denken. So sehe ich vor meinem geistigen Auge aktuelle folgende Aufteilung:

  1. Ein Task der sich um das Empfangen der CAN-Bus Nachrichten kümmert und diese in eine Queue schreibt
  2. Ein Task der diese Queue liest und die Daten im gewünschten Format per TCP oder UDP an den PC (Savvycan) sendet
  3. Ein Task der die Daten ebenfalls aus der Queue liest und in eine Datei auf die SD-Card schreibt
  4. Ein Task der sich später mal um das OTA kümmert (HTTP OTA Client)

Welches Datenformat?

Aus früheren Projekten kenne ich das SL-CAN Format, das habe ich schon häufig benutzt. Savvycan soll wohl besonders effektiv mit dem GVRET Format umgehen können, auch das ist sehr einfach konzipiert. Das gibt es im Binär oder im ASCII-Format. Letzteres würde ich erstmal nehmen:

t<TIMESTAMP> <CAN-ID> <MSGLEN> [<HEX_BYTE> ...]
t1783231293 780 8 11 22 33 44 55 66 77 88
2 „Gefällt mir“

Die CAN Sende/Empfangsroutinen

Als erstes definiere ich einen internen Datentyp für meine CAN Nachrichten:

typedef struct {
    uint32_t id;
    uint8_t dlc;
    uint8_t data[8];
    bool extended;
} can_frame_t;

Dann die internen RX/TX-Queues für die Inter-Task Kommunikation:

#define RX_QUEUE_SIZE 100
#define TX_QUEUE_SIZE 20

QueueHandle_t can_rx_queue;
QueueHandle_t can_tx_queue;

void app_main(void) {
    ...
    can_rx_queue = xQueueCreate(RX_QUEUE_SIZE, sizeof(can_frame_t));
    can_tx_queue = xQueueCreate(TX_QUEUE_SIZE, sizeof(can_frame_t));
    ...
}

In die so erzeugten Queues können dann Nachrichten hinein geschoben werden (FIFO), solange Platz ist. Das geschieht im RAM, die QUEUE_SIZEs müssen also mit Bedacht gewählt werden, gerade so groß das auch bei hoher CAN-Aktivität kein Frame verloren geht bevor er auf der SD-Card gespeichert oder vom Savvycan abgerufen wurde. Die 100/20 sind erstmal ein Pi*Daumen Wert den es später anzupassen gilt.

Nun implementiere ich die Empfangs und Sende-Routinen/Tasks. Diese sind recht simpel aufgebaut:

/**
 * Task receivces CAN frames from bus into queue
 */
void can_rx_task(void *arg)
{
    twai_message_t msg;

    while (1) {
        if (twai_receive(&msg, pdMS_TO_TICKS(100)) == ESP_OK) {
            can_frame_t frame = {
                .id = msg.identifier,
                .dlc = msg.data_length_code,
                .extended = msg.extd
            };
            memcpy(frame.data, msg.data, msg.data_length_code);
            xQueueSend(can_rx_queue, &frame, 0);
        }
    }
}

/**
 * Task to send CAN frames from queue to bus
 */
void can_tx_task(void *arg)
{
    can_frame_t frame;

    while (1) {
        if (xQueueReceive(can_tx_queue, &frame, portMAX_DELAY)) {
            twai_message_t msg = {
                .identifier = frame.id,
                .data_length_code = frame.dlc,
                .extd = frame.extended,
                .rtr = 0
            };
            memcpy(msg.data, frame.data, frame.dlc);
            twai_transmit(&msg, pdMS_TO_TICKS(50));
        }
    }
}

Die Tasks werden wie üblich über die main gestartet und laufen dann endlos (Daemon) im Hintergrund:

void app_main(void) {
    ...
    xTaskCreate(can_rx_task, "CAN-Bus Receiver", 2048, NULL, 1, NULL);
    xTaskCreate(can_tx_task, "CAN-Bus Transmitter", 2048, NULL, 1, NULL);
    ...
}

Sobald es was zu tun gibt speichern sie lediglich die Daten von einer Queue in die andere. Die Tasks laufen mit Prio 1 weil sie zeitkritisch sind.

SavvyCAN Tasks

Die Tasks zur Kommunikation mit dem SavvyCAN können dann so aussehen:

/**
 * Send a CAN frame from the RX_QUEUE via network (WiFi) to the connected client (e.g. SavvyCAN)
 */
void tcp_tx_task(void *arg)
{
    int sock = (int)arg;
    can_frame_t frame;

    while (1) {
        if (xQueueReceive(can_rx_queue, &frame, portMAX_DELAY)) {
            gvret_send_frame(sock, &frame);
        }
    }
}

/**
 * Send a CAN frame received from network (e.g. SavvyCAN) into the TX_QUEUE for CAN Bus transmition
 */
void tcp_rx_task(void *arg)
{
    int sock = (int)arg;
    char line[128];

    while (1) {
        int len = recv(sock, line, sizeof(line) - 1, 0);
        if (len <= 0) break;

        line[len] = 0;

        can_frame_t frame;
        if (parse_gvret_line(line, &frame)) {
            xQueueSend(can_tx_queue, &frame, 0);
        }
    }
}

Die beiden Subs zum kodieren/dekodieren des internen CAN Frame formates can_frame_t in eine GVRET Notation, inkl. Übertragung vom/zum Ziel habe ich mal bewusst ausgelagert, denn vielleicht möchte ich später auch mal andere Formate/Ziele unterstützen, was aber nichts an der Logik ändert:

/**
 * Convert a CAN message from RX_QUEUE into a GVRET formatted one and send it via network to the client
 */
void gvret_send_frame(int sock, const can_frame_t *f)
{
    char buf[128];
    int len = snprintf(
        buf, sizeof(buf),
        "t%u %X %u %02X %02X %02X %02X %02X %02X %02X %02X\n",
        esp_log_timestamp(),
        f->id,
        f->dlc,
        f->data[0], f->data[1], f->data[2], f->data[3],
        f->data[4], f->data[5], f->data[6], f->data[7]
    );
    send(sock, buf, len, 0);
}

/**
 * Convert a GVRET encoded CAN message into internal format
 */
bool parse_gvret_line(char *line, can_frame_t *out)
{
    if (line[0] != 't') return false;

    uint32_t ts;
    int consumed = 0;

    if (sscanf(line, "t%u %x %hhu%n",
               &ts, &out->id, &out->dlc, &consumed) < 3)
        return false;

    char *p = line + consumed;
    for (int i = 0; i < out->dlc; i++) {
        unsigned int b;
        if (sscanf(p, "%x", &b) != 1) return false;
        out->data[i] = b;
        p = strchr(p, ' ');
        if (!p) break;
        p++;
    }

    out->extended = (out->id > 0x7FF);
    return true;
}

Connect to my WiFi

Was ich hier erstmal noch unterschlagen habe ist das Thema WiFi. Grundsätzlich wählt man zwischen AP-Mode (Access Point, der ESP ist also der der das WLAN anbietet) oder STA-Mode (Station, der ESP verbindet sich mit einem existierenden WLAN). Ich möchte STA.

Der grundsätzliche Ablauf einer WiFi-Verbindung ist hier in diesem Ablaufdiagramm der Espressif-Docs ganz gut beschrieben:

Die Verbindungsparameter (SSID/Password) muss man natürlich auch irgendwo hinterlegen, das machen ich mit defines:

#define WIFI_SSID "MeinWLAN"
#define WIFI_PASS "MeinPasswort"

Um etwas auf einen Socket zu senden oder von diesem zu empfangen benötigt man natürlich zunächst eine Netzwerkverbindung zum WLAN. Dann muss sich der Code eine IP-Adresse per DHCP besorgen und verschiedene WiFi-Parameter im Flash (NVS) ablegen.

Das ist nicht so ganz trivial, weil es ja einen Handshake gibt, ggf. Retries, etc. Also im Kern ruft der Code eine Funktion auf die erst dann zurückkehrt wenn die Wifi Verbindung steht, nennen wir die Funktion “wifi_init_sta()”.

Darin initialisiert man zunächst den Flash-Treiber mit “nvs_flash_init()”. Danach erstellt man ein Netzwerk-Interface (IP-Stack) mit “esp_netif_init()”. Das darauffolgende “esp_netif_create_default_wifi_sta()” erzeuge eine WiFi Konfiguration für den STA-Mode (also WLAN-Client) und weißt diesem das zuvor erstellte Netzwerk-Interface und die Event-Loop zu:

nvs_flash_init();
esp_netif_init();
esp_event_loop_create_default();
esp_netif_create_default_wifi_sta();
set_hostname();

Anschließend setze ich noch den Gerätenamen (“YESWECAN”) mit dem sich das Device im WLAN einbucht. Andernfalls würde es “espressif” heißen (Default). Dann folgt noch weiteres “Zeug” um die Zugangsdaten zu übergeben, letztlich die Verbindung herzustellen. Das habe ich so aus dem WiFi-Demo kopiert:

void wifi_init_sta(void)
{
    ESP_LOGI(TAG, "Connecting to WiFi network %d ...", WIFI_SSID);

    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    wifi_event_group = xEventGroupCreate();
    esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();

    ESP_ERROR_CHECK(esp_netif_set_hostname(sta_netif, HOSTNAME));

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, NULL));

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = WIFI_SSID,
            .password = WIFI_PASS,
            .threshold.authmode = WIFI_AUTH_WPA2_PSK,
        },
    };

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());

    ESP_LOGI(TAG, "wifi_init_sta finished.");
}

static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    }
    else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        ESP_LOGI(TAG, "WIFI disconnected – retrying...");
        esp_wifi_connect();
        xEventGroupClearBits(wifi_event_group, WIFI_CONNECTED_BIT);
    }
    else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data;
        ESP_LOGI(TAG, "WIFI got IP: " IPSTR, IP2STR(&event->ip_info.ip));
        xEventGroupSetBits(wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

void wait_for_wifi(void)
{
    xEventGroupWaitBits(wifi_event_group, WIFI_CONNECTED_BIT,
                        pdFALSE, pdTRUE, portMAX_DELAY);
}

Die WIFI-Funktionen selbst sind bereits Bestandteil des ESP-IDF 5.x und müssen nicht extra hinzugefügt werden, hier reicht das einbinden der Header:

#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "nvs_flash.h"

Wir müssen aber die entsprechenden Requirements in die main/CMakeLists.txt einfügen:

idf_component_register(SRCS "app.c"
                    REQUIRES driver esp_wifi esp_netif nvs_flash esp_event
                    INCLUDE_DIRS ".")

Der Wifi-Connect hat bei mir in der Tat auf Anhieb funktioniert :slight_smile:

image

“Zu ihren Diensten!”

Weil wir als ESP32-Tool im Sinne eines Netzwerkes ein Server sind (wir bieten einen Datendienst an), müssen wir einen solchen natürlich auch implementieren. Dieser lauscht am erzeugten IP-Interface an einem bestimmten Port (ich habe Port 23 verwendet) auf eine eingehende Verbindung. Das Vorgehen ist wie unter Linux, man erzeugt einen Socket, parametriert ihn, bindet ihn an das Interface und stellt auf LISTEN():

#define GVRET_PORT 23

int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
struct sockaddr_in addr = {
    .sin_family = AF_INET,
    .sin_port = htons(GVRET_PORT),
    .sin_addr.s_addr = htonl(INADDR_ANY)
};
bind(listen_sock, (struct sockaddr *)&addr, sizeof(addr));
listen(listen_sock, 1);
ESP_LOGI("SOCKET", "Listening on port %d", GVRET_PORT);

Sobald ein Connect vorliegt wird der blockierende listen() verlassen, man akzeptiert die Verbindungsanforderung und liest mit recv() die Daten die von dort kommen. Damit ruft man dann einen Funktionshandler für die Daten auf. Sobald die Gegenstelle “auflegt” (recv() liefert -1) wird die Schleife verlassen und man schließt den Socket. Das sieht dann z.B. so aus:

    while (1) {
        client_sock = accept(listen_sock, NULL, NULL);
        ESP_LOGI("SOCKET", "Client connected");

        while (1) {
            uint8_t buf[128];
            int len = recv(client_sock, buf, sizeof(buf), 0);
            if (len <= 0) break;
            gvret_handle_command(buf, len);  // GVRET command handler in code
        }

        close(client_sock);
        client_sock = -1;
        ESP_LOGI("SOCKET", "Client disconnected");
    }

    close(client_sock);
    client_sock = -1;
    ESP_LOGI("SOCKET", "Client disconnected");

Verarbeitung von GVRET Befehlen

GVRET ist immer ASCII. Jeder Befehl wird mit einem Newline (\n) abgeschlossen und ist maximal 64 Bytes lang. Ungültige Befehle werden ignoriert oder mit einem “NACK” beantwortet.

Die Basis-Befehle sind:

  • t<ID><DLC><DATA> => Transmit Standard CAN Frame
  • T<ID><DLC><DATA> => Transmit Extended CAN Frame
  • sXX => Set CAN speed
  • v => Print version
  • V => Print info

Für “v” kann man einfach sowas wie “vESP32 GVRET 1.0” senden und “V” etwa “VESP32-CAN-LOGGER”.

Im Kern bauen wir also einen Parser der die Befehle von SavvyCAN liest und ausführt. Bei GVRET ist jeder Befehl nur ein Zeichen lang und steht im ersten Byte. Die Verarbeitung ist also letztlich eine große Switch/Case Anweisung:

void gvret_handle_command(uint8_t *buf, int len)
{
    // GVRET sendet ASCII-Befehle: t1238112233...
    // buf ist NICHT null-terminiert, daher temporär kopieren
    char line[64];
    int copy_len = len < sizeof(line)-1 ? len : sizeof(line)-1;
    memcpy(line, buf, copy_len);
    line[copy_len] = 0;

    switch (line[0])
    {
        case 'v':
            socket_send("vESP32 GVRET 1.0\n");
            break;
        case 'V':
            socket_send("VESP32-CAN-LOGGER\n");
            break;

        case 't':
           break;

        case 'T':
           break;

        case 's':
            break;

        default:
           break;
    }
}

Man kann sich mit einem einfachen Telnet-Client (z.B. PuTTY) an die IP und Port 23 connecten. Der einfache “v” und “V” funktionieren bereits.

Jetzt wäre mal ein spannendes Zwischenergebnis CAN-Daten empfangen oder mit “t” Daten senden zu können!

Eingehende CAN-Daten mit GVRET

Sobald über den CAN-Bus gültige Daten in den TWAI Controller geflossen sind, reagiert unser “can_rx_task()” der im Hintergrund läuft. Dieser stopft den Frame einfach im can_frame_t Format in die Queue “can_rx_queue”.

Der Task “tcp_tx_task"()” empfängt diese Nachricht der Queue und ruft seinerseits “gvret_send_frame()” auf, welches die Daten in ein GVRET Format bringt und mit “send()” in den Socket schickt.

Nach etwas Gefrickel mit dem Socket bekomme ich tatsächlich CAN-Frames angezeigt:

yesweCAN v0.1ß
t66580 123 8 DE AD BE EF 01 02 03 04
t67200 123 8 DE AD BE EF 01 02 03 04
t68910 123 8 DE AD BE EF 01 02 03 04
...

Das Problem mit dem Socket ist, das der Server-Task zunächst einen Listener-Socket auf das Interface bindet das nur dazu dient eingehende Verbindungen anzunehmen:

void socket_server_task(void *arg)
{
    int listen_sock;
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;
    socklen_t addr_len = sizeof(client_addr);

    listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
    if (listen_sock < 0) {
        vTaskDelete(NULL);
        return;
    }

    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(GVRET_PORT);

    bind(listen_sock, (struct sockaddr *)&server_addr, sizeof(server_addr));

    listen(listen_sock, 1);

    while (1) {
        ESP_LOGI("SOCKET", "Server ready for connection...");

        // Blocks until client connect
        client_sock = accept(listen_sock,(struct sockaddr *)&client_addr, &addr_len);
    ...

Durch die Backlog-Zahl 1 im “listen(listen_sock, 1)” wird auch keine weitere Verbindung mehr akzeptiert. Alsdann der listen_sock erstellt ist wartet accept() bis sich ein Client verbunden hat. Ist das geschehen liefert er einen client-socket zurück, das ist die Verbindung in die man als Client reinschreibt oder aus der man Daten erhält, der Listener-Socket bleibt einfach offen und sobald sich der Client disconnected wird dieser mit “close(client_sock)” geschlossen.

Worauf ich jetzt noch achten muss ist das nicht zwei Prozesse gleichzeitig auf den Client-Socket zugreifen (Race-Condition) bzw. das das Senden aufgrund von TCP gestückelt sein könnte. Sprich ein “send(client_sock, …)” liefert nicht alle Bytes auf einmal aus und muss daher öfter aufgerufen werden. Bei maximal 64 Byte pro Frame eher unwahrscheinlich, aber trotzdem…

Komfortable Eingabe von Variablen und sicheres WLAN-Passwort

Weil es mich interessiert hat wie man mit menuconfig auch eigene Parameter abfragen kann. In der main/Kconfig.projbuild kann man eigene Parameter hinterlegen, das habe ich so gemacht:

menu "yesweCAN Configuration"

    config WIFI_SSID
        string "WiFi Network to connect to (SSID)"
        default "SSID"
        help
            This is your WIFI network name (SSID) where to connect to

    config WIFI_PASS
        string "Password for WiFi Network"
        default "PASS"
        help
            This is your WIFI network password

    config CAN_TX_GPIO_NUM
        int "TX GPIO number"
        default 5 if IDF_TARGET_ESP32
        default 0
        help
            This option selects the GPIO pin used for the TX signal. Connect the TX signal to your transceiver.

    config CAN_RX_GPIO_NUM
        int "RX GPIO number"
        default 4 if IDF_TARGET_ESP32
        default 2
        help
            This option selects the GPIO pin used for the RX signal. Connect the RX signal to your transceiver.

endmenu

Anschließend muss man den build-Ordner löschen (Mülltonnen-Symbol in der GUI) und menuconfig (Zanhrad-Symbol) aufrufen, dann sieht man die neuen Optionen und kann bequem die Werte erfassen ohne was am Code ändern zu müssen:

Sobald man nach der Änderung “Save” klickt, erzeugt menuconfig im Projektverzeichnis eine sdkconfig” Datei. Diese enthält alle Default-Werte und Projekteinstellungen, sowie die selbst erstellten Parameter von oben:

Diese kann man dann in der .h oder .c den eigentlichen defines zuordnen:

#define TX_GPIO     CONFIG_CAN_TX_GPIO_NUM
#define RX_GPIO     CONFIG_CAN_RX_GPIO_NUM
#define WIFI_SSID   CONFIG_WIFI_SSID
#define WIFI_PASS   CONFIG_WIFI_PASS

Das ganze hat noch einen weiteren Vorteil. Per default (aber das sollte man kontrollieren) wird von VS Code die sdkconfig und sdkconfig.old (Backup) in die .gitignore eingetragen:

image

Das verhindert das diese Dateien ins Repository gelangen und somit öffentlich werden. Die sdkconfig muss sich ohnehin jeder der den Code compiliert neu erzeugen (lassen). Man sollte auch darauf achten das die .gitignore selbst ein Bestandteil des Repos ist (git add .gitignore).

WiFi, aber richtig!

Ich hatte die Komplexität einer WiFi Verbindung wohl etwas unterschätzt. Nachdem ich mich tiefer mit dem Thema beschäftigt hatte habe ich einen wirklich guten Stack gefunden und für meine Bedürfnisse angepasst, welcher WiFi nach allen Regeln der Kunst, wie im Espressif-Doc Wi-Fi Driver - ESP32 - — ESP-IDF Programming Guide v5.5.2 documentation beschrieben implementiert.

Weil ich das WiFi-Zeugs noch an anderen Stellen sicher gut gebrauchen kann habe ich es als Custom-Component gebaut, die kann ich dann überall wiederverwenden. Die Component liefert auch eigene Menuconfig Einstellungen über eine “Kconfig” Datei mit um sie komfortabel zu konfigurieren.

Das hier alles in der Tiefe zu erklären würde den Rahmen sprengen, daher will ich nur kurz die Basis-Konzepte dazu vorstellen. Die Espressif-API für WiFi verwendet einen eigenen Task mit einer Statemachine (welche man mit Aufruf von esp_event_loop_create_default() im Main-Code erzeugt) und ruft daraus dann, je nach WiFi-Zustand verschiedene Callback-Routinen auf. Diese müssen natürlich vorher registriert werden. Stark reduziert sieht das dann so aus:

esp_err_t wifi_sta_init(EventGroupHandle_t event_group)
{

    esp_ret = esp_event_handler_register(WIFI_EVENT,
                                        WIFI_EVENT_STA_START,
                                        &on_wifi_event,
                                        NULL);

    esp_ret = esp_event_handler_register(WIFI_EVENT,
                                        WIFI_EVENT_STA_STOP,
                                        &on_wifi_event,
                                        NULL);

    esp_ret = esp_event_handler_register(WIFI_EVENT,
                                        WIFI_EVENT_STA_CONNECTED,
                                        &on_wifi_event,
                                        NULL);

    esp_ret = esp_event_handler_register(WIFI_EVENT,
                                        WIFI_EVENT_STA_DISCONNECTED,
                                        &on_wifi_event,
                                        NULL);

Damit werden die Event-Handler für die WiFi-Events: START, STOP, CONNECTED, DISCONNECTED registriert. Die Event-Handler-Routine welche die dann verarbeitet schaut dann (ebenfalls stark reduziert) so aus:

static void on_wifi_event(void *arg,
                          esp_event_base_t event_base,
                          int32_t event_id,
                          void *event_data)
{
    switch (event_id)
    {
        case WIFI_EVENT_STA_START:

        case WIFI_EVENT_STA_STOP:

        case WIFI_EVENT_STA_CONNECTED:

        case WIFI_EVENT_STA_DISCONNECTED:

Im Main-Code nutzt man dann eigentlich nur drei API Funktionen:

esp_err_t wifi_sta_init(EventGroupHandle_t event_group);
esp_err_t wifi_sta_stop(void);
esp_err_t wifi_sta_reconnect(void);

Sobald man den Init aufgerufen hat, versucht der ESP eine WiFi Verbindung zum hinterlegten SSID aufzubauen. Mit Stop hält man das ganze an und Reconnect würde eine verlorene Verbindung wiederaufbauen, was man aber auch in den Menuconfig Settings einrichten kann das es automatisch geschieht.

Nach der WiFi-Verbindung versucht der Code per DHCP eine Adresse zu bekommen und weist diese dann einem Netzwerk-Interface zu, welches man in der Folge dann für die eigentliche Kommunikation auf TCP/IP Ebene nutzen kann.

Den Zustand der WiFi-Verbindung erkennt man über EventGroup Bits, ebenfalls ein neues Konzept welches ich gelernt habe. Diese Bits kann man abfragen und setzen, natürlich muss man zunächst eine EventGroup definieren und erstellen.

Das schaut im Main dann so aus:

    // Initialize event group for WiFi connection state
    network_event_group = xEventGroupCreate();

    // Initialize NVS: ESP32 WiFi driver uses NVS to store WiFi settings
    // Erase NVS partition if it's out of free space or new version
    esp_ret = nvs_flash_init();
    if ((esp_ret == ESP_ERR_NVS_NO_FREE_PAGES) ||
        (esp_ret == ESP_ERR_NVS_NEW_VERSION_FOUND)) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        esp_ret = nvs_flash_init();
    }
    if (esp_ret != ESP_OK) {
        ESP_LOGE(TAG, "Error (%d): Failed to initialize NVS", esp_ret);
        abort();
    }

    // Initialize TCP/IP network interface (only)
    // Must be called prior to initializing the network driver!
    esp_ret = esp_netif_init();
    if (esp_ret != ESP_OK) {
        ESP_LOGE(TAG, "Error (%d): Failed to initialize network interface", esp_ret);
        abort();
    }

    // Create default event loop that runs in the background
    // Must be called prior to initializing the network driver!
    esp_ret = esp_event_loop_create_default();
        if (esp_ret != ESP_OK) {
        ESP_LOGE(TAG, "Error (%d): Failed to create default event loop", esp_ret);
        abort();
    }

    // Initialize network connection
    esp_ret = wifi_sta_init(network_event_group);
    if (esp_ret != ESP_OK) {
        ESP_LOGE(TAG, "Error (%d): Failed to initialize WiFi", esp_ret);
        abort();
    }

    // Wait for network to connect
    ESP_LOGI(TAG, "Waiting for network to connect...");
    network_event_bits = xEventGroupWaitBits(network_event_group,
                                            WIFI_STA_CONNECTED_BIT,
                                            pdFALSE,
                                            pdTRUE,
                                            pdMS_TO_TICKS(connection_timeout_ms));
    if (network_event_bits & WIFI_STA_CONNECTED_BIT) {
        ESP_LOGI(TAG, "Connected to WiFi network");
    } else {
        ESP_LOGE(TAG, "Failed to connect to network");
        abort();
    }

    // Wait for IP address
    ESP_LOGI(TAG, "Waiting for IP address...");
    network_event_bits = xEventGroupWaitBits(network_event_group,
                                            WIFI_STA_IPV4_OBTAINED_BIT | WIFI_STA_IPV6_OBTAINED_BIT,
                                            pdFALSE,
                                            pdFALSE,
                                            pdMS_TO_TICKS(connection_timeout_ms));
    if (network_event_bits & WIFI_STA_IPV4_OBTAINED_BIT) {
        ESP_LOGI(TAG, "Obtained IPv4 address");
    } else if (network_event_bits & WIFI_STA_IPV6_OBTAINED_BIT) {
        ESP_LOGI(TAG, "Obtained IPv6 address");
    } else {
        ESP_LOGE(TAG, "Failed to obtain IP address");
    }

    // Test-Loop
    while (1) {

        // Make sure we are still connected
        if (network_event_bits & WIFI_STA_CONNECTED_BIT) {
            ESP_LOGI(TAG, "Still connected to WiFi network");
        } else {
            ESP_LOGE(TAG, "Lost connection to the network");
            abort();
        }

        // Delay
        vTaskDelay(sleep_time_ms / portTICK_PERIOD_MS);
    }

Ich habe noch keine Idee wie ich eine eigene Custom-Component bereitstelle, auch stammt diese leider nicht aus meiner Feder, dafür bin ich noch zu Rookie. Daher teile ich hier mal den Link zu dem Genius der das erstellt hat:

Passend dazu gibt es auch noch ein gutes Video

Das hat schon alles Niveau und unterscheidet sich deutlich von dem sonstigen YT-Unterhaltungs-Quatsch, ist aber auch entsprechend anspruchsvoll. Der Code macht aber in allen Belangen für mich Sinn und ich habe viel davon gelernt!

SavvyCAN GVRET

Anders als zunächst ermittelt arbeitet GVRET nicht mit einem ASCII-Basierenden Protokoll sondern einem binären. Leider konnte ich keine wirkliche Protokoll-Definition finden also habe ich angefangen es aus dem Sourcecode von SavvyCAN zu extrahieren. Dabei lernt man auch ne Menge :slight_smile:

Also sobald mit in SavvyCAN einen Connect (GVRET Socket) macht, sendet diese Bytefolge:

0xE7 0xE7 0xF1 0x0C 0xF1 0x06 0xF1 0x07 0xF1 0x01 0xF1 0x09

Immer wenn Savvy zum Device kommuniziert sendet es einen Command-Identifier (1. Byte) gefolgt von einem Command (2. Byte). Das sind alles einzelne Kommandos die ich nachfolgend erklären möchte:

0xE7 0xE

Das soll den Empfänger (ESP32) in den Binären Protokollmodus versetzen. Eine Antwort erwartet Savvy darauf scheinbar nicht?

Savvy sendet als nächstes

0xF1 0x0C

und erwartet das das Device ihm die Anzahl der CAN-Busse (1-3) zurückliefert die das Gerät besitzt. In meinem Fall erstmal nur 1. Die Antworten auf solche Requests bestehen aus der Anfrage selbst und dem Ergebnis, also ähnlich wie UDS das auf CAN Bussen macht. Also sende ich 0xF1 0x0C 0x01 zurück.

0xF1 0x06

Hiermit fragt Savvy die CAN Bus parameter an. Das muss ich mir noch erarbeiten.

0xF1 0x07

Da soll die Device Information geliefert werden, also die Versionsnummer vom Tool. Ich sende einfach 0xF1 0x07 0x01 0x02 0x20 0x00 0x00 0x00.

0xF1 0x01

Hiermit will Savvy die Uhrzeit vom Tool lesen um seine Zeitanzeige damit zu synchronisieren. Die Antwort ist ein Unix-Timestamp (now).

0xF1 0x09

Das ist ein Keepalive Kommando. Dieses muss innerhalb einer Sekunde mit 0xF1 0x09 0xDE 0xAD beantwortet werden, ansonsten bricht Savvy die Verbindung zum Tool ab.

Mit diesen Minimal-Antworten erhalte ich zumindest schon eine stabile Tool-Verbindung:

Nun untersuche ich mal wie ich einen CAN-Frame übermitteln muss. Dieser wird von Savvy nicht requested, den sendet man einfach.

UPDATE

Ich habs rausgefunden, ist etwas tricky aber geht. Muss noch das mit der Timebase klären, aber meine CAN-Pakete vom Test-Sender kommen an:

Jetzt juckt es mich schon in den Fingern das Ding mal ins Auto zu hängen :wink: Aber erstmal will ich noch das OTA bauen damit ich nicht immer raus in die Garage laufen muss um eine neue Firmware zu flashen… bin halt faul.

OTA - Nie wieder Kabel!

OTA (Over-the-Air Update) ermöglicht es die Firmware eines ESP32 über eine WLAN-Verbindung zu aktualisieren. Dabei gibt es zwei Modus, einen sicheren und einen unsicheren. Beim sicheren Modus wird der Flash so partitioniert das neben der laufenden Firmware nochmal genausoviel Platz für die heruntergeladene Firmware ist. Beim Boot wird dann einfach umgeschaltet. So hat man sogar ein Backup.

Um den Safe-Mode zu implementieren benötigt man im Flash zwei Partitionen: "“ota_0” und “ota_1”. Um die Partitionen im Flash habe ich mich bislang nicht gekümmert, das hat alles der Default von ESP-IDF für mich erledigt.

Über menuconfig wählt man “Partition Table” aus und kann gleich im ersten Parameter den Typ der Partitionstabelle wählen:

Praktischerweise ist hier gleich eine für OTA enthalten, ich brauche mich also nicht mit der partitions.csv rumplagen. Ich habe “Factory app, two OTA defitions” gewählt.

Ein Problem ist dabei das die Firmware ja irgendwo her kommen muss? Bei gekauften Devices ist das eine Website des Herstellers im Internet. Für meine Entwicklungen will ich aber keine öffentlich verfügbare URL auf einem Webserver dafür bereitstellen müssen, noch den compilierten Code jedesmal hochladen.

Die Antwort auf die Frage ist so einfach, wie erwartbar: Man braucht einen lokalen Webserver im LAN (erreichbar übers WLAN). Dies können viele IDEs wie Arduino-IDE aber auch Visual Studio Code, man kann es auch völlig ohne IDE direkt auf der CLI machen indem man z.B. python -m http.server 8000 in einem Terminal von VS Code einen lokalen Webserver startet. Dieser bietet einfach alle Dateien im Start-Ordner zum Download an (Index-Mode). Ich wechsle daher einfach in den build-Ordner, da liegt ja unsere *.bin.

Über einen Browser bekomme ich nun mit http://<MEINE_IP_ODER_HOSTNAME>:8000/ den Inhalt des build-Ordners aufgelistet. Und somit wäre unsere OTA-URL dann einfach:

http://<MEINE_IP_ODER_HOSTNAME>:8000/yesweCAN.bin

Zuerst muss die Component “esp_http_client” (für grundsätzliche Zugriffe über HTTP(S)) sowie “esp_https_ota” in main/CMakeLists.txt hinzugefügt werden:

idf_component_register(
    SRCS "app.c"
    REQUIRES
        driver
        nvs_flash
        esp_wifi esp_netif esp_event wifi_sta
        esp_https_ota esp_http_client
    INCLUDE_DIRS "."
)

# Add certificate (only for HTTPS connections)
# target_add_binary_data(${COMPONENT_LIB} "../certs/server_cert.pem" TEXT)

Dann wie immer die Header includen:

#include "esp_https_ota.h"

einen Task bauen der das OTA durchführt:

void ota_task(void *pvParameter)
{
    ESP_LOGI(TAG, "Start OTA task");

    esp_http_client_config_t http_cfg = {
        .url = OTA_URL,
        //.cert_pem = server_cert_pem_start,
        .timeout_ms = 10000,
    };

    esp_https_ota_config_t ota_cfg = {
        .http_config = &http_cfg,
    };

    esp_err_t ret = esp_https_ota(&ota_cfg);
    if (ret == ESP_OK) {
        ESP_LOGI(TAG, "OTA successfull, rebooting into new firmware...");
        esp_restart();
    } else {
        ESP_LOGE(TAG, "OTA failed: %s", esp_err_to_name(ret));
    }

    vTaskDelete(NULL);
}

und im Main-Code den Task starten sobald die WiFi Verbindung aufgebaut wurde:

xTaskCreate(&ota_task, "ota_task", 8192, NULL, 5, NULL);

Wichtig in dem Zusammenhang ist noch das man für den Zugriff auf eine nicht HTTPS URL in menuconfig die entsprechende Option setzen muss:

Damit das dann auch klappt muss man natürlich auf seinem PC die lokale Firewall dafür freischalten. Dann sieht das ganze mal so aus:

I (2128) yesweCAN: Start OTA task
W (2128) esp_https_ota: Continuing with insecure option because CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP is set.
W (2148) wifi:<ba-add>idx:1, ifx:0, tid:0, TAHI:0x100dfa7, TALO:0x7c12373c, (ssn:0, win:64, cur_ssn:0), CONF:0xc0000005
I (2168) esp_https_ota: Starting OTA...
I (2168) esp_https_ota: Writing to <ota_0> partition at offset 0x20000
I (16788) esp_image: segment 0: paddr=00020020 vaddr=420c0020 size=25044h (151620) map
I (16808) esp_image: segment 1: paddr=0004506c vaddr=40800000 size=0afach ( 44972)
I (16808) esp_image: segment 2: paddr=00050020 vaddr=42000020 size=be8d8h (780504) map
I (16908) esp_image: segment 3: paddr=0010e900 vaddr=4080afac size=0fbb0h ( 64432) 
I (16918) esp_image: segment 4: paddr=0011e4b8 vaddr=4081ab60 size=03780h ( 14208)
I (16918) esp_image: segment 0: paddr=00020020 vaddr=420c0020 size=25044h (151620) map
I (16938) esp_image: segment 1: paddr=0004506c vaddr=40800000 size=0afach ( 44972) 
I (16948) esp_image: segment 2: paddr=00050020 vaddr=42000020 size=be8d8h (780504) map
I (17038) esp_image: segment 3: paddr=0010e900 vaddr=4080afac size=0fbb0h ( 64432) 
I (17048) esp_image: segment 4: paddr=0011e4b8 vaddr=4081ab60 size=03780h ( 14208)
I (17098) yesweCAN: OTA successfull, rebooting into new firmware...

Nun muss ich noch was implementieren damit der Update nicht jedesmal stattfindet wenn der ESP bootet.

UPDATE

Bei einem “normalen” Gadget würde es sicher reichen wenn man das Update auf Geräteseite anstößt, z.B. durch einen Button beim Boot, oder eine Weboberfläche oder es schaut selbst periodisch nach. Dabei sollte es immer seine aktuelle Version gegen die verfügbaren auf einer “Quelle” (Webserver) prüfen. Diesen Versionscheck habe ich auch schon implementiert aber in meinem speziellen Fall will ich ja ein Remote-Update auslösen wann ich will, dazu muss ich entweder einen Remote-Boot integrieren oder einen Update-Befehl. So oder so muss das Gerät auf einen Befehl horchen.

Dazu fällt mir neben einem zusätzlichen Webserver, bei dem man auch wieder alle Probleme bezüglich Security (SSL, Kennwort, etc.) lösen muss noch ein das über eine kleine Custom-Erweiterung des GVRET Protokolls zu erledigen. Dann müsste ich wenigstens keinen weiteren Listener implementieren.

Ich will das Teil ja ins Auto stecken und dann von meinem Arbeitsplatz bequem testen und deployen.

Wow! Alle Achtung!

Das ist ein aufwändiges Projekt mit sehr aufwändiger Dokumentation! Vielen Dank dafür!

Habe nicht alles im Detail gelesen, aber das klingt sehr interessant - für mich vielleicht etwas zu komplex. Ein ähnliches Projekt gibt es z.B. im Pedelec-Forum um die CANBus-Daten aus ebikes auslesen zu können. Oft ist die Kommunikation dort noch verschlüsselt, was die Sache besonders erschwert. Ist sie das beim Kfz nicht? Kann ich mir kaum vorstellen.