From 701b52afcbb1c63911b5c1d309885deee3d587f1 Mon Sep 17 00:00:00 2001 From: nightflyer88 Date: Sun, 1 Nov 2020 14:52:24 +0100 Subject: [PATCH] Virtual weights built in --- CG_scale.ino | 2482 +++++++++++++++++++++-------------------- data/index.html | 880 --------------- data/index.html.gz | Bin 0 -> 21612 bytes data/models.html | 531 --------- data/models.html.gz | Bin 0 -> 19978 bytes data/settings.html | 1094 ------------------ data/settings.html.gz | Bin 0 -> 16268 bytes defaults.h | 18 +- settings_ESP8266.h | 6 + settings_WIFI_KIT_8.h | 6 + 10 files changed, 1297 insertions(+), 3720 deletions(-) delete mode 100755 data/index.html create mode 100755 data/index.html.gz delete mode 100755 data/models.html create mode 100755 data/models.html.gz delete mode 100755 data/settings.html create mode 100755 data/settings.html.gz diff --git a/CG_scale.ino b/CG_scale.ino index 6f4392d..92f5b10 100644 --- a/CG_scale.ino +++ b/CG_scale.ino @@ -1,14 +1,17 @@ /* ------------------------------------------------------------------ CG scale - (c) 2019 by M. Lehmann + (c) 2019-2020 by M. Lehmann ------------------------------------------------------------------ */ -#define CGSCALE_VERSION "2.11" +#define CGSCALE_VERSION "2.2" /* ****************************************************************** history: + V2.2 01.11.20 Virtual weights built in + V2.12 07.10.20 bug fixed: LR value was displayed in the wrong display position + Voltage for specified battery types deleted V2.11 18.08.20 code is now compatible with standard OLED displays and original code base (default pw length = 32) V2.1 18.07.20 added support for ESP8266 based Wifi Kit 8 @@ -95,7 +98,7 @@ #include #include #include -#include +#include #include #endif @@ -117,13 +120,19 @@ HX711_ADC LoadCell[] {HX711_ADC(PIN_LOADCELL1_DOUT, PIN_LOADCELL1_PD_SCK), HX711 #if defined(ESP8266) ESP8266WebServer server(80); IPAddress apIP(ip[0], ip[1], ip[2], ip[3]); -ESP8266HTTPUpdateServer httpUpdater; WiFiClientSecure httpsClient; File fsUploadFile; // a File object to temporarily store the received file #endif #include "defaults.h" +struct VirtualWeight { + String name; + float cg; + float weight; + bool enabled = false; +}; + struct Model { float distance[3] = {DISTANCE_X1, DISTANCE_X2, DISTANCE_X3}; #if defined(ESP8266) @@ -131,6 +140,7 @@ struct Model { float targetCGmin = 0; float targetCGmax = 0; uint8_t mechanicsType = 0; + VirtualWeight virtualWeight[MAX_VIRTUAL_WEIGHT]; #endif }; @@ -186,6 +196,88 @@ void(* resetCPU) (void) = 0; void resetCPU() {} #endif + +// convert time to string +char * TimeToString(unsigned long t) +{ + static char str[13]; + int h = t / 3600000; + t = t % 3600000; + int m = t / 60000; + t = t % 60000; + int s = t / 1000; + int ms = t - (s * 1000); + sprintf(str, "%02ld:%02d:%02d.%03d", h, m, s, ms); + return str; +} + + +// Count percentage from cell voltage +int percentBat(float cellVoltage) { + + int result = 0; + int elementCount = DATAPOINTS_PERCENTLIST; + byte batTypeArray = batType - 2; + + for (int i = 0; i < elementCount; i++) { + if (pgm_read_float( &percentList[batTypeArray][i][1]) == 100 ) { + elementCount = i; + break; + } + } + + float cellempty = pgm_read_float( &percentList[batTypeArray][0][0]); + float cellfull = pgm_read_float( &percentList[batTypeArray][elementCount][0]); + + if (cellVoltage >= cellfull) { + result = 100; + } else if (cellVoltage <= cellempty) { + result = 0; + } else { + for (int i = 0; i <= elementCount; i++) { + float curVolt = pgm_read_float(&percentList[batTypeArray][i][0]); + if (curVolt >= cellVoltage && i > 0) { + float lastVolt = pgm_read_float(&percentList[batTypeArray][i - 1][0]); + float curPercent = pgm_read_float(&percentList[batTypeArray][i][1]); + float lastPercent = pgm_read_float(&percentList[batTypeArray][i - 1][1]); + result = float((cellVoltage - lastVolt) / (curVolt - lastVolt)) * (curPercent - lastPercent) + lastPercent; + break; + } + } + } + + return result; +} + + +void printConsole(int t, String msg) { + Serial.print(TimeToString(millis())); + Serial.print(" ["); + switch (t) { + case T_BOOT: + Serial.print("BOOT"); + break; + case T_RUN: + Serial.print("RUN"); + break; + case T_ERROR: + Serial.print("ERROR"); + break; + case T_WIFI: + Serial.print("WIFI"); + break; + case T_UPDATE: + Serial.print("UPDATE"); + break; + case T_HTTPS: + Serial.print("HTTPS"); + break; + } + Serial.print("] "); + Serial.println(msg); +} + + void initOLED() { oledDisplay.begin(); oledDisplayHeight = oledDisplay.getDisplayHeight(); @@ -236,12 +328,12 @@ void initOLED() { } else { oledDisplay.setCursor(20, 64); } - oledDisplay.print(F("(c) 2019 M.Lehmann et al.")); + oledDisplay.print(F("(c) 2020 M.Lehmann")); } while ( oledDisplay.nextPage() ); } -void printOLED(String aLine1, String aLine2, String aLine3 = String("")); +//void printOLED(String aLine1, String aLine2, String aLine3 = String("")); void printOLED(String aLine1, String aLine2, String aLine3) { int ylineHeight = oledDisplayHeight / 3; @@ -253,26 +345,14 @@ void printOLED(String aLine1, String aLine2, String aLine3) { oledDisplay.print(aLine1); oledDisplay.setCursor(0, ylineHeight * 2); oledDisplay.print(aLine2); - if (aLine3 == "") { - oledDisplay.drawLine(0, ylineHeight * 2 + 2, oledDisplayWidth, ylineHeight * 2 + 2); - oledDisplay.setFont(oledFontTiny); - oledDisplay.setCursor(0, oledDisplayHeight); - oledDisplay.print("IP:" + WiFi.localIP().toString()); - String signature = "CG scale: V" + String(CGSCALE_VERSION); - oledDisplay.setCursor(oledDisplayWidth - oledDisplay.getStrWidth(signature.c_str()), oledDisplayHeight); - oledDisplay.print(signature); - } else { - oledDisplay.setCursor(0, oledDisplayHeight); - oledDisplay.print(aLine3); - } + oledDisplay.setCursor(0, oledDisplayHeight); + oledDisplay.print(aLine3); } while ( oledDisplay.nextPage() ); } void printScaleOLED() { // print to display - char buff1[8]; - char buff[12]; - char buff2[8]; + char buff[8]; int pos_weightTotal = 7; int pos_CG_length = 28; if (nLoadcells == 2) { @@ -289,26 +369,26 @@ void printScaleOLED() { if (errMsgCnt == 0) { // print battery if (batType > B_OFF) { - oledDisplay.drawXBMP(48, 1, 12, 6, batteryImage); - float percentVolt = percentBat(batVolt / batCells); - dtostrf(percentVolt, 3, 0, buff); - oledDisplay.drawBox(49, 2, (percentVolt / (100 / 8)), 4); - - oledDisplay.setFont(oledFontSmall); - oledDisplay.setCursor(78 - oledDisplay.getStrWidth(buff), 7); - if (batType > B_VOLT) { - dtostrf(percentVolt, 3, 0, buff); - oledDisplay.print(buff); - oledDisplay.print(F("%/")); + oledDisplay.drawXBMP(88, 1, 12, 6, batteryImage); + if (batType == B_VOLT) { + dtostrf(batVolt, 2, 2, buff); + } else { + dtostrf(batVolt, 3, 0, buff); + oledDisplay.drawBox(89, 2, (batVolt / (100 / 8)), 4); } - dtostrf(batVolt, 2, 2, buff); + oledDisplay.setFont(oledFontSmall); + oledDisplay.setCursor(123 - oledDisplay.getStrWidth(buff), 7); oledDisplay.print(buff); - oledDisplay.print(F("V")); + if (batType == B_VOLT) { + oledDisplay.print(F("V")); + } else { + oledDisplay.print(F("%")); + } } // print total weight oledDisplay.setFont(oledFontNormal); - dtostrf(weightTotal, 7, 1, buff); + dtostrf(weightTotal, 5, 1, buff); if (oledDisplayHeight <= 32) { oledDisplay.setCursor(1, 18); oledDisplay.print(F("M = ")); @@ -320,7 +400,7 @@ void printScaleOLED() { oledDisplay.print(F(" g")); // print CG longitudinal axis - dtostrf(CG_length, 7, 1, buff); + dtostrf(CG_length, 5, 1, buff); if (oledDisplayHeight <= 32) { oledDisplay.setCursor(1, 32); oledDisplay.print(F("CG = ")); @@ -338,9 +418,9 @@ void printScaleOLED() { oledDisplay.print(F("LR=")); dtostrf(CG_trans, 3, 0, buff); } else { + dtostrf(CG_trans, 5, 1, buff); oledDisplay.drawXBMP(2, 47, 18, 18, CGtransImage); oledDisplay.setCursor(93 - oledDisplay.getStrWidth(buff), 64); - dtostrf(CG_trans, 7, 1, buff); } oledDisplay.print(buff); oledDisplay.print(F(" mm")); @@ -357,7 +437,6 @@ void printScaleOLED() { } - #ifdef PIN_TARE_BUTTON void handleTareBtn() { static unsigned long lastTaraBtn = 0; @@ -460,1369 +539,1360 @@ bool getLoadcellError() { } -// Count percentage from cell voltage -int percentBat(float cellVoltage) { +#if defined(ESP8266) - int result = 0; - int elementCount = DATAPOINTS_PERCENTLIST; - byte batTypeArray = batType - 2; +void writeModelData(JsonObject object) { + char buff[8]; + String stringBuff; - for (int i = 0; i < elementCount; i++) { - if (pgm_read_float( &percentList[batTypeArray][i][1]) == 100 ) { - elementCount = i; - break; - } + dtostrf(weightTotal, 5, 1, buff); + stringBuff = buff; + stringBuff.trim(); + object["wt"] = stringBuff; + dtostrf(CG_length, 5, 1, buff); + stringBuff = buff; + stringBuff.trim(); + object["cg"] = stringBuff; + dtostrf(CG_trans, 5, 1, buff); + stringBuff = buff; + stringBuff.trim(); + object["cglr"] = stringBuff; + object["x1"] = model.distance[X1]; + object["x2"] = model.distance[X2]; + object["x3"] = model.distance[X3]; + object["cgmin"] = model.targetCGmin; + object["cgmax"] = model.targetCGmax; + object["mType"] = model.mechanicsType; + + JsonArray virtw = object.createNestedArray("virtual"); + for (int i=0; i < MAX_VIRTUAL_WEIGHT; i++){ + JsonArray virtWeight = virtw.createNestedArray(); + virtWeight.add(model.virtualWeight[i].name); + virtWeight.add(model.virtualWeight[i].cg); + virtWeight.add(model.virtualWeight[i].weight); + virtWeight.add(model.virtualWeight[i].enabled); + } +} + + +// save model to json file +bool saveModelJson(String modelName) { + + if (modelName.length() > MAX_MODELNAME_LENGHT) { + return false; } - float cellempty = pgm_read_float( &percentList[batTypeArray][0][0]); - float cellfull = pgm_read_float( &percentList[batTypeArray][elementCount][0]); + StaticJsonDocument jsonDoc; - if (cellVoltage >= cellfull) { - result = 100; - } else if (cellVoltage <= cellempty) { - result = 0; + if (SPIFFS.exists(MODEL_FILE)) { + // read json file + File f = SPIFFS.open(MODEL_FILE, "r"); + auto error = deserializeJson(jsonDoc, f); + f.close(); + if (error) { + return false; + } + // check if model exists + if (jsonDoc.containsKey(modelName)) { + writeModelData(jsonDoc[modelName]); + } else { + // otherwise create new + writeModelData(jsonDoc.createNestedObject(modelName)); + } + // write to file + if (!error) { + f = SPIFFS.open(MODEL_FILE, "w"); + serializeJson(jsonDoc, f); + f.close(); + } else { + return false; + } } else { - for (int i = 0; i <= elementCount; i++) { - float curVolt = pgm_read_float(&percentList[batTypeArray][i][0]); - if (curVolt >= cellVoltage && i > 0) { - float lastVolt = pgm_read_float(&percentList[batTypeArray][i - 1][0]); - float curPercent = pgm_read_float(&percentList[batTypeArray][i][1]); - float lastPercent = pgm_read_float(&percentList[batTypeArray][i - 1][1]); - result = float((cellVoltage - lastVolt) / (curVolt - lastVolt)) * (curPercent - lastPercent) + lastPercent; - break; - } + // creat new json + writeModelData(jsonDoc.createNestedObject(modelName)); + // write to file + if (!jsonDoc.isNull()) { + File f = SPIFFS.open(MODEL_FILE, "w"); + serializeJson(jsonDoc, f); + f.close(); + } else { + return false; } } - return result; + return true; } +// read model data from json file +bool openModelJson(String modelName) { -void setup() { - - // init serial - Serial.begin(115200); - Serial.println(); - delay(1000); - -#if defined(ESP8266) - printConsole(T_BOOT, "startup CG scale V" + String(CGSCALE_VERSION)); - - // init filesystem - SPIFFS.begin(); - EEPROM.begin(EEPROM_SIZE); - printConsole(T_BOOT, "init filesystem"); -#endif - - // read settings from eeprom - if (EEPROM.read(P_NUMBER_LOADCELLS) != 0xFF) { - nLoadcells = EEPROM.read(P_NUMBER_LOADCELLS); - } + StaticJsonDocument jsonDoc; - for (int i = LC1; i <= LC3; i++) { - if (EEPROM.read(P_DISTANCE_X1 + (i * sizeof(float))) != 0xFF) { - EEPROM.get(P_DISTANCE_X1 + (i * sizeof(float)), model.distance[i]); + if (SPIFFS.exists(MODEL_FILE)) { + // read json file + File f = SPIFFS.open(MODEL_FILE, "r"); + auto error = deserializeJson(jsonDoc, f); + f.close(); + if (error) { + return false; } + // check if model exists + if (jsonDoc.containsKey(modelName)) { + // load parameters from model + model.distance[X1] = jsonDoc[modelName]["x1"]; + model.distance[X2] = jsonDoc[modelName]["x2"]; + model.distance[X3] = jsonDoc[modelName]["x3"]; + model.targetCGmin = jsonDoc[modelName]["cgmin"]; + model.targetCGmax = jsonDoc[modelName]["cgmax"]; + model.mechanicsType = jsonDoc[modelName]["mType"]; - if (EEPROM.read(P_LOADCELL1_CALIBRATION_FACTOR + (i * sizeof(float))) != 0xFF) { - EEPROM.get(P_LOADCELL1_CALIBRATION_FACTOR + (i * sizeof(float)), calFactorLoadcell[i]); + JsonArray virtw = jsonDoc[modelName]["virtual"]; + if(virtw){ + for (int i=0; i < MAX_VIRTUAL_WEIGHT; i++){ + model.virtualWeight[i].name = virtw[i][0].as(); + model.virtualWeight[i].cg = virtw[i][1].as(); + model.virtualWeight[i].weight = virtw[i][2].as(); + model.virtualWeight[i].enabled = virtw[i][3].as(); + } + } + } else { + return false; } - } - if (EEPROM.read(P_BAT_TYPE) != 0xFF) { - batType = EEPROM.read(P_BAT_TYPE); - } + // save current model name to eeprom + modelName.toCharArray(model.name, MAX_MODELNAME_LENGHT + 1); + EEPROM.put(P_MODELNAME, model.name); + EEPROM.commit(); - if (EEPROM.read(P_BATT_CELLS) != 0xFF) { - batCells = EEPROM.read(P_BATT_CELLS); + return true; } - if (EEPROM.read(P_REF_WEIGHT) != 0xFF) { - EEPROM.get(P_REF_WEIGHT, refWeight); - } + return false; +} - if (EEPROM.read(P_REF_CG) != 0xFF) { - EEPROM.get(P_REF_CG, refCG); - } - for (int i = R1; i <= R2; i++) { - if (EEPROM.read(P_RESISTOR_R1 + (i * sizeof(float))) != 0xFF) { - EEPROM.get(P_RESISTOR_R1 + (i * sizeof(float)), resistor[i]); +// delete model from json file +bool deleteModelJson(String modelName) { + + StaticJsonDocument jsonDoc; + + if (SPIFFS.exists(MODEL_FILE)) { + // read json file + File f = SPIFFS.open(MODEL_FILE, "r"); + auto error = deserializeJson(jsonDoc, f); + f.close(); + if (error) { + return false; + } + // check if model exists + if (jsonDoc.containsKey(modelName)) { + jsonDoc.remove(modelName); + } else { + return false; + } + // if no models in json, kill it + if (jsonDoc.size() == 0) { + SPIFFS.remove(MODEL_FILE); + } else { + // write to file + if (!jsonDoc.isNull()) { + File f = SPIFFS.open(MODEL_FILE, "w"); + serializeJson(jsonDoc, f); + f.close(); + } else { + return false; + } } + return true; } -#if defined(ESP8266) - if (EEPROM.read(P_SSID_STA) != 0xFF) { - EEPROM.get(P_SSID_STA, ssid_STA); - } + return false; - if (EEPROM.read(P_PASSWORD_STA) != 0xFF) { - EEPROM.get(P_PASSWORD_STA, password_STA); - } +} - if (EEPROM.read(P_SSID_AP) != 0xFF) { - EEPROM.get(P_SSID_AP, ssid_AP); - } - if (EEPROM.read(P_PASSWORD_AP) != 0xFF) { - EEPROM.get(P_PASSWORD_AP, password_AP); +// send headvalues to client +void getHead() { + String response = ssid_AP; + response += "&"; + for (int i = 1; i <= errMsgCnt; i++) { + response += errMsg[i]; } + response += "&"; + response += CGSCALE_VERSION; + response += "&"; + response += gitVersion; + server.send(200, "text/html", response); +} - if (EEPROM.read(P_MODELNAME) != 0xFF) { - EEPROM.get(P_MODELNAME, model.name); - } - if (EEPROM.read(P_ENABLE_UPDATE) != 0xFF) { - EEPROM.get(P_ENABLE_UPDATE, enableUpdate); +// send values to client +void getValue() { + char buff[8]; + String response = ""; + dtostrf(weightTotal, 5, 1, buff); + response += buff; + response += "g&"; + dtostrf(CG_length, 5, 1, buff); + response += buff; + response += "mm&"; + dtostrf(CG_trans, 5, 1, buff); + response += buff; + response += "mm&"; + if (batType == B_VOLT) { + dtostrf(batVolt, 5, 2, buff); + response += buff; + response += "V"; + } else { + dtostrf(batVolt, 5, 0, buff); + response += buff; + response += "%"; } + server.send(200, "text/html", response); +} - if (EEPROM.read(P_ENABLE_OTA) != 0xFF) { - EEPROM.get(P_ENABLE_OTA, enableOTA); - } - // load current model - printConsole(T_BOOT, "open last model"); - if (!openModelJson(model.name)) { - saveModelJson(DEFAULT_NAME); - openModelJson(DEFAULT_NAME); +// send raw values to client +void getRawValue() { + char buff[8]; + String response = ""; + dtostrf(weightLoadCell[LC1], 5, 1, buff); + response += buff; + response += "g&"; + dtostrf(weightLoadCell[LC2], 5, 1, buff); + response += buff; + response += "g&"; + dtostrf(weightLoadCell[LC3], 5, 1, buff); + response += buff; + response += "g&"; + if (batType == B_VOLT) { + dtostrf(batVolt, 5, 2, buff); + response += buff; + response += "V"; + } else { + dtostrf(batVolt, 5, 0, buff); + response += buff; + response += "%"; } + server.send(200, "text/html", response); +} -#endif - // init OLED display - initOLED(); +// send parameters to client +void getParameter() { + char buff[8]; + String response = ""; + float weightTotal_saved = 0; + float CG_length_saved = 0; + float CG_trans_saved = 0; + model.targetCGmin = 0; + model.targetCGmax = 0; - // init & tare Loadcells - for (int i = LC1; i <= LC3; i++) { - if (i < nLoadcells) { - LoadCell[i].begin(); - LoadCell[i].setCalFactor(calFactorLoadcell[i]); -#if defined(ESP8266) - printConsole(T_BOOT, "init Loadcell " + String(i + 1)); -#endif + StaticJsonDocument jsonDoc; + + if (SPIFFS.exists(MODEL_FILE)) { + // read json file + File f = SPIFFS.open(MODEL_FILE, "r"); + auto error = deserializeJson(jsonDoc, f); + f.close(); + // check if model exists + if (!error && jsonDoc.containsKey(model.name)) { + weightTotal_saved = jsonDoc[model.name]["wt"]; + CG_length_saved = jsonDoc[model.name]["cg"]; + CG_trans_saved = jsonDoc[model.name]["cglr"]; + model.targetCGmin = jsonDoc[model.name]["cgmin"]; + model.targetCGmax = jsonDoc[model.name]["cgmax"]; + model.mechanicsType = jsonDoc[model.name]["mType"]; } } - // stabilize scale values - while (millis() < STABILISINGTIME) { - updateLoadcells(); + // parameter list + response += nLoadcells; + response += "&"; + for (int i = X1; i <= X3; i++) { + response += model.distance[i]; + response += "&"; } + response += refWeight; + response += "&"; + response += refCG; + response += "&"; + for (int i = LC1; i <= LC3; i++) { + response += calFactorLoadcell[i]; + response += "&"; + } + for (int i = R1; i <= R2; i++) { + response += resistor[i]; + response += "&"; + } + response += batType; + response += "&"; + response += batCells; + response += "&"; + response += ssid_STA; + response += "&"; + response += password_STA; + response += "&"; + response += ssid_AP; + response += "&"; + response += password_AP; + response += "&"; + response += model.name; + response += "&"; + dtostrf(weightTotal_saved, 5, 1, buff); + response += buff; + response += "g&"; + dtostrf(CG_length_saved, 5, 1, buff); + response += buff; + response += "mm&"; + dtostrf(CG_trans_saved, 5, 1, buff); + response += buff; + response += "mm&"; + response += model.targetCGmin; + response += "&"; + response += model.targetCGmax; + response += "&"; + response += model.mechanicsType; + response += "&"; + response += enableUpdate; + response += "&"; + response += enableOTA; + server.send(200, "text/html", response); +} - tareLoadcells(); - - getLoadcellError(); -#if defined(ESP8266) +// send virtual weights to client +void getVirtualWeight() { + String response = ""; + StaticJsonDocument jsonDoc; - printConsole(T_BOOT, "Wifi: STA mode - connecing with: " + String(ssid_STA)); + JsonArray virtw = jsonDoc.createNestedArray("virtual"); + for (int i=0; i < MAX_VIRTUAL_WEIGHT; i++){ + JsonArray virtWeight = virtw.createNestedArray(); + virtWeight.add(model.virtualWeight[i].name); + virtWeight.add(model.virtualWeight[i].cg); + virtWeight.add(model.virtualWeight[i].weight); + virtWeight.add(model.virtualWeight[i].enabled); + } - // Start by connecting to a WiFi network - WiFi.persistent(false); - WiFi.mode(WIFI_STA); - WiFi.begin(ssid_STA, password_STA); + serializeJson(jsonDoc["virtual"], response); + server.send(200, "text/html", response); +} - long timeoutWiFi = millis(); +// send available WiFi networks to client +void getWiFiNetworks() { + bool ssidSTAavailable = false; + String response = ""; + int n = WiFi.scanNetworks(); - while (WiFi.status() != WL_CONNECTED) { - delay(500); - Serial.print("."); - if (WiFi.status() == WL_NO_SSID_AVAIL) { - printConsole(T_ERROR, "\nWifi: No SSID available"); - break; - } else if (WiFi.status() == WL_CONNECT_FAILED) { - printConsole(T_ERROR, "\nWifi: Connection failed"); - break; - } else if ((millis() - timeoutWiFi) > TIMEOUT_CONNECT) { - printConsole(T_ERROR, "\nWifi: Timeout"); - break; + if (n > 0) { + for (int i = 0; i < n; ++i) { + response += WiFi.SSID(i); + if (WiFi.SSID(i) == ssid_STA) ssidSTAavailable = true; + if (i < n - 1) response += "&"; + } + if (!ssidSTAavailable) { + response += "&"; + response += ssid_STA; } } + server.send(200, "text/html", response); +} - if (WiFi.status() != WL_CONNECTED) { - // if WiFi not connected, switch to access point mode - wifiSTAmode = false; - printConsole(T_BOOT, "Wifi: AP mode - create access point: " + String(ssid_AP)); - WiFi.mode(WIFI_AP); - WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0)); - WiFi.softAP(ssid_AP, password_AP); - printConsole(T_RUN, "Wifi: Connected, IP: " + String(WiFi.softAPIP().toString())); - } else { - printConsole(T_RUN, "Wifi: Connected, IP: " + String(WiFi.localIP().toString())); - } - +// save parameters +void saveParameter() { + if (server.hasArg("nLoadcells")) nLoadcells = server.arg("nLoadcells").toInt(); + if (server.hasArg("distanceX1")) model.distance[X1] = server.arg("distanceX1").toFloat(); + if (server.hasArg("distanceX2")) model.distance[X2] = server.arg("distanceX2").toFloat(); + if (server.hasArg("distanceX3")) model.distance[X3] = server.arg("distanceX3").toFloat(); + if (server.hasArg("refWeight")) refWeight = server.arg("refWeight").toFloat(); + if (server.hasArg("refCG")) refCG = server.arg("refCG").toFloat(); + if (server.hasArg("calFactorLoadcell1")) calFactorLoadcell[LC1] = server.arg("calFactorLoadcell1").toFloat(); + if (server.hasArg("calFactorLoadcell2")) calFactorLoadcell[LC2] = server.arg("calFactorLoadcell2").toFloat(); + if (server.hasArg("calFactorLoadcell3")) calFactorLoadcell[LC3] = server.arg("calFactorLoadcell3").toFloat(); + if (server.hasArg("resistorR1")) resistor[R1] = server.arg("resistorR1").toFloat(); + if (server.hasArg("resistorR2")) resistor[R2] = server.arg("resistorR2").toFloat(); + if (server.hasArg("batType")) batType = server.arg("batType").toInt(); + if (server.hasArg("batCells")) batCells = server.arg("batCells").toInt(); + if (server.hasArg("ssid_STA")) server.arg("ssid_STA").toCharArray(ssid_STA, MAX_SSID_PW_LENGHT + 1); + if (server.hasArg("password_STA")) server.arg("password_STA").toCharArray(password_STA, MAX_SSID_PW_LENGHT + 1); + if (server.hasArg("ssid_AP")) server.arg("ssid_AP").toCharArray(ssid_AP, MAX_SSID_PW_LENGHT + 1); + if (server.hasArg("password_AP")) server.arg("password_AP").toCharArray(password_AP, MAX_SSID_PW_LENGHT + 1); + if (server.hasArg("mechanicsType")) model.mechanicsType = server.arg("mechanicsType").toInt(); + if (server.hasArg("enableUpdate")) enableUpdate = server.arg("enableUpdate").toInt(); + if (server.hasArg("enableOTA")) enableOTA = server.arg("enableOTA").toInt(); - // Set Hostname - String hostname = "disabled"; -#if ENABLE_MDNS - hostname = ssid_AP; - hostname.replace(" ", ""); - hostname.toLowerCase(); - if (!MDNS.begin(hostname, WiFi.localIP())) { - hostname = "mDNS failed"; - printConsole(T_ERROR, "Wifi: " + hostname); - } else { - hostname += ".local"; - printConsole(T_RUN, "Wifi: " + hostname); + EEPROM.put(P_NUMBER_LOADCELLS, nLoadcells); + for (int i = LC1; i <= LC3; i++) { + EEPROM.put(P_DISTANCE_X1 + (i * sizeof(float)), model.distance[i]); + saveCalFactor(i); } -#endif - - if (wifiSTAmode) { - printOLED("WiFi: " + String(ssid_STA), - "Host: " + String(hostname), - "IP : " + WiFi.localIP().toString()); - } else { - printOLED("WiFi: " + String(ssid_AP), - "Host: " + String(hostname), - "IP : " + WiFi.softAPIP().toString()); + EEPROM.put(P_REF_WEIGHT, refWeight); + EEPROM.put(P_REF_CG, refCG); + for (int i = R1; i <= R2; i++) { + EEPROM.put(P_RESISTOR_R1 + (i * sizeof(float)), resistor[i]); } + EEPROM.put(P_BAT_TYPE, batType); + EEPROM.put(P_BATT_CELLS, batCells); + EEPROM.put(P_SSID_STA, ssid_STA); + EEPROM.put(P_PASSWORD_STA, password_STA); + EEPROM.put(P_SSID_AP, ssid_AP); + EEPROM.put(P_PASSWORD_AP, password_AP); + EEPROM.put(P_ENABLE_UPDATE, enableUpdate); + EEPROM.put(P_ENABLE_OTA, enableOTA); + EEPROM.commit(); - delay(3000); - - // When the client requests data - server.on("/getHead", getHead); - server.on("/getValue", getValue); - server.on("/getRawValue", getRawValue); - server.on("/getParameter", getParameter); - server.on("/getWiFiNetworks", getWiFiNetworks); - server.on("/saveParameter", saveParameter); - server.on("/autoCalibrate", autoCalibrate); - server.on("/tare", runTare); - server.on("/saveModel", saveModel); - server.on("/openModel", openModel); - server.on("/deleteModel", deleteModel); - - // When the client upload file - server.on("/settings.html", HTTP_POST, []() { - server.send(200, "text/plain", ""); - }, handleFileUpload); - - // If the client requests any URI - server.onNotFound([]() { - if (!handleFileRead(server.uri())) - server.send(404, "text/plain", "CGscale Error: 404\n File or URL not Found !"); - }); + if (model.name != "") { + saveModelJson(model.name); + } - // init http updater - httpUpdater.setup(&server); + server.send(200, "text/plain", "saved"); +} - // init webserver - server.begin(); - printConsole(T_RUN, "Webserver is up and running"); - // init OTA (over the air update) - if (enableOTA) { - ArduinoOTA.setHostname(ssid_AP); - ArduinoOTA.setPassword(password_AP); +// calibrate cg scale +void autoCalibrate() { + while (!runAutoCalibrate()); + server.send(200, "text/plain", "Calibration successful"); +} - ArduinoOTA.onStart([]() { - String type; - if (ArduinoOTA.getCommand() == U_FLASH) { - type = "firmware"; - } else { // U_SPIFFS - type = "SPIFFS"; - } - // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() - updateMsg = "Updating " + type; - printConsole(T_UPDATE, type); - }); - ArduinoOTA.onEnd([]() { - updateMsg = "successful.."; - printUpdateProgress(100, 100); - }); +// tare cg scale +void runTare() { + tareLoadcells(); + if (!getLoadcellError()) { + server.send(200, "text/plain", "Tare completed"); + return; + } + server.send(404, "text/plain", "404: Tare failed !"); +} - ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { - printUpdateProgress(progress, total); - }); - ArduinoOTA.onError([](ota_error_t error) { - if (error == OTA_AUTH_ERROR) { - updateMsg = "Auth Failed"; - } else if (error == OTA_BEGIN_ERROR) { - updateMsg = "Begin Failed"; - } else if (error == OTA_CONNECT_ERROR) { - updateMsg = "Connect Failed"; - } else if (error == OTA_RECEIVE_ERROR) { - updateMsg = "Receive Failed"; - } else if (error == OTA_END_ERROR) { - updateMsg = "End Failed"; +// save model +void saveModel() { + if (server.hasArg("modelname")) { + if (server.hasArg("targetCGmin")) model.targetCGmin = server.arg("targetCGmin").toFloat(); + if (server.hasArg("targetCGmax")) model.targetCGmax = server.arg("targetCGmax").toFloat(); + if (server.hasArg("distanceX1")) model.distance[X1] = server.arg("distanceX1").toFloat(); + if (server.hasArg("distanceX2")) model.distance[X2] = server.arg("distanceX2").toFloat(); + if (server.hasArg("distanceX3")) model.distance[X3] = server.arg("distanceX3").toFloat(); + if (server.hasArg("mechanicsType")) model.mechanicsType = server.arg("mechanicsType").toInt(); + if(server.hasArg("virtualWeight")){ + StaticJsonDocument jsonDoc; + String json = server.arg("virtualWeight"); + json.replace("%22", "\""); + deserializeJson(jsonDoc, json); + JsonArray virtw = jsonDoc["virtual"]; + if(virtw){ + for (int i=0; i < MAX_VIRTUAL_WEIGHT; i++){ + model.virtualWeight[i].name = virtw[i][0].as(); + model.virtualWeight[i].weight = virtw[i][1].as(); + model.virtualWeight[i].cg = virtw[i][2].as(); + model.virtualWeight[i].enabled = virtw[i][3].as(); + } } - printUpdateProgress(0, 100); - }); - - ArduinoOTA.begin(); - printConsole(T_RUN, "OTA is up and running"); - } - - // https update - httpsClient.setInsecure(); - if (enableUpdate) { - // check for update - httpsUpdate(PROBE_UPDATE); + } + + if (saveModelJson(server.arg("modelname"))) { + server.send(200, "text/plain", "saved"); + return; + } } - -#endif - + server.send(404, "text/plain", "404: Save model failed !"); } -void loop() { - -#if defined(ESP8266) +// open model +void openModel() { + if (server.hasArg("modelname")) { + if (openModelJson(server.arg("modelname"))) { + server.send(200, "text/plain", "opened"); + return; + } + } + server.send(404, "text/plain", "404: Open model failed !"); +} -#if ENABLE_MDNS - MDNS.update(); -#endif - if (enableOTA) { - ArduinoOTA.handle(); +// delete model +void deleteModel() { + if (server.hasArg("modelname")) { + if (deleteModelJson(server.arg("modelname"))) { + server.send(200, "text/plain", "deleted"); + return; + } } - server.handleClient(); -#endif + server.send(404, "text/plain", "404: Delete model failed !"); +} -#ifdef PIN_TARE_BUTTON - handleTareBtn(); -#endif - updateLoadcells(); +// convert the file extension to the MIME type +String getContentType(String filename) { + if (filename.endsWith(".html")) return "text/html"; + else if (filename.endsWith(".png")) return "text/css"; + else if (filename.endsWith(".css")) return "text/css"; + else if (filename.endsWith(".js")) return "application/javascript"; + else if (filename.endsWith(".map")) return "application/json"; + else if (filename.endsWith(".ico")) return "image/x-icon"; + else if (filename.endsWith(".gz")) return "application/x-gzip"; + return "text/plain"; +} - // update loadcell values - if ((millis() - lastTimeLoadcell) > UPDATE_INTERVAL_LOADCELL) { - lastTimeLoadcell = millis(); - // get Loadcell weights - for (int i = LC1; i <= LC3; i++) { - if (i < nLoadcells) { - weightLoadCell[i] = LoadCell[i].getData(); - // IIR filter - weightLoadCell[i] = weightLoadCell[i] + SMOOTHING_LOADCELL * (lastWeightLoadCell[i] - weightLoadCell[i]); - lastWeightLoadCell[i] = weightLoadCell[i]; - } - } +// send file to the client (if it exists) +bool handleFileRead(String path) { + // If a folder is requested, send the index file + if (path.endsWith("/")) path += "index.html"; + String contentType = getContentType(path); + String pathWithGz = path + ".gz"; + + // If the file exists, either as a compressed archive, or normal + if (SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) { + if (SPIFFS.exists(pathWithGz)) + path += ".gz"; + File file = SPIFFS.open(path, "r"); + size_t sent = server.streamFile(file, contentType); + file.close(); + return true; } + return false; +} - // update display and serial menu - if ((millis() - lastTimeMenu) > UPDATE_INTERVAL_OLED_MENU) { - lastTimeMenu = millis(); +// upload a new file to the SPIFFS +void handleFileUpload() { - // total model weight - weightTotal = weightLoadCell[LC1] + weightLoadCell[LC2] + weightLoadCell[LC3]; - if (weightTotal < MINIMAL_TOTAL_WEIGHT && weightTotal > MINIMAL_TOTAL_WEIGHT * -1) { - weightTotal = 0; - } + HTTPUpload& upload = server.upload(); - if (weightTotal > MINIMAL_CG_WEIGHT) { - // CG longitudinal axis - CG_length = ((weightLoadCell[LC2] * model.distance[X2]) / weightTotal) + model.distance[X1]; + if (upload.status == UPLOAD_FILE_START) { + String filename = upload.filename; + if (!filename.startsWith("/")) filename = "/" + filename; + if (filename != MODEL_FILE ) server.send(500, "text/plain", "wrong file !"); + // Open the file for writing in SPIFFS (create if it doesn't exist) + fsUploadFile = SPIFFS.open(filename, "w"); + filename = String(); + } else if (upload.status == UPLOAD_FILE_WRITE) { + // Write the received bytes to the file + fsUploadFile.write(upload.buf, upload.currentSize); + } else if (upload.status == UPLOAD_FILE_END) { + // If the file was successfully created + if (fsUploadFile) { + fsUploadFile.close(); + // Redirect the client to the success page + server.sendHeader("Location", "/settings.html"); + server.send(303); + } else { + server.send(500, "text/plain", "500: couldn't create file"); + } + } -#if defined(ESP8266) - if (model.mechanicsType == 2) { - CG_length = ((weightLoadCell[LC2] * model.distance[X2]) / weightTotal) - model.distance[X1]; - } else if (model.mechanicsType == 3) { - CG_length = ((weightLoadCell[LC2] * model.distance[X2]) / weightTotal) * -1 + model.distance[X1]; - } -#endif +} - // CG transverse axis - if (nLoadcells == 3) { - CG_trans = (model.distance[X3] / 2) - (((weightLoadCell[LC1] + weightLoadCell[LC2] / 2) * model.distance[X3]) / weightTotal); - } - } else { - CG_length = 0; - CG_trans = 0; - } +// print update progress screen +void printUpdateProgress(unsigned int progress, unsigned int total) { + printConsole(T_UPDATE, updateMsg); - // read battery voltage - if (batType > B_OFF) { - batVolt = (analogRead(VOLTAGE_PIN) / 1024.0) * V_REF * ((resistor[R1] + resistor[R2]) / resistor[R2]) / 1000.0; - } + oledDisplay.firstPage(); + do { + oledDisplay.setFont(oledFontSmall); + oledDisplay.setCursor(0, 12); + oledDisplay.print(updateMsg); - printScaleOLED(); + oledDisplay.setCursor(107, 35); + oledDisplay.printf("%u%%\r", (progress / (total / 100))); - // serial connection - if (Serial) { - if (Serial.available() > 0) { + oledDisplay.drawFrame(0, 40, 128, 10); + oledDisplay.drawBox(0, 40, (progress / (total / 128)), 10); - switch (menuPage) - { - case MENU_HOME: - menuPage = Serial.parseInt(); - updateMenu = true; - break; - case MENU_LOADCELLS: - nLoadcells = Serial.parseInt(); - EEPROM.put(P_NUMBER_LOADCELLS, nLoadcells); -#if defined(ESP8266) - EEPROM.commit(); -#endif - menuPage = 0; - updateMenu = true; - break; - case MENU_DISTANCE_X1 ... MENU_DISTANCE_X3: - model.distance[menuPage - MENU_DISTANCE_X1] = Serial.parseFloat(); - EEPROM.put(P_DISTANCE_X1 + ((menuPage - MENU_DISTANCE_X1) * sizeof(float)), model.distance[menuPage - MENU_DISTANCE_X1]); -#if defined(ESP8266) - EEPROM.commit(); -#endif - menuPage = 0; - updateMenu = true; - break; - case MENU_REF_WEIGHT: - refWeight = Serial.parseFloat(); - EEPROM.put(P_REF_WEIGHT, refWeight); -#if defined(ESP8266) - EEPROM.commit(); -#endif - menuPage = 0; - updateMenu = true; - break; - case MENU_REF_CG: - refCG = Serial.parseFloat(); - EEPROM.put(P_REF_CG, refCG); -#if defined(ESP8266) - EEPROM.commit(); -#endif - menuPage = 0; - updateMenu = true; - break; - case MENU_AUTO_CALIBRATE: - if (Serial.read() == 'J') { - runAutoCalibrate(); - } - menuPage = 0; - updateMenu = true; - break; - case MENU_LOADCELL1_CALIBRATION_FACTOR ... MENU_LOADCELL3_CALIBRATION_FACTOR: - calFactorLoadcell[menuPage - MENU_LOADCELL1_CALIBRATION_FACTOR] = Serial.parseFloat(); - saveCalFactor(menuPage - MENU_LOADCELL1_CALIBRATION_FACTOR); - menuPage = 0; - updateMenu = true; - break; - case MENU_RESISTOR_R1 ... MENU_RESISTOR_R2: - resistor[menuPage - MENU_RESISTOR_R1] = Serial.parseFloat(); - EEPROM.put(P_RESISTOR_R1 + ((menuPage - MENU_RESISTOR_R1) * sizeof(float)), resistor[menuPage - MENU_RESISTOR_R1]); -#if defined(ESP8266) - EEPROM.commit(); -#endif - menuPage = 0; - updateMenu = true; - break; - case MENU_BATTERY_MEASUREMENT: - batType = Serial.parseInt(); - EEPROM.put(P_BAT_TYPE, batType); -#if defined(ESP8266) - EEPROM.commit(); -#endif - menuPage = 0; - updateMenu = true; - break; - case MENU_BATTERY_CELLS: - batCells = Serial.parseInt(); - EEPROM.put(P_BATT_CELLS, batCells); -#if defined(ESP8266) - EEPROM.commit(); -#endif - menuPage = 0; - updateMenu = true; - break; - case MENU_RESET_DEFAULT: - if (Serial.read() == 'J') { - // reset eeprom - for (int i = 0; i < EEPROM_SIZE; i++) { - EEPROM.write(i, 0xFF); - } - Serial.end(); -#if defined(ESP8266) - EEPROM.commit(); - // delete json model file - if (SPIFFS.exists(MODEL_FILE)) { - SPIFFS.remove(MODEL_FILE); - } -#endif - resetCPU(); - } - menuPage = 0; - updateMenu = true; - break; - default: - Serial.readString(); - menuPage = 0; - updateMenu = true; - break; - } - Serial.readString(); + } while ( oledDisplay.nextPage() ); +} - } - if (!updateMenu) - return; +// https update +bool httpsUpdate(uint8_t command) { + if (!httpsClient.connect(HOST, HTTPS_PORT)) { + printConsole(T_ERROR, "Wifi: connection to GIT failed"); + return false; + } - switch (menuPage) - { - case MENU_HOME: { - Serial.print(F("\n\n********************************************\nCG scale by M.Lehmann et al. - V")); - Serial.print(CGSCALE_VERSION); - Serial.print(F("\n\n")); + const char * headerKeys[] = {"Location"} ; + const size_t numberOfHeaders = 1; - Serial.print(MENU_LOADCELLS); - Serial.print(F(" - Set number of load cells (")); - Serial.print(nLoadcells); - Serial.print(F(")\n")); + HTTPClient https; + https.setUserAgent("cgscale"); + https.setRedirectLimit(0); + https.setFollowRedirects(true); - for (int i = X1; i <= X3; i++) { - Serial.print(MENU_DISTANCE_X1 + i); - Serial.print(F(" - Set distance X")); - Serial.print(i + 1); - Serial.print(F(" (")); - Serial.print(model.distance[i]); - Serial.print(F("mm)\n")); - } + String url = "https://" + String(HOST) + String(URL); + if (https.begin(httpsClient, url)) { + https.collectHeaders(headerKeys, numberOfHeaders); - Serial.print(MENU_REF_WEIGHT); - Serial.print(F(" - Set reference weight (")); - Serial.print(refWeight); - Serial.print(F("g)\n")); + printConsole(T_HTTPS, "GET: " + url); + int httpCode = https.GET(); + if (httpCode > 0) { + // response + if (httpCode == HTTP_CODE_FOUND) { + String newUrl = https.header("Location"); + gitVersion = newUrl.substring(newUrl.lastIndexOf('/') + 2).toFloat(); + if (gitVersion > String(CGSCALE_VERSION).toFloat()) { + printConsole(T_UPDATE, "Firmware update available: V" + String(gitVersion)); + } else { + printConsole(T_UPDATE, "Firmware version found on GitHub: V" + String(gitVersion) + " - current firmware is up to date"); + } + } else if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { + //Serial.println(https.getString()); + } else { + printConsole(T_ERROR, "HTTPS: GET... failed, " + https.errorToString(httpCode)); + https.end(); + return false; + } + } else { + return false; + } + https.end(); + } else { + printConsole(T_ERROR, "Wifi: Unable to connect"); + return false; + } - Serial.print(MENU_REF_CG); - Serial.print(F(" - Set reference CG (")); - Serial.print(refCG); - Serial.print(F("mm)\n")); + return true; +} - Serial.print(MENU_AUTO_CALIBRATE); - Serial.print(F(" - Start autocalibration\n")); +#endif - for (int i = LC1; i <= LC3; i++) { - Serial.print(MENU_LOADCELL1_CALIBRATION_FACTOR + i); - if ((MENU_LOADCELL1_CALIBRATION_FACTOR + i) < 10) Serial.print(F(" ")); - Serial.print(F(" - Set calibration factor of load cell ")); - Serial.print(i + 1); - Serial.print(F(" (")); - Serial.print(calFactorLoadcell[i]); - Serial.print(F(")\n")); - } - for (int i = R1; i <= R2; i++) { - Serial.print(MENU_RESISTOR_R1 + i); - Serial.print(F(" - Set value of resistor R")); - Serial.print(i + 1); - Serial.print(F(" (")); - Serial.print(resistor[i]); - Serial.print(F("ohm)\n")); - } - - Serial.print(MENU_BATTERY_MEASUREMENT); - Serial.print(F(" - Set battery type (")); - Serial.print(battTypName[batType]); - Serial.print(F(")\n")); - - Serial.print(MENU_BATTERY_CELLS); - Serial.print(F(" - Set number of battery cells (")); - Serial.print(batCells); - Serial.print(F(")\n")); +void setup() { - Serial.print(MENU_SHOW_ACTUAL); - Serial.print(F(" - Show actual values\n")); + // init serial + Serial.begin(115200); + Serial.println(); + delay(1000); #if defined(ESP8266) - Serial.print(MENU_WIFI_INFO); - Serial.print(F(" - Show WiFi network info\n")); + printConsole(T_BOOT, "startup CG scale V" + String(CGSCALE_VERSION)); + + // init filesystem + SPIFFS.begin(); + EEPROM.begin(EEPROM_SIZE); + printConsole(T_BOOT, "init filesystem"); #endif - Serial.print(MENU_RESET_DEFAULT); - Serial.print(F(" - Reset to factory defaults\n")); + // read settings from eeprom + if (EEPROM.read(P_NUMBER_LOADCELLS) != 0xFF) { + nLoadcells = EEPROM.read(P_NUMBER_LOADCELLS); + } - Serial.print(F("\n")); - for (int i = 1; i <= errMsgCnt; i++) { - Serial.print(errMsg[i]); - } + for (int i = LC1; i <= LC3; i++) { + if (EEPROM.read(P_DISTANCE_X1 + (i * sizeof(float))) != 0xFF) { + EEPROM.get(P_DISTANCE_X1 + (i * sizeof(float)), model.distance[i]); + } - Serial.print(F("\nPlease choose the menu number:")); + if (EEPROM.read(P_LOADCELL1_CALIBRATION_FACTOR + (i * sizeof(float))) != 0xFF) { + EEPROM.get(P_LOADCELL1_CALIBRATION_FACTOR + (i * sizeof(float)), calFactorLoadcell[i]); + } + } - updateMenu = false; - break; - } - case MENU_LOADCELLS: - Serial.print(F("\n\nNumber of load cells: ")); - Serial.println(nLoadcells); - printNewValueText(); - updateMenu = false; - break; - case MENU_DISTANCE_X1 ... MENU_DISTANCE_X3: - Serial.print("\n\nDistance X"); - Serial.print(menuPage - MENU_DISTANCE_X1 + 1); - Serial.print(F(": ")); - Serial.print(model.distance[menuPage - MENU_DISTANCE_X1]); - Serial.print(F("mm\n")); - printNewValueText(); - updateMenu = false; - break; - case MENU_REF_WEIGHT: - Serial.print(F("\n\nReference weight: ")); - Serial.print(refWeight); - Serial.print(F("g\n")); - printNewValueText(); - updateMenu = false; - break; - case MENU_REF_CG: - Serial.print(F("\n\nReference CG: ")); - Serial.print(refCG); - Serial.print(F("mm\n")); - printNewValueText(); - updateMenu = false; - break; - case MENU_AUTO_CALIBRATE: - Serial.print(F("\n\nPlease put the reference weight on the scale.\nStart auto calibration (J/N)?\n")); - updateMenu = false; - break; - case MENU_LOADCELL1_CALIBRATION_FACTOR ... MENU_LOADCELL3_CALIBRATION_FACTOR: - Serial.print("\n\nCalibration factor of load cell "); - Serial.print(menuPage - MENU_LOADCELL1_CALIBRATION_FACTOR + 1); - Serial.print(F(": ")); - Serial.println(calFactorLoadcell[menuPage - MENU_LOADCELL1_CALIBRATION_FACTOR]); - printNewValueText(); - updateMenu = false; - break; - case MENU_RESISTOR_R1 ... MENU_RESISTOR_R2: - Serial.print(F("\n\nValue of resistor R")); - Serial.print(menuPage - MENU_RESISTOR_R1 + 1); - Serial.print(F(": ")); - Serial.println(resistor[menuPage - MENU_RESISTOR_R1]); - printNewValueText(); - updateMenu = false; - break; - case MENU_BATTERY_MEASUREMENT: { - Serial.print(F("\n\nBattery type: ")); - Serial.println(battTypName[batType]); - for (int i = 0; i < NUMBER_BAT_TYPES; i++) { - Serial.print(i); - Serial.print(" = "); - Serial.println(battTypName[i]); - } - printNewValueText(); - updateMenu = false; - break; - } - case MENU_BATTERY_CELLS: - Serial.print(F("\n\nBattery cells: ")); - Serial.println(batCells); - printNewValueText(); - updateMenu = false; - break; - case MENU_SHOW_ACTUAL: - for (int i = LC1; i <= LC3; i++) { - if (i < nLoadcells) { - Serial.print(F("Lc")); - Serial.print(i + 1); - Serial.print(F(": ")); - Serial.print(weightLoadCell[i]); - Serial.print(F("g ")); - } - } - Serial.print(F("Total weight: ")); - Serial.print(weightTotal); - Serial.print(F("g CG length: ")); - Serial.print(CG_length); - if (nLoadcells == 3) { - Serial.print(F("mm CG trans: ")); - Serial.print(CG_trans); - Serial.print(F("mm")); - } - if (batType > B_OFF) { - Serial.print(F(" Battery:")); - Serial.print(batVolt); - if (batType == B_VOLT) { - Serial.print(F("V")); - } else { - Serial.print(F("%")); - } - } - Serial.println(); - break; -#if defined(ESP8266) - case MENU_WIFI_INFO: - { - Serial.println("\n\n********************************************\nWiFi network information\n"); + if (EEPROM.read(P_BAT_TYPE) != 0xFF) { + batType = EEPROM.read(P_BAT_TYPE); + } - Serial.println("# Current WiFi status:"); - WiFi.printDiag(Serial); - if (wifiSTAmode == false) { - Serial.print("Connected clients: "); - Serial.println(WiFi.softAPgetStationNum()); - } + if (EEPROM.read(P_BATT_CELLS) != 0xFF) { + batCells = EEPROM.read(P_BATT_CELLS); + } - Serial.println("\n# Available WiFi networks:"); - int wifiCnt = WiFi.scanNetworks(); - if (wifiCnt == 0) { - Serial.println("no networks found"); - } else { - for (int i = 0; i < wifiCnt; ++i) { - // Print SSID and RSSI for each network found - Serial.print(i + 1); - Serial.print(": "); - Serial.print(WiFi.SSID(i)); - Serial.print(" ("); - Serial.print(WiFi.RSSI(i)); - Serial.print("dBm) "); - switch (WiFi.encryptionType(i)) { - case ENC_TYPE_WEP: - Serial.print("WEP"); - break; - case ENC_TYPE_TKIP: - Serial.print("WPA"); - break; - case ENC_TYPE_CCMP: - Serial.print("WPA2"); - break; - case ENC_TYPE_AUTO: - Serial.print("Auto"); - break; - } - Serial.println(""); - } - } - } - updateMenu = false; - break; -#endif - case MENU_RESET_DEFAULT: - Serial.print(F("\n\nReset to factory defaults (J/N)?\n")); - updateMenu = false; - break; - } + if (EEPROM.read(P_REF_WEIGHT) != 0xFF) { + EEPROM.get(P_REF_WEIGHT, refWeight); + } - } else { - updateMenu = true; + if (EEPROM.read(P_REF_CG) != 0xFF) { + EEPROM.get(P_REF_CG, refCG); + } + + for (int i = R1; i <= R2; i++) { + if (EEPROM.read(P_RESISTOR_R1 + (i * sizeof(float))) != 0xFF) { + EEPROM.get(P_RESISTOR_R1 + (i * sizeof(float)), resistor[i]); } } -} +#if defined(ESP8266) + if (EEPROM.read(P_SSID_STA) != 0xFF) { + EEPROM.get(P_SSID_STA, ssid_STA); + } + if (EEPROM.read(P_PASSWORD_STA) != 0xFF) { + EEPROM.get(P_PASSWORD_STA, password_STA); + } -#if defined(ESP8266) + if (EEPROM.read(P_SSID_AP) != 0xFF) { + EEPROM.get(P_SSID_AP, ssid_AP); + } -// send headvalues to client -void getHead() { - String response = ssid_AP; - response += "&"; - for (int i = 1; i <= errMsgCnt; i++) { - response += errMsg[i]; + if (EEPROM.read(P_PASSWORD_AP) != 0xFF) { + EEPROM.get(P_PASSWORD_AP, password_AP); } - response += "&"; - response += CGSCALE_VERSION; - response += "&"; - response += gitVersion; - server.send(200, "text/html", response); -} + if (EEPROM.read(P_MODELNAME) != 0xFF) { + EEPROM.get(P_MODELNAME, model.name); + } -// send values to client -void getValue() { - char buff[8]; - String response = ""; - dtostrf(weightTotal, 5, 1, buff); - response += buff; - response += "g&"; - dtostrf(CG_length, 5, 1, buff); - response += buff; - response += "mm&"; - dtostrf(CG_trans, 5, 1, buff); - response += buff; - response += "mm&"; - if (batType == B_VOLT) { - dtostrf(batVolt, 5, 2, buff); - response += buff; - response += "V"; - } else { - float percentVolt = percentBat(batVolt / batCells); - dtostrf(percentVolt, 5, 0, buff); - response += buff; - response += "%/"; - dtostrf(batVolt, 5, 2, buff); - response += buff; - response += "V"; - } - Serial.print("send response: "); - Serial.println(response); - server.send(200, "text/html", response); -} - - -// send raw values to client -void getRawValue() { - char buff[8]; - String response = ""; - dtostrf(weightLoadCell[LC1], 5, 1, buff); - response += buff; - response += "g&"; - dtostrf(weightLoadCell[LC2], 5, 1, buff); - response += buff; - response += "g&"; - dtostrf(weightLoadCell[LC3], 5, 1, buff); - response += buff; - response += "g&"; - if (batType == B_VOLT) { - dtostrf(batVolt, 5, 2, buff); - response += buff; - response += "V"; - } else { - float percentVolt = percentBat(batVolt / batCells); - dtostrf(percentVolt, 5, 0, buff); - response += buff; - response += "%/"; - dtostrf(batVolt, 5, 2, buff); - response += buff; - response += "V"; + if (EEPROM.read(P_ENABLE_UPDATE) != 0xFF) { + EEPROM.get(P_ENABLE_UPDATE, enableUpdate); } - server.send(200, "text/html", response); -} - - -// send parameters to client -void getParameter() { - char buff[8]; - String response = ""; - float weightTotal_saved = 0; - float CG_length_saved = 0; - float CG_trans_saved = 0; - model.targetCGmin = 0; - model.targetCGmax = 0; - - StaticJsonDocument jsonDoc; - if (SPIFFS.exists(MODEL_FILE)) { - // read json file - File f = SPIFFS.open(MODEL_FILE, "r"); - auto error = deserializeJson(jsonDoc, f); - f.close(); - // check if model exists - if (!error && jsonDoc.containsKey(model.name)) { - weightTotal_saved = jsonDoc[model.name]["wt"]; - CG_length_saved = jsonDoc[model.name]["cg"]; - CG_trans_saved = jsonDoc[model.name]["cglr"]; - model.targetCGmin = jsonDoc[model.name]["cgmin"]; - model.targetCGmax = jsonDoc[model.name]["cgmax"]; - model.mechanicsType = jsonDoc[model.name]["mType"]; - } + if (EEPROM.read(P_ENABLE_OTA) != 0xFF) { + EEPROM.get(P_ENABLE_OTA, enableOTA); } - // parameter list - response += nLoadcells; - response += "&"; - for (int i = X1; i <= X3; i++) { - response += model.distance[i]; - response += "&"; - } - response += refWeight; - response += "&"; - response += refCG; - response += "&"; - for (int i = LC1; i <= LC3; i++) { - response += calFactorLoadcell[i]; - response += "&"; - } - for (int i = R1; i <= R2; i++) { - response += resistor[i]; - response += "&"; + // load current model + printConsole(T_BOOT, "open last model"); + if (!openModelJson(model.name)) { + saveModelJson(DEFAULT_NAME); + openModelJson(DEFAULT_NAME); } - response += batType; - response += "&"; - response += batCells; - response += "&"; - response += ssid_STA; - response += "&"; - response += password_STA; - response += "&"; - response += ssid_AP; - response += "&"; - response += password_AP; - response += "&"; - response += model.name; - response += "&"; - dtostrf(weightTotal_saved, 5, 1, buff); - response += buff; - response += "g&"; - dtostrf(CG_length_saved, 5, 1, buff); - response += buff; - response += "mm&"; - dtostrf(CG_trans_saved, 5, 1, buff); - response += buff; - response += "mm&"; - response += model.targetCGmin; - response += "&"; - response += model.targetCGmax; - response += "&"; - response += model.mechanicsType; - response += "&"; - response += enableUpdate; - response += "&"; - response += enableOTA; - server.send(200, "text/html", response); -} +#endif -// send available WiFi networks to client -void getWiFiNetworks() { - bool ssidSTAavailable = false; - String response = ""; - int n = WiFi.scanNetworks(); + // init OLED display + initOLED(); - if (n > 0) { - for (int i = 0; i < n; ++i) { - response += WiFi.SSID(i); - if (WiFi.SSID(i) == ssid_STA) ssidSTAavailable = true; - if (i < n - 1) response += "&"; - } - if (!ssidSTAavailable) { - response += "&"; - response += ssid_STA; + // init & tare Loadcells + for (int i = LC1; i <= LC3; i++) { + if (i < nLoadcells) { + LoadCell[i].begin(); + LoadCell[i].setCalFactor(calFactorLoadcell[i]); +#if defined(ESP8266) + printConsole(T_BOOT, "init Loadcell " + String(i + 1)); +#endif } } - server.send(200, "text/html", response); -} - - -// save parameters -void saveParameter() { - if (server.hasArg("nLoadcells")) nLoadcells = server.arg("nLoadcells").toInt(); - if (server.hasArg("distanceX1")) model.distance[X1] = server.arg("distanceX1").toFloat(); - if (server.hasArg("distanceX2")) model.distance[X2] = server.arg("distanceX2").toFloat(); - if (server.hasArg("distanceX3")) model.distance[X3] = server.arg("distanceX3").toFloat(); - if (server.hasArg("refWeight")) refWeight = server.arg("refWeight").toFloat(); - if (server.hasArg("refCG")) refCG = server.arg("refCG").toFloat(); - if (server.hasArg("calFactorLoadcell1")) calFactorLoadcell[LC1] = server.arg("calFactorLoadcell1").toFloat(); - if (server.hasArg("calFactorLoadcell2")) calFactorLoadcell[LC2] = server.arg("calFactorLoadcell2").toFloat(); - if (server.hasArg("calFactorLoadcell3")) calFactorLoadcell[LC3] = server.arg("calFactorLoadcell3").toFloat(); - if (server.hasArg("resistorR1")) resistor[R1] = server.arg("resistorR1").toFloat(); - if (server.hasArg("resistorR2")) resistor[R2] = server.arg("resistorR2").toFloat(); - if (server.hasArg("batType")) batType = server.arg("batType").toInt(); - if (server.hasArg("batCells")) batCells = server.arg("batCells").toInt(); - if (server.hasArg("ssid_STA")) server.arg("ssid_STA").toCharArray(ssid_STA, MAX_SSID_PW_LENGHT + 1); - if (server.hasArg("password_STA")) server.arg("password_STA").toCharArray(password_STA, MAX_SSID_PW_LENGHT + 1); - if (server.hasArg("ssid_AP")) server.arg("ssid_AP").toCharArray(ssid_AP, MAX_SSID_PW_LENGHT + 1); - if (server.hasArg("password_AP")) server.arg("password_AP").toCharArray(password_AP, MAX_SSID_PW_LENGHT + 1); - if (server.hasArg("mechanicsType")) model.mechanicsType = server.arg("mechanicsType").toInt(); - if (server.hasArg("enableUpdate")) enableUpdate = server.arg("enableUpdate").toInt(); - if (server.hasArg("enableOTA")) enableOTA = server.arg("enableOTA").toInt(); - EEPROM.put(P_NUMBER_LOADCELLS, nLoadcells); - for (int i = LC1; i <= LC3; i++) { - EEPROM.put(P_DISTANCE_X1 + (i * sizeof(float)), model.distance[i]); - saveCalFactor(i); - } - EEPROM.put(P_REF_WEIGHT, refWeight); - EEPROM.put(P_REF_CG, refCG); - for (int i = R1; i <= R2; i++) { - EEPROM.put(P_RESISTOR_R1 + (i * sizeof(float)), resistor[i]); + // stabilize scale values + while (millis() < STABILISINGTIME) { + updateLoadcells(); } - EEPROM.put(P_BAT_TYPE, batType); - EEPROM.put(P_BATT_CELLS, batCells); - EEPROM.put(P_SSID_STA, ssid_STA); - EEPROM.put(P_PASSWORD_STA, password_STA); - EEPROM.put(P_SSID_AP, ssid_AP); - EEPROM.put(P_PASSWORD_AP, password_AP); - EEPROM.put(P_ENABLE_UPDATE, enableUpdate); - EEPROM.put(P_ENABLE_OTA, enableOTA); - EEPROM.commit(); - if (model.name != "") { - saveModelJson(model.name); - } + tareLoadcells(); - server.send(200, "text/plain", "saved"); -} + getLoadcellError(); +#if defined(ESP8266) -// calibrate cg scale -void autoCalibrate() { - while (!runAutoCalibrate()); - server.send(200, "text/plain", "Calibration successful"); -} + printConsole(T_BOOT, "Wifi: STA mode - connecing with: " + String(ssid_STA)); + // Start by connecting to a WiFi network + WiFi.persistent(false); + WiFi.mode(WIFI_STA); + WiFi.begin(ssid_STA, password_STA); -// tare cg scale -void runTare() { - tareLoadcells(); - if (!getLoadcellError()) { - server.send(200, "text/plain", "Tare completed"); - return; - } - server.send(404, "text/plain", "404: Tare failed !"); -} + long timeoutWiFi = millis(); -// save model -void saveModel() { - if (server.hasArg("modelname")) { - if (server.hasArg("targetCGmin")) model.targetCGmin = server.arg("targetCGmin").toFloat(); - if (server.hasArg("targetCGmax")) model.targetCGmax = server.arg("targetCGmax").toFloat(); - if (server.hasArg("distanceX1")) model.distance[X1] = server.arg("distanceX1").toFloat(); - if (server.hasArg("distanceX2")) model.distance[X2] = server.arg("distanceX2").toFloat(); - if (server.hasArg("distanceX3")) model.distance[X3] = server.arg("distanceX3").toFloat(); - if (server.hasArg("mechanicsType")) model.mechanicsType = server.arg("mechanicsType").toInt(); - if (saveModelJson(server.arg("modelname"))) { - server.send(200, "text/plain", "saved"); - return; + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + if (WiFi.status() == WL_NO_SSID_AVAIL) { + printConsole(T_ERROR, "\nWifi: No SSID available"); + break; + } else if (WiFi.status() == WL_CONNECT_FAILED) { + printConsole(T_ERROR, "\nWifi: Connection failed"); + break; + } else if ((millis() - timeoutWiFi) > TIMEOUT_CONNECT) { + printConsole(T_ERROR, "\nWifi: Timeout"); + break; } } - server.send(404, "text/plain", "404: Save model failed !"); -} -// open model -void openModel() { - if (server.hasArg("modelname")) { - if (openModelJson(server.arg("modelname"))) { - server.send(200, "text/plain", "opened"); - return; - } + if (WiFi.status() != WL_CONNECTED) { + // if WiFi not connected, switch to access point mode + wifiSTAmode = false; + printConsole(T_BOOT, "Wifi: AP mode - create access point: " + String(ssid_AP)); + WiFi.mode(WIFI_AP); + WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0)); + WiFi.softAP(ssid_AP, password_AP); + printConsole(T_RUN, "Wifi: Connected, IP: " + String(WiFi.softAPIP().toString())); + } else { + printConsole(T_RUN, "Wifi: Connected, IP: " + String(WiFi.localIP().toString())); } - server.send(404, "text/plain", "404: Open model failed !"); -} -// delete model -void deleteModel() { - if (server.hasArg("modelname")) { - if (deleteModelJson(server.arg("modelname"))) { - server.send(200, "text/plain", "deleted"); - return; - } + // Set Hostname + String hostname = "disabled"; +#if ENABLE_MDNS + hostname = ssid_AP; + hostname.replace(" ", ""); + hostname.toLowerCase(); + if (!MDNS.begin(hostname, WiFi.localIP())) { + hostname = "mDNS failed"; + printConsole(T_ERROR, "Wifi: " + hostname); + } else { + hostname += ".local"; + printConsole(T_RUN, "Wifi: " + hostname); } - server.send(404, "text/plain", "404: Delete model failed !"); -} - +#endif -// convert the file extension to the MIME type -String getContentType(String filename) { - if (filename.endsWith(".html")) return "text/html"; - else if (filename.endsWith(".png")) return "text/css"; - else if (filename.endsWith(".css")) return "text/css"; - else if (filename.endsWith(".js")) return "application/javascript"; - else if (filename.endsWith(".map")) return "application/json"; - else if (filename.endsWith(".ico")) return "image/x-icon"; - else if (filename.endsWith(".gz")) return "application/x-gzip"; - return "text/plain"; -} + if (wifiSTAmode) { + printOLED("WiFi: " + String(ssid_STA), + "Host: " + String(hostname), + "IP: " + WiFi.localIP().toString()); + } else { + printOLED("WiFi: " + String(ssid_AP), + "Host: " + String(hostname), + "IP: " + WiFi.softAPIP().toString()); + } + delay(3000); -// send file to the client (if it exists) -bool handleFileRead(String path) { - // If a folder is requested, send the index file - if (path.endsWith("/")) path += "index.html"; - String contentType = getContentType(path); - String pathWithGz = path + ".gz"; + // When the client requests data + server.on("/getHead", getHead); + server.on("/getValue", getValue); + server.on("/getRawValue", getRawValue); + server.on("/getParameter", getParameter); + server.on("/getWiFiNetworks", getWiFiNetworks); + server.on("/getVirtualWeight", getVirtualWeight); + server.on("/saveParameter", saveParameter); + server.on("/autoCalibrate", autoCalibrate); + server.on("/tare", runTare); + server.on("/saveModel", saveModel); + server.on("/openModel", openModel); + server.on("/deleteModel", deleteModel); - // If the file exists, either as a compressed archive, or normal - if (SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) { - if (SPIFFS.exists(pathWithGz)) - path += ".gz"; - File file = SPIFFS.open(path, "r"); - size_t sent = server.streamFile(file, contentType); - file.close(); - return true; - } + // When the client upload file + server.on("/settings.html", HTTP_POST, []() { + server.send(200, "text/plain", ""); + }, handleFileUpload); - return false; -} + // If the client requests any URI + server.onNotFound([]() { + if (!handleFileRead(server.uri())) + server.send(404, "text/plain", "CGscale Error: 404\n File or URL not Found !"); + }); + // init ElegantOTA + ElegantOTA.begin(&server); -// upload a new file to the SPIFFS -void handleFileUpload() { + // init webserver + server.begin(); + printConsole(T_RUN, "Webserver is up and running"); - HTTPUpload& upload = server.upload(); + // init OTA (over the air update) + if (enableOTA) { + ArduinoOTA.setHostname(ssid_AP); + ArduinoOTA.setPassword(password_AP); - if (upload.status == UPLOAD_FILE_START) { - String filename = upload.filename; - if (!filename.startsWith("/")) filename = "/" + filename; - if (filename != MODEL_FILE ) server.send(500, "text/plain", "wrong file !"); - // Open the file for writing in SPIFFS (create if it doesn't exist) - fsUploadFile = SPIFFS.open(filename, "w"); - filename = String(); - } else if (upload.status == UPLOAD_FILE_WRITE) { - // Write the received bytes to the file - fsUploadFile.write(upload.buf, upload.currentSize); - } else if (upload.status == UPLOAD_FILE_END) { - // If the file was successfully created - if (fsUploadFile) { - fsUploadFile.close(); - // Redirect the client to the success page - server.sendHeader("Location", "/settings.html"); - server.send(303); - } else { - server.send(500, "text/plain", "500: couldn't create file"); - } - } + ArduinoOTA.onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) { + type = "firmware"; + } else { // U_SPIFFS + type = "SPIFFS"; + } + // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() + updateMsg = "Updating " + type; + printConsole(T_UPDATE, type); + }); -} + ArduinoOTA.onEnd([]() { + updateMsg = "successful.."; + printUpdateProgress(100, 100); + }); + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + printUpdateProgress(progress, total); + }); -// save model to json file -bool saveModelJson(String modelName) { + ArduinoOTA.onError([](ota_error_t error) { + if (error == OTA_AUTH_ERROR) { + updateMsg = "Auth Failed"; + } else if (error == OTA_BEGIN_ERROR) { + updateMsg = "Begin Failed"; + } else if (error == OTA_CONNECT_ERROR) { + updateMsg = "Connect Failed"; + } else if (error == OTA_RECEIVE_ERROR) { + updateMsg = "Receive Failed"; + } else if (error == OTA_END_ERROR) { + updateMsg = "End Failed"; + } + printUpdateProgress(0, 100); + }); - if (modelName.length() > MAX_MODELNAME_LENGHT) { - return false; + ArduinoOTA.begin(); + printConsole(T_RUN, "OTA is up and running"); } - StaticJsonDocument jsonDoc; - - if (SPIFFS.exists(MODEL_FILE)) { - // read json file - File f = SPIFFS.open(MODEL_FILE, "r"); - auto error = deserializeJson(jsonDoc, f); - f.close(); - if (error) { - return false; - } - // check if model exists - if (jsonDoc.containsKey(modelName)) { - writeModelData(jsonDoc[modelName]); - } else { - // otherwise create new - writeModelData(jsonDoc.createNestedObject(modelName)); - } - // write to file - if (!error) { - f = SPIFFS.open(MODEL_FILE, "w"); - serializeJson(jsonDoc, f); - f.close(); - } else { - return false; - } - } else { - // creat new json - writeModelData(jsonDoc.createNestedObject(modelName)); - // write to file - if (!jsonDoc.isNull()) { - File f = SPIFFS.open(MODEL_FILE, "w"); - serializeJson(jsonDoc, f); - f.close(); - } else { - return false; - } + // https update + httpsClient.setInsecure(); + if (enableUpdate) { + // check for update + httpsUpdate(PROBE_UPDATE); } - return true; -} +#endif +} -// read model data from json file -bool openModelJson(String modelName) { - StaticJsonDocument jsonDoc; +void loop() { - if (SPIFFS.exists(MODEL_FILE)) { - // read json file - File f = SPIFFS.open(MODEL_FILE, "r"); - auto error = deserializeJson(jsonDoc, f); - f.close(); - if (error) { - return false; - } - // check if model exists - if (jsonDoc.containsKey(modelName)) { - // load parameters from model - model.distance[X1] = jsonDoc[modelName]["x1"]; - model.distance[X2] = jsonDoc[modelName]["x2"]; - model.distance[X3] = jsonDoc[modelName]["x3"]; - model.targetCGmin = jsonDoc[modelName]["cgmin"]; - model.targetCGmax = jsonDoc[modelName]["cgmax"]; - model.mechanicsType = jsonDoc[modelName]["mType"]; - } else { - return false; - } +#if defined(ESP8266) - // save current model name to eeprom - modelName.toCharArray(model.name, MAX_MODELNAME_LENGHT + 1); - EEPROM.put(P_MODELNAME, model.name); - EEPROM.commit(); +#if ENABLE_MDNS + MDNS.update(); +#endif - return true; + if (enableOTA) { + ArduinoOTA.handle(); } + server.handleClient(); +#endif - return false; -} +#ifdef PIN_TARE_BUTTON + handleTareBtn(); +#endif + updateLoadcells(); -// delete model from json file -bool deleteModelJson(String modelName) { - - StaticJsonDocument jsonDoc; + // update loadcell values + if ((millis() - lastTimeLoadcell) > UPDATE_INTERVAL_LOADCELL) { + lastTimeLoadcell = millis(); - if (SPIFFS.exists(MODEL_FILE)) { - // read json file - File f = SPIFFS.open(MODEL_FILE, "r"); - auto error = deserializeJson(jsonDoc, f); - f.close(); - if (error) { - return false; - } - // check if model exists - if (jsonDoc.containsKey(modelName)) { - jsonDoc.remove(modelName); - } else { - return false; - } - // if no models in json, kill it - if (jsonDoc.size() == 0) { - SPIFFS.remove(MODEL_FILE); - } else { - // write to file - if (!jsonDoc.isNull()) { - File f = SPIFFS.open(MODEL_FILE, "w"); - serializeJson(jsonDoc, f); - f.close(); - } else { - return false; + // get Loadcell weights + for (int i = LC1; i <= LC3; i++) { + if (i < nLoadcells) { + weightLoadCell[i] = LoadCell[i].getData(); + // IIR filter + weightLoadCell[i] = weightLoadCell[i] + SMOOTHING_LOADCELL * (lastWeightLoadCell[i] - weightLoadCell[i]); + lastWeightLoadCell[i] = weightLoadCell[i]; } } - return true; } - return false; - -} - -void writeModelData(JsonObject object) { - char buff[8]; - String stringBuff; - - dtostrf(weightTotal, 5, 1, buff); - stringBuff = buff; - stringBuff.trim(); - object["wt"] = stringBuff; - dtostrf(CG_length, 5, 1, buff); - stringBuff = buff; - stringBuff.trim(); - object["cg"] = stringBuff; - dtostrf(CG_trans, 5, 1, buff); - stringBuff = buff; - stringBuff.trim(); - object["cglr"] = stringBuff; - object["x1"] = model.distance[X1]; - object["x2"] = model.distance[X2]; - object["x3"] = model.distance[X3]; - object["cgmin"] = model.targetCGmin; - object["cgmax"] = model.targetCGmax; - object["mType"] = model.mechanicsType; -} - - -// print update progress screen -void printUpdateProgress(unsigned int progress, unsigned int total) { - printConsole(T_UPDATE, updateMsg); - oledDisplay.firstPage(); - do { - oledDisplay.setFont(oledFontSmall); - oledDisplay.setCursor(0, 12); - oledDisplay.print(updateMsg); + // update display and serial menu + if ((millis() - lastTimeMenu) > UPDATE_INTERVAL_OLED_MENU) { - oledDisplay.setCursor(107, 35); - oledDisplay.printf("%u%%\r", (progress / (total / 100))); + lastTimeMenu = millis(); - oledDisplay.drawFrame(0, 40, 128, 10); - oledDisplay.drawBox(0, 40, (progress / (total / 128)), 10); + // total model weight + weightTotal = weightLoadCell[LC1] + weightLoadCell[LC2] + weightLoadCell[LC3]; + if (weightTotal < MINIMAL_TOTAL_WEIGHT && weightTotal > MINIMAL_TOTAL_WEIGHT * -1) { + weightTotal = 0; + } - } while ( oledDisplay.nextPage() ); -} + if (weightTotal > MINIMAL_CG_WEIGHT) { + // CG longitudinal axis + CG_length = ((weightLoadCell[LC2] * model.distance[X2]) / weightTotal) + model.distance[X1]; +#if defined(ESP8266) + if (model.mechanicsType == 2) { + CG_length = ((weightLoadCell[LC2] * model.distance[X2]) / weightTotal) - model.distance[X1]; + } else if (model.mechanicsType == 3) { + CG_length = ((weightLoadCell[LC2] * model.distance[X2]) / weightTotal) * -1 + model.distance[X1]; + } -// convert time to string -char * TimeToString(unsigned long t) -{ - static char str[13]; - int h = t / 3600000; - t = t % 3600000; - int m = t / 60000; - t = t % 60000; - int s = t / 1000; - int ms = t - (s * 1000); - sprintf(str, "%02ld:%02d:%02d.%03d", h, m, s, ms); - return str; -} + /* Virtual weights -void printConsole(int t, String msg) { - Serial.print(TimeToString(millis())); - Serial.print(" ["); - switch (t) { - case T_BOOT: - Serial.print("BOOT"); - break; - case T_RUN: - Serial.print("RUN"); - break; - case T_ERROR: - Serial.print("ERROR"); - break; - case T_WIFI: - Serial.print("WIFI"); - break; - case T_UPDATE: - Serial.print("UPDATE"); - break; - case T_HTTPS: - Serial.print("HTTPS"); - break; - } - Serial.print("] "); - Serial.println(msg); -} + m = weight + d = cg + d_new=(m1*d1+m2*d2)/(m1+m2) + */ + for (int i=0; i < MAX_VIRTUAL_WEIGHT; i++){ + if(model.virtualWeight[i].enabled == true){ + CG_length = (weightTotal * CG_length + model.virtualWeight[i].weight * model.virtualWeight[i].cg) / (weightTotal + model.virtualWeight[i].weight); + } + } -// https update -bool httpsUpdate(uint8_t command) { - if (!httpsClient.connect(HOST, HTTPS_PORT)) { - printConsole(T_ERROR, "Wifi: connection to GIT failed"); - return false; - } + for (int i=0; i < MAX_VIRTUAL_WEIGHT; i++){ + if(model.virtualWeight[i].enabled == true){ + weightTotal += model.virtualWeight[i].weight; + } + } - const char * headerKeys[] = {"Location"} ; - const size_t numberOfHeaders = 1; + + +#endif - HTTPClient https; - https.setUserAgent("cgscale"); - https.setRedirectLimit(0); - https.setFollowRedirects(true); + // CG transverse axis + if (nLoadcells == 3) { + CG_trans = (model.distance[X3] / 2) - (((weightLoadCell[LC1] + weightLoadCell[LC2] / 2) * model.distance[X3]) / weightTotal); + } - String url = "https://" + String(HOST) + String(URL); - if (https.begin(httpsClient, url)) { - https.collectHeaders(headerKeys, numberOfHeaders); + } else { + CG_length = 0; + CG_trans = 0; + } - printConsole(T_HTTPS, "GET: " + url); - int httpCode = https.GET(); - if (httpCode > 0) { - // response - if (httpCode == HTTP_CODE_FOUND) { - String newUrl = https.header("Location"); - gitVersion = newUrl.substring(newUrl.lastIndexOf('/') + 2).toFloat(); - if (gitVersion > String(CGSCALE_VERSION).toFloat()) { - printConsole(T_UPDATE, "Firmware update available: V" + String(gitVersion)); - } else { - printConsole(T_UPDATE, "Firmware version found on GitHub: V" + String(gitVersion) + " - current firmware is up to date"); - } - } else if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { - Serial.println(https.getString()); - } else { - printConsole(T_ERROR, "HTTPS: GET... failed, " + https.errorToString(httpCode)); - https.end(); - return false; + // read battery voltage + if (batType > B_OFF) { + batVolt = (analogRead(VOLTAGE_PIN) / 1024.0) * V_REF * ((resistor[R1] + resistor[R2]) / resistor[R2]) / 1000.0; +#if ENABLE_PERCENTLIST + if (batType > B_VOLT) { + batVolt = percentBat(batVolt / batCells); } - } else { - return false; +#endif } - https.end(); - } else { - printConsole(T_ERROR, "Wifi: Unable to connect"); - return false; - } - return true; -} + printScaleOLED(); + + // serial connection + if (Serial) { + if (Serial.available() > 0) { + switch (menuPage) + { + case MENU_HOME: + menuPage = Serial.parseInt(); + updateMenu = true; + break; + case MENU_LOADCELLS: + nLoadcells = Serial.parseInt(); + EEPROM.put(P_NUMBER_LOADCELLS, nLoadcells); +#if defined(ESP8266) + EEPROM.commit(); +#endif + menuPage = 0; + updateMenu = true; + break; + case MENU_DISTANCE_X1 ... MENU_DISTANCE_X3: + model.distance[menuPage - MENU_DISTANCE_X1] = Serial.parseFloat(); + EEPROM.put(P_DISTANCE_X1 + ((menuPage - MENU_DISTANCE_X1) * sizeof(float)), model.distance[menuPage - MENU_DISTANCE_X1]); +#if defined(ESP8266) + EEPROM.commit(); +#endif + menuPage = 0; + updateMenu = true; + break; + case MENU_REF_WEIGHT: + refWeight = Serial.parseFloat(); + EEPROM.put(P_REF_WEIGHT, refWeight); +#if defined(ESP8266) + EEPROM.commit(); +#endif + menuPage = 0; + updateMenu = true; + break; + case MENU_REF_CG: + refCG = Serial.parseFloat(); + EEPROM.put(P_REF_CG, refCG); +#if defined(ESP8266) + EEPROM.commit(); #endif + menuPage = 0; + updateMenu = true; + break; + case MENU_AUTO_CALIBRATE: + if (Serial.read() == 'J') { + runAutoCalibrate(); + } + menuPage = 0; + updateMenu = true; + break; + case MENU_LOADCELL1_CALIBRATION_FACTOR ... MENU_LOADCELL3_CALIBRATION_FACTOR: + calFactorLoadcell[menuPage - MENU_LOADCELL1_CALIBRATION_FACTOR] = Serial.parseFloat(); + saveCalFactor(menuPage - MENU_LOADCELL1_CALIBRATION_FACTOR); + menuPage = 0; + updateMenu = true; + break; + case MENU_RESISTOR_R1 ... MENU_RESISTOR_R2: + resistor[menuPage - MENU_RESISTOR_R1] = Serial.parseFloat(); + EEPROM.put(P_RESISTOR_R1 + ((menuPage - MENU_RESISTOR_R1) * sizeof(float)), resistor[menuPage - MENU_RESISTOR_R1]); +#if defined(ESP8266) + EEPROM.commit(); +#endif + menuPage = 0; + updateMenu = true; + break; + case MENU_BATTERY_MEASUREMENT: + batType = Serial.parseInt(); + EEPROM.put(P_BAT_TYPE, batType); +#if defined(ESP8266) + EEPROM.commit(); +#endif + menuPage = 0; + updateMenu = true; + break; + case MENU_BATTERY_CELLS: + batCells = Serial.parseInt(); + EEPROM.put(P_BATT_CELLS, batCells); +#if defined(ESP8266) + EEPROM.commit(); +#endif + menuPage = 0; + updateMenu = true; + break; + case MENU_RESET_DEFAULT: + if (Serial.read() == 'J') { + // reset eeprom + for (int i = 0; i < EEPROM_SIZE; i++) { + EEPROM.write(i, 0xFF); + } + Serial.end(); +#if defined(ESP8266) + EEPROM.commit(); + // delete json model file + if (SPIFFS.exists(MODEL_FILE)) { + SPIFFS.remove(MODEL_FILE); + } +#endif + resetCPU(); + } + menuPage = 0; + updateMenu = true; + break; + default: + Serial.readString(); + menuPage = 0; + updateMenu = true; + break; + } + Serial.readString(); + + } + + if (!updateMenu) + return; + + switch (menuPage) + { + case MENU_HOME: { + Serial.print(F("\n\n********************************************\nCG scale by M.Lehmann et al. - V")); + Serial.print(CGSCALE_VERSION); + Serial.print(F("\n\n")); + + Serial.print(MENU_LOADCELLS); + Serial.print(F(" - Set number of load cells (")); + Serial.print(nLoadcells); + Serial.print(F(")\n")); + + for (int i = X1; i <= X3; i++) { + Serial.print(MENU_DISTANCE_X1 + i); + Serial.print(F(" - Set distance X")); + Serial.print(i + 1); + Serial.print(F(" (")); + Serial.print(model.distance[i]); + Serial.print(F("mm)\n")); + } + + Serial.print(MENU_REF_WEIGHT); + Serial.print(F(" - Set reference weight (")); + Serial.print(refWeight); + Serial.print(F("g)\n")); + + Serial.print(MENU_REF_CG); + Serial.print(F(" - Set reference CG (")); + Serial.print(refCG); + Serial.print(F("mm)\n")); + + Serial.print(MENU_AUTO_CALIBRATE); + Serial.print(F(" - Start autocalibration\n")); + + for (int i = LC1; i <= LC3; i++) { + Serial.print(MENU_LOADCELL1_CALIBRATION_FACTOR + i); + if ((MENU_LOADCELL1_CALIBRATION_FACTOR + i) < 10) Serial.print(F(" ")); + Serial.print(F(" - Set calibration factor of load cell ")); + Serial.print(i + 1); + Serial.print(F(" (")); + Serial.print(calFactorLoadcell[i]); + Serial.print(F(")\n")); + } + + for (int i = R1; i <= R2; i++) { + Serial.print(MENU_RESISTOR_R1 + i); + Serial.print(F(" - Set value of resistor R")); + Serial.print(i + 1); + Serial.print(F(" (")); + Serial.print(resistor[i]); + Serial.print(F("ohm)\n")); + } + + Serial.print(MENU_BATTERY_MEASUREMENT); + Serial.print(F(" - Set battery type (")); + Serial.print(battTypName[batType]); + Serial.print(F(")\n")); + + Serial.print(MENU_BATTERY_CELLS); + Serial.print(F(" - Set number of battery cells (")); + Serial.print(batCells); + Serial.print(F(")\n")); + + Serial.print(MENU_SHOW_ACTUAL); + Serial.print(F(" - Show actual values\n")); + +#if defined(ESP8266) + Serial.print(MENU_WIFI_INFO); + Serial.print(F(" - Show WiFi network info\n")); +#endif + + Serial.print(MENU_RESET_DEFAULT); + Serial.print(F(" - Reset to factory defaults\n")); + + Serial.print(F("\n")); + for (int i = 1; i <= errMsgCnt; i++) { + Serial.print(errMsg[i]); + } + + Serial.print(F("\nPlease choose the menu number:")); + + updateMenu = false; + break; + } + case MENU_LOADCELLS: + Serial.print(F("\n\nNumber of load cells: ")); + Serial.println(nLoadcells); + printNewValueText(); + updateMenu = false; + break; + case MENU_DISTANCE_X1 ... MENU_DISTANCE_X3: + Serial.print("\n\nDistance X"); + Serial.print(menuPage - MENU_DISTANCE_X1 + 1); + Serial.print(F(": ")); + Serial.print(model.distance[menuPage - MENU_DISTANCE_X1]); + Serial.print(F("mm\n")); + printNewValueText(); + updateMenu = false; + break; + case MENU_REF_WEIGHT: + Serial.print(F("\n\nReference weight: ")); + Serial.print(refWeight); + Serial.print(F("g\n")); + printNewValueText(); + updateMenu = false; + break; + case MENU_REF_CG: + Serial.print(F("\n\nReference CG: ")); + Serial.print(refCG); + Serial.print(F("mm\n")); + printNewValueText(); + updateMenu = false; + break; + case MENU_AUTO_CALIBRATE: + Serial.print(F("\n\nPlease put the reference weight on the scale.\nStart auto calibration (J/N)?\n")); + updateMenu = false; + break; + case MENU_LOADCELL1_CALIBRATION_FACTOR ... MENU_LOADCELL3_CALIBRATION_FACTOR: + Serial.print("\n\nCalibration factor of load cell "); + Serial.print(menuPage - MENU_LOADCELL1_CALIBRATION_FACTOR + 1); + Serial.print(F(": ")); + Serial.println(calFactorLoadcell[menuPage - MENU_LOADCELL1_CALIBRATION_FACTOR]); + printNewValueText(); + updateMenu = false; + break; + case MENU_RESISTOR_R1 ... MENU_RESISTOR_R2: + Serial.print(F("\n\nValue of resistor R")); + Serial.print(menuPage - MENU_RESISTOR_R1 + 1); + Serial.print(F(": ")); + Serial.println(resistor[menuPage - MENU_RESISTOR_R1]); + printNewValueText(); + updateMenu = false; + break; + case MENU_BATTERY_MEASUREMENT: { + Serial.print(F("\n\nBattery type: ")); + Serial.println(battTypName[batType]); + for (int i = 0; i < NUMBER_BAT_TYPES; i++) { + Serial.print(i); + Serial.print(" = "); + Serial.println(battTypName[i]); + } + printNewValueText(); + updateMenu = false; + break; + } + case MENU_BATTERY_CELLS: + Serial.print(F("\n\nBattery cells: ")); + Serial.println(batCells); + printNewValueText(); + updateMenu = false; + break; + case MENU_SHOW_ACTUAL: + for (int i = LC1; i <= LC3; i++) { + if (i < nLoadcells) { + Serial.print(F("Lc")); + Serial.print(i + 1); + Serial.print(F(": ")); + Serial.print(weightLoadCell[i]); + Serial.print(F("g ")); + } + } + Serial.print(F("Total weight: ")); + Serial.print(weightTotal); + Serial.print(F("g CG length: ")); + Serial.print(CG_length); + if (nLoadcells == 3) { + Serial.print(F("mm CG trans: ")); + Serial.print(CG_trans); + Serial.print(F("mm")); + } + if (batType > B_OFF) { + Serial.print(F(" Battery:")); + Serial.print(batVolt); + if (batType == B_VOLT) { + Serial.print(F("V")); + } else { + Serial.print(F("%")); + } + } + Serial.println(); + break; +#if defined(ESP8266) + case MENU_WIFI_INFO: + { + Serial.println("\n\n********************************************\nWiFi network information\n"); + + Serial.println("# Current WiFi status:"); + WiFi.printDiag(Serial); + if (wifiSTAmode == false) { + Serial.print("Connected clients: "); + Serial.println(WiFi.softAPgetStationNum()); + } + + Serial.println("\n# Available WiFi networks:"); + int wifiCnt = WiFi.scanNetworks(); + if (wifiCnt == 0) { + Serial.println("no networks found"); + } else { + for (int i = 0; i < wifiCnt; ++i) { + // Print SSID and RSSI for each network found + Serial.print(i + 1); + Serial.print(": "); + Serial.print(WiFi.SSID(i)); + Serial.print(" ("); + Serial.print(WiFi.RSSI(i)); + Serial.print("dBm) "); + switch (WiFi.encryptionType(i)) { + case ENC_TYPE_WEP: + Serial.print("WEP"); + break; + case ENC_TYPE_TKIP: + Serial.print("WPA"); + break; + case ENC_TYPE_CCMP: + Serial.print("WPA2"); + break; + case ENC_TYPE_AUTO: + Serial.print("Auto"); + break; + } + Serial.println(""); + } + } + } + updateMenu = false; + break; +#endif + case MENU_RESET_DEFAULT: + Serial.print(F("\n\nReset to factory defaults (J/N)?\n")); + updateMenu = false; + break; + } + + } else { + updateMenu = true; + } + } +} diff --git a/data/index.html b/data/index.html deleted file mode 100755 index d87608c..0000000 --- a/data/index.html +++ /dev/null @@ -1,880 +0,0 @@ - - - - - - - - CG scale by M. Lehmann - - - - - - - -
-
- -
-
- - -
-
- -
-
- -
- - -
-
- -
-
-
-
-
-
- -
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
- - -
-
- -
-
- min: - -
-
- max: - -
-
- -
-
-
-
-
- -
(c) 2020 M. Lehmann - Version: --
- - - diff --git a/data/index.html.gz b/data/index.html.gz new file mode 100755 index 0000000000000000000000000000000000000000..2cde13c2b029b3ab60426b32385d38c30dd23609 GIT binary patch literal 21612 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%0A}A5<-7+St-O`4z6z%VkUQj{_r)RTd%II+$ z^#(wx+kwla*toAS9?5nhdb=EY49+hiX}DOsglo@-ix4B3b~5o9jqv^HP?V?hZExbl zLd$B2k}>V;GBjvTr$c6H%oYV*XoG;8N?WffBmysVY)z3Sy;NZf-HYgQUg=~6X(&7q zy#9<_mx2$zE9A%++SZ<@%!HGGZd~GJSZdu}N^X1lbV7}HKmah0=S^(Fx_pcC!gX&g zPY8YKBTy2{3}K80G&xL1ict-7Mg{`1GLZ~u)Tz}>-8N|#JWREjOzblQESOGQDksZU zFvyr`uM!d)W6%YjWn;~ql^DWw*6bqNZA4F{a~Yx}VkIuTJ_ zI4Co8OUFXLD0W<;2drw2$hq#a$*7Fr9V@$@-|J!wz25-4JrK)*weum<+gH7r?bve= zXBAb?UlYML+H?_`VDOMLP^Hxmx9OxU9<_k|wr8nJ92PnXm3Av>L3s&lCMlsVa8)6ySNN_2 z;a&@B!oc8Y{7!Qxh>XdWHi5JqtRsD!+4Y*X5)6Zl(AjBPhX_T|o)G-F%rVuo$o5!N z$Ue0&2BAVGlR-xtEZT7xxf90lhLSjjDi%1bE0jzq!692ge?khmFc6?lZ^`et@hsjk z{46dhl5Cap9pyU{&+Vk;rkPrU7Vk?0W$=4e=*$GTQ`WwJXt8tE;$+X_X1IzBLUk#J z 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 literal 0 HcmV?d00001 diff --git a/data/models.html b/data/models.html deleted file mode 100755 index 64ed539..0000000 --- a/data/models.html +++ /dev/null @@ -1,531 +0,0 @@ - - - - - - - - CG scale by M. Lehmann - - - - - - - - -
-
- -
-
- -
-
- -
- - - - - - - - -
-
- -
(c) 2020 M. Lehmann - Version: --
- - - diff --git a/data/models.html.gz b/data/models.html.gz new file mode 100755 index 0000000000000000000000000000000000000000..45371094cd2ad8b69af7a53f8c9f91218af33bf7 GIT binary patch literal 19978 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 - - - - - - - CG scale by M. Lehmann - - - - - - - -
-
- -
-
- -
-
- -
-
-
- - -
-

