433 lines
16 KiB
C++
433 lines
16 KiB
C++
#include "main.h"
|
|
#include "config.h"
|
|
#include "webserver.h"
|
|
#include "hash.h"
|
|
#include "led.h"
|
|
#include <map>
|
|
#include <AsyncTCP.h>
|
|
#include <ArduinoJson.h>
|
|
#include <esp32/clk.h> // for cpu frequency
|
|
#include "esp_rom_uart.h" // for uart wait idle
|
|
#include <Update.h>
|
|
|
|
// TODO
|
|
// - replace StaticJsonDocument by AsyncResponseStream
|
|
// - add lastseen to devices in devicelist
|
|
|
|
// Logging
|
|
static const char* TAG = "WEB";
|
|
|
|
AsyncWebServer server(80);
|
|
uint32_t apiToken = esp_random();
|
|
bool updateSuccess = false;
|
|
String updateError = "";
|
|
|
|
class EmbeddedFile;
|
|
static std::map<String, EmbeddedFile*> embeddedFiles;
|
|
class EmbeddedFile {
|
|
public:
|
|
const uint8_t *start;
|
|
int len;
|
|
String contentType;
|
|
EmbeddedFile(String name, String contentType, const uint8_t *start, int len) {
|
|
this->start = start;
|
|
this->len = len;
|
|
this->contentType = contentType;
|
|
embeddedFiles[name] = this;
|
|
}
|
|
};
|
|
|
|
#define EMBED_GZ_FILE(fileName, binName, contentType) \
|
|
extern const uint8_t binName##_File[] asm("_binary_" #binName "_start"); \
|
|
extern const uint8_t binName##_FileLen[] asm("_binary_" #binName "_size"); \
|
|
const EmbeddedFile binName##_Config(fileName,contentType,(const uint8_t*)binName##_File,(int)binName##_FileLen);
|
|
#include "embeddedfiles.h"
|
|
|
|
void send_embedded_file(String name, AsyncWebServerRequest *request)
|
|
{
|
|
std::map<String, EmbeddedFile*>::iterator it = embeddedFiles.find(name);
|
|
if (it != embeddedFiles.end()) {
|
|
EmbeddedFile* found = it->second;
|
|
AsyncWebServerResponse *response = request->beginResponse(200, found->contentType, found->start, found->len);
|
|
response->addHeader(F("Content-Encoding"), F("gzip"));
|
|
request->send(response);
|
|
} else {
|
|
request->send(404, "text/plain", "Not found");
|
|
}
|
|
}
|
|
|
|
String uptime_with_unit() {
|
|
int64_t uptime = esp_timer_get_time() / 1000000;
|
|
String uptime_unit;
|
|
if (uptime < 120) {
|
|
uptime_unit = " seconds";
|
|
} else {
|
|
if (uptime < 2 * 3600) {
|
|
uptime /= 60;
|
|
uptime_unit = " minutes";
|
|
} else if (uptime < 2 * 3600 * 24) {
|
|
uptime /= 3600;
|
|
uptime_unit = " hours";
|
|
} else {
|
|
uptime /= 86400;
|
|
uptime_unit = " days";
|
|
}
|
|
}
|
|
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
|
|
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
|
|
send_embedded_file("index.html", request);
|
|
});
|
|
|
|
// Route for all other defined pages
|
|
for (auto it = embeddedFiles.begin(); it != embeddedFiles.end(); it++) {
|
|
String uri = String("/") + it->first;
|
|
server.on(uri.c_str(), HTTP_GET, [it](AsyncWebServerRequest *request) {
|
|
send_embedded_file(it->first, request);
|
|
});
|
|
}
|
|
|
|
// API fast hack
|
|
server.on("/api/capabilities", HTTP_GET, [](AsyncWebServerRequest *request) {
|
|
LOGD(TAG, "capabilities called");
|
|
StaticJsonDocument<100> doc;
|
|
doc["apPwChange"] = "true";
|
|
String out;
|
|
serializeJson(doc, out);
|
|
request->send(200, "application/json", out);
|
|
});
|
|
|
|
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;
|
|
//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) {
|
|
LOGD(TAG, "config called");
|
|
StaticJsonDocument<1024> doc;
|
|
doc["systemName"] = config.getString("systemName");
|
|
doc["systemMode"] = String(config.getChar("systemMode"));
|
|
doc["nightMode"] = config.getBool("nightMode") ? "true" : "false";
|
|
doc["instDesc1"] = config.getString("instDesc1");
|
|
doc["instDesc2"] = config.getString("instDesc2");
|
|
doc["logLevel"] = loglevel;
|
|
doc["version"] = VERSION;
|
|
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["stopApTime"] = config.getShort("stopApTime");
|
|
doc["apHidden"] = config.getBool("apHidden") ? "true" : "false";
|
|
doc["cpuSpeed"] = config.getShort("cpuSpeed");
|
|
doc["tempFormat"] = String(config.getChar("tempFormat"));
|
|
doc["ledBrightness"] = config.getShort("ledBrightness");
|
|
doc["rgbBrightness"] = config.getShort("rgbBrightness");
|
|
doc["buzEnable"] = config.getBool("buzEnable") ? "true" : "false";
|
|
doc["buzPower"] = config.getByte("buzPower");
|
|
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];
|
|
doc["key1long"] = longcode[BUTTON_1];
|
|
doc["key2long"] = longcode[BUTTON_2];
|
|
doc["key3long"] = longcode[BUTTON_3];
|
|
doc["key4long"] = longcode[BUTTON_4];
|
|
doc["key5long"] = longcode[BUTTON_5];
|
|
doc["key6long"] = longcode[BUTTON_6];
|
|
doc["n2kSysInst"] = config.getByte("n2kSysInst");
|
|
doc["n2kDevInst"] = config.getByte("n2kDevInst");
|
|
doc["n2kDestA"] = config.getString("n2kDestA");
|
|
doc["switchBankA"] = config.getByte("switchBankA");
|
|
doc["n2kDestB"] = config.getString("n2kDestB");
|
|
doc["switchBankB"] = config.getByte("switchBankB");
|
|
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) {
|
|
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); */
|
|
});
|
|
|
|
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");
|
|
// config.clear();
|
|
response->print("{");
|
|
response->print(R"DELIM({"status": "FAILED"})DELIM");
|
|
response->print("}");
|
|
request->send(response);
|
|
});
|
|
|
|
server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request) {
|
|
StaticJsonDocument<512> doc;
|
|
doc["version"] = VERSION;
|
|
doc["pcbversion"] = PCBVERSION;
|
|
int cpu_freq = esp_clk_cpu_freq() / 1000000;
|
|
doc["cpuspeed"] = String(cpu_freq) + "MHz";
|
|
char ssid[13];
|
|
snprintf(ssid, 13, "%04X%08X", (uint16_t)(chipid >> 32), (uint32_t)chipid);
|
|
doc["ssid"] = ssid;
|
|
doc["fwtype"] = FIRMWARE_TYPE; // TODO ?
|
|
doc["chipid"] = CONFIG_IDF_FIRMWARE_CHIP_ID; // IDF chipid NOT device!
|
|
doc["uptime"] = uptime_with_unit();
|
|
doc["heap"]=(long)xPortGetFreeHeapSize();
|
|
doc["temp"] = String(temp, 1);
|
|
doc["hum"] = String(hum, 1);
|
|
switch (globalmode) {
|
|
case 'K':
|
|
doc["mode"] = "Keyboard";
|
|
break;
|
|
case 'A':
|
|
doc["mode"] = "Autopilot";
|
|
break;
|
|
case 'L':
|
|
doc["mode"] = "Logbook";
|
|
break;
|
|
default:
|
|
doc["mode"] = "*unknown*";
|
|
}
|
|
doc["n2kDestA"] = config.getString("n2kDestA");
|
|
doc["n2kDestB"] = config.getString("n2kDestB");
|
|
doc["n2kDestC"] = config.getString("n2kDestC");
|
|
|
|
Nmea2kTwai::Status n2kState = NMEA2000.getStatus();
|
|
Nmea2kTwai::STATE driverState = n2kState.state;
|
|
doc["n2kstate"] = NMEA2000.stateStr(driverState);
|
|
doc["n2knode"] = NMEA2000.GetN2kSource();
|
|
|
|
// use timeBucket of 8s
|
|
char salt[9];
|
|
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);
|
|
request->send(200, "application/json", out);
|
|
});
|
|
|
|
server.on("/api/fwinfo", HTTP_GET, [](AsyncWebServerRequest *request) {
|
|
StaticJsonDocument<200> doc;
|
|
doc["version"] = VERSION;
|
|
doc["build_date"] = BUILD_DATE;
|
|
doc["build_time"] = BUILD_TIME;
|
|
doc["idf"] = IDF_VERSION;
|
|
String out;
|
|
serializeJson(doc, out);
|
|
request->send(200, "application/json", out);
|
|
});
|
|
|
|
server.on("/api/setconfig", HTTP_POST, [](AsyncWebServerRequest *request) {
|
|
LOGD(TAG, "setconfig called");
|
|
// TODO _hash must be first parameter
|
|
int count = request->params();
|
|
bool need_save = false;
|
|
LOGD(TAG, "setconfig Received %d params", count);
|
|
for (int i = 0; i < count; i++) {
|
|
const AsyncWebParameter* p = request->getParam(i);
|
|
if (!config.hasKey(p->name().c_str())) {
|
|
LOGD(TAG, "POST %s=%s; key not found!", p->name(), p->value());
|
|
continue;
|
|
}
|
|
LOGD(TAG, "POST %s=%s", p->name(), p->value().c_str());
|
|
// get old value for comparison
|
|
ConfigValue oldval = config.get(p->name().c_str());
|
|
config.setValue(p->name().c_str(), p->value().c_str());
|
|
// check if value was changed
|
|
if (config.get(p->name().c_str()) == oldval) {
|
|
// comparison of variants!
|
|
LOGD(TAG, "Values are equal. No change detected.");
|
|
} else {
|
|
need_save = true;
|
|
}
|
|
}
|
|
if (need_save) {
|
|
LOGI(TAG, "Writing changed values to NVS");
|
|
config.save();
|
|
} else {
|
|
LOGI(TAG, "No changes, no action taken");
|
|
}
|
|
StaticJsonDocument<100> doc;
|
|
doc["status"] = "OK";
|
|
String out;
|
|
serializeJson(doc, out);
|
|
request->send(200, "application/json", out);
|
|
});
|
|
|
|
server.on("/api/update", HTTP_POST, [](AsyncWebServerRequest *request) {
|
|
// the request handler is triggered after the upload has finished...
|
|
// create the response, add header, and send response
|
|
LOGD(TAG, "Firmware update finished");
|
|
StaticJsonDocument<100> doc;
|
|
if (updateSuccess) {
|
|
doc["status"] = "OK";
|
|
doc["message"] = "Update sucessfull, rebooting...";
|
|
// Wait for TCP ACK to ensure response is received completely
|
|
request->client()->onAck([](void* arg, AsyncClient* c, size_t len, uint32_t time) {
|
|
ESP.restart();
|
|
});
|
|
} else {
|
|
doc["status"] = "FAILED";
|
|
doc["message"] = updateError;
|
|
}
|
|
String out;
|
|
serializeJson(doc, out);
|
|
request->send(200, "application/json", out);
|
|
}, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
|
// this is the new image upload part
|
|
if (index == 0) {
|
|
LOGI(TAG, "Retrieving firmware image named: %s", filename.c_str());
|
|
if (request->hasParam("md5", true)) {
|
|
String md5 = request->getParam("md5", true)->value();
|
|
LOGI(TAG, "MD5 hash: %s", md5.c_str());
|
|
Update.setMD5(md5.c_str());
|
|
}
|
|
if (! Update.begin(UPDATE_SIZE_UNKNOWN)) {
|
|
Update.printError(Serial);
|
|
updateError = "Update.begin() failed";
|
|
}
|
|
}
|
|
if (Update.write(data, len) != len) {
|
|
Update.printError(Serial);
|
|
updateError = "Update.write() failed";
|
|
}
|
|
if (final) {
|
|
if (Update.end(true)) {
|
|
updateSuccess = true;
|
|
} else {
|
|
Update.printError(Serial);
|
|
updateError = "Update.end() failed";
|
|
}
|
|
}
|
|
});
|
|
|
|
server.on("/api/devicelist", HTTP_GET, [](AsyncWebServerRequest *request) {
|
|
// NMEA2000 device list
|
|
LOGD(TAG, "devicelist called");
|
|
AsyncResponseStream *response = request->beginResponseStream("application/json");
|
|
response->print("[");
|
|
bool first = true;
|
|
for (uint8_t i = 0; i <= 252; i++) {
|
|
const tNMEA2000::tDevice *d = pN2kDeviceList->FindDeviceBySource(i);
|
|
if (d == nullptr) {
|
|
continue;
|
|
}
|
|
uint64_t NAME = d->GetName();
|
|
char hex_name[17];
|
|
snprintf(hex_name, sizeof(hex_name), "%08X%08X", (uint32_t)(NAME >> 32), (uint32_t)(NAME & 0xFFFFFFFF));
|
|
if (!first) {
|
|
response->print(",");
|
|
} else {
|
|
first = false;
|
|
}
|
|
// TODO last seen?
|
|
uint16_t mfcode = d->GetManufacturerCode();
|
|
response->printf(R"DELIM({"source":%d,"name":"%s","manufcode":%d,"model":"%s","manufname":"%s"})DELIM",
|
|
i, hex_name, mfcode, d->GetModelID(), NMEA2000.GetManufacturerName(mfcode));
|
|
}
|
|
response->print("]");
|
|
request->send(response);
|
|
});
|
|
|
|
server.on("/api/dyndevlist", HTTP_GET, [](AsyncWebServerRequest *request) {
|
|
// NMEA2000 dynmic config list: devices
|
|
AsyncResponseStream *response = request->beginResponseStream("application/json");
|
|
response->print("[");
|
|
bool first = true;
|
|
for (uint8_t i = 0; i <= 252; i++) {
|
|
const tNMEA2000::tDevice *d = pN2kDeviceList->FindDeviceBySource(i);
|
|
if (d == nullptr) {
|
|
continue;
|
|
}
|
|
uint64_t NAME = d->GetName();
|
|
char hex_name[17];
|
|
snprintf(hex_name, sizeof(hex_name), "%08X%08X", (uint32_t)(NAME >> 32), (uint32_t)(NAME & 0xFFFFFFFF));
|
|
if (!first) {
|
|
response->print(",");
|
|
} else {
|
|
first = false;
|
|
}
|
|
uint16_t mfcode = d->GetManufacturerCode();
|
|
response->printf(R"DELIM({"v":"%s","l":"%s - %s (%d)"})DELIM",
|
|
hex_name, d->GetModelID(), NMEA2000.GetManufacturerName(mfcode), mfcode);
|
|
}
|
|
response->print("]");
|
|
request->send(response);
|
|
});
|
|
|
|
}
|