More work on webserver

This commit is contained in:
2026-03-07 19:32:24 +01:00
parent 90d5261670
commit 99668574b3
9 changed files with 119 additions and 80 deletions

48
README
View File

@@ -6,22 +6,16 @@ OBP Keypad 6/1
- Configuration mode via long key press (>3 s) on DST - Configuration mode via long key press (>3 s) on DST
- Deep sleep and reset can be activated from configuration mode - Deep sleep and reset can be activated from configuration mode
- Configuration via web interface - Configuration via web interface
- Firmware update via web interface
- Buzzer for key press feedback - Buzzer for key press feedback
passive, allowing tones to be programmed via PWM passive, allowing tones to be programmed via PWM
- I²C temperature/humidity sensor SHT31 - I²C temperature/humidity sensor SHT31
- Light sensor, e.g. for automatic LED dimming
Later options Later options
- Brightness sensor, e.g. for automatic LED dimming - Brightness sensor, e.g. for automatic LED dimming
Reassign pins?: Reassign pins?:
I²C -> D0, D1 (GPIO 44, 43) I²C -> D0, D1 (GPIO 44, 43)
Reorder LEDs: A0 to A5 for the 6 LEDs
A6 as analog input for sensor
A7 reserved
- Version 2
- Seatalk 1 connector for Raymarine tiller pilot remote control
- Version X, always optional
- 2.9" ePaper display to show key assignments
also means: much more complex enclosure
To prevent the LEDs from being distracting, switching is possible between To prevent the LEDs from being distracting, switching is possible between
permanent illumination and only brief flashing on actuation: permanent illumination and only brief flashing on actuation:
@@ -63,6 +57,9 @@ Connections
To connect LED PCB To connect LED PCB
JST 2.54 XH 7-pin connector -> LEDs + GND JST 2.54 XH 7-pin connector -> LEDs + GND
To connect light sensor to LED PCB
2 pins for +3.3V and GPIO
For I²C modules For I²C modules
2x 4pin-pin female headers 2x 4pin-pin female headers
qwiic-connector (JST_SH_BM04B-SRSS-TB_04x1.00mm) qwiic-connector (JST_SH_BM04B-SRSS-TB_04x1.00mm)
@@ -83,10 +80,11 @@ Connections
Notes Notes
----- -----
With the currently used pre-wired buttons, the connection wires are Do not use used pre-wired buttons, the connection wires are extremely
extremely delicate. Easy to break and poor workmanship. delicate. Easy to break and poor workmanship.
Better to use buttons without cables. Solid wire with 0.25 mm² seems Better to use buttons without cables. Solid wire with 0.25 mm² seems
best suited, both on the button side and for insertion into the terminal block. best suited, both on the button side and for insertion into the terminal
block.
There are various variants with different spring forces. There are various variants with different spring forces.
Final button selection still pending. Final button selection still pending.
@@ -105,33 +103,7 @@ Do not use the 3.3 V pin. It is intended as an output!
The mapping from Nano pins to GPIOs still needs to be verified. The mapping from Nano pins to GPIOs still needs to be verified.
The Nano can be operated in two different mapping modes! The Nano can be operated in two different mapping modes!
KEY Color Pin Remarks The pin assignments are defined in main.h.
----- ------- -------- --------------------
1 B D2 GPIO5
2 B D3 GPIO6
3 B D4 GPIO7
4 B D5 GPIO8
5 B D6 GPIO9
6 Y D7 GPIO10
DST Y D8 GPIO17 Destination, configuration
LED Pin Remarks
------ ---------- ----------------------
A A0 GPIO1
B A1 GPIO2
C A2 GPIO3
RGB-R A3 GPIO4
RGB-G A6 GPIO13
RGB-B A7 GPIO14
CAN Pin Remarks
------ ---------- ----------------------
TX D9 GPIO18
RX D10 GPIO21
BUZZ Pin Remarks
------ ---------- ----------------------
TBD
Bill of materials (WIP) Bill of materials (WIP)
----------------- -----------------