-
- - - -
-
- - -
-
- - - -
-
- - - -
-
- - -
-
- - -
-

-
-
-
- -
- - - -
- -
-
- - -
- -
-
-
-
- -

-
- -
- -
-
-

-
- -
- -
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
- -
-
- -
-
- - - - - - -
-
- -
-
- - -
-
- - -
-
- - -
-
- -
-
-
-
- - -
-
- - - -
-
- - -
-
- - -
-
- - -
-
- -
-
-
-
-
- -
(c) 2020 M. Lehmann - Version: --
- - - diff --git a/data/settings.html.gz b/data/settings.html.gz new file mode 100755 index 0000000000000000000000000000000000000000..e48baa9f59dd280d003e77bf92e3fedeb3b9e04a GIT binary patch literal 16268 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 Ck2|&i literal 0 HcmV?d00001 diff --git a/defaults.h b/defaults.h index 67def8f..5bf92ad 100644 --- a/defaults.h +++ b/defaults.h @@ -51,15 +51,6 @@ enum { }; -#if defined(ESP8266) -// https update -enum { - PROBE_UPDATE, - UPDATE_FIRMWARE, - UPDATE_SPIFFS -}; - - // console msg type enum { T_BOOT, @@ -69,6 +60,15 @@ enum { T_UPDATE, T_HTTPS }; + + +#if defined(ESP8266) +// https update +enum { + PROBE_UPDATE, + UPDATE_FIRMWARE, + UPDATE_SPIFFS +}; #endif diff --git a/settings_ESP8266.h b/settings_ESP8266.h index ce9f35b..63fedc2 100644 --- a/settings_ESP8266.h +++ b/settings_ESP8266.h @@ -163,5 +163,11 @@ const char ip[4] = {1,2,3,4}; // default IP address +// **** virtual weight settings **** + +#define MAX_VIRTUAL_WEIGHT 10 + + + // **** end of settings **** #warning ESP8266 settings have been loaded diff --git a/settings_WIFI_KIT_8.h b/settings_WIFI_KIT_8.h index 62da088..82485d8 100644 --- a/settings_WIFI_KIT_8.h +++ b/settings_WIFI_KIT_8.h @@ -169,5 +169,11 @@ const char ip[4] = {1,2,3,4}; // default IP address +// **** virtual weight settings **** + +#define MAX_VIRTUAL_WEIGHT 10 + + + // **** end of settings **** #warning WIFI KIT 8 settings have been loaded