Keyboard functional and first sending of PGN 127502

This commit is contained in:
2026-01-04 19:28:21 +01:00
parent 12687b17ab
commit c953340362
5 changed files with 501 additions and 165 deletions

View File

@@ -13,9 +13,9 @@
#include "Nmea2kTwai.h"
#include <map>
#include "mbedtls/md.h" // for SHA256
#include <esp32/clk.h> // for cpu frequency
#include "driver/rtc_io.h" // for wakeup from deep sleep
String get_sha256(String payload) {
byte shaResult[32];
@@ -90,8 +90,10 @@ uint64_t chipid = ESP.getEfuseMac();
const char* wifi_ssid = "OBPKP61";
const char* wifi_pass = "keypad61";
bool apEnabled = false;
AsyncWebServer server(80);
unsigned long firstStart = 0;
unsigned long lastSensor = 0;
unsigned long lastPrint = 0;
unsigned long counter = 0;
@@ -100,12 +102,17 @@ bool rgb_r = false;
bool rgb_g = false;
bool rgb_b = false;
char destination = 'A'; // A | B | C
char mode = 'N'; // (N)ormal | (C)onfig
char ledmode = 'D'; // (D)ay | (N)ight
char audiomode = 'E'; // (E)nabled | (D)isabled
RTC_DATA_ATTR char destination = 'A'; // A | B | C im RTC-Speicher überlebt deep sleep
uint8_t led_brightness = 16; // analog pin: 0 .. 255
uint8_t rgb_brightness = 64;
uint buzzerpower = 50; // TBD make use of this
uint8_t keycode[6]; // configurable keycodes
SHT31 sht(SHT31_ADDRESS);
bool sht_available = false;
float temp = 0.0;
@@ -143,7 +150,7 @@ String uptime_with_unit() {
void ledtest() {
// all led one after another to test functionality
Serial.print("LED test started");
Serial.println("LED test started");
// Onbard RGB LED, inverted mode
digitalWrite(LED_IR, LOW);
@@ -173,9 +180,6 @@ void ledtest() {
analogWrite(LED_C, led_brightness);
delay(500);
analogWrite(LED_C, 0);
// select dst A finally
analogWrite(LED_A, led_brightness);
delay(500);
// enclosure rgb led (common cathode, so low is off)
analogWrite(RGBLED_R, rgb_brightness);
@@ -190,25 +194,126 @@ void ledtest() {
delay(700);
analogWrite(RGBLED_B, 0);
Serial.print("LED test finished");
Serial.println("LED test finished");
}
TaskHandle_t ledTaskHandle = NULL;
TaskHandle_t sensorTaskHandle = NULL;
TaskHandle_t keyTaskHandle = NULL;
QueueHandle_t ledQueue = NULL;
QueueHandle_t keyQueue = NULL;
void ledTask(void *parameter) {
Serial.println("Starting LED task");
for (;;) {
vTaskDelay(5000);
analogWrite(RGBLED_G, 10); // a short activity flash
vTaskDelay(20);
analogWrite(RGBLED_G, 0);
}
}
void sensorTask(void *parameter) {
Serial.println("Starting sensor task");
for (;;) {
vTaskDelay(10000); // nothing yet
}
}
void keyTask(void *parameter) {
// short key press <= 1s
// medium key press >1s and < 3s
// long key press >= 3s
Serial.println("Starting keyboard task");
constexpr uint8_t NUM_BUTTONS = 7;
constexpr gpio_num_t buttonPins[NUM_BUTTONS] = {
KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_DST
};
constexpr TickType_t DEBOUNCE_TIME = pdMS_TO_TICKS(30);
constexpr TickType_t POLL_INTERVAL = pdMS_TO_TICKS(10); // 100Hz update rate (10ms)
bool lastRead[NUM_BUTTONS];
bool lastStable[NUM_BUTTONS];
TickType_t lastDebounce[NUM_BUTTONS];
TickType_t pressStart[NUM_BUTTONS];
// init
for (int i = 0; i < NUM_BUTTONS; i++) {
lastRead[i]= HIGH;
lastStable[i] = HIGH;
lastDebounce[i]= 0;
pressStart[i] = 0;
}
for (;;) {
TickType_t now = xTaskGetTickCount();
for (int i = 0; i < NUM_BUTTONS; i++) {
bool reading = digitalRead(buttonPins[i]);
if (reading != lastRead[i]) {
lastDebounce[i] = now;
lastRead[i] = reading;
}
if ((now - lastDebounce[i]) > DEBOUNCE_TIME) {
if (reading != lastStable[i]) {
lastStable[i] = reading;
if (reading == LOW) {
// button pressed
pressStart[i] = now;
} else {
// button released: send to queue
TickType_t duration = now - pressStart[i];
ButtonEvent event;
event.buttonId = i;
if (duration < 1000) {
event.pressType = ButtonPressType::SHORT;
} else if (duration < 3000) {
event.pressType = ButtonPressType::MEDIUM;
} else {
event.pressType = ButtonPressType::LONG;
}
xQueueSend(keyQueue, &event, 0);
}
}
}
vTaskDelay(POLL_INTERVAL);
}
}
vTaskDelete(NULL);
}
void cpuFreqTimerCallback(TimerHandle_t xTimer) {
Serial.println("after 3 minutes: set CPU frequency to 160MHz");
setCpuFrequencyMhz(160);
}
void setup() {
Serial.begin(115200);
// while (!Serial) delay(10);
// while (!Serial) delay(10); verhindert Booten ohne USB-Verbindung
delay(500);
preferences.begin("nvs", false);
keycode[0] = preferences.getInt("key1", 1);
keycode[1] = preferences.getInt("key2", 2);
keycode[2] = preferences.getInt("key3", 3);
keycode[3] = preferences.getInt("key4", 4);
keycode[4] = preferences.getInt("key5", 5);
keycode[5] = preferences.getInt("key6", 6);
preferences.end();
// Configure I/O pins
// internal user led (red)
pinMode(LED_USER, OUTPUT);
// Init onbard RGB led
// Init onbard RGB led and switch off
pinMode(LED_IR, OUTPUT);
pinMode(LED_IG, OUTPUT);
pinMode(LED_IB, OUTPUT);
digitalWrite(LED_IR, HIGH);
digitalWrite(LED_IG, HIGH);
digitalWrite(LED_IB, HIGH);
// destination leds
pinMode(LED_A, OUTPUT);
@@ -244,6 +349,13 @@ void setup() {
ESP_LOGI(TAG, "Starting ...");
esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
if (cause == ESP_SLEEP_WAKEUP_EXT0) {
ESP_LOGI(TAG, " Wake up by key");
} else {
destination = 'A';
}
// N2K basics
nodeid = N2K_DEFAULT_NODEID;
ESP_LOGI(TAG, "N2K default node is %d", nodeid);
@@ -264,6 +376,7 @@ void setup() {
IPAddress ap_subnet(255, 255, 255, 0);
IPAddress ap_gateway(ap_addr);
WiFi.softAPConfig(ap_addr, ap_gateway, ap_subnet);
apEnabled = true;
// Route for root / web page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
@@ -292,11 +405,29 @@ void setup() {
request->send(200, "application/json", out);
});
server.on("/api/config", HTTP_GET, [](AsyncWebServerRequest *request){
StaticJsonDocument<100> doc;
StaticJsonDocument<512> doc;
doc["systemName"] = "Keypad1";
doc["logLevel"] = 0;
doc["version"] = "0.0";
doc["fwtype"] = "unknown";
doc["salt"] = "secret";
doc["AdminPassword"] = "********";
doc["useAdminPass"] = false;
doc["apIp"] = "192.168.15.1";
doc["apMask"] = "255.255.255.0";
doc["apPassword"] = "********";
doc["stopApTime"] = 0;
doc["cpuSpeed"] = 160;
doc["tempFormat"] = "C";
doc["ledBrightness"] = led_brightness;
doc["ledRgbBrightness"] = rgb_brightness;
doc["tempFormat"] = "C";
doc["key1"] = keycode[BUTTON_1];
doc["key2"] = keycode[BUTTON_2];
doc["key3"] = keycode[BUTTON_3];
doc["key4"] = keycode[BUTTON_4];
doc["key5"] = keycode[BUTTON_5];
doc["key6"] = keycode[BUTTON_6];
String out;
serializeJson(doc, out);
request->send(200, "application/json", out);
@@ -334,6 +465,13 @@ void setup() {
serializeJson(doc, out);
request->send(200, "application/json", out);
});
server.on("/api/update", HTTP_POST, [](AsyncWebServerRequest *request){
StaticJsonDocument<100> doc;
doc["status"] = "FAILED";
String out;
serializeJson(doc, out);
request->send(200, "application/json", out);
});
// TODO POST vom Client entgegennehmen
@@ -348,17 +486,37 @@ void setup() {
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
"0.1.0 (2026-01-04)", // Manufacturer's Software version code
"0.1", // Manufacturer's Model version
1 // LoadEquivalency (LEN)
);
uint32_t uid; // ISO 21bit identity number devived from chipid (MAC)
uid = ((chipid >> 32) ^ (chipid & 0xFFFFFFFF)) & 0x1FFFFF;
// 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
NMEA2000.SetDeviceInformation(uid, // Unique number. Use e.g. Serial number.
130, // Device function=Button Interface (or 190?)
110, // Device class=Human Interface? (or 140?)
2046, // Manufacturer code (custom OBP)
4 // Industrygoup=Marine
);
NMEA2000.SetForwardType(tNMEA2000::fwdt_Text);
// Debug
// NMEA2000.SetForwardStream(&Serial);
// NMEA2000.SetDebugMode(tNMEA2000::dm_2);
NMEA2000.SetMode(tNMEA2000::N2km_ListenAndNode, nodeid);
NMEA2000.SetForwardOwnMessages(false);
NMEA2000.SetHeartbeatIntervalAndOffset(NMEA2000_HEARTBEAT_INTERVAL);
const unsigned long TransmitPGNs[] = {
127502UL,
0
};
NMEA2000.ExtendTransmitMessages(TransmitPGNs);
NMEA2000.Open();
// internal user led (red)
digitalWrite(LED_USER, HIGH);
@@ -369,10 +527,11 @@ void setup() {
ledcSetup(LEDC_CHANNEL, LEDC_BASE_FREQ, LEDC_TIMER_8_BIT);
ledcAttachPin(BUZZER, LEDC_CHANNEL);
// Test tone while booting
// Buzzer 12V, 2500Hz +/- 200Hz, 30mA, ca. 90dB
if (ledcWriteTone(LEDC_CHANNEL, 2300) == 0) {
Serial.println("Error setting buzzer tone");
} else {
delay(750);
delay(50);
ledcWriteTone(LEDC_CHANNEL, 0); // Buzzer off
}
@@ -380,6 +539,19 @@ void setup() {
analogWrite(RGBLED_R, 0); // boot status off
delay(500);
ledtest();
// select current destination
switch (destination) {
case 'A':
analogWrite(LED_A, led_brightness);
break;
case 'B':
analogWrite(LED_B, led_brightness);
break;
case 'C':
analogWrite(LED_C, led_brightness);
break;
}
// I²C
Serial.print("SHT31_LIB_VERSION: ");
@@ -396,18 +568,47 @@ void setup() {
// Additional tests
String passhash = get_sha256("secretTEST");
xTaskCreatePinnedToCore(
ledTask, // Task function
"LEDTask", // Task name
10000, // Stack size (bytes)
NULL, // Parameters
1, // Priority
&ledTaskHandle, // Task handle
1 // Core 1
);
xTaskCreatePinnedToCore(sensorTask,"SensorTask", 10000, NULL, 1, &sensorTaskHandle, 1);
xTaskCreatePinnedToCore(keyTask,"KeyboardTask", 10000, NULL, 1, &keyTaskHandle, 1);
// Create queues (5 items, each uint16_t)
keyQueue = xQueueCreate(5, sizeof(ButtonEvent));
ledQueue = xQueueCreate(5, sizeof(uint8_t));
if (esp_sleep_is_valid_wakeup_gpio(KEY_DST)) {
Serial.println("DST-Taste ist als Wakeup-Pin konfiguriert");
esp_sleep_enable_ext0_wakeup(KEY_DST, 0);
} else {
Serial.println("Keine Wakeup-Funktion vorhanden! Deep-sleep deaktiviert.");
}
TimerHandle_t cpuFreqTimer = xTimerCreate(
"cpuFreqTimer",
pdMS_TO_TICKS(2 * 60 * 1000),
pdFALSE, // one shot only
nullptr,
cpuFreqTimerCallback
);
xTimerStart(cpuFreqTimer, 0);
}
void shortBeep() {
ledcWriteTone(LEDC_CHANNEL, 2500);
delay(15);
ledcWriteTone(LEDC_CHANNEL, 0);
}
void atariKeyclick() {
/*
* Anderer 12V-fähiger Buzzer!
*
Buzzer + → 12V
Buzzer → IRLZ44N Drain
IRLZ44N Source → GND
ESP32 GPIO43 → 1kΩ resistor → Gate
ESP32 GND must be connected to the same GND as the 12V supply.
* */
ledcWriteTone(LEDC_CHANNEL, 3200);
delayMicroseconds(3000);
ledcWriteTone(LEDC_CHANNEL, 0);
@@ -417,118 +618,161 @@ void atariKeyclick() {
ledcWriteTone(LEDC_CHANNEL, 0);
}
void SendToN2K(uint8_t keycode) {
tN2kMsg N2kMsg;
tN2kBinaryStatus bankstatus;
N2kResetBinaryStatus(bankstatus);
N2kSetStatusBinaryOnStatus(bankstatus, N2kOnOff_On, keycode);
// TODO
// Needs filled N2K device list to map NAME to current address
// N2kMsg.Destination = deviceAddress;
//
SetN2kPGN127502(N2kMsg, 0, bankstatus);
NMEA2000.SendMsg(N2kMsg);
Serial.print("PGN127502 sent: Switch=");
Serial.print(keycode);
Serial.println(" Action=On");
}
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;
analogWrite(RGBLED_R, 255);
} else {
rgb_r = true;
analogWrite(RGBLED_R, 255 - rgb_brightness);
ButtonEvent event;
if (xQueueReceive(keyQueue, &event, 0) == pdPASS) {
Serial.print("Button ");
Serial.print(event.buttonId);
Serial.print(" -> ");
switch (event.pressType) {
case ButtonPressType::SHORT:
Serial.println("SHORT");
break;
case ButtonPressType::MEDIUM:
Serial.println("MEDIUM");
break;
case ButtonPressType::LONG:
Serial.println("LONG");
break;
default:
Serial.print("UNBEKANNT: ");
Serial.println(static_cast<uint8_t>(event.pressType));
break;
}
}
if (digitalRead(KEY_2) == LOW) {
Serial.println("Button detected: 2");
button += 2;
if (rgb_g) {
rgb_g = false;
analogWrite(RGBLED_G, 255);
if (event.buttonId == BUTTON_DST) {
if (event.pressType == ButtonPressType::SHORT) {
if (mode == 'N') {
// switch destination only in normal mode
if (destination == 'A') {
destination = 'B';
analogWrite(LED_A, 0);
analogWrite(LED_B, led_brightness);
} else if (destination == 'B') {
destination = 'C';
analogWrite(LED_B, 0);
analogWrite(LED_C, led_brightness);
} else {
destination = 'A';
analogWrite(LED_C, 0);
analogWrite(LED_A, led_brightness);
}
Serial.print("New destination=");
Serial.println(destination);
}
} else if (event.pressType == ButtonPressType::LONG) {
shortBeep();
// switch config mode
if (mode == 'N') {
mode = 'C';
analogWrite(RGBLED_B, rgb_brightness); // blue status indicator
} else {
mode = 'N';
analogWrite(RGBLED_B, 0);
}
}
} else {
rgb_g = true;
analogWrite(RGBLED_G, 255 - rgb_brightness);
}
}
if (digitalRead(KEY_3) == LOW) {
Serial.println("Button detected: 3");
button += 4;
if (rgb_b) {
rgb_b = false;
analogWrite(RGBLED_B, 255);
} else {
rgb_b = true;
analogWrite(RGBLED_B, 255 - rgb_brightness);
// normal button
if (mode == 'N') {
// TODO send key code to destination
atariKeyclick();
SendToN2K(keycode[event.buttonId]);
} else {
// online config mode
switch (event.buttonId) {
case BUTTON_1: // switch day/night mode
if (ledmode == 'D') {
Serial.println("Night mode enabled");
ledmode = 'N';
} else {
Serial.println("Day mode enabled");
ledmode = 'D';
}
break;
case BUTTON_2: // switch audio on/off
if (audiomode == 'E') {
Serial.println("Disabled audio");
audiomode = 'D';
} else {
Serial.println("Enabled audio");
audiomode = 'E';
}
break;
case BUTTON_3: // switch accesspoint on/off
if (apEnabled) {
Serial.println("Disable Accesspoint");
WiFi.softAPdisconnect(true);
apEnabled = false;
} else {
Serial.println("Enable Accesspoint");
WiFi.softAP(wifi_ssid, wifi_pass);
apEnabled = true;
}
break;
case BUTTON_4: // reserved
break;
case BUTTON_5: // reset
Serial.println("Device reset");
analogWrite(RGBLED_B, 0);
delay(500);
analogWrite(RGBLED_G, 255);
delay(500);
analogWrite(RGBLED_G, 0);
delay(500);
analogWrite(RGBLED_G, 255);
delay(500);
analogWrite(RGBLED_G, 0);
delay(500);
analogWrite(RGBLED_G, 255);
delay(500);
ESP.restart();
break;
case BUTTON_6: // deep sleep
Serial.println("Going into deep sleep");
Serial.flush();
analogWrite(RGBLED_B, 0);
delay(500);
analogWrite(RGBLED_B, 255);
delay(500);
analogWrite(RGBLED_B, 0);
delay(500);
analogWrite(RGBLED_B, 255);
delay(500);
analogWrite(RGBLED_B, 0);
delay(500);
analogWrite(RGBLED_B, 255);
delay(500);
rtc_gpio_pullup_en(KEY_DST);
rtc_gpio_pulldown_dis(KEY_DST);
esp_deep_sleep_start();
break;
}
}
}
}
if (digitalRead(KEY_4) == LOW) {
Serial.println("Button detected: 4");
button += 8;
atariKeyclick();
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;
ledcWriteTone(LEDC_CHANNEL, 3200);
delay(8);
ledcWriteTone(LEDC_CHANNEL, 0);
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;
ledcWriteTone(LEDC_CHANNEL, 3200);
delay(8);
ledcWriteTone(LEDC_CHANNEL, 0);
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';
analogWrite(LED_A, 0);
analogWrite(LED_B, led_brightness);
} else if (destination == 'B') {
destination = 'C';
analogWrite(LED_B, 0);
analogWrite(LED_C, led_brightness);
} else {
destination = 'A';
analogWrite(LED_C, 0);
analogWrite(LED_A, led_brightness);
}
Serial.print("Destination=");
Serial.println(destination);
/*
digitalWrite(LED_USER, HIGH); // Turn LED on
delay(500); // Keep it on 0.5s
digitalWrite(LED_USER, LOW); // Turn LED off
*/
}
if (button > 0) {
Serial.print(temp, 1);
Serial.print("\t");
Serial.println(hum, 1);
// Debounce delay to avoid multiple triggers
delay(200);
}
// NMEA2000.loop();
// NMEA2000.ParseMessages();
NMEA2000.loop();
NMEA2000.ParseMessages();
if (millis() - lastSensor >= 5000) {
lastSensor = millis();
@@ -544,4 +788,5 @@ void loop() {
Serial.printf("Loop counter: %lu\n", counter);
}
delay(1); // 1ms für freertos
}