View File

@@ -37,12 +37,12 @@ static const ConfigDef configdefs[] = {
{"systemMode", ConfigType::CHAR, 'K'}, {"systemMode", ConfigType::CHAR, 'K'},
{"nightMode", ConfigType::BOOL, false}, {"nightMode", ConfigType::BOOL, false},
{"logLevel", ConfigType::BYTE, uint8_t(4)}, {"logLevel", ConfigType::BYTE, uint8_t(4)},
{"adminPassword", ConfigType::STRING, String("obpkp61")}, {"adminPassword", ConfigType::STRING, String(ADMIN_PASS)},
{"useAdminPass", ConfigType::BOOL, true}, {"useAdminPass", ConfigType::BOOL, true},
{"instDesc1", ConfigType::STRING, String("")}, {"instDesc1", ConfigType::STRING, String("")},
{"instDesc2", ConfigType::STRING, String("")}, {"instDesc2", ConfigType::STRING, String("")},
{"apEnable", ConfigType::BOOL, true}, {"apEnable", ConfigType::BOOL, true},
{"apPassword", ConfigType::STRING, String("obpkp61")}, {"apPassword", ConfigType::STRING, String(WIFI_PASS)},
{"apIp", ConfigType::STRING, String("192.168.15.1")}, {"apIp", ConfigType::STRING, String("192.168.15.1")},
{"apMask", ConfigType::STRING, String("255.255.255.0")}, {"apMask", ConfigType::STRING, String("255.255.255.0")},
{"stopApTime", ConfigType::SHORT, int16_t(0)}, {"stopApTime", ConfigType::SHORT, int16_t(0)},
@@ -94,6 +94,7 @@ public:
void loadValue(const char* key); void loadValue(const char* key);
void save(); void save();
void dump(); void dump();
void clear();
bool hasKey(const char* key); bool hasKey(const char* key);

View File

@@ -47,6 +47,9 @@
#define PREF_NAME "nvs" #define PREF_NAME "nvs"
// Generic
#define ADMIN_PASS "obpkp61"
// WIFI AP // WIFI AP
#define WIFI_CHANNEL 9 #define WIFI_CHANNEL 9
#define WIFI_MAX_STA 2 #define WIFI_MAX_STA 2

View File

@@ -220,6 +220,13 @@ void Config::dump() {
LOGI(TAG, "===================================="); LOGI(TAG, "====================================");
} }
void Config::clear() {
LOGI(TAG, "Clearing NVS volume: %s", PREF_NAME);
prefs.begin(PREF_NAME, false);
prefs.clear();
prefs.end();
}
template<typename T> template<typename T>
T Config::get(const char* key) const { T Config::get(const char* key) const {
return std::get<T>(values.at(key)); return std::get<T>(values.at(key));

View File

@@ -623,16 +623,18 @@ void send_sensor_brightness(uint16_t value) {
// device instance 8bits // device instance 8bits
// brightness 0-100%, resolution 0.1% 16bits // brightness 0-100%, resolution 0.1% 16bits
// 3 bytes reserved // 3 bytes reserved
uint16_t n2kvalue = value * 1000UL / 4095; // 0..100%, resolution 0.1
tN2kMsg N2kMsg; tN2kMsg N2kMsg;
N2kMsg.SetPGN(65280); // proprietary PGN N2kMsg.SetPGN(65280); // proprietary PGN
N2kMsg.Priority = 6; N2kMsg.Priority = 6;
// 11bits manuf.-code, 2bits reserved (1), 3bits industry group // 11bits manuf.-code, 2bits reserved (1), 3bits industry group
N2kMsg.Add2ByteUInt((N2K_MANUFACTURERCODE & 0x7FF) | (0x03 << 11) | ((N2K_INDUSTRYGROUP & 0x7) << 13)); N2kMsg.Add2ByteUInt((N2K_MANUFACTURERCODE & 0x7FF) | (0x03 << 11) | ((N2K_INDUSTRYGROUP & 0x7) << 13));
N2kMsg.AddByte(0); // instance not yet used now N2kMsg.AddByte(0); // instance not yet used now
N2kMsg.Add2ByteUInt(value * 1000UL / 4095); // resolution 0.1 N2kMsg.Add2ByteUInt(n2kvalue);
N2kMsg.AddByte(0xFF); //reserved bytes N2kMsg.AddByte(0xFF); //reserved bytes
N2kMsg.AddByte(0xFF); N2kMsg.AddByte(0xFF);
N2kMsg.AddByte(0xFF); N2kMsg.AddByte(0xFF);
LOGI(TAG, "Sending LDR value=%d (%d)", n2kvalue, value);
NMEA2000.SendMsg(N2kMsg); NMEA2000.SendMsg(N2kMsg);
} }
@@ -772,10 +774,9 @@ void loop() {
send_sensor_temphum(temp + 273.15, hum); send_sensor_temphum(temp + 273.15, hum);
#ifdef HARDWARE_V2 #ifdef HARDWARE_V2
// Send brightness to NMEA2000 (proprietary)
int ldrval = analogRead(LDR); int ldrval = analogRead(LDR);
LOGI(TAG, "LDR value =%d", ldrval); send_sensor_brightness(ldrval);
// TODO send brightness to NMEA2000
//send_sensor_brightness(ldrval);
#endif #endif
} }

