Initial commit

This commit is contained in:
2025-11-29 09:58:10 +01:00
commit 99a82917a7
8 changed files with 688 additions and 0 deletions

0
extra_post.py Normal file
View File

0
extra_pre.py Normal file
View File

34
include/hardware.h Normal file
View File

@@ -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

38
include/main.h Normal file
View File

@@ -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

View File

@@ -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";
}

View File

@@ -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

35
platformio.ini Normal file
View File

@@ -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

295
src/main.cpp Normal file
View File

@@ -0,0 +1,295 @@
#include <Arduino.h>
#include <Preferences.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <Wire.h>
#include <SHT31.h> // temp. sensor
#include <NMEA2000.h>
#include <N2kMsg.h>
#include <N2kMessages.h>
#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(
<!DOCTYPE HTML><html>
<head>
<title>ESP Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
</head>
<body>
<h2>ESP Web Server</h2>
<p>Work in progress</p>
</body>
</html>
)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);
}
}