commit 99a82917a7c3cda3c9a79e9c201f1296e0a3c6c0 Author: Thomas Hooge Date: Sat Nov 29 09:58:10 2025 +0100 Initial commit diff --git a/extra_post.py b/extra_post.py new file mode 100644 index 0000000..e69de29 diff --git a/extra_pre.py b/extra_pre.py new file mode 100644 index 0000000..e69de29 diff --git a/include/hardware.h b/include/hardware.h new file mode 100644 index 0000000..8fa9434 --- /dev/null +++ b/include/hardware.h @@ -0,0 +1,34 @@ +// General hardware definitions +#pragma once + +// Keys +#define KEY_1 1 // Key or touchpad +#define KEY_2 2 // Key or touchpad + +// I2S audio output -> PCM5102 DAC +#define I2S_DOUT 4 // data out +#define I2S_BCLK 5 // bit clock +#define I2S_LRC 6 // left right channel select +#define I2S_XSMT 7 // PCM5102 soft mute + +// SPI for SD-Card; VSPI pins for higher performance +#define SD_CS 10 +#define SD_MOSI 11 +#define SD_MISO 13 +#define SD_SCK 12 + +// I2C for control interface +#define I2C_SDA 8 +#define I2C_CLK 9 + +// CAN bus for NMEA2000 connection +#define CAN_RX 47 +#define CAN_TX 48 + +// RS485 for NMEA0183 connection / UART1 +#define SER_RX 18 +#define SER_TX 17 + +// I2C Addresses +// Address of DAC module +// Address of switchbank diff --git a/include/main.h b/include/main.h new file mode 100644 index 0000000..bf8ee80 --- /dev/null +++ b/include/main.h @@ -0,0 +1,38 @@ +#pragma + +// WIFI AP +#define WIFI_CHANNEL 9 +#define WIFI_MAX_STA 2 + +// Keys +#define KEY_1 GPIO_NUM_5 // D2 +#define KEY_2 GPIO_NUM_6 // D3 +#define KEY_3 GPIO_NUM_7 // D4 +#define KEY_4 GPIO_NUM_8 // D5 +#define KEY_5 GPIO_NUM_9 // D6 +#define KEY_6 GPIO_NUM_10 // D7 +#define KEY_DST GPIO_NUM_17 + +// LEDS +#define LED_A GPIO_NUM_1 +#define LED_B GPIO_NUM_2 +#define LED_C GPIO_NUM_3 +#define LED_RGBA GPIO_NUM_4 +#define LED_RGBB GPIO_NUM_13 +#define LED_RGBC GPIO_NUM_14 +#define LED_USER GPIO_NUM_48 + +// CAN bus for NMEA2000 connection +#define CAN_RX GPIO_NUM_18 // D9 +#define CAN_TX GPIO_NUM_21 // D10 +#define CAN_RECOVERY_PERIOD 3000 + +// NMEA2000 defaults +#define N2K_DEFAULT_NODEID 124 + +// I2C temp. sensor +#define I2C_SDA GPIO_NUM_11 // A4 +#define I2C_SCL GPIO_NUM_12 // A5 + +// I2C addresses +#define SHT31_ADDRESS 0x44 diff --git a/lib/Nmea2kTwai/Nmea2kTwai.cpp b/lib/Nmea2kTwai/Nmea2kTwai.cpp new file mode 100644 index 0000000..c08e0c0 --- /dev/null +++ b/lib/Nmea2kTwai/Nmea2kTwai.cpp @@ -0,0 +1,224 @@ +#include "Nmea2kTwai.h" +#include "driver/gpio.h" +#include "driver/twai.h" + +#define LOGID(id) ((id >> 8) & 0x1ffff) + +static const int TIMEOUT_OFFLINE = 256; // number of timeouts to consider offline + +Nmea2kTwai::Nmea2kTwai(gpio_num_t _TxPin, gpio_num_t _RxPin, unsigned long recP, unsigned long logP): + tNMEA2000(), RxPin(_RxPin), TxPin(_TxPin) +{ + if (RxPin < 0 || TxPin < 0){ + disabled = true; + } else { + // timers.addAction(logP,[this](){ logStatus(); }); + // timers.addAction(recP,[this](){ checkRecovery(); }); + } +} + +bool Nmea2kTwai::CANSendFrame(unsigned long id, unsigned char len, const unsigned char *buf, bool wait_sent) +{ + if (disabled) { + return true; + } + twai_message_t message; + memset(&message, 0, sizeof(message)); + message.identifier = id; + message.extd = 1; + message.data_length_code = len; + memcpy(message.data, buf,len); + esp_err_t rt = twai_transmit(&message, 0); + if (rt != ESP_OK){ + if (rt == ESP_ERR_TIMEOUT) { + if (txTimeouts < TIMEOUT_OFFLINE) txTimeouts++; + } + // logDebug(LOG_MSG,"twai transmit for %ld failed: %x",LOGID(id),(int)rt); + return false; + } + txTimeouts = 0; + // logDebug(LOG_MSG,"twai transmit id %ld, len %d",LOGID(id),(int)len); + return true; +} + +bool Nmea2kTwai::CANOpen() +{ + if (disabled){ + // logDebug(LOG_INFO,"CAN disabled"); + return true; + } + esp_err_t rt = twai_start(); + if (rt != ESP_OK){ + // logDebug(LOG_ERR,"CANOpen failed: %x",(int)rt); + return false; + } else { + // logDebug(LOG_INFO, "CANOpen ok"); + } + return true; +} + +bool Nmea2kTwai::CANGetFrame(unsigned long &id, unsigned char &len, unsigned char *buf) +{ + if (disabled) { + return false; + } + twai_message_t message; + esp_err_t rt = twai_receive(&message, 0); + if (rt != ESP_OK){ + return false; + } + if (! message.extd) { + return false; + } + id = message.identifier; + len = message.data_length_code; + if (len > 8) { + // logDebug(LOG_DEBUG,"twai: received invalid message %lld, len %d",LOGID(id),len); + len = 8; + } + // logDebug(LOG_MSG,"twai rcv id=%ld,len=%d, ext=%d",LOGID(message.identifier),message.data_length_code,message.extd); + if (! message.rtr) { + memcpy(buf, message.data, message.data_length_code); + } + return true; +} + +void Nmea2kTwai::initDriver() +{ + if (disabled) { + return; + } + twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(TxPin,RxPin, TWAI_MODE_NORMAL); + g_config.tx_queue_len = 20; + twai_timing_config_t t_config = TWAI_TIMING_CONFIG_250KBITS(); + twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); + esp_err_t rt = twai_driver_install(&g_config, &t_config, &f_config); + if (rt == ESP_OK) { + // logDebug(LOG_INFO,"twai driver initialzed, rx=%d,tx=%d",(int)RxPin,(int)TxPin); + } else { + // logDebug(LOG_ERR,"twai driver init failed: %x",(int)rt); + } +} + +/* This will be called on Open() before any other initialization. + * Inherit this, if buffers can be set for the driver and you want to + * change size of library send frame buffer size. + * See e.g. NMEA2000_teensy.cpp. + */ +void Nmea2kTwai::InitCANFrameBuffers() +{ + if (disabled) { + // logDebug(LOG_INFO,"twai init - disabled"); + } else{ + initDriver(); + } + tNMEA2000::InitCANFrameBuffers(); +} + +Nmea2kTwai::Status Nmea2kTwai::getStatus() +{ + twai_status_info_t state; + Status rt; + if (disabled) { + rt.state = ST_DISABLED; + return rt; + } + if (twai_get_status_info(&state) != ESP_OK) { + return rt; + } + switch (state.state) { + case TWAI_STATE_STOPPED: + rt.state = ST_STOPPED; + break; + case TWAI_STATE_RUNNING: + rt.state = ST_RUNNING; + break; + case TWAI_STATE_BUS_OFF: + rt.state = ST_BUS_OFF; + break; + case TWAI_STATE_RECOVERING: + rt.state = ST_RECOVERING; + break; + } + rt.rx_errors = state.rx_error_counter; + rt.tx_errors = state.tx_error_counter; + rt.tx_failed = state.tx_failed_count; + rt.rx_missed = state.rx_missed_count; + rt.rx_overrun = state.rx_overrun_count; + rt.tx_timeouts = txTimeouts; + if (rt.tx_timeouts >= TIMEOUT_OFFLINE && rt.state == ST_RUNNING) { + rt.state = ST_OFFLINE; + } + return rt; +} + +bool Nmea2kTwai::checkRecovery() +{ + if (disabled) { + return false; + } + Status canState = getStatus(); + bool strt = false; + if (canState.state != Nmea2kTwai::ST_RUNNING) { + if (canState.state == Nmea2kTwai::ST_BUS_OFF) { + strt = true; + bool rt = startRecovery(); + // logDebug(LOG_INFO, "twai BUS_OFF: start can recovery - result %d", (int)rt); + } + if (canState.state == Nmea2kTwai::ST_STOPPED) { + bool rt = CANOpen(); + // logDebug(LOG_INFO, "twai STOPPED: restart can driver - result %d", (int)rt); + } + } + return strt; +} + +void Nmea2kTwai::loop() +{ + if (disabled) { + return; + } + // timers.loop(); +} + +Nmea2kTwai::Status Nmea2kTwai::logStatus() +{ + Status canState = getStatus(); + /* logDebug(LOG_INFO, "twai state %s, rxerr %d, txerr %d, txfail %d, txtimeout %d, rxmiss %d, rxoverrun %d", + stateStr(canState.state), + canState.rx_errors, + canState.tx_errors, + canState.tx_failed, + canState.tx_timeouts, + canState.rx_missed, + canState.rx_overrun); */ + return canState; +} + +bool Nmea2kTwai::startRecovery() +{ + if (disabled) { + return false; + } + lastRecoveryStart = millis(); + esp_err_t rt = twai_driver_uninstall(); + if (rt != ESP_OK) { + // logDebug(LOG_ERR,"twai: deinit for recovery failed with %x",(int)rt); + } + initDriver(); + bool frt = CANOpen(); + return frt; +} + +const char * Nmea2kTwai::stateStr(const Nmea2kTwai::STATE &st) +{ + switch (st) { + case ST_BUS_OFF: return "BUS_OFF"; + case ST_RECOVERING: return "RECOVERING"; + case ST_RUNNING: return "RUNNING"; + case ST_STOPPED: return "STOPPED"; + case ST_OFFLINE: return "OFFLINE"; + case ST_DISABLED: return "DISABLED"; + } + return "ERROR"; +} diff --git a/lib/Nmea2kTwai/Nmea2kTwai.h b/lib/Nmea2kTwai/Nmea2kTwai.h new file mode 100644 index 0000000..2fa8db8 --- /dev/null +++ b/lib/Nmea2kTwai/Nmea2kTwai.h @@ -0,0 +1,62 @@ +#ifndef _NMEA2KTWAI_H +#define _NMEA2KTWAI_H +#include "NMEA2000.h" +// #include "GwTimer.h" + +class Nmea2kTwai : public tNMEA2000 { +public: + Nmea2kTwai(gpio_num_t _TxPin, gpio_num_t _RxPin, unsigned long recP=0, unsigned long logPeriod=0); + typedef enum { + ST_STOPPED, + ST_RUNNING, + ST_BUS_OFF, + ST_RECOVERING, + ST_OFFLINE, + ST_DISABLED, + ST_ERROR + } STATE; + typedef struct{ + //see https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/twai.html#_CPPv418twai_status_info_t + uint32_t rx_errors = 0; + uint32_t tx_errors = 0; + uint32_t tx_failed = 0; + uint32_t rx_missed = 0; + uint32_t rx_overrun = 0; + uint32_t tx_timeouts = 0; + STATE state = ST_ERROR; + } Status; + Status getStatus(); + unsigned long getLastRecoveryStart() { return lastRecoveryStart; } + void loop(); + static const char *stateStr(const STATE &st); + virtual bool CANOpen(); + virtual ~Nmea2kTwai(){}; + static const int LOG_ERR = 0; + static const int LOG_INFO = 1; + static const int LOG_DEBUG = 2; + static const int LOG_MSG = 3; + +protected: + virtual bool CANSendFrame(unsigned long id, unsigned char len, const unsigned char *buf, bool wait_sent=true); + virtual bool CANGetFrame(unsigned long &id, unsigned char &len, unsigned char *buf); + /* This will be called on Open() before any other initialization. + Inherit this, if buffers can be set for the driver and you want + to change size of library send frame buffer size. + See e.g. NMEA2000_teensy.cpp. */ + virtual void InitCANFrameBuffers(); + virtual void logDebug(int level,const char *fmt,...){} + +private: + void initDriver(); + bool startRecovery(); + bool checkRecovery(); + Status logStatus(); + gpio_num_t TxPin; + gpio_num_t RxPin; + uint32_t txTimeouts = 0; + // GwIntervalRunner timers; + bool disabled = false; + unsigned long lastRecoveryStart=0; +}; + +#endif diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..af35eb9 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,35 @@ +[platformio] +default_envs= + esp32-s3-nano + +[env] +platform = espressif32 +framework = arduino +lib_deps = + Preferences + Wifi + Wire + ESP32Async/AsyncTCP@3.4.9 + ESP32Async/ESPAsyncWebServer@3.9.1 + ttlappalainen/NMEA2000-library@4.24 + robtillaart/SHT31@^0.5.2 +# adafruit/Adafruit NeoPixel + +extra_scripts = + pre:extra_pre.py + post:extra_post.py +lib_ldf_mode = chain +monitor_speed = 115200 +build_flags = + -D PIO_ENV_BUILD=$PIOENV + -DBOARD_HAS_PSRAM + -DARDUINO_USB_CDC_ON_BOOT=1 + +[env:esp32-s3-nano] +build_type = release # debug | release +#board = esp32-s3-devkitc-1 +board = arduino_nano_esp32 +board_upload.flash_size = 16MB +board_build.partitions = default.csv +upload_port = /dev/ttyACM0 +upload_protocol = esptool diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..3d7b351 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,295 @@ +#include +#include +#include +#include +#include +#include +#include // temp. sensor +#include +#include +#include +#include "main.h" +#include "Nmea2kTwai.h" + +Preferences preferences; // persistent storage for configuration + +const char* wifi_ssid = "OBPKP61"; +const char* wifi_pass = "keypad61"; +AsyncWebServer server(80); +const char index_html[] PROGMEM = R"rawliteral( + + + ESP Web Server + + + + +