View File

@@ -76,6 +76,18 @@ String uptime_with_unit() {
return String(uptime) + " " + uptime_unit; return String(uptime) + " " + uptime_unit;
} }
bool check_pass(String hash) {
if (! config.getBool("useAdminPass")) {
return true;
}
char salt[9]; // use to easy get upper case hex
sprintf(salt, "%08X", apiToken + (millis()/1000UL & ~0x7UL));
String passhash = get_sha256(String(salt) + config.getString("adminPassword"));
LOGD(TAG, "check hash: %s", hash.c_str());
LOGD(TAG, "check against: %s", passhash.c_str());
return hash == passhash;
}
void webserver_init() { void webserver_init() {
// Route for root / web page // Route for root / web page
@@ -103,16 +115,23 @@ void webserver_init() {
server.on("/api/checkpass", HTTP_GET, [](AsyncWebServerRequest *request) { server.on("/api/checkpass", HTTP_GET, [](AsyncWebServerRequest *request) {
// hash has to be in sha256 format // hash has to be in sha256 format
AsyncResponseStream *response = request->beginResponseStream("application/json");
LOGD(TAG, "checkpass called"); LOGD(TAG, "checkpass called");
String hash = request->arg("hash"); String hash = request->arg("hash");
StaticJsonDocument<100> doc; //StaticJsonDocument<100> doc;
String passhash = get_sha256(config.getString("adminPassword")); //char salt[9];
LOGD(TAG, "check hash: %s", hash.c_str()); //sprintf(salt, "%08X", apiToken + (millis()/1000UL & ~0x7UL));
LOGD(TAG, "check against: %s", passhash.c_str()); //String passhash = get_sha256(String(salt) + config.getString("adminPassword"));
doc["status"] = hash == passhash ? "OK" : "FAILED"; //LOGD(TAG, "check hash: %s", hash.c_str());
String out; //LOGD(TAG, "check against: %s", passhash.c_str());
serializeJson(doc, out); //doc["status"] = check_pass(hash) ? "OK" : "FAILED";
request->send(200, "application/json", out); //String out;
//serializeJson(doc, out);
//request->send(200, "application/json", out);
response->print(R"({"status":)");
response->print(check_pass(hash) ? R"("OK")" : R"("FAILED")");
response->print("}");
request->send(response);
}); });
server.on("/api/config", HTTP_GET, [](AsyncWebServerRequest *request) { server.on("/api/config", HTTP_GET, [](AsyncWebServerRequest *request) {
@@ -125,12 +144,10 @@ void webserver_init() {
doc["instDesc2"] = config.getString("instDesc2"); doc["instDesc2"] = config.getString("instDesc2");
doc["logLevel"] = loglevel; doc["logLevel"] = loglevel;
doc["version"] = VERSION; doc["version"] = VERSION;
doc["AdminPassword"] = "********";
doc["useAdminPass"] = config.getBool("useAdminPass") ? "true" : "false"; doc["useAdminPass"] = config.getBool("useAdminPass") ? "true" : "false";
doc["apEnable"] = config.getBool("apEnable") ? "true" : "false"; doc["apEnable"] = config.getBool("apEnable") ? "true" : "false";
doc["apIp"] = config.getString("apIp"); doc["apIp"] = config.getString("apIp");
doc["apMask"] = config.getString("apMask"); doc["apMask"] = config.getString("apMask");
doc["apPassword"] = "********";
doc["stopApTime"] = config.getShort("stopApTime"); doc["stopApTime"] = config.getShort("stopApTime");
doc["apHidden"] = config.getBool("apHidden") ? "true" : "false"; doc["apHidden"] = config.getBool("apHidden") ? "true" : "false";
doc["cpuSpeed"] = config.getShort("cpuSpeed"); doc["cpuSpeed"] = config.getShort("cpuSpeed");
@@ -160,31 +177,55 @@ void webserver_init() {
doc["n2kDestC"] = config.getString("n2kDestC"); doc["n2kDestC"] = config.getString("n2kDestC");
doc["switchBankC"] = config.getByte("switchBankC"); doc["switchBankC"] = config.getByte("switchBankC");
doc["envInterval"] = config.getShort("envInterval"); doc["envInterval"] = config.getShort("envInterval");
// TODO needed? Perhaps because entry fields are created by this list
doc["AdminPassword"] = "********";
doc["apPassword"] = "********";
String out; String out;
serializeJson(doc, out); serializeJson(doc, out);
request->send(200, "application/json", out); request->send(200, "application/json", out);
}); });
server.on("/api/reset", HTTP_GET, [](AsyncWebServerRequest *request) { server.on("/api/reset", HTTP_GET, [](AsyncWebServerRequest *request) {
AsyncResponseStream *response = request->beginResponseStream("application/json");
String hash = request->arg("hash");
response->print(R"([{"status":)");
if (check_pass(hash)) {
LOGD(TAG, "reset called"); LOGD(TAG, "reset called");
StaticJsonDocument<100> doc; response->print(R"("OK")");
doc["status"] = "OK"; response->print("}]");
String out;
serializeJson(doc, out);
request->send(200, "application/json", out);
ledcWrite(LEDC_RGBLED_B, 0); // blue config light off ledcWrite(LEDC_RGBLED_B, 0); // blue config light off
led_blink(LEDC_RGBLED_G, 3, 4095, 500); led_blink(LEDC_RGBLED_G, 3, 4095, 500);
esp_rom_uart_tx_wait_idle(0); esp_rom_uart_tx_wait_idle(0);
ESP.restart(); ESP.restart();
}
LOGD(TAG, "reset failed: wrong password");
response->print(R"("FAILED")");
response->print("}]");
/*StaticJsonDocument<100> doc;
doc["status"] = "OK";
String out;
serializeJson(doc, out);
request->send(200, "application/json", out); */
}); });
server.on("/api/resetconfig", HTTP_GET, [](AsyncWebServerRequest *request) { server.on("/api/resetconfig", HTTP_GET, [](AsyncWebServerRequest *request) {
AsyncResponseStream *response = request->beginResponseStream("application/json");
String hash = request->arg("hash");
if (check_pass(hash)) {
LOGD(TAG, "resetconfig: checkpass successful");
} else {
LOGD(TAG, "resetconfig: checkpass failed");
}
LOGD(TAG, "resetconfig called"); LOGD(TAG, "resetconfig called");
StaticJsonDocument<100> doc; // config.clear();
doc["status"] = "FAILED"; response->print("{");
String out; response->print(R"DELIM({"status": "FAILED"})DELIM");
serializeJson(doc, out); response->print("}");
request->send(200, "application/json", out); request->send(response);
}); });
server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request) { server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request) {
@@ -229,6 +270,10 @@ void webserver_init() {
sprintf(salt, "%08X", apiToken + (millis()/1000UL & ~0x7UL)); sprintf(salt, "%08X", apiToken + (millis()/1000UL & ~0x7UL));
doc["salt"] = salt; doc["salt"] = salt;
// security warnings
doc["warnAdminPass"] = config.getString("AdminPassword") == ADMIN_PASS ? "true" : "false";
doc["warnApPass"] = config.getString("apPassword") == WIFI_PASS ? "true" : "false";
doc["status"] = "OK"; doc["status"] = "OK";
String out; String out;
serializeJson(doc, out); serializeJson(doc, out);

View File

@@ -44,15 +44,6 @@
"description": "Log level at the USB port.\nHigher level means more output.", "description": "Log level at the USB port.\nHigher level means more output.",
"category": "System" "category": "System"
}, },
{
"name": "adminPassword",
"label": "Admin Password",
"type": "password",
"default": "esp32admin",
"check": "checkAdminPass",
"description": "Set the password for configuration modifications and firmware upload",
"category": "System"
},
{ {
"name": "useAdminPass", "name": "useAdminPass",
"label": "Use Admin-Pass", "label": "Use Admin-Pass",
@@ -61,6 +52,18 @@
"description": "A password for configuration modifications is required.", "description": "A password for configuration modifications is required.",
"category": "System" "category": "System"
}, },
{
"name": "adminPassword",
"label": "Admin Password",
"type": "password",
"default": "esp32admin",
"check": "checkAdminPass",
"description": "Set the password for configuration modifications and firmware upload",
"category": "System",
"condition": {
"useAdminPass": "true"
}
},
{ {
"name": "instDesc1", "name": "instDesc1",
"label": "Description 1", "label": "Description 1",

View File

@@ -33,6 +33,9 @@ if (!window.isSecureContext) {
<div id="statusPage" class="tabPage"> <div id="statusPage" class="tabPage">
<div id="statusPageContent"> <div id="statusPageContent">
<div class="row hidden">
<span class="value" id="warnAdminPass">OK</span>
</div>
<div class="row"> <div class="row">
<span class="label">Firmware</span> <span class="label">Firmware</span>
<span class="value" id="version">---</span> <span class="value" id="version">---</span>

View File

@@ -68,10 +68,11 @@
} }
function update() { function update() {
let now = (new Date()).getTime(); let now = (new Date()).getTime();
let is_connected = (lastUpdate + 3 * updateInterval) > now;
let ce = document.getElementById('connected'); let ce = document.getElementById('connected');
let cl = document.getElementById('conn_label'); let cl = document.getElementById('conn_label');
if (ce) { if (ce) {
if ((lastUpdate + 3 * updateInterval) > now) { if (is_connected) {
ce.classList.add('ok'); ce.classList.add('ok');
cl.textContent = 'connected'; cl.textContent = 'connected';
} }
@@ -108,6 +109,7 @@
resetForm(); resetForm();
} }
}) })
if (is_connected) {
// check if any dynamic list needs update // check if any dynamic list needs update
for (let l in dynLists) { for (let l in dynLists) {
if (loadDynList(l)) { if (loadDynList(l)) {
@@ -115,6 +117,7 @@
} }
} }
} }
}
function resetForm(ev) { function resetForm(ev) {
getJson(apiPrefix + "/api/config") getJson(apiPrefix + "/api/config")
.then(function (jsonData) { .then(function (jsonData) {
@@ -942,6 +945,7 @@
forEl('#adminPassInput', function (el) { forEl('#adminPassInput', function (el) {
el.value = ''; el.value = '';
}); });
alert("Admin password not cached anymore.");
} }
function ensurePass() { function ensurePass() {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {