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
- Deep sleep and reset can be activated from configuration mode
- Configuration via web interface
- Firmware update via web interface
- Buzzer for key press feedback
passive, allowing tones to be programmed via PWM
- I²C temperature/humidity sensor SHT31
- Light sensor, e.g. for automatic LED dimming
Later options
- Brightness sensor, e.g. for automatic LED dimming
Reassign pins?:
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
permanent illumination and only brief flashing on actuation:
@@ -63,6 +57,9 @@ Connections
To connect LED PCB
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
2x 4pin-pin female headers
qwiic-connector (JST_SH_BM04B-SRSS-TB_04x1.00mm)
@@ -83,10 +80,11 @@ Connections
Notes
-----
With the currently used pre-wired buttons, the connection wires are
extremely delicate. Easy to break and poor workmanship.
Do not use used pre-wired buttons, the connection wires are extremely
delicate. Easy to break and poor workmanship.
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.
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 Nano can be operated in two different mapping modes!
KEY Color Pin Remarks
----- ------- -------- --------------------
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
The pin assignments are defined in main.h.
Bill of materials (WIP)
-----------------

View File

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

View File

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

View File

@@ -220,6 +220,13 @@ void Config::dump() {
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>
T Config::get(const char* key) const {
return std::get<T>(values.at(key));

View File

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

View File

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

View File

@@ -44,15 +44,6 @@
"description": "Log level at the USB port.\nHigher level means more output.",
"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",
"label": "Use Admin-Pass",
@@ -61,6 +52,18 @@
"description": "A password for configuration modifications is required.",
"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",
"label": "Description 1",

View File

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

View File

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