ESP Web Server

+

Work in progress

+ + +)rawliteral"; + +unsigned long lastPrint = 0; +unsigned long counter = 0; + +bool rgb_r = false; +bool rgb_g = false; +bool rgb_b = false; + +char destination = 'A'; // A | B | C + +SHT31 sht(SHT31_ADDRESS); + +int nodeid; // NMEA2000 id on bus +Nmea2kTwai &NMEA2000=*(new Nmea2kTwai(CAN_TX, CAN_RX, CAN_RECOVERY_PERIOD)); + + +String processor(const String& var) { + // dummy for now + return ""; +} + +/* Low level wifi setup (alternative) +static void wifi_event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ + // printf("Event nr: %ld!\n", event_id); +} + +void wifi_init_softap() +{ + esp_netif_init(); + esp_event_loop_create_default(); + esp_netif_create_default_wifi_ap(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL); + esp_wifi_init(&cfg); + wifi_config_t wifi_config = { + .ap = { + .ssid = wifi_ssid, + .ssid_len = strlen(wifi_ssid), + .channel = WIFI_CHANNEL, + .password = wifi_pass, + .max_connection = WIFI_MAX_STA, + .authmode = WIFI_AUTH_WPA2_PSK, + .pmf_cfg = { + .required = true, + }, + }, + }; + esp_wifi_set_mode(WIFI_MODE_AP); + esp_wifi_set_config(WIFI_IF_AP, &wifi_config); + esp_wifi_start(); + + ESP_LOGI(TAG, "wifi_init_softap finished. SSID:%s password:%s channel:%d", + ESP_WIFI_SSID, ESP_WIFI_PASS, ESP_WIFI_CHANNEL); +} +*/ + +void setup() { + + nodeid = N2K_DEFAULT_NODEID; + Serial.print("N2K default node id="); + Serial.println(nodeid); + + preferences.begin("nvs", false); + nodeid = preferences.getInt("LastNodeId", N2K_DEFAULT_NODEID); + preferences.end(); + + /* + // Connect as client to existing network + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + delay(1000); + Serial.println("Connecting to WiFi.."); + } + Serial.println(WiFi.localIP()); */ + + WiFi.persistent(false); + WiFi.mode(WIFI_MODE_AP); + + IPAddress ap_addr(192, 168, 15, 1); + IPAddress ap_subnet(255, 255, 255, 0); + IPAddress ap_gateway(ap_addr); + + int channel = WIFI_CHANNEL; + bool hidden = false; + WiFi.softAP(wifi_ssid, wifi_pass, channel, hidden, WIFI_MAX_STA); + + // Route for root / web page + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/html", index_html, processor); + }); + server.begin(); + + NMEA2000.SetProductInformation("00000001", // Manufacturer's Model serial code + 74, // Manufacturer's product code + "OBPkeypad6/1", // Manufacturer's Model ID + "1.0.0 (2025-11-28)", // Manufacturer's Software version code + "0.1" // Manufacturer's Model version + ); + + // TODO Device unique id stored in preferences + NMEA2000.SetDeviceInformation(1, // Unique number. Use e.g. Serial number. + 130, // Device function=Atmospheric + 85, // Device class=External Environment + 2046 + ); + + // Buttons active-low, internal resistor + pinMode(KEY_1, INPUT_PULLUP); + pinMode(KEY_2, INPUT_PULLUP); + pinMode(KEY_3, INPUT_PULLUP); + pinMode(KEY_4, INPUT_PULLUP); + pinMode(KEY_5, INPUT_PULLUP); + pinMode(KEY_6, INPUT_PULLUP); + pinMode(KEY_DST, INPUT_PULLUP); + + // internal user led (red) + pinMode(LED_USER, OUTPUT); + digitalWrite(LED_USER, HIGH); + delay(1000); + digitalWrite(LED_USER, LOW); + + // destination leds + pinMode(LED_A, OUTPUT); + digitalWrite(LED_A, HIGH); + pinMode(LED_B, OUTPUT); + digitalWrite(LED_B, LOW); + pinMode(LED_C, OUTPUT); + digitalWrite(LED_C, LOW); + + // Init onbard RGB LED + // TODO + + // enclosure rgb led (common anode) + pinMode(LED_RGBA, OUTPUT); + digitalWrite(LED_RGBA, HIGH); + pinMode(LED_RGBB, OUTPUT); + digitalWrite(LED_RGBB, HIGH); + pinMode(LED_RGBC, OUTPUT); + digitalWrite(LED_RGBC, HIGH); + + Serial.begin(115200); + delay(500); + Serial.println("Starting..."); + + // I²C + Serial.print("SHT31_LIB_VERSION: "); + Serial.println(SHT31_LIB_VERSION); + Wire.begin(I2C_SDA, I2C_SCL); + Wire.setClock(100000); + uint16_t stat = sht.readStatus(); + Serial.print(stat, HEX); + // stat = ffff anscheinend Fehler + // = 8010 läuft anscheinend + Serial.println(); + +} + +void loop() { + + // Button pressed? (active low) + uint8_t button = 0; + if (digitalRead(KEY_1) == LOW) { + Serial.println("Button detected: 1"); + button = 1; + if (rgb_r) { + rgb_r = false; + digitalWrite(LED_RGBA, HIGH); + } else { + rgb_r = true; + digitalWrite(LED_RGBA, LOW); + } + } + if (digitalRead(KEY_2) == LOW) { + Serial.println("Button detected: 2"); + button += 2; + if (rgb_g) { + rgb_g = false; + digitalWrite(LED_RGBB, HIGH); + } else { + rgb_g = true; + digitalWrite(LED_RGBB, LOW); + } + } + if (digitalRead(KEY_3) == LOW) { + Serial.println("Button detected: 3"); + button += 4; + if (rgb_b) { + rgb_b = false; + digitalWrite(LED_RGBC, HIGH); + } else { + rgb_b = true; + digitalWrite(LED_RGBC, LOW); + } + } + + if (digitalRead(KEY_4) == LOW) { + Serial.println("Button detected: 4"); + button += 8; + digitalWrite(LED_USER, HIGH); // Turn LED on + delay(500); // Keep it on 0.5s + digitalWrite(LED_USER, LOW); // Turn LED off + } + if (digitalRead(KEY_5) == LOW) { + Serial.println("Button detected: 5"); + button += 16; + digitalWrite(LED_USER, HIGH); // Turn LED on + delay(500); // Keep it on 0.5s + digitalWrite(LED_USER, LOW); // Turn LED off + } + if (digitalRead(KEY_6) == LOW) { + Serial.println("Button detected: 6"); + button += 32; + digitalWrite(LED_USER, HIGH); // Turn LED on + delay(500); // Keep it on 0.5s + digitalWrite(LED_USER, LOW); // Turn LED off + } + + if (digitalRead(KEY_DST) == LOW) { + Serial.println("Button detected: DST"); + button += 64; + + if (destination == 'A') { + destination = 'B'; + digitalWrite(LED_A, LOW); + digitalWrite(LED_B, HIGH); + } else if (destination == 'B') { + destination = 'C'; + digitalWrite(LED_B, LOW); + digitalWrite(LED_C, HIGH); + } else { + destination = 'A'; + digitalWrite(LED_C, LOW); + digitalWrite(LED_A, HIGH); + } + + /* + digitalWrite(LED_USER, HIGH); // Turn LED on + delay(500); // Keep it on 0.5s + digitalWrite(LED_USER, LOW); // Turn LED off + */ + + } + + if (button > 0) { + sht.read(); + Serial.print(sht.getTemperature(), 1); + Serial.print("\t"); + Serial.println(sht.getHumidity(), 1); + // Debounce delay to avoid multiple triggers + delay(200); + } + + + // ---- PRINT NUMBER EVERY SECOND ---- + if (millis() - lastPrint >= 1000) { + lastPrint = millis(); + counter++; + Serial.printf("Loop counter: %lu\n", counter); + } + +}