diff --git a/README b/README index 93dd7ee..df70e5f 100644 --- a/README +++ b/README @@ -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) ----------------- diff --git a/include/config.h b/include/config.h index b4e564f..76abc82 100644 --- a/include/config.h +++ b/include/config.h @@ -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); diff --git a/include/main.h b/include/main.h index bbbb381..f083dc7 100644 --- a/include/main.h +++ b/include/main.h @@ -47,6 +47,9 @@ #define PREF_NAME "nvs" +// Generic +#define ADMIN_PASS "obpkp61" + // WIFI AP #define WIFI_CHANNEL 9 #define WIFI_MAX_STA 2 diff --git a/src/config.cpp b/src/config.cpp index 0f99e82..994caf8 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -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 T Config::get(const char* key) const { return std::get(values.at(key)); diff --git a/src/main.cpp b/src/main.cpp index d621468..e635b3a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 } diff --git a/src/webserver.cpp b/src/webserver.cpp index 05585d5..6966eb1 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -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); diff --git a/web/config.json b/web/config.json index 913be4c..77f6362 100644 --- a/web/config.json +++ b/web/config.json @@ -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", diff --git a/web/index.html b/web/index.html index e6c6546..31ce0f3 100644 --- a/web/index.html +++ b/web/index.html @@ -33,6 +33,9 @@ if (!window.isSecureContext) {
+
Firmware --- diff --git a/web/index.js b/web/index.js index 996333f..e6c68d9 100644 --- a/web/index.js +++ b/web/index.js @@ -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) {