/**************************************************************************************************************************** Remote Power/SWR Meter - a solution to remotely measure RF power and VSWR over ethernet For Ethernet shields using WT32_ETH01 (ESP32 + LAN8720) Uses WebServer_WT32_ETH01, a library for the Ethernet LAN8720 in WT32_ETH01 to run WebServer Author: Michael Clemens, DK1MI Licensed under GPLv3 license (see LICENSE.md) VU meter code was taken from https://github.com/tomnomnom/vumeter, credits go to Tom Hudson (https://github.com/tomnomnom) *****************************************************************************************************************************/ #define DEBUG_ETHERNET_WEBSERVER_PORT Serial // Debug Level from 0 to 4 #define _ETHERNET_WEBSERVER_LOGLEVEL_ 3 #define FORMAT_SPIFFS_IF_FAILED true #include #include "javascript.h" #include "dashboard_css.h" #include "config_css.h" #include "index.h" // Main Web page header file #include #include "FS.h" #include "SPIFFS.h" String version = "1.0.0"; Preferences config; Preferences global_config; String band_config_items[] = { "b_show_mV", "b_show_dBm", "b_show_watt", "s_vswr_thresh", "b_vswr_beep", "s_ant_name", "s_max_led_pwr_f", "s_max_led_pwr_r", "s_max_led_vswr", "b_show_led_fwd", "b_show_led_ref", "b_show_led_vswr", "s_cable_loss" }; String band_config_defaults[] = { "true", "true", "true", "2", "true", " ", "100", "100", "3", "true", "true", "true", "0" }; String band_config_nice_names[] = { "Show voltage in mV (yes/no)", "Show power level in dBm (yes/no)", "Show power in Watt (yes/no)", "VSWR threshold that triggers a warning (e.g. 3)", "Beep if VSWR threshold is exceeded (yes/no)", "Name of the antenna", "Max. FWD power displayed by LED bar graph in W (e.g. 100)", "Max. REF power displayed by LED bar graph in W (e.g. 100)", "Max. VSWR displayed by LED bar graph (e.g. 3)", "Show LED graph for FWD power (yes/no)", "Show LED graph for REF power (yes/no)", "Show LED graph for VSWR (yes/no)","Cable loss in db (e.g. 3)" }; double fwd_array[3300] = {}; double ref_array[3300] = {}; int voltage_fwd, voltage_ref; double fwd_dbm = 0, ref_dbm = 0; double fwd_watt = 0, ref_watt = 0; byte iii = 0; String conf_content; String conf_textareas = ""; String conf_config_table = ""; String band = ""; String default_band = "70cm"; String band_fwd = band + "_fwd"; String band_ref = band + "_ref"; String band_list[] = { "1.25cm", "3cm", "6cm", "9cm", "13cm", "23cm", "70cm", "2m", "HF" }; int IO2_FWD = 2; int IO4_REF = 4; WebServer server(80); // Select the IP address according to your local network IPAddress myIP(192, 168, 1, 100); IPAddress myGW(192, 168, 1, 1); IPAddress mySN(255, 255, 255, 0); IPAddress myDNS(192, 168, 1, 1); // Reads a file from SPIFF and returns its content as a string String readFile(fs::FS &fs, const char *path) { Serial.printf("Reading file: %s\r\n", path); File file = fs.open(path); if (!file || file.isDirectory()) { Serial.println("failed to open file for reading"); return ""; } String ret = ""; Serial.println("read from file:"); while (file.available()) { ret += char(file.read()); } file.close(); return ret; } // Takes a string and writes it to a file on SPIFF void writeFile(fs::FS &fs, const char *path, const char *message) { Serial.printf("Writing file: %s\r\n", path); File file = fs.open(path, FILE_WRITE); if (!file) { Serial.println("failed to open file for writing"); return; } if (file.print(message)) { Serial.println("file written"); } else { Serial.println("file write failed"); } file.close(); } // converts dBm to Watt double dbm_to_watt(double dbm) { return pow(10.0, (dbm - 30.0) / 10.0); } // checks if a given voltage is lower as the smallest value // in the table or higher than the biggest value bool is_val_out_of_bounds(int mv, bool fwd) { double stored_val = 0; int key_a = 0; int key_b = 0; // searches for the first key (voltage) that has a value (dBm) for (int i = 0; i < 3300; i++) { if (fwd) { stored_val = fwd_array[i]; } else { stored_val = ref_array[i]; } if (stored_val != 0) { key_a = i; break; } } // searches for the last key (voltage) that has a value (dBm) for (int i = 3299; i > 0; i--) { if (fwd) { stored_val = fwd_array[i]; } else { stored_val = ref_array[i]; } if (stored_val != 0) { key_b = i; break; } } int lowerkey = min(key_a, key_b); // takes both values found above and assigns the lower key int higherkey = max(key_a, key_b); // takes both values found above and assigns the higher key // returns false if given voltage is between the lowest and highest configured voltages // returns true if voltage is out of bounds if (lowerkey <= mv and mv <= higherkey) return false; else { return true; } } // takes a voltage value and translates it // to dBm based on the corresponding lookup table double millivolt_to_dbm(int mv, bool fwd) { double lastval = 0; double nextval = 0; int lastkey = 0; int nextkey = 0; double stored_val = 0; bool ascending = true; int lowest_key_in_table = 0; int highest_key_in_table = 0; // check if table is ascending or descending double asc_tmp_val = 0; for (int i = 0; i < 3300; i++) { if (fwd) { stored_val = fwd_array[i]; } else { stored_val = ref_array[i]; } if (stored_val != 0) { if (asc_tmp_val == 0) { asc_tmp_val = stored_val; } else if (stored_val > asc_tmp_val) { ascending = true; break; } else if (stored_val < asc_tmp_val) { ascending = false; break; } } } // checks if the voltage values are opposite to the dBm values or // if both, voltage and dBm values are ascending if (ascending) { for (int i = 0; i < 3300; i++) { if (fwd) { stored_val = fwd_array[i]; } else { stored_val = ref_array[i]; } if (stored_val != 0) { if (lowest_key_in_table == 0) { lowest_key_in_table = i; //finds the lowest voltage value stored in the table } highest_key_in_table = i; // we will have the highest voltage value in the table at the end of the loop if (i < mv) { lastval = stored_val; lastkey = i; } else { nextval = stored_val; nextkey = i; break; } } } } else { for (int i = 3300; i > 0; i--) { if (fwd) { stored_val = fwd_array[i]; } else { stored_val = ref_array[i]; } if (stored_val != 0) { if (lowest_key_in_table == 0) { lowest_key_in_table = i; //finds the lowest voltage value stored in the table } highest_key_in_table = i; // we will have the highest voltage value in the table at the end of the loop if (i > mv) { lastval = stored_val; lastkey = i; } else { nextval = stored_val; nextkey = i; break; } } } } double lowerkey = min(lastkey, nextkey); double higherkey = max(lastkey, nextkey); double lowerval = min(lastval, nextval); double higherval = max(lastval, nextval); double diffkey = max(lastkey, nextkey) - min(lastkey, nextkey); double diffval = max(lastval, nextval) - min(lastval, nextval); double result = 0; if (ascending) { result = lowerval + ((diffval / diffkey) * (mv - lowerkey)); } else { result = higherval - ((diffval / diffkey) * (mv - lowerkey)); } return result; } // read voltages from both input pins // calculates avaerage value of 50 measurements void read_directional_couplers() { int voltage_sum_fwd = 0; int voltage_sum_ref = 0; // Takes 50 samples and sums them up for (iii = 0; iii < 50; iii++) { voltage_sum_fwd += analogReadMilliVolts(IO2_FWD); voltage_sum_ref += analogReadMilliVolts(IO4_REF); } // calculate the average value by deviding the above sum by 50 voltage_fwd = voltage_sum_fwd / 50; voltage_ref = voltage_sum_ref / 50; // calculate the dBm value from the voltage based on the calibration table fwd_dbm = millivolt_to_dbm(voltage_fwd, true); ref_dbm = millivolt_to_dbm(voltage_ref, false); // add cable loss to FWD dBm, substract cable loss from REF dBm double cable_loss = 0; cable_loss = config.getString(String("s_cable_loss").c_str()).toDouble(); //Serial.println("cable loss: " + String(cable_loss)); fwd_dbm = fwd_dbm - cable_loss; ref_dbm = ref_dbm + cable_loss; // calculate watt from dBm fwd_watt = dbm_to_watt(fwd_dbm); ref_watt = dbm_to_watt(ref_dbm); } // delivers the dashboard page in "index.h" void handleRoot() { String html = MAIN_page; String css = DB_STYLESHEET; String js = JAVASCRIPT; server.send(200, "text/html", css + js + html); } // delivers a 404 page if a non-existant resouurce is requested void handleNotFound() { String message = F("File Not Found\n\n"); message += F("URI: "); message += server.uri(); message += F("\nMethod: "); message += (server.method() == HTTP_GET) ? F("GET") : F("POST"); message += F("\nArguments: "); message += server.args(); message += F("\n"); for (uint8_t i = 0; i < server.args(); i++) { message += " " + server.argName(i) + ": " + server.arg(i) + "\n"; } server.send(404, F("text/plain"), message); } // executes the function to gather sensor data // delivers gathered data to dashboard page // invoked periodically by the dashboard page void handleDATA() { read_directional_couplers(); double vswr = 0; if (fwd_watt > ref_watt) { vswr = (1 + sqrt(ref_watt / fwd_watt)) / (1 - sqrt(ref_watt / fwd_watt)); } String vswr_str = "-1"; String fwd_watt_str = ""; String ref_watt_str = ""; if (vswr >= 1) { vswr_str = String(vswr); } double rl = fwd_dbm - ref_dbm; // get vswr_threshold from general config String vswr_threshold = config.getString(String("s_vswr_thresh").c_str()); String voltage_fwd_str = ""; String voltage_ref_str = ""; if (config.getString(String("b_show_mV").c_str()) != "false") { voltage_fwd_str = String(voltage_fwd) + " mV"; voltage_ref_str = String(voltage_ref) + " mV"; } String fwd_dbm_str = ""; String ref_dbm_str = ""; if (config.getString(String("b_show_dBm").c_str()) != "false") { fwd_dbm_str = String(fwd_dbm, 2); ref_dbm_str = String(ref_dbm, 2); } if (config.getString(String("b_show_watt").c_str()) != "false") { fwd_watt_str = String(fwd_watt, 10); } if (config.getString(String("b_show_watt").c_str()) != "false") { ref_watt_str = String(ref_watt, 10); } String rl_str = "-- "; if (rl > 0) { rl_str = (String(rl)); } rl_str.replace("nan", "-- "); String antenna_name = config.getString(String("s_ant_name").c_str()); String vswr_beep = config.getString(String("b_vswr_beep").c_str()); bool fwd_oob = is_val_out_of_bounds(voltage_fwd, true); bool ref_oob = is_val_out_of_bounds(voltage_ref, false); // Generate a semicolon seperated string that will be sent to the frontend String output = fwd_watt_str + ";"; // data[0]: FWD power in Watt output += fwd_dbm_str + ";"; // data[1]: FWD dBm value output += voltage_fwd_str + ";"; // data[2]: FWD voltage output += ref_watt_str + ";"; // data[3]: REF power in Watt output += ref_dbm_str + ";"; // data[4]: REF dBm value output += voltage_ref_str + ";"; // data[5]: REF voltage output += vswr_str + ";"; // data[6]: VSWR value output += rl_str + ";"; // data[7]: RL value output += band + ";"; // data[8]: band (e.g. "70cm") output += String(vswr_threshold) + ";"; // data[9]: VSWR threshold (e.g. "3") output += antenna_name + ";"; // data[10]: Name of antenna (e.g. "X200") output += vswr_beep + ";"; // data[11]: should it beep if VSWR is too high? (true/false) output += config.getString(String("s_max_led_pwr_f").c_str()) + ";"; // data[12]: highest value in Watt for the FWD LED graph (e.g. "100") output += config.getString(String("s_max_led_pwr_r").c_str()) + ";"; // data[13]: highest value in Watt for the REF LED graph (e.g. "1") output += config.getString(String("s_max_led_vswr").c_str()) + ";"; // data[14]: highest value in Watt for the VSWR LED graph (e.g. "3") output += String(fwd_oob) + ";"; // data[15]: Is the FWD voltage out of bounds? (true/false) output += String(ref_oob) + ";"; // data[16]: Is the REF voltage out of bounds? (true/false) output += config.getString(String("b_show_led_fwd").c_str()) + ";"; // data[17]: Show the FWD LED bar graph? (true/false) output += config.getString(String("b_show_led_ref").c_str()) + ";"; // data[18]: Show the REF LED bar graph? (true/false) output += config.getString(String("b_show_led_vswr").c_str()) + ";"; // data[19]: Show the VSWR LED bar graph? (true/false) output += version; // data[20]: program version server.send(200, "text/plane", output); } // main function for displaying the configuration page // invoked by the "configuration" button on the dashboard page void handleCONFIG() { if (conf_textareas == "") { build_textareas(); } if (conf_config_table == "") { build_config_table(); } String css = CFG_STYLESHEET; conf_content = css; conf_content += "
"; conf_content += "
"; conf_content += "Configuration
"; conf_content += "
"; conf_content += "
"; conf_content += "Band:
"; conf_content += "
"; conf_content += "
Translation Detector Voltage /mV to RF-Power Level /dBm
"; conf_content += "
"; conf_content += conf_textareas; conf_content += "
"; conf_content += "
General Configuration Items
"; conf_content += "
"; conf_content += conf_config_table; conf_content += "
"; conf_content += "
"; conf_content += "
- Version: " + version + "
"; conf_content += "
"; conf_content += "
"; conf_content += ""; server.send(200, "text/html", conf_content); } // generates the translation table for either the FWD or // REF values void build_textareas() { String fwd = readFile(SPIFFS, String("/" + band + "fwd.txt").c_str()); String ref = readFile(SPIFFS, String("/" + band + "ref.txt").c_str()); clear_fwd_ref_array(); save_string_to_array(fwd, fwd_array); save_string_to_array(ref, ref_array); String tbl = "
"; tbl += ""; tbl += ""; tbl += "
" + band + " FWD (mV:dBm)" + band + " REF (mV:dBm)
"; tbl += ""; tbl += ""; tbl += ""; tbl += "
"; tbl += ""; tbl += "
"; conf_textareas = tbl; } // generates the table with generic configuration items void build_config_table() { conf_config_table = "
"; conf_config_table += ""; //conf_config_table += ""; for (int i = 0; i < sizeof band_config_items / sizeof band_config_items[0]; i++) { if (!band_config_items[i].startsWith("x_")) { String stored_val = config.getString(band_config_items[i].c_str(), "xxx"); if (stored_val == "xxx") { config.putString(band_config_items[i].c_str(), band_config_defaults[i]); stored_val = config.getString(band_config_items[i].c_str(), ""); } conf_config_table += ""; } } conf_config_table += "
KeyValue
"; conf_config_table += band_config_nice_names[i]; conf_config_table += ""; if (String(stored_val).equalsIgnoreCase("true")) { conf_config_table += ""; } else if (String(stored_val).equalsIgnoreCase("false")) { conf_config_table += ""; } else { conf_config_table += ""; } conf_config_table += "
"; handleCONFIG(); } // Handle request from the config page to change or add values // to the XXXX value table for the selected band void handleMODCAL() { String fwd = server.arg("fwd_textarea") + "\n"; String ref = server.arg("ref_textarea") + "\n"; clear_fwd_ref_array(); save_string_to_array(fwd, fwd_array); save_string_to_array(ref, ref_array); String fwd_of_array = ""; for (int i = 0; i < sizeof fwd_array / sizeof fwd_array[0]; i++) { if (fwd_array[i] != 0) { fwd_of_array += String(i) + ":" + String(fwd_array[i], 5) + "\n"; } } writeFile(SPIFFS, String("/" + band + "fwd.txt").c_str(), fwd_of_array.c_str()); String ref_of_array = ""; for (int i = 0; i < sizeof ref_array / sizeof ref_array[0]; i++) { if (ref_array[i] != 0) { ref_of_array += String(i) + ":" + String(ref_array[i], 5) + "\n"; } } writeFile(SPIFFS, String("/" + band + "ref.txt").c_str(), ref_of_array.c_str()); build_textareas(); handleCONFIG(); } // resets all vlaues in the FWD and REF array void clear_fwd_ref_array() { for (int x = 0; x < sizeof(fwd_array) / sizeof(fwd_array[0]); x++) { fwd_array[x] = 0; ref_array[x] = 0; } } // after the user edited the calibration table via the frontend, // this function takes the resulting string, detonates it and // writes all rows into an array void save_string_to_array(String table_data, double arr[]) { int r = 0, t = 0; for (int i = 0; i < table_data.length(); i++) { if (table_data[i] == '\n' || i == table_data.length()) { if (i - r > 1) { String row = table_data.substring(r, i); t++; int r2 = 0, t2 = 0; for (int j = 0; j < row.length(); j++) { if (row[j] == ':' || row[j] == '\n') { if (j - r2 > 1) { int key = row.substring(r2, j).toInt(); double val = row.substring(j + 1).toDouble(); arr[key] = val; t2++; } r2 = (j + 1); } } } r = (i + 1); } } } // Handle request from the config page to change or add values // to the general config value table for the selected band void handleMODCFG() { for (int i = 0; i < sizeof band_config_items / sizeof band_config_items[0]; i++) { if (!server.hasArg(band_config_items[i]) and band_config_items[i].startsWith("b_")) { config.putString(band_config_items[i].c_str(), "false"); } else if (server.hasArg(band_config_items[i]) and band_config_items[i].startsWith("b_")) { config.putString(band_config_items[i].c_str(), "true"); } else { config.putString(band_config_items[i].c_str(), server.arg(band_config_items[i])); } } conf_config_table = ""; build_config_table(); } // changes the band according to the user's selection // regenerates the calibration tables and fills them // with the values assigned to the respective band // invoked by selecting a band from the select box of the config page void handleBAND() { band = server.arg("bands"); band_fwd = band + "_fwd"; band_ref = band + "_ref"; global_config.putString(String("x_selected_band").c_str(), band); config.end(); String bnd_cnf = "config_" + band; config.begin(bnd_cnf.c_str(), false); conf_textareas = ""; conf_config_table = ""; //build_config_table(); //build_textareas(); handleCONFIG(); } // initialization routine void setup() { Serial.begin(115200); //while (!Serial) // ; // Using this if Serial debugging is not necessary or not using Serial port //while (!Serial && (millis() < 3000)); Serial.print("\nStarting AdvancedWebServer on " + String(ARDUINO_BOARD)); Serial.println(" with " + String(SHIELD_TYPE)); Serial.println(WEBSERVER_WT32_ETH01_VERSION); // To be called before ETH.begin() WT32_ETH01_onEvent(); ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER); // Static IP, leave without this line to get IP via DHCP //ETH.config(myIP, myGW, mySN, myDNS); WT32_ETH01_waitForConnect(); // activates single web server endpoints server.on(F("/"), handleRoot); server.on("/readDATA", handleDATA); server.on("/config", handleCONFIG); server.on("/modcfg", handleMODCFG); server.on("/selectband", handleBAND); server.on("/modcal", handleMODCAL); if (!SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)) { Serial.println("SPIFFS Mount Failed"); return; } server.onNotFound(handleNotFound); server.begin(); Serial.print(F("HTTP EthernetWebServer is @ IP : ")); Serial.println(ETH.localIP()); analogReadResolution(12); global_config.begin("config", false); band = global_config.getString(String("x_selected_band").c_str()); if (band == "") { global_config.putString(String("x_selected_band").c_str(), default_band); band = default_band; } String bnd_cnf = "config_" + band; config.begin(bnd_cnf.c_str(), false); build_textareas(); } void loop() { server.handleClient(); }