diff --git a/.vscode/settings.json b/.vscode/settings.json index 9da6224a1..cd2a5ca5c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -57,6 +57,7 @@ "HFSR", "Meshtastic", "NEMAGPS", + "RDEF", "Ublox", "bkpt", "cfsr", diff --git a/docs/_config.yml b/docs/_config.yml index 9c1ecd436..1cb1a55ba 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,7 +1,7 @@ theme: jekyll-theme-cayman title: Meshtastic -description: An opensource hiking, pilot, skiing, Signal-App-extending GPS mesh communicator +description: An opensource hiking, pilot, skiing, secure GPS mesh communicator google_analytics: G-DRZ5H5EXHV include: [".well-known"] diff --git a/docs/software/power.md b/docs/software/power.md index cd0d2c913..e98e5bd0d 100644 --- a/docs/software/power.md +++ b/docs/software/power.md @@ -32,11 +32,15 @@ From lower to higher power consumption. onEntry: setBluetoothOn(true) onExit: -- full on (ON) - Everything is on +- serial API usage (SERIAL) - Screen is on, device doesn't sleep, bluetooth off + onEntry: setBluetooth off, screen on + onExit: + +- full on (ON) - Everything is on, can eventually timeout and lower to a lower power state onEntry: setBluetoothOn(true), screen.setOn(true) onExit: screen.setOn(false) -- serial API usage (SERIAL) - Screen is on, device doesn't sleep, bluetooth off +- has power (POWER) - Screen is on, device doesn't sleep, bluetooth on, will stay in this state as long as we have power onEntry: setBluetooth off, screen on onExit: @@ -56,9 +60,11 @@ From lower to higher power consumption. - While in NB/DARK/ON: If we receive EVENT_NODEDB_UPDATED we transition to ON (so the new screen can be shown) - While in DARK: While the phone talks to us over BLE (EVENT_CONTACT_FROM_PHONE) reset any sleep timers and stay in DARK (needed for bluetooth sw update and nice user experience if the user is reading/replying to texts) - while in LS/NB/DARK: if SERIAL_CONNECTED, go to serial +- while in any state: if we have AC power, go to POWER ### events that decrease cpu activity +- While in POWER: if lose AC go to ON - While in SERIAL: if SERIAL_DISCONNECTED, go to NB - While in ON: If PRESS event occurs, reset screen_on_secs timer and tell the screen to handle the pess - While in ON: If it has been more than screen_on_secs since a press, lower to DARK diff --git a/platformio.ini b/platformio.ini index c413b460b..5fe2fdbf4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -172,7 +172,7 @@ build_flags = -Isdk-nrfxlib/crypto/nrf_oberon/include -Lsdk-nrfxlib/crypto/nrf_oberon/lib/cortex-m4/hard-float/ -lliboberon_3.0.3 ;-DCFG_DEBUG=3 src_filter = - ${arduino_base.src_filter} - - + ${arduino_base.src_filter} - - - lib_ignore = BluetoothOTA monitor_port = /dev/ttyACM1 @@ -267,7 +267,7 @@ lib_deps = ; The Portduino based sim environment on top of linux [env:linux] platform = https://github.com/geeksville/platform-portduino.git -src_filter = ${env.src_filter} - - - +src_filter = ${env.src_filter} - - - - build_flags = ${arduino_base.build_flags} -O0 framework = arduino board = linux_x86_64 diff --git a/src/Power.cpp b/src/Power.cpp index 1145c51c4..7d032b666 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -119,6 +119,7 @@ void Power::readPowerStatus() const PowerStatus powerStatus = PowerStatus(hasBattery ? OptTrue : OptFalse, batteryLevel->isVBUSPlug() ? OptTrue : OptFalse, batteryLevel->isChargeing() ? OptTrue : OptFalse, batteryVoltageMv, batteryChargePercent); + DEBUG_MSG("Read power stat %d\n", powerStatus.getHasUSB()); newStatus.notifyObservers(&powerStatus); // If we have a battery at all and it is less than 10% full, force deep sleep @@ -237,9 +238,11 @@ void Power::loop() } if (axp.isVbusRemoveIRQ()) { DEBUG_MSG("USB unplugged\n"); + powerFSM.trigger(EVENT_POWER_DISCONNECTED); } if (axp.isVbusPlugInIRQ()) { DEBUG_MSG("USB plugged In\n"); + powerFSM.trigger(EVENT_POWER_CONNECTED); } if (axp.isBattPlugInIRQ()) { DEBUG_MSG("Battery inserted\n"); diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 128947bcd..f72e5842a 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -4,8 +4,8 @@ #include "MeshService.h" #include "NodeDB.h" #include "configuration.h" -#include "main.h" #include "graphics/Screen.h" +#include "main.h" #include "sleep.h" #include "target_specific.h" @@ -123,6 +123,12 @@ static void serialEnter() screen.setOn(true); } +static void powerEnter() +{ + screen.setOn(true); + setBluetoothEnable(true); +} + static void onEnter() { screen.setOn(true); @@ -155,16 +161,20 @@ State stateDARK(darkEnter, NULL, NULL, "DARK"); State stateSERIAL(serialEnter, NULL, NULL, "SERIAL"); State stateBOOT(bootEnter, NULL, NULL, "BOOT"); State stateON(onEnter, NULL, NULL, "ON"); +State statePOWER(powerEnter, NULL, NULL, "POWER"); Fsm powerFSM(&stateBOOT); void PowerFSM_setup() { - powerFSM.add_timed_transition(&stateBOOT, &stateON, 3 * 1000, NULL, "boot timeout"); + // If we already have AC power go to POWER state after init, otherwise go to ON + bool hasPower = powerStatus && powerStatus->getHasUSB(); + DEBUG_MSG("PowerFSM init, USB power=%d\n", hasPower); + powerFSM.add_timed_transition(&stateBOOT, hasPower ? &statePOWER : &stateON, 3 * 1000, NULL, "boot timeout"); powerFSM.add_transition(&stateLS, &stateDARK, EVENT_WAKE_TIMER, wakeForPing, "Wake timer"); - // Note we don't really use this transition, because when we wake from light sleep we _always_ transition to NB and then it - // handles things powerFSM.add_transition(&stateLS, &stateNB, EVENT_RECEIVED_PACKET, NULL, "Received packet"); + // Note we don't really use this transition, because when we wake from light sleep we _always_ transition to NB and then + // it handles things powerFSM.add_transition(&stateLS, &stateNB, EVENT_RECEIVED_PACKET, NULL, "Received packet"); powerFSM.add_transition(&stateNB, &stateNB, EVENT_RECEIVED_PACKET, NULL, "Received packet, resetting win wake"); @@ -172,7 +182,10 @@ void PowerFSM_setup() powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, NULL, "Press"); powerFSM.add_transition(&stateNB, &stateON, EVENT_PRESS, NULL, "Press"); powerFSM.add_transition(&stateDARK, &stateON, EVENT_PRESS, NULL, "Press"); + powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, screenPress, "Press"); powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, screenPress, "Press"); // reenter On to restart our timers + powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, screenPress, + "Press"); // Allow button to work while in serial API // Handle critically low power battery by forcing deep sleep powerFSM.add_transition(&stateBOOT, &stateSDS, EVENT_LOW_BATTERY, NULL, "LowBat"); @@ -199,6 +212,14 @@ void PowerFSM_setup() powerFSM.add_transition(&stateDARK, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); powerFSM.add_transition(&stateON, &stateSERIAL, EVENT_SERIAL_CONNECTED, NULL, "serial API"); + powerFSM.add_transition(&stateLS, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); + powerFSM.add_transition(&stateNB, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); + powerFSM.add_transition(&stateDARK, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); + powerFSM.add_transition(&stateON, &statePOWER, EVENT_POWER_CONNECTED, NULL, "power connect"); + + powerFSM.add_transition(&statePOWER, &stateON, EVENT_POWER_DISCONNECTED, NULL, "power disconnected"); + powerFSM.add_transition(&stateSERIAL, &stateON, EVENT_POWER_DISCONNECTED, NULL, "power disconnected"); + powerFSM.add_transition(&stateSERIAL, &stateNB, EVENT_SERIAL_DISCONNECTED, NULL, "serial disconnect"); powerFSM.add_transition(&stateDARK, &stateDARK, EVENT_CONTACT_FROM_PHONE, NULL, "Contact from phone"); diff --git a/src/PowerFSM.h b/src/PowerFSM.h index c89ad9148..4a219f570 100644 --- a/src/PowerFSM.h +++ b/src/PowerFSM.h @@ -13,9 +13,11 @@ #define EVENT_BLUETOOTH_PAIR 7 #define EVENT_NODEDB_UPDATED 8 // NodeDB has a big enough change that we think you should turn on the screen #define EVENT_CONTACT_FROM_PHONE 9 // the phone just talked to us over bluetooth -#define EVENT_LOW_BATTERY 10 // Battery is critically low, go to sleep +#define EVENT_LOW_BATTERY 10 // Battery is critically low, go to sleep #define EVENT_SERIAL_CONNECTED 11 #define EVENT_SERIAL_DISCONNECTED 12 +#define EVENT_POWER_CONNECTED 13 +#define EVENT_POWER_DISCONNECTED 14 extern Fsm powerFSM; diff --git a/src/SerialConsole.cpp b/src/SerialConsole.cpp index 683886ed5..f246b607c 100644 --- a/src/SerialConsole.cpp +++ b/src/SerialConsole.cpp @@ -40,6 +40,8 @@ void SerialConsole::onConnectionChanged(bool connected) if (connected) { // To prevent user confusion, turn off bluetooth while using the serial port api powerFSM.trigger(EVENT_SERIAL_CONNECTED); } else { + // FIXME, we get no notice of serial going away, we should instead automatically generate this event if we haven't + // received a packet in a while powerFSM.trigger(EVENT_SERIAL_DISCONNECTED); } } \ No newline at end of file diff --git a/src/configuration.h b/src/configuration.h index df8b2fb80..026bb1880 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -95,6 +95,8 @@ along with this program. If not, see . // Standard definitions for ESP32 targets // +#define HAS_WIFI + #define GPS_SERIAL_NUM 1 #define GPS_RX_PIN 34 #ifdef USE_JTAG diff --git a/src/esp32/WiFiServerAPI.cpp b/src/esp32/WiFiServerAPI.cpp index b5d555605..aa5963882 100644 --- a/src/esp32/WiFiServerAPI.cpp +++ b/src/esp32/WiFiServerAPI.cpp @@ -33,7 +33,7 @@ void WiFiServerAPI::loop() if (client.connected()) { StreamAPI::loop(); } else { - DEBUG_MSG("Client dropped connection, closing UDP server\n"); + DEBUG_MSG("Client dropped connection, closing TCP server\n"); delete this; } } @@ -44,7 +44,7 @@ WiFiServerPort::WiFiServerPort() : WiFiServer(MESHTASTIC_PORTNUM) {} void WiFiServerPort::init() { - DEBUG_MSG("Listening on TCP port %d\n", MESHTASTIC_PORTNUM); + DEBUG_MSG("API server sistening on TCP port %d\n", MESHTASTIC_PORTNUM); begin(); } @@ -52,6 +52,14 @@ void WiFiServerPort::loop() { auto client = available(); if (client) { - new WiFiServerAPI(client); + // Close any previous connection (see FIXME in header file) + if (openAPI) + delete openAPI; + + openAPI = new WiFiServerAPI(client); } + + if (openAPI) + // Allow idle processing so the API can read from its incoming stream + openAPI->loop(); } \ No newline at end of file diff --git a/src/esp32/WiFiServerAPI.h b/src/esp32/WiFiServerAPI.h index dcafa419d..a9b1e39b4 100644 --- a/src/esp32/WiFiServerAPI.h +++ b/src/esp32/WiFiServerAPI.h @@ -29,6 +29,13 @@ class WiFiServerAPI : public StreamAPI */ class WiFiServerPort : public WiFiServer { + /** The currently open port + * + * FIXME: We currently only allow one open TCP connection at a time, because we depend on the loop() call in this class to + * delegate to the worker. Once coroutines are implemented we can relax this restriction. + */ + WiFiServerAPI *openAPI = NULL; + public: WiFiServerPort(); diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index b460775a9..0706ddb44 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -31,6 +31,7 @@ along with this program. If not, see . #include "graphics/images.h" #include "main.h" #include "mesh-pb-constants.h" +#include "meshwifi/meshwifi.h" #include "target_specific.h" #include "utils.h" @@ -709,6 +710,11 @@ void Screen::drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUi screen->debugInfo.drawFrameSettings(display, state, x, y); } +void Screen::drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + Screen *screen = reinterpret_cast(state->userData); + screen->debugInfo.drawFrameWiFi(display, state, x, y); +} // restore our regular frame list void Screen::setFrames() @@ -740,6 +746,11 @@ void Screen::setFrames() // call a method on debugInfoScreen object (for more details) normalFrames[numframes++] = &Screen::drawDebugInfoSettingsTrampoline; + if (isWifiAvailable()) { + // call a method on debugInfoScreen object (for more details) + normalFrames[numframes++] = &Screen::drawDebugInfoWiFiTrampoline; + } + ui.setFrames(normalFrames, numframes); ui.enableAllIndicators(); @@ -798,18 +809,18 @@ void DebugInfo::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 { concurrency::LockGuard guard(&lock); snprintf(channelStr, sizeof(channelStr), "%s", channelName.c_str()); - - // Display power status - if (powerStatus->getHasBattery()) - drawBattery(display, x, y + 2, imgBattery, powerStatus); - else if (powerStatus->knowsUSB()) - display->drawFastImage(x, y + 2, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower); - // Display nodes status - drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus); - // Display GPS status - drawGPS(display, x + (SCREEN_WIDTH * 0.63), y + 2, gpsStatus); } + // Display power status + if (powerStatus->getHasBattery()) + drawBattery(display, x, y + 2, imgBattery, powerStatus); + else if (powerStatus->knowsUSB()) + display->drawFastImage(x, y + 2, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower); + // Display nodes status + drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus); + // Display GPS status + drawGPS(display, x + (SCREEN_WIDTH * 0.63), y + 2, gpsStatus); + // Draw the channel name display->drawString(x, y + FONT_HEIGHT, channelStr); // Draw our hardware ID to assist with bluetooth pairing @@ -828,6 +839,135 @@ void DebugInfo::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 } // Jm +void DebugInfo::drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ +#ifdef HAS_WIFI + const char *wifiName = radioConfig.preferences.wifi_ssid; + const char *wifiPsw = radioConfig.preferences.wifi_password; + + displayedNodeNum = 0; // Not currently showing a node pane + + display->setFont(ArialMT_Plain_10); + + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); + + if (radioConfig.preferences.wifi_ap_mode) { + display->drawString(x, y, String("WiFi: Software AP")); + } else if (WiFi.status() != WL_CONNECTED) { + display->drawString(x, y, String("WiFi: Not Connected")); + } else { + display->drawString(x, y, String("WiFi: Connected")); + + display->drawString(x + SCREEN_WIDTH - display->getStringWidth("RSSI " + String(WiFi.RSSI())), y, + "RSSI " + String(WiFi.RSSI())); + } + + /* + - WL_CONNECTED: assigned when connected to a WiFi network; + - WL_NO_SSID_AVAIL: assigned when no SSID are available; + - WL_CONNECT_FAILED: assigned when the connection fails for all the attempts; + - WL_CONNECTION_LOST: assigned when the connection is lost; + - WL_DISCONNECTED: assigned when disconnected from a network; + - WL_IDLE_STATUS: it is a temporary status assigned when WiFi.begin() is called and remains active until the number of + attempts expires (resulting in WL_CONNECT_FAILED) or a connection is established (resulting in WL_CONNECTED); + - WL_SCAN_COMPLETED: assigned when the scan networks is completed; + - WL_NO_SHIELD: assigned when no WiFi shield is present; + + */ + + if (WiFi.status() == WL_CONNECTED) { + if (radioConfig.preferences.wifi_ap_mode) { + display->drawString(x, y + FONT_HEIGHT * 1, "IP: " + String(WiFi.softAPIP().toString().c_str())); + } else { + display->drawString(x, y + FONT_HEIGHT * 1, "IP: " + String(WiFi.localIP().toString().c_str())); + } + } else if (WiFi.status() == WL_NO_SSID_AVAIL) { + display->drawString(x, y + FONT_HEIGHT * 1, "SSID Not Found"); + } else if (WiFi.status() == WL_CONNECTION_LOST) { + display->drawString(x, y + FONT_HEIGHT * 1, "Connection Lost"); + } else if (WiFi.status() == WL_CONNECT_FAILED) { + display->drawString(x, y + FONT_HEIGHT * 1, "Connection Failed"); + } else if (WiFi.status() == WL_DISCONNECTED) { + display->drawString(x, y + FONT_HEIGHT * 1, "Disconnected"); + } else if (WiFi.status() == WL_IDLE_STATUS) { + display->drawString(x, y + FONT_HEIGHT * 1, "Idle ... Reconnecting"); + } else { + // Codes: + // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code + if (getWifiDisconnectReason() == 2) { + display->drawString(x, y + FONT_HEIGHT * 1, "Authentication Invalid"); + } else if (getWifiDisconnectReason() == 3) { + display->drawString(x, y + FONT_HEIGHT * 1, "De-authenticated"); + } else if (getWifiDisconnectReason() == 4) { + display->drawString(x, y + FONT_HEIGHT * 1, "Disassociated Expired"); + } else if (getWifiDisconnectReason() == 5) { + display->drawString(x, y + FONT_HEIGHT * 1, "AP - Too Many Clients"); + } else if (getWifiDisconnectReason() == 6) { + display->drawString(x, y + FONT_HEIGHT * 1, "NOT_AUTHED"); + } else if (getWifiDisconnectReason() == 7) { + display->drawString(x, y + FONT_HEIGHT * 1, "NOT_ASSOCED"); + } else if (getWifiDisconnectReason() == 8) { + display->drawString(x, y + FONT_HEIGHT * 1, "Disassociated"); + } else if (getWifiDisconnectReason() == 9) { + display->drawString(x, y + FONT_HEIGHT * 1, "ASSOC_NOT_AUTHED"); + } else if (getWifiDisconnectReason() == 10) { + display->drawString(x, y + FONT_HEIGHT * 1, "DISASSOC_PWRCAP_BAD"); + } else if (getWifiDisconnectReason() == 11) { + display->drawString(x, y + FONT_HEIGHT * 1, "DISASSOC_SUPCHAN_BAD"); + } else if (getWifiDisconnectReason() == 13) { + display->drawString(x, y + FONT_HEIGHT * 1, "IE_INVALID"); + } else if (getWifiDisconnectReason() == 14) { + display->drawString(x, y + FONT_HEIGHT * 1, "MIC_FAILURE"); + } else if (getWifiDisconnectReason() == 15) { + display->drawString(x, y + FONT_HEIGHT * 1, "4WAY_HANDSHAKE_TIMEOUT"); + } else if (getWifiDisconnectReason() == 16) { + display->drawString(x, y + FONT_HEIGHT * 1, "GROUP_KEY_UPDATE_TIMEOUT"); + } else if (getWifiDisconnectReason() == 17) { + display->drawString(x, y + FONT_HEIGHT * 1, "IE_IN_4WAY_DIFFERS"); + } else if (getWifiDisconnectReason() == 18) { + display->drawString(x, y + FONT_HEIGHT * 1, "Invalid Group Cipher"); + } else if (getWifiDisconnectReason() == 19) { + display->drawString(x, y + FONT_HEIGHT * 1, "Invalid Pairwise Cipher"); + } else if (getWifiDisconnectReason() == 20) { + display->drawString(x, y + FONT_HEIGHT * 1, "AKMP_INVALID"); + } else if (getWifiDisconnectReason() == 21) { + display->drawString(x, y + FONT_HEIGHT * 1, "UNSUPP_RSN_IE_VERSION"); + } else if (getWifiDisconnectReason() == 22) { + display->drawString(x, y + FONT_HEIGHT * 1, "INVALID_RSN_IE_CAP"); + } else if (getWifiDisconnectReason() == 23) { + display->drawString(x, y + FONT_HEIGHT * 1, "802_1X_AUTH_FAILED"); + } else if (getWifiDisconnectReason() == 24) { + display->drawString(x, y + FONT_HEIGHT * 1, "CIPHER_SUITE_REJECTED"); + } else if (getWifiDisconnectReason() == 200) { + display->drawString(x, y + FONT_HEIGHT * 1, "BEACON_TIMEOUT"); + } else if (getWifiDisconnectReason() == 201) { + display->drawString(x, y + FONT_HEIGHT * 1, "AP Not Found"); + } else if (getWifiDisconnectReason() == 202) { + display->drawString(x, y + FONT_HEIGHT * 1, "AUTH_FAIL"); + } else if (getWifiDisconnectReason() == 203) { + display->drawString(x, y + FONT_HEIGHT * 1, "ASSOC_FAIL"); + } else if (getWifiDisconnectReason() == 204) { + display->drawString(x, y + FONT_HEIGHT * 1, "HANDSHAKE_TIMEOUT"); + } else if (getWifiDisconnectReason() == 205) { + display->drawString(x, y + FONT_HEIGHT * 1, "Connection Failed"); + } else { + display->drawString(x, y + FONT_HEIGHT * 1, "Unknown Status"); + } + } + + display->drawString(x, y + FONT_HEIGHT * 2, "SSID: " + String(wifiName)); + display->drawString(x, y + FONT_HEIGHT * 3, "PWD: " + String(wifiPsw)); + + /* Display a heartbeat pixel that blinks every time the frame is redrawn */ +#ifdef SHOW_REDRAWS + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; +#endif +#endif +} + void DebugInfo::drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { displayedNodeNum = 0; // Not currently showing a node pane @@ -838,30 +978,24 @@ void DebugInfo::drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *stat display->setTextAlignment(TEXT_ALIGN_LEFT); char batStr[20]; - if (powerStatus->getHasBattery()) - { + if (powerStatus->getHasBattery()) { int batV = powerStatus->getBatteryVoltageMv() / 1000; int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; - snprintf(batStr, sizeof(batStr), "B %01d.%02dV %3d%% %c%c", - batV, - batCv, - powerStatus->getBatteryChargePercent(), - powerStatus->getIsCharging() ? '+' : ' ', - powerStatus->getHasUSB() ? 'U' : ' '); + snprintf(batStr, sizeof(batStr), "B %01d.%02dV %3d%% %c%c", batV, batCv, powerStatus->getBatteryChargePercent(), + powerStatus->getIsCharging() ? '+' : ' ', powerStatus->getHasUSB() ? 'U' : ' '); // Line 1 display->drawString(x, y, batStr); - } - else - { + } else { // Line 1 display->drawString(x, y, String("USB")); - } + } + + display->drawString(x + SCREEN_WIDTH - display->getStringWidth("Mode " + String(channelSettings.modem_config)), + y, "Mode " + String(channelSettings.modem_config)); - //TODO: Display status of the BT radio - // display->drawString(x + SCREEN_WIDTH - display->getStringWidth("BT On"), y, "BT On"); // Line 2 uint32_t currentMillis = millis(); @@ -874,20 +1008,13 @@ void DebugInfo::drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *stat minutes %= 60; hours %= 24; - display->drawString(x, y + FONT_HEIGHT * 1, String(days) + "d " - + (hours < 10 ? "0" : "") + String(hours) + ":" - + (minutes < 10 ? "0" : "") + String(minutes) + ":" - + (seconds < 10 ? "0" : "") + String(seconds)); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth("Mode " + String(channelSettings.modem_config)), y + FONT_HEIGHT * 1, "Mode " + String(channelSettings.modem_config)); - - // Line 3 - // TODO: Use this line for WiFi information. - // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth("WiFi: 192.168.0.100"))) / 2, y + FONT_HEIGHT * 2, "WiFi: 192.168.0.100"); + display->drawString(x, y + FONT_HEIGHT * 1, + String(days) + "d " + (hours < 10 ? "0" : "") + String(hours) + ":" + (minutes < 10 ? "0" : "") + + String(minutes) + ":" + (seconds < 10 ? "0" : "") + String(seconds)); // Line 4 drawGPScoordinates(display, x, y + FONT_HEIGHT * 3, gpsStatus); - /* Display a heartbeat pixel that blinks every time the frame is redrawn */ #ifdef SHOW_REDRAWS if (heartbeat) diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index cd24a7e3a..6a7e0b524 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -47,6 +47,7 @@ class DebugInfo /// Renders the debug screen. void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); std::string channelName; @@ -220,6 +221,8 @@ class Screen : public concurrency::PeriodicTask static void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + /// Queue of commands to execute in doTask. TypedQueue cmdQueue; /// Whether we are using a display diff --git a/src/main.cpp b/src/main.cpp index e20572a6f..c53fdfeff 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -37,6 +37,8 @@ #include "SPILock.h" #include "graphics/Screen.h" #include "main.h" +#include "meshwifi/meshhttp.h" +#include "meshwifi/meshwifi.h" #include "sleep.h" #include "target_specific.h" #include @@ -217,9 +219,9 @@ void setup() // Currently only the tbeam has a PMU power = new Power(); - power->setup(); power->setStatusHandler(powerStatus); powerStatus->observe(&power->newStatus); + power->setup(); // Must be after status handler is installed, so that handler gets notified of the initial configuration // Init our SPI controller (must be before screen and lora) initSPI(); @@ -328,6 +330,9 @@ void setup() } #endif + // Initialize Wifi + initWifi(); + if (!rIf) recordCriticalError(ErrNoRadio); else @@ -394,6 +399,8 @@ void loop() userButtonAlt.tick(); #endif + loopWifi(); + // Show boot screen for first 3 seconds, then switch to normal operation. static bool showingBootScreen = true; if (showingBootScreen && (millis() > 3000)) { @@ -420,5 +427,8 @@ void loop() // feel slow msecstosleep = 10; + // TODO: This should go into a thread handled by FreeRTOS. + handleWebResponse(); + delay(msecstosleep); } diff --git a/src/main.h b/src/main.h index 74ad0b441..366ef8610 100644 --- a/src/main.h +++ b/src/main.h @@ -10,6 +10,8 @@ extern bool ssd1306_found; extern bool isCharging; extern bool isUSBPowered; + + // Global Screen singleton. extern graphics::Screen screen; //extern Observable newPowerStatus; //TODO: move this to main-esp32.cpp somehow or a helper class @@ -23,4 +25,4 @@ const char *getDeviceName(); -void nrf52Setup(), esp32Setup(), nrf52Loop(), esp32Loop(); \ No newline at end of file +void nrf52Setup(), esp32Setup(), nrf52Loop(), esp32Loop(); diff --git a/src/mesh/MeshRadio.h b/src/mesh/MeshRadio.h index 070c7c9a7..2cee8f89a 100644 --- a/src/mesh/MeshRadio.h +++ b/src/mesh/MeshRadio.h @@ -6,87 +6,14 @@ #include "configuration.h" #include "mesh.pb.h" -// US channel settings -#define CH0_US 903.08f // MHz -#define CH_SPACING_US 2.16f // MHz -#define NUM_CHANNELS_US 13 +// Map from old region names to new region enums +struct RegionInfo { + RegionCode code; + uint8_t numChannels; + uint8_t powerLimit; // Or zero for not set + float freq; + float spacing; + const char *name; // EU433 etc +}; -// EU433 channel settings -#define CH0_EU433 433.175f // MHz -#define CH_SPACING_EU433 0.2f // MHz -#define NUM_CHANNELS_EU433 8 - -// EU865 channel settings -#define CH0_EU865 865.2f // MHz -#define CH_SPACING_EU865 0.3f // MHz -#define NUM_CHANNELS_EU865 10 - -// CN channel settings -#define CH0_CN 470.0f // MHz -#define CH_SPACING_CN 2.0f // MHz FIXME, this is just a guess for 470-510 -#define NUM_CHANNELS_CN 20 - -// JP channel settings (AS1 bandplan) -#define CH0_JP 920.0f // MHz -#define CH_SPACING_JP 0.5f -#define NUM_CHANNELS_JP 10 - -// TW channel settings (AS2 bandplan 923-925MHz) -#define CH0_TW 923.0f // MHz -#define CH_SPACING_TW 0.2 -#define NUM_CHANNELS_TW 10 - -// AU/NZ channel settings 915-928MHz -#define CH0_ANZ 916.0f // MHz - avoid overcrowding on 915.0 -#define CH_SPACING_ANZ 0.5f -#define NUM_CHANNELS_ANZ 20 - -// KR channel settings (KR920-923) -// Start from TTN download channel freq. (921.9f is for download, others are for uplink) -#define CH0_KR 921.9f // MHz -#define CH_SPACING_KR 0.2f -#define NUM_CHANNELS_KR 8 - -// FIXME add defs for other regions and use them here -#ifdef HW_VERSION_US -#define CH0 CH0_US -#define CH_SPACING CH_SPACING_US -#define NUM_CHANNELS NUM_CHANNELS_US -#elif defined(HW_VERSION_EU433) -#define CH0 CH0_EU433 -#define CH_SPACING CH_SPACING_EU433 -#define NUM_CHANNELS NUM_CHANNELS_EU433 -#elif defined(HW_VERSION_EU865) -#define CH0 CH0_EU865 -#define CH_SPACING CH_SPACING_EU865 -#define NUM_CHANNELS NUM_CHANNELS_EU865 -#elif defined(HW_VERSION_CN) -#define CH0 CH0_CN -#define CH_SPACING CH_SPACING_CN -#define NUM_CHANNELS NUM_CHANNELS_CN -#elif defined(HW_VERSION_JP) -// Also called AS1 bandplan -#define CH0 CH0_JP -#define CH_SPACING CH_SPACING_JP -#define NUM_CHANNELS NUM_CHANNELS_JP -#elif defined(HW_VERSION_TW) -// Also called AS2 bandplan -#define CH0 CH0_TW -#define CH_SPACING CH_SPACING_TW -#define NUM_CHANNELS NUM_CHANNELS_TW -#elif defined(HW_VERSION_ANZ) -// Australia and NZ -#define CH0 CH0_ANZ -#define CH_SPACING CH_SPACING_ANZ -#define NUM_CHANNELS NUM_CHANNELS_ANZ -#elif defined(HW_VERSION_KR) -// Republic of Korea -#define CH0 CH0_KR -#define CH_SPACING CH_SPACING_KR -#define NUM_CHANNELS NUM_CHANNELS_KR -#else -// HW version not set - assume US -#define CH0 CH0_US -#define CH_SPACING CH_SPACING_US -#define NUM_CHANNELS NUM_CHANNELS_US -#endif \ No newline at end of file +extern const RegionInfo regions[]; \ No newline at end of file diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 3356e0922..8baed24c1 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -197,12 +197,14 @@ void MeshService::loop() } /// The radioConfig object just changed, call this to force the hw to change to the new settings -void MeshService::reloadConfig() +bool MeshService::reloadConfig() { // If we can successfully set this radio to these settings, save them to disk - nodeDB.resetRadioConfig(); // Don't let the phone send us fatally bad settings + bool didReset = nodeDB.resetRadioConfig(); // Don't let the phone send us fatally bad settings configChanged.notifyObservers(NULL); nodeDB.saveToDisk(); + + return didReset; } /// The owner User record just got updated, update our node DB and broadcast the info into the mesh diff --git a/src/mesh/MeshService.h b/src/mesh/MeshService.h index a12f087b3..a30f3d4ec 100644 --- a/src/mesh/MeshService.h +++ b/src/mesh/MeshService.h @@ -63,8 +63,10 @@ class MeshService */ void handleToRadio(MeshPacket &p); - /// The radioConfig object just changed, call this to force the hw to change to the new settings - void reloadConfig(); + /** The radioConfig object just changed, call this to force the hw to change to the new settings + * @return true if client devices should be sent a new set of radio configs + */ + bool reloadConfig(); /// The owner User record just got updated, update our node DB and broadcast the info into the mesh void reloadOwner(); diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 634bd9515..1f34eb5df 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -6,6 +6,7 @@ #include "CryptoEngine.h" #include "GPS.h" +#include "MeshRadio.h" #include "NodeDB.h" #include "PacketHistory.h" #include "PowerFSM.h" @@ -13,6 +14,7 @@ #include "configuration.h" #include "error.h" #include "mesh-pb-constants.h" +#include "meshwifi/meshwifi.h" #include #include @@ -102,14 +104,21 @@ const char *getChannelName() NodeDB::NodeDB() : nodes(devicestate.node_db), numNodes(&devicestate.node_db_count) {} -void NodeDB::resetRadioConfig() +bool NodeDB::resetRadioConfig() { + bool didFactoryReset = false; + /// 16 bytes of random PSK for our _public_ default channel that all devices power up on (AES128) static const uint8_t defaultpsk[] = {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0xbf}; - if (radioConfig.preferences.sds_secs == 0) { - DEBUG_MSG("RadioConfig reset!\n"); + if (radioConfig.preferences.factory_reset) { + DEBUG_MSG("Performing factory reset!\n"); + installDefaultDeviceState(); + didFactoryReset = true; + } else if (radioConfig.preferences.sds_secs == 0) { + DEBUG_MSG("Fixing bogus RadioConfig!\n"); + radioConfig.preferences.send_owner_interval = 4; // per sw-design.md radioConfig.preferences.position_broadcast_secs = 15 * 60; radioConfig.preferences.wait_bluetooth_secs = 120; @@ -123,8 +132,8 @@ void NodeDB::resetRadioConfig() radioConfig.has_preferences = true; // radioConfig.modem_config = RadioConfig_ModemConfig_Bw125Cr45Sf128; // medium range and fast - // channelSettings.modem_config = ChannelSettings_ModemConfig_Bw500Cr45Sf128; // short range and fast, but wide bandwidth - // so incompatible radios can talk together + // channelSettings.modem_config = ChannelSettings_ModemConfig_Bw500Cr45Sf128; // short range and fast, but wide + // bandwidth so incompatible radios can talk together channelSettings.modem_config = ChannelSettings_ModemConfig_Bw125Cr48Sf4096; // slow and long range channelSettings.tx_power = 0; // default @@ -147,12 +156,20 @@ void NodeDB::resetRadioConfig() radioConfig.preferences.position_broadcast_secs = 6 * 60; radioConfig.preferences.ls_secs = 60; } + + return didFactoryReset; } void NodeDB::installDefaultDeviceState() { + // We try to preserve the region setting because it will really bum users out if we discard it + String oldRegion = myNodeInfo.region; + RegionCode oldRegionCode = radioConfig.preferences.region; + memset(&devicestate, 0, sizeof(devicestate)); + *numNodes = 0; // Forget node DB + // init our devicestate with valid flags so protobuf writing/reading will work devicestate.has_my_node = true; devicestate.has_radio = true; @@ -181,6 +198,12 @@ void NodeDB::installDefaultDeviceState() // owner.short_name now sprintf(owner.long_name, "Unknown %02x%02x", ourMacAddr[4], ourMacAddr[5]); sprintf(owner.short_name, "?%02X", (unsigned)(myNodeInfo.my_node_num & 0xff)); + + // Restore region if possible + if (oldRegionCode != RegionCode_Unset) + radioConfig.preferences.region = oldRegionCode; + if (oldRegion.length()) + strcpy(myNodeInfo.region, oldRegion.c_str()); } void NodeDB::init() @@ -213,13 +236,29 @@ void NodeDB::init() // We set these _after_ loading from disk - because they come from the build and are more trusted than // what is stored in flash - strncpy(myNodeInfo.region, optstr(HW_VERSION), sizeof(myNodeInfo.region)); + if (xstr(HW_VERSION)[0]) + strncpy(myNodeInfo.region, optstr(HW_VERSION), sizeof(myNodeInfo.region)); + else + DEBUG_MSG("This build does not specify a HW_VERSION\n"); // Eventually new builds will no longer include this build flag + + // Check for the old style of region code strings, if found, convert to the new enum. + // Those strings will look like "1.0-EU433" + if (radioConfig.preferences.region == RegionCode_Unset && strncmp(myNodeInfo.region, "1.0-", 4) == 0) { + const char *regionStr = myNodeInfo.region + 4; // EU433 or whatever + for (const RegionInfo *r = regions; r->code != RegionCode_Unset; r++) + if (strcmp(r->name, regionStr) == 0) { + radioConfig.preferences.region = r->code; + break; + } + } + strncpy(myNodeInfo.firmware_version, optstr(APP_VERSION), sizeof(myNodeInfo.firmware_version)); strncpy(myNodeInfo.hw_model, HW_VENDOR, sizeof(myNodeInfo.hw_model)); resetRadioConfig(); // If bogus settings got saved, then fix them - DEBUG_MSG("NODENUM=0x%x, dbsize=%d\n", myNodeInfo.my_node_num, *numNodes); + DEBUG_MSG("legacy_region=%s, region=%d, NODENUM=0x%x, dbsize=%d\n", myNodeInfo.region, radioConfig.preferences.region, + myNodeInfo.my_node_num, *numNodes); } // We reserve a few nodenums for future use @@ -406,6 +445,12 @@ void NodeDB::updateFrom(const MeshPacket &mp) updateTextMessage = true; powerFSM.trigger(EVENT_RECEIVED_TEXT_MSG); notifyObservers(true); // Force an update whether or not our node counts have changed + + // This is going into the wifidev feature branch + // Only update the WebUI if WiFi is enabled + //#if WiFi_MODE != 0 + // notifyWebUI(); + //#endif } } break; diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index bd22f730f..e1a1b77d6 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -46,8 +46,13 @@ class NodeDB /// write to flash void saveToDisk(); - // Reinit radio config if needed, because sometimes a buggy android app might send us bogus settings - void resetRadioConfig(); + /** Reinit radio config if needed, because either: + * a) sometimes a buggy android app might send us bogus settings or + * b) the client set factory_reset + * + * @return true if the config was completely reset, in that case, we should send it back to the client + */ + bool resetRadioConfig(); /// given a subpacket sniffed from the network, update our DB state /// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index caadf8563..9ae5955d5 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -243,7 +243,10 @@ void PhoneAPI::handleSetRadio(const RadioConfig &r) { radioConfig = r; - service.reloadConfig(); + bool didReset = service.reloadConfig(); + if (didReset) { + state = STATE_SEND_MY_INFO; // Squirt a completely new set of configs to the client + } } /** diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 1a08f282e..1fcee0dc3 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -10,6 +10,24 @@ #include #include +#define RDEF(name, freq, spacing, num_ch, power_limit) \ + { \ + RegionCode_##name, num_ch, power_limit, freq, spacing, #name \ + } + +const RegionInfo regions[] = { + RDEF(US, 903.08f, 2.16f, 13, 0), RDEF(EU433, 433.175f, 0.2f, 8, 0), RDEF(EU865, 865.2f, 0.3f, 10, 0), + RDEF(CN, 470.0f, 2.0f, 20, 0), + RDEF(JP, 920.0f, 0.5f, 10, 13), // See https://github.com/meshtastic/Meshtastic-device/issues/346 power level 13 + RDEF(ANZ, 916.0f, 0.5f, 20, 0), // AU/NZ channel settings 915-928MHz + RDEF(KR, 921.9f, 0.2f, 8, 0), // KR channel settings (KR920-923) Start from TTN download channel + // freq. (921.9f is for download, others are for uplink) + RDEF(TW, 923.0f, 0.2f, 10, 0), // TW channel settings (AS2 bandplan 923-925MHz) + RDEF(Unset, 903.08f, 2.16f, 13, 0) // Assume US freqs if unset, Must be last +}; + +static const RegionInfo *myRegion; + /** * ## LoRaWAN for North America @@ -77,7 +95,15 @@ RadioInterface::RadioInterface() { assert(sizeof(PacketHeader) == 4 || sizeof(PacketHeader) == 16); // make sure the compiler did what we expected - myNodeInfo.num_channels = NUM_CHANNELS; + if (!myRegion) { + const RegionInfo *r = regions; + for (; r->code != RegionCode_Unset && r->code != radioConfig.preferences.region; r++) + ; + myRegion = r; + DEBUG_MSG("Wanted region %d, using %s\n", radioConfig.preferences.region, r->name); + + myNodeInfo.num_channels = myRegion->numChannels; // Tell our android app how many channels we have + } // Can't print strings this early - serial not setup yet // DEBUG_MSG("Set meshradio defaults name=%s\n", channelSettings.name); @@ -127,9 +153,12 @@ void RadioInterface::applyModemConfig() power = channelSettings.tx_power; + assert(myRegion); // Should have been found in init + // If user has manually specified a channel num, then use that, otherwise generate one by hashing the name - int channel_num = (channelSettings.channel_num ? channelSettings.channel_num - 1 : hash(channelSettings.name)) % NUM_CHANNELS; - freq = CH0 + CH_SPACING * channel_num; + int channel_num = + (channelSettings.channel_num ? channelSettings.channel_num - 1 : hash(channelSettings.name)) % myRegion->numChannels; + freq = myRegion->freq + myRegion->spacing * channel_num; DEBUG_MSG("Set radio: name=%s, config=%u, ch=%d, power=%d\n", channelSettings.name, channelSettings.modem_config, channel_num, power); @@ -143,9 +172,9 @@ void RadioInterface::limitPower() { uint8_t maxPower = 255; // No limit -#ifdef HW_VERSION_JP - maxPower = 13; // See https://github.com/meshtastic/Meshtastic-device/issues/346 -#endif + if (myRegion->powerLimit) + maxPower = myRegion->powerLimit; + if (power > maxPower) { DEBUG_MSG("Lowering transmit power because of regulatory limits\n"); power = maxPower; diff --git a/src/mesh/mesh.pb.c b/src/mesh/mesh.pb.c index ddc720cf3..11ec3f49f 100644 --- a/src/mesh/mesh.pb.c +++ b/src/mesh/mesh.pb.c @@ -59,3 +59,4 @@ PB_BIND(ManufacturingData, ManufacturingData, AUTO) + diff --git a/src/mesh/mesh.pb.h b/src/mesh/mesh.pb.h index 64b2758c3..b726ab6e1 100644 --- a/src/mesh/mesh.pb.h +++ b/src/mesh/mesh.pb.h @@ -25,6 +25,18 @@ typedef enum _Constants { Constants_Unused = 0 } Constants; +typedef enum _RegionCode { + RegionCode_Unset = 0, + RegionCode_US = 1, + RegionCode_EU433 = 2, + RegionCode_EU865 = 3, + RegionCode_CN = 4, + RegionCode_JP = 5, + RegionCode_ANZ = 6, + RegionCode_KR = 7, + RegionCode_TW = 8 +} RegionCode; + typedef enum _Data_Type { Data_Type_OPAQUE = 0, Data_Type_CLEAR_TEXT = 1, @@ -108,6 +120,8 @@ typedef struct _RadioConfig_UserPreferences { char wifi_ssid[33]; char wifi_password[64]; bool wifi_ap_mode; + RegionCode region; + bool factory_reset; pb_size_t ignore_incoming_count; uint32_t ignore_incoming[3]; } RadioConfig_UserPreferences; @@ -230,6 +244,10 @@ typedef struct _ToRadio { #define _Constants_MAX Constants_Unused #define _Constants_ARRAYSIZE ((Constants)(Constants_Unused+1)) +#define _RegionCode_MIN RegionCode_Unset +#define _RegionCode_MAX RegionCode_TW +#define _RegionCode_ARRAYSIZE ((RegionCode)(RegionCode_TW+1)) + #define _Data_Type_MIN Data_Type_OPAQUE #define _Data_Type_MAX Data_Type_CLEAR_READACK #define _Data_Type_ARRAYSIZE ((Data_Type)(Data_Type_CLEAR_READACK+1)) @@ -248,7 +266,7 @@ typedef struct _ToRadio { #define MeshPacket_init_default {0, 0, 0, {SubPacket_init_default}, 0, 0, 0, 0, 0} #define ChannelSettings_init_default {0, _ChannelSettings_ModemConfig_MIN, {0, {0}}, "", 0, 0, 0, 0} #define RadioConfig_init_default {false, RadioConfig_UserPreferences_init_default, false, ChannelSettings_init_default} -#define RadioConfig_UserPreferences_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "", "", 0, 0, {0, 0, 0}} +#define RadioConfig_UserPreferences_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "", "", 0, _RegionCode_MIN, 0, 0, {0, 0, 0}} #define NodeInfo_init_default {0, false, User_init_default, false, Position_init_default, 0, 0} #define MyNodeInfo_init_default {0, 0, 0, "", "", "", 0, 0, 0, 0, 0, 0, 0, 0} #define DeviceState_init_default {false, RadioConfig_init_default, false, MyNodeInfo_init_default, false, User_init_default, 0, {NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default, NodeInfo_init_default}, 0, {MeshPacket_init_default}, false, MeshPacket_init_default, 0, 0, 0} @@ -264,7 +282,7 @@ typedef struct _ToRadio { #define MeshPacket_init_zero {0, 0, 0, {SubPacket_init_zero}, 0, 0, 0, 0, 0} #define ChannelSettings_init_zero {0, _ChannelSettings_ModemConfig_MIN, {0, {0}}, "", 0, 0, 0, 0} #define RadioConfig_init_zero {false, RadioConfig_UserPreferences_init_zero, false, ChannelSettings_init_zero} -#define RadioConfig_UserPreferences_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "", "", 0, 0, {0, 0, 0}} +#define RadioConfig_UserPreferences_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "", "", 0, _RegionCode_MIN, 0, 0, {0, 0, 0}} #define NodeInfo_init_zero {0, false, User_init_zero, false, Position_init_zero, 0, 0} #define MyNodeInfo_init_zero {0, 0, 0, "", "", "", 0, 0, 0, 0, 0, 0, 0, 0} #define DeviceState_init_zero {false, RadioConfig_init_zero, false, MyNodeInfo_init_zero, false, User_init_zero, 0, {NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero, NodeInfo_init_zero}, 0, {MeshPacket_init_zero}, false, MeshPacket_init_zero, 0, 0, 0} @@ -322,7 +340,9 @@ typedef struct _ToRadio { #define RadioConfig_UserPreferences_wifi_ssid_tag 12 #define RadioConfig_UserPreferences_wifi_password_tag 13 #define RadioConfig_UserPreferences_wifi_ap_mode_tag 14 -#define RadioConfig_UserPreferences_ignore_incoming_tag 102 +#define RadioConfig_UserPreferences_region_tag 15 +#define RadioConfig_UserPreferences_factory_reset_tag 100 +#define RadioConfig_UserPreferences_ignore_incoming_tag 103 #define RouteDiscovery_route_tag 2 #define User_id_tag 1 #define User_long_name_tag 2 @@ -477,7 +497,9 @@ X(a, STATIC, SINGULAR, UINT32, min_wake_secs, 11) \ X(a, STATIC, SINGULAR, STRING, wifi_ssid, 12) \ X(a, STATIC, SINGULAR, STRING, wifi_password, 13) \ X(a, STATIC, SINGULAR, BOOL, wifi_ap_mode, 14) \ -X(a, STATIC, REPEATED, UINT32, ignore_incoming, 102) +X(a, STATIC, SINGULAR, UENUM, region, 15) \ +X(a, STATIC, SINGULAR, BOOL, factory_reset, 100) \ +X(a, STATIC, REPEATED, UINT32, ignore_incoming, 103) #define RadioConfig_UserPreferences_CALLBACK NULL #define RadioConfig_UserPreferences_DEFAULT NULL @@ -613,11 +635,11 @@ extern const pb_msgdesc_t ManufacturingData_msg; #define SubPacket_size 274 #define MeshPacket_size 313 #define ChannelSettings_size 84 -#define RadioConfig_size 277 -#define RadioConfig_UserPreferences_size 188 +#define RadioConfig_size 282 +#define RadioConfig_UserPreferences_size 193 #define NodeInfo_size 132 #define MyNodeInfo_size 110 -#define DeviceState_size 5429 +#define DeviceState_size 5434 #define DebugString_size 258 #define FromRadio_size 322 #define ToRadio_size 316 diff --git a/src/meshwifi/meshhttp.cpp b/src/meshwifi/meshhttp.cpp new file mode 100644 index 000000000..9106ab08b --- /dev/null +++ b/src/meshwifi/meshhttp.cpp @@ -0,0 +1,793 @@ +#include "meshwifi/meshhttp.h" +#include "NodeDB.h" +#include "configuration.h" +#include "main.h" +#include "meshwifi/meshwifi.h" +#include +#include + +WebServer webserver(80); + +// Maximum number of messages for chat history. Don't make this too big -- it'll use a +// lot of memory! +const uint16_t maxMessages = 50; + +struct message_t { + char sender[10]; + char message[250]; + int32_t gpsLat; + int32_t gpsLong; + uint32_t time; + bool fromMe; +}; + +struct messages_t { + message_t history[maxMessages]; +}; + +messages_t messages_history; + +String something = ""; +String sender = ""; + +void handleWebResponse() +{ + if (isWifiAvailable() == 0) { + return; + } + + // We're going to handle the DNS responder here so it + // will be ignored by the NRF boards. + handleDNSResponse(); + + webserver.handleClient(); +} + +void initWebServer() +{ + webserver.onNotFound(handleNotFound); + webserver.on("/json/chat/send/channel", handleJSONChatHistory); + webserver.on("/json/chat/send/user", handleJSONChatHistory); + webserver.on("/json/chat/history/channel", handleJSONChatHistory); + webserver.on("/json/chat/history/dummy", handleJSONChatHistoryDummy); + webserver.on("/json/chat/history/user", handleJSONChatHistory); + webserver.on("/json/stats", handleJSONChatHistory); + webserver.on("/hotspot-detect.html", handleHotspot); + webserver.on("/css/style.css", handleStyleCSS); + webserver.on("/scripts/script.js", handleScriptsScriptJS); + webserver.on("/", handleRoot); + webserver.begin(); +} + +void handleJSONChatHistory() +{ + int i; + + String out = ""; + out += "{\n"; + out += " \"data\" : {\n"; + out += " \"chat\" : "; + out += "["; + out += "\"" + sender + "\""; + out += ","; + out += "\"" + something + "\""; + out += "]\n"; + + for (i = 0; i < maxMessages; i++) { + out += "["; + out += "\"" + String(messages_history.history[i].sender) + "\""; + out += ","; + out += "\"" + String(messages_history.history[i].message) + "\""; + out += "]\n"; + } + + out += "\n"; + out += " }\n"; + out += "}\n"; + + webserver.send(200, "application/json", out); + return; +} + +void handleNotFound() +{ + String message = ""; + message += "File Not Found\n\n"; + message += "URI: "; + message += webserver.uri(); + message += "\nMethod: "; + message += (webserver.method() == HTTP_GET) ? "GET" : "POST"; + message += "\nArguments: "; + message += webserver.args(); + message += "\n"; + + for (uint8_t i = 0; i < webserver.args(); i++) { + message += " " + webserver.argName(i) + ": " + webserver.arg(i) + "\n"; + } + Serial.println(message); + webserver.send(404, "text/plain", message); +} + +/* + This supports the Apple Captive Network Assistant (CNA) Portal +*/ +void handleHotspot() +{ + DEBUG_MSG("Hotspot Request\n"); + + /* + If we don't do a redirect, be sure to return a "Success" message + otherwise iOS will have trouble detecting that the connection to the SoftAP worked. + */ + + String out = ""; + // out += "Success\n"; + out += "\n"; + webserver.send(200, "text/html", out); + return; +} + +void notifyWebUI() +{ + DEBUG_MSG("************ Got a message! ************\n"); + MeshPacket &mp = devicestate.rx_text_message; + NodeInfo *node = nodeDB.getNode(mp.from); + sender = (node && node->has_user) ? node->user.long_name : "???"; + + static char tempBuf[256]; // mesh.options says this is MeshPacket.encrypted max_size + assert(mp.decoded.which_payload == SubPacket_data_tag); + snprintf(tempBuf, sizeof(tempBuf), "%s", mp.decoded.data.payload.bytes); + + something = tempBuf; +} + +/* + To convert text to c strings: + + https://tomeko.net/online_tools/cpp_text_escape.php?lang=en +*/ +void handleRoot() +{ + + String out = ""; + out += + "\n" + "\n" + "\n" + "\n" + "\n" + " \n" + " Meshtastic - Chat\n" + " \n" + "\n" + "\n" + "\n" + "This area is under development. Please don't file bugs.\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\t\n" + "\t\tMeshtastic - Chat\n" + "\t\n" + "\n" + "\t\n" + " \n" + "\t \n" + "\t\tUsers\n" + "\t \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n" + " \n" + "\t\t All Users\n" + "\t\t \n" + "\n" + " \n" + " \n" + " \n" + " \n" + "\t\t\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " Send\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n" + "\n" + "\n" + "\n" + "\n" + ""; + webserver.send(200, "text/html", out); + return; +} + +void handleStyleCSS() +{ + + String out = ""; + out += + "/* latin-ext */\n" + "@font-face {\n" + " font-family: 'Lato';\n" + " font-style: normal;\n" + " font-weight: 400;\n" + " src: local('Lato Regular'), local('Lato-Regular'), url(./Google.woff2) format('woff2');\n" + " unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n" + "}\n" + "\n" + "\n" + "*, *:before, *:after {\n" + " box-sizing: border-box;\n" + "}\n" + "\n" + "body {\n" + " background: #C5DDEB;\n" + " font: 14px/20px \"Lato\", Arial, sans-serif;\n" + " padding: 40px 0;\n" + " color: white;\n" + "}\n" + "\n" + "\n" + " \n" + ".grid {\n" + " display: grid;\n" + " grid-template-columns:\n" + "\t1fr 4fr;\n" + " grid-template-areas:\n" + "\t\"header header\"\n" + "\t\"sidebar content\";\n" + " margin: 0 auto;\n" + " width: 750px;\n" + " background: #444753;\n" + " border-radius: 5px;\n" + "}\n" + "\n" + ".top {grid-area: header;}\n" + ".side {grid-area: sidebar;}\n" + ".main {grid-area: content;}\n" + "\n" + ".top {\n" + " border-bottom: 2px solid white;\n" + "}\n" + ".top-text {\n" + " font-weight: bold;\n" + " font-size: 24px;\n" + " text-align: center;\n" + " padding: 20px;\n" + "}\n" + "\n" + ".side {\n" + " width: 260px;\n" + " float: left;\n" + "}\n" + ".side .side-header {\n" + " padding: 20px;\n" + " border-bottom: 2px solid white;\n" + "}\n" + "\n" + ".side .side-header .side-text {\n" + " padding-left: 10px;\n" + " margin-top: 6px;\n" + " font-size: 16px;\n" + " text-align: left;\n" + " font-weight: bold;\n" + " \n" + "}\n" + "\n" + ".channel-list ul {\n" + " padding: 20px;\n" + " height: 570px;\n" + " list-style-type: none;\n" + "}\n" + ".channel-list ul li {\n" + " padding-bottom: 20px;\n" + "}\n" + "\n" + ".channel-list .channel-name {\n" + " font-size: 20px;\n" + " margin-top: 8px;\n" + " padding-left: 8px;\n" + "}\n" + "\n" + ".channel-list .message-count {\n" + " padding-left: 16px;\n" + " color: #92959E;\n" + "}\n" + "\n" + ".icon {\n" + " display: inline-block;\n" + " width: 1em;\n" + " height: 1em;\n" + " stroke-width: 0;\n" + " stroke: currentColor;\n" + " fill: currentColor;\n" + "}\n" + "\n" + ".icon-map-marker {\n" + " width: 0.5714285714285714em;\n" + "}\n" + "\n" + ".icon-circle {\n" + " width: 0.8571428571428571em;\n" + "}\n" + "\n" + ".content {\n" + " display: flex;\n" + " flex-direction: column;\n" + " flex-wrap: nowrap;\n" + "/* width: 490px; */\n" + " float: left;\n" + " background: #F2F5F8;\n" + "/* border-top-right-radius: 5px;\n" + " border-bottom-right-radius: 5px; */\n" + " color: #434651;\n" + "}\n" + ".content .content-header {\n" + " flex-grow: 0;\n" + " padding: 20px;\n" + " border-bottom: 2px solid white;\n" + "}\n" + "\n" + ".content .content-header .content-from {\n" + " padding-left: 10px;\n" + " margin-top: 6px;\n" + " font-size: 20px;\n" + " text-align: center;\n" + " font-size: 16px;\n" + "}\n" + ".content .content-header .content-from .content-from-highlight {\n" + " font-weight: bold;\n" + "}\n" + ".content .content-header .content-num-messages {\n" + " color: #92959E;\n" + "}\n" + "\n" + ".content .content-history {\n" + " flex-grow: 1;\n" + " padding: 20px 20px 20px;\n" + " border-bottom: 2px solid white;\n" + " overflow-y: scroll;\n" + " height: 375px;\n" + "}\n" + ".content .content-history ul {\n" + " list-style-type: none;\n" + " padding-inline-start: 10px;\n" + "}\n" + ".content .content-history .message-data {\n" + " margin-bottom: 10px;\n" + "}\n" + ".content .content-history .message-data-time {\n" + " color: #a8aab1;\n" + " padding-left: 6px;\n" + "}\n" + ".content .content-history .message {\n" + " color: white;\n" + " padding: 8px 10px;\n" + " line-height: 20px;\n" + " font-size: 14px;\n" + " border-radius: 7px;\n" + " margin-bottom: 30px;\n" + " width: 90%;\n" + " position: relative;\n" + "}\n" + ".content .content-history .message:after {\n" + " bottom: 100%;\n" + " left: 7%;\n" + " border: solid transparent;\n" + " content: \" \";\n" + " height: 0;\n" + " width: 0;\n" + " position: absolute;\n" + " pointer-events: none;\n" + " border-bottom-color: #86BB71;\n" + " border-width: 10px;\n" + " margin-left: -10px;\n" + "}\n" + ".content .content-history .my-message {\n" + " background: #86BB71;\n" + "}\n" + ".content .content-history .other-message {\n" + " background: #94C2ED;\n" + "}\n" + ".content .content-history .other-message:after {\n" + " border-bottom-color: #94C2ED;\n" + " left: 93%;\n" + "}\n" + ".content .content-message {\n" + " flex-grow: 0;\n" + " padding: 10px;\n" + "}\n" + ".content .content-message textarea {\n" + " width: 100%;\n" + " border: none;\n" + " padding: 10px 10px;\n" + " font: 14px/22px \"Lato\", Arial, sans-serif;\n" + " margin-bottom: 10px;\n" + " border-radius: 5px;\n" + " resize: none;\n" + "}\n" + "\n" + ".content .content-message button {\n" + " float: right;\n" + " color: #94C2ED;\n" + " font-size: 16px;\n" + " text-transform: uppercase;\n" + " border: none;\n" + " cursor: pointer;\n" + " font-weight: bold;\n" + " background: #F2F5F8;\n" + "}\n" + ".content .content-message button:hover {\n" + " color: #75b1e8;\n" + "}\n" + "/* Tooltip container */\n" + ".tooltip {\n" + " color: #86BB71;\n" + " position: relative;\n" + " display: inline-block;\n" + " border-bottom: 1px dotted black; /* If you want dots under the hoverable text */\n" + "}\n" + "/* Tooltip text */\n" + ".tooltip .tooltiptext {\n" + " visibility: hidden;\n" + " width: 120px;\n" + " background-color: #444753;\n" + " color: #fff;\n" + " text-align: center;\n" + " padding: 5px 0;\n" + " border-radius: 6px;\n" + " /* Position the tooltip text - see examples below! */\n" + " position: absolute;\n" + " z-index: 1;\n" + "}\n" + "\n" + "/* Show the tooltip text when you mouse over the tooltip container */\n" + ".tooltip:hover .tooltiptext {\n" + " visibility: visible;\n" + "}\n" + "\n" + ".online, .offline, .me {\n" + " margin-right: 3px;\n" + " font-size: 10px;\n" + "}\n" + "\n" + ".online {\n" + " color: #86BB71;\n" + "}\n" + "\n" + ".offline {\n" + " color: #E38968;\n" + "}\n" + "\n" + ".me {\n" + " color: #94C2ED;\n" + "}\n" + "\n" + ".align-left {\n" + " text-align: left;\n" + "}\n" + "\n" + ".align-right {\n" + " text-align: right;\n" + "}\n" + "\n" + ".float-right {\n" + " float: right;\n" + "}\n" + "\n" + ".clearfix:after {\n" + " visibility: hidden;\n" + " display: block;\n" + " font-size: 0;\n" + " content: \" \";\n" + " clear: both;\n" + " height: 0;\n" + "}"; + + webserver.send(200, "text/css", out); + return; +} + +void handleScriptsScriptJS() +{ + String out = ""; + out += "String.prototype.toHHMMSS = function () {\n" + " var sec_num = parseInt(this, 10); // don't forget the second param\n" + " var hours = Math.floor(sec_num / 3600);\n" + " var minutes = Math.floor((sec_num - (hours * 3600)) / 60);\n" + " var seconds = sec_num - (hours * 3600) - (minutes * 60);\n" + "\n" + " if (hours < 10) {hours = \"0\"+hours;}\n" + " if (minutes < 10) {minutes = \"0\"+minutes;}\n" + " if (seconds < 10) {seconds = \"0\"+seconds;}\n" + "// return hours+':'+minutes+':'+seconds;\n" + "\treturn hours+'h'+minutes+'m';\n" + "}\n" + "String.prototype.padLeft = function (length, character) { \n" + " return new Array(length - this.length + 1).join(character || ' ') + this; \n" + "};\n" + "\n" + "Date.prototype.toFormattedString = function () {\n" + " return [String(this.getFullYear()).substr(2, 2),\n" + "\t\t\tString(this.getMonth()+1).padLeft(2, '0'),\n" + " String(this.getDate()).padLeft(2, '0')].join(\"/\") + \" \" +\n" + " [String(this.getHours()).padLeft(2, '0'),\n" + " String(this.getMinutes()).padLeft(2, '0')].join(\":\");\n" + "};\n" + "\n" + "function getData(file) {\n" + "\tfetch(file)\n" + "\t.then(function (response) {\n" + "\t\treturn response.json();\n" + "\t})\n" + "\t.then(function (datafile) {\n" + "\t\tupdateData(datafile);\n" + "\t})\n" + "\t.catch(function (err) {\n" + "\t\tconsole.log('error: ' + err);\n" + "\t});\n" + "}\n" + "\t\n" + "function updateData(datafile) {\n" + "// Update System Details\n" + "\tupdateSystem(datafile);\n" + "//\tUpdate Userlist and message count\n" + "\tupdateUsers(datafile);\n" + "// Update Chat\n" + "\tupdateChat(datafile);\n" + "}\n" + "\n" + "function updateSystem(datafile) {\n" + "// Update System Info \n" + "\tvar sysContainer = document.getElementById(\"content-from-id\");\n" + "\tvar newHTML = datafile.data.system.channel;\n" + "\tvar myDate = new Date( datafile.data.system.timeGPS *1000);\n" + "\tnewHTML += ' @' + myDate.toFormattedString();\n" + "\tvar newSec = datafile.data.system.timeSinceStart;\n" + "\tvar strsecondUp = newSec.toString();\n" + "\tnewHTML += ' Up:' + strsecondUp.toHHMMSS();\n" + "\tsysContainer.innerHTML = newHTML;\n" + "}\n" + "\n" + "function updateUsers(datafile) {\n" + "\tvar mainContainer = document.getElementById(\"userlist-id\");\n" + "\tvar htmlUsers = '';\n" + "\tvar timeBase = datafile.data.system.timeSinceStart;\n" + "//\tvar lookup = {};\n" + " for (var i = 0; i < datafile.data.users.length; i++) {\n" + " htmlUsers += formatUsers(datafile.data.users[i],timeBase);\n" + "\t}\n" + "\tmainContainer.innerHTML = htmlUsers;\n" + "}\n" + "\n" + "function formatUsers(user,timeBase) {\n" + "\tnewHTML = '';\n" + " newHTML += '' + user.NameLong + '(' + user.NameShort + ')';\n" + " newHTML += '';\n" + "\tvar secondsLS = timeBase - user.lastSeen;\n" + "\tvar strsecondsLS = secondsLS.toString();\n" + "\tnewHTML += 'Seen: '+strsecondsLS.toHHMMSS()+' ago ';\n" + "\tif (user.lat == 0 || user.lon == 0) {\n" + "\t\tnewHTML += '';\n" + "\t} else {\n" + "\t\tnewHTML += 'lat:' + user.lat + ' lon:'+ user.lon+ " + "'';\n" + "\t}\n" + " newHTML += '';\n" + " newHTML += '';\n" + "\treturn(newHTML);\n" + "}\n" + "\n" + "function onlineStatus(time) {\n" + "\tif (time < 3600) {\n" + "\t\treturn \"online\"\n" + "\t} else {\n" + "\t\treturn \"offline\"\n" + "\t}\n" + "}\n" + "\n" + "function updateChat(datafile) {\n" + "// Update Chat\n" + "\tvar chatContainer = document.getElementById(\"chat-history-id\");\n" + "\tvar htmlChat = '';\n" + "\tvar timeBase = datafile.data.system.timeSinceStart;\n" + "\tfor (var i = 0; i < datafile.data.chat.length; i++) {\n" + "\t\thtmlChat += formatChat(datafile.data.chat[i],timeBase);\n" + "\t}\n" + "\tchatContainer.innerHTML = htmlChat;\n" + "\tscrollHistory();\n" + "}\n" + "\n" + "function formatChat(data,timeBase) {\n" + "\tvar secondsTS = timeBase - data.timestamp;\n" + "\tvar strsecondsTS = secondsTS.toString();\n" + "\tnewHTML = '';\n" + "\tif (data.local == 1) {\n" + "\t\tnewHTML += '';\n" + "\t\tnewHTML += '' + data.NameLong + '(' + data.NameShort + ')';\n" + "\t\tnewHTML += '' + strsecondsTS.toHHMMSS() + ' ago';\n" + "\t\tnewHTML += '';\n" + "\t\tnewHTML += '' + data.chatLine + '';\n" + "\t} else {\n" + "\t\tnewHTML += '';\n" + "\t\tnewHTML += '' + strsecondsTS.toHHMMSS() + ' ago ';\n" + "\t\tnewHTML += '' + data.NameLong + '(' + data.NameShort + ')';\n" + "//\t\tnewHTML += '';\n" + "\t\tnewHTML += '';\n" + "\t\tnewHTML += '' + data.chatLine + '';\n" + "\t}\n" + "\n" + " newHTML += '';\n" + "\treturn(newHTML);\t\n" + "}\n" + "\n" + "function scrollHistory() {\n" + "\tvar chatContainer = document.getElementById(\"chat-div-id\");\n" + "\tchatContainer.scrollTop = chatContainer.scrollHeight;\n" + "}\n" + "\n" + "\n" + "getData('/json/chat/history/dummy');\n" + "\n" + "\n" + "//window.onload=function(){\n" + "//\talert('onload');\n" + "// Async - Run scroll 0.5sec after onload event\n" + "//\tsetTimeout(scrollHistory(),500);\n" + "// }"; + + webserver.send(200, "text/javascript", out); + return; +} + +void handleJSONChatHistoryDummy() +{ + String out = ""; + out += "{\n" + "\t\"data\": {\n" + "\t\t\"system\": {\n" + "\t\t\t\"timeSinceStart\": 3213544,\n" + "\t\t\t\"timeGPS\": 1600830985,\n" + "\t\t\t\"channel\": \"ourSecretPlace\"\n" + "\t\t},\n" + "\t\t\"users\": [{\n" + "\t\t\t\t\"NameShort\": \"J\",\n" + "\t\t\t\t\"NameLong\": \"John\",\n" + "\t\t\t\t\"lastSeen\": 3207544,\n" + "\t\t\t\t\"lat\" : -2.882243,\n" + "\t\t\t\t\"lon\" : -111.038580\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"NameShort\": \"D\",\n" + "\t\t\t\t\"NameLong\": \"David\",\n" + "\t\t\t\t\"lastSeen\": 3212544,\n" + "\t\t\t\t\"lat\" : -12.24452,\n" + "\t\t\t\t\"lon\" : -61.87351\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"NameShort\": \"P\",\n" + "\t\t\t\t\"NameLong\": \"Peter\",\n" + "\t\t\t\t\"lastSeen\": 3213444,\n" + "\t\t\t\t\"lat\" : 0,\n" + "\t\t\t\t\"lon\" : 0\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"NameShort\": \"M\",\n" + "\t\t\t\t\"NameLong\": \"Mary\",\n" + "\t\t\t\t\"lastSeen\": 3211544,\n" + "\t\t\t\t\"lat\" : 16.45478,\n" + "\t\t\t\t\"lon\" : 11.40166\n" + "\t\t\t}\n" + "\t\t],\n" + "\t\t\"chat\": [{\n" + "\t\t\t\t\"local\": 0,\n" + "\t\t\t\t\"NameShort\": \"J\",\n" + "\t\t\t\t\"NameLong\": \"John\",\n" + "\t\t\t\t\"chatLine\": \"Hello\",\n" + "\t\t\t\t\"timestamp\" : 3203544\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"local\": 0,\n" + "\t\t\t\t\"NameShort\": \"D\",\n" + "\t\t\t\t\"NameLong\": \"David\",\n" + "\t\t\t\t\"chatLine\": \"Hello There\",\n" + "\t\t\t\t\"timestamp\" : 3204544\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"local\": 0,\n" + "\t\t\t\t\"NameShort\": \"J\",\n" + "\t\t\t\t\"NameLong\": \"John\",\n" + "\t\t\t\t\"chatLine\": \"Where you been?\",\n" + "\t\t\t\t\"timestamp\" : 3205544\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"local\": 0,\n" + "\t\t\t\t\"NameShort\": \"D\",\n" + "\t\t\t\t\"NameLong\": \"David\",\n" + "\t\t\t\t\"chatLine\": \"I was on Channel 2\",\n" + "\t\t\t\t\"timestamp\" : 3206544\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"local\": 0,\n" + "\t\t\t\t\"NameShort\": \"J\",\n" + "\t\t\t\t\"NameLong\": \"John\",\n" + "\t\t\t\t\"chatLine\": \"With Mary again?\",\n" + "\t\t\t\t\"timestamp\" : 3207544\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"local\": 0,\n" + "\t\t\t\t\"NameShort\": \"D\",\n" + "\t\t\t\t\"NameLong\": \"David\",\n" + "\t\t\t\t\"chatLine\": \"She's better looking than you\",\n" + "\t\t\t\t\"timestamp\" : 3208544\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"local\": 0,\n" + "\t\t\t\t\"NameShort\": \"M\",\n" + "\t\t\t\t\"NameLong\": \"Mary\",\n" + "\t\t\t\t\"chatLine\": \"Well, Hi\",\n" + "\t\t\t\t\"timestamp\" : 3209544\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"local\": 0,\n" + "\t\t\t\t\"NameShort\": \"D\",\n" + "\t\t\t\t\"NameLong\": \"David\",\n" + "\t\t\t\t\"chatLine\": \"You're Here\",\n" + "\t\t\t\t\"timestamp\" : 3210544\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"local\": 0,\n" + "\t\t\t\t\"NameShort\": \"M\",\n" + "\t\t\t\t\"NameLong\": \"Mary\",\n" + "\t\t\t\t\"chatLine\": \"Wanted to say Howdy.\",\n" + "\t\t\t\t\"timestamp\" : 3211544\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"local\": 0,\n" + "\t\t\t\t\"NameShort\": \"D\",\n" + "\t\t\t\t\"NameLong\": \"David\",\n" + "\t\t\t\t\"chatLine\": \"Better come down and visit sometime\",\n" + "\t\t\t\t\"timestamp\" : 3212544\n" + "\t\t\t},\n" + "\t\t\t{\n" + "\t\t\t\t\"local\": 1,\n" + "\t\t\t\t\"NameShort\": \"P\",\n" + "\t\t\t\t\"NameLong\": \"Peter\",\n" + "\t\t\t\t\"chatLine\": \"Where is everybody?\",\n" + "\t\t\t\t\"timestamp\" : 3213444\n" + "\t\t\t}\n" + "\t\t]\n" + "\t}\n" + "}"; + + webserver.send(200, "application/json", out); + return; +} \ No newline at end of file diff --git a/src/meshwifi/meshhttp.h b/src/meshwifi/meshhttp.h new file mode 100644 index 000000000..42c6644bf --- /dev/null +++ b/src/meshwifi/meshhttp.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +void initWebServer(); + +void handleNotFound(); + +void handleWebResponse(); + +void handleJSONChatHistory(); + +void notifyWebUI(); + +void handleHotspot(); + + +void handleStyleCSS(); +void handleRoot(); +void handleScriptsScriptJS(); +void handleJSONChatHistoryDummy(); \ No newline at end of file diff --git a/src/meshwifi/meshwifi.cpp b/src/meshwifi/meshwifi.cpp new file mode 100644 index 000000000..0c775b5c8 --- /dev/null +++ b/src/meshwifi/meshwifi.cpp @@ -0,0 +1,255 @@ +#include "meshwifi.h" +#include "NodeDB.h" +#include "WiFiServerAPI.h" +#include "configuration.h" +#include "main.h" +#include "meshwifi/meshhttp.h" +#include +#include + +static void WiFiEvent(WiFiEvent_t event); + +// DNS Server for the Captive Portal +DNSServer dnsServer; + +static WiFiServerPort *apiPort; + +uint8_t wifiDisconnectReason = 0; + +bool isWifiAvailable() +{ + const char *wifiName = radioConfig.preferences.wifi_ssid; + const char *wifiPsw = radioConfig.preferences.wifi_password; + + if (*wifiName && *wifiPsw) { + + // Once every 10 seconds, try to reconnect. + + return 1; + } else { + return 0; + } +} + +// Disable WiFi +void deinitWifi() +{ + /* + Note from Jm (jm@casler.org - Sept 16, 2020): + + A bug in the ESP32 SDK was introduced in Oct 2019 that keeps the WiFi radio from + turning back on after it's shut off. See: + https://github.com/espressif/arduino-esp32/issues/3522 + + Until then, WiFi should only be allowed when there's no power + saving on the 2.4g transceiver. + */ + + WiFi.mode(WIFI_MODE_NULL); + DEBUG_MSG("WiFi Turned Off\n"); + // WiFi.printDiag(Serial); +} + +// Startup WiFi +void initWifi() +{ + if (isWifiAvailable() == 0) { + return; + } + + if (radioConfig.has_preferences) { + const char *wifiName = radioConfig.preferences.wifi_ssid; + const char *wifiPsw = radioConfig.preferences.wifi_password; + + if (*wifiName && *wifiPsw) { + if (radioConfig.preferences.wifi_ap_mode) { + + IPAddress apIP(192, 168, 42, 1); + WiFi.onEvent(WiFiEvent); + + WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0)); + DEBUG_MSG("STARTING WIFI AP: ssid=%s, ok=%d\n", wifiName, WiFi.softAP(wifiName, wifiPsw)); + DEBUG_MSG("MY IP ADDRESS: %s\n", WiFi.softAPIP().toString().c_str()); + + dnsServer.start(53, "*", apIP); + + } else { + WiFi.mode(WIFI_MODE_STA); + WiFi.onEvent(WiFiEvent); + // esp_wifi_set_ps(WIFI_PS_NONE); // Disable power saving + + //WiFiEventId_t eventID = WiFi.onEvent( + WiFi.onEvent( + [](WiFiEvent_t event, WiFiEventInfo_t info) { + Serial.print("\nWiFi lost connection. Reason: "); + Serial.println(info.disconnected.reason); + + /* + If we are disconnected from the AP for some reason, + save the error code. + + For a reference to the codes: + https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code + */ + wifiDisconnectReason = info.disconnected.reason; + }, + WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED); + + DEBUG_MSG("JOINING WIFI: ssid=%s\n", wifiName); + if (WiFi.begin(wifiName, wifiPsw) == WL_CONNECTED) { + DEBUG_MSG("MY IP ADDRESS: %s\n", WiFi.localIP().toString().c_str()); + } else { + DEBUG_MSG("Started Joining WIFI\n"); + } + } + } + } else + DEBUG_MSG("Not using WIFI\n"); +} + +/// Perform idle loop processing required by the wifi layer +void loopWifi() +{ + // FIXME, once we have coroutines - just use a coroutine instead of this nasty loopWifi() + if (apiPort) + apiPort->loop(); +} + +static void initApiServer() +{ + // Start API server on port 4403 + if (!apiPort) { + apiPort = new WiFiServerPort(); + apiPort->init(); + } +} + +// Called by the Espressif SDK to +static void WiFiEvent(WiFiEvent_t event) +{ + DEBUG_MSG("************ [WiFi-event] event: %d ************\n", event); + + switch (event) { + case SYSTEM_EVENT_WIFI_READY: + DEBUG_MSG("WiFi interface ready\n"); + break; + case SYSTEM_EVENT_SCAN_DONE: + DEBUG_MSG("Completed scan for access points\n"); + break; + case SYSTEM_EVENT_STA_START: + DEBUG_MSG("WiFi client started\n"); + break; + case SYSTEM_EVENT_STA_STOP: + DEBUG_MSG("WiFi clients stopped\n"); + break; + case SYSTEM_EVENT_STA_CONNECTED: + DEBUG_MSG("Connected to access point\n"); + break; + case SYSTEM_EVENT_STA_DISCONNECTED: + DEBUG_MSG("Disconnected from WiFi access point\n"); + // Event 5 + + reconnectWiFi(); + break; + case SYSTEM_EVENT_STA_AUTHMODE_CHANGE: + DEBUG_MSG("Authentication mode of access point has changed\n"); + break; + case SYSTEM_EVENT_STA_GOT_IP: + DEBUG_MSG("Obtained IP address: \n"); + Serial.println(WiFi.localIP()); + + // Start web server + initWebServer(); + initApiServer(); + + break; + case SYSTEM_EVENT_STA_LOST_IP: + DEBUG_MSG("Lost IP address and IP address is reset to 0\n"); + break; + case SYSTEM_EVENT_STA_WPS_ER_SUCCESS: + DEBUG_MSG("WiFi Protected Setup (WPS): succeeded in enrollee mode\n"); + break; + case SYSTEM_EVENT_STA_WPS_ER_FAILED: + DEBUG_MSG("WiFi Protected Setup (WPS): failed in enrollee mode\n"); + break; + case SYSTEM_EVENT_STA_WPS_ER_TIMEOUT: + DEBUG_MSG("WiFi Protected Setup (WPS): timeout in enrollee mode\n"); + break; + case SYSTEM_EVENT_STA_WPS_ER_PIN: + DEBUG_MSG("WiFi Protected Setup (WPS): pin code in enrollee mode\n"); + break; + case SYSTEM_EVENT_AP_START: + DEBUG_MSG("WiFi access point started\n"); + Serial.println(WiFi.softAPIP()); + + // Start web server + initWebServer(); + initApiServer(); + + break; + case SYSTEM_EVENT_AP_STOP: + DEBUG_MSG("WiFi access point stopped\n"); + break; + case SYSTEM_EVENT_AP_STACONNECTED: + DEBUG_MSG("Client connected\n"); + break; + case SYSTEM_EVENT_AP_STADISCONNECTED: + DEBUG_MSG("Client disconnected\n"); + break; + case SYSTEM_EVENT_AP_STAIPASSIGNED: + DEBUG_MSG("Assigned IP address to client\n"); + break; + case SYSTEM_EVENT_AP_PROBEREQRECVED: + DEBUG_MSG("Received probe request\n"); + break; + case SYSTEM_EVENT_GOT_IP6: + DEBUG_MSG("IPv6 is preferred\n"); + break; + case SYSTEM_EVENT_ETH_START: + DEBUG_MSG("Ethernet started\n"); + break; + case SYSTEM_EVENT_ETH_STOP: + DEBUG_MSG("Ethernet stopped\n"); + break; + case SYSTEM_EVENT_ETH_CONNECTED: + DEBUG_MSG("Ethernet connected\n"); + break; + case SYSTEM_EVENT_ETH_DISCONNECTED: + DEBUG_MSG("Ethernet disconnected\n"); + break; + case SYSTEM_EVENT_ETH_GOT_IP: + DEBUG_MSG("Obtained IP address\n"); + break; + default: + break; + } +} + +void handleDNSResponse() +{ + if (radioConfig.preferences.wifi_ap_mode) { + dnsServer.processNextRequest(); + } +} + +void reconnectWiFi() +{ + const char *wifiName = radioConfig.preferences.wifi_ssid; + const char *wifiPsw = radioConfig.preferences.wifi_password; + + if (radioConfig.has_preferences) { + + if (*wifiName && *wifiPsw) { + + DEBUG_MSG("... Reconnecting to WiFi access point"); + + WiFi.mode(WIFI_MODE_STA); + WiFi.begin(wifiName, wifiPsw); + } + } +} + +uint8_t getWifiDisconnectReason() +{ + return wifiDisconnectReason; +} \ No newline at end of file diff --git a/src/meshwifi/meshwifi.h b/src/meshwifi/meshwifi.h new file mode 100644 index 000000000..f22d69a63 --- /dev/null +++ b/src/meshwifi/meshwifi.h @@ -0,0 +1,25 @@ +#pragma once + +#include "configuration.h" +#include +#include + +#ifdef HAS_WIFI +#include +#include +#endif + +void initWifi(); +void deinitWifi(); + +/// Perform idle loop processing required by the wifi layer +void loopWifi(); + +bool isWifiAvailable(); + +void handleDNSResponse(); + +void reconnectWiFi(); + +uint8_t getWifiDisconnectReason(); + diff --git a/src/nimble/BluetoothUtil.cpp b/src/nimble/BluetoothUtil.cpp index 490bc13d4..f294802cd 100644 --- a/src/nimble/BluetoothUtil.cpp +++ b/src/nimble/BluetoothUtil.cpp @@ -1,10 +1,9 @@ #include "BluetoothUtil.h" #include "BluetoothSoftwareUpdate.h" #include "NimbleBluetoothAPI.h" -#include "NodeDB.h" // FIXME - we shouldn't really douch this here - we are using it only because we currently do wifi setup when ble gets turned on #include "PhoneAPI.h" #include "PowerFSM.h" -#include "WiFi.h" +#include #include "configuration.h" #include "esp_bt.h" #include "host/util/util.h" @@ -13,6 +12,7 @@ #include "services/gap/ble_svc_gap.h" #include "services/gatt/ble_svc_gatt.h" #include +#include "meshwifi/meshwifi.h" static bool pinShowing; @@ -503,33 +503,8 @@ void reinitBluetooth() nimble_port_freertos_init(ble_host_task); } -void initWifi() -{ - // Note: Wifi is not yet supported ;-) - strcpy(radioConfig.preferences.wifi_ssid, ""); - strcpy(radioConfig.preferences.wifi_password, ""); - if (radioConfig.has_preferences) { - const char *wifiName = radioConfig.preferences.wifi_ssid; - - if (*wifiName) { - const char *wifiPsw = radioConfig.preferences.wifi_password; - if (radioConfig.preferences.wifi_ap_mode) { - DEBUG_MSG("STARTING WIFI AP: ssid=%s, ok=%d\n", wifiName, WiFi.softAP(wifiName, wifiPsw)); - } else { - WiFi.mode(WIFI_MODE_STA); - DEBUG_MSG("JOINING WIFI: ssid=%s\n", wifiName); - if (WiFi.begin(wifiName, wifiPsw) == WL_CONNECTED) { - DEBUG_MSG("MY IP ADDRESS: %s\n", WiFi.localIP().toString().c_str()); - } else { - DEBUG_MSG("Started Joining WIFI\n"); - } - } - } - } else - DEBUG_MSG("Not using WIFI\n"); -} - bool bluetoothOn; +bool firstTime = 1; // Enable/disable bluetooth. void setBluetoothEnable(bool on) @@ -542,11 +517,29 @@ void setBluetoothEnable(bool on) Serial.printf("Pre BT: %u heap size\n", ESP.getFreeHeap()); // ESP_ERROR_CHECK( heap_trace_start(HEAP_TRACE_LEAKS) ); reinitBluetooth(); - initWifi(); + + // Don't try to reconnect wifi before bluetooth is configured. + // WiFi is initialized from main.cpp in setup() . + if (firstTime) { + firstTime = 0; + } else { + initWifi(); + } } else { + + /* + // If WiFi is in use, disable shutting down the radio. + if (isWifiAvailable()) { + return; + } + */ + + // shutdown wifi + deinitWifi(); + // We have to totally teardown our bluetooth objects to prevent leaks deinitBLE(); - WiFi.mode(WIFI_MODE_NULL); // shutdown wifi + Serial.printf("Shutdown BT: %u heap size\n", ESP.getFreeHeap()); // ESP_ERROR_CHECK( heap_trace_stop() ); // heap_trace_dump(); diff --git a/src/nrf52/wifi-stubs.cpp b/src/nrf52/wifi-stubs.cpp new file mode 100644 index 000000000..30321e5e4 --- /dev/null +++ b/src/nrf52/wifi-stubs.cpp @@ -0,0 +1,16 @@ +#include "meshwifi/meshhttp.h" +#include "meshwifi/meshwifi.h" + +void initWifi() {} + +void deinitWifi() {} + +bool isWifiAvailable() +{ + return false; +} + +void handleWebResponse() {} + +/// Perform idle loop processing required by the wifi layer +void loopWifi() {} \ No newline at end of file diff --git a/src/portduino/wifi-stubs.cpp b/src/portduino/wifi-stubs.cpp new file mode 120000 index 000000000..b90e66295 --- /dev/null +++ b/src/portduino/wifi-stubs.cpp @@ -0,0 +1 @@ +../nrf52/wifi-stubs.cpp \ No newline at end of file