From 727ab96ee0681517a1f98a616ed252137647be72 Mon Sep 17 00:00:00 2001 From: Millibyte Products Date: Sat, 24 Jan 2026 15:00:02 -0500 Subject: [PATCH 01/42] Millibyte platforms and specific configuration --- ESP32/data/www/power-min.js | 1 + ESP32/lib/enum.h | 1 + ESP32/lib/pinMap.h | 76 +++++++---- ESP32/lib/settingConstants.h | 6 +- ESP32/platformio.ini | 134 +++++++++--------- ESP32/src/BatteryHandler.h | 45 +++--- ESP32/src/DisplayHandler.h | 179 +++++++++++------------- ESP32/src/HTTP/HTTPBase.h | 5 +- ESP32/src/HTTP/HTTPSHandler.hpp | 2 +- ESP32/src/MessageHandler.h | 72 ++++++++++ ESP32/src/NetworkHandler.h | 66 +++++++++ ESP32/src/TagHandler.h | 81 ++++++----- ESP32/src/TaskHandler.h | 234 ++++++++++++++++++++++++++++++++ ESP32/src/TemperatureHandler.h | 60 ++------ ESP32/src/VoiceHandler.hpp | 11 +- ESP32/src/WebHandler.h | 8 +- ESP32/src/WebHandler_psychic.h | 8 +- ESP32/src/WifiHandler.h | 11 +- ESP32/src/main.cpp | 66 +++------ 19 files changed, 710 insertions(+), 356 deletions(-) create mode 100644 ESP32/data/www/power-min.js create mode 100644 ESP32/src/MessageHandler.h create mode 100644 ESP32/src/NetworkHandler.h create mode 100644 ESP32/src/TaskHandler.h diff --git a/ESP32/data/www/power-min.js b/ESP32/data/www/power-min.js new file mode 100644 index 0000000..d818008 --- /dev/null +++ b/ESP32/data/www/power-min.js @@ -0,0 +1 @@ +function powerSetup(){isBoardType(BoardType.SR6PCB)?togglePowerSettings(!0):togglePowerSettings(!1)}function wsPowerStatus(e){var t=e.message,o=t.servoVoltage,n=t.inputVoltage;document.getElementById("currentServoVoltage").value=o,document.getElementById("currentInputVoltage").value=n}function togglePowerSettings(e){Utils.toggleControlVisibilityByClassName("powerOnly",e)} \ No newline at end of file diff --git a/ESP32/lib/enum.h b/ESP32/lib/enum.h index 7322e93..2c4df28 100644 --- a/ESP32/lib/enum.h +++ b/ESP32/lib/enum.h @@ -48,6 +48,7 @@ enum class BoardType: int CRIMZZON, ISAAC, SSR1PCB, + SR6PCB, MAX }; #if MOTOR_TYPE == 1 diff --git a/ESP32/lib/pinMap.h b/ESP32/lib/pinMap.h index fd5e3d2..0aa1814 100644 --- a/ESP32/lib/pinMap.h +++ b/ESP32/lib/pinMap.h @@ -72,9 +72,9 @@ // class PinMapInfo { // public: -// PinMapInfo(DeviceType deviceType, BoardType boardType, PinMap* pinMap): -// m_deviceType(deviceType), -// m_boardType(boardType), +// PinMapInfo(DeviceType deviceType, BoardType boardType, PinMap* pinMap): +// m_deviceType(deviceType), +// m_boardType(boardType), // m_pinMap(pinMap) { } // DeviceType deviceType() { @@ -88,7 +88,7 @@ // template::value>> // const T pinMap() { // return static_cast(m_pinMap); -// } +// } // const PinMap* pinMap() { // return m_pinMap; // } @@ -102,7 +102,7 @@ class PinMap { public: PinMap(PinMap const&) = delete; void operator=(PinMap const&) = delete; - + DeviceType deviceType() { return m_deviceType; } void setDeviceType(DeviceType deviceType) { m_deviceType = deviceType; } BoardType boardType() { return m_boardType; } @@ -169,13 +169,13 @@ class PinMap { void setSleeveTemp(const int8_t &sleeveTemp) { m_sleeveTemp = sleeveTemp; } int8_t buttonSetPin(int8_t index) const { return m_buttonSetPins[index]; } - void setButtonSetPin(const int8_t pin, int8_t index) { + void setButtonSetPin(const int8_t pin, int8_t index) { if(index >= MAX_BUTTON_SETS) { LogHandler::error("Pin_map", "Invalid index for button set %d", index); return; } - m_buttonSetPins[index] = pin; + m_buttonSetPins[index] = pin; } int8_t i2cSda() const { return m_i2cSda; } @@ -243,43 +243,43 @@ class PinMap { ESPTimer m_timers[MAX_TIMERS] = { #if CONFIG_IDF_TARGET_ESP32 { ESP_H_TIMER0_FREQUENCY, "High 0", ESP_TIMER_FREQUENCY_DEFAULT, { - {"High 0 CH0", ESPTimerChannelNum::HIGH0_CH0}, + {"High 0 CH0", ESPTimerChannelNum::HIGH0_CH0}, {"High 0 CH1", ESPTimerChannelNum::HIGH0_CH1} } }, { ESP_H_TIMER1_FREQUENCY, "High 1", ESP_TIMER_FREQUENCY_DEFAULT, { - {"High 1 CH2", ESPTimerChannelNum::HIGH1_CH2}, + {"High 1 CH2", ESPTimerChannelNum::HIGH1_CH2}, {"High 1 CH3", ESPTimerChannelNum::HIGH1_CH3} } }, { ESP_H_TIMER2_FREQUENCY, "High 2", ESP_TIMER_FREQUENCY_DEFAULT, { - {"High 2 CH4", ESPTimerChannelNum::HIGH2_CH4}, + {"High 2 CH4", ESPTimerChannelNum::HIGH2_CH4}, {"High 2 CH5", ESPTimerChannelNum::HIGH2_CH5} } }, { ESP_H_TIMER3_FREQUENCY, "High 3", ESP_TIMER_FREQUENCY_DEFAULT, { - {"High 3 CH6", ESPTimerChannelNum::HIGH3_CH6}, + {"High 3 CH6", ESPTimerChannelNum::HIGH3_CH6}, {"High 3 CH7", ESPTimerChannelNum::HIGH3_CH7} } }, #endif { ESP_L_TIMER0_FREQUENCY, "Low 0", ESP_TIMER_FREQUENCY_DEFAULT, { - {"Low 0 CH0", ESPTimerChannelNum::LOW0_CH0}, + {"Low 0 CH0", ESPTimerChannelNum::LOW0_CH0}, {"Low 0 CH1", ESPTimerChannelNum::LOW0_CH1} } }, { ESP_L_TIMER1_FREQUENCY, "Low 1", ESP_TIMER_FREQUENCY_DEFAULT, { - {"Low 1 CH2", ESPTimerChannelNum::LOW1_CH2}, + {"Low 1 CH2", ESPTimerChannelNum::LOW1_CH2}, {"Low 1 CH3", ESPTimerChannelNum::LOW1_CH3} } }, { ESP_L_TIMER2_FREQUENCY, "Low 2", ESP_TIMER_FREQUENCY_DEFAULT, { - {"Low 2 CH4", ESPTimerChannelNum::LOW2_CH4}, + {"Low 2 CH4", ESPTimerChannelNum::LOW2_CH4}, {"Low 2 CH5", ESPTimerChannelNum::LOW2_CH5} } }, { ESP_L_TIMER3_FREQUENCY, "Low 3", ESP_TIMER_FREQUENCY_DEFAULT, { - {"Low 3 CH6", ESPTimerChannelNum::LOW3_CH6}, + {"Low 3 CH6", ESPTimerChannelNum::LOW3_CH6}, {"Low 3 CH7", ESPTimerChannelNum::LOW3_CH7} } } @@ -287,13 +287,13 @@ class PinMap { // void setCommonTimers() { // m_timers.timerH3 = { - // ESP_TIMER_FREQUENCY_DEFAULT, + // ESP_TIMER_FREQUENCY_DEFAULT, // { - // {SQUEEZE_PIN, SqueezeServo_PWM, squeeze()}, + // {SQUEEZE_PIN, SqueezeServo_PWM, squeeze()}, // {TWIST_SERVO_PIN, TwistServo_PWM, twist()} // } // }; - + // } private: // PWM @@ -364,7 +364,7 @@ class PinMapSSR1 : public PinMap { int8_t pwmChannel3() const { return m_pwmChannel3; } void setPwmChannel3(const int8_t &pwmChannel3) { m_pwmChannel3 = pwmChannel3; } -protected: +protected: PinMapSSR1(DeviceType deviceType, BoardType boardType) : PinMap(deviceType, boardType) {} private: int8_t m_encoder = BLDC_ENCODER_PIN_DEFAULT; @@ -400,7 +400,7 @@ class PinMapOSR : public PinMap { int8_t pitchLeftChannel() const { return m_pitchLeftChannel; } void setPitchLeftChannel(const int8_t &pitchLeftChannel) { m_pitchLeftChannel = pitchLeftChannel; } -protected: +protected: PinMapOSR(DeviceType deviceType, BoardType boardType) : PinMap(deviceType, boardType) {} private: int8_t m_pitchLeft = PITCH_LEFT_SERVO_PIN_DEFAULT; @@ -467,7 +467,7 @@ class PinMapSR6 : public PinMapOSR { int8_t leftUpperServoChannel() const { return m_leftUpperServoChannel; } void setLeftUpperServoChannel(const int8_t &channel) { m_leftUpperServoChannel = channel; } -protected: +protected: PinMapSR6(DeviceType deviceType, BoardType boardType) : PinMapOSR(deviceType, boardType) {} private: int8_t m_pitchRight = PITCH_RIGHTSERVO_PIN_DEFAULT; @@ -511,7 +511,7 @@ class PinMapINControl : public PinMapSR6 { // // Vibe3_PIN = json["Vibe3_PIN"] | 32; setHeater(5); } -protected: +protected: PinMapINControl(DeviceType deviceType, BoardType boardType) : PinMapSR6(deviceType, boardType) {} }; @@ -540,7 +540,7 @@ class PinMapSR6MB : public PinMapSR6 { // caseFanFrequency = json["caseFanFrequency"] | 25; // Display_Screen_Height = json["Display_Screen_Height"] | 32; } -protected: +protected: PinMapSR6MB(DeviceType deviceType, BoardType boardType) : PinMapSR6(deviceType, boardType) {} }; @@ -575,6 +575,34 @@ class PinMapSSR1PCB : public PinMapSSR1 { setHeater(-1); setTwistFeedBack(-1); } - protected: + protected: PinMapSSR1PCB(DeviceType deviceType, BoardType boardType) : PinMapSSR1(deviceType, boardType) {} +}; + +class PinMapSR6PCB : PinMapSR6 { + public: + static PinMapSR6PCB* getInstance() + { + static PinMapSR6PCB instance(DeviceType::SR6, BoardType::N8R8); + return &instance; + } + void overideDefaults() override { + setI2cSda(22); + setI2cScl(21); + + setValve(-1); + setTwist(-1); + setSqueeze(-1); + setVibe0(-1); + setVibe1(-1); + setVibe2(-1); + setVibe3(-1); + setSleeveTemp(-1); + setInternalTemp(-1); + setCaseFan(-1); + setHeater(-1); + setTwistFeedBack(-1); + } + protected: + PinMapSR6PCB(DeviceType deviceType, BoardType boardType) : PinMapSR6(deviceType, boardType) {} }; \ No newline at end of file diff --git a/ESP32/lib/settingConstants.h b/ESP32/lib/settingConstants.h index 5f27e33..25a4e77 100644 --- a/ESP32/lib/settingConstants.h +++ b/ESP32/lib/settingConstants.h @@ -17,7 +17,7 @@ #if MOTOR_TYPE == 0 #define DEVICE_TYPE_DEFAULT (uint8_t)DeviceType::OSR -#else +#else #define DEVICE_TYPE_DEFAULT (uint8_t)DeviceType::SSR1 #endif #define MOTOR_TYPE_DEFAULT MOTOR_TYPE @@ -33,6 +33,7 @@ #define AP_MODE_IP_DEFAULT "192.168.69.1" #define AP_MODE_GATEWAY_DEFAULT "192.168.69.254" #define AP_MODE_SUBNET_DEFAULT "255.255.255.0" +#ifndef BOARD_TYPE_DEFAULT #if CONFIG_IDF_TARGET_ESP32 #define BOARD_TYPE_DEFAULT (uint8_t)BoardType::DEVKIT #elif CONFIG_IDF_TARGET_ESP32S3 @@ -42,6 +43,9 @@ #define BOARD_TYPE_DEFAULT (uint8_t)BoardType::N8R8 #endif #endif +#else + #define BOARD_TYPE_DEFAULT BOARD_TYPE +#endif #define LOG_LEVEL_DEFAULT (uint8_t)LogLevel::INFO //#define FULL_BUILD_DEFAULT false #define TCODE_VERSION_DEFAULT (uint8_t)TCodeVersion::v0_3 diff --git a/ESP32/platformio.ini b/ESP32/platformio.ini index 3f06443..c8c573e 100644 --- a/ESP32/platformio.ini +++ b/ESP32/platformio.ini @@ -21,10 +21,10 @@ monitor_rts = 0 monitor_dtr = 0 [common] -lib_deps = +lib_deps = ;ArduinoJson@7.2.0 ArduinoJson@7.4.2 - + Arduino hacker-cb/MPark-Variant @ 1.4.0 ;Arduino_TCode_Parser @@ -32,12 +32,12 @@ lib_deps = https://github.com/multiaxis/TCode-Library#a5ec926 ;e8bb528 ;https://github.com/joltwallet/esp_littlefs.git ; required for espidf with arduino component - + https://github.com/jcfain/LTC2944-Arduino-Library.git dfrobot/DFRobot_DF2301Q@^1.0.0 ;nanopb/Nanopb @ 0.4.8 build_unflags = -; -std=gnu++11 +; -std=gnu++11 build_flags = -I src -I src/BLE @@ -58,7 +58,7 @@ lib_ldf_mode = chain ;to evaluate C/C++ Preprocessor conditional syntax for diff board_build.filesystem = littlefs [common:temperature] -lib_deps = +lib_deps = ;paulstoffregen/OneWire@2.3.8 ; https://github.com/PaulStoffregen/OneWire.git#72249e2 https://github.com/PaulStoffregen/OneWire.git#800f26f @@ -71,12 +71,12 @@ lib_deps = ;r-downing/AutoPID@^1.0.3 [common:display] -lib_deps = +lib_deps = ;adafruit/Adafruit SSD1306@2.5.10 adafruit/Adafruit SSD1306@2.5.15 [common:bldc] -lib_deps = +lib_deps = # Arduino V3 askuric/Simple FOC@2.3.5 simplefoc/SimpleFOCDrivers@1.0.9 @@ -102,14 +102,14 @@ framework = arduino ; platformio/framework-arduinoespressif32-libs @ https://github.com/espressif/arduino-esp32/releases/download/3.0.4/esp32-arduino-libs-3.0.4.zip ######################################################################################################################################################### extends = common, com-ports -lib_deps = +lib_deps = ${common.lib_deps} -build_flags = +build_flags = ${common.build_flags} -monitor_filters = esp32_exception_decoder +monitor_filters = esp32_exception_decoder ;direct - ;send_on_enter - ;colorize + ;send_on_enter + ;colorize ;debug @@ -122,8 +122,8 @@ build_flags = ${common.build_flags} -D ESP8266 framework = arduino monitor_filters = esp8266_exception_decoder - send_on_enter - colorize + send_on_enter + colorize ;debug ; [common:PICO] @@ -132,14 +132,14 @@ monitor_filters = esp8266_exception_decoder ; board = pico ; framework = arduino ; build_flags = -D PICO_BUILD -; lib_deps = ${common.lib_deps} +; lib_deps = ${common.lib_deps} ; khoih-prog/AsyncWebServer_RP2040W @ 1.5.0 [common:ESP32-wifi] # This is included as a required dependency in ESPAsyncWebServer lib_ignore = ;AsyncTCP -lib_deps = +lib_deps = ${common:ESP32.lib_deps} # Am getting a watchdog timeout loading the html page so I forked Async tcp to make a small modification for now. ;https://github.com/jcfain/AsyncTCP.git#3c03a52 @@ -152,7 +152,7 @@ lib_deps = build_flags = ${common:ESP32.build_flags} -D CONFIG_ASYNC_TCP_QUEUE_SIZE=64 -D ASYNCWEBSERVER_REGEX [common:ESP32-bluetooth] -lib_deps = +lib_deps = ${common:ESP32.lib_deps} h2zero/NimBLE-Arduino@2.3.4 ;https://github.com/h2zero/NimBLE-Arduino.git#7bdcae5 @@ -160,12 +160,12 @@ build_flags = -D NIMBLE_LATEST [common:ESP8266-wifi] extends = common:ESP8266 -lib_deps = +lib_deps = ${common:ESP8266.lib_deps} ESP Async WebServer@1.2.4 ;https://github.com/me-no-dev/ESPAsyncWebServer.git build_flags = ${common:ESP8266.build_flags} -D ASYNCWEBSERVER_REGEX - + #Common build [env:esp32doit-devkit-v1] extends = common:ESP32, common:ESP32-wifi @@ -175,30 +175,30 @@ build_type = release board_build.partitions = huge_app.csv ; f_cpu = 240000000L ; mcu = esp32 -build_flags = -D WROOM32_MODULE +build_flags = -D WROOM32_MODULE ${common:ESP32-wifi.build_flags} ${common:ESP32-bluetooth.build_flags} -D DEBUG_BUILD=0 -D BUILD_TEMP=1 -D BUILD_DISPLAY=1 -D BLUETOOTH_TCODE=0 -D BLE_TCODE=1 -D WIFI_TCODE=1 -D MOTOR_TYPE=0 -D SECURE_WEB=0 -D COEXIST=1 #-D CORE_DEBUG_LEVEL=5 #-D FW_VERSION=%%date%% -lib_deps = ${common:ESP32.lib_deps} - ${common:ESP32-wifi.lib_deps} - ${common:display.lib_deps} +lib_deps = ${common:ESP32.lib_deps} + ${common:ESP32-wifi.lib_deps} + ${common:display.lib_deps} ${common:temperature.lib_deps} ${common:ESP32-bluetooth.lib_deps} [env:esp32doit-devkit-v1-bldc] extends = env:esp32doit-devkit-v1 build_unflags = -D MOTOR_TYPE=0 -build_flags = ${env:esp32doit-devkit-v1.build_flags} - -D MOTOR_TYPE=1 -lib_deps = ${env:esp32doit-devkit-v1.lib_deps} - ${common:bldc.lib_deps} +build_flags = ${env:esp32doit-devkit-v1.build_flags} + -D MOTOR_TYPE=1 +lib_deps = ${env:esp32doit-devkit-v1.lib_deps} + ${common:bldc.lib_deps} lib_archive = false #required for SimpleFOC # Debug builds #Debug build that does NOT require special hardware [env:esp32doit-devkit-v1-debug] extends = env:esp32doit-devkit-v1 -build_unflags = -D DEBUG_BUILD=0 ;-Os +build_unflags = -D DEBUG_BUILD=0 ;-Os build_flags = ${env:esp32doit-devkit-v1.build_flags} -D DEBUG_BUILD=1 build_type = debug @@ -207,7 +207,7 @@ build_type = debug #https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/jtag-debugging/configure-ft2232h-jtag.html #Launch Zadig and select all devices. Then replace the Dual RS232-HS (interface 0) ONLY. Leave interface 1 at default. #leaving this note because my PC reverts drivers allot and I need redo these steps. -# IMPORTANT: DO NOT use pins 12, 13, 14 & 15 during debugging with esp-prog. +# IMPORTANT: DO NOT use pins 12, 13, 14 & 15 during debugging with esp-prog. # By default there are a few servos on these pins but removed from the debug build with the # ESP_PROG pre processor define [env:esp32doit-devkit-v1-debug-prog] @@ -238,24 +238,24 @@ build_type = release board_build.partitions = huge_app.csv build_flags = -D S3_ZERO -D ARDUINO_USB_MODE=1 - -D ARDUINO_USB_CDC_ON_BOOT=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 -D BOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ${common:ESP32-wifi.build_flags} ${common:ESP32-bluetooth.build_flags} - -D DEBUG_BUILD=0 -D BUILD_TEMP=1 -D BUILD_DISPLAY=1 -D BLUETOOTH_TCODE=0 -D BLE_TCODE=1 -D WIFI_TCODE=1 -D MOTOR_TYPE=0 -D SECURE_WEB=0 -D COEXIST=1 #-D CORE_DEBUG_LEVEL=5 -lib_deps = ${common:ESP32-wifi.lib_deps} - ${common:display.lib_deps} + -D DEBUG_BUILD=0 -D BUILD_TEMP=1 -D BUILD_DISPLAY=1 -D BLUETOOTH_TCODE=0 -D BLE_TCODE=1 -D WIFI_TCODE=1 -D MOTOR_TYPE=0 -D SECURE_WEB=0 -D COEXIST=1 #-D CORE_DEBUG_LEVEL=5 +lib_deps = ${common:ESP32-wifi.lib_deps} + ${common:display.lib_deps} ${common:temperature.lib_deps} - ${common:ESP32-bluetooth.lib_deps} + ${common:ESP32-bluetooth.lib_deps} [env:esp32-s3-zero-bldc] extends = env:esp32-s3-zero build_unflags = -D MOTOR_TYPE=0 -build_flags = ${env:esp32-s3-zero.build_flags} - -D MOTOR_TYPE=1 -lib_deps = ${env:esp32-s3-zero.lib_deps} - ${common:bldc.lib_deps} +build_flags = ${env:esp32-s3-zero.build_flags} + -D MOTOR_TYPE=1 +lib_deps = ${env:esp32-s3-zero.lib_deps} + ${common:bldc.lib_deps} lib_archive = false #required for SimpleFOC [env:esp32-s3-devkitc-1-N8R8] @@ -265,31 +265,31 @@ build_type = release board_build.partitions = default_8MB.csv ;board_build.partitions = max_app_8MB.csv board_build.arduino.memory_type = dio_opi ; NEEDED FOR PSRAM -;board_build.arduino.memory_type = opi_qspi +;board_build.arduino.memory_type = opi_qspi build_flags = -D BOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ${common:ESP32-wifi.build_flags} ${common:ESP32-bluetooth.build_flags} - -D DEBUG_BUILD=0 -D BUILD_TEMP=1 -D BUILD_DISPLAY=1 -D BLUETOOTH_TCODE=0 -D BLE_TCODE=1 -D WIFI_TCODE=1 -D MOTOR_TYPE=0 -D SECURE_WEB=0 -D COEXIST=1 #-D CORE_DEBUG_LEVEL=5 -lib_deps = ${common:ESP32-wifi.lib_deps} - ${common:display.lib_deps} + -D DEBUG_BUILD=0 -D BUILD_TEMP=1 -D BUILD_DISPLAY=1 -D BLUETOOTH_TCODE=0 -D BLE_TCODE=1 -D WIFI_TCODE=1 -D MOTOR_TYPE=0 -D SECURE_WEB=0 -D COEXIST=1 #-D CORE_DEBUG_LEVEL=5 +lib_deps = ${common:ESP32-wifi.lib_deps} + ${common:display.lib_deps} ${common:temperature.lib_deps} - ${common:ESP32-bluetooth.lib_deps} + ${common:ESP32-bluetooth.lib_deps} [env:esp32-s3-devkitc-1-N8R8-bldc] extends = env:esp32-s3-devkitc-1-N8R8 build_unflags = -D MOTOR_TYPE=0 -build_flags = ${env:esp32-s3-devkitc-1-N8R8.build_flags} - -D MOTOR_TYPE=1 -lib_deps = ${env:esp32-s3-devkitc-1-N8R8.lib_deps} - ${common:bldc.lib_deps} +build_flags = ${env:esp32-s3-devkitc-1-N8R8.build_flags} + -D MOTOR_TYPE=1 +lib_deps = ${env:esp32-s3-devkitc-1-N8R8.lib_deps} + ${common:bldc.lib_deps} lib_archive = false #required for SimpleFOC - + [env:esp32-s3-devkitc-1-N8R8-debug] extends = env:esp32-s3-devkitc-1-N8R8 build_unflags = -D DEBUG_BUILD=0 -build_flags = ${env:esp32-s3-devkitc-1-N8R8.build_flags} - -D DEBUG_BUILD=1 +build_flags = ${env:esp32-s3-devkitc-1-N8R8.build_flags} + -D DEBUG_BUILD=1 build_type = debug debug_init_break = tbreak setup debug_tool = cmsis-dap @@ -299,10 +299,10 @@ upload_protocol = cmsis-dap ; [env:pico] ; extends = common:PICO ; build_flags = ${common:PICO.build_flags} -; -D DEBUG_BUILD=0 -D BUILD_TEMP=0 -D BUILD_DISPLAY=0 -D BLUETOOTH_TCODE=0 -D BLE_TCODE=1 -D WIFI_TCODE=1 -D MOTOR_TYPE=0 -D SECURE_WEB=0 #-D CORE_DEBUG_LEVEL=5 -; lib_deps = ${common:PICO.lib_deps} +; -D DEBUG_BUILD=0 -D BUILD_TEMP=0 -D BUILD_DISPLAY=0 -D BLUETOOTH_TCODE=0 -D BLE_TCODE=1 -D WIFI_TCODE=1 -D MOTOR_TYPE=0 -D SECURE_WEB=0 #-D CORE_DEBUG_LEVEL=5 +; lib_deps = ${common:PICO.lib_deps} ; ${common:display.lib_deps} -; ;${common:TCode_V2.lib_deps} +; ;${common:TCode_V2.lib_deps} ; ${common:ESP32-bluetooth.lib_deps} ; [env:lolin_s3] @@ -313,24 +313,24 @@ upload_protocol = cmsis-dap ; build_type = release ; build_flags = ${common:ESP32-wifi.build_flags} ; -D BOARD_HAS_PSRAM -; -D ARDUINO_USB_CDC_ON_BOOT=1 +; -D ARDUINO_USB_CDC_ON_BOOT=1 ; -mfix-esp32-psram-cache-issue ; -D DEBUG_BUILD=0 -D BUILD_TEMP=1 -D BUILD_DISPLAY=1 -D BLUETOOTH_TCODE=0 -D WIFI_TCODE=1 -D MOTOR_TYPE=0 -D SECURE_WEB=0 #-D CORE_DEBUG_LEVEL=5 #-D FW_VERSION=%%date%% -; lib_deps = ${common:ESP32-wifi.lib_deps} -; ${common:display.lib_deps} +; lib_deps = ${common:ESP32-wifi.lib_deps} +; ${common:display.lib_deps} ; ${common:temperature.lib_deps} -; ${common:ESP32-bluetooth.lib_deps} +; ${common:ESP32-bluetooth.lib_deps} ; #ESP32 DA (Dual antennae) ; [env:esp32doit-devkit-D A] ; extends = common:esp32doit-devkit-v1-wifi ; build_flags = ${common:ESP32-wifi.build_flags} -; -D DEBUG_BUILD=0 -D BUILD_TEMP=0 -D BUILD_DISPLAY=0 -D BLUETOOTH_TCODE=0 -D WIFI_TCODE=1 #-D CORE_DEBUG_LEVEL=5 +; -D DEBUG_BUILD=0 -D BUILD_TEMP=0 -D BUILD_DISPLAY=0 -D BLUETOOTH_TCODE=0 -D WIFI_TCODE=1 #-D CORE_DEBUG_LEVEL=5 ; [env:esp32doit-devkit-D A-display-temp] ; extends = env:esp32doit-devkit-v1-display-temp ; build_flags = ${common:ESP32-wifi.build_flags} -; -D DEBUG_BUILD=0 -D BUILD_TEMP=1 -D BUILD_DISPLAY=1 -D BLUETOOTH_TCODE=0 -D WIFI_TCODE=1 #-D CORE_DEBUG_LEVEL=5 +; -D DEBUG_BUILD=0 -D BUILD_TEMP=1 -D BUILD_DISPLAY=1 -D BLUETOOTH_TCODE=0 -D WIFI_TCODE=1 #-D CORE_DEBUG_LEVEL=5 ; [env:esp8266-ESP01] ; extends = common:ESP8266-wifi @@ -338,8 +338,16 @@ upload_protocol = cmsis-dap ; build_type = release ; board_build.partitions = esp-01-partitions.csv ; build_flags = ${common:ESP8266-wifi.build_flags} -; -D ESP01=1 -D DEBUG_BUILD=0 -D BUILD_TEMP=0 -D BUILD_DISPLAY=0 -D BLUETOOTH_TCODE=0 -D BLE_TCODE=0 -D WIFI_TCODE=1 -D MOTOR_TYPE=0 -D SECURE_WEB=0 #-D CORE_DEBUG_LEVEL=5 -; lib_deps = ${common:ESP8266-wifi.lib_deps} -; ; ${common:display.lib_deps} +; -D ESP01=1 -D DEBUG_BUILD=0 -D BUILD_TEMP=0 -D BUILD_DISPLAY=0 -D BLUETOOTH_TCODE=0 -D BLE_TCODE=0 -D WIFI_TCODE=1 -D MOTOR_TYPE=0 -D SECURE_WEB=0 #-D CORE_DEBUG_LEVEL=5 +; lib_deps = ${common:ESP8266-wifi.lib_deps} +; ; ${common:display.lib_deps} ; ; ${common:temperature.lib_deps} -; ; ${common:TCode_V2.lib_deps} \ No newline at end of file +; ; ${common:TCode_V2.lib_deps} + +[env:ssr1_pcb] +extends = env:esp32doit-devkit-v1-bldc +build_flags = -D DEFAULT_BOARD=BoardType::SSR1PCB + +[env:sr6_pcb] +extends = env:esp32doit-devkit-v1 +build_flags = -D DEFAULT_BOARD=BoardType::SR6PCB \ No newline at end of file diff --git a/ESP32/src/BatteryHandler.h b/ESP32/src/BatteryHandler.h index a808f22..116d4bd 100644 --- a/ESP32/src/BatteryHandler.h +++ b/ESP32/src/BatteryHandler.h @@ -37,7 +37,7 @@ using BATTERY_STATE_FUNCTION_PTR_T = void (*)(float capacityRemainingPercentage, /** This class is setup for a specific board with an LTC2944 gas guage * The module used is CJMCU-294. */ -class BatteryHandler { +class BatteryHandler : public Task { public: static bool connected() { return m_battery_connected; @@ -76,7 +76,7 @@ class BatteryHandler { LogHandler::info(_TAG, "Connecting to monitor"); while (!gauge.begin()) { Serial.print("."); - vTaskDelay(1000/portTICK_PERIOD_MS); + this->wait(1000); if(millis() > timeout) { LogHandler::error(_TAG, "Detecting battery gauge (LTC2944) timed out. Exit."); return false; @@ -101,31 +101,24 @@ class BatteryHandler { } void loop() { - _isRunning = true; - LogHandler::debug(_TAG, "Battery task cpu core: %u", xPortGetCoreID()); - TickType_t pxPreviousWakeTime = millis(); - while(_isRunning) { - if(m_battery_connected && millis() >= lastTick) { - LogHandler::verbose(_TAG, "Enter getBatteryLevel"); - lastTick = millis() + tick; - m_batteryCapacity = gauge.getRemainingCapacity(); - m_batteryVoltage = gauge.getVoltage(); - m_batteryTemp = gauge.getTemperature(); - - LogHandler::verbose(_TAG, "Battery remaining capacity: %f", m_batteryCapacity); - LogHandler::verbose(_TAG, "Battery voltage: %f", m_batteryVoltage); - LogHandler::verbose(_TAG, "Battery temp: %f", m_batteryTemp); - float capacityRemainingPercentage = 0; - if(m_batteryCapacity > 0) - capacityRemainingPercentage = m_batteryCapacity / m_maxCapacity * 100; - - if(message_callback) - message_callback(capacityRemainingPercentage, m_batteryCapacity, m_batteryVoltage, m_batteryTemp); - } - xTaskDelayUntil(&pxPreviousWakeTime, 5000/portTICK_PERIOD_MS); + if(m_battery_connected && millis() >= lastTick) { + LogHandler::verbose(_TAG, "Enter getBatteryLevel"); + lastTick = millis() + tick; + m_batteryCapacity = gauge.getRemainingCapacity(); + m_batteryVoltage = gauge.getVoltage(); + m_batteryTemp = gauge.getTemperature(); + + LogHandler::verbose(_TAG, "Battery remaining capacity: %f", m_batteryCapacity); + LogHandler::verbose(_TAG, "Battery voltage: %f", m_batteryVoltage); + LogHandler::verbose(_TAG, "Battery temp: %f", m_batteryTemp); + float capacityRemainingPercentage = 0; + if(m_batteryCapacity > 0) + capacityRemainingPercentage = m_batteryCapacity / m_maxCapacity * 100; + + if(message_callback) + message_callback(capacityRemainingPercentage, m_batteryCapacity, m_batteryVoltage, m_batteryTemp); } - - vTaskDelete( NULL ); + this->sleep(5000); // 5000ms } //void setup() { diff --git a/ESP32/src/DisplayHandler.h b/ESP32/src/DisplayHandler.h index dda7b1f..aac6c16 100644 --- a/ESP32/src/DisplayHandler.h +++ b/ESP32/src/DisplayHandler.h @@ -36,11 +36,12 @@ SOFTWARE. */ #endif #include "BatteryHandler.h" #include "TagHandler.h" +#include "TaskHandler.h" // #if ISAAC_NEWTONGUE_BUILD // #include "animationFrames.h" // #endif -class DisplayHandler +class DisplayHandler : public Task { public: @@ -73,7 +74,7 @@ class DisplayHandler LogHandler::info(_TAG, "Could not connect address"); return; } - vTaskDelay(2000/portTICK_PERIOD_MS); + this->wait(2000); if(displayConnected) { //display.setFont(Adafruit5x7); @@ -139,13 +140,6 @@ class DisplayHandler } } - static void startLoop(void* displayHandlerRef) - { - //LogHandler::debug(TagHandler::DisplayHandler, "Starting loop"); - //if(((DisplayHandler*)displayHandlerRef)->isConnected()) - ((DisplayHandler*)displayHandlerRef)->loop(); - } - bool isConnected() { return displayConnected; } @@ -162,107 +156,92 @@ class DisplayHandler void loop() { - LogHandler::debug(_TAG, "Display task cpu core: %u", xPortGetCoreID()); - if(!isConnected()) { - LogHandler::warning(_TAG, "Display not connected when starting loop"); - vTaskDelete( NULL ); - return; - } - TickType_t pxPreviousWakeTime = millis(); - _isRunning = true; - while(_isRunning) { - if(!m_animationPlaying && displayConnected && millis() >= lastUpdate + nextUpdate) { - LogHandler::verbose(_TAG, "Enter display loop"); - lastUpdate = millis(); - clearDisplay(); - setTextSize(1); - int headerPadding = is32() ? 0 : 3; - // Serial.print("Display Core: "); - // Serial.println(xPortGetCoreID()); + + if(!m_animationPlaying && displayConnected && millis() >= lastUpdate + nextUpdate) { + LogHandler::verbose(_TAG, "Enter display loop"); + lastUpdate = millis(); + clearDisplay(); + setTextSize(1); + int headerPadding = is32() ? 0 : 3; + // Serial.print("Display Core: "); + // Serial.println(xPortGetCoreID()); #if WIFI_TCODE - if(WifiHandler::isConnected()) { - LogHandler::verbose(_TAG, "Enter wifi connected"); - startLine(headerPadding); - display.print(_ipAddress); - - drawBatteryLevel(); - - // Draw Wifi signal bars - int barHeight = is32() ? 8 : 10; - int bars; - // int bars = map(RSSI,-80,-44,1,6); // this method doesn't refelct the Bars well - // simple if then to set the number of bars - int8_t RSSI = WifiHandler::getRSSI(); - if (RSSI > -55) { - bars = 5; - } else if (RSSI < -55 && RSSI > -65) { - bars = 4; - } else if (RSSI < -65 && RSSI > -70) { - bars = 3; - } else if (RSSI < -70 && RSSI > -78) { - bars = 2; - } else if (RSSI < -78 && RSSI > -82) { - bars = 1; - } else { - bars = 0; - } - for (int b=0; b <= bars; b++) { - display.fillRect((m_settingsFactory->getDisplayScreenWidth() - 17) + (b*3), barHeight - (b*2),2,b*2,WHITE); - } + if(WifiHandler::isConnected()) { + LogHandler::verbose(_TAG, "Enter wifi connected"); + startLine(headerPadding); + display.print(_ipAddress); - newLine(headerPadding); - if(m_settingsFactory->getVersionDisplayed()) { - LogHandler::verbose(_TAG, "Enter versionDisplayed"); - left(m_settingsFactory->getTcodeVersionString()); - right(FIRMWARE_VERSION_NAME); - newLine(); - } - - } else if(WifiHandler::apMode()) { - LogHandler::verbose(_TAG, "Enter apMode"); - startLine(headerPadding); - left("AP:"); - left(m_settingsFactory->getAPModeIP(), 4); - drawBatteryLevel(); - newLine(headerPadding); - if(!is32()) { - left("SSID:"); - left(m_settingsFactory->getAPModeSSID(), 6); - newLine(); - } - if((is32() && m_settingsFactory->getVersionDisplayed() && !m_settingsFactory->getSleeveTempDisplayed() && !m_settingsFactory->getInternalTempDisplayed()) - || m_settingsFactory->getVersionDisplayed()) { - left(m_settingsFactory->getTcodeVersionString()); - right(FIRMWARE_VERSION_NAME); - newLine(); - } else if(is32()) { - left("SSID:"); - left(m_settingsFactory->getAPModeSSID(), 6); - newLine(); - } + drawBatteryLevel(); + + // Draw Wifi signal bars + int barHeight = is32() ? 8 : 10; + int bars; + // int bars = map(RSSI,-80,-44,1,6); // this method doesn't refelct the Bars well + // simple if then to set the number of bars + int8_t RSSI = WifiHandler::getRSSI(); + if (RSSI > -55) { + bars = 5; + } else if (RSSI < -55 && RSSI > -65) { + bars = 4; + } else if (RSSI < -65 && RSSI > -70) { + bars = 3; + } else if (RSSI < -70 && RSSI > -78) { + bars = 2; + } else if (RSSI < -78 && RSSI > -82) { + bars = 1; } else { - LogHandler::verbose(_TAG, "Enter Wifi error"); - display.print("Wifi error"); - drawBatteryLevel(); + bars = 0; } + for (int b=0; b <= bars; b++) { + display.fillRect((m_settingsFactory->getDisplayScreenWidth() - 17) + (b*3), barHeight - (b*2),2,b*2,WHITE); + } + + newLine(headerPadding); + if(m_settingsFactory->getVersionDisplayed()) { + LogHandler::verbose(_TAG, "Enter versionDisplayed"); + left(m_settingsFactory->getTcodeVersionString()); + right(FIRMWARE_VERSION_NAME); + newLine(); + } + + } else if(WifiHandler::apMode()) { + LogHandler::verbose(_TAG, "Enter apMode"); + startLine(headerPadding); + left("AP:"); + left(m_settingsFactory->getAPModeIP(), 4); + drawBatteryLevel(); + newLine(headerPadding); + if(!is32()) { + left("SSID:"); + left(m_settingsFactory->getAPModeSSID(), 6); + newLine(); + } + if((is32() && m_settingsFactory->getVersionDisplayed() && !m_settingsFactory->getSleeveTempDisplayed() && !m_settingsFactory->getInternalTempDisplayed()) + || m_settingsFactory->getVersionDisplayed()) { + left(m_settingsFactory->getTcodeVersionString()); + right(FIRMWARE_VERSION_NAME); + newLine(); + } else if(is32()) { + left("SSID:"); + left(m_settingsFactory->getAPModeSSID(), 6); + newLine(); + } + } else { + LogHandler::verbose(_TAG, "Enter Wifi error"); + display.print("Wifi error"); + drawBatteryLevel(); + } #endif #if BUILD_TEMP - if(m_settingsFactory->getSleeveTempDisplayed() || m_settingsFactory->getInternalTempDisplayed()) { - is32() ? draw32Temp() : draw64Temp(); - } + if(m_settingsFactory->getSleeveTempDisplayed() || m_settingsFactory->getInternalTempDisplayed()) { + is32() ? draw32Temp() : draw64Temp(); + } #endif - display.display(); - } - xTaskDelayUntil(&pxPreviousWakeTime, 5000/portTICK_PERIOD_MS); - // Serial.print("Display task: "); // stack size used - // Serial.print(uxTaskGetStackHighWaterMark( NULL )); // stack size used - // Serial.println(); - // Serial.flush(); + display.display(); } - - vTaskDelete( NULL ); + this->sleep(200); } void println(String value) diff --git a/ESP32/src/HTTP/HTTPBase.h b/ESP32/src/HTTP/HTTPBase.h index 6bb58d2..9d041c8 100644 --- a/ESP32/src/HTTP/HTTPBase.h +++ b/ESP32/src/HTTP/HTTPBase.h @@ -1,8 +1,9 @@ #pragma once #include "WebSocketBase.h" -class HTTPBase { +#include "TaskHandler.h" +class HTTPBase : public Task { public: - virtual void setup(uint16_t port, WebSocketBase* webSocketHandler, bool apMode) = 0; + virtual void setup_http(uint16_t port, WebSocketBase* webSocketHandler, bool apMode) = 0; virtual void stop() = 0; virtual bool isRunning() = 0; }; \ No newline at end of file diff --git a/ESP32/src/HTTP/HTTPSHandler.hpp b/ESP32/src/HTTP/HTTPSHandler.hpp index 41cd1bf..a1e3514 100644 --- a/ESP32/src/HTTP/HTTPSHandler.hpp +++ b/ESP32/src/HTTP/HTTPSHandler.hpp @@ -26,7 +26,7 @@ using namespace httpsserver; class HTTPSHandler : public HTTPBase { public: - void setup(int port, WebSocketBase* webSocketHandler, bool apMode) override { + void setup_http(int port, WebSocketBase* webSocketHandler, bool apMode) override { setupHandlers(webSocketHandler, apMode); // The websocket handler can be linked to the server by using a WebsocketNode: diff --git a/ESP32/src/MessageHandler.h b/ESP32/src/MessageHandler.h new file mode 100644 index 0000000..32f0ae9 --- /dev/null +++ b/ESP32/src/MessageHandler.h @@ -0,0 +1,72 @@ +#ifndef _MESSAGE_HANDLER_H_ +#define _MESSAGE_HANDLER_H_ + +#include + +#include "TagHandler.h" + +namespace Messages { +struct message_t +{ + uint8_t id; + const char* message; + union + { + /* data */ + uint32_t u_data; + int32_t s_data; + float f_data; + }; +}; + +class MessageSink +{ + protected: + virtual void sink(message_t) = 0; + private: + uint32_t _tags; + public: + MessageSink(uint32_t tags = ALL_TAGS) : _tags(tags) {} + + void handle_msg(uint32_t tags, message_t msg) + { + if (_tags & tags) + { + this->sink(msg); + } + } +}; + +class MessageHandler +{ + private: + std::vector> _sinks; + public: + void push(std::unique_ptr sink) + { + _sinks.push_back(std::move(sink)); + } + + void send(uint32_t tags, const char* msg) + { + message_t _msg{msg, {{0}}}; + send(tags, _msg); + } + + void send(uint32_t tags, message_t msg) + { + for(auto&& sink : _sinks) + { + sink->handle_msg(tags, msg); + } + } + + MessageHandler* getInstance() + { + static MessageHandler instance; + return &instance; + } +}; +}; + +#endif // _MESSAGE_HANDLER_H_ diff --git a/ESP32/src/NetworkHandler.h b/ESP32/src/NetworkHandler.h new file mode 100644 index 0000000..f28e653 --- /dev/null +++ b/ESP32/src/NetworkHandler.h @@ -0,0 +1,66 @@ + + +class NetworkHandler : public Task +{ +private: + const bool &apMode; + const int &port; + const int &udpPort; + const char *hostname; + const char *friendlyName; + MDNSHandler mdnsHandler; + HTTPBase *webHandler = nullptr; + WebSocketBase *webSocketHandler = nullptr; + TaskHandle_t httpsTask; + const char* _TAG = TagHandler::NetworkHandler; +public: + NetworkHandler(const bool &apMode, const int &port, const int &udpPort, const char *hostname, const char *friendlyName) + : apMode(apMode), port(port), udpPort(udpPort), hostname(hostname), friendlyName(friendlyName) + { + } + void setup() override { + LogHandler::info(_TAG, "Setting up network handler"); + // Network setup code here + } + + void loop() override { + // Network handling code here + } + + void start() + { + if((MODULE_CURRENT != ModuleType::WROOM32 || (!bluetoothEnabled && !bleEnabled)) && !webHandler) + { + displayPrint("Starting web server"); + #if !SECURE_WEB + webHandler = new WebHandler(); + webSocketHandler = new WebSocketHandler(); + #else + LogHandler::debug(TagHandler::Main, "Start https task"); + webHandler = new HTTPSHandler(); + webSocketHandler = new SecureWebSocketHandler(); + auto httpsStatus = xTaskCreateUniversal( + HTTPSHandler::startLoop, /* Function to implement the task */ + "HTTPSTask", /* Name of the task */ + 8192 * 3, /* Stack size in words */ + webHandler, /* Task input parameter */ + 3, /* Priority of the task */ + &httpsTask, /* Task handle. */ + -1); /* Core where the task should run */ + if (httpsStatus != pdPASS) + { + LogHandler::error(TagHandler::Main, "Could not start https task."); + } + #endif + webHandler->setup(port, webSocketHandler, apMode); + LogHandler::debug(TagHandler::Main, "Web DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); + } else { + displayPrint("WebServer disabled"); + LogHandler::info(TagHandler::Main, "WebServer disabled due to bluetooth and chip model"); + } + if (!apMode) {// mdns breaks apmode? + mdnsHandler.setup(hostname, friendlyName, port, udpPort); + LogHandler::debug(TagHandler::Main, "MDNS DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); + } + } +} \ No newline at end of file diff --git a/ESP32/src/TagHandler.h b/ESP32/src/TagHandler.h index 712bc88..0d35967 100644 --- a/ESP32/src/TagHandler.h +++ b/ESP32/src/TagHandler.h @@ -22,43 +22,51 @@ SOFTWARE. */ #pragma once #include +#include -class TagHandler { - public: - static const char* Main; - static const char* MainLoop; - static const char* DisplayHandler; - static const char* TemperatureHandler; - static const char* BatteryHandler; - static const char* SettingsHandler; - static const char* WifiHandler; - static const char* UdpHandler; - static const char* WebsocketsHandler; - static const char* WebHandler; - static const char* WebsocketBase; - static const char* SecureWebsocketsHandler; - static const char* SecureWebsocketClient; - static const char* HTTPSHandler; - static const char* SystemCommandHandler; - static const char* BLEHandler; - static const char* BLEConfigurationHandler; - static const char* BluetoothHandler; - static const char* ServoHandler; - static const char* TCodeHandler; - static const char* BLDCHandler; - static const char* ToyHandler; - static const char* MotorHandler; - static const char* MotionHandler; - static const char* VoiceHandler; - static const char* ButtonHandler; - static const char* MdnsHandler; - static const char* SettingsFactory; +using tag_t = uint32_t; +#define MASK(i) (1 << i) - static const std::vector AvailableTags; - static bool HasTag(const char*); +enum Tag : uint32_t { + MAIN = 0x01, + DISPLAY = 0x02, + TEMPERATURE = 0x04, + BATTERY = 0x08, + SETTINGS = 0x10, + WIFI = 0x20, + UDP = 0x40, + WEBSOCKETS_SERVER = 0x80, + WEBSOCKET_BASE = 0x100, + SECURE_WEBSOCKET_SERVER = 0x200, + SECURE_WEBSOCKET_CLIENT = 0x400, + HTTPS = 0x800, + WEB = 0x1000, + SYSTEM_COMMAND = 0x2000, + BLE = 0x4000, + BLE_CONFIGURATION = 0x8000, + BLUETOOTH = 0x10000, + SERVO = 0x20000, + TCODE = 0x40000, + MOTOR = 0x80000, + MOTION = 0x100000, + VOICE = 0x200000, + BUTTON = 0x400000, + MDNS = 0x800000, + SETTINGS_FACTORY = 0x1000000, }; -const char* TagHandler::Main = "main"; +std::map + +const uint32_t ALL_TAGS = 0xFFFFFFFF; + +class TagHandler { + protected: + static const std::map _tag_names; + public: + TagHandler() : + _tags = { + {MASK(1), "main"}, + {MASK()} const char* TagHandler::MainLoop = "main-loop"; const char* TagHandler::DisplayHandler = "display-handler"; const char* TagHandler::TemperatureHandler = "temperature-handler"; @@ -85,7 +93,12 @@ const char* TagHandler::MotionHandler = "motion-handler"; const char* TagHandler::VoiceHandler = "voice-handler"; const char* TagHandler::ButtonHandler = "button-handler"; const char* TagHandler::MdnsHandler = "mdns-handler"; -const char* TagHandler::SettingsFactory = "settings-factory"; +const char* TagHandler::SettingsFactory = "settings-factory";} + } + static const std::map tags; + static bool HasTag(const char*); +}; + const std::vector TagHandler::AvailableTags = { diff --git a/ESP32/src/TaskHandler.h b/ESP32/src/TaskHandler.h new file mode 100644 index 0000000..db2f019 --- /dev/null +++ b/ESP32/src/TaskHandler.h @@ -0,0 +1,234 @@ +#ifndef TASK_HANDLER_H_ +#define TASK_HANDLER_H_ + +#include + +#include +#include +#include + +#include +#include + +namespace TaskHandler { + +class Task; + +// Context is encoded in the task class, can be cast to appropriate structure +using Task_t = std::function; + +// Rates are in microseconds +enum Rates : int32_t +{ + ONDEMAND = 0, + MAX = 100, + FAST = 1000, + SLOW = 25000, +}; + +// A task wraps a periodic task and assigns it a tick rate (software timer) +class Task { +private: + Rates _tick_rate; + long long _last_tick; + long long _sleep_target; +public: + Task(Rates rate) : _tick_rate(rate) + { + _last_tick = 0; + } + + Task(const Task &rhs) { + _tick_rate = rhs._tick_rate; + _last_tick = rhs._last_tick; + } + + // Perform setup, grab peripherals, etc + virtual void initialize() { + _last_tick = micros(); + this->setup(); + } + // Invoke update routine (handle_tick) + void handle_tick() + { + long long us = micros(); + long long delta = abs(us - _last_tick); + if (_sleep_target > 0) + { + _sleep_target -= delta; + return; + } + if (delta >= _tick_rate) { + _last_tick = micros(); + loop(); + } + } + + void sleep(long long ms) + { + _sleep_target = ms * 1000; + } + + void wait(long long ms) + { + vTaskDelay(ms / portTICK_PERIOD_MS); + } + + virtual void setup() = 0; + virtual void loop() = 0; +}; + +// Task wraps a simple std::function +class FunctionalTask : public Task +{ +private: + Task_t _handler; +public: + FunctionalTask(Task_t handler) : Task(Rates::SLOW), _handler(handler) {} + + void setup() + { + // No setup needed + } + void loop() override { + if (_handler) + { + _handler(this); + } + } +}; + +enum Core { + PRO_CPU = PRO_CPU_NUM, + APP_CPU = APP_CPU_NUM, +}; + +enum Priority { + IDLE = 0, + REALTIME = 0, + PRIORITY = 1, + LAZY = 2, +}; + +// Per-core (per-thread) executor/task queue +class TaskExecutor { + private: + static constexpr uint32_t STACK_SIZE = configMINIMAL_STACK_SIZE * 8; + static constexpr uint32_t INITIAL_CAPACITY = 8; + std::vector> _tasks; + TaskHandle_t _thread_handle; + const std::string _name; + const uint32_t _priority; + const uint32_t _core; + + + public: + TaskExecutor(std::string name, uint32_t priority = IDLE, uint32_t core = APP_CPU) : _name(name), _priority(priority), _core(core) + { + _tasks.reserve(INITIAL_CAPACITY); + } + + TaskExecutor(const char* name, uint32_t priority = IDLE, uint32_t core = APP_CPU) : _name(name), _priority(priority), _core(core) + { + _tasks.reserve(INITIAL_CAPACITY); + } + + // Add a task to the list + void registerTask(std::unique_ptr task) + { + if (task) + { + _tasks.push_back(std::move(task)); + } + } + + // Run setup code, acquire hardware locks, etc + void initialize() + { + for(auto&& task : _tasks) + { + task->initialize(); + } + } + + // Start the execution thread (TODO: Do we need this? Can we use a lambda?) + void start() + { + auto status = xTaskCreatePinnedToCore( + [](void* context){ + TaskHandler::TaskExecutor* manager = static_cast(context); + if (manager) + { + manager->run(); + } + }, + _name.c_str(), + STACK_SIZE, + reinterpret_cast(this), + tskIDLE_PRIORITY + _priority, + &_thread_handle, + _core); + + if (status != pdPASS) + { + LogHandler::error(_name.c_str(), "Could not start task."); + } + } + + void run() + { + for(;;) + { + for(auto&& task : _tasks) + { + task->handle_tick(); + } + taskYIELD(); + } + } +}; + +class TaskManager +{ + private: + TaskExecutor realtimeThread; + TaskExecutor priorityThread; + TaskExecutor lazyThread; + public: + TaskManager() : + realtimeThread("realtime", Priority::REALTIME, Core::PRO_CPU), + priorityThread("priority", Priority::PRIORITY, Core::APP_CPU), + lazyThread("lazy", Priority::LAZY, Core::APP_CPU) {} + + TaskExecutor* realtimeTasks() { return &realtimeThread; } + TaskExecutor* priorityTasks() { return &priorityThread; } + TaskExecutor* lazyTasks() { return &lazyThread; } + + void start() + { + realtimeThread.start(); + priorityThread.start(); + lazyThread.start(); + } + + void realtime(Task* t, Rates rate = Rates::FAST) + { + realtimeThread.registerTask(std::make_unique(t)); + } + + void priority(Task* t, Rates rate = Rates::FAST) + { + priorityThread.registerTask(std::make_unique(t)); + } + + void lazy(Task* t, Rates rate = Rates::FAST) + { + lazyThread.registerTask(std::make_unique(t)); + } +}; + +// Global instance +static TaskManager taskManager; +}; + +#endif // TASK_HANDLER_H_ \ No newline at end of file diff --git a/ESP32/src/TemperatureHandler.h b/ESP32/src/TemperatureHandler.h index cb7f862..485ad5d 100644 --- a/ESP32/src/TemperatureHandler.h +++ b/ESP32/src/TemperatureHandler.h @@ -30,6 +30,8 @@ SOFTWARE. */ #include "SettingsHandler.h" // #include "LogHandler.h" #include "TagHandler.h" +#include "TaskHandler.h" +#include "MessageHandler.h" enum class TemperatureType { INTERNAL, @@ -68,10 +70,7 @@ const char* TemperatureState::HEAT = "Heating"; const char* TemperatureState::COOLING = "Cooling"; const char* TemperatureState::OFF = "Off"; -using TEMP_CHANGE_FUNCTION_PTR_T = void (*)(TemperatureType type, const char* message, float temp); -using STATE_CHANGE_FUNCTION_PTR_T = void (*)(TemperatureType type, const char* state); - -class TemperatureHandler { +class TemperatureHandler : public Task { public: void setup(bool internalTempEnabled, @@ -191,48 +190,13 @@ class TemperatureHandler { fanControlInitialized = true; } - static void startLoop(void * parameter) { - ((TemperatureHandler*)parameter)->loop(); - } - void loop() { - _isRunning = true; - LogHandler::debug(_TAG, "Temp task cpu core: %u", xPortGetCoreID()); - lastSleeveTempRequest = millis(); - TickType_t pxPreviousWakeTime = millis(); - while(_isRunning) - { - getInternalTemp(); - getSleeveTemp(); - chackFailSafe(); - - xTaskDelayUntil(&pxPreviousWakeTime, 5000/portTICK_PERIOD_MS); - // Serial.print("uxTaskGetStackHighWaterMark: "); - // Serial.println(uxTaskGetStackHighWaterMark(NULL) *4); - // Serial.print("xPortGetFreeHeapSize: "); - // Serial.println(xPortGetFreeHeapSize()); - } - - LogHandler::debug(_TAG, "Temp task exit"); - vTaskDelete( NULL ); + getInternalTemp(); + getSleeveTemp(); + chackFailSafe(); + this->sleep(5000); } - void setMessageCallback(TEMP_CHANGE_FUNCTION_PTR_T f) // Sets the callback function used by TCode - { - if (f == nullptr) { - message_callback = 0; - } else { - message_callback = f; - } - } - void setStateChangeCallback(STATE_CHANGE_FUNCTION_PTR_T f) // Sets the callback function used by TCode - { - if (f == nullptr) { - state_change_callback = 0; - } else { - state_change_callback = f; - } - } bool isMaxTempTriggered() { return maxTempTriggerInternal; } @@ -286,8 +250,12 @@ class TemperatureHandler { LogHandler::verbose(_TAG, "sleeve getTempC duration: %d", micros() - start); String statusJson("{\"temp\":\"" + String(_currentSleeveTemp) + "\", \"status\":\""+m_lastSleeveStatus+"\"}"); - if(tempChanged && message_callback) { - message_callback(TemperatureType::SLEEVE, statusJson.c_str(), _currentSleeveTemp); + if(tempChanged) { + Messages::message_t m = { + .message = statusJson.c_str(); + .data_f = _currentSleeveTemp + }; + Messages::MessageHandler::getInstance()->send(m) } requestSleeveTemp(); } @@ -433,6 +401,8 @@ class TemperatureHandler { if(!targetSleeveTempReached) { targetSleeveTempReached = true; String* command = new String("tempReached"); + auto msgs = Messages::MessageHandler::getInstance(); + msgs->send() if(message_callback) message_callback(TemperatureType::SLEEVE, "tempReached", _currentSleeveTemp); } diff --git a/ESP32/src/VoiceHandler.hpp b/ESP32/src/VoiceHandler.hpp index b12c97d..0b2818a 100644 --- a/ESP32/src/VoiceHandler.hpp +++ b/ESP32/src/VoiceHandler.hpp @@ -30,7 +30,7 @@ SOFTWARE. */ using VOICE_COMMAND_FUNCTION_PTR_T = void (*)(const char* tcodeCommand); -class VoiceHandler { +class VoiceHandler : public Task { public: bool setup() { @@ -46,7 +46,7 @@ class VoiceHandler { return false; } tries++; - delay(1000); + this->wait(1000); } _isConnected = true; LogHandler::info(_TAG, "Begin ok!"); @@ -124,13 +124,8 @@ class VoiceHandler { @brief Get the ID corresponding to the command word @return Return the obtained command word ID, returning 0 means no valid ID is obtained */ - _isRunning = true; - LogHandler::debug(_TAG, "Voice task cpu core: %u", xPortGetCoreID()); - TickType_t pxPreviousWakeTime = millis(); - while(_isRunning) { toTCode(asr.getCMDID()); - xTaskDelayUntil(&pxPreviousWakeTime, 1000/portTICK_PERIOD_MS); - } + this->sleep(1000); } void toTCode(uint8_t voiceCommand) { diff --git a/ESP32/src/WebHandler.h b/ESP32/src/WebHandler.h index 82fdc35..05181a5 100644 --- a/ESP32/src/WebHandler.h +++ b/ESP32/src/WebHandler.h @@ -42,7 +42,7 @@ SOFTWARE. */ class WebHandler : public HTTPBase { public: // bool MDNSInitialized = false; - void setup(uint16_t port, WebSocketBase* webSocketHandler, bool apMode) override { + void setup_http(uint16_t port, WebSocketBase* webSocketHandler, bool apMode) override { stop(); if (port < 1 || port > 65535) port = 80; @@ -440,6 +440,12 @@ class WebHandler : public HTTPBase { bool isRunning() override { return initialized; } + + void setup() override { + } + + void loop() override { + } private: bool initialized = false; const char* _TAG = TagHandler::WebHandler; diff --git a/ESP32/src/WebHandler_psychic.h b/ESP32/src/WebHandler_psychic.h index f9f0d44..a606423 100644 --- a/ESP32/src/WebHandler_psychic.h +++ b/ESP32/src/WebHandler_psychic.h @@ -42,7 +42,7 @@ SOFTWARE. */ class WebHandler : public HTTPBase { public: // bool MDNSInitialized = false; - void setup(int port, WebSocketBase* webSocketHandler, bool apMode) override { + void setup_http(int port, WebSocketBase* webSocketHandler, bool apMode) override { stop(); if (port < 1) port = 80; @@ -296,6 +296,12 @@ class WebHandler : public HTTPBase { return initialized; } + bool setup() override { + } + + void loop() override { + } + private: bool initialized = false; const char* _TAG = TagHandler::WebHandler; diff --git a/ESP32/src/WifiHandler.h b/ESP32/src/WifiHandler.h index ed11010..f2b5fc8 100644 --- a/ESP32/src/WifiHandler.h +++ b/ESP32/src/WifiHandler.h @@ -31,6 +31,7 @@ SOFTWARE. */ // // #include "LogHandler.h" #include "SettingsHandler.h" #include "TagHandler.h" +#include "TaskHandler.h" enum class WiFiStatus { @@ -46,7 +47,7 @@ enum class WiFiReason AP_MODE }; using WIFI_STATUS_FUNCTION_PTR_T = void (*)(WiFiStatus status, WiFiReason reason); -class WifiHandler +class WifiHandler : public Task { public: ~WifiHandler() @@ -322,6 +323,14 @@ class WifiHandler } }; + void setup() override + { + } + + void loop() override + { + } + private: WIFI_STATUS_FUNCTION_PTR_T wifiStatus_callback; const char *_TAG = TagHandler::WifiHandler; diff --git a/ESP32/src/main.cpp b/ESP32/src/main.cpp index cd07d3b..9eaadf6 100644 --- a/ESP32/src/main.cpp +++ b/ESP32/src/main.cpp @@ -100,25 +100,19 @@ SOFTWARE. */ #endif #endif +#include "TaskHandler.h" + #include "BatteryHandler.h" #include "MotionHandler.hpp" #include "VoiceHandler.hpp" #include "ButtonHandler.hpp" +TaskManager taskManager; TickType_t pxPreviousWakeTime = millis(); SystemCommandHandler *systemCommandHandler; -SettingsFactory *settingsFactory; // BLEConfigurationHandler* bleConfigurationHandler; // TcpHandler tcpHandler; -MotorHandler *motorHandler; -BatteryHandler *batteryHandler; -TaskHandle_t batteryTask; -TaskHandle_t httpsTask; - -MotionHandler motionHandler; -VoiceHandler *voiceHandler; ButtonHandler *buttonHandler = 0; -TaskHandle_t voiceTask; #if BLUETOOTH_TCODE BluetoothHandler *btHandler = 0; #endif @@ -258,17 +252,7 @@ void TCodePassthroughCommandCallback(const char *in) Serial.println(temp); } } -void profileChangeCallback(uint8_t profile) -{ -} -void logCallBack(const char *input, size_t length, LogLevel level) -{ -#if WIFI_TCODE - // if(webSocketHandler) { - // webSocketHandler->sendDebug(in, level); - // } -#endif -} + #if BUILD_TEMP void tempChangeCallBack(TemperatureType type, const char *message, float temp) { @@ -497,11 +481,11 @@ void wifiStatusCallBack(WiFiStatus status, WiFiReason reason) // bleConfigurationHandler->setup(); // #endif } - } - else if(status == WiFiStatus::IP) + } + else if(status == WiFiStatus::IP) { #if BUILD_DISPLAY - if(displayHandler) + if(displayHandler) { String ipaddress = wifi.ip().toString(); displayPrint("Connected IP: " + ipaddress); @@ -621,14 +605,14 @@ void settingChangeCallback(const SettingProfile &profile, const char *settingTha } } else if (profile == SettingProfile::ChannelRanges) - { + { if (strcmp(settingThatChanged, CHANNEL_PROFILE) == 0) { // TODO add channe; specific updates when moving to its own save...maybe... motionHandler.updateChannelRanges(); } else if (strcmp(settingThatChanged, "channelRangesEnabled") == 0) { webSocketHandler->sendCommand("channelRangesEnabled", SettingsHandler::getChannelRangesEnabled() ? "true" : "false"); } - + } } void loadI2CModules(bool displayEnabled, bool batteryEnabled, bool voiceEnabled) @@ -754,15 +738,16 @@ void setup() SettingsHandler::init(); SettingsHandler::setMessageCallback(settingChangeCallback); LogHandler::debug(TagHandler::Main, "Settings handler DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - + taskManager.init(); + LogHandler::debug(TagHandler::Main, "Task manager DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); #if BLE_TCODE settingsFactory->getValue(BLE_ENABLED, bleEnabled); - + //bleEnabled = true; #endif #if BLUETOOTH_TCODE settingsFactory->getValue(BLUETOOTH_ENABLED, bluetoothEnabled); - + //bluetoothEnabled = true; #endif @@ -865,8 +850,7 @@ void setup() settingsFactory->getValue(CASE_FAN_MAX_PWM, caseFanMaxPWM); if (sleeveTempEnabled || internalTempEnabled || fanControlEnabled) { - temperatureHandler = new TemperatureHandler(); - temperatureHandler->setup(internalTempEnabled, + taskManager.priority(new TemperatureHandler(internalTempEnabled, sleeveTempEnabled, pinMap->sleeveTemp(), pinMap->internalTemp(), @@ -879,9 +863,7 @@ void setup() fanControlEnabled, caseFanFrequency, caseFanResolution, - caseFanMaxPWM); - temperatureHandler->setMessageCallback(tempChangeCallBack); - temperatureHandler->setStateChangeCallback(tempStateChangeCallBack); + caseFanMaxPWM)); LogHandler::debug(TagHandler::Main, "Start temperature task"); auto tempStartStatus = xTaskCreatePinnedToCore( TemperatureHandler::startLoop, /* Function to implement the task */ @@ -900,22 +882,8 @@ void setup() #endif #if BUILD_DISPLAY - if (displayEnabled) - { - displayHandler = new DisplayHandler(); - displayHandler->setup(Display_I2C_Address, fanControlEnabled, pinMap->displayReset()); - // #if ISAAC_NEWTONGUE_BUILD - // xTaskCreatePinnedToCore( - // DisplayHandler::startAnimationDontPanic,/* Function to implement the task */ - // "DisplayTask", /* Name of the task */ - // 10000, /* Stack size in words */ - // displayHandler, /* Task input parameter */ - // 25, /* Priority of the task */ - // &animationTask, /* Task handle. */ - // APP_CPU_NUM); /* Core where the task should run */ - // #endif - } - LogHandler::debug(TagHandler::Main, "Display DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); + taskManager.lazy(new DisplayHandler(Display_I2C_Address, fanControlEnabled, pinMap->displayReset())); + LogHandler::debug(TagHandler::Main, "Display DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); #endif #if BLE_TCODE From 733c7f65e3dfb9d7cba4038bc3c7298d94c47475 Mon Sep 17 00:00:00 2001 From: Millibyte Products Date: Sat, 24 Jan 2026 16:53:36 -0500 Subject: [PATCH 02/42] Rebasing --- ESP32/boards/sr6pcb.json | 37 + ESP32/boards/ssr1pcb.json | 37 + ESP32/lib/struct/channel.h | 4 +- ESP32/platformio.ini | 182 ++--- .../src/BLE/BLECharacteristicCallbacksBase.h | 93 --- ESP32/src/BLE/BLEConfigurationHandler.h | 323 --------- ESP32/src/BLE/BLEHCControlCallback.h | 120 ---- ESP32/src/BLE/BLEHandler.hpp | 208 ------ ESP32/src/BLE/BLEHandlerBase.h | 100 --- ESP32/src/BLE/BLEHandlerHC.h | 70 -- ESP32/src/BLE/BLEHandlerLove.h | 74 -- ESP32/src/BLE/BLEHandlerTCode.h | 59 -- ESP32/src/BLE/BLELoveControlCallback.h | 159 ----- ESP32/src/BLE/BLEServerCallbacksBase.h | 66 -- ESP32/src/BLE/BLETCodeControlCallback.h | 74 -- ESP32/src/BatteryHandler.h | 211 ------ ESP32/src/BluetoothHandler.h | 100 --- ESP32/src/ButtonHandler.hpp | 213 ------ ESP32/src/DisplayHandler.h | 635 ------------------ ESP32/src/HTTP/HTTPBase.h | 9 - ESP32/src/SettingsHandler.h | 144 ++-- ESP32/src/TCode/v0.4/EventHandler.h | 4 +- ESP32/src/TCode/v0.4/OutputStream.h | 66 -- ESP32/src/TCode/v0.4/TCode0_4.h | 19 +- desktop.ini | 4 + 25 files changed, 212 insertions(+), 2799 deletions(-) create mode 100644 ESP32/boards/sr6pcb.json create mode 100644 ESP32/boards/ssr1pcb.json delete mode 100644 ESP32/src/BLE/BLECharacteristicCallbacksBase.h delete mode 100644 ESP32/src/BLE/BLEConfigurationHandler.h delete mode 100644 ESP32/src/BLE/BLEHCControlCallback.h delete mode 100644 ESP32/src/BLE/BLEHandler.hpp delete mode 100644 ESP32/src/BLE/BLEHandlerBase.h delete mode 100644 ESP32/src/BLE/BLEHandlerHC.h delete mode 100644 ESP32/src/BLE/BLEHandlerLove.h delete mode 100644 ESP32/src/BLE/BLEHandlerTCode.h delete mode 100644 ESP32/src/BLE/BLELoveControlCallback.h delete mode 100644 ESP32/src/BLE/BLEServerCallbacksBase.h delete mode 100644 ESP32/src/BLE/BLETCodeControlCallback.h delete mode 100644 ESP32/src/BatteryHandler.h delete mode 100644 ESP32/src/BluetoothHandler.h delete mode 100644 ESP32/src/ButtonHandler.hpp delete mode 100644 ESP32/src/DisplayHandler.h delete mode 100644 ESP32/src/HTTP/HTTPBase.h delete mode 100644 ESP32/src/TCode/v0.4/OutputStream.h create mode 100644 desktop.ini diff --git a/ESP32/boards/sr6pcb.json b/ESP32/boards/sr6pcb.json new file mode 100644 index 0000000..ffd4b7b --- /dev/null +++ b/ESP32/boards/sr6pcb.json @@ -0,0 +1,37 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32_out.ld" + }, + "core": "esp32", + "extra_flags": "-DARDUINO_ESP32_DEV", + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "mcu": "esp32", + "variant": "sr6pcb" + }, + "connectivity": [ + "wifi", + "bluetooth", + "ethernet", + "can" + ], + "debug": { + "openocd_board": "esp-wroom-32.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "SR6PCB", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://millibyte.store", + "vendor": "Millibyte LLC" +} \ No newline at end of file diff --git a/ESP32/boards/ssr1pcb.json b/ESP32/boards/ssr1pcb.json new file mode 100644 index 0000000..26cd748 --- /dev/null +++ b/ESP32/boards/ssr1pcb.json @@ -0,0 +1,37 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32_out.ld" + }, + "core": "esp32", + "extra_flags": "-DARDUINO_ESP32_DEV", + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "mcu": "esp32", + "variant": "ssr1pcb" + }, + "connectivity": [ + "wifi", + "bluetooth", + "ethernet", + "can" + ], + "debug": { + "openocd_board": "esp-wroom-32.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "SSR1PCB", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://millibyte.store", + "vendor": "Millibyte LLC" +} \ No newline at end of file diff --git a/ESP32/lib/struct/channel.h b/ESP32/lib/struct/channel.h index c6b6746..5ed5c1e 100644 --- a/ESP32/lib/struct/channel.h +++ b/ESP32/lib/struct/channel.h @@ -2,7 +2,7 @@ #include #include -#include "settingConstants.h" +#include "../settingConstants.h" struct Channel { const char* Name; @@ -30,7 +30,7 @@ struct Channel { obj[CHANNEL_IS_SWITCH] = isSwitch; obj[CHANNEL_SR6_ONLY] = sr6Only; } - + void fromJson(const JsonObject& obj) { Name = obj[CHANNEL_NAME]; FriendlyName = obj[CHANNEL_FRIENDLY_NAME]; diff --git a/ESP32/platformio.ini b/ESP32/platformio.ini index c8c573e..6e6728c 100644 --- a/ESP32/platformio.ini +++ b/ESP32/platformio.ini @@ -22,22 +22,12 @@ monitor_dtr = 0 [common] lib_deps = - ;ArduinoJson@7.2.0 ArduinoJson@7.4.2 - Arduino hacker-cb/MPark-Variant @ 1.4.0 - ;Arduino_TCode_Parser - ;https://github.com/multiaxis/TCode-Library#e0e572e - https://github.com/multiaxis/TCode-Library#a5ec926 - ;e8bb528 - ;https://github.com/joltwallet/esp_littlefs.git ; required for espidf with arduino component - + https://github.com/multiaxis/TCode-Library#v0.1.2 https://github.com/jcfain/LTC2944-Arduino-Library.git dfrobot/DFRobot_DF2301Q@^1.0.0 - ;nanopb/Nanopb @ 0.4.8 -build_unflags = -; -std=gnu++11 build_flags = -I src -I src/BLE @@ -45,117 +35,63 @@ build_flags = -I src/logging -I src/Motion -I src/TCode - ;-I src/TCode/v0.2 -I src/TCode/v0.3 -I src/TCode/v0.4 -I lib -I lib/Ext -I lib/struct -; -std=c++17 -;build_flags = -I../ParsingLibrary lib_compat_mode = strict lib_ldf_mode = chain ;to evaluate C/C++ Preprocessor conditional syntax for different builds. Keeps from compiling uneeded libraries. board_build.filesystem = littlefs [common:temperature] lib_deps = - ;paulstoffregen/OneWire@2.3.8 - ; https://github.com/PaulStoffregen/OneWire.git#72249e2 - https://github.com/PaulStoffregen/OneWire.git#800f26f - ;milesburton/DallasTemperature@3.11.0 + https://github.com/PaulStoffregen/OneWire.git#v2.3.8 milesburton/DallasTemperature@4.0.4 - ;Adafruit BusIO@1.16.1 Adafruit BusIO@1.17.2 - ; SPI@2.0.0 - ; Wire@2.0.0 - ;r-downing/AutoPID@^1.0.3 [common:display] lib_deps = - ;adafruit/Adafruit SSD1306@2.5.10 adafruit/Adafruit SSD1306@2.5.15 [common:bldc] lib_deps = -# Arduino V3 askuric/Simple FOC@2.3.5 simplefoc/SimpleFOCDrivers@1.0.9 -; # Arduino V2 -; askuric/Simple FOC@2.3.2 -; simplefoc/SimpleFOCDrivers@1.0.6 - [common:ESP32] -; platform = espressif32@6.8.1 -; https://github.com/pioarduino/platform-espressif32/ -;platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip ; double exception after display init? -;platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.04/platform-espressif32.zip -;platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.07/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip framework = arduino -# ################################################################################################ -# https://github.com/platformio/platform-espressif32/releases -# https://github.com/espressif/arduino-esp32/releases/tag/3.0.4 -# https://github.com/espressif/esp-idf/releases/tag/v5.1.4 -; platform_packages = -; platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.4 -; platformio/framework-arduinoespressif32-libs @ https://github.com/espressif/arduino-esp32/releases/download/3.0.4/esp32-arduino-libs-3.0.4.zip -######################################################################################################################################################### extends = common, com-ports lib_deps = ${common.lib_deps} build_flags = ${common.build_flags} monitor_filters = esp32_exception_decoder - ;direct - ;send_on_enter - ;colorize - ;debug - [common:ESP8266] platform = espressif8266@4.2.0 extends = common lib_deps = ${common.lib_deps} -build_unflags = ${common.build_unflags} -Werror=unused-const-variable +build_unflags = -Werror=unused-const-variable build_flags = ${common.build_flags} -D ESP8266 framework = arduino monitor_filters = esp8266_exception_decoder send_on_enter colorize - ;debug - -; [common:PICO] -; extends = common -; platform = raspberrypi -; board = pico -; framework = arduino -; build_flags = -D PICO_BUILD -; lib_deps = ${common.lib_deps} -; khoih-prog/AsyncWebServer_RP2040W @ 1.5.0 [common:ESP32-wifi] -# This is included as a required dependency in ESPAsyncWebServer -lib_ignore = - ;AsyncTCP lib_deps = ${common:ESP32.lib_deps} - # Am getting a watchdog timeout loading the html page so I forked Async tcp to make a small modification for now. - ;https://github.com/jcfain/AsyncTCP.git#3c03a52 ESP32Async/AsyncTCP@3.4.7 ESP32Async/ESPAsyncWebServer@3.8.0 - ;hoeken/PsychicHttp - ;https://github.com/me-no-dev/ESPAsyncWebServer.git#67de9cd - ;https://github.com/jasenk2/esp32_https_server.git#esp_tls - ;https://github.com/jasenk2/esp32_https_server.git build_flags = ${common:ESP32.build_flags} -D CONFIG_ASYNC_TCP_QUEUE_SIZE=64 -D ASYNCWEBSERVER_REGEX [common:ESP32-bluetooth] lib_deps = ${common:ESP32.lib_deps} h2zero/NimBLE-Arduino@2.3.4 - ;https://github.com/h2zero/NimBLE-Arduino.git#7bdcae5 build_flags = -D NIMBLE_LATEST [common:ESP8266-wifi] @@ -163,7 +99,6 @@ extends = common:ESP8266 lib_deps = ${common:ESP8266.lib_deps} ESP Async WebServer@1.2.4 - ;https://github.com/me-no-dev/ESPAsyncWebServer.git build_flags = ${common:ESP8266.build_flags} -D ASYNCWEBSERVER_REGEX #Common build @@ -171,10 +106,7 @@ build_flags = ${common:ESP8266.build_flags} -D ASYNCWEBSERVER_REGEX extends = common:ESP32, common:ESP32-wifi board = esp32doit-devkit-v1 build_type = release -; flash_mode = dio board_build.partitions = huge_app.csv -; f_cpu = 240000000L -; mcu = esp32 build_flags = -D WROOM32_MODULE ${common:ESP32-wifi.build_flags} ${common:ESP32-bluetooth.build_flags} @@ -194,8 +126,6 @@ lib_deps = ${env:esp32doit-devkit-v1.lib_deps} ${common:bldc.lib_deps} lib_archive = false #required for SimpleFOC -# Debug builds -#Debug build that does NOT require special hardware [env:esp32doit-devkit-v1-debug] extends = env:esp32doit-devkit-v1 build_unflags = -D DEBUG_BUILD=0 ;-Os @@ -216,8 +146,6 @@ debug_init_break = tbreak setup debug_tool = esp-prog build_flags = ${env:esp32doit-devkit-v1-debug.build_flags} -D ESP_PROG -;upload_protocol = esp-prog - [env:esp32-devkit-v4] extends = env:esp32doit-devkit-v1 @@ -232,8 +160,6 @@ board = az-delivery-devkit-v4 [env:esp32-s3-zero] extends = common:ESP32, common:ESP32-wifi board = esp32-s3-fh4r2 -;board = waveshare_esp32_s3_zero -;board = esp32-s3-devkitm-1 #Using the custom board above. Keep and eye out for an official board release build_type = release board_build.partitions = huge_app.csv build_flags = -D S3_ZERO @@ -263,9 +189,7 @@ extends = common:ESP32, common:ESP32-wifi board = esp32-s3-devkitc-1 build_type = release board_build.partitions = default_8MB.csv -;board_build.partitions = max_app_8MB.csv board_build.arduino.memory_type = dio_opi ; NEEDED FOR PSRAM -;board_build.arduino.memory_type = opi_qspi build_flags = -D BOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ${common:ESP32-wifi.build_flags} @@ -295,59 +219,53 @@ debug_init_break = tbreak setup debug_tool = cmsis-dap upload_protocol = cmsis-dap -# Other builds that do not work -; [env:pico] -; extends = common:PICO -; build_flags = ${common:PICO.build_flags} -; -D DEBUG_BUILD=0 -D BUILD_TEMP=0 -D BUILD_DISPLAY=0 -D BLUETOOTH_TCODE=0 -D BLE_TCODE=1 -D WIFI_TCODE=1 -D MOTOR_TYPE=0 -D SECURE_WEB=0 #-D CORE_DEBUG_LEVEL=5 -; lib_deps = ${common:PICO.lib_deps} -; ${common:display.lib_deps} -; ;${common:TCode_V2.lib_deps} -; ${common:ESP32-bluetooth.lib_deps} - -; [env:lolin_s3] -; extends = common:ESP32-wifi -; board = lolin_s3 -; board_build.partitions = default_16MB.csv -; platform = espressif32 -; build_type = release -; build_flags = ${common:ESP32-wifi.build_flags} -; -D BOARD_HAS_PSRAM -; -D ARDUINO_USB_CDC_ON_BOOT=1 -; -mfix-esp32-psram-cache-issue -; -D DEBUG_BUILD=0 -D BUILD_TEMP=1 -D BUILD_DISPLAY=1 -D BLUETOOTH_TCODE=0 -D WIFI_TCODE=1 -D MOTOR_TYPE=0 -D SECURE_WEB=0 #-D CORE_DEBUG_LEVEL=5 #-D FW_VERSION=%%date%% -; lib_deps = ${common:ESP32-wifi.lib_deps} -; ${common:display.lib_deps} -; ${common:temperature.lib_deps} -; ${common:ESP32-bluetooth.lib_deps} - -; #ESP32 DA (Dual antennae) -; [env:esp32doit-devkit-D A] -; extends = common:esp32doit-devkit-v1-wifi -; build_flags = ${common:ESP32-wifi.build_flags} -; -D DEBUG_BUILD=0 -D BUILD_TEMP=0 -D BUILD_DISPLAY=0 -D BLUETOOTH_TCODE=0 -D WIFI_TCODE=1 #-D CORE_DEBUG_LEVEL=5 - -; [env:esp32doit-devkit-D A-display-temp] -; extends = env:esp32doit-devkit-v1-display-temp -; build_flags = ${common:ESP32-wifi.build_flags} -; -D DEBUG_BUILD=0 -D BUILD_TEMP=1 -D BUILD_DISPLAY=1 -D BLUETOOTH_TCODE=0 -D WIFI_TCODE=1 #-D CORE_DEBUG_LEVEL=5 - -; [env:esp8266-ESP01] -; extends = common:ESP8266-wifi -; board = esp01_1m -; build_type = release -; board_build.partitions = esp-01-partitions.csv -; build_flags = ${common:ESP8266-wifi.build_flags} -; -D ESP01=1 -D DEBUG_BUILD=0 -D BUILD_TEMP=0 -D BUILD_DISPLAY=0 -D BLUETOOTH_TCODE=0 -D BLE_TCODE=0 -D WIFI_TCODE=1 -D MOTOR_TYPE=0 -D SECURE_WEB=0 #-D CORE_DEBUG_LEVEL=5 -; lib_deps = ${common:ESP8266-wifi.lib_deps} -; ; ${common:display.lib_deps} -; ; ${common:temperature.lib_deps} -; ; ${common:TCode_V2.lib_deps} - +#Board specific builds [env:ssr1_pcb] -extends = env:esp32doit-devkit-v1-bldc -build_flags = -D DEFAULT_BOARD=BoardType::SSR1PCB +extends = common:ESP32, common:ESP32-wifi +board = ssr1pcb +build_type = release +board_build.partitions = huge_app.csv +build_flags = -D WROOM32_MODULE + ${common:ESP32-wifi.build_flags} + ${common:ESP32-bluetooth.build_flags} + -D DEBUG_BUILD=0 + -D BUILD_TEMP=1 + -D BUILD_DISPLAY=1 + -D BLUETOOTH_TCODE=0 + -D BLE_TCODE=1 + -D WIFI_TCODE=1 + -D MOTOR_TYPE=1 + -D SECURE_WEB=0 + -D COEXIST=1 + -D DEFAULT_BOARD=BoardType::SSR1PCB +lib_deps = ${common:ESP32.lib_deps} + ${common:ESP32-wifi.lib_deps} + ${common:display.lib_deps} + ${common:temperature.lib_deps} + ${common:ESP32-bluetooth.lib_deps} + ${common:bldc.lib_deps} +lib_archive = false #required for SimpleFOC [env:sr6_pcb] -extends = env:esp32doit-devkit-v1 -build_flags = -D DEFAULT_BOARD=BoardType::SR6PCB \ No newline at end of file +extends = common:ESP32, common:ESP32-wifi +board = ssr1pcb +build_type = release +board_build.partitions = huge_app.csv +build_flags = -D WROOM32_MODULE + ${common:ESP32-wifi.build_flags} + ${common:ESP32-bluetooth.build_flags} + -D DEBUG_BUILD=0 + -D BUILD_TEMP=1 + -D BUILD_DISPLAY=1 + -D BLUETOOTH_TCODE=0 + -D BLE_TCODE=1 + -D WIFI_TCODE=1 + -D MOTOR_TYPE=0 + -D SECURE_WEB=0 + -D COEXIST=1 + -D DEFAULT_BOARD=BoardType::SR6PCB +lib_deps = ${common:ESP32.lib_deps} + ${common:ESP32-wifi.lib_deps} + ${common:display.lib_deps} + ${common:temperature.lib_deps} + ${common:ESP32-bluetooth.lib_deps} \ No newline at end of file diff --git a/ESP32/src/BLE/BLECharacteristicCallbacksBase.h b/ESP32/src/BLE/BLECharacteristicCallbacksBase.h deleted file mode 100644 index 632e9a2..0000000 --- a/ESP32/src/BLE/BLECharacteristicCallbacksBase.h +++ /dev/null @@ -1,93 +0,0 @@ -/* MIT License - -Copyright (c) 2024 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include - -#include -#include -#include - -#include "logging/LogHandler.h" - -#include "BLEServerCallbacksBase.h" -#include "TagHandler.h" -#include "constants.h" - -class BLECharacteristicCallbacksBase: public NimBLECharacteristicCallbacks { -public: - - #ifdef NIMBLE_LATEST - void onRead(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override { - #else - void onRead(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc) override { - #endif - Serial.print(pCharacteristic->getUUID().toString().c_str()); - Serial.print(": onRead(), value: "); - Serial.println(pCharacteristic->getValue().c_str()); - }; - void onNotify(NimBLECharacteristic* pCharacteristic) { - Serial.println("Sending notification to clients"); - }; - - /** - * The value returned in code is the NimBLE host return code. - */ - #ifdef NIMBLE_LATEST - void onStatus(NimBLECharacteristic* pCharacteristic, int code) override { - #else - void onStatus(NimBLECharacteristic* pCharacteristic, Status s, int code) override { - #endif - String str = ("Notification/Indication return code: "); - str += code; - str += ", "; - str += NimBLEUtils::returnCodeToString(code); - Serial.println(str); - }; - - #ifdef NIMBLE_LATEST - void onSubscribe(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo, uint16_t subValue) override { - String str = "Client ID: "; - str += connInfo.getConnHandle(); - str += " Address: "; - str += connInfo.getAddress().toString().c_str(); - #else - void onSubscribe(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc, uint16_t subValue) override { - String str = ""; - #endif - if(subValue == 0) { - str += " Unsubscribed to "; - }else if(subValue == 1) { - str += " Subscribed to notfications for "; - } else if(subValue == 2) { - str += " Subscribed to indications for "; - } else if(subValue == 3) { - str += " Subscribed to notifications and indications for "; - } - str += std::string(pCharacteristic->getUUID()).c_str(); - - Serial.println(str); - }; - - private: -}; diff --git a/ESP32/src/BLE/BLEConfigurationHandler.h b/ESP32/src/BLE/BLEConfigurationHandler.h deleted file mode 100644 index 98fa84e..0000000 --- a/ESP32/src/BLE/BLEConfigurationHandler.h +++ /dev/null @@ -1,323 +0,0 @@ -/* MIT License - -Copyright (c) 2024 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once -/* This works but we cant use Bluetooth and Wifi at the same time with out performance loss due to them having the same radio.... -We need all the performance we can get in wifi so commenting this out. Maybe oneday they will launch a new board with them seperate. -For now this is only for first configs */ -#include -#include -#include -#include -#include -#include -#include "SettingsHandler.h" -// #include "LogHandler.h" -#include "TagHandler.h" - - -#define SERVICE_UUID "ff1b451d-3070-4276-9c81-5dc5ea1043bc" // UART service UUID -#define CHARACTERISTIC_UUID "c5f1543e-338d-47a0-8525-01e3c621359d" - -class CharacteristicCallbacks: public BLECharacteristicCallbacks -{ - String recievedJsonConfiguration = ""; - - #ifdef ESP_ARDUINO3 - String sendJsonConfiguration = ""; - #else - std::string sendJsonConfiguration = ""; - #endif - unsigned sendChunkIndex = 0; - bool sentLength = false; - int sendMaxLen = 499; - - - void onWrite(BLECharacteristic *pCharacteristic) - { - SettingsHandler::printMemory(); - LogHandler::verbose(_TAG, "*** BLE onWrite"); - #ifdef ESP_ARDUINO3 - String rxValue = pCharacteristic->getValue(); - #else - std::string rxValue = pCharacteristic->getValue(); - #endif - - if (rxValue.length() > 0) - { - LogHandler::debug(_TAG, "*********"); - LogHandler::debug(_TAG, "BLE Characteristic Received Value: "); - - for (int i = 0; i < rxValue.length(); i++) { - Serial.print(rxValue[i]); - } - - Serial.println(); - - // Do stuff based on the command received from the app - if (rxValue.find(">>r<<") == 0) // Restart - { - LogHandler::debug(_TAG, "*** BLE Restarting"); - ESP.restart(); - } - else if (rxValue.find(">>t<<") == -1) - { - pCharacteristic->setValue(">"); // More please (Doesnt really matter as the que is client side) - pCharacteristic->notify(); - LogHandler::debug(_TAG, "*** BLE Characteristic Sent Value: "); - LogHandler::debug(_TAG, "Ok"); - LogHandler::debug(_TAG, " ***"); - recievedJsonConfiguration += rxValue.data(); - } - else - { - // Save json - LogHandler::debug(_TAG, "Done: %s", recievedJsonConfiguration); - if(SettingsHandler::saveAll(recievedJsonConfiguration)) - { - pCharacteristic->setValue(">>f<<"); // Finish saving - pCharacteristic->notify(); - LogHandler::debug(_TAG, "*** BLE Finish saving"); - } - else - { - pCharacteristic->setValue(">>e<<"); // Error - LogHandler::error(_TAG, "*** BLE Error saving"); - pCharacteristic->notify(); - } - recievedJsonConfiguration = ""; - } - - Serial.println(); - LogHandler::verbose(_TAG, "BLE onWrite ***"); - } - } - void onRead(BLECharacteristic *pCharacteristic) - { - LogHandler::verbose(_TAG, "*** BLE onRead"); - // char* sentValue = SettingsHandler::getJsonForBLE(); - if(sendJsonConfiguration.empty()) { - char settings[2048] = {0}; - #warning need to send settings somehow - //SettingsHandler::ser(settings); - LogHandler::debug(_TAG, "BLE Get wifi settings: %s", settings); - if (strlen(settings) == 0) { - LogHandler::error(_TAG, "*** BLE onRead empty"); - pCharacteristic->setValue(">>e<<"); - pCharacteristic->notify(); - return; - } - //LogHandler::info(_TAG, "*** Sent Value: %s", wifiSetting); - //const int len = strlen(wifiSetting); - //LogHandler::info(_TAG, "*** strlen: %i", strlen(wifiSetting)); - sendJsonConfiguration = std::string(settings); - LogHandler::debug(_TAG, "BLE Get wifi string: %s", sendJsonConfiguration.c_str()); - } - - // size_t chunksize = wifiSettingsString.size()/19+1; - // for(size_t i=0; isetValue(value); - // pCharacteristic->notify(); - // } - if(!sentLength) { - sentLength = true; - char lengthNotify[10]; - sprintf(lengthNotify, ">>l<<:%zu", sendJsonConfiguration.length()); - pCharacteristic->setValue(lengthNotify); - pCharacteristic->notify(); - - } else if(sendChunkIndex < sendJsonConfiguration.length()) { - if(sendJsonConfiguration.length() > sendMaxLen) { - std::string value = sendJsonConfiguration.substr(sendChunkIndex,sendMaxLen); - printf("index %d, length: %i, %s\n", sendChunkIndex, sendJsonConfiguration.length(), value.c_str()); - pCharacteristic->setValue(value); - pCharacteristic->notify(); - // printf("ESP.getFreeHeap() %i\n", ESP.getFreeHeap()); - // printf("ESP.getHeapSize() %i\n", ESP.getHeapSize()); - sendChunkIndex += sendMaxLen; - - //delay(3); - } else { - pCharacteristic->setValue(sendJsonConfiguration); - pCharacteristic->notify(); - } - } else { - LogHandler::debug(_TAG, ">>f<<"); - pCharacteristic->setValue(">>f<<"); - pCharacteristic->notify(); - sendJsonConfiguration = ""; - sentLength = false; - sendChunkIndex = 0; - } - // int i = 0; - // int maxLen = 19; - // if(len > maxLen) { - // while (i*maxLen < len) { - // char chunk[maxLen + 1]; - // memset(chunk, '\0', sizeof(chunk)); - // memcpy(chunk, wifiSetting+(i*maxLen), maxLen); - // pCharacteristic->setValue((uint8_t*)chunk, maxLen); - // pCharacteristic->notify(); - - // printf("loop %d, i*maxLen: %i : %s\n", i, i*maxLen, chunk); - // delay(3); - // i++; - // } - // } else { - // pCharacteristic->setValue(wifiSetting); - // pCharacteristic->notify(); - // } - Serial.println(); - LogHandler::debug(_TAG, "BLE Onread Ok ***"); - } -public: - void resetState() { - recievedJsonConfiguration = ""; - sendJsonConfiguration = ""; - sentLength = false; - sendChunkIndex = 0; - } -private: - const char* _TAG = TagHandler::BLEHandler; - // QueueHandle_t debugInQueue; - // int m_lastSend; - // TaskHandle_t* emptyQueueHandle; - // bool emptyQueueRunning; -}; -CharacteristicCallbacks *characteristicCallbacks = new CharacteristicCallbacks(); - -class MyServerCallbacks: public BLEServerCallbacks { - void onConnect(BLEServer* pServer) - { - LogHandler::debug(_TAG, "*********"); - LogHandler::debug(_TAG, "Device connected"); - characteristicCallbacks->resetState(); - }; - void onDisconnect(BLEServer* pServer) - { - LogHandler::debug(_TAG, "*********"); - LogHandler::debug(_TAG, "Device disconnected"); - characteristicCallbacks->resetState(); - BLEDevice::startAdvertising(); - } -private: - const char* _TAG = TagHandler::BLEHandler; -}; - -class DescriptorCallbacks: public BLEDescriptorCallbacks -{ - void onWrite(BLEDescriptor *pDescriptor) - { - LogHandler::debug(_TAG, "*********"); - LogHandler::debug(_TAG, "Descriptor onWrite: "); - } - void onRead(BLEDescriptor *pDescriptor) - { - LogHandler::debug(_TAG, "*********"); - LogHandler::debug(_TAG, "Descriptor onRead: "); - } -private: - const char* _TAG = TagHandler::BLEHandler; -}; - -class BLEConfigurationHandler -{ - private: - const char* _TAG = TagHandler::BLEHandler; - BLECharacteristic *pCharacteristic; - bool deviceConnected = false; - bool isInitailized = false; - float txValue = 0; - BLEServer *pServer; - BLEService *pService; - BLEAdvertising *pAdvertising; - public: - void setup() - { - // Create the BLE Device - LogHandler::info(_TAG, "Setup BLE: %s", "TCodeConfig"); - BLEDevice::init("TCodeConfig"); // Give it a name - //BLEDevice::setMTU(23); - // Create the BLE Server - pServer = BLEDevice::createServer(); - pServer->setCallbacks(new MyServerCallbacks()); - // Create the BLE Service - pService = pServer->createService(SERVICE_UUID); - - // Create a BLE Characteristic - // Create a BLE Characteristic - pCharacteristic = pService->createCharacteristic( - CHARACTERISTIC_UUID, - BLECharacteristic::PROPERTY_READ | - BLECharacteristic::PROPERTY_WRITE | - BLECharacteristic::PROPERTY_WRITE_NR | - BLECharacteristic::PROPERTY_NOTIFY | - BLECharacteristic::PROPERTY_INDICATE - ); - BLEDescriptor* p2902Descriptor = new BLEDescriptor(CHARACTERISTIC_UUID); - p2902Descriptor->setCallbacks(new DescriptorCallbacks()); - pCharacteristic->addDescriptor(p2902Descriptor); - pCharacteristic->setCallbacks(characteristicCallbacks); - - // Start the service - pService->start(); - - // Start advertising - // Start advertising - pAdvertising = BLEDevice::getAdvertising(); - pAdvertising->addServiceUUID(SERVICE_UUID); - pAdvertising->setScanResponse(true); - pAdvertising->setMinPreferred(0x0); // set value to 0x00 to not advertise this parameter - BLEDevice::startAdvertising(); - LogHandler::info(_TAG, "BLE waiting a client connection to notify..."); - - isInitailized = true; - - LogHandler::debug(_TAG, "Exit setup"); - } - - void stop() - { - if(isInitailized) - { - LogHandler::info(_TAG, "Stop"); - BLEDevice::deinit(true); - LogHandler::debug(_TAG, "deinit"); - isInitailized = false; - if(pServer != nullptr) - delete(pServer); - if(pService != nullptr) - delete(pService); - if(pCharacteristic != nullptr) - delete(pCharacteristic); - if(pAdvertising != nullptr) - delete(pAdvertising); - if(characteristicCallbacks != nullptr) - delete(characteristicCallbacks); - } - } -}; - - diff --git a/ESP32/src/BLE/BLEHCControlCallback.h b/ESP32/src/BLE/BLEHCControlCallback.h deleted file mode 100644 index 0f387b4..0000000 --- a/ESP32/src/BLE/BLEHCControlCallback.h +++ /dev/null @@ -1,120 +0,0 @@ -/* MIT License - -Copyright (c) 2024 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include -#include - -#include -#include -#include - -#include "logging/LogHandler.h" - -#include "BLECharacteristicCallbacksBase.h" -#include "TagHandler.h" -#include "constants.h" - -class BLEHCControlCallback: public BLECharacteristicCallbacksBase -{ -public: - BLEHCControlCallback(QueueHandle_t tcodeQueue): m_TCodeQueue(tcodeQueue) { } - // At some point this signature will change because its in master so if Bluetooth breaks, check the source class signature. - #ifdef NIMBLE_LATEST - void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override { - #else - void onWrite(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc) override { - #endif - // uint16_t handle = pCharacteristic->getHandle(); - NimBLEAttValue rxValue = pCharacteristic->getValue(); - size_t rxLength = rxValue.length(); - const uint8_t* rxData = rxValue.data(); - if(rxLength > 4) { - Serial.println("Warning: it seems the format of HC data has changed."); - return; - } - - // // //esp_gatt_char_prop_t prop = pCharacteristic->; - // // //Serial.println(*rxValue,HEX); - // // //00F418713F41 - // // // uint8_t x; - // // // sscanf(rxValue, "%x", &x); - // // //int value = (int)strtol(rxValue, NULL, 0); - // // // LogHandler::info(_TAG, "rxValue: %u", *rxValue); - // Serial.print("rxLength: "); - // Serial.print(rxLength); - // Serial.println(); - // Serial.print("handle: "); - // Serial.print(handle); - // Serial.println(); - // // LogHandler::info(m_TAG, "handle: %d", handle); - // // LogHandler::info(m_TAG, "rxLength: %d", rxLength); - - // // // xQueueSend(m_TCodeQueue, input, 0); - // // Serial.println(); - // for (int i = 0; i < rxLength; i++) { - // //tcode.ByteInput(input[i]); - // //tcode.ByteInput(input[i]); - // Serial.print(rxData[i] < 16 ? "0" : ""); - // Serial.print(rxData[i],HEX); - // if(i> 16; - tcode[MAX_COMMAND] = {0}; - snprintf(tcode, MAX_COMMAND, "L0%03dI%d\n", tcodeBytes, speedBytes); - - LogHandler::verbose(TagHandler::BLEHandler, "Receive HC tcode: %s", tcode); - if(xQueueSend(m_TCodeQueue, tcode, 0) != pdTRUE) { - LogHandler::error(TagHandler::BLEHandler, "Failed to write to queue"); - } - } -private: - const char* m_TAG = TagHandler::BLEHandler; - QueueHandle_t m_TCodeQueue; - char tcode[MAX_COMMAND] = {0}; - - template - T swap_endian(T u) - { - static_assert (CHAR_BIT == 8, "CHAR_BIT != 8"); - - union - { - T u; - unsigned char u8[sizeof(T)]; - } source, dest; - - source.u = u; - - for (size_t k = 0; k < sizeof(T); k++) - dest.u8[k] = source.u8[sizeof(T) - k - 1]; - - return dest.u; - } -}; \ No newline at end of file diff --git a/ESP32/src/BLE/BLEHandler.hpp b/ESP32/src/BLE/BLEHandler.hpp deleted file mode 100644 index ddf7054..0000000 --- a/ESP32/src/BLE/BLEHandler.hpp +++ /dev/null @@ -1,208 +0,0 @@ -/* MIT License - -Copyright (c) 2024 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include -#include -#include -// #include -#include -#include -#include "esp_coexist.h" -#include "constants.h" -// #include "LogHandler.h" -#include "TagHandler.h" -#include "TCode/MotorHandler.h" -#include "logging/LogHandler.h" -#include "BLEHandlerBase.h" -#include "BLEHandlerTCode.h" -#include "BLEHandlerLove.h" -#include "BLEHandlerHC.h" - -class BLEHandler -{ -public: - BLEHandler() - { - m_TCodeQueue = xQueueCreate(25, sizeof(char[MAX_COMMAND])); - m_callBackQueue = xQueueCreate(5, sizeof(char[MAX_COMMAND])); - } - void setup() - { - // auto callbacks = getCaracteristicCallbacks(); - SettingsFactory::getInstance()->getValue(BLE_DEVICE_TYPE, m_bleDeviceType); - - m_subHandler = getHandler(); - m_subHandler->setup(m_TCodeQueue); - - // LogHandler::debug(_TAG, "Setting up BLE Characteristics"); - // if(m_isHC) { - // m_tcodeCharacteristic = new BLECharacteristic(callbacks->CHARACTERISTIC_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR); - // m_tcodeCharacteristic2 = new BLECharacteristic(callbacks->CHARACTERISTIC_UUID2, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR); - // m_tcodeCharacteristic2->setCallbacks(callbacks); - // } else { - // m_tcodeCharacteristic = new BLECharacteristic(callbacks->CHARACTERISTIC_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE_NR); - // } - // LogHandler::debug(_TAG, "Setting up BLE Characteristic Callbacks"); - // m_tcodeCharacteristic->setCallbacks(callbacks); - - // pService->addCharacteristic(m_tcodeCharacteristic); - - // if(m_isHC) { - // pService->addCharacteristic(m_tcodeCharacteristic2); - // } - - // //https://github.com/espressif/esp-idf/issues/12434 - // // Before sending BLE command - // esp_coex_status_bit_set(ESP_COEX_ST_TYPE_BLE, ESP_COEX_BLE_ST_MESH_CONFIG); - - // // After sending - // esp_coex_status_bit_clear(ESP_COEX_ST_TYPE_BLE, ESP_COEX_BLE_ST_MESH_CONFIG); - // Before sending BLE command - // esp_coex_preference_set(ESP_COEX_PREFER_BT); - - // After sending - // esp_coex_preference_set(ESP_COEX_PREFER_WIFI); - - // xTaskCreatePinnedToCore( - // bleTask,/* Function to implement the task */ - // "BLETask", /* Name of the task */ - // configMINIMAL_STACK_SIZE*4, /* Stack size in words */ - // static_cast(this), /* Task input parameter */ - // tskIDLE_PRIORITY, /* Priority of the task */ - // &m_bleTask, /* Task handle. */ - // CONFIG_BT_NIMBLE_PINNED_TO_CORE); /* Core where the task should run */ - } - - // static void bleTask(void* arg) { - // BLEHandler* handler = static_cast(arg); - // TickType_t pxPreviousWakeTime = millis(); - // while(1) { - // auto len = handler->m_tcodeCharacteristic->getDataLength(); - // if(len) { - // const char* value = handler->m_tcodeCharacteristic->getValue().c_str(); - // LogHandler::verbose(_TAG, "Recieve tcode: %s", value); - // //strncpy(buf, value, m_tcodeCharacteristic->getDataLength()); - // // if(strlen(value)) - // // handler->m_motorHandler->read(value, len); - // //handler->m_motorHandler->read(value); - // if(xQueueSend(m_TCodeQueue, value, 0) != pdTRUE) { - // //LogHandler::error(_TAG, "Failed to write to queue"); - // } - // } - // xTaskDelayUntil(&pxPreviousWakeTime, 10/portTICK_PERIOD_MS); - // } - // } - - void read(char *buf) - { - // if(m_tcodeCharacteristic->getDataLength()) { - // const char* value = m_tcodeCharacteristic->getValue().c_str(); - // strncpy(buf, value, m_tcodeCharacteristic->getDataLength()); - // } - // else - // { - // buf[0] = {0}; - // } - if (xQueueReceive(m_TCodeQueue, buf, 0)) - { - // LogHandler::verbose(_TAG, "Recieve tcode: %s", buf); - } - else - { - // LogHandler::error(_TAG, "Failed to read from queue"); - buf[0] = {0}; - } - } - - bool isConnected() - { - if(!m_subHandler) - return false; - return m_subHandler->isConnected(); - } - - void CommandCallback(const char *in) - { - if(m_subHandler) - m_subHandler->CommandCallback(in); - } - - static void disable() - { - LogHandler::info(_TAG, "Disable BLE"); - //BLEDevice::deinit(); - //esp_err_t disable = esp_bt_controller_deinit(); - esp_err_t disable = esp_bt_controller_mem_release(ESP_BT_MODE_BTDM); - // esp_bluedroid_disable(); - // esp_bluedroid_deinit(); - // esp_bt_controller_disable(); - // esp_bt_controller_deinit(); - //esp_err_t disable = esp_bt_mem_release(ESP_BT_MODE_BTDM); - if (disable != ESP_OK) - { - LogHandler::error(_TAG, "Disable fail: %s", esp_err_to_name(disable)); - } - }; - -private: - // friend class BLETCodeControlCallback; - // friend class BLELoveControlCallback; - static const char *_TAG; - // const char* BLE_DEVICE_NAME = "TCODE-ESP32"; - // const char* BLE_TCODE_SERVICE_UUID = "ff1b451d-3070-4276-9c81-5dc5ea1043bc"; - // const char* BLE_TCODE_CHARACTERISTIC_UUID = "c5f1543e-338d-47a0-8525-01e3c621359d"; - BLEDeviceType m_bleDeviceType; - bool m_isHC = false; - bool m_isLove = true; - BLEHandlerBase *m_subHandler = 0; - - QueueHandle_t m_TCodeQueue; - QueueHandle_t m_callBackQueue; - // // Haptics connect UUID's - // const char* BLE_DEVICE_NAME_HC = "OSR-ESP32"; - // const char* BLE_TCODE_SERVICE_UUID_HC = "00004000-0000-1000-8000-0000101A2B3C"; - // const char* BLE_TCODE_CHARACTERISTIC_UUID_HC = "00002000-0001-1000-8000-0000101A2B3C"; - // const char* BLE_TCODE_CHARACTERISTIC_UUID2_HC = "00002000-0002-1000-8000-0000101A2B3C"; - - TaskHandle_t m_bleTask; - BLEHandlerBase *getHandler() - { - if (m_bleDeviceType == BLEDeviceType::LOVE) - { - LogHandler::info(_TAG, "Setting up BLE Love handler"); - static BLEHandlerLove bleHandler; - return &bleHandler; - } - if (m_bleDeviceType == BLEDeviceType::HC) - { - LogHandler::info(_TAG, "Setting up BLE HC handler"); - static BLEHandlerHC bleHandler; - return &bleHandler; - } - LogHandler::info(_TAG, "Setting up BLE Tcode handler"); - static BLEHandlerTCode bleHandler; - return &bleHandler; - }; -}; -const char *BLEHandler::_TAG = TagHandler::BLEHandler; diff --git a/ESP32/src/BLE/BLEHandlerBase.h b/ESP32/src/BLE/BLEHandlerBase.h deleted file mode 100644 index a0134af..0000000 --- a/ESP32/src/BLE/BLEHandlerBase.h +++ /dev/null @@ -1,100 +0,0 @@ -/* MIT License - -Copyright (c) 2024 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include -#include -#include -//#include -#include -#include -#include "esp_coexist.h" -#include "constants.h" -// #include "LogHandler.h" -#include "TagHandler.h" -#include "logging/LogHandler.h" -#include "BLEServerCallbacksBase.h" - -class BLEHandlerBase { -public: - const char* NAME = "TCODE-ESP32"; - const char* SERVICE_UUID = "ff1b451d-3070-4276-9c81-5dc5ea1043bc"; - BLEHandlerBase(const char* name, const char* serviceUUID): - NAME(name), - SERVICE_UUID(serviceUUID) { } - - void setup(QueueHandle_t tcodeQueue) - { - LogHandler::info(TagHandler::BLEHandler, "Setting up BLE handler: %s", NAME); - BLEDevice::init(NAME); - esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, ESP_PWR_LVL_P9); - esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, ESP_PWR_LVL_P9); - esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_SCAN ,ESP_PWR_LVL_P9); - LogHandler::debug(TagHandler::BLEHandler, "Setting up BLE Create server"); - BLEServer *pServer = BLEDevice::createServer(); - pServer->setCallbacks(getServerCallbacks()); - pServer->advertiseOnDisconnect(true); - - BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); - pAdvertising->addServiceUUID(SERVICE_UUID); - pAdvertising->enableScanResponse(true); - // Functions that help with iPhone connections issue - // https://github.com/nkolban/esp32-snippets/issues/768 - pAdvertising->setPreferredParams(0x06, 0x12); - - LogHandler::debug(TagHandler::BLEHandler, "Setting up BLE service"); - BLEService *pService = pServer->createService(SERVICE_UUID); - - setupCharacteristics(pService, pAdvertising, tcodeQueue); - - LogHandler::debug(TagHandler::BLEHandler, "Starting BLE service"); - if(pService->start()) { - LogHandler::info(TagHandler::BLEHandler, "Started BLE service"); - } else { - LogHandler::error(TagHandler::BLEHandler, "Failed to start BLE service."); - } - - LogHandler::debug(TagHandler::BLEHandler, "Starting BLE advertising"); - if(BLEDevice::startAdvertising()) - LogHandler::info(TagHandler::BLEHandler, "Started BLE server."); - else - LogHandler::error(TagHandler::BLEHandler, "Failed to start BLE advertising."); - - } - - bool isConnected() - { - return getServerCallbacks()->isConnected(); - } - - virtual void CommandCallback(const char* in) = 0; - -private: - bool m_connected = false; - ServerCallbacks* getServerCallbacks() - { - static ServerCallbacks callbacks; - return &callbacks; - }; - virtual void setupCharacteristics(BLEService *pService, BLEAdvertising *pAdvertising, QueueHandle_t tcodeQueue) = 0; -}; \ No newline at end of file diff --git a/ESP32/src/BLE/BLEHandlerHC.h b/ESP32/src/BLE/BLEHandlerHC.h deleted file mode 100644 index 106bf4f..0000000 --- a/ESP32/src/BLE/BLEHandlerHC.h +++ /dev/null @@ -1,70 +0,0 @@ - -/* MIT License - -Copyright (c) 2024 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include -#include -#include -//#include -#include -#include -#include "logging/LogHandler.h" - -#include "BLEHCControlCallback.h" -#include "constants.h" -#include "TagHandler.h" -#include "BLEHandlerBase.h" - -class BLEHandlerHC :public BLEHandlerBase { -public: - BLEHandlerHC() : BLEHandlerBase("OSR-ESP32", "00004000-0000-1000-8000-0000101A2B3C") { } -private: - const char* CHARACTERISTIC_UUID = "00002000-0001-1000-8000-0000101A2B3C"; - const char* CHARACTERISTIC_UUID2 = "00002000-0002-1000-8000-0000101A2B3C"; - BLECharacteristic* m_characteristic; - BLECharacteristic* m_characteristic2; - // Haptics connect UUID's - // const char* NAME = "OSR-ESP32"; - // const char* SERVICE_UUID = "00004000-0000-1000-8000-0000101A2B3C"; - // const char* CHARACTERISTIC_UUID = "00002000-0001-1000-8000-0000101A2B3C"; - // const char* CHARACTERISTIC_UUID2_HC = "00002000-0002-1000-8000-0000101A2B3C"; - - void setupCharacteristics(BLEService *pService, BLEAdvertising *pAdvertising, QueueHandle_t tcodeQueue) override { - m_characteristic = new BLECharacteristic(CHARACTERISTIC_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE_NR); - m_characteristic2 = new BLECharacteristic(CHARACTERISTIC_UUID2, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE_NR); - LogHandler::debug(TagHandler::BLEHandler, "Setting up BLE TCode Characteristic Callbacks"); - m_characteristic->setCallbacks(getCaracteristicCallbacks(tcodeQueue)); - pService->addCharacteristic(m_characteristic); - pService->addCharacteristic(m_characteristic2); - } - - BLECharacteristicCallbacksBase* getCaracteristicCallbacks(QueueHandle_t tcodeQueue) { - static BLEHCControlCallback callbacks(tcodeQueue); - return &callbacks; - } - void CommandCallback(const char* in) override { - // m_characteristic->setValue(in); - // m_characteristic->notify(); - }; -}; \ No newline at end of file diff --git a/ESP32/src/BLE/BLEHandlerLove.h b/ESP32/src/BLE/BLEHandlerLove.h deleted file mode 100644 index 7beaa90..0000000 --- a/ESP32/src/BLE/BLEHandlerLove.h +++ /dev/null @@ -1,74 +0,0 @@ -/* MIT License - -Copyright (c) 2024 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include -#include -#include -#include -#include -#include "logging/LogHandler.h" - -#include "BLELoveControlCallback.h" -#include "constants.h" -#include "TagHandler.h" -#include "BLEHandlerBase.h" - -class BLEHandlerLove :public BLEHandlerBase { -public: - BLEHandlerLove() : BLEHandlerBase("LOVE-H", "50300001-0023-4bd4-bbd5-a6920e4c5653") { } -private: - const char* TXCHARACTERISTIC_UUID = "50300003-0023-4bd4-bbd5-a6920e4c5653"; - const char* RXCHARACTERISTIC_UUID = "50300002-0023-4bd4-bbd5-a6920e4c5653"; - BLECharacteristic* m_pTxCharacteristic; - BLECharacteristic* m_pRxCharacteristic; - - void setupCharacteristics(BLEService *pService, BLEAdvertising *pAdvertising, QueueHandle_t tcodeQueue) override { - BLEDevice::setSecurityAuth(true, true, true); - m_pTxCharacteristic = pService->createCharacteristic( - TXCHARACTERISTIC_UUID, - NIMBLE_PROPERTY::NOTIFY - ); - m_pRxCharacteristic = pService->createCharacteristic( - RXCHARACTERISTIC_UUID, - NIMBLE_PROPERTY::WRITE | - NIMBLE_PROPERTY::WRITE_NR - ); - auto callbacks = getCaracteristicCallbacks(tcodeQueue); - static_cast(callbacks)->setTX(m_pTxCharacteristic); - m_pRxCharacteristic->setCallbacks(callbacks); - pAdvertising->enableScanResponse(false); - pAdvertising->setPreferredParams(0x0, 0x0); // set value to 0x00 to not advertise this parameter - //pAdvertising->setScanResponseData(NimBLEAdvertisementData::) - //pService->addCharacteristic(m_pRxCharacteristic); - } - - BLECharacteristicCallbacksBase* getCaracteristicCallbacks(QueueHandle_t tcodeQueue) { - static BLELoveControlCallback callbacks(tcodeQueue); - return &callbacks; - } - void CommandCallback(const char* in) override { - // m_pTxCharacteristic->setValue(in); - // m_pTxCharacteristic->notify(); - }; -}; \ No newline at end of file diff --git a/ESP32/src/BLE/BLEHandlerTCode.h b/ESP32/src/BLE/BLEHandlerTCode.h deleted file mode 100644 index a3c7be6..0000000 --- a/ESP32/src/BLE/BLEHandlerTCode.h +++ /dev/null @@ -1,59 +0,0 @@ -/* MIT License - -Copyright (c) 2024 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include -#include -#include -//#include -#include -#include -#include "logging/LogHandler.h" - -#include "BLETCodeControlCallback.h" -#include "constants.h" -#include "TagHandler.h" -#include "BLEHandlerBase.h" - -class BLEHandlerTCode :public BLEHandlerBase { -public: - BLEHandlerTCode() : BLEHandlerBase("TCODE-ESP32", "ff1b451d-3070-4276-9c81-5dc5ea1043bc") { } -private: - const char* CHARACTERISTIC_UUID = "c5f1543e-338d-47a0-8525-01e3c621359d"; - BLECharacteristic* m_characteristic; - - void setupCharacteristics(BLEService *pService, BLEAdvertising *pAdvertising, QueueHandle_t tcodeQueue) override { - m_characteristic = new BLECharacteristic(CHARACTERISTIC_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE_NR); - m_characteristic->setCallbacks(getCaracteristicCallbacks(tcodeQueue)); - pService->addCharacteristic(m_characteristic); - } - - BLECharacteristicCallbacksBase* getCaracteristicCallbacks(QueueHandle_t tcodeQueue) { - static BLETCodeControlCallback callbacks(tcodeQueue); - return &callbacks; - } - void CommandCallback(const char* in) override { - //m_characteristic->setValue(in); - m_characteristic->notify((const uint8_t*)in, strlen(in)); - }; -}; \ No newline at end of file diff --git a/ESP32/src/BLE/BLELoveControlCallback.h b/ESP32/src/BLE/BLELoveControlCallback.h deleted file mode 100644 index 2ef6e57..0000000 --- a/ESP32/src/BLE/BLELoveControlCallback.h +++ /dev/null @@ -1,159 +0,0 @@ -/* MIT License - -Copyright (c) 2024 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include - -#include -#include -#include - -#include "logging/LogHandler.h" - -#include "BLECharacteristicCallbacksBase.h" -#include "TagHandler.h" -#include "constants.h" - -class BLELoveControlCallback: public BLECharacteristicCallbacksBase -{ -public: - BLELoveControlCallback(QueueHandle_t tcodeQueue): m_TCodeQueue(tcodeQueue) { - LogHandler::debug(TagHandler::BLEHandler, "Setting up BLE love Characteristic Callbacks"); - - SettingsFactory::getInstance()->getValue(BLE_LOVE_DEVICE_TYPE, m_bleLoveDeviceType); - } - // At some point this signature will change because its in master so if Bluetooth breaks, check the source class signature. - #ifdef NIMBLE_LATEST - void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override { - #else - void onWrite(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc) override { - #endif - //assert(pCharacteristic == pRxCharacteristic); - int len = pCharacteristic->getLength(); - if (len) { - std::string rxValue = pCharacteristic->getValue(); - //LogHandler::verbose(TagHandler::BLEHandler, "*********"); - LogHandler::verbose(TagHandler::BLEHandler, "Received Value: %s", rxValue.c_str()); - if(m_bleLoveDeviceType == BLELoveDeviceType::EDGE) { - executeEdgeDevice(rxValue); - } - } - - }; - - #ifdef NIMBLE_LATEST - void onRead(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override { - #else - void onRead(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc) override { - #endif - Serial.print(pCharacteristic->getUUID().toString().c_str()); - Serial.print(": onRead(), value: "); - Serial.println(pCharacteristic->getValue().c_str()); - }; - void onNotify(NimBLECharacteristic* pCharacteristic) { - Serial.println("Sending notification to clients"); - }; - - void setTX(BLECharacteristic* pTxCharacteristic) { - m_pTxCharacteristic = pTxCharacteristic; - } - -private: - char tcodeBuffer[MAX_COMMAND]; - QueueHandle_t m_TCodeQueue; - BLELoveDeviceType m_bleLoveDeviceType; - BLECharacteristic* m_pTxCharacteristic = 0; - - void buildTCode(const char* channel, const int &loveValue, const u_int16_t &interval, char* buf) { - sprintf(buf, "%s%04ldI%u\n", channel, map(loveValue, 0,20,0,9999), interval); - } - void buildTCode(const char* channel, const int &loveValue, char* buf) { - sprintf(buf, "%s%04ld\n", channel, map(loveValue, 0,20,0,9999)); - } - void buildTCodeSpeed(const char* channel, const int &loveValue, const u_int16_t &speed, char* buf) { - sprintf(buf, "%s%04ldS%u\n", channel, map(loveValue, 0,20,0,9999), speed); - } - -void executeEdgeDevice(std::string rxValue) { - // Serial.println(); - // Serial.println("*********"); - if(m_pTxCharacteristic) - { - static uint8_t messageBuf[64]; - if (rxValue == "DeviceType;") { - Serial.println("$Responding to Device Enquiry"); - //memmove(messageBuf, "P:37:FFFFFFFFFFFF;", 18); // Edge - memmove(messageBuf, "H:11:0082059AD3BD;", 18); //H solace EA gravity 9AD3BD - //memmove(messageBuf, "C:11:0082059AD3BD;", 18); // C Nora - // memmove(messageBuf, "W:11:0082059AD3BD;", 18); //W: -domi - //memmove(messageBuf, "H:11:FFFFFFFFFFFF;", 18); //H solace EA gravity 9AD3BD - // CONFIGURATION: ^ Use a BLE address of the Lovense device you're cloning. - m_pTxCharacteristic->setValue(messageBuf, 18); - m_pTxCharacteristic->notify(); - } else if (rxValue == "Battery;") { - memmove(messageBuf, "69;", 3); - m_pTxCharacteristic->setValue(messageBuf, 3); - m_pTxCharacteristic->notify(); - } else if (rxValue == "PowerOff;") { - memmove(messageBuf, "OK;", 3); - m_pTxCharacteristic->setValue(messageBuf, 3); - m_pTxCharacteristic->notify(); - } else if (rxValue == "RotateChange;") { - memmove(messageBuf, "OK;", 3); - m_pTxCharacteristic->setValue(messageBuf, 3); - m_pTxCharacteristic->notify(); - } else if (rxValue.rfind("Status:", 0) == 0) { - memmove(messageBuf, "2;", 2); - m_pTxCharacteristic->setValue(messageBuf, 3); - m_pTxCharacteristic->notify(); - } else if (rxValue.rfind("Vibrate:", 0) == 0) { - int vibration = std::atoi(rxValue.substr(8).c_str()); - memmove(messageBuf, "OK;", 3); - m_pTxCharacteristic->setValue(messageBuf, 3); - m_pTxCharacteristic->notify(); - buildTCode("V0", vibration, tcodeBuffer); - } else if (rxValue.rfind("Vibrate1:", 0) == 0) { - int vibration = std::atoi(rxValue.substr(9).c_str()); - memmove(messageBuf, "OK;", 3); - m_pTxCharacteristic->setValue(messageBuf, 3); - m_pTxCharacteristic->notify(); - buildTCode("V0", vibration, tcodeBuffer); - } else if (rxValue.rfind("Vibrate2:", 0) == 0) { - int vibration = std::atoi(rxValue.substr(9).c_str()); - memmove(messageBuf, "OK;", 3); - m_pTxCharacteristic->setValue(messageBuf, 3); - m_pTxCharacteristic->notify(); - buildTCode("V1", vibration, tcodeBuffer); - } else { - LogHandler::warning(TagHandler::BLEHandler, "$Unknown request"); - memmove(messageBuf, "ERR;", 4); - m_pTxCharacteristic->setValue(messageBuf, 4); - m_pTxCharacteristic->notify(); - } - } - LogHandler::verbose(TagHandler::BLEHandler, "Receive love tcode: %s", tcodeBuffer); - if(xQueueSend(m_TCodeQueue, tcodeBuffer, 0) != pdTRUE) { - LogHandler::error(TagHandler::BLEHandler, "Failed to write to queue"); - } - } -}; \ No newline at end of file diff --git a/ESP32/src/BLE/BLEServerCallbacksBase.h b/ESP32/src/BLE/BLEServerCallbacksBase.h deleted file mode 100644 index c0bd8f1..0000000 --- a/ESP32/src/BLE/BLEServerCallbacksBase.h +++ /dev/null @@ -1,66 +0,0 @@ -/* MIT License - -Copyright (c) 2024 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include - -#include -#include -#include - -#include "logging/LogHandler.h" - -#include "TagHandler.h" -#include "constants.h" - - -class ServerCallbacks: public NimBLEServerCallbacks { -public: - bool isConnected() { - return m_connected; - } -private: - #ifdef NIMBLE_LATEST - void onConnect(BLEServer* pServer, NimBLEConnInfo& connInfo) override { - LogHandler::info(TagHandler::BLEHandler, "A client has connected via BLE: %s", - connInfo.getAddress().toString().c_str() - ); - #else - void onConnect(BLEServer* pServer, ble_gap_conn_desc* desc) override { - LogHandler::info(TagHandler::BLEHandler, "A client has connected via BLE"); - #endif - m_connected = true; - }; - #ifdef NIMBLE_LATEST - void onDisconnect(BLEServer* pServer, NimBLEConnInfo& connInfo, int reason) override { - LogHandler::info(TagHandler::BLEHandler, "A client has disconnected from BLE: %s", - connInfo.getAddress().toString().c_str() - ); - #else - void onDisconnect(BLEServer* pServer, ble_gap_conn_desc* desc) override { - LogHandler::info(TagHandler::BLEHandler, "A client has disconnected from BLE"); - #endif - m_connected = false; - } - bool m_connected = false; -}; \ No newline at end of file diff --git a/ESP32/src/BLE/BLETCodeControlCallback.h b/ESP32/src/BLE/BLETCodeControlCallback.h deleted file mode 100644 index c802a87..0000000 --- a/ESP32/src/BLE/BLETCodeControlCallback.h +++ /dev/null @@ -1,74 +0,0 @@ -/* MIT License - -Copyright (c) 2024 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include - -#include -#include -#include - -#include "logging/LogHandler.h" - -#include "BLECharacteristicCallbacksBase.h" -#include "TagHandler.h" -#include "constants.h" - -class BLETCodeControlCallback: public BLECharacteristicCallbacksBase -{ -public: - BLETCodeControlCallback(QueueHandle_t tcodeQueue): m_TCodeQueue(tcodeQueue){ } - // const char* NAME = "TCODE-ESP32"; - // const char* SERVICE_UUID = "ff1b451d-3070-4276-9c81-5dc5ea1043bc"; - // At some point this signature will change because its in master so if Bluetooth breaks, check the source class signature. - #ifdef NIMBLE_LATEST - void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override { - #else - void onWrite(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc) override { - #endif - - size_t len = pCharacteristic->getLength(); - if(len) { - NimBLEAttValue rxValue = pCharacteristic->getValue(); - LogHandler::verbose(TagHandler::BLEHandler, "Recieve tcode: %s", rxValue.c_str()); - if(xQueueSend(m_TCodeQueue, rxValue.c_str(), 0) != pdTRUE) { - LogHandler::error(TagHandler::BLEHandler, "Failed to write to queue"); - } - } - }; - - #ifdef NIMBLE_LATEST - void onRead(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override { - #else - void onRead(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc) override { - #endif - Serial.print(pCharacteristic->getUUID().toString().c_str()); - Serial.print(": onRead(), value: "); - Serial.println(pCharacteristic->getValue().c_str()); - }; - void onNotify(NimBLECharacteristic* pCharacteristic) { - Serial.println("Sending notification to clients"); - }; -private: - QueueHandle_t m_TCodeQueue; -}; \ No newline at end of file diff --git a/ESP32/src/BatteryHandler.h b/ESP32/src/BatteryHandler.h deleted file mode 100644 index 116d4bd..0000000 --- a/ESP32/src/BatteryHandler.h +++ /dev/null @@ -1,211 +0,0 @@ -/* MIT License - -Copyright (c) 2024 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - - -#include -#include -#include -#include "SettingsHandler.h" -#include "utils.h" -// #include "driver/adc.h" -// #include "esp_adc_cal.h" -// #include "LogHandler.h" -#include "TagHandler.h" - -using BATTERY_STATE_FUNCTION_PTR_T = void (*)(float capacityRemainingPercentage, float capacityRemaining, float voltage, float temperature); -/** This class is setup for a specific board with an LTC2944 gas guage - * The module used is CJMCU-294. -*/ -class BatteryHandler : public Task { -public: - static bool connected() { - return m_battery_connected; - } - - static void startLoop(void * parameter) { - ((BatteryHandler*)parameter)->loop(); - } - - /** Maximum value is 22000 mAh */ - static void setBatteryCapacity(int maxCapacity) { - if(connected()) { - gauge.setBatteryCapacity(maxCapacity); - m_maxCapacity = maxCapacity; - LogHandler::info(_TAG, "Battery capacity max set to %u mAh", maxCapacity); - } - } - - /** Sets accumulated charge registers to the maximum value */ - static void setBatteryToFull() { - if(connected()) { - SettingsFactory* settingsFactory = SettingsFactory::getInstance(); - setBatteryCapacity(settingsFactory->getBatteryCapacityMax()); - gauge.setBatteryToFull(); - LogHandler::info(_TAG, "Battery capacity set to full"); - } - } - - bool setup() { - LogHandler::info(_TAG, "Setup battery monitor"); - if(!SettingsHandler::waitForI2CDevices(LTC2944_ADDRESS)) { - return false; - } - Wire.begin(); - long timeout = millis() + 10000; - LogHandler::info(_TAG, "Connecting to monitor"); - while (!gauge.begin()) { - Serial.print("."); - this->wait(1000); - if(millis() > timeout) { - LogHandler::error(_TAG, "Detecting battery gauge (LTC2944) timed out. Exit."); - return false; - } - } - LogHandler::info(_TAG, "Monitor connected!"); - m_battery_connected = true; - gauge.setADCMode(ADC_MODE_SLEEP); // In sleep mode, voltage and temperature measurements will only take place when requested - SettingsFactory* settingsFactory = SettingsFactory::getInstance(); - setBatteryCapacity(settingsFactory->getBatteryCapacityMax()); - gauge.startMeasurement(); - LogHandler::debug(_TAG, "Complete"); - return true; - } - - void setMessageCallback(BATTERY_STATE_FUNCTION_PTR_T f) { - if (f == nullptr) { - message_callback = 0; - } else { - message_callback = f; - } - } - - void loop() { - if(m_battery_connected && millis() >= lastTick) { - LogHandler::verbose(_TAG, "Enter getBatteryLevel"); - lastTick = millis() + tick; - m_batteryCapacity = gauge.getRemainingCapacity(); - m_batteryVoltage = gauge.getVoltage(); - m_batteryTemp = gauge.getTemperature(); - - LogHandler::verbose(_TAG, "Battery remaining capacity: %f", m_batteryCapacity); - LogHandler::verbose(_TAG, "Battery voltage: %f", m_batteryVoltage); - LogHandler::verbose(_TAG, "Battery temp: %f", m_batteryTemp); - float capacityRemainingPercentage = 0; - if(m_batteryCapacity > 0) - capacityRemainingPercentage = m_batteryCapacity / m_maxCapacity * 100; - - if(message_callback) - message_callback(capacityRemainingPercentage, m_batteryCapacity, m_batteryVoltage, m_batteryTemp); - } - this->sleep(5000); // 5000ms - } - - //void setup() { - // Method to get via ADC I had issues with. - // if(SettingsHandler::getBatteryLevelEnabled()) { - // LogHandler::info("batteryHandler", "Setting up voltage on pin: %ld", SettingsHandler::getBattery_Voltage_PIN()); - // adc1_config_width(ADC_WIDTH_12Bit); - // m_adc1Channel = gpioToADC1(SettingsHandler::getBattery_Voltage_PIN()); - // if(m_adc1Channel == adc1_channel_t::ADC1_CHANNEL_MAX) { - // LogHandler::error(_TAG, "Invalid Battery voltage pin: %ld", SettingsHandler::getBattery_Voltage_PIN()); - // } - // if(m_adc1Channel != adc1_channel_t::ADC1_CHANNEL_MAX) { - // LogHandler::info("batteryHandler", "ADC channel: %ld", (int)m_adc1Channel); - // adc1_config_channel_atten(m_adc1Channel, ADC_ATTEN_DB_11); - - // LogHandler::debug("batteryHandler", "Calibrating battery voltage"); - // m_val_type = esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 0, &m_adc1_chars); - // //Check type of calibration value used to characterize ADC - // LogHandler::debug("batteryHandler", "Complete: "); - // if (m_val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { - // LogHandler::debug("batteryHandler", "eFuse Vref"); - // } else if (m_val_type == ESP_ADC_CAL_VAL_EFUSE_TP) { - // LogHandler::debug("batteryHandler", "Two Point"); - // } else { - // LogHandler::debug("batteryHandler", "Default"); - // } - // } - // m_battery_connected = true; - // } - //} - // Method to get via ADC I had issues with. - //void getBatteryLevel() { - // if(m_adc1Channel == adc1_channel_t::ADC1_CHANNEL_MAX) { - // return; - // } - // https://esp32tutorials.com/esp32-adc-esp-idf/ - //int adc_value = adc1_get_raw(m_adc1Channel); - // uint16_t raw = analogRead(SettingsHandler::getBattery_Voltage_PIN()); // analogRead is less accurate than adc1_get_raw - // m_batteryVoltage = (raw * 3.3 ) / 4095; - // uint32_t mV; - // if (m_val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { - // //LogHandler::debug("batteryHandler", "eFuse Vref"); - // mV = esp_adc_cal_raw_to_voltage(adc_value, &m_adc1_chars); - // } else if (m_val_type == ESP_ADC_CAL_VAL_EFUSE_TP) { - // //LogHandler::debug("batteryHandler", "Two Point"); - // mV = esp_adc_cal_raw_to_voltage(adc_value, &m_adc1_chars); - // } else { - // //LogHandler::debug("batteryHandler", "Default"); - // mV = ((adc_value * 3.3 ) / 4095) * 1000; - // } - // if(mV > 0) { - // m_batteryVoltage = mV / 1000.0; - // } else { - // m_batteryVoltage = 0.0; - // } - - //https://www.youtube.com/watch?v=qKUrXwkr3cc - //https://github.com/G6EJD/LiPo_Battery_Capacity_Estimator/blob/master/ReadBatteryCapacity_LIPO.ino - //author David Bird - // uint8_t percentage = 2808.3808 * pow(m_batteryVoltage, 4) - 43560.9157 * pow(m_batteryVoltage, 3) + 252848.5888 * pow(m_batteryVoltage, 2) - 650767.4615 * m_batteryVoltage + 626532.5703; - - // Serial.print("mV: "); - // Serial.println(mV); - // Serial.print("areadValue: "); - // Serial.println(areadValue); - // Serial.print("m_batteryVoltage calc: "); - // Serial.println((adc_value * 3.3 ) / 4095); - //} -private: - static const char* _TAG; - BATTERY_STATE_FUNCTION_PTR_T message_callback = 0; - static LTC2944 gauge; - unsigned long lastTick = 0; - int tick = 5000; - bool _isRunning; - float m_batteryVoltage = 0.0; - float m_batteryCapacity = 0.0; - float m_batteryTemp = 0.0; - - static int m_maxCapacity; - static bool m_battery_connected; - // esp_adc_cal_characteristics_t m_adc1_chars; - // esp_adc_cal_value_t m_val_type; - // adc1_channel_t m_adc1Channel; -}; - -const char* BatteryHandler::_TAG = TagHandler::BatteryHandler; -LTC2944 BatteryHandler::gauge; -bool BatteryHandler::m_battery_connected = false; -int BatteryHandler::m_maxCapacity; \ No newline at end of file diff --git a/ESP32/src/BluetoothHandler.h b/ESP32/src/BluetoothHandler.h deleted file mode 100644 index e2e0750..0000000 --- a/ESP32/src/BluetoothHandler.h +++ /dev/null @@ -1,100 +0,0 @@ -/* MIT License - -Copyright (c) 2024 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - - -#pragma once - -#include -//#include -#include "SettingsHandler.h" -#include "logging/LogHandler.h" -#include "TagHandler.h" -#include "esp_coexist.h" -#include "esp_bt.h" - -#if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED) -#error Bluetooth is not enabled! Please run `make menuconfig` to and enable it -#endif - -class BluetoothHandler -{ - public: - bool setup() { - LogHandler::info(_TAG, "Starting bluetooth serial: %s", "TCodeESP32"); - if(!SerialBT.begin("TCodeESP32")) - { - LogHandler::error(_TAG, "An error occurred initializing Bluetooth serial"); - return false; - } - LogHandler::info(_TAG, "Bluetooth started"); - _isConnected = true; - return true; - } - - byte read() { - return SerialBT.read(); - } - - void stop() { - _isConnected = false; - SerialBT.disconnect(); - SerialBT.end(); - } - - String readStringUntil(char terminator) { - return SerialBT.readStringUntil(terminator); - } - - void CommandCallback(const String& in){ //This overwrites the callback for message return - if(_isConnected) - SerialBT.print(in); - } - - void write(uint8_t message) { - // Serial.print("BTWrite: "); - // Serial.println(message); - SerialBT.write(message); - } - void println(const char* message) { - // Serial.print("BTWrite: "); - // Serial.println(message); - SerialBT.println(message); - } - - int available() { - return SerialBT.available(); - } - - bool isConnected() - { - return _isConnected; - } - - static void disable() { - esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); - } - - private: - const char* _TAG = TagHandler::BluetoothHandler; - bool _isConnected = false; - BluetoothSerial SerialBT; -}; \ No newline at end of file diff --git a/ESP32/src/ButtonHandler.hpp b/ESP32/src/ButtonHandler.hpp deleted file mode 100644 index 12e94e7..0000000 --- a/ESP32/src/ButtonHandler.hpp +++ /dev/null @@ -1,213 +0,0 @@ -/* MIT License - -Copyright (c) 2024 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - - -#pragma once - -#include -// #include "LogHandler.h" -#include "TagHandler.h" -#include "SettingsHandler.h" -#include "constants.h" -#include "struct/buttonSet.h" -#include "settingsFactory.h" - -#define TASK_WAIT 10/portTICK_PERIOD_MS - - -class ButtonHandler { - - -public: - ButtonHandler() { - buttonIndexMap[0] = 900; - buttonIndexMap[1] = 1200; - buttonIndexMap[2] = 2000; - buttonIndexMap[3] = 4096; - } - - void init(uint16_t analogDebounce, const char bootButtonCommand[MAX_COMMAND], ButtonSet *buttonSets) { - if(m_initialized) { - return; - } - buttonAnalogDebounce = analogDebounce; - m_buttonQueue = xQueueCreate(MAX_BUTTON_SETS * MAX_BUTTONS, sizeof(struct ButtonModel*)); - if(m_buttonQueue == NULL) { - LogHandler::error(_TAG, "Error creating the debug queue"); - } - bool bootButtonEnabled = BOOT_BUTTON_ENABLED_DEFAULT; - SettingsFactory* settingsFactory = SettingsFactory::getInstance(); - settingsFactory->getValue(BOOT_BUTTON_ENABLED, bootButtonEnabled); - if(bootButtonEnabled) - initBootbutton(bootButtonCommand); - bool buttonSetsEnabled = BUTTON_SETS_ENABLED_DEFAULT; - settingsFactory->getValue(BUTTON_SETS_ENABLED, buttonSetsEnabled); - if(buttonSetsEnabled) - initAnalogButtons(buttonSets); - m_initialized = true; - } - - void initAnalogButtons(ButtonSet *buttonSets) { - for(int i = 0; i < MAX_BUTTON_SETS; i++) { - m_buttonSets[i] = ButtonSet(buttonSets[i]); - auto buttonSet = m_buttonSets[i]; - auto buttonPin = buttonSet.pin; - if(buttonPin > -1) { - //LogHandler::debug(_TAG, "initAnalogButtons '%s' pin: %d, buton 0 name: %s", buttonSet.name, buttonPin, buttonSet.buttons[0].name); - auto pin = digitalPinToInterrupt(buttonPin);// Check valid pin iterrupt - if(pin == -1) { - LogHandler::error(_TAG, "Invalid interupt button pin: %d", buttonPin); - continue; - }; - xTaskCreate(&analog_button_task, "buttonTask", (uint16_t)configMINIMAL_STACK_SIZE, this, 5, &buttonTask); - return; - // LogHandler::debug(_TAG, "Checking button set: %s, pin: %ld", buttonSet.name, buttonPin); - // if(buttonSet.pullMode == gpio_pull_mode_t::GPIO_PULLDOWN_ONLY) { - // LogHandler::info(_TAG, "Setting up pull down button set %u on pin: %ld", i +1, buttonPin); - // pinMode(buttonPin, INPUT_PULLDOWN); - // attachInterrupt(buttonPin, buttonInterrupt, RISING); - // } else { - // LogHandler::info(_TAG, "Setting up pull up button set %u on pin: %ld", i +1, buttonPin); - // pinMode(buttonPin, INPUT_PULLUP); - // attachInterrupt(buttonPin, buttonInterrupt, FALLING); - // } - } - } - } - - void initBootbutton(const char command[MAX_COMMAND]) { - pinMode(0, INPUT_PULLDOWN); - attachInterrupt(digitalPinToInterrupt(0), bootButtonInterrupt, CHANGE); - snprintf(bootButtonModel.name, sizeof(bootButtonModel.name), "Boot button"); - updateBootButtonCommand(command); - } - - void updateBootButtonCommand(const char bootButtonCommand[MAX_COMMAND]) { - xSemaphoreTake(xMutex, portMAX_DELAY); - if(strlen(bootButtonCommand) > 0) { - strncpy(bootButtonModel.command, bootButtonCommand, sizeof(bootButtonModel.command)); - } else { - bootButtonModel.command[0] = {0}; - } - xSemaphoreGive(xMutex); - } - - void updateAnalogButtonCommands(ButtonSet buttonSets[MAX_BUTTON_SETS]) { - xSemaphoreTake(xMutex, portMAX_DELAY); - for(int i = 0; i < MAX_BUTTON_SETS; i++) { - m_buttonSets[i] = ButtonSet(buttonSets[i]); - } - xSemaphoreGive(xMutex); - } - - void updateAnalogDebounce(uint16_t debounce) { - buttonAnalogDebounce = debounce; - } - - void read(ButtonModel* &buf) { - void* recieve; - if(xQueueReceive(m_buttonQueue, &(recieve), 0)) { - buf = (ButtonModel*)recieve; - LogHandler::debug(_TAG, "Recieve command in button queue: %s: %s", buf->name, buf->command); - } else { - buf = 0; - } - } - - static void bootButtonInterrupt() { - BaseType_t pxHigherPriorityTaskWoken = pdFALSE; - xSemaphoreTakeFromISR(xMutex, &pxHigherPriorityTaskWoken); - digitalRead(digitalPinToInterrupt(0)) == HIGH ? bootButtonModel.press() : bootButtonModel.release(); - struct ButtonModel *pxMessage = &(bootButtonModel);// Why did I have to do all this!? - xQueueSendFromISR(m_buttonQueue, ( void * ) &pxMessage, &pxHigherPriorityTaskWoken); - xSemaphoreGiveFromISR(xMutex, &pxHigherPriorityTaskWoken); - if( pxHigherPriorityTaskWoken ) - { - portYIELD_FROM_ISR(); - } - } - -private: - static const char* _TAG; - bool m_initialized = false; - - - static long m_lastDebounce; - static volatile bool m_enterInterupt; - static SemaphoreHandle_t xMutex; - static uint16_t buttonIndexMap[MAX_BUTTONS]; - static ButtonSet m_buttonSets[MAX_BUTTON_SETS]; - static QueueHandle_t m_buttonQueue; - static uint8_t buttonAnalogTolorance; - static uint16_t buttonAnalogDebounce; - static ButtonModel bootButtonModel; - - TaskHandle_t buttonTask = NULL; - static void analog_button_task(void* arg) { - for(;;) { - if(millis() - m_lastDebounce > buttonAnalogDebounce) { - m_lastDebounce = millis(); - readButtons(); - } - vTaskDelay(TASK_WAIT); - } - } - static bool readButtons() { - uint8_t index = 0; - for(int i = 0; i < MAX_BUTTON_SETS; i++) { - if(m_buttonSets[i].pin > -1) { - for(int j = 0; j < MAX_BUTTONS; j++) { - auto value = analogRead(m_buttonSets[i].pin); - auto index = m_buttonSets[i].buttons[j].index; - // LogHandler causes stack overflow. - //LogHandler::verbose(_TAG, "readButtons value: %ld, index: %ld, index value: %ld", value, index, buttonIndexMap[index]); - bool isPressedValue = value >= buttonIndexMap[index] - buttonAnalogTolorance && value <= buttonIndexMap[index] + buttonAnalogTolorance; - if(!m_buttonSets[i].buttons[j].isPressed() && isPressedValue) { - //LogHandler::debug(_TAG, "Button '%s' pressed: %u, set index: %u button index: %u", m_buttonSets[i].buttons[j].name, value, i, j); - // xSemaphoreTake(xMutex, portMAX_DELAY); - m_buttonSets[i].buttons[j].press(); - struct ButtonModel *pxMessage = &(m_buttonSets[i].buttons[j]);// Why did I have to do all this!? - xQueueSend(m_buttonQueue, ( void * ) &pxMessage, portMAX_DELAY); - // xSemaphoreGive(xMutex); - return true; - } else if(m_buttonSets[i].buttons[j].isPressed() && !isPressedValue) { - //LogHandler::debug(_TAG, "Button '%s' released", m_buttonSets[i].buttons[j].name); - m_buttonSets[i].buttons[j].release(); - struct ButtonModel *pxMessage = &(m_buttonSets[i].buttons[j]);// Why did I have to do all this!? - xQueueSend(m_buttonQueue, ( void * ) &pxMessage, portMAX_DELAY); - } - } - } - } - return false; - } -}; -const char* ButtonHandler::_TAG = TagHandler::ButtonHandler; -uint8_t ButtonHandler::buttonAnalogTolorance = 150; -uint16_t ButtonHandler::buttonAnalogDebounce = 150; -QueueHandle_t ButtonHandler::m_buttonQueue; -ButtonSet ButtonHandler::m_buttonSets[MAX_BUTTON_SETS]; -ButtonModel ButtonHandler::bootButtonModel; -uint16_t ButtonHandler::buttonIndexMap[MAX_BUTTONS]; -SemaphoreHandle_t ButtonHandler::xMutex = xSemaphoreCreateMutex(); -volatile bool ButtonHandler::m_enterInterupt = false; -long ButtonHandler::m_lastDebounce = millis(); \ No newline at end of file diff --git a/ESP32/src/DisplayHandler.h b/ESP32/src/DisplayHandler.h deleted file mode 100644 index aac6c16..0000000 --- a/ESP32/src/DisplayHandler.h +++ /dev/null @@ -1,635 +0,0 @@ -/* MIT License - -Copyright (c) 2024 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include -#include -#include "Adafruit_SSD1306_RSB.h" -#include "SettingsHandler.h" -// #include "LogHandler.h" -#if WIFI_TCODE -#include "WifiHandler.h" -#endif -#include -#if BUILD_TEMP -#include "TemperatureHandler.h" -#endif -#include "BatteryHandler.h" -#include "TagHandler.h" -#include "TaskHandler.h" -// #if ISAAC_NEWTONGUE_BUILD -// #include "animationFrames.h" -// #endif - -class DisplayHandler : public Task -{ -public: - - DisplayHandler() : - m_settingsFactory(SettingsFactory::getInstance()), - display( - m_settingsFactory->getDisplayScreenWidth(), - m_settingsFactory->getDisplayScreenHeight(), - //64, - &Wire, - -1, 100000UL, 100000UL) { } - - void setup(int I2CAddress, bool fanControlEnabled, int8_t rstPin = -1) - { - m_fanControlEnabled = fanControlEnabled; - - LogHandler::info(_TAG, "Setting up display"); - int tries = 0; - SettingsHandler::waitForI2CDevices(I2CAddress); - if(SettingsHandler::systemI2CAddresses.size() == 0) { - return; - } - - //Wire.begin(); - //Wire.setClock(100000UL); - if(!I2CAddress) { - LogHandler::info(_TAG, "No address to connect to"); - return; - } else if(!connectDisplay(I2CAddress, rstPin)) { - LogHandler::info(_TAG, "Could not connect address"); - return; - } - this->wait(2000); - if(displayConnected) - { - //display.setFont(Adafruit5x7); - display.clearDisplay(); - display.setTextColor(WHITE); - display.setTextSize(1); - } else - LogHandler::error(_TAG, "Display is not connected"); - LogHandler::info(_TAG, "Setting up display finished"); - } - - void setLocalIPAddress(IPAddress ipAddress) - { - _ipAddress = ipAddress; - } - - void setSleeveTemp(float temp) { - LogHandler::verbose(_TAG, "setSleeveTemp: %f", temp); - m_sleeveTemp = temp; - memset(m_sleeveTempString,'\0',sizeof(m_sleeveTempString)); - if(temp < 0) { - strcpy(m_sleeveTempString, "XX.XX\0"); - } else { - dtostrf(temp, sizeof(m_sleeveTempString) - 1, 2, m_sleeveTempString); - } - } - void setInternalTemp(float temp) { - LogHandler::verbose(_TAG, "setInternalTemp: %f", temp); - m_internalTemp = temp; - memset(m_internalTempString,'\0',sizeof(m_internalTempString)); - if(temp < 0) { - strcpy(m_internalTempString, "XX.XX\0"); - } else { - dtostrf(temp, sizeof(m_internalTempString) - 1, 2, m_internalTempString); - } - } - void setHeateState(const char* state) { - LogHandler::verbose(_TAG, "setHeateState: %s", state); - m_HeatState = String(state); - } - void setHeateStateShort(const char* state) { - LogHandler::verbose(_TAG, "setHeateStateShort: %s", state); - m_HeatStateShort = String(state); - } - void setFanState(const char* state) { - LogHandler::verbose(_TAG, "setFanState: %s", state); - m_fanState = String(state); - } - void setBatteryInformation(float capacityRemainingPercentage, float voltage, float temperature) { - LogHandler::verbose(_TAG, "setBatteryCapacityRemainingPercentage: %f", capacityRemainingPercentage); - m_batteryCapacityRemainingPercentage = capacityRemainingPercentage; - LogHandler::verbose(_TAG, "setBatteryVoltage: %f", voltage); - m_batteryVoltage = voltage; - LogHandler::verbose(_TAG, "setBatteryTemperature: %f", temperature); - m_batteryTemperature = temperature; - } - - void clearDisplay() - { - LogHandler::verbose(_TAG, "clear display"); - if(displayConnected) { - display.clearDisplay(); - } - } - - bool isConnected() { - return displayConnected; - } - - bool isRunning() - { - return _isRunning; - } - void stopRunning() - { - LogHandler::verbose(_TAG, "stopRunning"); - _isRunning = false; - } - - void loop() - { - - if(!m_animationPlaying && displayConnected && millis() >= lastUpdate + nextUpdate) { - LogHandler::verbose(_TAG, "Enter display loop"); - lastUpdate = millis(); - clearDisplay(); - setTextSize(1); - int headerPadding = is32() ? 0 : 3; - // Serial.print("Display Core: "); - // Serial.println(xPortGetCoreID()); - -#if WIFI_TCODE - if(WifiHandler::isConnected()) { - LogHandler::verbose(_TAG, "Enter wifi connected"); - startLine(headerPadding); - display.print(_ipAddress); - - drawBatteryLevel(); - - // Draw Wifi signal bars - int barHeight = is32() ? 8 : 10; - int bars; - // int bars = map(RSSI,-80,-44,1,6); // this method doesn't refelct the Bars well - // simple if then to set the number of bars - int8_t RSSI = WifiHandler::getRSSI(); - if (RSSI > -55) { - bars = 5; - } else if (RSSI < -55 && RSSI > -65) { - bars = 4; - } else if (RSSI < -65 && RSSI > -70) { - bars = 3; - } else if (RSSI < -70 && RSSI > -78) { - bars = 2; - } else if (RSSI < -78 && RSSI > -82) { - bars = 1; - } else { - bars = 0; - } - for (int b=0; b <= bars; b++) { - display.fillRect((m_settingsFactory->getDisplayScreenWidth() - 17) + (b*3), barHeight - (b*2),2,b*2,WHITE); - } - - newLine(headerPadding); - if(m_settingsFactory->getVersionDisplayed()) { - LogHandler::verbose(_TAG, "Enter versionDisplayed"); - left(m_settingsFactory->getTcodeVersionString()); - right(FIRMWARE_VERSION_NAME); - newLine(); - } - - } else if(WifiHandler::apMode()) { - LogHandler::verbose(_TAG, "Enter apMode"); - startLine(headerPadding); - left("AP:"); - left(m_settingsFactory->getAPModeIP(), 4); - drawBatteryLevel(); - newLine(headerPadding); - if(!is32()) { - left("SSID:"); - left(m_settingsFactory->getAPModeSSID(), 6); - newLine(); - } - if((is32() && m_settingsFactory->getVersionDisplayed() && !m_settingsFactory->getSleeveTempDisplayed() && !m_settingsFactory->getInternalTempDisplayed()) - || m_settingsFactory->getVersionDisplayed()) { - left(m_settingsFactory->getTcodeVersionString()); - right(FIRMWARE_VERSION_NAME); - newLine(); - } else if(is32()) { - left("SSID:"); - left(m_settingsFactory->getAPModeSSID(), 6); - newLine(); - } - } else { - LogHandler::verbose(_TAG, "Enter Wifi error"); - display.print("Wifi error"); - drawBatteryLevel(); - } -#endif -#if BUILD_TEMP - if(m_settingsFactory->getSleeveTempDisplayed() || m_settingsFactory->getInternalTempDisplayed()) { - is32() ? draw32Temp() : draw64Temp(); - } -#endif - - display.display(); - } - this->sleep(200); - } - - void println(String value) - { - if(displayConnected && !m_animationPlaying) - { - display.println(value); - display.display(); - //Serial.println(value); - //display.startvertscroll(0x04, 0x1F, true); - } - } - - void println(int value) - { - if(displayConnected && !m_animationPlaying) - { - display.println(value); - display.display(); - } - } - - // static void startAnimationDontPanic(void* displayHandlerRef) - // { - // ((DisplayHandler*)displayHandlerRef)->playBootAnimationDontPanic(); - // } - - // void playBootAnimationDontPanic() - // { -// #if ISAAC_NEWTONGUE_BUILD -// if(displayConnected) -// { -// display.clearDisplay(); -// m_animationPlaying = true; -// int endTime = millis() + m_animationMilliSeconds; -// int currentFrameIndex = 0; -// while(millis() < endTime) -// { -// display.drawBitmap(0, 0, dontPanicAnimationFrames[currentFrameIndex], m_settingsFactory->getDisplayScreenWidth(), m_settingsFactory->getDisplayScreenHeight(), 1); -// display.display(); -// currentFrameIndex++; -// if(currentFrameIndex == dontPanicAnimationFramesCount) -// currentFrameIndex = 0; -// vTaskDelay(30); -// display.clearDisplay(); -// } -// m_animationPlaying = false; -// } -// #endif - // vTaskDelete( NULL ); - // } - -private: - const char* _TAG = TagHandler::DisplayHandler; - SettingsFactory* m_settingsFactory; - bool m_fanControlEnabled; - IPAddress _ipAddress; - bool displayConnected = false; - int lastUpdate = 0; - const int nextUpdate = 1000; - bool _isRunning = false; - - int currentLine = 0; - int lineCount = 0; - int lineHeight = 10; - int charWidth = 6; - - float m_internalTemp = -127.0f; - float m_sleeveTemp = -127.0f; - char m_internalTempString[7] = "XX.XX"; - char m_sleeveTempString[7] = "XX.XX"; - String m_fanState = "Unknown"; - String m_HeatState = "Unknown"; - String m_HeatStateShort = "U"; - float m_batteryVoltage = 0.0; - int m_batteryCapacityRemainingPercentage; - float m_batteryTemperature; - - Adafruit_SSD1306_RSB display; - bool m_animationPlaying = false; - int m_animationMilliSeconds = 10000; - - // Text size 1 is 6x8, 2 is 12x16, 3 is 18x24, etc - void setTextSize(uint8_t size) { - if(size >= 1) { - charWidth = 6 * size; - lineHeight = getLineHeight(size); - display.setTextSize(size); - } - } - void newLine(int additionalPixels = 0, int newLineTextSize = 0) { - int newLine = currentLine + (lineHeight + additionalPixels); - if(newLineTextSize > 0) - setTextSize(newLineTextSize); - if(newLine > m_settingsFactory->getDisplayScreenHeight() - lineHeight) { - LogHandler::warning(_TAG, "End of the display reached when newLine! Current: %i, New: %i, Max: %i", currentLine, newLine, m_settingsFactory->getDisplayScreenHeight() - lineHeight); - } - currentLine = newLine; - display.setCursor(0, currentLine); - //clearCurrentLine(); - } - int getLineHeight(uint8_t size) { - int margin = 2; - return 8 * size + margin; - } - void startLine(int additionalPixels = 0) { - currentLine = (0 + additionalPixels); - display.setCursor(0, currentLine); - } - bool hasNextLine(int newLineTextSize = 1) { - - return currentLine + getLineHeight(newLineTextSize) <= m_settingsFactory->getDisplayScreenHeight() - getLineHeight(newLineTextSize); - } - void space(int count = 1) { - display.setCursor(display.getCursorX() + (count * charWidth), currentLine); - } - void right(const char* text, int margin = 0) { - display.setCursor((m_settingsFactory->getDisplayScreenWidth() - strlen(text) * charWidth) - margin * charWidth, currentLine); - display.print(text); - } - void left(const char* text, int margin = 0) { - display.setCursor(margin * charWidth, currentLine); - display.print(text); - } - void center(const char* text) { - display.setCursor((m_settingsFactory->getDisplayScreenWidth() - (strlen(text) * charWidth)) / 2, currentLine); - display.print(text); - } - void clearCurrentLine() { - display.fillRect(0, currentLine, m_settingsFactory->getDisplayScreenWidth(), 10, BLACK); - } - bool is32() { - return m_settingsFactory->getDisplayScreenHeight() == 32; - } - - void draw64Temp() { - if(m_settingsFactory->getSleeveTempDisplayed() && !m_settingsFactory->getInternalTempDisplayed()) { - LogHandler::verbose(_TAG, "Enter draw64Temp sleeveTempDisplayed"); - if(m_settingsFactory->getVersionDisplayed()) { - LogHandler::verbose(_TAG, "versionDisplayed"); - char buf[17]; - getTempString("Sleeve: ", m_sleeveTempString, buf, sizeof(buf)); - left(buf); - newLine(); - left(m_HeatState.c_str(), 3); - } else { - setTextSize(3); - char buf[9]; - getTempString("", m_sleeveTempString, buf, sizeof(buf)); - left(buf); - newLine(0, 1); - center(m_HeatState.c_str()); - } - } else if(!m_settingsFactory->getSleeveTempDisplayed() && m_settingsFactory->getInternalTempDisplayed()) { - LogHandler::verbose(_TAG, "Enter draw64Temp internalTempDisplayed"); - if(m_settingsFactory->getVersionDisplayed()) { - LogHandler::verbose(_TAG, "versionDisplayed"); - char buf[19]; - getTempString("Internal: ", m_internalTempString, buf, sizeof(buf)); - left(buf); - if(m_fanControlEnabled) { - newLine(); - left(m_fanState.c_str(), 3); - } - } else { - setTextSize(3); - char buf[9]; - getTempString("", m_internalTempString, buf, sizeof(buf)); - center(buf); - newLine(0, 1); - if(m_fanControlEnabled) { - center(m_fanState.c_str()); - } - } - } else if(m_settingsFactory->getSleeveTempDisplayed() && m_settingsFactory->getInternalTempDisplayed()) { - LogHandler::verbose(_TAG, "Enter draw64Temp sleeveTempDisplayed && internalTempDisplayed"); - left("Sleeve"); - right("Internal"); - - newLine(); - - char buf[9]; - getTempString("", m_sleeveTempString, buf, sizeof(buf)); - left(buf); - if(m_settingsFactory->getVersionDisplayed()) { - display.print("("+m_HeatStateShort+")"); - } - char buf2[9]; - getTempString("", m_internalTempString, buf2, sizeof(buf2)); - right(buf2); - - if(!m_settingsFactory->getVersionDisplayed()) { - LogHandler::verbose(_TAG, "versionDisplayed"); - newLine(); - - left(m_HeatState.c_str()); - if(m_fanControlEnabled) { - right(m_fanState.c_str()); - } - } - } - } - void draw32Temp() { - if(m_settingsFactory->getSleeveTempDisplayed() && !m_settingsFactory->getInternalTempDisplayed()) { - LogHandler::verbose(_TAG, "Enter draw32Temp sleeveTempDisplayed"); - char buf[20]; - if(m_settingsFactory->getVersionDisplayed() || !hasNextLine()) { - LogHandler::verbose(_TAG, "versionDisplayed"); - getTempString("Sleeve: ", m_sleeveTempString, buf, sizeof(buf)); - left(buf); - display.print("("+m_HeatStateShort+")"); - } else if(hasNextLine(2)) { - LogHandler::verbose(_TAG, "hasNextLine(2)"); - setTextSize(2); - getTempString("SLT: ", m_sleeveTempString, buf, sizeof(buf)); - left(buf); - setTextSize(1); - display.print("("+m_HeatStateShort+")"); - } else { - getTempString("Sleeve: ", m_sleeveTempString, buf, sizeof(buf)); - left(buf); - newLine(); - left(m_HeatState.c_str(), 3); - } - } else if(!m_settingsFactory->getSleeveTempDisplayed() && m_settingsFactory->getInternalTempDisplayed()) { - LogHandler::verbose(_TAG, "Enter draw32Temp internalTempDisplayed"); - char buf[21]; - if(m_settingsFactory->getVersionDisplayed() || !hasNextLine()) { - LogHandler::verbose(_TAG, "versionDisplayed"); - getTempString("Internal: ", m_internalTempString, buf, sizeof(buf)); - left(buf); - } else if(hasNextLine(2)) { - LogHandler::verbose(_TAG, "hasNextLine(2)"); - setTextSize(2); - getTempString("INT:", m_internalTempString, buf, sizeof(buf)); - left(buf); - // setTextSize(1); - // display.print("("+m_HeatStateShort+")"); - } else { - getTempString("Internal: ", m_internalTempString, buf, sizeof(buf)); - left(buf); - newLine(); - left(m_fanState.c_str(), 3); - } - } else if(m_settingsFactory->getSleeveTempDisplayed() && m_settingsFactory->getInternalTempDisplayed()) { - LogHandler::verbose(_TAG, "Enter draw32Temp sleeveTempDisplayed && internalTempDisplayed"); - char buf[10]; - if(m_settingsFactory->getVersionDisplayed() || !hasNextLine()) { - LogHandler::verbose(_TAG, "versionDisplayed"); - getTempString("S", m_sleeveTempString, buf, sizeof(buf)); - left(buf); - display.print("("+m_HeatStateShort+")"); - getTempString("I", m_internalTempString, buf, sizeof(buf)); - right(buf); - } else { - left("Sleeve", 1); - right("Internal", 1); - newLine(); - getTempString("", m_sleeveTempString, buf, sizeof(buf)); - left(buf, 1); - char buf2[10]; - getTempString("", m_internalTempString, buf2, sizeof(buf2)); - right(buf2, 2); - } - } - } - - void drawBatteryLevel() { - bool wifiConnected = false; -#if WIFI_TCODE - wifiConnected = WifiHandler::isConnected(); -#endif - if(BatteryHandler::connected()) { - LogHandler::verbose(_TAG, "Enter draw battery"); - if(m_settingsFactory->getBatteryLevelNumeric()) { - //double voltageNumber = mapf(m_batteryVoltage, 0.0, 3.3, 0.0, m_settingsFactory->getBatteryVoltageMax()); - if (false) {//Display voltage - display.setCursor((m_settingsFactory->getDisplayScreenWidth() - (m_batteryVoltage < 10.0 ? 3 : 4) * charWidth) - (wifiConnected ? 3 : 0) * charWidth, currentLine); - display.print(m_batteryVoltage, 1); - } else { - display.setCursor((m_settingsFactory->getDisplayScreenWidth() - (m_batteryCapacityRemainingPercentage < 10.0 ? 3 : 4) * charWidth) - (wifiConnected ? 3 : 0) * charWidth, currentLine); - display.print(m_batteryCapacityRemainingPercentage, 1); - display.print("%"); - } - } else { - int batteryBars; - if(false) {//Display voltage - if (m_batteryVoltage >= 3.17) { - batteryBars = 5; - } else if (m_batteryVoltage < 3.17 && m_batteryVoltage > 3.09) { - batteryBars = 4; - } else if (m_batteryVoltage < 3.09 && m_batteryVoltage > 3.02) { - batteryBars = 3; - } else if (m_batteryVoltage < 3.02 && m_batteryVoltage > 2.92) { - batteryBars = 2; - } else if (m_batteryVoltage < 2.92 && m_batteryVoltage > 2.83) { - batteryBars = 1; - } else { - batteryBars = 0; - } - } else { - if (m_batteryCapacityRemainingPercentage >= 80) { - batteryBars = 5; - } else if (m_batteryCapacityRemainingPercentage < 80 && m_batteryCapacityRemainingPercentage > 60) { - batteryBars = 4; - } else if (m_batteryCapacityRemainingPercentage < 60 && m_batteryCapacityRemainingPercentage > 40) { - batteryBars = 3; - } else if (m_batteryCapacityRemainingPercentage < 40 && m_batteryCapacityRemainingPercentage > 20) { - batteryBars = 2; - } else if (m_batteryCapacityRemainingPercentage < 20 && m_batteryCapacityRemainingPercentage > 1) { - batteryBars = 1; - } else { - batteryBars = 0; - } - } - for (int b=0; b < batteryBars; b++) { - display.fillRect( - (m_settingsFactory->getDisplayScreenWidth() - (!wifiConnected ? 20 : 37)) + (b*3), - 2, - 2, - lineHeight - 4, - WHITE); - } - display.drawRect( - m_settingsFactory->getDisplayScreenWidth() - (!wifiConnected ? 23 : 40), - 1, - 20, - lineHeight-2, - WHITE); // draw the outline box - } - } - } - // bool tryConnect() //Connects to the wrong address - // { - // unsigned int vecSize = m_settingsFactory->getSystemI2CAddresses.size()(); - // LogHandler::info(_TAG, "System I2c device count %ld", vecSize); - // if(vecSize) { - // for(unsigned int i = 0; i < vecSize; i++) - // { - // //const char* address = m_settingsFactory->getSystemI2CAddresses[i].c_str()(); - // char buf[10]; - // hexToString(m_settingsFactory->getSystemI2CAddresses[i](), buf); - // LogHandler::info(_TAG, "Trying to connect to %s", buf); - // if(m_settingsFactory->getSystemI2CAddresses[i]() && connectDisplay(m_settingsFactory->getSystemI2CAddresses[i]())) { - // LogHandler::info(_TAG, "Sucess!"); - // m_settingsFactory->getDisplay_I2C_Address() = m_settingsFactory->getSystemI2CAddresses[i](); - // m_settingsFactory->getSave()(); - // return true; - // } else { - // LogHandler::info(_TAG, "Failed.."); - // } - // } - // LogHandler::info(_TAG, "Could not connect to any I2C address"); - // } - // return false; - // } - - bool connectDisplay(int address, int pin) { - if(LogHandler::getLogLevel() == LogLevel::DEBUG) { - char buf[10]; - hexToString(address, buf); - LogHandler::debug(_TAG, "Connect to display at address: %s", buf); - LogHandler::debug(_TAG, "byte: %ld", address); - } - if (pin >= 0) - { - displayConnected = display.begin(SSD1306_SWITCHCAPVCC, address, pin); - if (!displayConnected) - LogHandler::error(_TAG, "SSD1306 RST_PIN allocation failed"); - } - else - { - displayConnected = display.begin(SSD1306_SWITCHCAPVCC, address); - if (!displayConnected) - LogHandler::error(_TAG, "SSD1306 allocation failed"); - } - LogHandler::debug(_TAG, "Exit connectDisplay connected: %ld", displayConnected); - return displayConnected; - } - - void getTempString(const char* displayText, char* temp, char* buf, int size) { - strtrim(temp); - snprintf(buf, size, "%s%s%cC", displayText, temp, (char)247); - //strtrim(buf); - // //tempText[strlen(tempText)] = '\n'; - - // strtrim(temp); - // String tempText = displayText + String(temp) + (char)247 + "C";// TODO: remove String - // strcpy(buf, tempText.c_str()); - } -}; diff --git a/ESP32/src/HTTP/HTTPBase.h b/ESP32/src/HTTP/HTTPBase.h deleted file mode 100644 index 9d041c8..0000000 --- a/ESP32/src/HTTP/HTTPBase.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once -#include "WebSocketBase.h" -#include "TaskHandler.h" -class HTTPBase : public Task { - public: - virtual void setup_http(uint16_t port, WebSocketBase* webSocketHandler, bool apMode) = 0; - virtual void stop() = 0; - virtual bool isRunning() = 0; -}; \ No newline at end of file diff --git a/ESP32/src/SettingsHandler.h b/ESP32/src/SettingsHandler.h index 32650e0..97a13a2 100644 --- a/ESP32/src/SettingsHandler.h +++ b/ESP32/src/SettingsHandler.h @@ -65,7 +65,7 @@ class SettingsHandler static inline MotionProfile* motionProfiles; static inline ButtonSet* buttonSets; - + // static bool staticIP; static char currentIP[IP_ADDRESS_LEN]; static char currentGateway[IP_ADDRESS_LEN]; @@ -75,20 +75,20 @@ class SettingsHandler static bool apMode; - + // template::value || std::is_integral::value || std::is_enum::value || std::is_floating_point::value || std::is_same::value>> // static void getValue(const char* name, T &value) // { // m_settingsFactory->getValue(name, value); // } - + // static void getValue(const char* name, char* value, size_t len) // { // m_settingsFactory->getValue(name, value, len); // } - // static void defaultValue(const char* name) + // static void defaultValue(const char* name) // { // m_settingsFactory->defaultValue(name); // } @@ -159,7 +159,7 @@ class SettingsHandler restartRequired = delayInSec; } - static void printWebAddress(const char* hostAddress) + static void printWebAddress(const char* hostAddress) { char webServerportString[6]; int webServerPort = 0; @@ -167,20 +167,20 @@ class SettingsHandler sprintf(webServerportString, ":%d", webServerPort); LogHandler::info(_TAG, "Web address: http://%s%s", hostAddress, webServerPort == 80 ? "" : webServerportString); } - - static bool saveAll(JsonObject obj = JsonObject()) + + static bool saveAll(JsonObject obj = JsonObject()) { if(!m_settingsFactory->saveAllToDisk(obj) || !saveMotionProfiles(obj) || !saveButtons(obj)) return false; return true; } - + static bool saveAll(const String& data) { LogHandler::debug(_TAG, "Save frome string"); printFree(); JsonDocument doc; - + DeserializationError error = deserializeJson(doc, data); if (error) { @@ -255,7 +255,7 @@ class SettingsHandler JsonObject logLevelVerbose = logLevels.add(); logLevelVerbose["name"] = "Verbose"; logLevelVerbose["value"] = LogLevel::VERBOSE; - + JsonArray tcodeVersions = doc["tcodeVersions"].to(); JsonObject v03 = tcodeVersions.add(); v03["name"] = "v0.3"; @@ -356,14 +356,14 @@ class SettingsHandler JsonObject defaultLoveDevice = bleLoveDevices.add(); defaultLoveDevice["name"] = "Edge"; defaultLoveDevice["value"] = BLELoveDeviceType::EDGE; - + JsonArray availableChannels = doc["availableChannels"].to(); channelMap.serialize(availableChannels); doc[MOTION_ENABLED] = getMotionEnabled(); // int motionProfileSelectedIndex = MOTION_PROFILE_SELECTED_INDEX_DEFAULT; // m_settingsFactory->getValue(MOTION_PROFILE_SELECTED_INDEX, motionProfileSelectedIndex); - doc[MOTION_PROFILE_SELECTED_INDEX] = motionSelectedProfileIndex; + doc[MOTION_PROFILE_SELECTED_INDEX] = motionSelectedProfileIndex; JsonArray availableTimers = doc["availableTimers"].to(); JsonArray timerChannels = doc["timerChannels"].to(); @@ -424,7 +424,7 @@ class SettingsHandler static bool loadButtons(bool loadDefault, JsonObject json = JsonObject()) { LogHandler::info(_TAG, "Loading buttons"); return loadSettingsJson(BUTTON_SETTINGS_PATH, loadDefault, m_buttonsMutex, [](const JsonObject json, bool& mutableLoadDefault) -> bool { - + // const bool bootButtonEnabled = SettingsHandler::getValue(BOOT_BUTTON_ENABLED); // const bool buttonSetsEnabled = SettingsHandler::getValue(BUTTON_SETS_ENABLED);; // const char* bootButtonCommand = SettingsHandler::getValue(BOOT_BUTTON_COMMAND);; @@ -454,7 +454,7 @@ class SettingsHandler for(int i = 0; i < MAX_BUTTON_SETS; i++) { buttonSets[i] = ButtonSet(); buttonSets[i].pin = pinMap->buttonSetPin(i); - + sprintf(buttonSets[i].name, "Button set %u", i+1); LogHandler::debug(_TAG, "Default buttonset name: %s, index: %u, pin: %ld", buttonSets[i].name, i, buttonSets[i].pin); for(int j = 0; j < MAX_BUTTONS; j++) { @@ -465,7 +465,7 @@ class SettingsHandler } } } else { - std::vector pins; + std::vector pins; for(int i = 0; i < MAX_BUTTON_SETS; i++) { auto set = ButtonSet(); set.fromJson(buttonSetsObj[i].as()); @@ -479,10 +479,10 @@ class SettingsHandler m_settingsFactory->setValue(BUTTON_SET_PINS, pins); m_settingsFactory->savePins(); } - + if(initialized) sendMessage(SettingProfile::Button, "analogButtonCommands"); - + return true; }, saveButtons, json); } @@ -500,14 +500,14 @@ class SettingsHandler bool bootButtonEnabled = BOOT_BUTTON_ENABLED_DEFAULT; m_settingsFactory->getValue(BOOT_BUTTON_ENABLED, bootButtonEnabled); - doc[BOOT_BUTTON_ENABLED] = bootButtonEnabled; + doc[BOOT_BUTTON_ENABLED] = bootButtonEnabled; bool buttonSetsEnabled = BUTTON_SETS_ENABLED_DEFAULT; m_settingsFactory->getValue(BUTTON_SETS_ENABLED, buttonSetsEnabled); doc[BUTTON_SETS_ENABLED] = buttonSetsEnabled; // char bootButtonCommand[BOOT_BUTTON_COMMAND_LEN] = {0}; // m_settingsFactory->getValue(BOOT_BUTTON_COMMAND, bootButtonCommand, BOOT_BUTTON_COMMAND_LEN); const char* bootButtonCommand = m_settingsFactory->getBootButtonCommand(); - doc[BOOT_BUTTON_COMMAND] = bootButtonCommand; + doc[BOOT_BUTTON_COMMAND] = bootButtonCommand; int buttonAnalogDebounce = BUTTON_ANALOG_DEBOUNCE_DEFAULT; m_settingsFactory->getValue(BUTTON_ANALOG_DEBOUNCE, buttonAnalogDebounce); doc[BUTTON_ANALOG_DEBOUNCE] = buttonAnalogDebounce; @@ -518,7 +518,7 @@ class SettingsHandler { //JsonObject obj; doc["buttonSets"][i]["name"] = buttonSets[i].name; - + doc["buttonSets"][i]["pin"] = buttonSets[i].pin; pins.push_back(buttonSets[i].pin); doc["buttonSets"][i]["pullMode"] = (uint8_t)buttonSets[i].pullMode; @@ -554,7 +554,7 @@ class SettingsHandler motionDefaultProfileIndex = json[MOTION_PROFILE_DEFAULT_INDEX] | MOTION_PROFILE_SELECTED_INDEX_DEFAULT; if(!initialized) motionSelectedProfileIndex = motionDefaultProfileIndex; - + JsonArray motionProfilesObj = json[MOTION_PROFILES].as(); if(motionProfilesObj.isNull()) { LogHandler::info(_TAG, "No motion profiles stored, loading default"); @@ -645,7 +645,7 @@ class SettingsHandler saving = false; return false; } - + xSemaphoreGive(m_motionMutex); saving = false; return true; @@ -662,9 +662,9 @@ class SettingsHandler LogHandler::info(_TAG, "Loading channel profile"); return loadSettingsJson(CHANNELS_SETTINGS_PATH, loadDefault, m_channelsMutex, [](const JsonObject json, bool& mutableLoadDefault) -> bool { - + JsonArray channelProfileObj = json[CHANNEL_PROFILE].as(); - + if(channelProfileObj.isNull()) { LogHandler::info(_TAG, "No channel profile stored, loading default"); mutableLoadDefault = true; @@ -722,7 +722,7 @@ class SettingsHandler saving = false; return false; } - + xSemaphoreGive(m_channelsMutex); saving = false; return true; @@ -732,7 +732,7 @@ class SettingsHandler { return motionProfiles[motionSelectedProfileIndex].channels; } - + static bool getMotionEnabled() { return motionEnabled; @@ -750,7 +750,7 @@ class SettingsHandler setValue(newValue, motionPaused, SettingProfile::MotionProfile, MOTION_PAUSED); } - static int getMotionDefaultProfileIndex() + static int getMotionDefaultProfileIndex() { return motionDefaultProfileIndex; } @@ -937,7 +937,7 @@ class SettingsHandler //m_settingsFactory->setValue(MOTION_PROFILE_SELECTED_INDEX, profileIndex); setValue(profileIndex, motionSelectedProfileIndex, SettingProfile::MotionProfile, MOTION_PROFILE_SELECTED_INDEX); } - + static void cycleMotionProfile() { if(!getMotionEnabled()) { setMotionEnabled(true); @@ -1064,7 +1064,7 @@ class SettingsHandler return true; } - static bool I2CScan() + static bool I2CScan() { systemI2CAddresses.clear(); byte error, address; @@ -1080,14 +1080,14 @@ class SettingsHandler return false; } Wire.begin(sdaPin, sclPin); - for(address = 1; address < 127; address++ ) + for(address = 1; address < 127; address++ ) { Wire.beginTransmission(address); error = Wire.endTransmission(); - if (error == 0) + if (error == 0) { //Serial.print("I2C device found at address 0x"); - // if (address<16) + // if (address<16) // { // Serial.print("0"); // } @@ -1096,7 +1096,7 @@ class SettingsHandler // std::stringstream I2C_Address_String; // I2C_Address_String << "0x" << std::hex << address; // std::string foundAddress = I2C_Address_String.str(); - + char buf[10]; hexToString(address, buf); LogHandler::info(_TAG, "I2C device found at address %s, byte %ld", buf, address); @@ -1104,10 +1104,10 @@ class SettingsHandler systemI2CAddresses.push_back((int)address); nDevices++; } - else if (error==4) + else if (error==4) { Serial.print("Unknow error at address 0x"); - if (address<16) + if (address<16) { Serial.print("0"); } @@ -1116,7 +1116,7 @@ class SettingsHandler // I2C_Address_String << "0x" << std::hex << address; // std::string foundAddress = I2C_Address_String.str(); // LogHandler::error(_TAG, "Unknow error at address %s", foundAddress); - } + } } if (nDevices == 0) { LogHandler::info(_TAG, "No I2C devices found"); @@ -1124,13 +1124,13 @@ class SettingsHandler } return true; } - - static Channel* getChannel(const char *name) + + static Channel* getChannel(const char *name) { return channelMap.get(name); } - static uint16_t getChannelMin(const char *name) + static uint16_t getChannelMin(const char *name) { Channel* channelProfile = channelMap.get(name); if(!channelProfile) @@ -1141,7 +1141,7 @@ class SettingsHandler return channelProfile->min; } - static uint16_t getChannelMax(const char *name) + static uint16_t getChannelMax(const char *name) { Channel* channelProfile = channelMap.get(name); if(!channelProfile) @@ -1152,7 +1152,7 @@ class SettingsHandler return channelProfile->max; } - static uint16_t getChannelUserMin(const char *name) + static uint16_t getChannelUserMin(const char *name) { Channel* channelProfile = channelMap.get(name); if(!channelProfile) @@ -1163,7 +1163,7 @@ class SettingsHandler return channelProfile->userMin; } - static uint16_t getChannelUserMax(const char *name) + static uint16_t getChannelUserMax(const char *name) { Channel* channelProfile = channelMap.get(name); if(!channelProfile) @@ -1174,7 +1174,7 @@ class SettingsHandler return channelProfile->userMax; } - static void setChannelMin(const char *name, uint16_t value) + static void setChannelMin(const char *name, uint16_t value) { Channel* channelProfile = channelMap.get(name); if(!channelProfile) @@ -1185,7 +1185,7 @@ class SettingsHandler channelProfile->userMin = value; } - static void setChannelMax(const char *name, uint16_t value) + static void setChannelMax(const char *name, uint16_t value) { Channel* channelProfile = channelMap.get(name); if(!channelProfile) @@ -1206,7 +1206,7 @@ class SettingsHandler private: static const char *_TAG; - + static SettingsFactory* m_settingsFactory; static SemaphoreHandle_t m_motionMutex; static SemaphoreHandle_t m_channelsMutex; @@ -1225,7 +1225,7 @@ class SettingsHandler static int motionSelectedProfileIndex; static int motionDefaultProfileIndex; // static MotionProfile motionProfiles[maxMotionProfileCount]; - + // static bool voiceEnabled; // static bool voiceMuted; // static int voiceWakeTime ; @@ -1275,7 +1275,7 @@ class SettingsHandler // for (auto x : ChannelMapV3) { // currentChannels.push_back(x); // } -// } +// } // #endif @@ -1287,7 +1287,7 @@ class SettingsHandler // currentChannels[i].min = !min ? 1 : min; // currentChannels[i].max = !max ? tcodeMax : max; // } - + // sendMessage("channelRanges", "channelRanges");// TODO: channelranges should be in its own json // udpServerPort = json["udpServerPort"] | 8000; @@ -1327,7 +1327,7 @@ class SettingsHandler // BLDC_StrokeLength = json["BLDC_StrokeLength"] | 120; // setBoardPinout(json); - + // if(isBoardType(BoardType::CRIMZZON)) { // heaterResolution = json["heaterResolution"] | 8; // caseFanResolution = json["caseFanResolution"] | 10; @@ -1407,7 +1407,7 @@ class SettingsHandler // caseFanResolution = json["caseFanResolution"] | 10; // } // caseFanMaxDuty = pow(2, caseFanResolution) - 1; - + // lubeEnabled = json["lubeEnabled"]; // setValue(json, voiceEnabled, "voiceHandler", "voiceEnabled", false); @@ -1455,7 +1455,7 @@ class SettingsHandler // // tags.push_back(TagHandler::BLDCHandler); // // tags.push_back(TagHandler::ServoHandler3); // // LogHandler::setTags(tags); - + // for (size_t i = 0; i < currentChannels.size(); i++) // { // doc["channelRanges"][currentChannels[i].Name]["min"] = currentChannels[i].min; @@ -1475,7 +1475,7 @@ class SettingsHandler // } // doc["fullBuild"] = fullBuild; // doc["TCodeVersion"] = (int)TCodeVersionEnum; - + // doc["udpServerPort"] = udpServerPort; // doc["webServerPort"] = webServerPort; // doc["hostname"] = hostname; @@ -1526,7 +1526,7 @@ class SettingsHandler // doc["BLDC_MotorA_ZeroElecAngle"] = round2(BLDC_MotorA_ZeroElecAngle); // doc["BLDC_RailLength"] = BLDC_RailLength; // doc["BLDC_StrokeLength"] = BLDC_StrokeLength; - + // LogHandler::debug(_TAG, "save %s max: %f", "BLDC_MotorA_Voltage", doc["BLDC_MotorA_Current"].as()); // doc["staticIP"] = staticIP; @@ -1634,13 +1634,13 @@ class SettingsHandler // } /// @brief Locks the mutex checks for an existing file and creates it if it doesnt exist. Calls the callback function and gives the mutex. - /// @param filepath - /// @param mutableLoadDefault - /// @param mutex - /// @param jsonSize - /// @param loadFunction - /// @param json - /// @return + /// @param filepath + /// @param mutableLoadDefault + /// @param mutex + /// @param jsonSize + /// @param loadFunction + /// @param json + /// @return static bool loadSettingsJson(const char* filepath, bool loadDefault, SemaphoreHandle_t& mutex, std::function loadFunction, std::function saveFunction, JsonObject json = JsonObject()) { JsonDocument doc; //jsonSize bool mutableLoadDefault = loadDefault; @@ -1663,12 +1663,12 @@ class SettingsHandler } /// @brief Locks the mutex and validates the file exists. calls the calback and serializes the data in a file to disk. Releases the mutex. - /// @param filepath - /// @param mutex - /// @param jsonSize - /// @param saveFunction - /// @param json - /// @return + /// @param filepath + /// @param mutex + /// @param jsonSize + /// @param saveFunction + /// @param json + /// @return static bool saveSettingsJson(const char* filepath, SemaphoreHandle_t& mutex, int jsonSize, std::function saveFunction, std::function loadFunction, JsonObject json = JsonObject()) { saving = true; xSemaphoreTake(mutex, portMAX_DELAY); @@ -1800,7 +1800,7 @@ class SettingsHandler } // /** If the parameter json is ommited or the pin value doesnt exist on the object then the pins are set to default. */ // static void setBoardPinout(JsonObject json = JsonObject()) { - + // #if MOTOR_TYPE == 0 // if(isBoardType(BoardType::ISAAC)) { // // RightServo_PIN = 2; @@ -1885,7 +1885,7 @@ class SettingsHandler // Vibe3_PIN = json["Vibe3_PIN"] | 32; // } // } -// #elif MOTOR_TYPE == 1 +// #elif MOTOR_TYPE == 1 // // BLDC motor // BLDC_Encoder_PIN = json["BLDC_Encoder_PIN"] | 33; // BLDC_ChipSelect_PIN = json["BLDC_ChipSelect_PIN"] | 5; @@ -2000,7 +2000,7 @@ class SettingsHandler setValue(newValue, variable, profile, propertyName); } - template + template static void setValue(JsonObject json, char (&variable)[n], const SettingProfile &profile, const char *propertyName, const char *defaultValue) { const char *newValue = json[propertyName] | defaultValue; @@ -2067,8 +2067,8 @@ class SettingsHandler if (valueChanged) sendMessage(profile, propertyName); } - - template + + template static void setValue(const char *newValue, char (&variable)[n], const SettingProfile &profile, const char *propertyName) { bool valueChanged = initialized && strcmp(variable, newValue) != -1; @@ -2439,7 +2439,7 @@ char SettingsHandler::currentDns2[IP_ADDRESS_LEN] = DNS2_DEFAULT; // bool SettingsHandler::BLDC_UseHallSensor = false; // int SettingsHandler::BLDC_Pulley_Circumference = 60; // int SettingsHandler::BLDC_Encoder_PIN = 33;// PWM feedback pin (if used) - P pad on AS5048a -// int SettingsHandler::BLDC_Enable_PIN = 14;// Motor enable - EN on SFOCMini +// int SettingsHandler::BLDC_Enable_PIN = 14;// Motor enable - EN on SFOCMini // int SettingsHandler::BLDC_HallEffect_PIN = 12; // int SettingsHandler::BLDC_PWMchannel1_PIN = 27; // int SettingsHandler::BLDC_PWMchannel2_PIN = 26; diff --git a/ESP32/src/TCode/v0.4/EventHandler.h b/ESP32/src/TCode/v0.4/EventHandler.h index 4aaf527..9af587b 100644 --- a/ESP32/src/TCode/v0.4/EventHandler.h +++ b/ESP32/src/TCode/v0.4/EventHandler.h @@ -4,7 +4,7 @@ // Decodes T-code commands and uses them to control servos a single brushless motor // It can handle: // 3x linear channels (L0, L1, L2) -// 3x rotation channels (R0, R1, R2) +// 3x rotation channels (R0, R1, R2) // 3x vibration channels (V0, V1, V2) // 3x auxilliary channels (A0, A1, A2) // This code is designed to drive the SSR1 stroker robot, but is also intended to be @@ -73,7 +73,7 @@ class EventHandler: public TCodeIObserver { } break; case DeviceCommandType::GetAssignedAxisValues: { - + } break; case DeviceCommandType::GetTCodeVersion: { diff --git a/ESP32/src/TCode/v0.4/OutputStream.h b/ESP32/src/TCode/v0.4/OutputStream.h deleted file mode 100644 index 6bdcc30..0000000 --- a/ESP32/src/TCode/v0.4/OutputStream.h +++ /dev/null @@ -1,66 +0,0 @@ -/* MIT License - -Copyright (c) 2024 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include "Arduino.h" -#include "outputstream/TOutputStreamInterface.h" - -class OutputStream : public TCode::Output::TCodeIOutputStream { - public: - void write(const char value) const override { - //// Serial.print(value); - }; - void write(const char *value) const override { - // Serial.print(value); - }; - void write(const __FlashStringHelper *value) const override { - // Serial.print(value); - }; - void write(const String &value) const override { - // Serial.print(value); - }; - void print(const char value) const override { - // Serial.print(value); - }; - void print(const char *value) const override { - // Serial.print(value); - }; - void print(const __FlashStringHelper *value) const override { - // Serial.print(value); - }; - void print(const String &value) const override { - // Serial.print(value); - }; - void println(const char value) const override { - // Serial.println(value); - }; - void println(const char *value) const override { - // Serial.println(value); - }; - void println(const __FlashStringHelper *value) const override { - // Serial.println(value); - }; - void println(const String &value) const override { - // Serial.println(value); - }; -}; \ No newline at end of file diff --git a/ESP32/src/TCode/v0.4/TCode0_4.h b/ESP32/src/TCode/v0.4/TCode0_4.h index 97a823f..5366372 100644 --- a/ESP32/src/TCode/v0.4/TCode0_4.h +++ b/ESP32/src/TCode/v0.4/TCode0_4.h @@ -4,7 +4,6 @@ #include #include "TCodeBaseV4.h" #include "TagHandler.h" -#include "OutputStream.h" #include "EventHandler.h" @@ -17,7 +16,6 @@ class TCode0_4 : public TCodeBaseV4 firmwareID = firmware; // #ESP32# Enable EEPROM - m_tcode.setOutputStream(&m_outputStream); m_tcode.registerEventObserver(&m_eventHandler); // m_tcode.registerInterface(&button); @@ -52,11 +50,11 @@ class TCode0_4 : public TCodeBaseV4 void setAxisData(TCodeAxis* channel, const AxisData &data) override { m_tcode.setAxisData(channel->getId(), data); } - void setAxisData(TCodeAxis* channel, - const float value, - const AxisExtentionType extentionType, - const unsigned long commandExtension, - AxisRampData rampIn, + void setAxisData(TCodeAxis* channel, + const float value, + const AxisExtentionType extentionType, + const unsigned long commandExtension, + AxisRampData rampIn, AxisRampData rampOut) override { AxisData data = { @@ -74,7 +72,7 @@ class TCode0_4 : public TCodeBaseV4 m_eventHandler.registerOnNotify(f); TCodeBaseV4::setMessageCallback(f); } - + virtual void setAxisData(TCodeAxis* channel, const float value, const AxisExtentionType extentionType, const unsigned long commandExtension) { AxisData data = { value, @@ -117,7 +115,6 @@ class TCode0_4 : public TCodeBaseV4 EventHandler m_eventHandler; private: const char *_TAG = TagHandler::TCodeHandler; - OutputStream m_outputStream; const char *firmwareID; const static int m_axisCount = 11; TCodeManager m_tcode; @@ -158,10 +155,10 @@ class TCode0_4 : public TCodeBaseV4 // AxisExtentionType toExtensionType(const char &extension) { // if(extension == 'S') { // return AxisExtentionType::Speed; - // } + // } // if(extension == 'I') { // return AxisExtentionType::Time; - // } + // } // return AxisExtentionType::None; // } }; diff --git a/desktop.ini b/desktop.ini new file mode 100644 index 0000000..ab17096 --- /dev/null +++ b/desktop.ini @@ -0,0 +1,4 @@ +[ViewState] +Mode= +Vid= +FolderType=Documents From cf092fa6b9b32cbe0e496dfe873285af732ba0fd Mon Sep 17 00:00:00 2001 From: Millibyte Products Date: Mon, 23 Mar 2026 14:36:24 -0400 Subject: [PATCH 03/42] Update wip 2 --- ESP32/configure_wifi_and_reset.bat | 25 + ESP32/configure_wifi_and_reset.ps1 | 190 ++ ESP32/configure_wifi_and_reset.sh | 174 ++ ESP32/lib/settingConstants.h | 6 +- ESP32/platformio.ini | 24 +- ESP32/src/HTTP/HTTPSHandler.hpp | 2 +- ESP32/src/HTTP/SecureWebSocketHandler.hpp | 8 +- ESP32/src/HTTP/WebSocketBase.h | 41 +- ESP32/src/MDNSHandler.hpp | 2 +- ESP32/src/Motion/MotionGenerator.hpp | 2 +- ESP32/src/Motion/MotionHandler.hpp | 2 +- ESP32/src/PowerHandler.h | 46 + ESP32/src/SystemCommandHandler.h | 208 +- ESP32/src/TCode/MotorHandler.h | 2 +- ESP32/src/TCode/v0.2/ServoHandler0_2.h | 2 +- ESP32/src/TCode/v0.3/BLDCHandler0_3.h | 2 +- ESP32/src/TCode/v0.3/MotorHandler0_3.h | 2 +- ESP32/src/TCode/v0.3/ServoHandler0_3.h | 2 +- ESP32/src/TCode/v0.4/BLDCHandler0_4.h | 2 +- ESP32/src/TCode/v0.4/MotorHandler0_4.h | 2 +- ESP32/src/TCode/v0.4/ServoHandler0_4.h | 2 +- ESP32/src/UdpHandler.h | 143 +- ESP32/src/VoiceHandler.hpp | 167 +- ESP32/src/WebHandler_psychic.h | 6 +- ESP32/src/WebSocketHandler_psychic.h | 6 +- ESP32/src/WifiHandler.h | 136 +- .../bluetooth/BLE/BLEConfigurationHandler.h | 323 ++ .../src/bluetooth/BLE/BLEHCControlCallback.h | 120 + ESP32/src/bluetooth/BLE/BLEHandler.hpp | 208 ++ ESP32/src/bluetooth/BluetoothHandler.h | 103 + ESP32/src/display/DisplayHandler.h | 762 +++++ ESP32/src/logging/TagHandler.h | 239 ++ ESP32/src/main.cpp | 1274 +------- ESP32/src/messages/MessageHandler.h | 110 + ESP32/src/network/HTTP/HTTPBase.h | 11 + ESP32/src/network/NetworkHandler.h | 59 + ESP32/src/network/WebHandler.h | 572 ++++ ESP32/src/network/WebSocketHandler.h | 303 ++ ESP32/src/sensors/BatteryHandler.h | 223 ++ ESP32/src/sensors/ButtonHandler.hpp | 255 ++ ESP32/src/sensors/TemperatureHandler.h | 131 + ESP32/src/serial/SerialHandler.h | 91 + ESP32/src/settings/FilesystemHandler.h | 46 + ESP32/src/settings/OperatingModeHandler.h | 84 + ESP32/src/settings/SettingsHandler.h | 2610 +++++++++++++++++ ESP32/src/tasks/TaskHandler.h | 298 ++ ESP32/src/utils.h | 97 +- 47 files changed, 7602 insertions(+), 1521 deletions(-) create mode 100644 ESP32/configure_wifi_and_reset.bat create mode 100644 ESP32/configure_wifi_and_reset.ps1 create mode 100644 ESP32/configure_wifi_and_reset.sh create mode 100644 ESP32/src/PowerHandler.h create mode 100644 ESP32/src/bluetooth/BLE/BLEConfigurationHandler.h create mode 100644 ESP32/src/bluetooth/BLE/BLEHCControlCallback.h create mode 100644 ESP32/src/bluetooth/BLE/BLEHandler.hpp create mode 100644 ESP32/src/bluetooth/BluetoothHandler.h create mode 100644 ESP32/src/display/DisplayHandler.h create mode 100644 ESP32/src/logging/TagHandler.h create mode 100644 ESP32/src/messages/MessageHandler.h create mode 100644 ESP32/src/network/HTTP/HTTPBase.h create mode 100644 ESP32/src/network/NetworkHandler.h create mode 100644 ESP32/src/network/WebHandler.h create mode 100644 ESP32/src/network/WebSocketHandler.h create mode 100644 ESP32/src/sensors/BatteryHandler.h create mode 100644 ESP32/src/sensors/ButtonHandler.hpp create mode 100644 ESP32/src/sensors/TemperatureHandler.h create mode 100644 ESP32/src/serial/SerialHandler.h create mode 100644 ESP32/src/settings/FilesystemHandler.h create mode 100644 ESP32/src/settings/OperatingModeHandler.h create mode 100644 ESP32/src/settings/SettingsHandler.h create mode 100644 ESP32/src/tasks/TaskHandler.h diff --git a/ESP32/configure_wifi_and_reset.bat b/ESP32/configure_wifi_and_reset.bat new file mode 100644 index 0000000..3cf77d6 --- /dev/null +++ b/ESP32/configure_wifi_and_reset.bat @@ -0,0 +1,25 @@ +@echo off +setlocal + +if "%~1"=="" ( + echo Usage: %~nx0 ^ ^ [COMx] + echo Example: %~nx0 MyWifi MyPass123 COM7 + exit /b 1 +) + +set "SSID=%~1" +set "PASSWORD=%~2" +set "PORT=%~3" + +if "%PASSWORD%"=="" ( + echo Usage: %~nx0 ^ ^ [COMx] + exit /b 1 +) + +if "%PORT%"=="" ( + powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0configure_wifi_and_reset.ps1" -Ssid "%SSID%" -Password "%PASSWORD%" +) else ( + powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0configure_wifi_and_reset.ps1" -Ssid "%SSID%" -Password "%PASSWORD%" -Port "%PORT%" +) + +endlocal diff --git a/ESP32/configure_wifi_and_reset.ps1 b/ESP32/configure_wifi_and_reset.ps1 new file mode 100644 index 0000000..f37cb09 --- /dev/null +++ b/ESP32/configure_wifi_and_reset.ps1 @@ -0,0 +1,190 @@ +param( + [Parameter(Mandatory = $true)] + [string]$Ssid, + + [Parameter(Mandatory = $true)] + [string]$Password, + + [string]$Port, + + [int]$Baud = 115200, + + [int]$CommandResponseTimeoutSeconds = 5, + + [int]$IpReadTimeoutSeconds = 30 +) + +$ErrorActionPreference = "Stop" + +function Get-AutoPort { + $available = [System.IO.Ports.SerialPort]::GetPortNames() | Sort-Object + if (-not $available -or $available.Count -eq 0) { + throw "No serial ports found." + } + + $preferred = @() + try { + $deviceRows = Get-CimInstance Win32_PnPEntity | Where-Object { $_.Name -match "\(COM\d+\)" } + foreach ($row in $deviceRows) { + if ($row.Name -match "\((COM\d+)\)") { + $com = $Matches[1] + if ( + $row.Name -match "ESP32|USB|UART|CP210|CH340|CH910|Silicon Labs|FTDI" -and + $available -contains $com + ) { + $preferred += $com + } + } + } + } + catch { + # Fallback to first port when CIM query is unavailable. + } + + if ($preferred.Count -gt 0) { + return $preferred[0] + } + + return $available[0] +} + +if (-not $Port) { + $Port = Get-AutoPort +} + +Write-Host "Using serial port: $Port" +Write-Host "Baud: $Baud" + +$serial = New-Object System.IO.Ports.SerialPort $Port, $Baud, ([System.IO.Ports.Parity]::None), 8, ([System.IO.Ports.StopBits]::One) +$serial.NewLine = "`n" +$serial.ReadTimeout = 300 +$serial.WriteTimeout = 1000 +$serial.DtrEnable = $false +$serial.RtsEnable = $false + +function Read-Until { + param( + [int]$TimeoutSeconds, + [string[]]$SuccessPatterns, + [string[]]$FailurePatterns + ) + + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + $lines = New-Object System.Collections.Generic.List[string] + + while ((Get-Date) -lt $deadline) { + try { + $line = $serial.ReadLine() + if ($null -eq $line) { + continue + } + + $line = $line.Trim() + if ($line.Length -eq 0) { + continue + } + + Write-Host "< $line" + $lines.Add($line) + + foreach ($failPattern in $FailurePatterns) { + if ($line -match $failPattern) { + throw "Device reported an error: $line" + } + } + + foreach ($successPattern in $SuccessPatterns) { + if ($line -match $successPattern) { + return @{ Matched = $true; Lines = $lines } + } + } + } + catch [System.TimeoutException] { + Start-Sleep -Milliseconds 80 + } + } + + return @{ Matched = $false; Lines = $lines } +} + +function Send-And-Validate { + param( + [string]$Command, + [string]$Display, + [string[]]$SuccessPatterns, + [switch]$RequireMatch + ) + + $failPatterns = @( + "Unknown command", + "Unknown save command", + "Invalid command", + "Invalid value", + "Error" + ) + + Write-Host "Sending: $Display" + $serial.WriteLine($Command) + + $result = Read-Until -TimeoutSeconds $CommandResponseTimeoutSeconds -SuccessPatterns $SuccessPatterns -FailurePatterns $failPatterns + + if ($RequireMatch -and -not $result.Matched) { + throw "Did not receive expected confirmation for command: $Display" + } +} + +function Read-DeviceIp { + $ipPatterns = @( + "IP\s*Address:\s*([0-9]{1,3}(\.[0-9]{1,3}){3})", + "WiFi\s*connected:\s*([0-9]{1,3}(\.[0-9]{1,3}){3})", + "Captive\s*portal\s*IP:\s*([0-9]{1,3}(\.[0-9]{1,3}){3})" + ) + + $deadline = (Get-Date).AddSeconds($IpReadTimeoutSeconds) + while ((Get-Date) -lt $deadline) { + Write-Host "Sending: #ip" + $serial.WriteLine("#ip") + + $result = Read-Until -TimeoutSeconds 2 -SuccessPatterns $ipPatterns -FailurePatterns @() + if ($result.Matched) { + foreach ($line in $result.Lines) { + foreach ($pattern in $ipPatterns) { + if ($line -match $pattern) { + $candidate = $Matches[1] + if ($candidate -and $candidate -ne "0.0.0.0") { + return $candidate + } + } + } + } + } + + Start-Sleep -Milliseconds 500 + } + + throw "Did not detect device IP via #ip query within timeout." +} + +try { + $serial.Open() + Start-Sleep -Milliseconds 500 + + # Drain boot noise before issuing commands. + $null = Read-Until -TimeoutSeconds 1 -SuccessPatterns @() -FailurePatterns @() + + Send-And-Validate -Command "#wifi-ssid:$Ssid" -Display "#wifi-ssid:" -SuccessPatterns @("Wifi SSID changed to:", "Restart is required after save") -RequireMatch + Send-And-Validate -Command "#wifi-pass:$Password" -Display "#wifi-pass:" -SuccessPatterns @("Wifi password changed to a value of", "Restart is required after save") -RequireMatch + Send-And-Validate -Command '$save' -Display '$save' -SuccessPatterns @("Settings saved!") -RequireMatch + Send-And-Validate -Command "#restart" -Display "#restart" -SuccessPatterns @() + + Write-Host "Waiting for device to reboot and report Wi-Fi IP..." + $ip = Read-DeviceIp + Write-Host "Device IP: $ip" + Write-Host "Done. Validation passed and device restart completed." +} +finally { + if ($serial.IsOpen) { + $serial.Close() + } + $serial.Dispose() +} diff --git a/ESP32/configure_wifi_and_reset.sh b/ESP32/configure_wifi_and_reset.sh new file mode 100644 index 0000000..7f41941 --- /dev/null +++ b/ESP32/configure_wifi_and_reset.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -lt 2 || $# -gt 3 ]]; then + echo "Usage: $0 [serial_port]" + echo "Example: $0 MyWifi MyPass123 /dev/ttyUSB0" + exit 1 +fi + +SSID="$1" +PASSWORD="$2" +PORT="${3:-}" +BAUD=115200 +COMMAND_TIMEOUT=5 +IP_TIMEOUT=30 + +pick_port() { + if [[ -n "$PORT" ]]; then + if [[ ! -e "$PORT" ]]; then + echo "Serial port not found: $PORT" >&2 + exit 1 + fi + echo "$PORT" + return + fi + + if compgen -G "/dev/serial/by-id/*" > /dev/null; then + local first_by_id + first_by_id=$(ls -1 /dev/serial/by-id/* | head -n 1) + if [[ -n "$first_by_id" ]]; then + readlink -f "$first_by_id" + return + fi + fi + + local candidates=(/dev/ttyUSB* /dev/ttyACM*) + for c in "${candidates[@]}"; do + if [[ -e "$c" ]]; then + echo "$c" + return + fi + done + + echo "No serial port found. Pass one explicitly as the third argument." >&2 + exit 1 +} + +PORT="$(pick_port)" + +echo "Using serial port: $PORT" +echo "Baud: $BAUD" + +stty -F "$PORT" "$BAUD" cs8 -cstopb -parenb -ixon -ixoff -echo -hupcl +exec 3<> "$PORT" + +send_cmd() { + local cmd="$1" + local display="${2:-$1}" + echo "Sending: $display" + printf '%s\r\n' "$cmd" >&3 +} + +read_until() { + local timeout="$1" + shift + local pattern_count="$1" + shift + + local patterns=() + local i + for ((i=0; i&2 + return 2 + fi + + if (( ${#patterns[@]} == 0 )); then + continue + fi + + for pat in "${patterns[@]}"; do + if [[ "$line" =~ $pat ]]; then + return 0 + fi + done + fi + done + + return 1 +} + +extract_ip_from_line() { + local line="$1" + if [[ "$line" =~ IP[[:space:]]Address:[[:space:]]([0-9]{1,3}(\.[0-9]{1,3}){3}) ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + if [[ "$line" =~ WiFi[[:space:]]connected:[[:space:]]([0-9]{1,3}(\.[0-9]{1,3}){3}) ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + if [[ "$line" =~ Captive[[:space:]]portal[[:space:]]IP:[[:space:]]([0-9]{1,3}(\.[0-9]{1,3}){3}) ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + return 1 +} + +read_until 1 0 || true + +send_cmd "#wifi-ssid:$SSID" "#wifi-ssid:" +if ! read_until "$COMMAND_TIMEOUT" 2 "Wifi SSID changed to:" "Restart is required after save"; then + echo "Missing confirmation for #wifi-ssid." >&2 + exit 1 +fi + +send_cmd "#wifi-pass:$PASSWORD" "#wifi-pass:" +if ! read_until "$COMMAND_TIMEOUT" 2 "Wifi password changed to a value of" "Restart is required after save"; then + echo "Missing confirmation for #wifi-pass." >&2 + exit 1 +fi + +send_cmd '$save' +if ! read_until "$COMMAND_TIMEOUT" 1 "Settings saved!"; then + echo "Missing confirmation for \$save." >&2 + exit 1 +fi + +send_cmd "#restart" +echo "Waiting for device to reboot and report Wi-Fi IP..." + +deadline=$((SECONDS + IP_TIMEOUT)) +DEVICE_IP="" +next_query=0 +while (( SECONDS < deadline )); do + if (( SECONDS >= next_query )); then + send_cmd "#ip" + next_query=$((SECONDS + 2)) + fi + + if IFS= read -r -t 0.2 line <&3; then + line="${line%$'\r'}" + [[ -z "$line" ]] && continue + echo "< $line" + + candidate_ip="" + if candidate_ip="$(extract_ip_from_line "$line")"; then + if [[ "$candidate_ip" != "0.0.0.0" ]]; then + DEVICE_IP="$candidate_ip" + break + fi + fi + fi +done + +if [[ -z "$DEVICE_IP" ]]; then + echo "Did not detect device IP in serial logs within timeout." >&2 + exit 1 +fi + +echo "Device IP: $DEVICE_IP" +echo "Done. Validation passed and device restart completed." diff --git a/ESP32/lib/settingConstants.h b/ESP32/lib/settingConstants.h index 25a4e77..cbc58da 100644 --- a/ESP32/lib/settingConstants.h +++ b/ESP32/lib/settingConstants.h @@ -16,7 +16,7 @@ // Setting defaults #if MOTOR_TYPE == 0 - #define DEVICE_TYPE_DEFAULT (uint8_t)DeviceType::OSR +#define DEVICE_TYPE_DEFAULT (uint8_t)DEFAULT_DEVICE #else #define DEVICE_TYPE_DEFAULT (uint8_t)DeviceType::SSR1 #endif @@ -35,7 +35,7 @@ #define AP_MODE_SUBNET_DEFAULT "255.255.255.0" #ifndef BOARD_TYPE_DEFAULT #if CONFIG_IDF_TARGET_ESP32 - #define BOARD_TYPE_DEFAULT (uint8_t)BoardType::DEVKIT +#define BOARD_TYPE_DEFAULT (uint8_t)DEFAULT_BOARD #elif CONFIG_IDF_TARGET_ESP32S3 #ifdef S3_ZERO #define BOARD_TYPE_DEFAULT (uint8_t)BoardType::ZERO @@ -44,7 +44,7 @@ #endif #endif #else - #define BOARD_TYPE_DEFAULT BOARD_TYPE +#define BOARD_TYPE_DEFAULT DEFAULT_BOARD #endif #define LOG_LEVEL_DEFAULT (uint8_t)LogLevel::INFO //#define FULL_BUILD_DEFAULT false diff --git a/ESP32/platformio.ini b/ESP32/platformio.ini index 6e6728c..3873441 100644 --- a/ESP32/platformio.ini +++ b/ESP32/platformio.ini @@ -25,7 +25,7 @@ lib_deps = ArduinoJson@7.4.2 Arduino hacker-cb/MPark-Variant @ 1.4.0 - https://github.com/multiaxis/TCode-Library#v0.1.2 + https://github.com/multiaxis/TCode-Library#a5ec926 https://github.com/jcfain/LTC2944-Arduino-Library.git dfrobot/DFRobot_DF2301Q@^1.0.0 build_flags = @@ -110,7 +110,7 @@ board_build.partitions = huge_app.csv build_flags = -D WROOM32_MODULE ${common:ESP32-wifi.build_flags} ${common:ESP32-bluetooth.build_flags} - -D DEBUG_BUILD=0 -D BUILD_TEMP=1 -D BUILD_DISPLAY=1 -D BLUETOOTH_TCODE=0 -D BLE_TCODE=1 -D WIFI_TCODE=1 -D MOTOR_TYPE=0 -D SECURE_WEB=0 -D COEXIST=1 #-D CORE_DEBUG_LEVEL=5 #-D FW_VERSION=%%date%% + -D DEBUG_BUILD=0 -D BUILD_TEMP=1 -D BUILD_DISPLAY=1 -D BLUETOOTH_TCODE=0 -D BLE_TCODE=1 -D WIFI_TCODE=1 -D MOTOR_TYPE=0 -D SECURE_WEB=0 -D COEXIST=1 -D DEFAULT_DEVICE=DeviceType::OSR -D DEFAULT_BOARD=BoardType::DEVKIT #-D CORE_DEBUG_LEVEL=5 #-D FW_VERSION=%%date%% lib_deps = ${common:ESP32.lib_deps} ${common:ESP32-wifi.lib_deps} ${common:display.lib_deps} @@ -121,7 +121,7 @@ lib_deps = ${common:ESP32.lib_deps} extends = env:esp32doit-devkit-v1 build_unflags = -D MOTOR_TYPE=0 build_flags = ${env:esp32doit-devkit-v1.build_flags} - -D MOTOR_TYPE=1 + -D MOTOR_TYPE=1 -D DEFAULT_DEVICE=DeviceType::SR1 -D DEFAULT_BOARD=BoardType::DEVKIT lib_deps = ${env:esp32doit-devkit-v1.lib_deps} ${common:bldc.lib_deps} lib_archive = false #required for SimpleFOC @@ -223,6 +223,9 @@ upload_protocol = cmsis-dap [env:ssr1_pcb] extends = common:ESP32, common:ESP32-wifi board = ssr1pcb +board_build.variants_dir = ./boards/variants +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip +framework = arduino build_type = release board_build.partitions = huge_app.csv build_flags = -D WROOM32_MODULE @@ -238,7 +241,9 @@ build_flags = -D WROOM32_MODULE -D SECURE_WEB=0 -D COEXIST=1 -D DEFAULT_BOARD=BoardType::SSR1PCB -lib_deps = ${common:ESP32.lib_deps} + -D DEFAULT_DEVICE=DeviceType::BLDC +lib_deps = ${common.lib_deps} + ${common:ESP32.lib_deps} ${common:ESP32-wifi.lib_deps} ${common:display.lib_deps} ${common:temperature.lib_deps} @@ -248,10 +253,15 @@ lib_archive = false #required for SimpleFOC [env:sr6_pcb] extends = common:ESP32, common:ESP32-wifi -board = ssr1pcb +board = sr6pcb +board_build.variants_dir = ./boards/variants +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip +framework = arduino build_type = release board_build.partitions = huge_app.csv build_flags = -D WROOM32_MODULE + ${common.build_flags} + ${common:ESP32.build_flags} ${common:ESP32-wifi.build_flags} ${common:ESP32-bluetooth.build_flags} -D DEBUG_BUILD=0 @@ -264,7 +274,9 @@ build_flags = -D WROOM32_MODULE -D SECURE_WEB=0 -D COEXIST=1 -D DEFAULT_BOARD=BoardType::SR6PCB -lib_deps = ${common:ESP32.lib_deps} + -D DEFAULT_DEVICE=DeviceType::SR6 +lib_deps = ${common.lib_deps} + ${common:ESP32.lib_deps} ${common:ESP32-wifi.lib_deps} ${common:display.lib_deps} ${common:temperature.lib_deps} diff --git a/ESP32/src/HTTP/HTTPSHandler.hpp b/ESP32/src/HTTP/HTTPSHandler.hpp index a1e3514..d91adff 100644 --- a/ESP32/src/HTTP/HTTPSHandler.hpp +++ b/ESP32/src/HTTP/HTTPSHandler.hpp @@ -17,7 +17,7 @@ #include "HTTPBase.h" #include "SecureWebSocketHandler.hpp" -#include "SettingsHandler.h" +#include "settings/SettingsHandler.h" #include "TagHandler.h" #include "LogHandler.h" #include "SystemCommandHandler.h" diff --git a/ESP32/src/HTTP/SecureWebSocketHandler.hpp b/ESP32/src/HTTP/SecureWebSocketHandler.hpp index 73fc841..20e8a61 100644 --- a/ESP32/src/HTTP/SecureWebSocketHandler.hpp +++ b/ESP32/src/HTTP/SecureWebSocketHandler.hpp @@ -4,10 +4,10 @@ #include #include #include "WebSocketBase.h" -#include "SettingsHandler.h" -#include "LogHandler.h" -#include "TagHandler.h" -#include "BatteryHandler.h" +#include "settings/SettingsHandler.h" +#include "logging/LogHandler.h" +#include "logging/TagHandler.h" +#include "sensors/BatteryHandler.h" #include "SecureWebSocketClient.hpp" diff --git a/ESP32/src/HTTP/WebSocketBase.h b/ESP32/src/HTTP/WebSocketBase.h index 0186f76..e1ab7d2 100644 --- a/ESP32/src/HTTP/WebSocketBase.h +++ b/ESP32/src/HTTP/WebSocketBase.h @@ -3,7 +3,7 @@ #include #include // #include "LogHandler.h" -#include "BatteryHandler.h" +#include "sensors/BatteryHandler.h" class WebSocketBase { public: @@ -11,23 +11,23 @@ class WebSocketBase { virtual void sendCommand(const char* command, const char* message = 0) = 0; virtual void closeAll() = 0; - void getTCode(char* webSocketData) + void getTCode(char* webSocketData) { if(!tCodeInQueue || tCodeInQueue == NULL) { if(millis() >= lastMessage + messageLimit) { lastMessage = millis(); - LogHandler::error(_TAG, "TCode queue was null"); + LogHandler::error(Tags::WebSocketServer, "TCode queue was null"); } return; - } - if(xQueueReceive(tCodeInQueue, webSocketData, 0)) + } + if (xQueueReceive(tCodeInQueue, webSocketData, 0)) { //tcode->toCharArray(webSocketData, tcode->length() + 1); // Serial.print("Top tcode: "); // Serial.println(webSocketData); } - else + else { webSocketData[0] = {0}; } @@ -52,18 +52,18 @@ class WebSocketBase { else sprintf(buf, "{ \"command\": \"%s\" , \"message\": \"%s\" }", command, message); } - void processWebSocketTextMessage(const char* msg) + void processWebSocketTextMessage(const char* msg) { - if(strpbrk(msg, "{") == nullptr) + if (strpbrk(msg, "{") == nullptr) { if(!tCodeInQueue || tCodeInQueue == NULL) { - LogHandler::error(_TAG, "TCode queue was null"); - } - else + LogHandler::error(Tags::WebSocketServer, "TCode queue was null"); + } + else { - - LogHandler::verbose(_TAG, "Websocket tcode in: %s", msg); + + LogHandler::verbose(Tags::WebSocketServer, "Websocket tcode in: %s", msg); xQueueSend(tCodeInQueue, msg, 0); // Serial.print("Time between ws calls: "); // Serial.println(millis() - lastCall); @@ -71,7 +71,7 @@ class WebSocketBase { // lastCall = millis(); //executeTCode(msg); } - // if (strcmp(msg, SettingsHandler::HandShakeChannel) == 0) + // if (strcmp(msg, SettingsHandler::HandShakeChannel) == 0) // { // sendCommand(SettingsHandler::TCodeVersionName); // } @@ -80,14 +80,14 @@ class WebSocketBase { { JsonDocument doc; //255 DeserializationError error = deserializeJson(doc, msg); - if (error) + if (error) { - LogHandler::error(_TAG, "Failed to read websocket json"); + LogHandler::error(Tags::WebSocketServer, "Failed to read websocket json"); return; } JsonObject jsonObj = doc.as(); - if(!jsonObj["command"].isNull()) + if (!jsonObj["command"].isNull()) { String command = jsonObj["command"].as(); String message = jsonObj["message"].as(); @@ -99,10 +99,10 @@ class WebSocketBase { // Serial.println(message->c_str()); // if(tCodeInQueue == NULL)return; // xQueueSend(tCodeInQueue, &message, 0); - } - // else + } + // else // { - // LogHandler::verbose(_TAG, "Websocket tcode in JSON: %s", msg); + // LogHandler::verbose(Tags::WebSocketServer, "Websocket tcode in JSON: %s", msg); // char tcode[MAX_COMMAND]; // SettingsHandler::processTCodeJson(tcode, msg); // // Serial.print("tcode JSON converted:"); @@ -113,7 +113,6 @@ class WebSocketBase { } private: - const char* _TAG = "webSocket-base"; // std::mutex serial_mtx; // static QueueHandle_t debugInQueue; // static TaskHandle_t* emptyQueueHandle; diff --git a/ESP32/src/MDNSHandler.hpp b/ESP32/src/MDNSHandler.hpp index d2b051c..d953240 100644 --- a/ESP32/src/MDNSHandler.hpp +++ b/ESP32/src/MDNSHandler.hpp @@ -30,7 +30,7 @@ SOFTWARE. */ #else #include #endif -#include "SettingsHandler.h" +#include "settings/SettingsHandler.h" // #include "LogHandler.h" class MDNSHandler { public: diff --git a/ESP32/src/Motion/MotionGenerator.hpp b/ESP32/src/Motion/MotionGenerator.hpp index e369383..6b388df 100644 --- a/ESP32/src/Motion/MotionGenerator.hpp +++ b/ESP32/src/Motion/MotionGenerator.hpp @@ -24,7 +24,7 @@ SOFTWARE. */ #pragma once #include -#include "SettingsHandler.h" +#include "settings/SettingsHandler.h" // #include "LogHandler.h" #include "TagHandler.h" diff --git a/ESP32/src/Motion/MotionHandler.hpp b/ESP32/src/Motion/MotionHandler.hpp index 214046f..917085e 100644 --- a/ESP32/src/Motion/MotionHandler.hpp +++ b/ESP32/src/Motion/MotionHandler.hpp @@ -2,7 +2,7 @@ #include #include -#include "SettingsHandler.h" +#include "settings/SettingsHandler.h" // #include "LogHandler.h" #include "TagHandler.h" #include "MotionGenerator.hpp" diff --git a/ESP32/src/PowerHandler.h b/ESP32/src/PowerHandler.h new file mode 100644 index 0000000..a1b08a2 --- /dev/null +++ b/ESP32/src/PowerHandler.h @@ -0,0 +1,46 @@ +#ifndef POWER_HANDLER_H +#define POWER_HANDLER_H + +#include + +class PowerHandler { + public: + PowerHandler() { + m_settingsFactory = SettingsFactory::getInstance(); + + } + + bool setup() { + Wire.begin(); + if (!SettingsHandler::waitForI2CDevices(MCP4018_ADDRESS)) { + LogHandler::error(_TAG, "MCP4018 not found on I2C bus."); + return false; + } + long timeout = millis() + 10000; + while(!mcp4018.begin()) { + LogHandler::error(_TAG, "Failed to initialize MCP4018. Retrying..."); + vTaskDelay(1000 / portTICK_PERIOD_MS); + if(millis() > timeout) { + LogHandler::error(_TAG, "Detecting MCP4018 timed out. Exit."); + return false; + } + } + return true; + } + + void startLoop() + { + while (true) + { + mcp4018. + vTaskDelay(1000 / portTICK_PERIOD_MS); + } + } + private: + SettingsFactory* m_settingsFactory; + static MCP4018 mcp4018(I2C1); + static uint16_t enable_pin = 13; + static uint16_t pd_config_pins[3] = {27, 14, 12}; +}; + +#endif // POWER_HANDLER_H \ No newline at end of file diff --git a/ESP32/src/SystemCommandHandler.h b/ESP32/src/SystemCommandHandler.h index f13086f..a09d814 100644 --- a/ESP32/src/SystemCommandHandler.h +++ b/ESP32/src/SystemCommandHandler.h @@ -24,25 +24,31 @@ SOFTWARE. */ #pragma once #include -#include "SettingsHandler.h" +#if ESP8266 == 1 +#include +#else +#include +#endif +#include "settings/SettingsHandler.h" #include "utils.h" -#include "TagHandler.h" +#include "logging/LogHandler.h" +#include "logging/TagHandler.h" #include "struct/command.hpp" #include "settingsFactory.h" class SystemCommandHandler { -public: +public: SystemCommandHandler() { tCodeQueue = xQueueCreate(10, sizeof(char[MAX_COMMAND])); if(tCodeQueue == NULL) { - LogHandler::error(_TAG, "Error creating the tcode queue"); + LogHandler::error(Tags::SystemCommand, "Error creating the tcode queue"); } m_settingsFactory = SettingsFactory::getInstance(); } bool process(const char* in) { xSemaphoreTake(xMutex, portMAX_DELAY); if(isSaveCommand(in)) { - LogHandler::debug(_TAG, "Enter process save command: %s", in); + LogHandler::debug(Tags::SystemCommand, "Enter process save command: %s", in); for(Command command : saveCommands) { if(match(in, command.command)) { command.callback(); @@ -51,12 +57,12 @@ class SystemCommandHandler { } } - LogHandler::error(_TAG, "Unknown save command: %s\n", in); + LogHandler::error(Tags::SystemCommand, "Unknown save command: %s\n", in); xSemaphoreGive(xMutex); return false; } else if(isOtherCommand(in)) { - LogHandler::debug(_TAG, "Enter process other command: %s", in); + LogHandler::debug(Tags::SystemCommand, "Enter process other command: %s", in); for(auto command : commands) { if(match(in, command.command)) { @@ -74,10 +80,10 @@ class SystemCommandHandler { } } CommandValuePair valuePair; - if(!getCommandValue(in, valuePair)) + if(!getCommandValue(in, valuePair)) return false; - LogHandler::debug(_TAG, "Value command: %s:%s", valuePair.command, valuePair.value); + LogHandler::debug(Tags::SystemCommand, "Value command: %s:%s", valuePair.command, valuePair.value); for(auto command : commandCharValues) { if(match(valuePair.command, command.command)) { command.callback(valuePair.value); @@ -98,7 +104,7 @@ class SystemCommandHandler { } } - LogHandler::error(_TAG, "Unknown command: %s", in); + LogHandler::error(Tags::SystemCommand, "Unknown command: %s", in); xSemaphoreGive(xMutex); return false; } @@ -117,22 +123,22 @@ class SystemCommandHandler { // } // if(!button->isPressed()) {// Filter out other commands button release event for now. // strlcpy(buf, button->command, MAX_COMMAND); - // } + // } // return false; // } /// @brief This function is mainly for concatenating the button state for commands sent externally. - /// @param button - /// @param buf + /// @param button + /// @param buf /// @return true if any commands where added. bool process(ButtonModel* button, char buf[MAX_COMMAND]) { - LogHandler::debug(_TAG, "Enter process button command: %s", button->command); + LogHandler::debug(Tags::SystemCommand, "Enter process button command: %s", button->command); char temp[MAX_COMMAND]; strlcpy(temp, button->command, MAX_COMMAND); char *token = strtok(temp, " ");//Split incoming at TCode delemiter "space" buf[0] = {0}; while( token != NULL ) {// Specify if the button is pressed or released only for externaly sent commands. - LogHandler::debug(_TAG, "Searching command: %s", token); + LogHandler::debug(Tags::SystemCommand, "Searching command: %s", token); bool externalFound = false; for(auto command : commandExternal) { if(match(command.command, token)) { @@ -150,7 +156,7 @@ class SystemCommandHandler { } if(strlen(buf)) { strcat(buf, "\n"); - LogHandler::debug(_TAG, "Finish process button command: %s", buf); + LogHandler::debug(Tags::SystemCommand, "Finish process button command: %s", buf); return true; } return false; @@ -160,7 +166,7 @@ class SystemCommandHandler { return isSaveCommand(in) || isOtherCommand(in); //return strpbrk(DELEMITER_SAVE, in) != nullptr || strpbrk(DELEMITER, in) != nullptr; } - + bool isSaveCommand(const char* in) { return startsWith(in, DELEMITER_SAVE); } @@ -171,21 +177,21 @@ class SystemCommandHandler { bool isValueCommand(const char* in) { return contains(in, (&DELEMITER_VALUE)); } - + bool isSettingCommand(const char* in) { return startsWith(in, DELEMITER); } - + void registerExternalCommandCallback(std::function callback) { m_externalCommandCallback = callback; } - bool getTCode(char* buf) + bool getTCode(char* buf) { if(!tCodeQueue) { return false; - } + } if(!xQueueReceive(tCodeQueue, buf, 0)) { buf[0] = {0}; return false; @@ -193,17 +199,16 @@ class SystemCommandHandler { return true; } -private: +private: SemaphoreHandle_t xMutex = xSemaphoreCreateMutex(); QueueHandle_t tCodeQueue; - const char* _TAG = TagHandler::SystemCommandHandler; SettingsFactory* m_settingsFactory; std::function m_externalCommandCallback = 0; std::function m_otherCommandCallback = 0; const char* DELEMITER = "#"; - const char* DELEMITER_SAVE = "$"; - const char DELEMITER_VALUE = ':'; + const char* DELEMITER_SAVE = "$"; + const char DELEMITER_VALUE = ':'; const Command HELP{{"Help", "#help", "Print the help screen", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { return execute([this]() -> bool { printCommandHelp(); @@ -232,10 +237,10 @@ class SystemCommandHandler { const Command DEFAULT_ALL{{"Default all", "$defaultAll", "Saves all settings to default", SaveRequired::NO, RestartRequired::YES, SettingType::NONE}, [this]() -> bool { return execute([this]() -> bool { if(!m_settingsFactory->resetAll()) { - LogHandler::error(_TAG, "Error resetting all to default"); + LogHandler::error(Tags::SystemCommand, "Error resetting all to default"); return false; } - LogHandler::info(_TAG, "All settings reset to default!"); + LogHandler::info(Tags::SystemCommand, "All settings reset to default!"); return true; }, SaveRequired::NO, RestartRequired::YES); }}; @@ -245,31 +250,38 @@ class SystemCommandHandler { return true; }); }}; + const Command PRINT_IP{ {"Print IP", "#ip", "Print current STA or AP IP address", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { + return execute([]() -> bool { + IPAddress ip = (WiFi.isConnected()) ? WiFi.localIP() : WiFi.softAPIP(); + Serial.printf("IP Address: %s\n", ip.toString().c_str()); + return true; + }); + } }; const Command CLEAR_LOGS_INCLUDE{{"Clear log include", "#clear-log-include", "Clears all the log included tags", SaveRequired::YES, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { return execute([this]() -> bool { LogHandler::clearIncludes(); - LogHandler::debug(_TAG, "Tags cleared"); - return m_settingsFactory->setValue(LOG_INCLUDETAGS, LogHandler::getIncludes()) != SettingFile::NONE; + LogHandler::debug(Tags::SystemCommand, "Tags cleared"); + return m_settingsFactory->setValue(LOG_INCLUDETAGS, "") != SettingFile::NONE; }, SaveRequired::YES); }}; const Command CLEAR_LOGS_EXCLUDE{{"Clear log exclude", "#clear-log-exclude", "Clears all the log excluded tags", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { return execute([this]() -> bool { LogHandler::clearExcludes(); - LogHandler::debug(_TAG, "Filters cleared"); - return m_settingsFactory->setValue(LOG_EXCLUDETAGS, LogHandler::getExcludes()) != SettingFile::NONE; + LogHandler::debug(Tags::SystemCommand, "Filters cleared"); + return m_settingsFactory->setValue(LOG_EXCLUDETAGS, "") != SettingFile::NONE; }, SaveRequired::NO); }}; const Command CHANNEL_RANGES_ENABLE{{"Channel ranges enable", "#channel-ranges-enable", "Enables the channel range limits temporarily", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { return validateBool("Channel ranges", true, SettingsHandler::getChannelRangesEnabled(), [this](bool value) -> bool { SettingsHandler::setChannelRangesEnabled(true); - LogHandler::debug(_TAG, "Channel ranges enabled"); + LogHandler::debug(Tags::SystemCommand, "Channel ranges enabled"); return true; }); }}; const Command CHANNEL_RANGES_DISABLE{{"Channel ranges disable", "#channel-ranges-disable", "Disables the channel range limits temporarily", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { return validateBool("Channel ranges", false, SettingsHandler::getChannelRangesEnabled(), [this](bool value) -> bool { SettingsHandler::setChannelRangesEnabled(false); - LogHandler::debug(_TAG, "Channel ranges disabled"); + LogHandler::debug(Tags::SystemCommand, "Channel ranges disabled"); return true; }); }}; @@ -277,21 +289,21 @@ class SystemCommandHandler { return execute([this]() -> bool { bool enabled = SettingsHandler::getChannelRangesEnabled(); SettingsHandler::setChannelRangesEnabled(!enabled); - LogHandler::debug(_TAG, !enabled ? "Channel ranges enabled" : "Channel ranges disabled"); + LogHandler::debug(Tags::SystemCommand, !enabled ? "Channel ranges enabled" : "Channel ranges disabled"); return true; }); }}; const Command MOTION_ENABLE{{"Motion enable", "#motion-enable", "Enables the motion generator", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { return validateBool("Motion", true, SettingsHandler::getMotionEnabled(), [this](bool value) -> bool { SettingsHandler::setMotionEnabled(value); - LogHandler::debug(_TAG, "Motion enabled"); + LogHandler::debug(Tags::SystemCommand, "Motion enabled"); return true; }); }}; const Command MOTION_DISABLE{{"Motion disable", "#motion-disable", "Disables the motion generator", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { return validateBool("Motion", false, SettingsHandler::getMotionEnabled(), [this](bool value) -> bool { SettingsHandler::setMotionEnabled(value); - LogHandler::debug(_TAG, "Motion disabled"); + LogHandler::debug(Tags::SystemCommand, "Motion disabled"); writeTCode("DSTOP\n"); return true; }); @@ -300,7 +312,7 @@ class SystemCommandHandler { return execute([this]() -> bool { bool enabled = SettingsHandler::getMotionEnabled(); SettingsHandler::setMotionEnabled(!enabled); - LogHandler::debug(_TAG, !enabled ? "Motion enabled" : "Motion disabled"); + LogHandler::debug(Tags::SystemCommand, !enabled ? "Motion enabled" : "Motion disabled"); if(!enabled) { writeTCode("DSTOP\n"); } @@ -310,7 +322,7 @@ class SystemCommandHandler { const Command MOTION_HOME{{"Motion home", "#device-home", "Sends all axis' to its home position", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { char buf[MAX_COMMAND]; SettingsHandler::channelMap.tCodeHome(buf); - LogHandler::debug(_TAG, "Device home: %s", buf); + LogHandler::debug(Tags::SystemCommand, "Device home: %s", buf); writeTCode(buf); return true; }}; @@ -320,26 +332,26 @@ class SystemCommandHandler { return true; }); }}; - const Command PAUSE{{"Pause", "#pause", "Pauses all motion of the device", SaveRequired::YES, RestartRequired::YES, SettingType::NONE}, [this]() -> bool { + const Command PAUSE{{"Pause", "#pause", "Pauses all motion of the device", SaveRequired::YES, RestartRequired::YES, SettingType::NONE}, [this]() -> bool { return execute([this]() -> bool { SettingsHandler::motionPaused = true; - LogHandler::debug(_TAG, "Device paused"); + LogHandler::debug(Tags::SystemCommand, "Device paused"); return true; }); return true; }}; - const Command RESUME{{"Resume", "#resume", "Resumes all motion of the device", SaveRequired::YES, RestartRequired::YES, SettingType::NONE}, [this]() -> bool { + const Command RESUME{{"Resume", "#resume", "Resumes all motion of the device", SaveRequired::YES, RestartRequired::YES, SettingType::NONE}, [this]() -> bool { return execute([this]() -> bool { SettingsHandler::motionPaused = false; - LogHandler::debug(_TAG, "Device resumed"); + LogHandler::debug(Tags::SystemCommand, "Device resumed"); return true; }); return true; }}; - const Command PAUSE_TOGGLE{{"Pause toggle", "#pause-toggle", "Pauses all motion of the device", SaveRequired::YES, RestartRequired::YES, SettingType::NONE}, [this]() -> bool { + const Command PAUSE_TOGGLE{{"Pause toggle", "#pause-toggle", "Pauses all motion of the device", SaveRequired::YES, RestartRequired::YES, SettingType::NONE}, [this]() -> bool { return execute([this]() -> bool { SettingsHandler::motionPaused = !SettingsHandler::motionPaused; - LogHandler::debug(_TAG, SettingsHandler::motionPaused ? "Device paused" : "Device resumed"); + LogHandler::debug(Tags::SystemCommand, SettingsHandler::motionPaused ? "Device paused" : "Device resumed"); return true; }); return true; @@ -347,7 +359,7 @@ class SystemCommandHandler { const CommandValue MOTION_HOME_SPEED{{"Motion home", "#device-home", "Sends all axis' to its home position at specified speed (S)", SaveRequired::NO, RestartRequired::NO, SettingType::Number}, [this](const int value) -> bool { char buf[MAX_COMMAND]; SettingsHandler::channelMap.tCodeHome(buf, value); - LogHandler::debug(_TAG, "Device home speed: %s", buf); + LogHandler::debug(Tags::SystemCommand, "Device home speed: %s", buf); writeTCode(buf); return true; }}; @@ -356,14 +368,14 @@ class SystemCommandHandler { m_settingsFactory->setValue(SSID_SETTING, value); //strcpy(SettingsHandler::ssid, value); return true; - }, SaveRequired::YES, RestartRequired::YES); + }, SaveRequired::YES, RestartRequired::YES); }}; const CommandValueWIFI_PASS{{"Wifi pass", "#wifi-pass", "Sets the password of the wifi AP", SaveRequired::YES, RestartRequired::YES, SettingType::String}, [this](const char* value) -> bool { return validateMaxLength("Wifi password", value, WIFI_PASS_LEN, true, [this](const char* value) -> bool { m_settingsFactory->setValue(WIFI_PASS_SETTING, value); //strcpy(SettingsHandler::wifiPass, value); return true; - }, SaveRequired::YES, RestartRequired::YES); + }, SaveRequired::YES, RestartRequired::YES); }}; const CommandValueBOARD_TYPE{{"Board type", "#board-type", BOARD_TYPES_HELP, SaveRequired::YES, RestartRequired::YES, SettingType::Number}, [this](const int value) -> bool { return executeValue(value, [this](const int value) -> bool { @@ -378,7 +390,7 @@ class SystemCommandHandler { const CommandValueLOG_LEVEL{{"Log level", "#log-level", LOG_LEVEL_HELP, SaveRequired::YES, RestartRequired::NO, SettingType::Number}, [this](const int value) -> bool { return executeValue(value, [this](const int value) -> bool { if(value > (int)LogLevel::VERBOSE || value < 0) { - LogHandler::error(_TAG, "Invalid value: %ld. Valid log levels are %s", value, LOG_LEVEL_HELP); + LogHandler::error(Tags::SystemCommand, "Invalid value: %ld. Valid log levels are %s", value, LOG_LEVEL_HELP); return false; } return m_settingsFactory->setValue(LOG_LEVEL_SETTING, value) != SettingFile::NONE; @@ -386,55 +398,50 @@ class SystemCommandHandler { }}; const CommandValueADD_LOG_INCLUDE{{"Add log include", "#add-log-include", "Adds a tag to the log includes", SaveRequired::YES, RestartRequired::NO, SettingType::String}, [this](const char* value) -> bool { return executeValue(value, [this](const char* value) -> bool { - if(!TagHandler::HasTag(value)) { - LogHandler::error(_TAG, "Invalid value: %s", value); - return false; - } - if(!LogHandler::addInclude(value)) { - LogHandler::error(_TAG, "Tag already exists: %s", value); + uint32_t tag_masks = 0; + if (Tags::from_str(value, tag_masks)) + { + LogHandler::error(Tags::SystemCommand, "Invalid value(s): %s", value); return false; } - - return m_settingsFactory->setValue(LOG_INCLUDETAGS, LogHandler::getIncludes()) != SettingFile::NONE; + LogHandler::addIncludes(tag_masks); + return m_settingsFactory->setValue(LOG_INCLUDETAGS, Tags::as_str(LogHandler::getIncludes()).c_str()) != SettingFile::NONE; }, SaveRequired::YES); }}; const CommandValueREMOVE_LOG_INCLUDE{{"Remove log include", "#remove-log-include", "Removes a tag from the log includes", SaveRequired::YES, RestartRequired::NO, SettingType::String}, [this](const char* value) -> bool { return executeValue(value, [this](const char* value) -> bool { - if(!TagHandler::HasTag(value)) { - LogHandler::error(_TAG, "Invalid value: %s", value); - return false; - } - if(!LogHandler::removeInclude(value)) { - LogHandler::error(_TAG, "Tag did not exist: %s", value); + uint32_t tag_masks = 0; + if (Tags::from_str(value, tag_masks)) + { + LogHandler::error(Tags::SystemCommand, "Invalid value(s): %s", value); return false; } - return m_settingsFactory->setValue(LOG_INCLUDETAGS, LogHandler::getIncludes()) != SettingFile::NONE; + LogHandler::removeIncludes(tag_masks); + return m_settingsFactory->setValue(LOG_INCLUDETAGS, Tags::as_str(LogHandler::getIncludes()).c_str()) != SettingFile::NONE; }, SaveRequired::YES); }}; const CommandValueADD_LOG_EXCLUDE{{"Add log exclude", "#add-log-exclude", "Adds a tag to the log excludes", SaveRequired::YES, RestartRequired::NO, SettingType::String}, [this](const char* value) -> bool { return executeValue(value, [this](const char* value) -> bool { - if(!TagHandler::HasTag(value)) { - LogHandler::error(_TAG, "Invalid value: %s", value); - return false; - } - if(!LogHandler::addExclude(value)) { - LogHandler::error(_TAG, "Tag filter already exists: %s", value); + uint32_t tag_masks = 0; + if (Tags::from_str(value, tag_masks)) + { + LogHandler::error(Tags::SystemCommand, "Invalid value(s): %s", value); return false; } - return m_settingsFactory->setValue(LOG_EXCLUDETAGS, LogHandler::getExcludes()) != SettingFile::NONE; + LogHandler::addExcludes(tag_masks); + return m_settingsFactory->setValue(LOG_EXCLUDETAGS, Tags::as_str(LogHandler::getExcludes()).c_str()) != SettingFile::NONE; }, SaveRequired::YES); }}; const CommandValueREMOVE_LOG_EXCLUDE{{"Remove log exclude", "#remove-log-exclude", "Removes a tag from the log excludes", SaveRequired::YES, RestartRequired::NO, SettingType::String}, [this](const char* value) -> bool { return executeValue(value, [this](const char* value) -> bool { - if(!TagHandler::HasTag(value)) { - LogHandler::error(_TAG, "Invalid value: %s", value); - return false; - } - if(!LogHandler::removeExclude(value)) { - LogHandler::error(_TAG, "Tag filter did not exist: %s", value); + uint32_t tag_masks = 0; + if (Tags::from_str(value, tag_masks)) + { + LogHandler::error(Tags::SystemCommand, "Invalid value(s): %s", value); return false; } - return m_settingsFactory->setValue(LOG_EXCLUDETAGS, LogHandler::getExcludes()) != SettingFile::NONE; + LogHandler::removeExclude(tag_masks); + return m_settingsFactory->setValue(LOG_EXCLUDETAGS, Tags::as_str(LogHandler::getExcludes()).c_str()) != SettingFile::NONE; }, SaveRequired::YES); }}; const CommandValueMOTION_PROFILE_NAME{{"Motion profile set by name", "#motion-profile-name", "Sets the current running profile by name", SaveRequired::NO, RestartRequired::NO, SettingType::String}, [this](const char* value) -> bool { @@ -447,7 +454,7 @@ class SystemCommandHandler { return validateGreaterThanZero("Motion profile", value, [this](int value) -> bool { int profileAsIndex = value - 1; if(profileAsIndex > MAX_MOTION_PROFILE_COUNT) { - LogHandler::error(_TAG, "Motion profile %ld does not exist", profileAsIndex); + LogHandler::error(Tags::SystemCommand, "Motion profile %ld does not exist", profileAsIndex); return false; } SettingsHandler::setMotionProfile(profileAsIndex); @@ -478,17 +485,17 @@ class SystemCommandHandler { } return true; }}; - const CommandValue SETTING{{"Setting", "#setting", "Modify a setting ex. #setting::", SaveRequired::YES, RestartRequired::NO, SettingType::String}, [this](const char* value) -> bool { + const CommandValue SETTING{{"Setting", "#setting", "Modify a setting ex. #setting::", SaveRequired::YES, RestartRequired::NO, SettingType::String}, [this](const char* value) -> bool { CommandValuePair valuePair; - if(!getCommandValue(value, valuePair)) + if(!getCommandValue(value, valuePair)) return false; - const Setting* setting = m_settingsFactory->getSetting(valuePair.command); + const Setting* setting = m_settingsFactory->getSetting(valuePair.command); if(!setting) { return false; } return executeValue(value, [this, setting, valuePair](const char* value) -> bool { - - LogHandler::debug(_TAG, "Searching for setting command '%s' value: '%s'", valuePair.command, valuePair.value); + + LogHandler::debug(Tags::SystemCommand, "Searching for setting command '%s' value: '%s'", valuePair.command, valuePair.value); bool error = false; switch(setting->type) { case SettingType::String: { @@ -504,7 +511,7 @@ class SystemCommandHandler { if(m_settingsFactory->setValue(setting->name, value) != SettingFile::NONE) { return true; } - LogHandler::debug(_TAG, "value: %d", value); + LogHandler::debug(Tags::SystemCommand, "value: %d", value); } break; case SettingType::Float: { @@ -514,7 +521,7 @@ class SystemCommandHandler { if(m_settingsFactory->setValue(setting->name, value) != SettingFile::NONE) { return true; } - LogHandler::debug(_TAG, "value: %f", value); + LogHandler::debug(Tags::SystemCommand, "value: %f", value); } break; case SettingType::Double: { @@ -524,7 +531,7 @@ class SystemCommandHandler { if(m_settingsFactory->setValue(setting->name, value) != SettingFile::NONE) { return true; } - LogHandler::debug(_TAG, "value: %f", value); + LogHandler::debug(Tags::SystemCommand, "value: %f", value); } break; case SettingType::Boolean: { @@ -534,7 +541,7 @@ class SystemCommandHandler { if(m_settingsFactory->setValue(setting->name, value) != SettingFile::NONE) { return true; } - LogHandler::debug(_TAG, "value: %d", value); + LogHandler::debug(Tags::SystemCommand, "value: %d", value); } break; case SettingType::ArrayString: { @@ -544,23 +551,24 @@ class SystemCommandHandler { } break; default: - LogHandler::error(_TAG, "Invalid setting type: %ld", (int)setting->type); + LogHandler::error(Tags::SystemCommand, "Invalid setting type: %ld", (int)setting->type); } return false; }, SaveRequired::YES, setting->isRestartRequired); }}; - + Command saveCommands[2] { SAVE, DEFAULT_ALL, }; - Command commands[17] = { + Command commands[18] = { HELP, AVAILABLE_SETTINGS, PRINT_MEMORY, RESTART, + PRINT_IP, CLEAR_LOGS_INCLUDE, CLEAR_LOGS_EXCLUDE, CHANNEL_RANGES_ENABLE, @@ -609,7 +617,7 @@ class SystemCommandHandler { // void setupSettingsCommands() { // auto allSettings = m_settingsFactory->AllSettings; - + // for(SettingFileInfo* settingsInfo : allSettings) // { // for(const Setting& setting : settingsInfo->settings) @@ -650,7 +658,7 @@ class SystemCommandHandler { // } // break; // default: - // LogHandler::error(_TAG, "Invalid setting type: %ld", (int)setting.type); + // LogHandler::error(Tags::SystemCommand, "Invalid setting type: %ld", (int)setting.type); // } // } // } @@ -682,20 +690,20 @@ class SystemCommandHandler { // Commands with values int indexofDelim = getposition(in, strlen(in), DELEMITER_VALUE); if(indexofDelim == -1) { - LogHandler::error(_TAG, "Invalid command format: '%s' missing colon, correct format is #:", in); + LogHandler::error(Tags::SystemCommand, "Invalid command format: '%s' missing colon, correct format is #:", in); xSemaphoreGive(xMutex); return false; } const char* commandAlone = substr(in, 0, indexofDelim); if(!strlen(commandAlone)) { - LogHandler::error(_TAG, "Invalid command format: '%s' missing command, correct format is #:", in); + LogHandler::error(Tags::SystemCommand, "Invalid command format: '%s' missing command, correct format is #:", in); xSemaphoreGive(xMutex); return false; } valuePair.command = commandAlone; const char* valueAlone = substr(in, indexofDelim +1, strlen(in)); if(!strlen(valueAlone)) { - LogHandler::error(_TAG, "Invalid command format: '%s' missing value, correct format is #:", in); + LogHandler::error(Tags::SystemCommand, "Invalid command format: '%s' missing value, correct format is #:", in); xSemaphoreGive(xMutex); return false; } @@ -705,7 +713,7 @@ class SystemCommandHandler { int getInt(const char* value, bool &error) { if(!isStringIntegral(value)) { - LogHandler::debug(_TAG, "getInt '%s' not integral", value); + LogHandler::debug(Tags::SystemCommand, "getInt '%s' not integral", value); error = true; return false; } @@ -713,7 +721,7 @@ class SystemCommandHandler { } float getFloat(const char* value, bool &error) { if(!isStringIntegral(value)) { - LogHandler::debug(_TAG, "getFloat '%s' not integral", value); + LogHandler::debug(Tags::SystemCommand, "getFloat '%s' not integral", value); error = true; return false; } @@ -721,7 +729,7 @@ class SystemCommandHandler { } double getDouble(const char* value, bool &error) { if(!isStringIntegral(value)) { - LogHandler::debug(_TAG, "getDouble '%s' not integral", value); + LogHandler::debug(Tags::SystemCommand, "getDouble '%s' not integral", value); error = true; return false; } @@ -736,7 +744,7 @@ class SystemCommandHandler { } uint8_t valueInt = getInt(value, error); if(error) { - LogHandler::debug(_TAG, "getBoolean '%s' not integral", value); + LogHandler::debug(Tags::SystemCommand, "getBoolean '%s' not integral", value); return false; } if(valueInt == 0 || valueInt == 1) @@ -900,7 +908,7 @@ class SystemCommandHandler { char buf[MAX_COMMAND] = {0}; auto allSettings = m_settingsFactory->AllSettings; - + for(SettingFileInfo* settingsInfo : allSettings) { for(const Setting& setting : settingsInfo->settings) diff --git a/ESP32/src/TCode/MotorHandler.h b/ESP32/src/TCode/MotorHandler.h index bce1fbc..ff39e1e 100644 --- a/ESP32/src/TCode/MotorHandler.h +++ b/ESP32/src/TCode/MotorHandler.h @@ -25,7 +25,7 @@ SOFTWARE. */ #include #include "Global.h" #include "TCodeBase.h" -#include "SettingsHandler.h" +#include "settings/SettingsHandler.h" #include "TagHandler.h" class MotorHandler { diff --git a/ESP32/src/TCode/v0.2/ServoHandler0_2.h b/ESP32/src/TCode/v0.2/ServoHandler0_2.h index 1611a7c..a1b62de 100644 --- a/ESP32/src/TCode/v0.2/ServoHandler0_2.h +++ b/ESP32/src/TCode/v0.2/ServoHandler0_2.h @@ -21,7 +21,7 @@ #pragma once #include -#include "SettingsHandler.h" +#include "settings/SettingsHandler.h" #include "ToyComs.h" #include "Global.h" #include "MotorHandler.h" diff --git a/ESP32/src/TCode/v0.3/BLDCHandler0_3.h b/ESP32/src/TCode/v0.3/BLDCHandler0_3.h index 045245b..bf13b9a 100644 --- a/ESP32/src/TCode/v0.3/BLDCHandler0_3.h +++ b/ESP32/src/TCode/v0.3/BLDCHandler0_3.h @@ -22,7 +22,7 @@ #include #include #include "TCode0_3.h" -#include "SettingsHandler.h" +#include "settings/SettingsHandler.h" #include "Global.h" #include "MotorHandler0_3.h" #include "TagHandler.h" diff --git a/ESP32/src/TCode/v0.3/MotorHandler0_3.h b/ESP32/src/TCode/v0.3/MotorHandler0_3.h index c8d1f50..67794c0 100644 --- a/ESP32/src/TCode/v0.3/MotorHandler0_3.h +++ b/ESP32/src/TCode/v0.3/MotorHandler0_3.h @@ -25,7 +25,7 @@ SOFTWARE. */ #include #include "Global.h" #include "TCode0_3.h" -#include "SettingsHandler.h" +#include "settings/SettingsHandler.h" #include "TagHandler.h" #include "logging/LogHandler.h" diff --git a/ESP32/src/TCode/v0.3/ServoHandler0_3.h b/ESP32/src/TCode/v0.3/ServoHandler0_3.h index b13f032..b6e6dc8 100644 --- a/ESP32/src/TCode/v0.3/ServoHandler0_3.h +++ b/ESP32/src/TCode/v0.3/ServoHandler0_3.h @@ -22,7 +22,7 @@ #pragma once #include "TCode0_3.h" -#include "SettingsHandler.h" +#include "settings/SettingsHandler.h" #include "Global.h" #include "MotorHandler0_3.h" #include "TagHandler.h" diff --git a/ESP32/src/TCode/v0.4/BLDCHandler0_4.h b/ESP32/src/TCode/v0.4/BLDCHandler0_4.h index 9d60066..24bdb0e 100644 --- a/ESP32/src/TCode/v0.4/BLDCHandler0_4.h +++ b/ESP32/src/TCode/v0.4/BLDCHandler0_4.h @@ -23,7 +23,7 @@ #include #include "TCode0_4.h" -#include "SettingsHandler.h" +#include "settings/SettingsHandler.h" #include "Global.h" #include "MotorHandler0_4.h" #include "TagHandler.h" diff --git a/ESP32/src/TCode/v0.4/MotorHandler0_4.h b/ESP32/src/TCode/v0.4/MotorHandler0_4.h index 68c3766..e5a4e90 100644 --- a/ESP32/src/TCode/v0.4/MotorHandler0_4.h +++ b/ESP32/src/TCode/v0.4/MotorHandler0_4.h @@ -26,7 +26,7 @@ SOFTWARE. */ #include "Global.h" #include "MotorHandler.h" #include "TCode0_4.h" -#include "SettingsHandler.h" +#include "settings/SettingsHandler.h" #include "TagHandler.h" class MotorHandler0_4: public MotorHandler { diff --git a/ESP32/src/TCode/v0.4/ServoHandler0_4.h b/ESP32/src/TCode/v0.4/ServoHandler0_4.h index f2cb58f..525d8d7 100644 --- a/ESP32/src/TCode/v0.4/ServoHandler0_4.h +++ b/ESP32/src/TCode/v0.4/ServoHandler0_4.h @@ -22,7 +22,7 @@ #pragma once #include "TCode0_4.h" -#include "SettingsHandler.h" +#include "settings/SettingsHandler.h" #include "Global.h" #include "MotorHandler0_4.h" #include "TagHandler.h" diff --git a/ESP32/src/UdpHandler.h b/ESP32/src/UdpHandler.h index e7af2cb..6ab1268 100644 --- a/ESP32/src/UdpHandler.h +++ b/ESP32/src/UdpHandler.h @@ -22,99 +22,120 @@ SOFTWARE. */ #pragma once - #include #include #include -#include "SettingsHandler.h" +#include +#include "settings/SettingsHandler.h" #include "logging/LogHandler.h" #include "TagHandler.h" +#include "tasks/TaskHandler.h" - -class Udphandler +class UdpHandler : public TaskHandler::Task { - public: - bool setup(int localPort) - { - LogHandler::info(_TAG, "Starting UDP on port: %ld", localPort); - if(!m_server.listen(localPort)) +public: + UdpHandler() : Task(TaskHandler::Rates::SLOW) {} + void setup() override + { + // Defer UDP initialization until WiFi/lwip is ready + // Don't initialize here - let loop() handle it on first real call + udpInitialized = false; + m_TCodeQueue = xQueueCreate(25, sizeof(char[MAX_COMMAND])); + SettingsFactory* m_settingsFactory = SettingsFactory::getInstance(); + m_tcodeVersion = m_settingsFactory->getTcodeVersion(); + } + + bool initializeUdp() + { + if (udpInitialized) + return true; + + int localPort = SettingsFactory::getInstance()->getUdpServerPort(); + LogHandler::info(Tags::Udp, "Starting UDP on port: %ld", localPort); + if (!m_udp.listen(localPort)) { - LogHandler::error(_TAG, "UDP Error Listening"); + LogHandler::error(Tags::Udp, "UDP Error Listening"); return false; } - LogHandler::info(_TAG, "UDP Listening"); - SettingsFactory* m_settingsFactory = SettingsFactory::getInstance(); - m_tcodeVersion = m_settingsFactory->getTcodeVersion(); - m_server.onPacket(udpCallback, static_cast(this)); - //m_server.onPacket(udpCallback2); - m_TCodeQueue = xQueueCreate(25, sizeof(char[MAX_COMMAND])); - // if(xTaskCreatePinnedToCore( - // handlerTask,/* Function to implement the task */ - // "UDPTask", /* Name of the task */ - // configMINIMAL_STACK_SIZE*4, /* Stack size in words */ - // static_cast(this), /* Task input parameter */ - // tskIDLE_PRIORITY, /* Priority of the task */ - // &m_task, /* Task handle. */ - // WIFI_TASK_CORE_ID) == pdFALSE) /* Core where the task should run */ - // return; - initialized = true; + LogHandler::info(Tags::Udp, "UDP Listening"); + m_udp.onPacket(udpCallback, static_cast(this)); + udpInitialized = true; return true; - } - - static void udpCallback(void * arg, AsyncUDPPacket& packet) + } + + void loop() override + { + // Lazy-initialize UDP once WiFi/lwip is ready + if (!udpInitialized) + { + // Do not call AsyncUDP until WiFi stack has been enabled. + if (WiFi.getMode() == WIFI_OFF) + { + return; + } + + // Try initialize; if it fails, retry on a later tick. + initializeUdp(); + } + } + + static void udpCallback(void *arg, AsyncUDPPacket &packet) { - Udphandler* udp = static_cast(arg); - //LogHandler::verbose(udp->_TAG, "UDP recieve: %s", packet.data()); + UdpHandler *udp = static_cast(arg); + // LogHandler::verbose(udp->Tags::Udp, "UDP recieve: %s", packet.data()); udp->_lastConnectedPort = packet.remotePort(); udp->_lastConnectedIP = packet.remoteIP(); udp->packetBuffer[0] = {0}; - + memcpy(udp->packetBuffer, packet.data(), packet.length()); - //size_t len = packet.readBytes(udp->packetBuffer, sizeof(packetBuffer)); + // size_t len = packet.readBytes(udp->packetBuffer, sizeof(packetBuffer)); udp->packetBuffer[packet.length()] = '\0'; - if(xQueueSend(udp->m_TCodeQueue, udp->packetBuffer, 0) != pdTRUE) - LogHandler::error(udp->_TAG, "UDP queue full"); + if (xQueueSend(udp->m_TCodeQueue, udp->packetBuffer, 0) != pdTRUE) + LogHandler::error(Tags::Udp, "UDP queue full"); } - void CommandCallback(const char* in) - { //This overwrites the callback for message return - if(initialized && _lastConnectedPort > 0) { - LogHandler::debug(_TAG, "Sending udp to client: %s", in); + void CommandCallback(const char *in) + { // This overwrites the callback for message return + if (udpInitialized && _lastConnectedPort > 0) + { + LogHandler::debug(Tags::Udp, "Sending udp to client: %s", in); int i = 0; AsyncUDPMessage message; while (in[i] != 0) message.write((uint8_t)in[i++]); - m_server.sendTo(message, _lastConnectedIP, _lastConnectedPort); - //m_server.endPacket(); + m_udp.sendTo(message, _lastConnectedIP, _lastConnectedPort); + // m_udp.endPacket(); } } - void read(char* buf) - { - if (!initialized) + void read(char *buf) + { + if (!udpInitialized) { buf[0] = {0}; return; } - if(xQueueReceive(m_TCodeQueue, buf, 0)) { - //LogHandler::verbose(_TAG, "Recieve tcode: %s", buf); - } else { - //LogHandler::error(_TAG, "Failed to read from queue"); - buf[0] = {0}; + if (xQueueReceive(m_TCodeQueue, buf, 0)) + { + // LogHandler::verbose(Tags::Udp, "Recieve tcode: %s", buf); + } + else + { + // LogHandler::error(Tags::Udp, "Failed to read from queue"); + buf[0] = {0}; return; - } - } - - private: - const char* _TAG = TagHandler::UdpHandler; + } + } + +private: TCodeVersion m_tcodeVersion; - TaskHandle_t m_task; - QueueHandle_t m_TCodeQueue; - - AsyncUDP m_server; + TaskHandle_t m_task; + QueueHandle_t m_TCodeQueue; + + AsyncUDP m_udp; IPAddress _lastConnectedIP; int _lastConnectedPort = 0; - bool initialized = false; - char packetBuffer[MAX_COMMAND] = {0}; //buffer to hold incoming packet - char jsonIdentifier[2] = "{"; + bool udpInitialized = false; + char packetBuffer[MAX_COMMAND] = {0}; // buffer to hold incoming packet + char jsonIdentifier[2] = "{"; }; diff --git a/ESP32/src/VoiceHandler.hpp b/ESP32/src/VoiceHandler.hpp index 0b2818a..2c65d47 100644 --- a/ESP32/src/VoiceHandler.hpp +++ b/ESP32/src/VoiceHandler.hpp @@ -25,33 +25,41 @@ SOFTWARE. */ #include #include "DFRobot_DF2301Q.h" #include -// #include "LogHandler.h" -#include "TagHandler.h" +#include "logging/LogHandler.h" +#include "logging/TagHandler.h" +#include "settings/SettingsHandler.h" +#include "tasks/TaskHandler.h" -using VOICE_COMMAND_FUNCTION_PTR_T = void (*)(const char* tcodeCommand); +using VOICE_COMMAND_FUNCTION_PTR_T = void (*)(const char *tcodeCommand); -class VoiceHandler : public Task { - +class VoiceHandler : public TaskHandler::Task +{ public: - bool setup() { - LogHandler::info(_TAG, "Setup voice"); - if(!SettingsHandler::waitForI2CDevices(DF2301Q_I2C_ADDR)) { - return false; + VoiceHandler() : Task(TaskHandler::Rates::ONDEMAND) {} + void setup() override + { + LogHandler::info(Tags::Voice, "Setup voice"); + if (!SettingsHandler::waitForI2CDevices(DF2301Q_I2C_ADDR)) + { + return; } int tries = 0; - while (!asr.begin() && tries <= 3) {// Returns true no matter what right now. - LogHandler::error(_TAG, "Could not connect, trying again."); - if(tries >= 3){ - LogHandler::error(_TAG, "Communication with device failed, please check connection"); - return false; + while (!asr.begin() && tries <= 3) + { // Returns true no matter what right now. + LogHandler::error(Tags::Voice, "Could not connect, trying again."); + if (tries >= 3) + { + LogHandler::error(Tags::Voice, "Communication with device failed, please check connection"); + return; } tries++; this->wait(1000); } _isConnected = true; - LogHandler::info(_TAG, "Begin ok!"); - SettingsFactory* settingsFactory = SettingsFactory::getInstance(); - if(settingsFactory->getVoiceVolume() > 0) { + LogHandler::info(Tags::Voice, "Begin ok!"); + SettingsFactory *settingsFactory = SettingsFactory::getInstance(); + if (settingsFactory->getVoiceVolume() > 0) + { setVolume(settingsFactory->getVoiceVolume()); } setMuteMode(settingsFactory->getVoiceMuted()); @@ -62,11 +70,11 @@ class VoiceHandler : public Task { */ // uint8_t wakeTime = 0; // wakeTime = asr.getWakeTime(); - // LogHandler::info(_TAG, "wakeTime: %ld", wakeTime); - - return true; + // LogHandler::info(Tags::Voice, "wakeTime: %ld", wakeTime); + } - bool isConnected() { + bool isConnected() + { return _isConnected; } @@ -74,112 +82,125 @@ class VoiceHandler : public Task { * @brief Set voice volume * @param value - Volume value(1~7) */ - void setVolume(int value) { + void setVolume(int value) + { asr.setVolume(value); } /** @brief Set mute mode @param value - Mute mode; set value 1: mute, 0: unmute */ - void setMuteMode(bool value) { + void setMuteMode(bool value) + { asr.setMuteMode(value); } /** @brief Set wake-up duration @param value - Wake-up duration (0-255) */ - void setWakeTime(int value) { + void setWakeTime(int value) + { asr.setWakeTime(value); } /** @brief Play the corresponding reply audio according to the ID @param CMDID - command word ID */ - void playByCMDID(int value) { + void playByCMDID(int value) + { asr.playByCMDID(value); } - static void startLoop(void * parameter) { - ((VoiceHandler*)parameter)->loop(); - } - - void setMessageCallback(VOICE_COMMAND_FUNCTION_PTR_T f) { - if (f == nullptr) { - message_callback = 0; - } else { - message_callback = f; - } - } - -private: - const char* _TAG = TagHandler::VoiceHandler; + static void startLoop(void *parameter) + { + ((VoiceHandler *)parameter)->loop(); + } + + void setMessageCallback(VOICE_COMMAND_FUNCTION_PTR_T f) + { + if (f == nullptr) + { + message_callback = 0; + } + else + { + message_callback = f; + } + } - //I2C communication +private: + // I2C communication DFRobot_DF2301Q_I2C asr; - VOICE_COMMAND_FUNCTION_PTR_T message_callback = 0; + VOICE_COMMAND_FUNCTION_PTR_T message_callback = 0; bool _isRunning = false; bool _isConnected = false; - void loop() { /** - @brief Get the ID corresponding to the command word - @return Return the obtained command word ID, returning 0 means no valid ID is obtained - */ - toTCode(asr.getCMDID()); - this->sleep(1000); + void loop() override + { /** +@brief Get the ID corresponding to the command word +@return Return the obtained command word ID, returning 0 means no valid ID is obtained +*/ + toTCode(asr.getCMDID()); + this->sleep(1000); } - void toTCode(uint8_t voiceCommand) { + void toTCode(uint8_t voiceCommand) + { - switch (voiceCommand) { - case 5: - LogHandler::verbose(_TAG, "Custom Command: %ld", voiceCommand); + switch (voiceCommand) + { + case 5: + LogHandler::verbose(Tags::Voice, "Custom Command: %ld", voiceCommand); sendMessage("#motion-enable"); char command[32]; - sprintf(command, "#motion-profile-set:%d", SettingsHandler::getMotionDefaultProfileIndex() +1); + sprintf(command, "#motion-profile-set:%d", SettingsHandler::getMotionDefaultProfileIndex() + 1); sendMessage(command); break; - case 6: - LogHandler::verbose(_TAG, "Custom Command: %d", voiceCommand); + case 6: + LogHandler::verbose(Tags::Voice, "Custom Command: %d", voiceCommand); sendMessage("#motion-enable"); sendMessage("#motion-profile-set:1"); break; - case 7: - LogHandler::verbose(_TAG, "Custom Command: %d", voiceCommand); + case 7: + LogHandler::verbose(Tags::Voice, "Custom Command: %d", voiceCommand); sendMessage("#motion-enable"); sendMessage("#motion-profile-set:2"); break; - case 8: - LogHandler::verbose(_TAG, "Custom Command: %d", voiceCommand); + case 8: + LogHandler::verbose(Tags::Voice, "Custom Command: %d", voiceCommand); sendMessage("#motion-enable"); sendMessage("#motion-profile-set:3"); break; - case 9: - LogHandler::verbose(_TAG, "Custom Command: %d", voiceCommand); + case 9: + LogHandler::verbose(Tags::Voice, "Custom Command: %d", voiceCommand); sendMessage("#motion-enable"); sendMessage("#motion-profile-set:4"); break; - case 10: - LogHandler::verbose(_TAG, "Custom Command: %d", voiceCommand); + case 10: + LogHandler::verbose(Tags::Voice, "Custom Command: %d", voiceCommand); sendMessage("#motion-enable"); sendMessage("#motion-profile-set:5"); break; - case 11: - LogHandler::verbose(_TAG, "Custom Command: %d", voiceCommand); + case 11: + LogHandler::verbose(Tags::Voice, "Custom Command: %d", voiceCommand); sendMessage("#motion-disable"); break; - - default:if (voiceCommand == 1 || voiceCommand == 2) { - LogHandler::verbose(_TAG, "Wakup command: %d", voiceCommand); + + default: + if (voiceCommand == 1 || voiceCommand == 2) + { + LogHandler::verbose(Tags::Voice, "Wakup command: %d", voiceCommand); + } + else if (voiceCommand != 0) + { + LogHandler::verbose(Tags::Voice, "Command not used: %d", voiceCommand); } - else if (voiceCommand != 0) { - LogHandler::verbose(_TAG, "Command not used: %d", voiceCommand); - } } } - void sendMessage(const char* message) { - if(message_callback) + void sendMessage(const char *message) + { + if (message_callback) message_callback(message); } }; - diff --git a/ESP32/src/WebHandler_psychic.h b/ESP32/src/WebHandler_psychic.h index a606423..7a84d47 100644 --- a/ESP32/src/WebHandler_psychic.h +++ b/ESP32/src/WebHandler_psychic.h @@ -32,10 +32,10 @@ SOFTWARE. */ // #endif //#include #include "HTTP/HTTPBase.h" -#include "WifiHandler.h" +#include "network/WifiHandler.h" #include "WebSocketHandler_psychic.h" -#include "TagHandler.h" -#include "SystemCommandHandler.h" +#include "logging/TagHandler.h" +#include "messages/SystemCommandHandler.h" // #if !CONFIG_HTTPD_WS_SUPPORT // #error This example cannot be used unless HTTPD_WS_SUPPORT is enabled in esp-http-server component configuration // #endif diff --git a/ESP32/src/WebSocketHandler_psychic.h b/ESP32/src/WebSocketHandler_psychic.h index 284951c..6069cd6 100644 --- a/ESP32/src/WebSocketHandler_psychic.h +++ b/ESP32/src/WebSocketHandler_psychic.h @@ -27,10 +27,10 @@ SOFTWARE. */ #include #include //#include "HTTP/WebSocketBase.h" -#include "SettingsHandler.h" +#include "settings/SettingsHandler.h" // #include "LogHandler.h" -#include "TagHandler.h" -#include "BatteryHandler.h" +#include "logging/TagHandler.h" +#include "sensors/BatteryHandler.h" PsychicWebSocketHandler ws; diff --git a/ESP32/src/WifiHandler.h b/ESP32/src/WifiHandler.h index f2b5fc8..376eefc 100644 --- a/ESP32/src/WifiHandler.h +++ b/ESP32/src/WifiHandler.h @@ -29,9 +29,9 @@ SOFTWARE. */ #include #endif // // #include "LogHandler.h" -#include "SettingsHandler.h" +#include "settings/SettingsHandler.h" #include "TagHandler.h" -#include "TaskHandler.h" +#include "tasks/TaskHandler.h" enum class WiFiStatus { @@ -47,9 +47,10 @@ enum class WiFiReason AP_MODE }; using WIFI_STATUS_FUNCTION_PTR_T = void (*)(WiFiStatus status, WiFiReason reason); -class WifiHandler : public Task +class WifiHandler : public TaskHandler::Task { public: + WifiHandler() : Task(TaskHandler::Rates::ONDEMAND) {} ~WifiHandler() { if (onApEventID != 0) @@ -76,64 +77,73 @@ class WifiHandler : public Task { return _apMode; } - bool connect(const char* hostname, const char* ssid, const char* pass) + bool connect(const char *ssid, const char *pass) { - LogHandler::info(_TAG, "Setting up wifi"); + LogHandler::info(Tags::Wifi, "Setting up wifi"); m_settingsFactory = SettingsFactory::getInstance(); _apMode = false; + m_connectingInProgress = true; // Serial.println("Setting mode"); // if (onApEventID != 0) // WiFi.removeEvent(onApEventID); + if (onApEventID != 0) + WiFi.removeEvent(onApEventID); onApEventID = WiFi.onEvent([this](arduino_event_id_t event, arduino_event_info_t info) { this->WiFiEvent(event, info); }); WiFi.mode(WIFI_STA); WiFi.setSleep(false); - WiFi.setHostname(hostname); - bool isStatic = STATICIP_DEFAULT; + WiFi.setHostname("TCodeESP32"); + bool isStatic = false; m_settingsFactory->getValue(STATICIP, isStatic); if (isStatic) { const char *ipAddressString = m_settingsFactory->getValue(LOCALIP); - LogHandler::info(_TAG, "Setting static IP settings: %s", ipAddressString); + LogHandler::info(Tags::Wifi, "Setting static IP settings: %s", ipAddressString); IPAddress ipAddress; if (!ipAddress.fromString(ipAddressString)) { - LogHandler::error(_TAG, "Invalid static IP address: %s", ipAddressString); + LogHandler::error(Tags::Wifi, "Invalid static IP address: %s", ipAddressString); return false; } IPAddress gateway; const char *gatewayString = m_settingsFactory->getValue(GATEWAY); if (!gateway.fromString(gatewayString)) { - LogHandler::error(_TAG, "Invalid static gateway address: %s", gatewayString); + LogHandler::error(Tags::Wifi, "Invalid static gateway address: %s", gatewayString); return false; } IPAddress subnet; const char *subnetString = m_settingsFactory->getValue(SUBNET); if (!subnet.fromString(subnetString)) { - LogHandler::error(_TAG, "Invalid static subnet address: %s", subnetString); + LogHandler::error(Tags::Wifi, "Invalid static subnet address: %s", subnetString); return false; } IPAddress dns1 = (uint32_t)0; const char *dns1String = m_settingsFactory->getValue(DNS1); if (strlen(dns1String) > 0 && !dns1.fromString(dns1String)) { - LogHandler::error(_TAG, "Invalid static dns1 address: %s", dns1String); + LogHandler::error(Tags::Wifi, "Invalid static dns1 address: %s", dns1String); return false; } IPAddress dns2 = (uint32_t)0; const char *dns2String = m_settingsFactory->getValue(DNS2); if (strlen(dns2String) > 0 && !dns2.fromString(dns2String)) { - LogHandler::error(_TAG, "Invalid static dns2 address: %s", dns2String); + LogHandler::error(Tags::Wifi, "Invalid static dns2 address: %s", dns2String); return false; } WiFi.config(ipAddress, gateway, subnet, dns1, dns2); } printMac(); - LogHandler::info(_TAG, "Establishing connection to %s", ssid); + LogHandler::info(Tags::Wifi, "Establishing connection to %s", ssid); + if (WiFi.status() == WL_CONNECTED && strcmp(WiFi.SSID().c_str(), ssid) == 0) + { + LogHandler::info(Tags::Wifi, "Already connected to %s", ssid); + m_connectingInProgress = false; + return true; + } if (pass[0] == '\0') WiFi.begin(ssid); else @@ -141,18 +151,19 @@ class WifiHandler : public Task int connectStartTimeout = millis() + connectTimeOut; while (!isConnected() && millis() < connectStartTimeout) { - vTaskDelay(1000/portTICK_PERIOD_MS); - Serial.print("."); + vTaskDelay(250 / portTICK_PERIOD_MS); } if (millis() >= connectStartTimeout) { - LogHandler::error(_TAG, "Wifi timed out connection to AP"); + LogHandler::error(Tags::Wifi, "Wifi timed out connection to AP"); + m_connectingInProgress = false; WiFi.disconnect(true, true); return false; } WiFi.setSleep(false); _apMode = false; + m_connectingInProgress = false; return true; } @@ -173,27 +184,27 @@ class WifiHandler : public Task switch (event) { case ARDUINO_EVENT_WIFI_STA_START: - LogHandler::info(_TAG, "Station Mode Started"); + LogHandler::info(Tags::Wifi, "Station Mode Started"); break; case ARDUINO_EVENT_WIFI_STA_GOT_IP: + m_connectingInProgress = false; strncpy(SettingsHandler::currentIP, WiFi.localIP().toString().c_str(), IP4ADDR_STRLEN_MAX); - strncpy(SettingsHandler::currentGateway, WiFi.gatewayIP().toString().c_str(), IP4ADDR_STRLEN_MAX); - strncpy(SettingsHandler::currentSubnet, WiFi.subnetMask().toString().c_str(), IP4ADDR_STRLEN_MAX); + strncpy(SettingsHandler::currentGateway, WiFi.subnetMask().toString().c_str(), IP4ADDR_STRLEN_MAX); + strncpy(SettingsHandler::currentSubnet, WiFi.gatewayIP().toString().c_str(), IP4ADDR_STRLEN_MAX); strncpy(SettingsHandler::currentDns1, WiFi.dnsIP().toString().c_str(), IP4ADDR_STRLEN_MAX); - LogHandler::info(_TAG, "Connected to: %s", WiFi.SSID().c_str()); - LogHandler::info(_TAG, "IP Address: %s", SettingsHandler::currentIP); + LogHandler::info(Tags::Wifi, "Connected to: %s", WiFi.SSID().c_str()); + LogHandler::info(Tags::Wifi, "IP Address: %s", SettingsHandler::currentIP); if (wifiStatus_callback) wifiStatus_callback(WiFiStatus::IP, WiFiReason::UNKNOWN); - //SettingsHandler::printWebAddress(SettingsHandler::currentIP); + // SettingsHandler::printWebAddress(SettingsHandler::currentIP); break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: { - LogHandler::warning(_TAG, "Disconnected from station, attempting reconnection"); - LogHandler::info(_TAG, "Reason: %u", lastReason); uint8_t reason = info.wifi_sta_disconnected.reason; + LogHandler::warning(Tags::Wifi, "Disconnected from station, reason: %u", reason); if (reason == WIFI_REASON_NO_AP_FOUND) { - LogHandler::info(_TAG, "WIFI_REASON_NO_AP_FOUND"); + LogHandler::info(Tags::Wifi, "WIFI_REASON_NO_AP_FOUND"); lastReason = reason; } else if (reason == WIFI_REASON_AUTH_FAIL || reason == WIFI_REASON_CONNECTION_FAIL) @@ -202,24 +213,37 @@ class WifiHandler : public Task } else if (reason == WIFI_REASON_BEACON_TIMEOUT || reason == WIFI_REASON_HANDSHAKE_TIMEOUT) { - LogHandler::info(_TAG, "WIFI_REASON_BEACON_TIMEOUT or WIFI_REASON_HANDSHAKE_TIMEOUT"); + LogHandler::info(Tags::Wifi, "WIFI_REASON_BEACON_TIMEOUT or WIFI_REASON_HANDSHAKE_TIMEOUT"); } else if (reason == WIFI_REASON_AUTH_EXPIRE) { - LogHandler::info(_TAG, "WIFI_REASON_AUTH_EXPIRE"); + LogHandler::info(Tags::Wifi, "WIFI_REASON_AUTH_EXPIRE"); } else { - LogHandler::info(_TAG, "Unknown reason %u", lastReason); + LogHandler::info(Tags::Wifi, "Unknown reason %u", lastReason); + } + + // Avoid reconnect races while the initial WiFi.begin() connection is still in progress. + if (m_connectingInProgress) + { + LogHandler::debug(Tags::Wifi, "Initial STA connect still in progress; skipping immediate reconnect"); + break; + } + + const unsigned long now = millis(); + if (!_apMode && WiFi.getMode() == WIFI_STA && (now - m_lastReconnectAttemptMs) > 2000) + { + m_lastReconnectAttemptMs = now; + WiFi.reconnect(); } - WiFi.reconnect(); break; } case ARDUINO_EVENT_WIFI_STA_STOP: - LogHandler::error(_TAG, "Station Mode Stopped: %u", info.wifi_sta_disconnected.reason); + LogHandler::error(Tags::Wifi, "Station Mode Stopped: %u", info.wifi_sta_disconnected.reason); if (lastReason == WIFI_REASON_NO_AP_FOUND) { - LogHandler::info(_TAG, "WIFI_REASON_NO_AP_FOUND"); + LogHandler::info(Tags::Wifi, "WIFI_REASON_NO_AP_FOUND"); if (wifiStatus_callback) wifiStatus_callback(WiFiStatus::DISCONNECTED, WiFiReason::NO_AP); } @@ -236,22 +260,22 @@ class WifiHandler : public Task lastReason = 0; break; case ARDUINO_EVENT_WPS_ER_SUCCESS: - LogHandler::info(_TAG, "WPS Successfull, stopping WPS and connecting to: %s", WiFi.SSID().c_str()); + LogHandler::info(Tags::Wifi, "WPS Successfull, stopping WPS and connecting to: %s", WiFi.SSID().c_str()); WiFi.begin(); break; case ARDUINO_EVENT_WPS_ER_FAILED: - LogHandler::error(_TAG, "WPS Failed, retrying"); + LogHandler::error(Tags::Wifi, "WPS Failed, retrying"); WiFi.reconnect(); break; case ARDUINO_EVENT_WPS_ER_TIMEOUT: - LogHandler::error(_TAG, "WPS Timedout, retrying"); + LogHandler::error(Tags::Wifi, "WPS Timedout, retrying"); WiFi.reconnect(); break; case ARDUINO_EVENT_WPS_ER_PIN: - LogHandler::debug(_TAG, "ARDUINO_EVENT_WPS_ER_PIN"); + LogHandler::debug(Tags::Wifi, "ARDUINO_EVENT_WPS_ER_PIN"); break; case ARDUINO_EVENT_WIFI_AP_STACONNECTED: - LogHandler::debug(_TAG, "ARDUINO_EVENT_WIFI_AP_STACONNECTED"); + LogHandler::debug(Tags::Wifi, "ARDUINO_EVENT_WIFI_AP_STACONNECTED"); if (wifiStatus_callback) wifiStatus_callback(WiFiStatus::CONNECTED, WiFiReason::AP_MODE); // if(_apMode) @@ -263,7 +287,7 @@ class WifiHandler : public Task // } break; case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: - LogHandler::debug(_TAG, "ARDUINO_EVENT_WIFI_AP_STADISCONNECTED"); + LogHandler::debug(Tags::Wifi, "ARDUINO_EVENT_WIFI_AP_STADISCONNECTED"); if (wifiStatus_callback) wifiStatus_callback(WiFiStatus::DISCONNECTED, WiFiReason::AP_MODE); if (_apMode) @@ -276,13 +300,13 @@ class WifiHandler : public Task } } - bool startAp(const char* hostname, const char* ssid, const char* pass, const uint8_t& channel, const bool& hidden, const char* ip, const char* subnet, const char* gateway) + bool startAp(const char *ssid, const char *pass, const uint8_t &channel, const bool &hidden, const char *ip, const char *subnet, const char *gateway) { // WiFi.disconnect(true, true); - LogHandler::info(TagHandler::WifiHandler, "Starting in APMode: SSID: %s, Hidden: %u, Channel: %u, IP: %s, Subnet: %s, Gateway: %s", ssid, hidden, channel, ip, subnet, gateway); - // LogHandler::info(TagHandler::WifiHandler, "Password: %s", pass); + LogHandler::info(Tags::Wifi, "Starting in APMode: SSID: %s, Hidden: %u, Channel: %u, IP: %s, Subnet: %s, Gateway: %s", ssid, hidden, channel, ip, subnet, gateway); + // LogHandler::info(Tags::Wifi, "Password: %s", pass); WiFi.mode(WIFI_AP); - WiFi.setHostname(hostname); + // WiFi.setHostname("TCodeESP32"); WiFi.softAP(ssid, pass, channel, hidden, 1); printMac(); @@ -299,11 +323,12 @@ class WifiHandler : public Task gateway_.fromString(ip); if (!WiFi.softAPConfig(ip_, gateway_, subnet_)) { - LogHandler::error(_TAG, "AP Mode Failed to configure"); + LogHandler::error(Tags::Wifi, "AP Mode Failed to configure"); return false; } _apMode = true; - LogHandler::info(TagHandler::WifiHandler, "APMode started"); + m_connectingInProgress = false; + LogHandler::info(Tags::Wifi, "APMode started"); SettingsHandler::printWebAddress(WiFi.softAPIP().toString().c_str()); return true; } @@ -313,13 +338,13 @@ class WifiHandler : public Task } static void disable() { - LogHandler::info(TagHandler::WifiHandler, "Disable WiFi"); + LogHandler::info(Tags::Wifi, "Disable WiFi"); WiFi.disconnect(true, true); WiFi.mode(WIFI_OFF); esp_err_t disable = esp_wifi_deinit(); if (disable != ESP_OK) { - LogHandler::error(TagHandler::WifiHandler, "Disable fail: %s", esp_err_to_name(disable)); + LogHandler::error(Tags::Wifi, "Disable fail: %s", esp_err_to_name(disable)); } }; @@ -333,13 +358,14 @@ class WifiHandler : public Task private: WIFI_STATUS_FUNCTION_PTR_T wifiStatus_callback; - const char *_TAG = TagHandler::WifiHandler; SettingsFactory *m_settingsFactory; int connectTimeOut = 10000; int onApEventID = 0; static int8_t _rssi; static bool _apMode; uint8_t lastReason; + bool m_connectingInProgress = false; + unsigned long m_lastReconnectAttemptMs = 0; // String translateEncryptionType(wifi_auth_mode_t encryptionType) { // switch (encryptionType) { // case (WIFI_AUTH_OPEN): @@ -358,14 +384,14 @@ class WifiHandler : public Task // } void printMac() { - //char macAddress[18] = {0}; - #ifdef ESP_ARDUINO3 - //strlcpy(macAddress, Network.macAddress().c_str(), sizeof(macAddress)); - LogHandler::info(_TAG, "Mac: %s", Network.macAddress().c_str()); - #else - //strlcpy(macTemp, WiFi.macAddress().c_str(), sizeof(macTemp)); - LogHandler::info(_TAG, "Mac: %s", WiFi.macAddress().c_str()); - #endif +// char macAddress[18] = {0}; +#ifdef ESP_ARDUINO3 + // strlcpy(macAddress, Network.macAddress().c_str(), sizeof(macAddress)); + LogHandler::info(Tags::Wifi, "Mac: %s", Network.macAddress().c_str()); +#else + // strlcpy(macTemp, WiFi.macAddress().c_str(), sizeof(macTemp)); + LogHandler::info(Tags::Wifi, "Mac: %s", WiFi.macAddress().c_str()); +#endif } }; bool WifiHandler::_apMode = false; \ No newline at end of file diff --git a/ESP32/src/bluetooth/BLE/BLEConfigurationHandler.h b/ESP32/src/bluetooth/BLE/BLEConfigurationHandler.h new file mode 100644 index 0000000..ad6719b --- /dev/null +++ b/ESP32/src/bluetooth/BLE/BLEConfigurationHandler.h @@ -0,0 +1,323 @@ +/* MIT License + +Copyright (c) 2024 Jason C. Fain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +#pragma once +/* This works but we cant use Bluetooth and Wifi at the same time with out performance loss due to them having the same radio.... +We need all the performance we can get in wifi so commenting this out. Maybe oneday they will launch a new board with them seperate. +For now this is only for first configs */ +#include +#include +#include +#include +#include +#include +#include "settings/SettingsHandler.h" +// #include "LogHandler.h" +#include "TagHandler.h" + + +#define SERVICE_UUID "ff1b451d-3070-4276-9c81-5dc5ea1043bc" // UART service UUID +#define CHARACTERISTIC_UUID "c5f1543e-338d-47a0-8525-01e3c621359d" + +class CharacteristicCallbacks: public BLECharacteristicCallbacks +{ + String recievedJsonConfiguration = ""; + + #ifdef ESP_ARDUINO3 + String sendJsonConfiguration = ""; + #else + std::string sendJsonConfiguration = ""; + #endif + unsigned sendChunkIndex = 0; + bool sentLength = false; + int sendMaxLen = 499; + + + void onWrite(BLECharacteristic *pCharacteristic) + { + SettingsHandler::printMemory(); + LogHandler::verbose(_TAG, "*** BLE onWrite"); + #ifdef ESP_ARDUINO3 + String rxValue = pCharacteristic->getValue(); + #else + std::string rxValue = pCharacteristic->getValue(); + #endif + + if (rxValue.length() > 0) + { + LogHandler::debug(_TAG, "*********"); + LogHandler::debug(_TAG, "BLE Characteristic Received Value: "); + + for (int i = 0; i < rxValue.length(); i++) { + Serial.print(rxValue[i]); + } + + Serial.println(); + + // Do stuff based on the command received from the app + if (rxValue.find(">>r<<") == 0) // Restart + { + LogHandler::debug(_TAG, "*** BLE Restarting"); + ESP.restart(); + } + else if (rxValue.find(">>t<<") == -1) + { + pCharacteristic->setValue(">"); // More please (Doesnt really matter as the que is client side) + pCharacteristic->notify(); + LogHandler::debug(_TAG, "*** BLE Characteristic Sent Value: "); + LogHandler::debug(_TAG, "Ok"); + LogHandler::debug(_TAG, " ***"); + recievedJsonConfiguration += rxValue.data(); + } + else + { + // Save json + LogHandler::debug(_TAG, "Done: %s", recievedJsonConfiguration); + if(SettingsHandler::saveAll(recievedJsonConfiguration)) + { + pCharacteristic->setValue(">>f<<"); // Finish saving + pCharacteristic->notify(); + LogHandler::debug(_TAG, "*** BLE Finish saving"); + } + else + { + pCharacteristic->setValue(">>e<<"); // Error + LogHandler::error(_TAG, "*** BLE Error saving"); + pCharacteristic->notify(); + } + recievedJsonConfiguration = ""; + } + + Serial.println(); + LogHandler::verbose(_TAG, "BLE onWrite ***"); + } + } + void onRead(BLECharacteristic *pCharacteristic) + { + LogHandler::verbose(_TAG, "*** BLE onRead"); + // char* sentValue = SettingsHandler::getJsonForBLE(); + if(sendJsonConfiguration.empty()) { + char settings[2048] = {0}; + #warning need to send settings somehow + //SettingsHandler::ser(settings); + LogHandler::debug(_TAG, "BLE Get wifi settings: %s", settings); + if (strlen(settings) == 0) { + LogHandler::error(_TAG, "*** BLE onRead empty"); + pCharacteristic->setValue(">>e<<"); + pCharacteristic->notify(); + return; + } + //LogHandler::info(_TAG, "*** Sent Value: %s", wifiSetting); + //const int len = strlen(wifiSetting); + //LogHandler::info(_TAG, "*** strlen: %i", strlen(wifiSetting)); + sendJsonConfiguration = std::string(settings); + LogHandler::debug(_TAG, "BLE Get wifi string: %s", sendJsonConfiguration.c_str()); + } + + // size_t chunksize = wifiSettingsString.size()/19+1; + // for(size_t i=0; isetValue(value); + // pCharacteristic->notify(); + // } + if(!sentLength) { + sentLength = true; + char lengthNotify[10]; + sprintf(lengthNotify, ">>l<<:%zu", sendJsonConfiguration.length()); + pCharacteristic->setValue(lengthNotify); + pCharacteristic->notify(); + + } else if(sendChunkIndex < sendJsonConfiguration.length()) { + if(sendJsonConfiguration.length() > sendMaxLen) { + std::string value = sendJsonConfiguration.substr(sendChunkIndex,sendMaxLen); + printf("index %d, length: %i, %s\n", sendChunkIndex, sendJsonConfiguration.length(), value.c_str()); + pCharacteristic->setValue(value); + pCharacteristic->notify(); + // printf("ESP.getFreeHeap() %i\n", ESP.getFreeHeap()); + // printf("ESP.getHeapSize() %i\n", ESP.getHeapSize()); + sendChunkIndex += sendMaxLen; + + //delay(3); + } else { + pCharacteristic->setValue(sendJsonConfiguration); + pCharacteristic->notify(); + } + } else { + LogHandler::debug(_TAG, ">>f<<"); + pCharacteristic->setValue(">>f<<"); + pCharacteristic->notify(); + sendJsonConfiguration = ""; + sentLength = false; + sendChunkIndex = 0; + } + // int i = 0; + // int maxLen = 19; + // if(len > maxLen) { + // while (i*maxLen < len) { + // char chunk[maxLen + 1]; + // memset(chunk, '\0', sizeof(chunk)); + // memcpy(chunk, wifiSetting+(i*maxLen), maxLen); + // pCharacteristic->setValue((uint8_t*)chunk, maxLen); + // pCharacteristic->notify(); + + // printf("loop %d, i*maxLen: %i : %s\n", i, i*maxLen, chunk); + // delay(3); + // i++; + // } + // } else { + // pCharacteristic->setValue(wifiSetting); + // pCharacteristic->notify(); + // } + Serial.println(); + LogHandler::debug(_TAG, "BLE Onread Ok ***"); + } +public: + void resetState() { + recievedJsonConfiguration = ""; + sendJsonConfiguration = ""; + sentLength = false; + sendChunkIndex = 0; + } +private: + Tags::tag_t _TAG = Tags::BLE; + // QueueHandle_t debugInQueue; + // int m_lastSend; + // TaskHandle_t* emptyQueueHandle; + // bool emptyQueueRunning; +}; +CharacteristicCallbacks *characteristicCallbacks = new CharacteristicCallbacks(); + +class MyServerCallbacks: public BLEServerCallbacks { + void onConnect(BLEServer* pServer) + { + LogHandler::debug(_TAG, "*********"); + LogHandler::debug(_TAG, "Device connected"); + characteristicCallbacks->resetState(); + }; + void onDisconnect(BLEServer* pServer) + { + LogHandler::debug(_TAG, "*********"); + LogHandler::debug(_TAG, "Device disconnected"); + characteristicCallbacks->resetState(); + BLEDevice::startAdvertising(); + } +private: + Tags::tag_t _TAG = Tags::BLE; +}; + +class DescriptorCallbacks: public BLEDescriptorCallbacks +{ + void onWrite(BLEDescriptor *pDescriptor) + { + LogHandler::debug(_TAG, "*********"); + LogHandler::debug(_TAG, "Descriptor onWrite: "); + } + void onRead(BLEDescriptor *pDescriptor) + { + LogHandler::debug(_TAG, "*********"); + LogHandler::debug(_TAG, "Descriptor onRead: "); + } +private: + Tags::tag_t _TAG = Tags::BLE; +}; + +class BLEConfigurationHandler +{ + private: + Tags::tag_t _TAG = Tags::BLE; + BLECharacteristic *pCharacteristic; + bool deviceConnected = false; + bool isInitailized = false; + float txValue = 0; + BLEServer *pServer; + BLEService *pService; + BLEAdvertising *pAdvertising; + public: + void setup() + { + // Create the BLE Device + LogHandler::info(_TAG, "Setup BLE: %s", "TCodeConfig"); + BLEDevice::init("TCodeConfig"); // Give it a name + //BLEDevice::setMTU(23); + // Create the BLE Server + pServer = BLEDevice::createServer(); + pServer->setCallbacks(new MyServerCallbacks()); + // Create the BLE Service + pService = pServer->createService(SERVICE_UUID); + + // Create a BLE Characteristic + // Create a BLE Characteristic + pCharacteristic = pService->createCharacteristic( + CHARACTERISTIC_UUID, + BLECharacteristic::PROPERTY_READ | + BLECharacteristic::PROPERTY_WRITE | + BLECharacteristic::PROPERTY_WRITE_NR | + BLECharacteristic::PROPERTY_NOTIFY | + BLECharacteristic::PROPERTY_INDICATE + ); + BLEDescriptor* p2902Descriptor = new BLEDescriptor(CHARACTERISTIC_UUID); + p2902Descriptor->setCallbacks(new DescriptorCallbacks()); + pCharacteristic->addDescriptor(p2902Descriptor); + pCharacteristic->setCallbacks(characteristicCallbacks); + + // Start the service + pService->start(); + + // Start advertising + // Start advertising + pAdvertising = BLEDevice::getAdvertising(); + pAdvertising->addServiceUUID(SERVICE_UUID); + pAdvertising->setScanResponse(true); + pAdvertising->setMinPreferred(0x0); // set value to 0x00 to not advertise this parameter + BLEDevice::startAdvertising(); + LogHandler::info(_TAG, "BLE waiting a client connection to notify..."); + + isInitailized = true; + + LogHandler::debug(_TAG, "Exit setup"); + } + + void stop() + { + if(isInitailized) + { + LogHandler::info(_TAG, "Stop"); + BLEDevice::deinit(true); + LogHandler::debug(_TAG, "deinit"); + isInitailized = false; + if(pServer != nullptr) + delete(pServer); + if(pService != nullptr) + delete(pService); + if(pCharacteristic != nullptr) + delete(pCharacteristic); + if(pAdvertising != nullptr) + delete(pAdvertising); + if(characteristicCallbacks != nullptr) + delete(characteristicCallbacks); + } + } +}; + + diff --git a/ESP32/src/bluetooth/BLE/BLEHCControlCallback.h b/ESP32/src/bluetooth/BLE/BLEHCControlCallback.h new file mode 100644 index 0000000..2dc80d7 --- /dev/null +++ b/ESP32/src/bluetooth/BLE/BLEHCControlCallback.h @@ -0,0 +1,120 @@ +/* MIT License + +Copyright (c) 2024 Jason C. Fain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +#pragma once + +#include +#include + +#include +#include +#include + +#include "logging/LogHandler.h" + +#include "BLECharacteristicCallbacksBase.h" +#include "TagHandler.h" +#include "constants.h" + +class BLEHCControlCallback: public BLECharacteristicCallbacksBase +{ +public: + BLEHCControlCallback(QueueHandle_t tcodeQueue): m_TCodeQueue(tcodeQueue) { } + // At some point this signature will change because its in master so if Bluetooth breaks, check the source class signature. + #ifdef NIMBLE_LATEST + void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override { + #else + void onWrite(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc) override { + #endif + // uint16_t handle = pCharacteristic->getHandle(); + NimBLEAttValue rxValue = pCharacteristic->getValue(); + size_t rxLength = rxValue.length(); + const uint8_t* rxData = rxValue.data(); + if(rxLength > 4) { + Serial.println("Warning: it seems the format of HC data has changed."); + return; + } + + // // //esp_gatt_char_prop_t prop = pCharacteristic->; + // // //Serial.println(*rxValue,HEX); + // // //00F418713F41 + // // // uint8_t x; + // // // sscanf(rxValue, "%x", &x); + // // //int value = (int)strtol(rxValue, NULL, 0); + // // // LogHandler::info(_TAG, "rxValue: %u", *rxValue); + // Serial.print("rxLength: "); + // Serial.print(rxLength); + // Serial.println(); + // Serial.print("handle: "); + // Serial.print(handle); + // Serial.println(); + // // LogHandler::info(m_TAG, "handle: %d", handle); + // // LogHandler::info(m_TAG, "rxLength: %d", rxLength); + + // // // xQueueSend(m_TCodeQueue, input, 0); + // // Serial.println(); + // for (int i = 0; i < rxLength; i++) { + // //tcode.ByteInput(input[i]); + // //tcode.ByteInput(input[i]); + // Serial.print(rxData[i] < 16 ? "0" : ""); + // Serial.print(rxData[i],HEX); + // if(i> 16; + tcode[MAX_COMMAND] = {0}; + snprintf(tcode, MAX_COMMAND, "L0%03dI%d\n", tcodeBytes, speedBytes); + + LogHandler::verbose(Tags::BLE, "Receive HC tcode: %s", tcode); + if(xQueueSend(m_TCodeQueue, tcode, 0) != pdTRUE) { + LogHandler::error(Tags::BLE, "Failed to write to queue"); + } + } +private: + Tags::tag_t m_TAG = Tags::BLE; + QueueHandle_t m_TCodeQueue; + char tcode[MAX_COMMAND] = {0}; + + template + T swap_endian(T u) + { + static_assert (CHAR_BIT == 8, "CHAR_BIT != 8"); + + union + { + T u; + unsigned char u8[sizeof(T)]; + } source, dest; + + source.u = u; + + for (size_t k = 0; k < sizeof(T); k++) + dest.u8[k] = source.u8[sizeof(T) - k - 1]; + + return dest.u; + } +}; \ No newline at end of file diff --git a/ESP32/src/bluetooth/BLE/BLEHandler.hpp b/ESP32/src/bluetooth/BLE/BLEHandler.hpp new file mode 100644 index 0000000..489fc96 --- /dev/null +++ b/ESP32/src/bluetooth/BLE/BLEHandler.hpp @@ -0,0 +1,208 @@ +/* MIT License + +Copyright (c) 2024 Jason C. Fain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +#pragma once + +#include +#include +#include +// #include +#include +#include +#include "esp_coexist.h" +#include "constants.h" +// #include "LogHandler.h" +#include "TagHandler.h" +#include "TCode/MotorHandler.h" +#include "logging/LogHandler.h" +#include "BLEHandlerBase.h" +#include "BLEHandlerTCode.h" +#include "BLEHandlerLove.h" +#include "BLEHandlerHC.h" + +class BLEHandler +{ +public: + BLEHandler() + { + m_TCodeQueue = xQueueCreate(25, sizeof(char[MAX_COMMAND])); + m_callBackQueue = xQueueCreate(5, sizeof(char[MAX_COMMAND])); + } + void setup() + { + // auto callbacks = getCaracteristicCallbacks(); + SettingsFactory::getInstance()->getValue(BLE_DEVICE_TYPE, m_bleDeviceType); + + m_subHandler = getHandler(); + m_subHandler->setup(m_TCodeQueue); + + // LogHandler::debug(_TAG, "Setting up BLE Characteristics"); + // if(m_isHC) { + // m_tcodeCharacteristic = new BLECharacteristic(callbacks->CHARACTERISTIC_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR); + // m_tcodeCharacteristic2 = new BLECharacteristic(callbacks->CHARACTERISTIC_UUID2, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR); + // m_tcodeCharacteristic2->setCallbacks(callbacks); + // } else { + // m_tcodeCharacteristic = new BLECharacteristic(callbacks->CHARACTERISTIC_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE_NR); + // } + // LogHandler::debug(_TAG, "Setting up BLE Characteristic Callbacks"); + // m_tcodeCharacteristic->setCallbacks(callbacks); + + // pService->addCharacteristic(m_tcodeCharacteristic); + + // if(m_isHC) { + // pService->addCharacteristic(m_tcodeCharacteristic2); + // } + + // //https://github.com/espressif/esp-idf/issues/12434 + // // Before sending BLE command + // esp_coex_status_bit_set(ESP_COEX_ST_TYPE_BLE, ESP_COEX_BLE_ST_MESH_CONFIG); + + // // After sending + // esp_coex_status_bit_clear(ESP_COEX_ST_TYPE_BLE, ESP_COEX_BLE_ST_MESH_CONFIG); + // Before sending BLE command + // esp_coex_preference_set(ESP_COEX_PREFER_BT); + + // After sending + // esp_coex_preference_set(ESP_COEX_PREFER_WIFI); + + // xTaskCreatePinnedToCore( + // bleTask,/* Function to implement the task */ + // "BLETask", /* Name of the task */ + // configMINIMAL_STACK_SIZE*4, /* Stack size in words */ + // static_cast(this), /* Task input parameter */ + // tskIDLE_PRIORITY, /* Priority of the task */ + // &m_bleTask, /* Task handle. */ + // CONFIG_BT_NIMBLE_PINNED_TO_CORE); /* Core where the task should run */ + } + + // static void bleTask(void* arg) { + // BLEHandler* handler = static_cast(arg); + // TickType_t pxPreviousWakeTime = millis(); + // while(1) { + // auto len = handler->m_tcodeCharacteristic->getDataLength(); + // if(len) { + // const char* value = handler->m_tcodeCharacteristic->getValue().c_str(); + // LogHandler::verbose(_TAG, "Recieve tcode: %s", value); + // //strncpy(buf, value, m_tcodeCharacteristic->getDataLength()); + // // if(strlen(value)) + // // handler->m_motorHandler->read(value, len); + // //handler->m_motorHandler->read(value); + // if(xQueueSend(m_TCodeQueue, value, 0) != pdTRUE) { + // //LogHandler::error(_TAG, "Failed to write to queue"); + // } + // } + // xTaskDelayUntil(&pxPreviousWakeTime, 10/portTICK_PERIOD_MS); + // } + // } + + void read(char *buf) + { + // if(m_tcodeCharacteristic->getDataLength()) { + // const char* value = m_tcodeCharacteristic->getValue().c_str(); + // strncpy(buf, value, m_tcodeCharacteristic->getDataLength()); + // } + // else + // { + // buf[0] = {0}; + // } + if (xQueueReceive(m_TCodeQueue, buf, 0)) + { + // LogHandler::verbose(_TAG, "Recieve tcode: %s", buf); + } + else + { + // LogHandler::error(_TAG, "Failed to read from queue"); + buf[0] = {0}; + } + } + + bool isConnected() + { + if(!m_subHandler) + return false; + return m_subHandler->isConnected(); + } + + void CommandCallback(const char *in) + { + if(m_subHandler) + m_subHandler->CommandCallback(in); + } + + static void disable() + { + LogHandler::info(_TAG, "Disable BLE"); + //BLEDevice::deinit(); + //esp_err_t disable = esp_bt_controller_deinit(); + esp_err_t disable = esp_bt_controller_mem_release(ESP_BT_MODE_BTDM); + // esp_bluedroid_disable(); + // esp_bluedroid_deinit(); + // esp_bt_controller_disable(); + // esp_bt_controller_deinit(); + //esp_err_t disable = esp_bt_mem_release(ESP_BT_MODE_BTDM); + if (disable != ESP_OK) + { + LogHandler::error(_TAG, "Disable fail: %s", esp_err_to_name(disable)); + } + }; + +private: + // friend class BLETCodeControlCallback; + // friend class BLELoveControlCallback; + static Tags::tag_t _TAG; + // const char* BLE_DEVICE_NAME = "TCODE-ESP32"; + // const char* BLE_TCODE_SERVICE_UUID = "ff1b451d-3070-4276-9c81-5dc5ea1043bc"; + // const char* BLE_TCODE_CHARACTERISTIC_UUID = "c5f1543e-338d-47a0-8525-01e3c621359d"; + BLEDeviceType m_bleDeviceType; + bool m_isHC = false; + bool m_isLove = true; + BLEHandlerBase *m_subHandler = 0; + + QueueHandle_t m_TCodeQueue; + QueueHandle_t m_callBackQueue; + // // Haptics connect UUID's + // const char* BLE_DEVICE_NAME_HC = "OSR-ESP32"; + // const char* BLE_TCODE_SERVICE_UUID_HC = "00004000-0000-1000-8000-0000101A2B3C"; + // const char* BLE_TCODE_CHARACTERISTIC_UUID_HC = "00002000-0001-1000-8000-0000101A2B3C"; + // const char* BLE_TCODE_CHARACTERISTIC_UUID2_HC = "00002000-0002-1000-8000-0000101A2B3C"; + + TaskHandle_t m_bleTask; + BLEHandlerBase *getHandler() + { + if (m_bleDeviceType == BLEDeviceType::LOVE) + { + LogHandler::info(_TAG, "Setting up BLE Love handler"); + static BLEHandlerLove bleHandler; + return &bleHandler; + } + if (m_bleDeviceType == BLEDeviceType::HC) + { + LogHandler::info(_TAG, "Setting up BLE HC handler"); + static BLEHandlerHC bleHandler; + return &bleHandler; + } + LogHandler::info(_TAG, "Setting up BLE Tcode handler"); + static BLEHandlerTCode bleHandler; + return &bleHandler; + }; +}; +Tags::tag_t BLEHandler::_TAG = Tags::BLE; diff --git a/ESP32/src/bluetooth/BluetoothHandler.h b/ESP32/src/bluetooth/BluetoothHandler.h new file mode 100644 index 0000000..b017bdf --- /dev/null +++ b/ESP32/src/bluetooth/BluetoothHandler.h @@ -0,0 +1,103 @@ +/* MIT License + +Copyright (c) 2024 Jason C. Fain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +#pragma once + +#include +// #include +#include "settings/SettingsHandler.h" +#include "logging/LogHandler.h" +#include "tasks/TaskHandler.h" +#include "TagHandler.h" +#include "esp_coexist.h" +#include "esp_bt.h" + +#if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED) +#error Bluetooth is not enabled! Please run `make menuconfig` to and enable it +#endif + +class BluetoothHandler : public TaskHandler::Task +{ +public: + void setup() override + { + LogHandler::info(Tags::Bluetooth, "Starting bluetooth serial: %s", "TCodeESP32"); + if (!SerialBT.begin("TCodeESP32")) + { + LogHandler::error(Tags::Bluetooth, "An error occurred initializing Bluetooth serial"); + return; + } + LogHandler::info(Tags::Bluetooth, "Bluetooth started"); + _isConnected = true; + } + + byte read() + { + return SerialBT.read(); + } + + void stop() + { + _isConnected = false; + SerialBT.disconnect(); + SerialBT.end(); + } + + String readStringUntil(char terminator) + { + return SerialBT.readStringUntil(terminator); + } + + void CommandCallback(const String &in) + { + if (_isConnected) + SerialBT.print(in); + } + + void write(uint8_t message) + { + SerialBT.write(message); + } + void println(const char *message) + { + SerialBT.println(message); + } + + int available() + { + return SerialBT.available(); + } + + bool isConnected() + { + return _isConnected; + } + + static void disable() + { + esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); + } + +private: + bool _isConnected = false; + BluetoothSerial SerialBT; +}; \ No newline at end of file diff --git a/ESP32/src/display/DisplayHandler.h b/ESP32/src/display/DisplayHandler.h new file mode 100644 index 0000000..cb17af1 --- /dev/null +++ b/ESP32/src/display/DisplayHandler.h @@ -0,0 +1,762 @@ +/* MIT License + +Copyright (c) 2024 Jason C. Fain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +#pragma once + +#include +#include +#include "Adafruit_SSD1306_RSB.h" +#include "settings/SettingsHandler.h" +#include "logging/LogHandler.h" +#if WIFI_TCODE +#include "network/WifiHandler.h" +#endif +#include +#if BUILD_TEMP +#include "sensors/TemperatureHandler.h" +#endif +#include "sensors/BatteryHandler.h" +#include "logging/TagHandler.h" +#include "tasks/TaskHandler.h" +// #if ISAAC_NEWTONGUE_BUILD +// #include "animationFrames.h" +// #endif + +class DisplayHandler : public TaskHandler::Task +{ +public: + DisplayHandler() : m_settingsFactory(SettingsFactory::getInstance()), + display( + m_settingsFactory->getDisplayScreenWidth(), + m_settingsFactory->getDisplayScreenHeight(), + // 64, + &Wire, + -1, 100000UL, 100000UL), + Task(TaskHandler::Rates::SLOW) + { + } + + void setup(int I2CAddress, bool fanControlEnabled, int8_t rstPin = -1) + { + m_fanControlEnabled = fanControlEnabled; + + LogHandler::info(Tags::Display, "Setting up display"); + int tries = 0; + SettingsHandler::waitForI2CDevices(I2CAddress); + if (SettingsHandler::systemI2CAddresses.size() == 0) + { + return; + } + + // Wire.begin(); + // Wire.setClock(100000UL); + if (!I2CAddress) + { + LogHandler::info(Tags::Display, "No address to connect to"); + return; + } + else if (!connectDisplay(I2CAddress, rstPin)) + { + LogHandler::info(Tags::Display, "Could not connect address"); + return; + } + this->wait(2000); + if (displayConnected) + { + // display.setFont(Adafruit5x7); + display.clearDisplay(); + display.setTextColor(WHITE); + display.setTextSize(1); + } + else + LogHandler::error(Tags::Display, "Display is not connected"); + LogHandler::info(Tags::Display, "Setting up display finished"); + } + + void setLocalIPAddress(IPAddress ipAddress) + { + _ipAddress = ipAddress; + } + + void setSleeveTemp(float temp) + { + LogHandler::verbose(Tags::Display, "setSleeveTemp: %f", temp); + m_sleeveTemp = temp; + memset(m_sleeveTempString, '\0', sizeof(m_sleeveTempString)); + if (temp < 0) + { + strcpy(m_sleeveTempString, "XX.XX\0"); + } + else + { + dtostrf(temp, sizeof(m_sleeveTempString) - 1, 2, m_sleeveTempString); + } + } + void setInternalTemp(float temp) + { + LogHandler::verbose(Tags::Display, "setInternalTemp: %f", temp); + m_internalTemp = temp; + memset(m_internalTempString, '\0', sizeof(m_internalTempString)); + if (temp < 0) + { + strcpy(m_internalTempString, "XX.XX\0"); + } + else + { + dtostrf(temp, sizeof(m_internalTempString) - 1, 2, m_internalTempString); + } + } + void setHeateState(const char *state) + { + LogHandler::verbose(Tags::Display, "setHeateState: %s", state); + m_HeatState = String(state); + } + void setHeateStateShort(const char *state) + { + LogHandler::verbose(Tags::Display, "setHeateStateShort: %s", state); + m_HeatStateShort = String(state); + } + void setFanState(const char *state) + { + LogHandler::verbose(Tags::Display, "setFanState: %s", state); + m_fanState = String(state); + } + void setBatteryInformation(float capacityRemainingPercentage, float voltage, float temperature) + { + LogHandler::verbose(Tags::Display, "setBatteryCapacityRemainingPercentage: %f", capacityRemainingPercentage); + m_batteryCapacityRemainingPercentage = capacityRemainingPercentage; + LogHandler::verbose(Tags::Display, "setBatteryVoltage: %f", voltage); + m_batteryVoltage = voltage; + LogHandler::verbose(Tags::Display, "setBatteryTemperature: %f", temperature); + m_batteryTemperature = temperature; + } + + void clearDisplay() + { + LogHandler::verbose(Tags::Display, "clear display"); + if (displayConnected) + { + display.clearDisplay(); + } + } + + bool isConnected() + { + return displayConnected; + } + + bool isRunning() + { + return _isRunning; + } + void stopRunning() + { + LogHandler::verbose(Tags::Display, "stopRunning"); + _isRunning = false; + } + + void loop() + { + + if (!m_animationPlaying && displayConnected && millis() >= lastUpdate + nextUpdate) + { + LogHandler::verbose(Tags::Display, "Enter display loop"); + lastUpdate = millis(); + clearDisplay(); + setTextSize(1); + int headerPadding = is32() ? 0 : 3; + // Serial.print("Display Core: "); + // Serial.println(xPortGetCoreID()); + +#if WIFI_TCODE + if (WifiHandler::isConnected()) + { + LogHandler::verbose(Tags::Display, "Enter wifi connected"); + startLine(headerPadding); + display.print(_ipAddress); + + drawBatteryLevel(); + + // Draw Wifi signal bars + int barHeight = is32() ? 8 : 10; + int bars; + // int bars = map(RSSI,-80,-44,1,6); // this method doesn't refelct the Bars well + // simple if then to set the number of bars + int8_t RSSI = WifiHandler::getRSSI(); + if (RSSI > -55) + { + bars = 5; + } + else if (RSSI < -55 && RSSI > -65) + { + bars = 4; + } + else if (RSSI < -65 && RSSI > -70) + { + bars = 3; + } + else if (RSSI < -70 && RSSI > -78) + { + bars = 2; + } + else if (RSSI < -78 && RSSI > -82) + { + bars = 1; + } + else + { + bars = 0; + } + for (int b = 0; b <= bars; b++) + { + display.fillRect((m_settingsFactory->getDisplayScreenWidth() - 17) + (b * 3), barHeight - (b * 2), 2, b * 2, WHITE); + } + + newLine(headerPadding); + if (m_settingsFactory->getVersionDisplayed()) + { + LogHandler::verbose(Tags::Display, "Enter versionDisplayed"); + left(m_settingsFactory->getTcodeVersionString()); + right(FIRMWARE_VERSION_NAME); + newLine(); + } + } + else if (WifiHandler::apMode()) + { + LogHandler::verbose(Tags::Display, "Enter apMode"); + startLine(headerPadding); + left("AP:"); + left(m_settingsFactory->getAPModeIP(), 4); + drawBatteryLevel(); + newLine(headerPadding); + if (!is32()) + { + left("SSID:"); + left(m_settingsFactory->getAPModeSSID(), 6); + newLine(); + } + if ((is32() && m_settingsFactory->getVersionDisplayed() && !m_settingsFactory->getSleeveTempDisplayed() && !m_settingsFactory->getInternalTempDisplayed()) || m_settingsFactory->getVersionDisplayed()) + { + left(m_settingsFactory->getTcodeVersionString()); + right(FIRMWARE_VERSION_NAME); + newLine(); + } + else if (is32()) + { + left("SSID:"); + left(m_settingsFactory->getAPModeSSID(), 6); + newLine(); + } + } + else + { + LogHandler::verbose(Tags::Display, "Enter Wifi error"); + display.print("Wifi error"); + drawBatteryLevel(); + } +#endif +#if BUILD_TEMP + if (m_settingsFactory->getSleeveTempDisplayed() || m_settingsFactory->getInternalTempDisplayed()) + { + is32() ? draw32Temp() : draw64Temp(); + } +#endif + + display.display(); + } + this->sleep(200); + } + + void println(String value) + { + if (displayConnected && !m_animationPlaying) + { + display.println(value); + display.display(); + // Serial.println(value); + // display.startvertscroll(0x04, 0x1F, true); + } + } + + void println(int value) + { + if (displayConnected && !m_animationPlaying) + { + display.println(value); + display.display(); + } + } + + // static void startAnimationDontPanic(void* displayHandlerRef) + // { + // ((DisplayHandler*)displayHandlerRef)->playBootAnimationDontPanic(); + // } + + // void playBootAnimationDontPanic() + // { + // #if ISAAC_NEWTONGUE_BUILD + // if(displayConnected) + // { + // display.clearDisplay(); + // m_animationPlaying = true; + // int endTime = millis() + m_animationMilliSeconds; + // int currentFrameIndex = 0; + // while(millis() < endTime) + // { + // display.drawBitmap(0, 0, dontPanicAnimationFrames[currentFrameIndex], m_settingsFactory->getDisplayScreenWidth(), m_settingsFactory->getDisplayScreenHeight(), 1); + // display.display(); + // currentFrameIndex++; + // if(currentFrameIndex == dontPanicAnimationFramesCount) + // currentFrameIndex = 0; + // vTaskDelay(30); + // display.clearDisplay(); + // } + // m_animationPlaying = false; + // } + // #endif + // vTaskDelete( NULL ); + // } + +private: + SettingsFactory *m_settingsFactory; + bool m_fanControlEnabled; + IPAddress _ipAddress; + bool displayConnected = false; + int lastUpdate = 0; + const int nextUpdate = 1000; + bool _isRunning = false; + + int currentLine = 0; + int lineCount = 0; + int lineHeight = 10; + int charWidth = 6; + + float m_internalTemp = -127.0f; + float m_sleeveTemp = -127.0f; + char m_internalTempString[7] = "XX.XX"; + char m_sleeveTempString[7] = "XX.XX"; + String m_fanState = "Unknown"; + String m_HeatState = "Unknown"; + String m_HeatStateShort = "U"; + float m_batteryVoltage = 0.0; + int m_batteryCapacityRemainingPercentage; + float m_batteryTemperature; + + Adafruit_SSD1306_RSB display; + bool m_animationPlaying = false; + int m_animationMilliSeconds = 10000; + + // Text size 1 is 6x8, 2 is 12x16, 3 is 18x24, etc + void setTextSize(uint8_t size) + { + if (size >= 1) + { + charWidth = 6 * size; + lineHeight = getLineHeight(size); + display.setTextSize(size); + } + } + void newLine(int additionalPixels = 0, int newLineTextSize = 0) + { + int newLine = currentLine + (lineHeight + additionalPixels); + if (newLineTextSize > 0) + setTextSize(newLineTextSize); + if (newLine > m_settingsFactory->getDisplayScreenHeight() - lineHeight) + { + LogHandler::warning(Tags::Display, "End of the display reached when newLine! Current: %i, New: %i, Max: %i", currentLine, newLine, m_settingsFactory->getDisplayScreenHeight() - lineHeight); + } + currentLine = newLine; + display.setCursor(0, currentLine); + // clearCurrentLine(); + } + int getLineHeight(uint8_t size) + { + int margin = 2; + return 8 * size + margin; + } + void startLine(int additionalPixels = 0) + { + currentLine = (0 + additionalPixels); + display.setCursor(0, currentLine); + } + bool hasNextLine(int newLineTextSize = 1) + { + + return currentLine + getLineHeight(newLineTextSize) <= m_settingsFactory->getDisplayScreenHeight() - getLineHeight(newLineTextSize); + } + void space(int count = 1) + { + display.setCursor(display.getCursorX() + (count * charWidth), currentLine); + } + void right(const char *text, int margin = 0) + { + display.setCursor((m_settingsFactory->getDisplayScreenWidth() - strlen(text) * charWidth) - margin * charWidth, currentLine); + display.print(text); + } + void left(const char *text, int margin = 0) + { + display.setCursor(margin * charWidth, currentLine); + display.print(text); + } + void center(const char *text) + { + display.setCursor((m_settingsFactory->getDisplayScreenWidth() - (strlen(text) * charWidth)) / 2, currentLine); + display.print(text); + } + void clearCurrentLine() + { + display.fillRect(0, currentLine, m_settingsFactory->getDisplayScreenWidth(), 10, BLACK); + } + bool is32() + { + return m_settingsFactory->getDisplayScreenHeight() == 32; + } + + void draw64Temp() + { + if (m_settingsFactory->getSleeveTempDisplayed() && !m_settingsFactory->getInternalTempDisplayed()) + { + LogHandler::verbose(Tags::Display, "Enter draw64Temp sleeveTempDisplayed"); + if (m_settingsFactory->getVersionDisplayed()) + { + LogHandler::verbose(Tags::Display, "versionDisplayed"); + char buf[17]; + getTempString("Sleeve: ", m_sleeveTempString, buf, sizeof(buf)); + left(buf); + newLine(); + left(m_HeatState.c_str(), 3); + } + else + { + setTextSize(3); + char buf[9]; + getTempString("", m_sleeveTempString, buf, sizeof(buf)); + left(buf); + newLine(0, 1); + center(m_HeatState.c_str()); + } + } + else if (!m_settingsFactory->getSleeveTempDisplayed() && m_settingsFactory->getInternalTempDisplayed()) + { + LogHandler::verbose(Tags::Display, "Enter draw64Temp internalTempDisplayed"); + if (m_settingsFactory->getVersionDisplayed()) + { + LogHandler::verbose(Tags::Display, "versionDisplayed"); + char buf[19]; + getTempString("Internal: ", m_internalTempString, buf, sizeof(buf)); + left(buf); + if (m_fanControlEnabled) + { + newLine(); + left(m_fanState.c_str(), 3); + } + } + else + { + setTextSize(3); + char buf[9]; + getTempString("", m_internalTempString, buf, sizeof(buf)); + center(buf); + newLine(0, 1); + if (m_fanControlEnabled) + { + center(m_fanState.c_str()); + } + } + } + else if (m_settingsFactory->getSleeveTempDisplayed() && m_settingsFactory->getInternalTempDisplayed()) + { + LogHandler::verbose(Tags::Display, "Enter draw64Temp sleeveTempDisplayed && internalTempDisplayed"); + left("Sleeve"); + right("Internal"); + + newLine(); + + char buf[9]; + getTempString("", m_sleeveTempString, buf, sizeof(buf)); + left(buf); + if (m_settingsFactory->getVersionDisplayed()) + { + display.print("(" + m_HeatStateShort + ")"); + } + char buf2[9]; + getTempString("", m_internalTempString, buf2, sizeof(buf2)); + right(buf2); + + if (!m_settingsFactory->getVersionDisplayed()) + { + LogHandler::verbose(Tags::Display, "versionDisplayed"); + newLine(); + + left(m_HeatState.c_str()); + if (m_fanControlEnabled) + { + right(m_fanState.c_str()); + } + } + } + } + void draw32Temp() + { + if (m_settingsFactory->getSleeveTempDisplayed() && !m_settingsFactory->getInternalTempDisplayed()) + { + LogHandler::verbose(Tags::Display, "Enter draw32Temp sleeveTempDisplayed"); + char buf[20]; + if (m_settingsFactory->getVersionDisplayed() || !hasNextLine()) + { + LogHandler::verbose(Tags::Display, "versionDisplayed"); + getTempString("Sleeve: ", m_sleeveTempString, buf, sizeof(buf)); + left(buf); + display.print("(" + m_HeatStateShort + ")"); + } + else if (hasNextLine(2)) + { + LogHandler::verbose(Tags::Display, "hasNextLine(2)"); + setTextSize(2); + getTempString("SLT: ", m_sleeveTempString, buf, sizeof(buf)); + left(buf); + setTextSize(1); + display.print("(" + m_HeatStateShort + ")"); + } + else + { + getTempString("Sleeve: ", m_sleeveTempString, buf, sizeof(buf)); + left(buf); + newLine(); + left(m_HeatState.c_str(), 3); + } + } + else if (!m_settingsFactory->getSleeveTempDisplayed() && m_settingsFactory->getInternalTempDisplayed()) + { + LogHandler::verbose(Tags::Display, "Enter draw32Temp internalTempDisplayed"); + char buf[21]; + if (m_settingsFactory->getVersionDisplayed() || !hasNextLine()) + { + LogHandler::verbose(Tags::Display, "versionDisplayed"); + getTempString("Internal: ", m_internalTempString, buf, sizeof(buf)); + left(buf); + } + else if (hasNextLine(2)) + { + LogHandler::verbose(Tags::Display, "hasNextLine(2)"); + setTextSize(2); + getTempString("INT:", m_internalTempString, buf, sizeof(buf)); + left(buf); + // setTextSize(1); + // display.print("("+m_HeatStateShort+")"); + } + else + { + getTempString("Internal: ", m_internalTempString, buf, sizeof(buf)); + left(buf); + newLine(); + left(m_fanState.c_str(), 3); + } + } + else if (m_settingsFactory->getSleeveTempDisplayed() && m_settingsFactory->getInternalTempDisplayed()) + { + LogHandler::verbose(Tags::Display, "Enter draw32Temp sleeveTempDisplayed && internalTempDisplayed"); + char buf[10]; + if (m_settingsFactory->getVersionDisplayed() || !hasNextLine()) + { + LogHandler::verbose(Tags::Display, "versionDisplayed"); + getTempString("S", m_sleeveTempString, buf, sizeof(buf)); + left(buf); + display.print("(" + m_HeatStateShort + ")"); + getTempString("I", m_internalTempString, buf, sizeof(buf)); + right(buf); + } + else + { + left("Sleeve", 1); + right("Internal", 1); + newLine(); + getTempString("", m_sleeveTempString, buf, sizeof(buf)); + left(buf, 1); + char buf2[10]; + getTempString("", m_internalTempString, buf2, sizeof(buf2)); + right(buf2, 2); + } + } + } + + void drawBatteryLevel() + { + bool wifiConnected = false; +#if WIFI_TCODE + wifiConnected = WifiHandler::isConnected(); +#endif + if (BatteryHandler::connected()) + { + LogHandler::verbose(Tags::Display, "Enter draw battery"); + if (m_settingsFactory->getBatteryLevelNumeric()) + { + // double voltageNumber = mapf(m_batteryVoltage, 0.0, 3.3, 0.0, m_settingsFactory->getBatteryVoltageMax()); + if (false) + { // Display voltage + display.setCursor((m_settingsFactory->getDisplayScreenWidth() - (m_batteryVoltage < 10.0 ? 3 : 4) * charWidth) - (wifiConnected ? 3 : 0) * charWidth, currentLine); + display.print(m_batteryVoltage, 1); + } + else + { + display.setCursor((m_settingsFactory->getDisplayScreenWidth() - (m_batteryCapacityRemainingPercentage < 10.0 ? 3 : 4) * charWidth) - (wifiConnected ? 3 : 0) * charWidth, currentLine); + display.print(m_batteryCapacityRemainingPercentage, 1); + display.print("%"); + } + } + else + { + int batteryBars; + if (false) + { // Display voltage + if (m_batteryVoltage >= 3.17) + { + batteryBars = 5; + } + else if (m_batteryVoltage < 3.17 && m_batteryVoltage > 3.09) + { + batteryBars = 4; + } + else if (m_batteryVoltage < 3.09 && m_batteryVoltage > 3.02) + { + batteryBars = 3; + } + else if (m_batteryVoltage < 3.02 && m_batteryVoltage > 2.92) + { + batteryBars = 2; + } + else if (m_batteryVoltage < 2.92 && m_batteryVoltage > 2.83) + { + batteryBars = 1; + } + else + { + batteryBars = 0; + } + } + else + { + if (m_batteryCapacityRemainingPercentage >= 80) + { + batteryBars = 5; + } + else if (m_batteryCapacityRemainingPercentage < 80 && m_batteryCapacityRemainingPercentage > 60) + { + batteryBars = 4; + } + else if (m_batteryCapacityRemainingPercentage < 60 && m_batteryCapacityRemainingPercentage > 40) + { + batteryBars = 3; + } + else if (m_batteryCapacityRemainingPercentage < 40 && m_batteryCapacityRemainingPercentage > 20) + { + batteryBars = 2; + } + else if (m_batteryCapacityRemainingPercentage < 20 && m_batteryCapacityRemainingPercentage > 1) + { + batteryBars = 1; + } + else + { + batteryBars = 0; + } + } + for (int b = 0; b < batteryBars; b++) + { + display.fillRect( + (m_settingsFactory->getDisplayScreenWidth() - (!wifiConnected ? 20 : 37)) + (b * 3), + 2, + 2, + lineHeight - 4, + WHITE); + } + display.drawRect( + m_settingsFactory->getDisplayScreenWidth() - (!wifiConnected ? 23 : 40), + 1, + 20, + lineHeight - 2, + WHITE); // draw the outline box + } + } + } + // bool tryConnect() //Connects to the wrong address + // { + // unsigned int vecSize = m_settingsFactory->getSystemI2CAddresses.size()(); + // LogHandler::info(Tags::Display, "System I2c device count %ld", vecSize); + // if(vecSize) { + // for(unsigned int i = 0; i < vecSize; i++) + // { + // //const char* address = m_settingsFactory->getSystemI2CAddresses[i].c_str()(); + // char buf[10]; + // hexToString(m_settingsFactory->getSystemI2CAddresses[i](), buf); + // LogHandler::info(Tags::Display, "Trying to connect to %s", buf); + // if(m_settingsFactory->getSystemI2CAddresses[i]() && connectDisplay(m_settingsFactory->getSystemI2CAddresses[i]())) { + // LogHandler::info(Tags::Display, "Sucess!"); + // m_settingsFactory->getDisplay_I2C_Address() = m_settingsFactory->getSystemI2CAddresses[i](); + // m_settingsFactory->getSave()(); + // return true; + // } else { + // LogHandler::info(Tags::Display, "Failed.."); + // } + // } + // LogHandler::info(Tags::Display, "Could not connect to any I2C address"); + // } + // return false; + // } + + bool connectDisplay(int address, int pin) + { + if (LogHandler::getLogLevel() == LogLevel::DEBUG) + { + char buf[10]; + hexToString(address, buf); + LogHandler::debug(Tags::Display, "Connect to display at address: %s", buf); + LogHandler::debug(Tags::Display, "byte: %ld", address); + } + if (pin >= 0) + { + displayConnected = display.begin(SSD1306_SWITCHCAPVCC, address, pin); + if (!displayConnected) + LogHandler::error(Tags::Display, "SSD1306 RST_PIN allocation failed"); + } + else + { + displayConnected = display.begin(SSD1306_SWITCHCAPVCC, address); + if (!displayConnected) + LogHandler::error(Tags::Display, "SSD1306 allocation failed"); + } + LogHandler::debug(Tags::Display, "Exit connectDisplay connected: %ld", displayConnected); + return displayConnected; + } + + void getTempString(const char *displayText, char *temp, char *buf, int size) + { + strtrim(temp); + snprintf(buf, size, "%s%s%cC", displayText, temp, (char)247); + // strtrim(buf); + // //tempText[strlen(tempText)] = '\n'; + + // strtrim(temp); + // String tempText = displayText + String(temp) + (char)247 + "C";// TODO: remove String + // strcpy(buf, tempText.c_str()); + } +}; diff --git a/ESP32/src/logging/TagHandler.h b/ESP32/src/logging/TagHandler.h new file mode 100644 index 0000000..cfb97a4 --- /dev/null +++ b/ESP32/src/logging/TagHandler.h @@ -0,0 +1,239 @@ +/* MIT License + +Copyright (c) 2024 Jason C. Fain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR ALL_TAGS = PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +#pragma once +#include +#include +#include + +namespace Tags +{ + // Increase backing type if additional tags are required + using tag_t = uint32_t; + + enum + { + Main = 0, + Display, + Temperature, + Battery, + Settings, + Wifi, + Udp, + WebSocketServer, + WebSocketClient, + SecureWebSocketServer, + SecureWebSocketClient, + Https, + Web, + SystemCommand, + BLE, + BLEConfiguration, + Bluetooth, + Servo, + TCode, + Motor, + Motion, + Voice, + Button, + Mdns, + SettingsFactory, + Tasks, + Network, + PinMap, + Filesystem, + MessageQueue, + LAST, + }; + const tag_t INVALID = LAST; + constexpr tag_t ALL_TAGS = ~(0); + static_assert((LAST <= (sizeof(tag_t) * 8), "Too many Tags defined for backing type, increase size of tag_t")); + + uint32_t as_mask(tag_t tag) + { + if (tag >= LAST) + { + return 0; + } + return (0x1 << static_cast(tag)); + } + + bool is_set(uint32_t mask, tag_t tag) + { + return (mask & as_mask(tag)) != 0; + } + + void set_tags(uint32_t &mask, tag_t tag) + { + mask |= as_mask(tag); + } + + void unset_tags(uint32_t &mask, tag_t tag) + { + mask &= ~as_mask(tag); + } + + // This will return the lowest tag matching the mask + tag_t from_mask(uint32_t mask) + { + for (uint32_t i = 0; i < static_cast(LAST); ++i) + { + if (mask & (0x1 << i)) + { + return static_cast(i); + } + } + return 0; + } + + constexpr const char *TAG_STRINGS[] = { + "main", + "display", + "temperature", + "battery", + "settings", + "wifi", + "udp", + "websocket-server", + "websocket-base", + "secure-websocket-server", + "secure-websocket-client", + "https", + "web", + "system-command", + "ble", + "ble-configuration", + "bluetooth", + "servo", + "tcode", + "motor", + "motion", + "voice", + "button", + "mdns", + "settings-factory", + "tasks", + "network", + "pin-map", + "filesystem", + "message-queue", + }; + + constexpr const char* AvailableTags[] = { + "main", + "display", + "temperature", + "battery", + "settings", + "wifi", + "udp", + "websocket-server", + "websocket-base", + "secure-websocket-server", + "secure-websocket-client", + "https", + "web", + "system-command", + "ble", + "ble-configuration", + "bluetooth", + "servo", + "tcode", + "motor", + "motion", + "voice", + "button", + "mdns", + "settings-factory", + "tasks", + "network", + "pin-map", + "filesystem", + "message-queue", + }; + + std::string as_str(uint32_t tag_mask) + { + std::string result = ""; + for (tag_t index = static_cast(0); index < LAST; index = static_cast(index + 1)) + { + if (is_set(tag_mask, index)) + { + try + { + if (!result.empty()) + { + result += ", "; + } + result += TAG_STRINGS[static_cast(index)]; + } + catch (::std::bad_alloc &) + { + // We're out of memory, return what we have + return result; + } + } + } + return result; + } + + // Accepts a comma, or whitespace separated list of tags + bool from_str(const char *str, tag_t &found_tags) + { + // We need a copy of the buffer on the heap + // since strtok_r modifies the string + // Use strtok_r to be reentrant/thread safe vs + char *workingbuffer = strdup(str); + char *working_ptr; + char *token; + const char *delims = ", \t\n"; // basically all whitespace and comma + + found_tags = 0; + + if (!workingbuffer) + { + // Out of memory... give up + return false; + } + while ((token = strtok_r(workingbuffer, delims, &working_ptr)) != nullptr) + { + // Linear search + tag_t tag; + for (tag = static_cast(0); tag < LAST; tag = static_cast(tag + 1)) + { + if (strcmp(TAG_STRINGS[static_cast(tag)], token) == 0) + { + found_tags |= as_mask(tag); + break; + } + } + // no token found, token is invaild... abort + if (tag == LAST) + { + free(workingbuffer); + return false; + } + } + free(workingbuffer); + return true; + } +} diff --git a/ESP32/src/main.cpp b/ESP32/src/main.cpp index 9eaadf6..814addf 100644 --- a/ESP32/src/main.cpp +++ b/ESP32/src/main.cpp @@ -35,35 +35,20 @@ SOFTWARE. */ // #include #endif -#if ESP8266 == 1 -#include -#include -#include -#include -#include -#endif - #include "utils.h" #include #include #include "logging/LogHandler.h" -#include "SettingsHandler.h" -#include "SystemCommandHandler.h" -#if WIFI_TCODE -#include "WifiHandler.h" -#endif - -#if BUILD_TEMP -#include "TemperatureHandler.h" -#endif -#if BUILD_DISPLAY -#include "DisplayHandler.h" -#endif -#if BLUETOOTH_TCODE -#include "BluetoothHandler.h" -#endif -#include "TCode/MotorHandler.h" -// #include "BLEConfigurationHandler.h" +#include "settings/SettingsHandler.h" +#include "settings/FilesystemHandler.h" +#include "settings/OperatingModeHandler.h" +#include "messages/SystemCommandHandler.h" +#include "serial/SerialHandler.h" +#include "network/WifiHandler.h" +#include "sensors/TemperatureHandler.h" +#include "display/DisplayHandler.h" +#include "bluetooth/BluetoothHandler.h" +#include "tcode/MotorHandler.h" #if MOTOR_TYPE == 0 #include "ServoHandler0_3.h" @@ -73,335 +58,39 @@ SOFTWARE. */ #include "BLDCHandler0_4.h" #endif -#if WIFI_TCODE -#include "UdpHandler.h" -// #include "TcpHandler.h" -#include "HTTP/HTTPBase.h" -#include "HTTP/WebSocketBase.h" +#include "network/UdpHandler.h" +#include "network/HTTP/HTTPBase.h" +#include "network/HTTP/WebSocketBase.h" #if !SECURE_WEB -#include "WebHandler.h" -//#include "WebHandler_psychic.h" +#include "network/WebHandler.h" #else -#include "HTTP\HTTPSHandler.hpp" -#endif -#include "MDNSHandler.hpp" +#include "network/HTTP/HTTPSHandler.hpp" #endif +#include "network/MDNSHandler.hpp" // #include "OTAHandler.h" -#if BLE_TCODE -#include "BLEHandler.hpp" -#endif - -#if WIFI_TCODE -#if !SECURE_WEB -#include "WebSocketHandler.h" -//#include "WebSocketHandler_psychic.h" -#else -#include "HTTP/SecureWebSocketHandler.hpp" -#endif -#endif +#include "bluetooth/BLE/BLEHandler.hpp" -#include "TaskHandler.h" +#include "network/WebSocketHandler.h" +#include "tasks/TaskHandler.h" -#include "BatteryHandler.h" -#include "MotionHandler.hpp" -#include "VoiceHandler.hpp" -#include "ButtonHandler.hpp" +#include "sensors/BatteryHandler.h" +#include "motion/MotionHandler.hpp" +#include "sensors/VoiceHandler.hpp" +#include "sensors/ButtonHandler.hpp" -TaskManager taskManager; TickType_t pxPreviousWakeTime = millis(); -SystemCommandHandler *systemCommandHandler; -// BLEConfigurationHandler* bleConfigurationHandler; -// TcpHandler tcpHandler; -ButtonHandler *buttonHandler = 0; -#if BLUETOOTH_TCODE -BluetoothHandler *btHandler = 0; -#endif - -#if WIFI_TCODE -Udphandler *udpHandler = 0; -WifiHandler wifi; -MDNSHandler mdnsHandler; -HTTPBase *webHandler = 0; -WebSocketBase *webSocketHandler = 0; -#endif - -#if BUILD_TEMP -TemperatureHandler *temperatureHandler = 0; -#endif -#if BLE_TCODE -BLEHandler *bleHandler = 0; -#endif - -#if BUILD_DISPLAY -DisplayHandler *displayHandler; -TaskHandle_t displayTask; -// #if ISAAC_NEWTONGUE_BUILD -// TaskHandle_t animationTask; -// #endif -#endif -#if BUILD_TEMP -TaskHandle_t temperatureTask; -#endif // This has issues running with the webserver. // OTAHandler otaHandler; bool setupSucceeded = false; bool restarting = false; +unsigned long restartAtMs = 0; -String serialData; -char commandTCodeData[MAX_COMMAND]; -char udpData[MAX_COMMAND]; -char webSocketData[MAX_COMMAND]; -#if BLE_TCODE -char bleData[MAX_COMMAND]; -#endif -#if BLUETOOTH_TCODE -String bluetoothData; -#endif -char movement[MAX_COMMAND]; -ButtonModel *buttonCommand = 0; -bool dStopped = false; -bool tcodeV2Recieved = false; -bool bluetoothEnabled = BLUETOOTH_ENABLED_DEFAULT; -bool bleEnabled = BLE_ENABLED_DEFAULT; - -unsigned long bench[10]; -unsigned long benchLast[10]; -bool benchEnable = false; -bool benchEnableZero = false; -unsigned long benchThreshHold = 1300; - -void benchStart(int benchNumber) -{ - if (benchEnable || (benchEnableZero && benchNumber == 0)) - bench[benchNumber] = micros(); -} -void benchFinish(const char *systemUnderBench, int benchNumber) -{ - if (benchEnable || (benchEnableZero && benchNumber == 0)) - { - unsigned long timeTaken = micros() - bench[benchNumber]; - if (timeTaken > benchThreshHold) - { - Serial.printf("%s: %lu\n", systemUnderBench, timeTaken); - bench[benchNumber] = 0; - benchLast[benchNumber] = timeTaken; - } - } -} - -void displayPrint(String text) -{ -#if BUILD_DISPLAY - if(displayHandler) - displayHandler->println(text); -#endif -} - -void TCodeCommandCallback(const char *in) -{ - - if (systemCommandHandler->isCommand(in)) - { - systemCommandHandler->process(in); - } - else - { -#if BLUETOOTH_TCODE - if (btHandler && btHandler->isConnected()) - btHandler->CommandCallback(in); -#endif -#if BLE_TCODE - if (bleHandler && bleHandler->isConnected()) - bleHandler->CommandCallback(in); -#endif -#if WIFI_TCODE - if (webSocketHandler) - webSocketHandler->CommandCallback(in); - if (udpHandler) - udpHandler->CommandCallback(in); -#endif - if (Serial) - Serial.println(in); - } -} -void TCodePassthroughCommandCallback(const char *in) -{ - if (systemCommandHandler->isCommand(in)) - { - // This seems wrong but since we are only calling this from one place its fine for now. - char temp[strlen(in) + 2]; - temp[0] = {0}; - strcpy(temp, in); - strcat(temp, "\n"); -////////////////////////////////////////////////////////////////////////////////////// -#if BLUETOOTH_TCODE - if (btHandler && btHandler->isConnected()) - btHandler->CommandCallback(temp); -#endif -#if BLE_TCODE +void displayPrint(const char* message); +void startNetworking(bool apMode, int webPort, int udpPort, const char* hostname, const char* friendlyName); +void ensureNetworkingAvailable(); -#endif -#if WIFI_TCODE - if (webSocketHandler) - webSocketHandler->CommandCallback(temp); - if (udpHandler) - udpHandler->CommandCallback(temp); -#endif - if (Serial) - Serial.println(temp); - } -} - -#if BUILD_TEMP -void tempChangeCallBack(TemperatureType type, const char *message, float temp) -{ -#if WIFI_TCODE - if (webSocketHandler) - { - if (strpbrk(message, "{") == nullptr) - { - webSocketHandler->sendCommand(message); - } - else - { - if (type == TemperatureType::SLEEVE) - { - webSocketHandler->sendCommand("sleeveTempStatus", message); - } - else - { - webSocketHandler->sendCommand("internalTempStatus", message); - } - } - } -#endif -#if BUILD_DISPLAY - if (displayHandler) - { - if (type == TemperatureType::SLEEVE) - { - displayHandler->setSleeveTemp(temp); - } - else - { - displayHandler->setInternalTemp(temp); - } - } -#endif -} -void tempStateChangeCallBack(TemperatureType type, const char *state) -{ -#if BUILD_DISPLAY - if (displayHandler) - { - if (type == TemperatureType::SLEEVE) - { - LogHandler::verbose(TagHandler::Main, "tempStateChangeCallBack heat: %s", state); - displayHandler->setHeateState(state); - if (temperatureHandler) - displayHandler->setHeateStateShort(temperatureHandler->getShortSleeveControlStatus(state)); - } - else - { - LogHandler::verbose(TagHandler::Main, "tempStateChangeCallBack fan: %s", state); - displayHandler->setFanState(state); - } - } -#endif -} -#endif -#if WIFI_TCODE -void startNetworking(const bool &apMode, const int &port, const int &udpPort, const char *hostname, const char *friendlyName) -{ - if((MODULE_CURRENT != ModuleType::WROOM32 || (!bluetoothEnabled && !bleEnabled)) && !webHandler) - { - displayPrint("Starting web server"); - #if !SECURE_WEB - webHandler = new WebHandler(); - webSocketHandler = new WebSocketHandler(); - #else - LogHandler::debug(TagHandler::Main, "Start https task"); - webHandler = new HTTPSHandler(); - webSocketHandler = new SecureWebSocketHandler(); - auto httpsStatus = xTaskCreateUniversal( - HTTPSHandler::startLoop, /* Function to implement the task */ - "HTTPSTask", /* Name of the task */ - 8192 * 3, /* Stack size in words */ - webHandler, /* Task input parameter */ - 3, /* Priority of the task */ - &httpsTask, /* Task handle. */ - -1); /* Core where the task should run */ - if (httpsStatus != pdPASS) - { - LogHandler::error(TagHandler::Main, "Could not start https task."); - } - #endif - webHandler->setup(port, webSocketHandler, apMode); - LogHandler::debug(TagHandler::Main, "Web DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - } else { - displayPrint("WebServer disabled"); - LogHandler::info(TagHandler::Main, "WebServer disabled due to bluetooth and chip model"); - } - if (!apMode) {// mdns breaks apmode? - bool mdnsEnabled = MDNS_ENABLED_DEFAULT; - settingsFactory->getValue(MDNS_ENABLED, mdnsEnabled); - if(mdnsEnabled) - { - mdnsHandler.setup(hostname, friendlyName, port, udpPort); - LogHandler::debug(TagHandler::Main, "MDNS DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - } - } -} -#endif - -void startBLEConfig() -{ - // Disabled. Android Application needs maintenance - // if(!bleConfigurationHandler) { - // displayPrint("Starting BLE config"); - // bleConfigurationHandler = new BLEConfigurationHandler(); - // bleConfigurationHandler->setup(); - // } -} - -#if BLE_TCODE -void startBLETCode() -{ - if (!bleHandler) - { - displayPrint("Starting BLE"); - bleHandler = new BLEHandler(); - bleHandler->setup(); - } -} -#endif - -#if BLUETOOTH_TCODE -void startBlueTooth() -{ - if (bluetoothEnabled && !btHandler) - { - displayPrint("Starting Bluetooth serial"); - btHandler = new BluetoothHandler(); - btHandler->setup(); - } -} -#endif - -#if WIFI_TCODE -bool startUDPTCode(int port) -{ - if (!udpHandler) - { - displayPrint("Starting UDP"); - udpHandler = new Udphandler(); - if (!udpHandler->setup(port)) - return false; - } - return true; -} -#endif +extern WifiHandler wifi; void startConfigMode(const int &webPort, const int &udpPort, const char *hostname, const char *friendlyName) { @@ -415,13 +104,13 @@ void startConfigMode(const int &webPort, const int &udpPort, const char *hostnam char subnet[IP_ADDRESS_LEN]; char gateway[IP_ADDRESS_LEN]; - + SettingsFactory *settingsFactory = SettingsFactory::getInstance(); settingsFactory->getValue(AP_MODE_PASS, pass, WIFI_PASS_LEN); settingsFactory->getValue(AP_MODE_SUBNET, subnet, IP_ADDRESS_LEN); settingsFactory->getValue(AP_MODE_GATEWAY, gateway, IP_ADDRESS_LEN); settingsFactory->getValue(AP_MODE_HIDDEN, hidden); settingsFactory->getValue(AP_MODE_CHANNEL, channel); - if (wifi.startAp(hostname, settingsFactory->getAPModeSSID(), pass, channel, hidden, settingsFactory->getAPModeIP(), subnet, gateway)) + if (wifi.startAp(settingsFactory->getAPModeSSID(), pass, channel, hidden, settingsFactory->getAPModeIP(), subnet, gateway)) { displayPrint("APMode started"); startNetworking(SettingsHandler::apMode, webPort, udpPort, hostname, friendlyName); @@ -431,837 +120,180 @@ void startConfigMode(const int &webPort, const int &udpPort, const char *hostnam displayPrint("APMode start failed"); } #endif - -#if BLE_TCODE || BLUETOOTH_TCODE - if(bleEnabled || bluetoothEnabled) { - startBLEConfig(); - } -#endif } -#if WIFI_TCODE -void wifiStatusCallBack(WiFiStatus status, WiFiReason reason) -{ - if (status == WiFiStatus::CONNECTED) - { - LogHandler::debug(TagHandler::Main, "wifiStatusCallBack WiFiStatus::CONNECTED"); - if (reason == WiFiReason::AP_MODE) - { - LogHandler::debug(TagHandler::Main, "wifiStatusCallBack WiFiReason::AP_MODE"); - // if(bleConfigurationHandler) - // bleConfigurationHandler->stop(); // If a client connects to the ap stop the BLE to save memory. - } - } - else if(status == WiFiStatus::DISCONNECTED) - { - // wifi.dispose(); - // startApMode(); - LogHandler::debug(TagHandler::Main, "wifiStatusCallBack Not connected"); - if (reason == WiFiReason::NO_AP || reason == WiFiReason::UNKNOWN) - { - LogHandler::debug(TagHandler::Main, "wifiStatusCallBack WiFiReason::NO_AP || WiFiReason::UNKNOWN"); - startConfigMode( - settingsFactory->getWebServerPort(), - settingsFactory->getUdpServerPort(), - settingsFactory->getHostname(), - settingsFactory->getFriendlyName()); - } - else if (reason == WiFiReason::AUTH) - { - LogHandler::debug(TagHandler::Main, "wifiStatusCallBack WiFiReason::AUTH"); - LogHandler::warning(TagHandler::Main, "Connection auth failed: Resetting wifi password and restarting"); - settingsFactory->defaultValue(WIFI_PASS_SETTING); - ESP.restart(); - } - else if (reason == WiFiReason::AP_MODE) - { - LogHandler::debug(TagHandler::Main, "wifiStatusCallBack WiFiReason::AP_MODE"); - // #ifdef !ESP32_DA - // if(bleConfigurationHandler) - // bleConfigurationHandler->setup(); - // #endif - } - } - else if(status == WiFiStatus::IP) - { -#if BUILD_DISPLAY - if(displayHandler) - { - String ipaddress = wifi.ip().toString(); - displayPrint("Connected IP: " + ipaddress); - displayHandler->setLocalIPAddress(wifi.ip()); - } -#endif - } -} -#endif - -void batteryVoltageCallback(float capacityRemainingPercentage, float capacityRemaining, float voltage, float temperature) +// These go on the stack (always allocate all necessary memory) +FilesystemHandler filesystemHandler; +SerialHandler serialHandler; +WifiHandler wifi; +UdpHandler udpHandler; +ButtonHandler buttonHandler; +BatteryHandler batteryHandler; +HTTPBase* webHandler = nullptr; +WebSocketBase* webSocketHandler = nullptr; +bool networkingStarted = false; + +void displayPrint(const char* message) { -#if BUILD_DISPLAY - if (displayHandler) + if (message && message[0] != '\0') { - displayHandler->setBatteryInformation(capacityRemainingPercentage, voltage, temperature); + Serial.println(message); + LogHandler::info(Tags::Main, "%s", message); } -#endif -#if WIFI_TCODE - if (webSocketHandler) - { - String statusJson("{\"batteryCapacityRemaining\":\"" + String(capacityRemaining) + "\", \"batteryCapacityRemainingPercentage\":\"" + String(capacityRemainingPercentage) + "\", \"batteryVoltage\":\"" + String(voltage) + "\", \"batteryTemperature\":\"" + String(temperature) + "\"}"); - webSocketHandler->sendCommand("batteryStatus", statusJson.c_str()); - } -#endif } -void settingChangeCallback(const SettingProfile &profile, const char *settingThatChanged) -{ - LogHandler::verbose(TagHandler::Main, "settingChangeCallback: %s", settingThatChanged); - if (profile == SettingProfile::System) - { - if (!strcmp(settingThatChanged, LOG_LEVEL_SETTING)) - { - LogHandler::setLogLevel(settingsFactory->getLogLevel()); - } - else if (!strcmp(settingThatChanged, LOG_INCLUDETAGS)) - { - LogHandler::setIncludes(settingsFactory->getLogIncludes()); - } - else if (!strcmp(settingThatChanged, LOG_EXCLUDETAGS)) - { - LogHandler::setExcludes(settingsFactory->getLogExcludes()); - } - } - else if (profile == SettingProfile::MotionProfile) - { - if (strcmp(settingThatChanged, MOTION_PROFILE_SELECTED_INDEX) == 0 || strcmp(settingThatChanged, MOTION_PROFILES) == 0) { - motionHandler.setMotionChannels(SettingsHandler::getMotionChannels()); - //} else if(strcmp(settingThatChanged, "motionChannels") == 0) { - // motionHandler.setMotionChannels(SettingsHandler::getGetMotionChannels()()); - } else if (strcmp(settingThatChanged, MOTION_ENABLED) == 0) { - LogHandler::verbose(TagHandler::Main, "MOTION_ENABLED: %d", SettingsHandler::getMotionEnabled()); - motionHandler.setEnabled(SettingsHandler::getMotionEnabled()); - } - // else if(strcmp(settingThatChanged, "motionAmplitudeGlobal") == 0) - // motionHandler.setAmplitude(SettingsHandler::getGetMotionAmplitudeGlobal()()); - // else if(strcmp(settingThatChanged, "motionOffsetGlobal") == 0) - // motionHandler.setOffset(SettingsHandler::getGetMotionOffsetGlobal()()); - // else if(strcmp(settingThatChanged, "motionPeriodGlobal") == 0) - // motionHandler.setPeriod(SettingsHandler::getGetMotionPeriodGlobal()()); - // else if(strcmp(settingThatChanged, "motionUpdateGlobal") == 0) - // motionHandler.setUpdate(SettingsHandler::getGetMotionUpdateGlobal()()); - // else if(strcmp(settingThatChanged, "motionPhaseGlobal") == 0) - // motionHandler.setPhase(SettingsHandler::getGetMotionPhaseGlobal()()); - // else if(strcmp(settingThatChanged, "motionReversedGlobal") == 0) - // motionHandler.setReverse(SettingsHandler::getGetMotionReversedGlobal()()); - // else if(strcmp(settingThatChanged, "motionAmplitudeGlobalRandom") == 0) - // motionHandler.setAmplitudeRandom(SettingsHandler::getGetMotionAmplitudeGlobalRandom()()); - // else if(strcmp(settingThatChanged, "motionAmplitudeGlobalRandomMin") == 0) - // motionHandler.setAmplitudeRandomMin(SettingsHandler::getGetMotionAmplitudeGlobalRandomMin()()); - // else if(strcmp(settingThatChanged, "motionAmplitudeGlobalRandomMax") == 0) - // motionHandler.setAmplitudeRandomMax(SettingsHandler::getGetMotionAmplitudeGlobalRandomMax()()); - // else if(strcmp(settingThatChanged, "motionPeriodGlobalRandom") == 0) - // motionHandler.setPeriodRandom(SettingsHandler::getGetMotionPeriodGlobalRandom()()); - // else if(strcmp(settingThatChanged, "motionPeriodGlobalRandomMin") == 0) - // motionHandler.setPeriodRandomMin(SettingsHandler::getGetMotionPeriodGlobalRandomMin()()); - // else if(strcmp(settingThatChanged, "motionPeriodGlobalRandomMax") == 0) - // motionHandler.setPeriodRandomMax(SettingsHandler::getGetMotionPeriodGlobalRandomMax()()); - // else if(strcmp(settingThatChanged, "motionOffsetGlobalRandom") == 0) - // motionHandler.setOffsetRandom(SettingsHandler::getGetMotionOffsetGlobalRandom()()); - // else if(strcmp(settingThatChanged, "motionOffsetGlobalRandomMin") == 0) - // motionHandler.setOffsetRandomMin(SettingsHandler::getGetMotionOffsetGlobalRandomMin()()); - // else if(strcmp(settingThatChanged, "motionOffsetGlobalRandomMax") == 0) - // motionHandler.setOffsetRandomMax(SettingsHandler::getGetMotionOffsetGlobalRandomMax()()); - // else if(strcmp(settingThatChanged, "motionRandomChangeMin") == 0) - // motionHandler.setMotionRandomChangeMin(SettingsHandler::getGetMotionRandomChangeMin()()); - // else if(strcmp(settingThatChanged, "motionRandomChangeMax") == 0) - // motionHandler.setMotionRandomChangeMax(SettingsHandler::getGetMotionRandomChangeMax()()); - } - else if (voiceHandler && profile == SettingProfile::Voice) - { - if (strcmp(settingThatChanged, "voiceMuted") == 0) - { - voiceHandler->setMuteMode(settingsFactory->getVoiceMuted()); - } - else if (strcmp(settingThatChanged, "voiceVolume") == 0) - { - voiceHandler->setVolume(settingsFactory->getVoiceVolume()); - } - else if (strcmp(settingThatChanged, "voiceWakeTime") == 0) - { - voiceHandler->setWakeTime(settingsFactory->getVoiceWakeTime()); - } - } - else if (buttonHandler && profile == SettingProfile::Button) - { - if (strcmp(settingThatChanged, "bootButtonCommand") == 0) - buttonHandler->updateBootButtonCommand(settingsFactory->getBootButtonCommand()); - else if (strcmp(settingThatChanged, "analogButtonCommands") == 0) - { - buttonHandler->updateAnalogButtonCommands(settingsFactory->getButtonSets()); - } - else if (strcmp(settingThatChanged, "buttonAnalogDebounce") == 0) - { - buttonHandler->updateAnalogDebounce(settingsFactory->getButtonAnalogDebounce()); - } - } - else if (profile == SettingProfile::ChannelRanges) - { - if (strcmp(settingThatChanged, CHANNEL_PROFILE) == 0) { - // TODO add channe; specific updates when moving to its own save...maybe... - motionHandler.updateChannelRanges(); - } else if (strcmp(settingThatChanged, "channelRangesEnabled") == 0) { - webSocketHandler->sendCommand("channelRangesEnabled", SettingsHandler::getChannelRangesEnabled() ? "true" : "false"); - } - - } -} -void loadI2CModules(bool displayEnabled, bool batteryEnabled, bool voiceEnabled) -{ -#if BUILD_DISPLAY - if (displayEnabled) - { - LogHandler::debug(TagHandler::Main, "Start Display task"); - auto displayStatus = xTaskCreatePinnedToCore( - DisplayHandler::startLoop, /* Function to implement the task */ - "DisplayTask", /* Name of the task */ - configMINIMAL_STACK_SIZE * 4, /* Stack size in words used to be 5000 */ - displayHandler, /* Task input parameter */ - 1, /* Priority of the task */ - &displayTask, /* Task handle. */ - APP_CPU_NUM); /* Core where the task should run */ - if (displayStatus != pdPASS) - { - LogHandler::error(TagHandler::Main, "Could not start display task."); - } - } -#endif - if (batteryEnabled) - { - batteryHandler = new BatteryHandler(); - if (batteryHandler->setup()) - { - LogHandler::debug(TagHandler::Main, "Start Battery task"); - auto batteryStatus = xTaskCreatePinnedToCore( - BatteryHandler::startLoop, /* Function to implement the task */ - "BatteryTask", /* Name of the task */ - configMINIMAL_STACK_SIZE, /* Stack size in words used to be 4028 */ - batteryHandler, /* Task input parameter */ - 1, /* Priority of the task */ - &batteryTask, /* Task handle. */ - APP_CPU_NUM); /* Core where the task should run */ - if (batteryStatus != pdPASS) - { - LogHandler::error(TagHandler::Main, "Could not start battery task."); - } - batteryHandler->setMessageCallback(batteryVoltageCallback); - } - } - if (voiceEnabled) - { - voiceHandler = new VoiceHandler(); - voiceHandler->setMessageCallback(TCodeCommandCallback); - if (voiceHandler->setup()) - { - LogHandler::debug(TagHandler::Main, "Start Voice task"); - auto voiceStatus = xTaskCreatePinnedToCore( - VoiceHandler::startLoop, /* Function to implement the task */ - "VoiceTask", /* Name of the task */ - configMINIMAL_STACK_SIZE, /* Stack size in words used to be 4028 */ - voiceHandler, /* Task input parameter */ - 1, /* Priority of the task */ - &voiceTask, /* Task handle. */ - APP_CPU_NUM); /* Core where the task should run */ - if (voiceStatus != pdPASS) - { - LogHandler::error(TagHandler::Main, "Could not start voice task."); - } - } - } -} -void setup() +void startNetworking(bool apMode, int webPort, int udpPort, const char* hostname, const char* friendlyName) { + (void)udpPort; + (void)hostname; + (void)friendlyName; - // setCpuFrequencyMhz(240); - - // see if we can use the onboard led for status - // https://github.com/kriswiner/ESP32/blob/master/PWM/ledcWrite_demo_ESP32.ino - // digitalWrite(5, LOW);// Turn off on-board blue led - - Serial.begin(115200); - Serial.printf("Startup DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - - LogHandler::setLogLevel(LogLevel::INFO); - LogHandler::setMessageCallback(logCallBack); - - Serial.println(); - LogHandler::info(TagHandler::Main, "Firmware version: %s", FIRMWARE_VERSION_NAME); - // LogHandler::info(TagHandler::Main, "Esp arduino version: %s", ESP_ARDUINO_VERSION_STR); - LogHandler::info(TagHandler::Main, "ESP IDF version: %s", esp_get_idf_version()); - uint32_t chipId = 0; - for (int i = 0; i < 17; i = i + 8) + if (networkingStarted) { - chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; - } - LogHandler::info(TagHandler::Main, "ESP32 Chip model = %s Rev %d", ESP.getChipModel(), ESP.getChipRevision()); - LogHandler::info(TagHandler::Main, "This chip has %d cores", ESP.getChipCores()); - LogHandler::info(TagHandler::Main, "Chip ID: %u", chipId); - Serial.println(); - - // esp_log_level_set("*", ESP_LOG_VERBOSE); - // LogHandler::debug("main", "this is verbose"); - // LogHandler::debug("main", "this is debug"); - // LogHandler::info("main", "this is info"); - // LogHandler::warning("main", "this is warning"); - // LogHandler::error("main", "this is error"); - - if (!LittleFS.begin(true)) - { - LogHandler::error(TagHandler::Main, "An Error has occurred while mounting LittleFS"); - setupSucceeded = false; - return; - } - LogHandler::debug(TagHandler::Main, "LittleFS DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - - // LogHandler::setLogLevel(LogLevel::DEBUG); - settingsFactory = SettingsFactory::getInstance(); - settingsFactory->setMessageCallback(settingChangeCallback); - if (!settingsFactory->init()) - { - LogHandler::error(TagHandler::Main, "Failed to load settings..."); return; } - LogHandler::debug(TagHandler::Main, "Settings factory DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - // LogHandler::setLogLevel(LogLevel::DEBUG); - - const PinMap *pinMap = settingsFactory->getPins(); - - SettingsHandler::init(); - SettingsHandler::setMessageCallback(settingChangeCallback); - LogHandler::debug(TagHandler::Main, "Settings handler DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - taskManager.init(); - LogHandler::debug(TagHandler::Main, "Task manager DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); -#if BLE_TCODE - settingsFactory->getValue(BLE_ENABLED, bleEnabled); - - //bleEnabled = true; -#endif -#if BLUETOOTH_TCODE - settingsFactory->getValue(BLUETOOTH_ENABLED, bluetoothEnabled); - - //bluetoothEnabled = true; -#endif - -#if WIFI_TCODE - if ((!bluetoothEnabled && !bleEnabled) || COEXIST) - wifi.setWiFiStatusCallback(wifiStatusCallBack); -#endif - - // Get ConfigurationSettings - bool fanControlEnabled = FAN_CONTROL_ENABLED_DEFAULT; - settingsFactory->getValue(FAN_CONTROL_ENABLED, fanControlEnabled); - - // Cached (Requires reboot) - MotorType motorType; - BoardType boardType; - DeviceType deviceType; - settingsFactory->getValue(MOTOR_TYPE_SETTING, motorType); - settingsFactory->getValue(BOARD_TYPE_SETTING, boardType); - settingsFactory->getValue(DEVICE_TYPE, deviceType); - SettingsHandler::channelMap.init(settingsFactory->getTcodeVersion(), motorType, deviceType); - // bool lubeEnabled; - // bool feedbackTwist; - // bool analogTwist; - bool bootButtonEnabled = BOOT_BUTTON_ENABLED_DEFAULT; - bool buttonSetsEnabled = BUTTON_SETS_ENABLED_DEFAULT; - - // settingsFactory->getValue(FEEDBACK_TWIST, feedbackTwist); - // settingsFactory->getValue(ANALOG_TWIST, analogTwist); - settingsFactory->getValue(BOOT_BUTTON_ENABLED, bootButtonEnabled); - settingsFactory->getValue(BUTTON_SETS_ENABLED, buttonSetsEnabled); - - bool batteryLevelEnabled = BATTERY_LEVEL_ENABLED_DEFAULT; - bool voiceEnabled = VOICE_ENABLED_DEFAULT; - settingsFactory->getValue(BATTERY_LEVEL_ENABLED, batteryLevelEnabled); - settingsFactory->getValue(VOICE_ENABLED, voiceEnabled); - - bool displayEnabled = DISPLAY_ENABLED_DEFAULT; - settingsFactory->getValue(DISPLAY_ENABLED, displayEnabled); - char Display_I2C_AddressString[DISPLAY_I2C_ADDRESS_LEN] = {0}; - settingsFactory->getValue(DISPLAY_I2C_ADDRESS, Display_I2C_AddressString, DISPLAY_I2C_ADDRESS_LEN); - int Display_I2C_Address = (int)strtol(Display_I2C_AddressString, NULL, 0); - - systemCommandHandler = new SystemCommandHandler(); - systemCommandHandler->registerExternalCommandCallback(TCodePassthroughCommandCallback); - LogHandler::debug(TagHandler::Main, "System command handler DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - -#if MOTOR_TYPE == 0 - if (settingsFactory->getTcodeVersion() == TCodeVersion::v0_3) + if (!webHandler) { - motorHandler = new ServoHandler0_3(); - } - else if (settingsFactory->getTcodeVersion() == TCodeVersion::v0_4) - { - motorHandler = new ServoHandler0_4(); - } -#if !DEBUG_BUILD && TCODE_V2 - // else if(settingsFactory->getTcodeVersion() == TCodeVersion::v0_2) - // motorHandler = new ServoHandler0_2(); -#endif - else - { - LogHandler::error(TagHandler::Main, "Invalid TCode version: %ld", settingsFactory->getTcodeVersion()); - return; // TODO: this stops apmode and not what we want - } -#elif MOTOR_TYPE == 1 - if (settingsFactory->getTcodeVersion() == TCodeVersion::v0_3) - { - motorHandler = new BLDCHandler0_3(); - } - else if (settingsFactory->getTcodeVersion() == TCodeVersion::v0_4) - { - motorHandler = new BLDCHandler0_4(); - } +#if !SECURE_WEB + webHandler = new WebHandler(); + webSocketHandler = new WebSocketHandler(); #else - LogHandler::error(TagHandler::Main, "Invalid motor type defined!"); - return; + webHandler = new HTTPSHandler(); + webSocketHandler = new SecureWebSocketHandler(); #endif - - motorHandler->setMessageCallback(TCodeCommandCallback); - LogHandler::debug(TagHandler::Main, "Motor handler DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - // SystemCommandHandler::registerOtherCommandCallback(TCodeCommandCallback); - -#if BUILD_TEMP - bool sleeveTempEnabled; - bool internalTempEnabled; - int8_t heaterChannel = pinMap->heaterChannel(); - int heaterFrequency = heaterChannel > -1 ? pinMap->getChannelFrequency(pinMap->heaterChannel()) : ESP_TIMER_FREQUENCY_DEFAULT; - int heaterResolution; - float heaterThreshold; - int8_t caseFanChannel = pinMap->caseFanChannel(); - int caseFanFrequency = caseFanChannel > -1 ? pinMap->getChannelFrequency(pinMap->caseFanChannel()) : ESP_TIMER_FREQUENCY_DEFAULT; - int caseFanResolution; - int caseFanMaxPWM; - settingsFactory->getValue(TEMP_SLEEVE_ENABLED, sleeveTempEnabled); - settingsFactory->getValue(TEMP_INTERNAL_ENABLED, internalTempEnabled); - settingsFactory->getValue(HEATER_RESOLUTION, heaterResolution); - settingsFactory->getValue(HEATER_THRESHOLD, heaterThreshold); - settingsFactory->getValue(CASE_FAN_RESOLUTION, caseFanResolution); - settingsFactory->getValue(CASE_FAN_MAX_PWM, caseFanMaxPWM); - if (sleeveTempEnabled || internalTempEnabled || fanControlEnabled) - { - taskManager.priority(new TemperatureHandler(internalTempEnabled, - sleeveTempEnabled, - pinMap->sleeveTemp(), - pinMap->internalTemp(), - pinMap->heater(), - heaterChannel, - pinMap->caseFan(), - caseFanChannel, - heaterFrequency, - heaterResolution, - fanControlEnabled, - caseFanFrequency, - caseFanResolution, - caseFanMaxPWM)); - LogHandler::debug(TagHandler::Main, "Start temperature task"); - auto tempStartStatus = xTaskCreatePinnedToCore( - TemperatureHandler::startLoop, /* Function to implement the task */ - "TempTask", /* Name of the task */ - static_cast(configMINIMAL_STACK_SIZE * 3), /* Stack size in words used to be 5000 */ - temperatureHandler, /* Task input parameter */ - 1, /* Priority of the task */ - &temperatureTask, /* Task handle. */ - APP_CPU_NUM); /* Core where the task should run */ - if (tempStartStatus != pdPASS) - { - LogHandler::error(TagHandler::Main, "Could not start temperature task."); - } - LogHandler::debug(TagHandler::Main, "Temp DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); } -#endif -#if BUILD_DISPLAY - taskManager.lazy(new DisplayHandler(Display_I2C_Address, fanControlEnabled, pinMap->displayReset())); - LogHandler::debug(TagHandler::Main, "Display DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); -#endif - -#if BLE_TCODE - if (bleEnabled) - { - startBLETCode(); - } - else - { - BLEHandler::disable(); - } - LogHandler::debug(TagHandler::Main, "BLE DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); -#else - esp_bt_controller_mem_release(ESP_BT_MODE_BTDM) -#endif -#if BLUETOOTH_TCODE - if (bluetoothEnabled) - { - startBlueTooth(); - } - else + if (webHandler && webSocketHandler) { - BluetoothHandler::disable(); + webHandler->setup_http(webPort, webSocketHandler, apMode); + networkingStarted = true; + LogHandler::info(Tags::Main, "Configuration interfaces started on port %d (%s mode)", webPort, apMode ? "AP" : "STA"); } - LogHandler::debug(TagHandler::Main, "Bluetooth DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); -#else - esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); -#endif - -#if BLE_TCODE || BLUETOOTH_TCODE - if (WIFI_TCODE && !COEXIST && (bluetoothEnabled || bleEnabled)) - { - WifiHandler::disable(); - LogHandler::debug(TagHandler::Main, "Wifi disable DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - } -#endif +} +void ensureNetworkingAvailable() +{ #if WIFI_TCODE - if ((!bluetoothEnabled && !bleEnabled) || COEXIST) - { - char ssid[SSID_LEN]; - char wifiPass[WIFI_PASS_LEN]; - bool staticIP; - char localIP[IP_ADDRESS_LEN]; - char gateway[IP_ADDRESS_LEN]; - char subnet[IP_ADDRESS_LEN]; - char dns1[IP_ADDRESS_LEN]; - char dns2[IP_ADDRESS_LEN]; + SettingsFactory* settingsFactory = SettingsFactory::getInstance(); + const int webPort = settingsFactory->getWebServerPort(); + const int udpPort = settingsFactory->getUdpServerPort(); + const char* hostname = settingsFactory->getHostname(); + const char* friendlyName = settingsFactory->getFriendlyName(); - settingsFactory->getValue(SSID_SETTING, ssid, SSID_LEN); - settingsFactory->getValue(WIFI_PASS_SETTING, wifiPass, WIFI_PASS_LEN); - settingsFactory->getValue(STATICIP, staticIP); - settingsFactory->getValue(LOCALIP, localIP, IP_ADDRESS_LEN); - settingsFactory->getValue(GATEWAY, gateway, IP_ADDRESS_LEN); - settingsFactory->getValue(SUBNET, subnet, IP_ADDRESS_LEN); - settingsFactory->getValue(DNS1, dns1, IP_ADDRESS_LEN); - settingsFactory->getValue(DNS2, dns2, IP_ADDRESS_LEN); - if (strcmp(wifiPass, WIFI_PASS_DONOTCHANGE_DEFAULT) != 0 && strlen(ssid)) - { - displayPrint("Setting up wifi..."); - LogHandler::info(TagHandler::Main, "Setting up wifi..."); - displayPrint("Connecting to: "); - LogHandler::info(TagHandler::Main, "Connecting to: %s", ssid); - displayPrint(ssid); - if (wifi.connect(settingsFactory->getHostname(), ssid, wifiPass)) - { -// String ipaddress = wifi.ip().toString(); -// displayPrint("Connected IP: " + ipaddress); -// LogHandler::info(TagHandler::Main, "Connected IP: %s", ipaddress.c_str()); -// #if BUILD_DISPLAY -// displayHandler->setLocalIPAddress(wifi.ip()); -// #endif - if (!startUDPTCode(settingsFactory->getUdpServerPort())) - { - LogHandler::error(TagHandler::Main, "Error starting UDP server!"); - return; - } - LogHandler::debug(TagHandler::Main, "UDP DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - startNetworking(false, - settingsFactory->getWebServerPort(), - settingsFactory->getUdpServerPort(), - settingsFactory->getHostname(), - settingsFactory->getFriendlyName()); - } - } - else - { - startConfigMode( - settingsFactory->getWebServerPort(), - settingsFactory->getUdpServerPort(), - settingsFactory->getHostname(), - settingsFactory->getFriendlyName()); - } - } -#endif - // otaHandler.setup(); - displayPrint("Setting up motor"); - motorHandler->setup(); - LogHandler::debug(TagHandler::Main, "Motor DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - motionHandler.setup(settingsFactory->getTcodeVersion()); - LogHandler::debug(TagHandler::Main, "Motion handler DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - loadI2CModules(displayEnabled, batteryLevelEnabled, voiceEnabled); - LogHandler::debug(TagHandler::Main, "I2C DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); + char ssid[SSID_LEN] = { 0 }; + char pass[WIFI_PASS_LEN] = { 0 }; + settingsFactory->getValue(SSID_SETTING, ssid, sizeof(ssid)); + settingsFactory->getValue(WIFI_PASS_SETTING, pass, sizeof(pass)); - if (bootButtonEnabled || buttonSetsEnabled) + bool connectedToSta = false; + if (strlen(ssid) > 0 && strcmp(ssid, SSID_DEFAULT) != 0) { - buttonHandler = new ButtonHandler(); - buttonHandler->init(settingsFactory->getButtonAnalogDebounce(), - settingsFactory->getBootButtonCommand(), - settingsFactory->getButtonSets()); + displayPrint("Attempting WiFi STA connection"); + connectedToSta = wifi.connect(ssid, pass); } - setupSucceeded = true; - LogHandler::debug(TagHandler::Main, "Setup finished"); - SettingsHandler::printFree(); -} - -// Main loop functions///////////////////////////////////////////////// -void readTCode(String &tcode) -{ - if (motorHandler) + if (connectedToSta) { - motorHandler->read(tcode); - tcode.clear(); + SettingsHandler::apMode = false; + String staIp = wifi.ip().toString(); + LogHandler::info(Tags::Main, "WiFi connected, IP Address: %s", staIp.c_str()); + String staMsg = "WiFi connected: " + staIp; + displayPrint(staMsg.c_str()); + startNetworking(false, webPort, udpPort, hostname, friendlyName); + return; } -} -void readTCode(char *tcode, int len) -{ - if (motorHandler) + displayPrint("Starting AP configuration mode"); + startConfigMode(webPort, udpPort, hostname, friendlyName); + if (WifiHandler::apMode()) { - motorHandler->read(tcode, len); - tcode[0] = {0}; + String apIp = WiFi.softAPIP().toString(); + LogHandler::info(Tags::Main, "Captive portal active, AP IP Address: %s", apIp.c_str()); + String apMsg = "Captive portal IP: " + apIp; + displayPrint(apMsg.c_str()); } -} -void processButton() -{ - if (buttonHandler) + if (!WifiHandler::apMode()) { - buttonHandler->read(buttonCommand); - if (buttonCommand) + LogHandler::warning(Tags::Main, "Primary AP startup failed, retrying with defaults"); + if (wifi.startAp(AP_MODE_SSID_DEFAULT, AP_MODE_PASS_DEFAULT, AP_MODE_CHANNEL_DEFAULT, AP_MODE_HIDDEN_DEFAULT, AP_MODE_IP_DEFAULT, AP_MODE_SUBNET_DEFAULT, AP_MODE_GATEWAY_DEFAULT)) { - char command[MAX_COMMAND]; - systemCommandHandler->process(buttonCommand, command); - if (strlen(command) > 0) - { - readTCode(command, strlen(command)); - } + SettingsHandler::apMode = true; + String apIp = WiFi.softAPIP().toString(); + LogHandler::info(Tags::Main, "Captive portal active, AP IP Address: %s", apIp.c_str()); + String apMsg = "Captive portal IP: " + apIp; + displayPrint(apMsg.c_str()); + startNetworking(true, webPort, udpPort, hostname, friendlyName); } } -} - -void getTCodeInput() -{ - if (Serial.available() > 0) - { - serialData = Serial.readStringUntil('\n'); - if(!serialData.isEmpty()) - serialData += '\n'; - } - else if (serialData.length()) - { - serialData.clear(); - } - if (systemCommandHandler) - { - systemCommandHandler->getTCode(commandTCodeData); - } -#if BLUETOOTH_TCODE - if (btHandler && btHandler->isConnected() && btHandler->available() > 0) - { - bluetoothData = btHandler->readStringUntil('\n'); - } -#endif -#if WIFI_TCODE - if (webSocketHandler) - { - benchStart(1); - webSocketHandler->getTCode(webSocketData); - benchFinish("Websocket get", 1); - } - if (udpHandler) - { - benchStart(2); - udpHandler->read(udpData); - benchFinish("Udp get", 2); - } -#endif -#if BLE_TCODE - if (bleHandler) - { - bleHandler->read(bleData); - } #endif } -void processCommand() +void setup() { - // Read and process tcode $ and # commands - if (serialData.length() > 0) - { - if (systemCommandHandler && systemCommandHandler->isCommand(serialData.c_str())) - { - // systemCommandHandler->process(serialData.c_str()); - readTCode(serialData); - } - } -#if BLUETOOTH_TCODE - if (bluetoothData.length() > 0) - { - if (systemCommandHandler && systemCommandHandler->isCommand(bluetoothData.c_str())) - { - // systemCommandHandler->process(bluetoothData.c_str()); - readTCode(bluetoothData); - } - } -#endif -#if WIFI_TCODE - else if (strlen(udpData) > 0 && systemCommandHandler && systemCommandHandler->isCommand(udpData)) - { - // systemCommandHandler->process(udpData); - readTCode(udpData, strlen(udpData)); - } - else if (strlen(webSocketData) > 0 && systemCommandHandler && systemCommandHandler->isCommand(webSocketData)) + Serial.begin(115200); + Serial.println("BOOT: setup entered"); + Serial.println(); + LogHandler::setLogLevel(LogLevel::INFO); + LogHandler::info(Tags::Main, "Firmware version: %s", FIRMWARE_VERSION_NAME); + uint32_t chipId = 0; + for (int i = 0; i < 17; i = i + 8) { - // systemCommandHandler->process(webSocketData); - readTCode(webSocketData, strlen(webSocketData)); + chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; } -#endif -} + LogHandler::info(Tags::Main, "ESP32 Chip model = %s Rev %d", ESP.getChipModel(), ESP.getChipRevision()); + LogHandler::info(Tags::Main, "This chip has %d cores", ESP.getChipCores()); + LogHandler::info(Tags::Main, "Chip ID: %u", chipId); + Serial.println(); -void processMotionHandlerMovement() -{ - motionHandler.getMovement(movement, MAX_COMMAND); - if (strlen(movement) > 0) - { - LogHandler::verbose(TagHandler::MainLoop, "motion handler writing: %s", movement); - readTCode(movement, strlen(movement)); - } + Serial.println("BOOT: FilesystemHandler::init"); + FilesystemHandler::init(); + Serial.println("BOOT: SettingsHandler::init"); + SettingsHandler::init(); + Serial.println("BOOT: SerialHandler::init"); + SerialHandler::init(); + + TaskHandler::Manager& taskManager = TaskHandler::global(); + Serial.println("BOOT: Registering tasks"); + LogHandler::info(Tags::Main, "Initializing tasks"); + taskManager.critical(&serialHandler); // Ensure serial commands always stay responsive + LogHandler::info(Tags::Main, "Serial handler initialized"); + taskManager.critical(&buttonHandler); // Command/input path + LogHandler::info(Tags::Main, "Button handler initialized"); + taskManager.priority(&wifi); // Network connection state machine + LogHandler::info(Tags::Main, "WiFi handler initialized"); + taskManager.priority(&udpHandler); // TCode ingress/egress transport + LogHandler::info(Tags::Main, "UDP handler initialized"); + taskManager.auxiliary(&batteryHandler); // Low-priority telemetry polling + LogHandler::info(Tags::Main, "Battery handler initialized"); + // Handles advanced fuctions (motor, ota, wifi, etc) + Serial.println("BOOT: OperatingModeHandler::init"); + OperatingModeHandler::init(); + LogHandler::info(Tags::Main, "Operating mode handler initialized"); + Serial.println("BOOT: taskManager.start"); + taskManager.start(); + LogHandler::info(Tags::Main, "Tasks started"); + Serial.println("BOOT: network bring-up"); + ensureNetworkingAvailable(); + Serial.println("BOOT: setup complete"); } void loop() { - // if(setupSucceeded && SettingsHandler::getSaving()) { - // motorHandler->execute(); - // vTaskDelay(250/portTICK_PERIOD_MS); - // return; - // } - // LogHandler::verbose(TagHandler::MainLoop, "Enter loop ############################################"); - tcodeV2Recieved = false; - benchStart(0); - if (SettingsHandler::restartRequired > -1 || restarting) - { // check the flag here to determine if a restart is required - if (SettingsHandler::restartRequired <= 0 && !restarting) - { - LogHandler::info(TagHandler::Main, "Restarting ESP"); - ESP.restart(); - restarting = true; - } - else - { - LogHandler::info(TagHandler::Main, "Restarting ESP in: %ld", SettingsHandler::restartRequired); - } - vTaskDelay(1000 / portTICK_PERIOD_MS); - SettingsHandler::restartRequired--; - } -#if BUILD_TEMP - else if (temperatureHandler && temperatureHandler->isMaxTempTriggered()) + TaskHandler::global().update(); + + if (SettingsHandler::restartRequired >= 0 && !restarting) { - char stop[7] = "DSTOP\n"; - readTCode(stop, 7); - LogHandler::error(TagHandler::Main, "Internal temp has reached maximum user set. Main loop disabled! Restart system to enable the loop."); - temperatureHandler->setFanState(); - vTaskDelay(5000 / portTICK_PERIOD_MS); + restarting = true; + restartAtMs = millis() + (static_cast(SettingsHandler::restartRequired) * 1000UL); + LogHandler::info(Tags::Main, "Restart scheduled in %d second(s)", SettingsHandler::restartRequired); } -#endif - else - { - if (setupSucceeded) - { - // otaHandler.handle(); - - getTCodeInput(); // Must be executed first! - - processButton(); - - processCommand(); - if (!SettingsHandler::getMotionPaused()) - { - dStopped = false; - benchStart(3); - if (SettingsHandler::getMotionEnabled()) - { // Motion overrides all other input - processMotionHandlerMovement(); - } - else if (strlen(commandTCodeData) > 0) - { - LogHandler::verbose(TagHandler::MainLoop, "system command tcode writing: %s", commandTCodeData); - readTCode(commandTCodeData, strlen(commandTCodeData)); - } - else if (serialData.length() > 0) - { - LogHandler::verbose(TagHandler::MainLoop, "serial writing: %s", serialData.c_str()); - readTCode(serialData); -#if WIFI_TCODE == 1 - } - else if (strlen(webSocketData) > 0) - { - LogHandler::verbose(TagHandler::MainLoop, "webSocket writing: %s", webSocketData); - readTCode(webSocketData, strlen(webSocketData)); - } - else if (!SettingsHandler::apMode && strlen(udpData) > 0) - { - benchStart(6); - LogHandler::verbose(TagHandler::MainLoop, "udp writing: %s", udpData); - readTCode(udpData, strlen(udpData)); - benchFinish("Udp write", 6); -#endif -#if BLE_TCODE - } - else if (strlen(bleData) > 0) - { - LogHandler::verbose(TagHandler::MainLoop, "BLE writing: %s", bleData); - readTCode(bleData, strlen(bleData)); -#endif -#if BLUETOOTH_TCODE - } - else if (bluetoothData.length() > 0) - { - LogHandler::verbose(TagHandler::MainLoop, "bluetooth writing: %s", bluetoothData.c_str()); - readTCode(bluetoothData); -#endif - } - benchFinish("Input check", 3); - } - else if (!dStopped) - { // All motion is paused execute stop. - // movement[0] = {0}; - // udpData[0] = {0}; - // webSocketData[0] = {0}; - // serialData.clear(); - char stop[7] = "DSTOP\n"; - readTCode(stop, 7); - dStopped = true; - tcodeV2Recieved = false; -#if BLE_TCODE - // bleData = {0}; -#endif -#if BLUETOOTH_TCODE - // bluetoothData.clear(); -#endif - } - - benchStart(4); - if (motorHandler) - motorHandler->execute(); - benchFinish("Execute", 4); - -#if BUILD_TEMP - benchStart(5); - if (temperatureHandler && temperatureHandler->isRunning()) - { - temperatureHandler->setHeaterState(); - temperatureHandler->setFanState(); - } - benchFinish("Temp check", 5); -#endif - } - } - if (!setupSucceeded) + if (restarting && millis() >= restartAtMs) { - LogHandler::error(TagHandler::Main, "There was an issue in setup"); - vTaskDelay(5000 / portTICK_PERIOD_MS); - } else { - //xTaskDelayUntil(&pxPreviousWakeTime, 10/portTICK_PERIOD_MS); + LogHandler::info(Tags::Main, "Restarting now"); + delay(50); + ESP.restart(); } - - benchFinish("Main loop", 0); } diff --git a/ESP32/src/messages/MessageHandler.h b/ESP32/src/messages/MessageHandler.h new file mode 100644 index 0000000..e951009 --- /dev/null +++ b/ESP32/src/messages/MessageHandler.h @@ -0,0 +1,110 @@ +#ifndef _MESSAGE_HANDLER_H_ +#define _MESSAGE_HANDLER_H_ + +#include +#include + +#include "utils.h" +#include "logging/TagHandler.h" +#include "logging/LogHandler.h" + +namespace Messages +{ + // A message structure for inter-task communication + struct message_t + { + uint32_t tag_mask; + union + { + /* data */ + uint32_t u_data; + int32_t s_data; + float f_data; + }; + const char *message; + }; + + // A message sink buffers and processes messages + constexpr size_t MESSAGE_SINK_DEFAULT_BUFFER_SIZE = 10; + class MessageSink + { + private: + uint32_t _tags_include; + uint32_t _tags_exclude; + Ringbuffer _message_buffer; + message_t _current_message; + public: + MessageSink(uint32_t include_tags = Tags::ALL_TAGS, uint32_t exclude_tags = 0) : _tags_include(include_tags), _tags_exclude(exclude_tags) {} + + void handle_msg(uint32_t tags, message_t msg) + { + if ((_tags_include & tags) && !(_tags_exclude & tags)) + { + if (!_message_buffer.push(msg)) + { + const char* message = msg.message ? msg.message : "(null)"; + LogHandler::error(Tags::MessageQueue, "Sink message buffer full, dropping message: %s", message); + } + } + } + + message_t* next_message() + { + if (_message_buffer.pop(_current_message)) + { + return &_current_message; + } + return nullptr; + } + }; + + // Routes messages to registered sinks + class MessageRouter + { + private: + inline static MessageRouter* _singleton = nullptr; + std::vector _sinks; + public: + MessageRouter() { + if (!_singleton) { + _singleton = this; + } + } + + void push_sink(MessageSink* sink) + { + _sinks.push_back(sink); + } + + void send(uint32_t tags, const char *msg) + { + message_t _msg{ + .tag_mask = tags, + .s_data = -1, + .message = msg, + }; + send(_msg); + } + + void send(message_t msg) + { + for (auto &sink : _sinks) + { + if (sink) + { + sink->handle_msg(msg.tag_mask, msg); + } + } + } + + static MessageRouter *getInstance() + { + if (!_singleton) { + _singleton = new MessageRouter(); + } + return _singleton; + } + }; +}; + +#endif // _MESSAGE_HANDLER_H_ diff --git a/ESP32/src/network/HTTP/HTTPBase.h b/ESP32/src/network/HTTP/HTTPBase.h new file mode 100644 index 0000000..71726ff --- /dev/null +++ b/ESP32/src/network/HTTP/HTTPBase.h @@ -0,0 +1,11 @@ +#pragma once +#include "WebSocketBase.h" +#include "tasks/TaskHandler.h" +class HTTPBase : public TaskHandler::Task +{ +public: + HTTPBase() : Task(TaskHandler::Rates::ONDEMAND) {} + virtual void setup_http(uint16_t port, WebSocketBase *webSocketHandler, bool apMode) = 0; + virtual void stop() = 0; + virtual bool isRunning() = 0; +}; \ No newline at end of file diff --git a/ESP32/src/network/NetworkHandler.h b/ESP32/src/network/NetworkHandler.h new file mode 100644 index 0000000..6753611 --- /dev/null +++ b/ESP32/src/network/NetworkHandler.h @@ -0,0 +1,59 @@ + + +class NetworkHandler : public TaskHandler::Task +{ +private: + const bool &apMode; + const int &port; + const int &udpPort; + const char *hostname; + const char *friendlyName; + MDNSHandler mdnsHandler; + HTTPBase *webHandler = nullptr; + WebSocketBase* webSocketHandler = nullptr; + +public: + NetworkHandler(const bool &apMode, const int &port, const int &udpPort, const char *hostname, const char *friendlyName) + : apMode(apMode), port(port), udpPort(udpPort), hostname(hostname), friendlyName(friendlyName) + { + } + void setup() override + { + LogHandler::info(Tags::Network, "Setting up network handler"); + // Network setup code here + } + + void loop() override + { + // Network handling code here + } + + void start() + { + if ((MODULE_CURRENT != ModuleType::WROOM32 || (!bluetoothEnabled && !bleEnabled)) && !webHandler) + { + displayPrint("Starting web server"); +#if !SECURE_WEB + webHandler = new WebHandler(); + webSocketHandler = new WebSocketHandler(); +#else + LogHandler::debug(Tags::Main, "Start https task"); + webHandler = new HTTPSHandler(); + webSocketHandler = new SecureWebSocketHandler(); + TaskHandler::global().priority(static_cast(webHandler)); +#endif + webHandler->setup(port, webSocketHandler, apMode); + LogHandler::debug(Tags::Main, "Web DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); + } + else + { + displayPrint("WebServer disabled"); + LogHandler::info(Tags::Main, "WebServer disabled due to bluetooth and chip model"); + } + if (!apMode) + { // mdns breaks apmode? + mdnsHandler.setup(hostname, friendlyName, port, udpPort); + LogHandler::debug(Tags::Main, "MDNS DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); + } + } +} \ No newline at end of file diff --git a/ESP32/src/network/WebHandler.h b/ESP32/src/network/WebHandler.h new file mode 100644 index 0000000..6aa3ce2 --- /dev/null +++ b/ESP32/src/network/WebHandler.h @@ -0,0 +1,572 @@ +/* MIT License + +Copyright (c) 2024 Jason C. Fain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +#pragma once + +#include +#include +#include +#if ESP8266 == 1 +#include +#else +#include +#endif +#include +#include "HTTP/HTTPBase.h" +#include "network/WifiHandler.h" +#include "WebSocketHandler.h" +#include "logging/TagHandler.h" +#include "messages/SystemCommandHandler.h" +#include "tasks/TaskHandler.h" +#if !CONFIG_HTTPD_WS_SUPPORT +#error This example cannot be used unless HTTPD_WS_SUPPORT is enabled in esp-http-server component configuration +#endif +class WebHandler : public HTTPBase, public TaskHandler::Task +{ +public: + WebHandler() : Task(TaskHandler::Rates::ONDEMAND) {} + // bool MDNSInitialized = false; + void setup_http(uint16_t port, WebSocketBase *webSocketHandler, bool apMode) override + { + stop(); + if (port < 1 || port > 65535) + port = 80; + LogHandler::info(Tags::Web, "Starting web server on port: %i", port); + server = new AsyncWebServer(port); + ((WebSocketHandler *)webSocketHandler)->setup(server); + m_settingsFactory = SettingsFactory::getInstance(); + server->on("/wifiSettings", HTTP_GET, [](AsyncWebServerRequest *request) + { + char info[550]; + SettingsHandler::getWifiInfo(info); + if (strlen(info) == 0) { + AsyncWebServerResponse *response = request->beginResponse(504, "application/text", "Error getting wifi settings"); + request->send(response); + return; + } + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", info); + request->send(response); }); + + server->on("/settings", HTTP_GET, [this](AsyncWebServerRequest *request) + { + // request->send(LittleFS, COMMON_SETTINGS_PATH, "application/json"); + sendChunked(request, COMMON_SETTINGS_PATH); }); + + server->on("/pins", HTTP_GET, [this](AsyncWebServerRequest *request) + { + request->send(LittleFS, PIN_SETTINGS_PATH, "application/json"); + // sendChunked(request, PIN_SETTINGS_PATH); + }); + + server->on("/systemInfo", HTTP_GET, [](AsyncWebServerRequest *request) + { + if(SettingsHandler::restartRequired > -1 || !SettingsHandler::initialized) { + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"status\": \"restarting\"}"); + request->send(response); + return; + } + String systemInfo; + SettingsHandler::getSystemInfo(systemInfo); + if (!systemInfo.length()) { + AsyncWebServerResponse *response = request->beginResponse(504, "application/text", "Error getting user settings"); + request->send(response); + return; + } + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", systemInfo.c_str()); + request->send(response); }); + + server->on("/motionProfiles", HTTP_GET, [this](AsyncWebServerRequest *request) + { + // request->send(LittleFS, MOTION_PROFILE_SETTINGS_PATH, "application/json"); + sendChunked(request, MOTION_PROFILE_SETTINGS_PATH); }); + + server->on("/channelsProfile", HTTP_GET, [this](AsyncWebServerRequest *request) + { + // request->send(LittleFS, CHANNELS_SETTINGS_PATH, "application/json"); + sendChunked(request, CHANNELS_SETTINGS_PATH); }); + + server->on("/buttonSettings", HTTP_GET, [this](AsyncWebServerRequest *request) + { + // request->send(LittleFS, BUTTON_SETTINGS_PATH, "application/json"); + sendChunked(request, BUTTON_SETTINGS_PATH); }); + + // server->on("/log", HTTP_GET, [this](AsyncWebServerRequest *request) + // { + // Serial.println("Get log..."); + // //request->send(LittleFS, LOG_PATH); + // sendChunked(request, LOG_PATH); + // }); + + // server->on("/connectWifi", HTTP_POST, [this](AsyncWebServerRequest *request) + // { + // WifiHandler wifi; + // //const size_t capacity = JSON_OBJECT_SIZE(2); + // JsonDocument doc; + // char ssid[SSID_LEN] = {0}; + // char pass[WIFI_PASS_LEN] = {0}; + // m_settingsFactory->getValue(SSID_SETTING, ssid, SSID_LEN); + // m_settingsFactory->getValue(WIFI_PASS_SETTING, pass, WIFI_PASS_LEN); + // if (wifi.connect(ssid, pass)) + // { + + // doc["connected"] = true; + // doc["IPAddress"] = wifi.ip().toString(); + // } + // else + // { + // doc["connected"] = false; + // doc["IPAddress"] = "0.0.0.0"; + + // } + // String output; + // serializeJson(doc, output); + // AsyncWebServerResponse *response = request->beginResponse(200, "application/json", output); + // request->send(response); + // }); + + // server->on("/toggleContinousTwist", HTTP_POST, [this](AsyncWebServerRequest *request) + // { + // m_settingsFactory->setValue(CONTINUOUS_TWIST, !m_settingsFactory->getContinuousTwist()); + // if (m_settingsFactory->saveCommon()) + // { + // char returnJson[45]; + // sprintf(returnJson, "{\"msg\":\"done\", \"continousTwist\":%s }", m_settingsFactory->getContinuousTwist() ? "true" : "false"); + // AsyncWebServerResponse *response = request->beginResponse(200, "application/json", returnJson); + // request->send(response); + // } + // else + // { + // AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"Error saving settings\"}"); + // request->send(response); + // } + // }); + + server->on("^\\/sensor\\/([0-9]+)$", HTTP_GET, [](AsyncWebServerRequest *request) + { String sensorId = request->pathArg(0); }); + + server->on("^\\/changeBoard\\/([0-9]+)$", HTTP_POST, [this](AsyncWebServerRequest *request) + { + auto boardTypeString = request->pathArg(0); + int boardType = boardTypeString.isEmpty() ? (int)BoardType::DEVKIT : boardTypeString.toInt(); + // if(boardType == (int)BoardType::CRIMZZON || boardType == (int)BoardType::ISAAC) { + // m_settingsFactory->setValue(DEVICE_TYPE, DeviceType::SR6); + // } else if(boardType == (int)BoardType::SSR1PCB) { + // m_settingsFactory->setValue(DEVICE_TYPE, DeviceType::SSR1); + // m_settingsFactory->setValue(BLDC_ENCODER, BLDCEncoderType::MT6701); + // } + // Serial.println("Settings pinout default"); + // m_settingsFactory->setValue(BOARD_TYPE_SETTING, boardType); + if(m_settingsFactory->changeBoardType(boardType)) + { + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); + request->send(response); + } + else + { + AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"msg\":\"Error changing board type\"}"); + request->send(response); + } }); + server->on("^\\/changeDevice\\/([0-9]+)$", HTTP_POST, [this](AsyncWebServerRequest *request) + { + auto deviceTypeString = request->pathArg(0); + int deviceType = deviceTypeString.isEmpty() ? (int)DeviceType::OSR : deviceTypeString.toInt(); + // Serial.println("Settings pinout default"); + // m_settingsFactory->setValue(DEVICE_TYPE, deviceType); + // if(m_settingsFactory->saveCommon() && m_settingsFactory->defaultPinout()) + if (m_settingsFactory->changeDeviceType(deviceType)) + { + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); + request->send(response); + } + else + { + AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"msg\":\"Error changing device type\"}"); + request->send(response); + } }); + + // upload a file to /upload + // server->on("/upload", HTTP_POST, [](AsyncWebServerRequest *request){ + // request->send(200); + // }, handleUpload);server->on("/reset", HTTP_POST, [](AsyncWebServerRequest *request){ + + server->on("/restart", HTTP_POST, [webSocketHandler, apMode](AsyncWebServerRequest *request) + { + //if(apMode) { + //request->send(200, "text/plain",String("Restarting device, wait about 10-20 seconds and navigate to ") + (SettingsHandler::getHostname()) + ".local or the network IP address in your browser address bar."); + //} + String message = "{\"msg\":\"restarting\",\"apMode\":"; + message += apMode ? "true}" : "false}"; + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", message); + request->send(response); + webSocketHandler->closeAll(); + SettingsHandler::restart(2); }); + + server->on("/default", HTTP_POST, [this](AsyncWebServerRequest *request) + { + Serial.println("Settings default"); + if(m_settingsFactory->resetAll()) { + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); + request->send(response); + SettingsHandler::restart(5); + } else { + sendError(request); + } }); + + AsyncCallbackJsonWebHandler *settingsUpdateHandler = new AsyncCallbackJsonWebHandler("/settings", [this](AsyncWebServerRequest *request, JsonVariant &json) + { + Serial.println("API save settings..."); + JsonObject jsonObj = json.as(); + if (m_settingsFactory->saveCommon(jsonObj)) + { + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); + request->send(response); + } + else + { + AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"msg\":\"Error saving settings\"}"); + request->send(response); + } }); //, 32768U );//Bad request? increase the size. + + AsyncCallbackJsonWebHandler *pinsHandler = new AsyncCallbackJsonWebHandler("/pins", [this](AsyncWebServerRequest *request, JsonVariant &json) + { + Serial.println("API save pins..."); + JsonObject jsonObj = json.as(); + if (m_settingsFactory->savePins(jsonObj)) + { + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); + request->send(response); + } + else + { + AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"msg\":\"Error saving pins\"}"); + request->send(response); + } }); //, 1000U );//Bad request? increase the size. + + AsyncCallbackJsonWebHandler *wifiUpdateHandler = new AsyncCallbackJsonWebHandler("/wifiSettings", [this](AsyncWebServerRequest *request, JsonVariant &json) + { + Serial.println("API save wifi settings..."); + JsonObject jsonObj = json.as(); + if (m_settingsFactory->saveWifi(jsonObj)) + { + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); + request->send(response); + } + else + { + AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"msg\":\"Error saving wifi settings\"}"); + request->send(response); + } }); //, 500U );//Bad request? increase the size. + + AsyncCallbackJsonWebHandler *motionProfileUpdateHandler = new AsyncCallbackJsonWebHandler("/motionProfiles", [](AsyncWebServerRequest *request, JsonVariant &json) + { + Serial.println("API save motion profiles..."); + JsonObject jsonObj = json.as(); + if (SettingsHandler::saveMotionProfiles(jsonObj)) + { + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); + request->send(response); + } + else + { + AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"msg\":\"Error saving motion profiles\"}"); + request->send(response); + } }); //, 30000U );//Bad request? increase the size. + + AsyncCallbackJsonWebHandler *channelsProfileUpdateHandler = new AsyncCallbackJsonWebHandler("/channelsProfile", [](AsyncWebServerRequest *request, JsonVariant &json) + { + Serial.println("API save channels profile..."); + JsonObject jsonObj = json.as(); + if (SettingsHandler::saveChannels(jsonObj)) + { + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); + request->send(response); + } + else + { + AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"msg\":\"Error saving channels profile\"}"); + request->send(response); + } }); //, 5000U );//Bad request? increase the size. + + AsyncCallbackJsonWebHandler *buttonsUpdateHandler = new AsyncCallbackJsonWebHandler("/buttonSettings", [](AsyncWebServerRequest *request, JsonVariant &json) + { + Serial.println("API save button settings..."); + JsonObject jsonObj = json.as(); + if (SettingsHandler::saveButtons(jsonObj)) + { + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); + request->send(response); + } + else + { + AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"msg\":\"Error saving button settings\"}"); + request->send(response); + } }); //, 10000U );//Bad request? increase the size. + + // //To upload through terminal you can use: curl -F "image=@firmware.bin" esp8266-webupdate.local/update + // server->on("/update", HTTP_POST, [this](AsyncWebServerRequest *request){ + // // the request handler is triggered after the upload has finished... + // // create the response, add header, and send response + // AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", (Update.hasError())?"FAIL":"OK"); + // response->addHeader("Connection", "close"); + // response->addHeader("Access-Control-Allow-Origin", "*"); + // SettingsHandler::getRestartRequired() = true; + // request->send(response); + // },[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { + // //Upload handler chunks in data + + // if(!index){ // if index == 0 then this is the first frame of data + // Serial.printf("UploadStart: %s\n", filename.c_str()); + // Serial.setDebugOutput(true); + + // // calculate sketch space required for the update + // uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; + // if(!Update.begin(maxSketchSpace)){//start with max available size + // Update.printError(Serial); + // } + // Update.runAsync(true); // tell the updaterClass to run in async mode + // } + + // //Write chunked data to the free sketch space + // if(Update.write(data, len) != len){ + // Update.printError(Serial); + // } + + // if(final){ // if the final flag is set then this is the last frame of data + // if(Update.end(true)){ //true to set the size to the current progress + // Serial.printf("Update Success: %u B\nRebooting...\n", index+len); + // } else { + // Update.printError(Serial); + // } + // Serial.setDebugOutput(false); + // } + // }); + + server->addHandler(settingsUpdateHandler); + server->addHandler(pinsHandler); + server->addHandler(wifiUpdateHandler); + server->addHandler(motionProfileUpdateHandler); + server->addHandler(channelsProfileUpdateHandler); + server->addHandler(buttonsUpdateHandler); + + server->onNotFound([this](AsyncWebServerRequest *request) + { + if (handleStaticFile(request)) return; + Serial.printf("AsyncWebServerRequest Not found: %s", request->url().c_str()); + if (request->method() == HTTP_OPTIONS) { + request->send(200); + } else { + AsyncWebServerResponse *response = request->beginResponse(404, "application/text", String("AsyncWebServerRequest Not found") + request->url()); + request->send(response); + } }); + // server->on("/", HTTP_GET, [this](AsyncWebServerRequest *request) + // { + // // request->send(LittleFS, COMMON_SETTINGS_PATH, "application/json"); + // Serial.println("index"); + // sendChunked(request, "/www/index-min.html", "text/html"); + // }); + //"^\\/pinoutDefault\\/([0-9]+)$" + // server->on("\\/.*\\.js", HTTP_GET, [this](AsyncWebServerRequest *request) + // { + // // request->send(LittleFS, COMMON_SETTINGS_PATH, "application/json"); + // const char* filename = request->pathArg(0).c_str(); + // Serial.printf("JS file: %s\n", filename); + // sendChunked(request, filename, "application/javascript"); + // }); + // server->on("/settings-min.js", HTTP_GET, [this](AsyncWebServerRequest *request) + // { + // sendChunked(request, "/www/settings-min.js", 4096, "text/javascript"); + // }); + // server->on("/motion-generator-min.js", HTTP_GET, [this](AsyncWebServerRequest *request) + // { + // sendChunked(request, "/www/motion-generator-min.js", 1024, "text/javascript"); + // }); + + // server->rewrite("/", "/wifiSettings.htm").setFilter(ON_AP_FILTER); + server->serveStatic("/", LittleFS, "/www/"); + // .setDefaultFile("index-min.html"); + // //.setCacheControl("max-age=60000"); + server->begin(); + initialized = true; + } + void stop() override + { + if (initialized) + { + initialized = false; + server->end(); + } + // if(MDNSInitialized) + // { + // MDNS.end(); + // MDNSInitialized = false; + // } + } + bool isRunning() override + { + return initialized; + } + + void setup() override + { + } + + void loop() override + { + } + +private: + bool initialized = false; + SettingsFactory *m_settingsFactory; + AsyncWebServer *server; + + void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) + { + if (!index) + { + Serial.printf("UploadStart: %s\n", filename.c_str()); + } + for (size_t i = 0; i < len; i++) + { + Serial.write(data[i]); + } + if (final) + { + Serial.printf("UploadEnd: %s, %u B\n", filename.c_str(), index + len); + } + } + void sendError(AsyncWebServerRequest *request, int code = 500) + { + const char *lastError = LogHandler::getLastError(); + char responseMessage[1057]; + sprintf(responseMessage, "{\"msg\":\"Error: %s\"}", strlen(lastError) > 0 ? lastError : "Unknown error"); + AsyncWebServerResponse *response = request->beginResponse(code, "application/json", responseMessage); + request->send(response); + } + + void sendChunked(AsyncWebServerRequest *request, const char *filePath, const char *mimeType = "application/json", const bool &isGZip = false) + { + LogHandler::debug(Tags::Web, "[sendChunked] Open file: %s\n", filePath); + File file{LittleFS.open(filePath, FILE_READ)}; + + AsyncWebServerResponse *response = request->beginChunkedResponse( + mimeType, + [this, file]( + uint8_t *buffer, + const size_t max_len, + const size_t index) mutable -> size_t + { + LogHandler::debug(Tags::Web, "[beginChunkedResponse] Enter chunked file: %s\n", file.name()); + size_t length; + + // Restrict chunk size so we don't run out of RAM + // static const size_t max_chunk{chunkSize}; + // if (max_chunk < max_len) + // { + // LogHandler::debug(Tags::Web,"Max chunk %u Max len %u for: %s\n", chunkSize, max_len, file.name()); + // length = file.read(buffer, max_chunk); + // } + // else + // { + // LogHandler::debug(Tags::Web,"Max len %u exceded max chunk %u for: %s\n", max_len, chunkSize, file.name()); + length = file.read(buffer, max_len); + // } + + if (length == 0) + { + LogHandler::debug(Tags::Web, "[beginChunkedResponse] Close file: %s\n", file.name()); + file.close(); + } + + return length; + }); + + // Force download + // response->addHeader("Content-Disposition", "attachment; filename=\"userSettings.json\""); + if (isGZip) + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + } + + bool handleStaticFile(AsyncWebServerRequest *request) + { + String requestUrl = request->url(); + LogHandler::debug(Tags::Web, "[handleStaticFile] requet url: %s", requestUrl); + String path = "/www" + requestUrl; + LogHandler::debug(Tags::Web, "[handleStaticFile] static path: %s", path); + + if (path.endsWith("/")) + path += F("index-min.html"); + String mimeType; + String pathWithGz = path + ".gz"; + + if (LittleFS.exists(pathWithGz) || LittleFS.exists(path)) + { + bool gzipped = false; + if (LittleFS.exists(pathWithGz)) + { + gzipped = true; + path += ".gz"; + } + // else + // { + if (path.endsWith(".html")) + { + mimeType = "text/html"; + } + else if (path.endsWith(".js")) + { + mimeType = "text/javascript"; + } + else if (path.endsWith(".json")) + { + mimeType = "application/json"; + } + else if (path.endsWith(".css")) + { + mimeType = "text/css"; + } + // } + sendChunked(request, path.c_str(), mimeType.c_str(), gzipped); + + return true; + } + + return false; + } + // void startMDNS(char* hostName, char* friendlyName) + // { + // if(MDNSInitialized) + // MDNS.end(); + // Serial.print("hostName: "); + // Serial.println(hostName); + // Serial.print("friendlyName: "); + // Serial.println(friendlyName); + // if (!MDNS.begin(hostName)) { + // printf("MDNS Init failed"); + // return; + // } + // MDNS.setInstanceName(friendlyName); + // MDNS.addService("http", "tcp", 80); + // MDNS.addService("tcode", "udp", SettingsHandler::getUdpServerPort()); + // MDNSInitialized = true; + // } +}; diff --git a/ESP32/src/network/WebSocketHandler.h b/ESP32/src/network/WebSocketHandler.h new file mode 100644 index 0000000..437373e --- /dev/null +++ b/ESP32/src/network/WebSocketHandler.h @@ -0,0 +1,303 @@ +/* MIT License + +Copyright (c) 2024 Jason C. Fain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +#pragma once + +// #include +#include +#include +#include +#include "HTTP/WebSocketBase.h" +#include "settings/SettingsHandler.h" +// #include "LogHandler.h" +#include "logging/TagHandler.h" +#include "sensors/BatteryHandler.h" + +AsyncWebSocket ws("/ws"); + +struct WebSocketCommand +{ + const char *command; + const char *message; +}; + +class WebSocketHandler : public WebSocketBase +{ +public: + void setup(AsyncWebServer *server) + { + LogHandler::info(Tags::WebSocketServer, "Setting up webSocket"); + ws.onEvent([&](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) + { onWsEvent(server, client, type, arg, data, len); }); + server->addHandler(&ws); + tCodeInQueue = xQueueCreate(25, sizeof(char[MAX_COMMAND])); + if (tCodeInQueue == NULL) + { + LogHandler::error(Tags::WebSocketServer, "Error creating the tcode queue"); + } + // debugInQueue = xQueueCreate(10, sizeof(char[MAX_COMMAND])); + // if(debugInQueue == NULL) { + // LogHandler::error(Tags::WebSocket, "Error creating the debug queue"); + // } + // xTaskCreate(this->emptyQueue, "emptyDebugQueue", 2042, this, tskIDLE_PRIORITY, emptyQueueHandle); + isInitialized = true; + } + + void CommandCallback(const char *in) override + { // This overwrites the callback for message return + if (isInitialized && ws.count() > 0) + sendCommand(in); + } + + // void sendDebug(const char* message, LogLevel level) { + // if (level != LogLevel::VERBOSE && isInitialized && debugInQueue != NULL && uxQueueMessagesWaiting(debugInQueue) < 10 && serial_mtx.try_lock()) { + // std::lock_guard lck(serial_mtx, std::adopt_lock); + // // char messageToSend[MAX_COMMAND]; + // // if(sizeof(message) > 253) { + // // strncpy(messageToSend, message, 253); + // // messageToSend[254] = '\0'; + // // Serial.println("truncated"); + // // } else { + // // strcpy(messageToSend, message); + // // messageToSend[strlen(message)] = '\0'; + // // } + // // if(level >= LogLevel::DEBUG) { + // // Serial.print("insert to q: "); + // // Serial.println(message); + // // } + // xQueueSend(debugInQueue, message, 0); + // } + // } + + void sendCommand(const char *command, const char *message = 0) override + { + if (isInitialized && command_mtx.try_lock()) + { + std::lock_guard lck(command_mtx, std::adopt_lock); + m_lastSend = millis(); + + char commandJson[MAX_COMMAND]; + compileCommand(commandJson, command, message); + // if(client) + // client->text(commandJson); + // else + ws.textAll(commandJson); + } + } + + // // Did not work last I tried it. Gave up. + // // template + // // void sendCommands(WebSocketCommand (&commands)[N], AsyncWebSocketClient* client = 0) + // // { + // // if(isInitialized && command_mtx.try_lock()) { + // // std::lock_guard lck(command_mtx, std::adopt_lock); + // // m_lastSend = millis(); + + // // char commandsJson[MAX_COMMAND]; + // // std::strcat(commandsJson, "["); + // // for (int i = 0; i < N; i++) + // // { + // // if(commands[i].command) { + // // char commandJson[128]; + // // compileCommand(commandJson, commands[i].command, commands[i].message); + // // Serial.print("compileCommand: "); + // // Serial.println(commandJson); + // // std::strcat(commandsJson, commandJson); + // // if(i < N-1) + // // std::strcat(commandsJson, ","); + // // } + // // } + // // std::strcat(commandsJson, "]"); + // // Serial.print("commandsJson: "); + // // Serial.println(commandsJson); + // // if(client) + // // client->text(commandsJson); + // // else + // // ws.textAll(commandsJson); + // // } + // // } + + // void getTCode(char* webSocketData) + // { + // if(tCodeInQueue == NULL) + // { + // LogHandler::error(Tags::WebSocket, "TCode queue was null"); + // return; + // } + // if(xQueueReceive(tCodeInQueue, webSocketData, 0)) + // { + // //tcode->toCharArray(webSocketData, tcode->length() + 1); + // // Serial.print("Top tcode: "); + // // Serial.println(webSocketData); + // } + // else + // { + // webSocketData[0] = {0}; + // } + // ws.cleanupClients(); + // } + + void closeAll() override + { + for (AsyncWebSocketClient *pClient : m_clients) + pClient->close(); + } + +private: + bool isInitialized = false; + // std::mutex serial_mtx; + // std::mutex command_mtx; + // unsigned long lastCall; + std::list m_clients; + // QueueHandle_t tCodeInQueue; + // static QueueHandle_t debugInQueue; + static int m_lastSend; + // static TaskHandle_t* emptyQueueHandle; + // static bool emptyQueueRunning; + + // static void emptyQueue(void *webSocketHandler) { + // while (true) { + // if(ws.count() > 0 && millis() - m_lastSend > 50 && uxQueueMessagesWaiting(debugInQueue)) { + // char lastMessage[MAX_COMMAND]; + // if(xQueueReceive(debugInQueue, lastMessage, 0)) { + // if(LogHandler::getLogLevel() == LogLevel::VERBOSE) + // Serial.printf("read from q: %s\n", lastMessage); + // ((WebSocketHandler*)webSocketHandler)->sendCommand("debug", lastMessage); + // } + // } + // vTaskDelay(100/portTICK_PERIOD_MS); + // } + // vTaskDelete(NULL); + // } + + void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) + { + if (type == WS_EVT_CONNECT) + { + LogHandler::debug(Tags::WebSocketServer, "ws[%s][%u] connect\n", server->url(), client->id()); + // client->printf("Hello Client %u :)", client->id()); + // client->ping(); + // client->client()->setNoDelay(true); + m_clients.push_back(client); + } + else if (type == WS_EVT_DISCONNECT) + { + LogHandler::debug(Tags::WebSocketServer, "ws[%s][%u] disconnect\n", server->url(), client->id()); + m_clients.remove(client); + } + else if (type == WS_EVT_ERROR) + { + LogHandler::debug(Tags::WebSocketServer, "ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t *)arg), (char *)data); + } + else if (type == WS_EVT_PONG) + { + LogHandler::debug(Tags::WebSocketServer, "ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len) ? (char *)data : ""); + } + else if (type == WS_EVT_DATA) + { + AwsFrameInfo *info = (AwsFrameInfo *)arg; + // String msg = ""; + if (info->final && info->index == 0 && info->len == len) + { + // the whole message is in a single frame and we got all of it's data + // Serial.printf("ws[%s][%u] %s-message[%llu]: ", server->url(), client->id(), (info->opcode == WS_TEXT)?"text":"binary", info->len); + + // if(info->opcode == WS_TEXT) + // { + // for(size_t i=0; i < info->len; i++) + // { + // msg += (char) data[i]; + // } + // } + // else + // { + // char buff[3]; + // for(size_t i=0; i < info->len; i++) + // { + // sprintf(buff, "%02x ", (uint8_t) data[i]); + // msg += buff ; + // } + // } + // Serial.printf("%s\n",msg.c_str()); + + if (info->opcode == WS_TEXT) + { + data[len] = 0; + processWebSocketTextMessage((char *)data); + } + else + client->binary("I got your binary message"); + } + else + { + // message is comprised of multiple frames or the frame is split into multiple packets + // if(info->index == 0) + // { + // if(info->num == 0) + // Serial.printf("ws[%s][%u] %s-message start\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary"); + // Serial.printf("ws[%s][%u] frame[%u] start[%llu]\n", server->url(), client->id(), info->num, info->len); + // } + + // Serial.printf("ws[%s][%u] frame[%u] %s[%llu - %llu]: ", server->url(), client->id(), info->num, (info->message_opcode == WS_TEXT)?"text":"binary", info->index, info->index + len); + + // if(info->opcode == WS_TEXT) + // { + // for(size_t i=0; i < len; i++) + // { + // msg += (char) data[i]; + // } + // } + // else + // { + // char buff[3]; + // for(size_t i=0; i < len; i++) + // { + // sprintf(buff, "%02x ", (uint8_t) data[i]); + // msg += buff ; + // } + // } + // Serial.printf("%s\n",msg.c_str()); + + if ((info->index + len) == info->len) + { + // Serial.printf("ws[%s][%u] frame[%u] end[%llu]\n", server->url(), client->id(), info->num, info->len); + if (info->final) + { + // Serial.printf("ws[%s][%u] %s-message end\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary"); + if (info->message_opcode == WS_TEXT) + { + data[len] = 0; + processWebSocketTextMessage((char *)data); + } + else + client->binary("I got your binary message"); + } + } + } + } + } +}; + +// bool WebSocketHandler::emptyQueueRunning = false; +// QueueHandle_t WebSocketHandler::debugInQueue; +int WebSocketHandler::m_lastSend = 0; +// TaskHandle_t* WebSocketHandler::emptyQueueHandle = NULL; \ No newline at end of file diff --git a/ESP32/src/sensors/BatteryHandler.h b/ESP32/src/sensors/BatteryHandler.h new file mode 100644 index 0000000..76cd254 --- /dev/null +++ b/ESP32/src/sensors/BatteryHandler.h @@ -0,0 +1,223 @@ +/* MIT License + +Copyright (c) 2024 Jason C. Fain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +#pragma once + +#include +#include +#include +#include "settings/SettingsHandler.h" +#include "utils.h" +#include "LogHandler.h" +#include "TagHandler.h" +#include "tasks/TaskHandler.h" + +using BATTERY_STATE_FUNCTION_PTR_T = void (*)(float capacityRemainingPercentage, float capacityRemaining, float voltage, float temperature); +/** This class is setup for a specific board with an LTC2944 gas guage + * The module used is CJMCU-294. + */ +class BatteryHandler : public TaskHandler::Task +{ +public: + BatteryHandler() : Task(TaskHandler::Rates::SLOW) {} + static bool connected() + { + return m_battery_connected; + } + + static void startLoop(void *parameter) + { + ((BatteryHandler *)parameter)->loop(); + } + + /** Maximum value is 22000 mAh */ + static void setBatteryCapacity(int maxCapacity) + { + if (connected()) + { + gauge.setBatteryCapacity(maxCapacity); + m_maxCapacity = maxCapacity; + LogHandler::info(Tags::Battery, "Battery capacity max set to %u mAh", maxCapacity); + } + } + + /** Sets accumulated charge registers to the maximum value */ + static void setBatteryToFull() + { + if (connected()) + { + SettingsFactory *settingsFactory = SettingsFactory::getInstance(); + setBatteryCapacity(settingsFactory->getBatteryCapacityMax()); + gauge.setBatteryToFull(); + LogHandler::info(Tags::Battery, "Battery capacity set to full"); + } + } + + void setup() override + { + LogHandler::info(Tags::Battery, "Setup battery monitor"); + if (!SettingsHandler::waitForI2CDevices(LTC2944_ADDRESS)) + { + return; + } + Wire.begin(); + LogHandler::info(Tags::Battery, "Connecting to monitor"); + uint32_t timeout = millis() + 10000; + while (!gauge.begin()) + { + Serial.print("."); + this->wait(1000); + if (millis() > timeout) + { + LogHandler::error(Tags::Battery, "Detecting battery gauge (LTC2944) timed out. Exit."); + return; + } + } + LogHandler::info(Tags::Battery, "Monitor connected!"); + m_battery_connected = true; + gauge.setADCMode(ADC_MODE_SLEEP); // In sleep mode, voltage and temperature measurements will only take place when requested + SettingsFactory *settingsFactory = SettingsFactory::getInstance(); + setBatteryCapacity(settingsFactory->getBatteryCapacityMax()); + gauge.startMeasurement(); + LogHandler::debug(Tags::Battery, "Complete"); + } + + void setMessageCallback(BATTERY_STATE_FUNCTION_PTR_T f) + { + if (f == nullptr) + { + message_callback = 0; + } + else + { + message_callback = f; + } + } + + void loop() + { + if (m_battery_connected && millis() >= lastTick) + { + LogHandler::verbose(Tags::Battery, "Enter getBatteryLevel"); + lastTick = millis() + tick; + m_batteryCapacity = gauge.getRemainingCapacity(); + m_batteryVoltage = gauge.getVoltage(); + m_batteryTemp = gauge.getTemperature(); + + LogHandler::verbose(Tags::Battery, "Battery remaining capacity: %f", m_batteryCapacity); + LogHandler::verbose(Tags::Battery, "Battery voltage: %f", m_batteryVoltage); + LogHandler::verbose(Tags::Battery, "Battery temp: %f", m_batteryTemp); + float capacityRemainingPercentage = 0; + if (m_batteryCapacity > 0) + capacityRemainingPercentage = m_batteryCapacity / m_maxCapacity * 100; + + if (message_callback) + message_callback(capacityRemainingPercentage, m_batteryCapacity, m_batteryVoltage, m_batteryTemp); + } + } + + // void setup() { + // Method to get via ADC I had issues with. + // if(SettingsHandler::getBatteryLevelEnabled()) { + // LogHandler::info("batteryHandler", "Setting up voltage on pin: %ld", SettingsHandler::getBattery_Voltage_PIN()); + // adc1_config_width(ADC_WIDTH_12Bit); + // m_adc1Channel = gpioToADC1(SettingsHandler::getBattery_Voltage_PIN()); + // if(m_adc1Channel == adc1_channel_t::ADC1_CHANNEL_MAX) { + // LogHandler::error(Tags::Battery, "Invalid Battery voltage pin: %ld", SettingsHandler::getBattery_Voltage_PIN()); + // } + // if(m_adc1Channel != adc1_channel_t::ADC1_CHANNEL_MAX) { + // LogHandler::info("batteryHandler", "ADC channel: %ld", (int)m_adc1Channel); + // adc1_config_channel_atten(m_adc1Channel, ADC_ATTEN_DB_11); + + // LogHandler::debug("batteryHandler", "Calibrating battery voltage"); + // m_val_type = esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 0, &m_adc1_chars); + // //Check type of calibration value used to characterize ADC + // LogHandler::debug("batteryHandler", "Complete: "); + // if (m_val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { + // LogHandler::debug("batteryHandler", "eFuse Vref"); + // } else if (m_val_type == ESP_ADC_CAL_VAL_EFUSE_TP) { + // LogHandler::debug("batteryHandler", "Two Point"); + // } else { + // LogHandler::debug("batteryHandler", "Default"); + // } + // } + // m_battery_connected = true; + // } + //} + // Method to get via ADC I had issues with. + // void getBatteryLevel() { + // if(m_adc1Channel == adc1_channel_t::ADC1_CHANNEL_MAX) { + // return; + // } + // https://esp32tutorials.com/esp32-adc-esp-idf/ + // int adc_value = adc1_get_raw(m_adc1Channel); + // uint16_t raw = analogRead(SettingsHandler::getBattery_Voltage_PIN()); // analogRead is less accurate than adc1_get_raw + // m_batteryVoltage = (raw * 3.3 ) / 4095; + // uint32_t mV; + // if (m_val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) { + // //LogHandler::debug("batteryHandler", "eFuse Vref"); + // mV = esp_adc_cal_raw_to_voltage(adc_value, &m_adc1_chars); + // } else if (m_val_type == ESP_ADC_CAL_VAL_EFUSE_TP) { + // //LogHandler::debug("batteryHandler", "Two Point"); + // mV = esp_adc_cal_raw_to_voltage(adc_value, &m_adc1_chars); + // } else { + // //LogHandler::debug("batteryHandler", "Default"); + // mV = ((adc_value * 3.3 ) / 4095) * 1000; + // } + // if(mV > 0) { + // m_batteryVoltage = mV / 1000.0; + // } else { + // m_batteryVoltage = 0.0; + // } + + // https://www.youtube.com/watch?v=qKUrXwkr3cc + // https://github.com/G6EJD/LiPo_Battery_Capacity_Estimator/blob/master/ReadBatteryCapacity_LIPO.ino + // author David Bird + // uint8_t percentage = 2808.3808 * pow(m_batteryVoltage, 4) - 43560.9157 * pow(m_batteryVoltage, 3) + 252848.5888 * pow(m_batteryVoltage, 2) - 650767.4615 * m_batteryVoltage + 626532.5703; + + // Serial.print("mV: "); + // Serial.println(mV); + // Serial.print("areadValue: "); + // Serial.println(areadValue); + // Serial.print("m_batteryVoltage calc: "); + // Serial.println((adc_value * 3.3 ) / 4095); + //} +private: + BATTERY_STATE_FUNCTION_PTR_T message_callback = 0; + static LTC2944 gauge; + unsigned long lastTick = 0; + int tick = 5000; + bool _isRunning; + float m_batteryVoltage = 0.0; + float m_batteryCapacity = 0.0; + float m_batteryTemp = 0.0; + + static int m_maxCapacity; + static bool m_battery_connected; + // esp_adc_cal_characteristics_t m_adc1_chars; + // esp_adc_cal_value_t m_val_type; + // adc1_channel_t m_adc1Channel; +}; + +LTC2944 BatteryHandler::gauge; +bool BatteryHandler::m_battery_connected = false; +int BatteryHandler::m_maxCapacity; \ No newline at end of file diff --git a/ESP32/src/sensors/ButtonHandler.hpp b/ESP32/src/sensors/ButtonHandler.hpp new file mode 100644 index 0000000..c13ce09 --- /dev/null +++ b/ESP32/src/sensors/ButtonHandler.hpp @@ -0,0 +1,255 @@ +/* MIT License + +Copyright (c) 2024 Jason C. Fain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +#pragma once + +#include +#include "logging/LogHandler.h" +#include "logging/TagHandler.h" +#include "settings/SettingsHandler.h" +#include "constants.h" +#include "struct/buttonSet.h" +#include "messages/MessageHandler.h" +#include "settingsFactory.h" +#include "tasks/TaskHandler.h" + +#define TASK_WAIT 10 / portTICK_PERIOD_MS + +class ButtonHandler : public TaskHandler::Task +{ +public: + ButtonHandler() : Task(TaskHandler::Rates::FAST) + { + buttonIndexMap[0] = 900; + buttonIndexMap[1] = 1200; + buttonIndexMap[2] = 2000; + buttonIndexMap[3] = 4096; + } + + void init(uint16_t analogDebounce, const char bootButtonCommand[MAX_COMMAND], ButtonSet *buttonSets) + { + if (m_initialized) + { + return; + } + buttonAnalogDebounce = analogDebounce; + m_buttonQueue = xQueueCreate(MAX_BUTTON_SETS * MAX_BUTTONS, sizeof(struct ButtonModel *)); + if (m_buttonQueue == NULL) + { + LogHandler::error(Tags::Button, "Error creating the debug queue"); + } + bool bootButtonEnabled = BOOT_BUTTON_ENABLED_DEFAULT; + SettingsFactory *settingsFactory = SettingsFactory::getInstance(); + settingsFactory->getValue(BOOT_BUTTON_ENABLED, bootButtonEnabled); + if (bootButtonEnabled) + initBootbutton(bootButtonCommand); + bool buttonSetsEnabled = BUTTON_SETS_ENABLED_DEFAULT; + settingsFactory->getValue(BUTTON_SETS_ENABLED, buttonSetsEnabled); + if (buttonSetsEnabled) + initAnalogButtons(buttonSets); + m_initialized = true; + } + + void initAnalogButtons(ButtonSet *buttonSets) + { + for (int i = 0; i < MAX_BUTTON_SETS; i++) + { + m_buttonSets[i] = ButtonSet(buttonSets[i]); + auto buttonSet = m_buttonSets[i]; + auto buttonPin = buttonSet.pin; + if (buttonPin > -1) + { + // LogHandler::debug(Tags::Button, "initAnalogButtons '%s' pin: %d, buton 0 name: %s", buttonSet.name, buttonPin, buttonSet.buttons[0].name); + auto pin = digitalPinToInterrupt(buttonPin); // Check valid pin iterrupt + if (pin == -1) + { + LogHandler::error(Tags::Button, "Invalid interupt button pin: %d", buttonPin); + continue; + }; + // LogHandler::debug(Tags::Button, "Checking button set: %s, pin: %ld", buttonSet.name, buttonPin); + // if(buttonSet.pullMode == gpio_pull_mode_t::GPIO_PULLDOWN_ONLY) { + // LogHandler::info(Tags::Button, "Setting up pull down button set %u on pin: %ld", i +1, buttonPin); + // pinMode(buttonPin, INPUT_PULLDOWN); + // attachInterrupt(buttonPin, buttonInterrupt, RISING); + // } else { + // LogHandler::info(Tags::Button, "Setting up pull up button set %u on pin: %ld", i +1, buttonPin); + // pinMode(buttonPin, INPUT_PULLUP); + // attachInterrupt(buttonPin, buttonInterrupt, FALLING); + // } + } + } + } + + void initBootbutton(const char command[MAX_COMMAND]) + { + pinMode(0, INPUT_PULLDOWN); + attachInterrupt(digitalPinToInterrupt(0), bootButtonInterrupt, CHANGE); + snprintf(bootButtonModel.name, sizeof(bootButtonModel.name), "Boot button"); + updateBootButtonCommand(command); + } + + void updateBootButtonCommand(const char bootButtonCommand[MAX_COMMAND]) + { + xSemaphoreTake(xMutex, portMAX_DELAY); + if (strlen(bootButtonCommand) > 0) + { + strncpy(bootButtonModel.command, bootButtonCommand, sizeof(bootButtonModel.command)); + } + else + { + bootButtonModel.command[0] = {0}; + } + xSemaphoreGive(xMutex); + } + + void updateAnalogButtonCommands(ButtonSet buttonSets[MAX_BUTTON_SETS]) + { + xSemaphoreTake(xMutex, portMAX_DELAY); + for (int i = 0; i < MAX_BUTTON_SETS; i++) + { + m_buttonSets[i] = ButtonSet(buttonSets[i]); + } + xSemaphoreGive(xMutex); + } + + void updateAnalogDebounce(uint16_t debounce) + { + buttonAnalogDebounce = debounce; + } + + void read(ButtonModel *&buf) + { + void *recieve; + if (xQueueReceive(m_buttonQueue, &(recieve), 0)) + { + buf = (ButtonModel *)recieve; + LogHandler::debug(Tags::Button, "Recieve command in button queue: %s: %s", buf->name, buf->command); + } + else + { + buf = 0; + } + } + + static void bootButtonInterrupt() + { + BaseType_t pxHigherPriorityTaskWoken = pdFALSE; + xSemaphoreTakeFromISR(xMutex, &pxHigherPriorityTaskWoken); + digitalRead(digitalPinToInterrupt(0)) == HIGH ? bootButtonModel.press() : bootButtonModel.release(); + struct ButtonModel *pxMessage = &(bootButtonModel); // Why did I have to do all this!? + xQueueSendFromISR(m_buttonQueue, (void *)&pxMessage, &pxHigherPriorityTaskWoken); + xSemaphoreGiveFromISR(xMutex, &pxHigherPriorityTaskWoken); + if (pxHigherPriorityTaskWoken) + { + portYIELD_FROM_ISR(); + } + } + + void setup() override + { + SettingsFactory *settingsFactory = SettingsFactory::getInstance(); + this->init(settingsFactory->getButtonAnalogDebounce(), + settingsFactory->getBootButtonCommand(), + settingsFactory->getButtonSets()); + } + + void loop() override + { + if (millis() - m_lastDebounce > buttonAnalogDebounce) + { + m_lastDebounce = millis(); + readButtons(); + } + + this->read(buttonCommand); + if (buttonCommand) + { + Messages::message_t msg{}; + msg.tag_mask = Tags::as_mask(Tags::Button); + msg.message = buttonCommand->command; + if (strlen(buttonCommand->command) > 0) + { + Messages::MessageRouter::getInstance()->send(msg); + } + } + } + +private: + bool m_initialized = false; + ButtonModel* buttonCommand = nullptr; + + static long m_lastDebounce; + static volatile bool m_enterInterupt; + static SemaphoreHandle_t xMutex; + static uint16_t buttonIndexMap[MAX_BUTTONS]; + static ButtonSet m_buttonSets[MAX_BUTTON_SETS]; + static QueueHandle_t m_buttonQueue; + static uint8_t buttonAnalogTolorance; + static uint16_t buttonAnalogDebounce; + static ButtonModel bootButtonModel; + + static bool readButtons() + { + uint8_t index = 0; + for (int i = 0; i < MAX_BUTTON_SETS; i++) + { + if (m_buttonSets[i].pin > -1) + { + for (int j = 0; j < MAX_BUTTONS; j++) + { + auto value = analogRead(m_buttonSets[i].pin); + auto index = m_buttonSets[i].buttons[j].index; + // LogHandler causes stack overflow. + // LogHandler::verbose(Tags::Button, "readButtons value: %ld, index: %ld, index value: %ld", value, index, buttonIndexMap[index]); + bool isPressedValue = value >= buttonIndexMap[index] - buttonAnalogTolorance && value <= buttonIndexMap[index] + buttonAnalogTolorance; + if (!m_buttonSets[i].buttons[j].isPressed() && isPressedValue) + { + // LogHandler::debug(Tags::Button, "Button '%s' pressed: %u, set index: %u button index: %u", m_buttonSets[i].buttons[j].name, value, i, j); + // xSemaphoreTake(xMutex, portMAX_DELAY); + m_buttonSets[i].buttons[j].press(); + struct ButtonModel *pxMessage = &(m_buttonSets[i].buttons[j]); // Why did I have to do all this!? + xQueueSend(m_buttonQueue, (void *)&pxMessage, portMAX_DELAY); + // xSemaphoreGive(xMutex); + return true; + } + else if (m_buttonSets[i].buttons[j].isPressed() && !isPressedValue) + { + // LogHandler::debug(Tags::Button, "Button '%s' released", m_buttonSets[i].buttons[j].name); + m_buttonSets[i].buttons[j].release(); + struct ButtonModel *pxMessage = &(m_buttonSets[i].buttons[j]); // Why did I have to do all this!? + xQueueSend(m_buttonQueue, (void *)&pxMessage, portMAX_DELAY); + } + } + } + } + return false; + } +}; +uint8_t ButtonHandler::buttonAnalogTolorance = 150; +uint16_t ButtonHandler::buttonAnalogDebounce = 150; +QueueHandle_t ButtonHandler::m_buttonQueue; +ButtonSet ButtonHandler::m_buttonSets[MAX_BUTTON_SETS]; +ButtonModel ButtonHandler::bootButtonModel; +uint16_t ButtonHandler::buttonIndexMap[MAX_BUTTONS]; +SemaphoreHandle_t ButtonHandler::xMutex = xSemaphoreCreateMutex(); +volatile bool ButtonHandler::m_enterInterupt = false; +long ButtonHandler::m_lastDebounce = millis(); \ No newline at end of file diff --git a/ESP32/src/sensors/TemperatureHandler.h b/ESP32/src/sensors/TemperatureHandler.h new file mode 100644 index 0000000..cd0f249 --- /dev/null +++ b/ESP32/src/sensors/TemperatureHandler.h @@ -0,0 +1,131 @@ +/* MIT License + +Copyright (c) 2024 Jason C. Fain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +#pragma once + +#include "settingsFactory.h" +#include "logging/LogHandler.h" +#include "logging/TagHandler.h" +#include "tasks/TaskHandler.h" + +enum class TemperatureType +{ + INTERNAL, + SLEEVE +}; + +using TEMPERATURE_STATE_FUNCTION_PTR_T = void (*)(TemperatureType type, const char* status, float tempC); + +class TemperatureHandler : public TaskHandler::Task +{ +public: + TemperatureHandler() : Task(TaskHandler::Rates::SLOW) {} + + // TaskHandler entry point. Configuration is done via setup(...) overload below. + void setup() override + { + } + + // Backward-compatible configuration API used by legacy call sites. + void setup(bool internalTempEnabled, + bool sleeveTempEnabled, + int8_t sleeveTempPin, + int8_t internalTempPin, + int8_t heaterPin, + int8_t heaterChannel, + int8_t caseFanPin, + int8_t caseFanChannel, + int heaterFrequency, + int heaterResolution, + bool fanControlEnabled, + int fanFrequency, + int fanResolution, + int maxFanPWM) + { + (void)internalTempEnabled; + (void)sleeveTempEnabled; + (void)sleeveTempPin; + (void)internalTempPin; + (void)heaterPin; + (void)heaterChannel; + (void)caseFanPin; + (void)caseFanChannel; + (void)heaterFrequency; + (void)heaterResolution; + (void)fanControlEnabled; + (void)fanFrequency; + (void)fanResolution; + (void)maxFanPWM; + m_running = true; + } + + void loop() override + { + if (!m_running) + { + return; + } + // Placeholder until full temperature control logic is restored. + this->sleep(5000); + } + + void setMessageCallback(TEMPERATURE_STATE_FUNCTION_PTR_T callback) + { + m_messageCallback = callback; + } + + bool isMaxTempTriggered() const + { + return false; + } + + float getInternalTemp() const + { + return m_internalTemp; + } + + float getSleeveTemp() const + { + return m_sleeveTemp; + } + + const char* getInternalState() const + { + return "Unknown"; + } + + const char* getSleeveState() const + { + return "Unknown"; + } + + void stopRunning() + { + m_running = false; + } + +private: + TEMPERATURE_STATE_FUNCTION_PTR_T m_messageCallback = nullptr; + bool m_running = false; + float m_internalTemp = 0.0f; + float m_sleeveTemp = 0.0f; +}; \ No newline at end of file diff --git a/ESP32/src/serial/SerialHandler.h b/ESP32/src/serial/SerialHandler.h new file mode 100644 index 0000000..042ff1f --- /dev/null +++ b/ESP32/src/serial/SerialHandler.h @@ -0,0 +1,91 @@ +/* MIT License + +Copyright (c) 2024 Jason C. Fain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +#ifndef SERIAL_HANDLER_H_ +#define SERIAL_HANDLER_H_ + +#include + +#include "logging/LogHandler.h" +#include "logging/TagHandler.h" +#include "messages/SystemCommandHandler.h" +#include "tasks/TaskHandler.h" + +class SerialHandler : public TaskHandler::Task +{ +public: + SerialHandler() : Task(TaskHandler::Rates::FAST) {} + + static void init() + { + LogHandler::info(Tags::Main, "Serial command handler initialized"); + } + + void setup() override + { + m_index = 0; + m_buffer[0] = '\0'; + LogHandler::info(Tags::Main, "Serial command task started"); + } + + void loop() override + { + while (Serial.available() > 0) + { + const char ch = static_cast(Serial.read()); + + if (ch == '\r') + { + continue; + } + + if (ch == '\n') + { + if (m_index > 0) + { + m_buffer[m_index] = '\0'; + m_commandHandler.process(m_buffer); + m_index = 0; + m_buffer[0] = '\0'; + } + continue; + } + + if (m_index >= (MAX_COMMAND - 1)) + { + LogHandler::warning(Tags::Main, "Serial input exceeded max command length; dropping line"); + m_index = 0; + m_buffer[0] = '\0'; + continue; + } + + m_buffer[m_index++] = ch; + } + } + +private: + SystemCommandHandler m_commandHandler; + char m_buffer[MAX_COMMAND] = { 0 }; + size_t m_index = 0; +}; + +#endif // SERIAL_HANDLER_H_ diff --git a/ESP32/src/settings/FilesystemHandler.h b/ESP32/src/settings/FilesystemHandler.h new file mode 100644 index 0000000..4600e5f --- /dev/null +++ b/ESP32/src/settings/FilesystemHandler.h @@ -0,0 +1,46 @@ +#ifndef FILESYSTEM_HANDLER_H_ +#define FILESYSTEM_HANDLER_H_ + +#include +#include +#include "logging/LogHandler.h" +#include "logging/TagHandler.h" + +class FilesystemHandler +{ + public: + static void init() + { + if(!LittleFS.begin(true)) + { + LogHandler::error(Tags::Filesystem, "An error has occurred while mounting LittleFS"); + } + else + { + LogHandler::info(Tags::Filesystem, "LittleFS mounted successfully"); + } + } + + static bool exists(const char* path) + { + return LittleFS.exists(path); + } + + File open(const char* path, const char* mode = FILE_READ, const bool create = false) + { + return LittleFS.open(path, mode, create); + } + + static int write(File &file, const uint8_t *buf, size_t size) + { + return file.write(buf, size); + } + + static int read(File &file, uint8_t *buf, size_t size) + { + return file.read(buf, size); + } + }; + + +#endif // FILESYSTEM_HANDLER_H_ \ No newline at end of file diff --git a/ESP32/src/settings/OperatingModeHandler.h b/ESP32/src/settings/OperatingModeHandler.h new file mode 100644 index 0000000..2697279 --- /dev/null +++ b/ESP32/src/settings/OperatingModeHandler.h @@ -0,0 +1,84 @@ +#ifndef _OPERATING_MODE_HANDLER_H_ +#define _OPERATING_MODE_HANDLER_H_ + +#include "tasks/TaskHandler.h" +#include "settings/ConfigurationHandler.h" +#include "logging/LogHandler.h" +#include "logging/TagHandler.h" + +class OperatingModeHandler : public TaskHandler::Task { +private: + inline static OperatingModeHandler* _singleton = nullptr; + OperatingMode _currentMode = OperatingMode::STARTUP_MODE; +public: + OperatingModeHandler() : TaskHandler::Task(TaskHandler::Rates::SLOW) {} + + static void init() + { + if (!_singleton) + { + _singleton = new OperatingModeHandler(); + TaskHandler::global().priority(_singleton); + } + } + static OperatingModeHandler* global() + { + return _singleton; + } + + static OperatingMode getOperatingMode() + { + return global()->_currentMode; + } + + static void setOperatingMode(OperatingMode mode) + { + OperatingModeHandler* self = global(); + if (mode == self->_currentMode) + { + return; + } + + switch(mode) + { + case OperatingMode::STARTUP_MODE: + LogHandler::info(Tags::Settings, "Switching to STARTUP_MODE"); + break; + case OperatingMode::CONFIGURATION_MODE: + LogHandler::info(Tags::Settings, "Switching to CONFIGURATION_MODE"); + break; + case OperatingMode::OTA_MODE: + LogHandler::info(Tags::Settings, "Switching to OTA_MODE"); + break; + case OperatingMode::NORMAL_OPERATION: + LogHandler::info(Tags::Settings, "Switching to NORMAL_OPERATION"); + break; + } + global()->_currentMode = mode; + } + void setup() override + { + } + + void loop() override + { + switch(_currentMode) + { + case OperatingMode::STARTUP_MODE: + // Handle startup tasks + break; + case OperatingMode::CONFIGURATION_MODE: + // Handle configuration tasks + break; + case OperatingMode::OTA_MODE: + // Handle OTA tasks + break; + case OperatingMode::NORMAL_OPERATION: + // Handle normal operation tasks + break; + } + } +}; + + +#endif // _OPERATING_MODE_HANDLER_H_ diff --git a/ESP32/src/settings/SettingsHandler.h b/ESP32/src/settings/SettingsHandler.h new file mode 100644 index 0000000..bdf3082 --- /dev/null +++ b/ESP32/src/settings/SettingsHandler.h @@ -0,0 +1,2610 @@ +/* MIT License + +Copyright (c) 2024 Jason C. Fain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +// // #include "LogHandler.h" +#include "utils.h" +#include "TagHandler.h" +#include "struct/voice.h" +#include "struct/motionProfile.h" +#include "struct/channel.h" +#include "struct/motionChannel.h" +#include "struct/buttonSet.h" +#include "enum.h" +#include "constants.h" +#include "channelMap.hpp" +#include "settingConstants.h" +#include "settingsFactory.h" + +#define DESERIALIZE_SIZE 32768 +#define SERIALIZE_SIZE 24576 + +#define LOG_BUILD_FEATURE(feature) LogHandler::debug(Tags::Settings, "Build feature: %s", #feature); + +// using SETTING_STATE_FUNCTION_PTR_T = void (*)(const char *group, const char *settingNameThatChanged); + +class SettingsHandler +{ +public: + static bool initialized; + static int restartRequired; + static bool saving; + static bool motionPaused; + static bool fullBuild; + static inline bool channelRangesEnabled = true; + static LogLevel logLevel; + static std::vector systemI2CAddresses; + + static ChannelMap channelMap; + static BuildFeature buildFeatures[(int)BuildFeature::MAX_FEATURES]; + + static inline MotionProfile *motionProfiles; + static inline ButtonSet *buttonSets; + + // static bool staticIP; + static char currentIP[IP_ADDRESS_LEN]; + static char currentGateway[IP_ADDRESS_LEN]; + static char currentSubnet[IP_ADDRESS_LEN]; + static char currentDns1[IP_ADDRESS_LEN]; + static char currentDns2[IP_ADDRESS_LEN]; + + static bool apMode; + + // template::value || std::is_integral::value || std::is_enum::value || std::is_floating_point::value || std::is_same::value>> + // static void getValue(const char* name, T &value) + // { + // m_settingsFactory->getValue(name, value); + // } + + // static void getValue(const char* name, char* value, size_t len) + // { + // m_settingsFactory->getValue(name, value, len); + // } + + // static void defaultValue(const char* name) + // { + // m_settingsFactory->defaultValue(name); + // } + + static void init() + { + m_settingsFactory = SettingsFactory::getInstance(); + // Initialize SettingsFactory to load pin map and other cached settings + if (!m_settingsFactory->init()) + { + LogHandler::error(Tags::Settings, "SettingsFactory initialization failed"); + return; + } + + motionProfiles = m_settingsFactory->getMotionProfiles(); + buttonSets = m_settingsFactory->getButtonSets(); + setBuildFeatures(); + setMotorType(); + + // loadWifiInfo(false); + // loadSettings(false); + loadChannels(false); + loadMotionProfiles(false); + loadButtons(false); + + LogHandler::debug(Tags::Settings, "Last reset reason: %s", machine_reset_cause()); + initialized = true; + } + + static void setMessageCallback(SETTING_STATE_FUNCTION_PTR_T f) + { + LogHandler::debug(Tags::Settings, "setMessageCallback"); + if (f == nullptr) + { + message_callback = 0; + } + else + { + message_callback = f; + } + } + + // static bool isBoardType(BoardType value) { + // return m_settingsFactory->getBoardType() == value; + // } + + static void printFree(bool forcePrint = false) + { + if (forcePrint || LogHandler::getLogLevel() == LogLevel::DEBUG) + { + uint32_t freeHEap = ESP.getFreeHeap(); + uint32_t heapSize = ESP.getHeapSize(); + // https://esp32.com/viewtopic.php?t=27780 + // https://github.com/espressif/esp-idf/blob/master/components/heap/include/esp_heap_caps.h#L20-L37 + // esp_get_free_internal_heap_size + Serial.printf("Used heap INTERNAL: %u/%u Free: %u\n", heapSize - freeHEap, heapSize, freeHEap); + Serial.printf("Free psram: %u\n", ESP.getFreePsram()); + Serial.printf("Total Psram: %u\n", ESP.getPsramSize()); + Serial.printf("LittleFS used: %i\n", LittleFS.usedBytes()); + Serial.printf("LittleFS total: %i\n", LittleFS.totalBytes()); + // LogHandler::debug(Tags::Settings, "Used Psram: %u/%u", ESP.getPsramSize() - ESP.getFreePsram(), ESP.getPsramSize()); + Serial.printf("Sketch size: %u\n", ESP.getSketchSize()); + Serial.printf("Sketch free space: %u\n", ESP.getFreeSketchSpace()); + Serial.printf("DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); + Serial.printf("IRAM %u\n", heap_caps_get_free_size(MALLOC_CAP_32BIT)); + Serial.printf("FREE_HEAP Default %u\n", esp_get_free_heap_size()); + Serial.printf("MIN_FREE_HEAP %u\n", esp_get_minimum_free_heap_size()); + // uxTaskGetStackHighWaterMark + } + } + + static void restart(const int delayInSec = 0) + { + LogHandler::info(Tags::Settings, "Schedule device restart in %ld seconds", delayInSec); + // Restart in main task loop + restartRequired = delayInSec; + } + + static void printWebAddress(const char *hostAddress) + { + char webServerportString[6]; + int webServerPort = 0; + m_settingsFactory->getValue(WEBSERVER_PORT, webServerPort); + sprintf(webServerportString, ":%d", webServerPort); + LogHandler::info(Tags::Settings, "Web address: http://%s%s", hostAddress, webServerPort == 80 ? "" : webServerportString); + } + + static bool saveAll(JsonObject obj = JsonObject()) + { + if (!m_settingsFactory->saveAllToDisk(obj) || !saveMotionProfiles(obj) || !saveButtons(obj)) + return false; + return true; + } + + static bool saveAll(const String &data) + { + LogHandler::debug(Tags::Settings, "Save frome string"); + printFree(); + JsonDocument doc; + + DeserializationError error = deserializeJson(doc, data); + if (error) + { + LogHandler::error(Tags::Settings, "Settings save: Deserialize error: %s", error.c_str()); + return false; + } + printFree(); + JsonObject obj = doc.as(); + if (!saveAll(obj)) + { + LogHandler::error(Tags::Settings, "Settings save: save error"); + return false; + } + return true; + } + + static void getWifiInfo(char *buf) + { + JsonDocument doc; // 100 + + JsonDocument wifiDoc = m_settingsFactory->getNetworkSettings(); + + doc.set(wifiDoc); + const char *wifiPass = doc[WIFI_PASS_SETTING]; + if (strcmp(wifiPass, WIFI_PASS_DONOTCHANGE_DEFAULT)) + { + doc[WIFI_PASS_SETTING] = DECOY_PASS; // Never set to actual password + } + else + { + doc[WIFI_PASS_SETTING] = WIFI_PASS_DONOTCHANGE_DEFAULT; + } + const char *apPass = doc[AP_MODE_PASS]; + if (strcmp(apPass, AP_MODE_PASS_DEFAULT)) + { + doc[AP_MODE_PASS] = DECOY_PASS; // Never set to actual password + } + else + { + doc[AP_MODE_PASS] = AP_MODE_PASS_DEFAULT; + } + + String output; + serializeJson(doc, output); + doc.clear(); + if (LogHandler::getLogLevel() == LogLevel::VERBOSE) + Serial.printf("Network Info: %s\n", output.c_str()); + buf[0] = {0}; + strcpy(buf, output.c_str()); + } + + static void getSystemInfo(String &buf) + { + JsonDocument doc; // 3500 + + doc["esp32Version"] = FIRMWARE_VERSION_NAME; + doc["esp32VersionNum"] = FIRMWARE_VERSION; + doc["TCodeVersion"] = m_settingsFactory->getTcodeVersion(); + doc["lastRebootReason"] = machine_reset_cause(); + doc["channelRangesEnabled"] = getChannelRangesEnabled(); + + JsonArray logLevels = doc["logLevels"].to(); + JsonObject logLevelNone = logLevels.add(); + logLevelNone["name"] = "None"; + logLevelNone["value"] = LogLevel::NONE; + JsonObject logLevelError = logLevels.add(); + logLevelError["name"] = "Error"; + logLevelError["value"] = LogLevel::ERROR; + JsonObject logLevelWarning = logLevels.add(); + logLevelWarning["name"] = "Warning"; + logLevelWarning["value"] = LogLevel::WARNING; + JsonObject logLevelInfo = logLevels.add(); + logLevelInfo["name"] = "info"; + logLevelInfo["value"] = LogLevel::INFO; + JsonObject logLevelDebug = logLevels.add(); + logLevelDebug["name"] = "Debug"; + logLevelDebug["value"] = LogLevel::DEBUG; + JsonObject logLevelVerbose = logLevels.add(); + logLevelVerbose["name"] = "Verbose"; + logLevelVerbose["value"] = LogLevel::VERBOSE; + + JsonArray tcodeVersions = doc["tcodeVersions"].to(); + JsonObject v03 = tcodeVersions.add(); + v03["name"] = "v0.3"; + v03["value"] = TCodeVersion::v0_3; + JsonObject v04 = tcodeVersions.add(); + v04["name"] = "v0.4 (Experimental)"; + v04["value"] = TCodeVersion::v0_4; + JsonArray boardTypes = doc["boardTypes"].to(); +#if CONFIG_IDF_TARGET_ESP32 + JsonObject devkit = boardTypes.add(); + devkit["name"] = "Devkit"; + devkit["value"] = (uint8_t)BoardType::DEVKIT; +#if MOTOR_TYPE == 0 + JsonObject SR6MB = boardTypes.add(); + SR6MB["name"] = "SR6MB"; + SR6MB["value"] = (uint8_t)BoardType::CRIMZZON; + JsonObject INControl = boardTypes.add(); + INControl["name"] = "IN-Control"; + INControl["value"] = (uint8_t)BoardType::ISAAC; + JsonObject SR6PCB = boardTypes.add(); + SR6PCB["name"] = "SR6PCB"; + SR6PCB["value"] = (uint8_t)BoardType::SR6PCB; +#elif MOTOR_TYPE == 1 + JsonObject SSR1PCB = boardTypes.add(); + SSR1PCB["name"] = "SSR1PCB"; + SSR1PCB["value"] = (uint8_t)BoardType::SSR1PCB; +#endif +#elif CONFIG_IDF_TARGET_ESP32S3 +#ifdef S3_ZERO + JsonObject S3_Zero = boardTypes.add(); + S3_Zero["name"] = "S3 Zero"; + S3_Zero["value"] = (uint8_t)BoardType::ZERO; +#else + JsonObject N8R8 = boardTypes.add(); + N8R8["name"] = "S3 N8R8"; + N8R8["value"] = (uint8_t)BoardType::N8R8; +#endif +#endif + int motorType = MOTOR_TYPE_DEFAULT; + m_settingsFactory->getValue(MOTOR_TYPE_SETTING, motorType); + doc["motorType"] = motorType; + JsonArray buildFeaturesJsonArray = doc["buildFeatures"].to(); + for (BuildFeature value : buildFeatures) + { + buildFeaturesJsonArray.add((int)value); + } + doc["moduleType"] = (int)MODULE_CURRENT; + + JsonArray availableTagsJsonArray = doc["availableTags"].to(); + for (const char *tag : Tags::AvailableTags) + { + availableTagsJsonArray.add(tag); + } + JsonArray systemI2CAddressesJsonArray = doc["systemI2CAddresses"].to(); + systemI2CAddressesJsonArray.add("0x0"); + for (int value : systemI2CAddresses) + { + char buf[10]; + hexToString(value, buf); + systemI2CAddressesJsonArray.add(buf); + } + + JsonArray deviceTypes = doc["deviceTypes"].to(); + JsonObject defaultDevice = deviceTypes.add(); +#if MOTOR_TYPE == 0 + defaultDevice["name"] = "OSR"; + defaultDevice["value"] = DeviceType::OSR; + JsonObject SR6 = deviceTypes.add(); + SR6["name"] = "SR6"; + SR6["value"] = DeviceType::SR6; + JsonObject TVIBE = deviceTypes.add(); + TVIBE["name"] = "TVIBE"; + TVIBE["value"] = DeviceType::TVIBE; +#elif MOTOR_TYPE == 1 + defaultDevice["name"] = "SSR1"; + defaultDevice["value"] = DeviceType::SSR1; + JsonArray encoderTypes = doc["encoderTypes"].to(); + JsonObject defaultEncoder = encoderTypes.add(); + defaultEncoder["name"] = "MT6701 SSI"; + defaultEncoder["value"] = BLDCEncoderType::MT6701; + JsonObject PWM = encoderTypes.add(); + PWM["name"] = "PWM"; + PWM["value"] = BLDCEncoderType::PWM; + JsonObject SPI = encoderTypes.add(); + SPI["name"] = "SPI"; + SPI["value"] = BLDCEncoderType::SPI; +#endif + + JsonArray bleDeviceTypes = doc["bleDeviceTypes"].to(); + JsonObject defaultBleDevice = bleDeviceTypes.add(); + defaultBleDevice["name"] = "TCode"; + defaultBleDevice["value"] = BLEDeviceType::TCODE; + JsonObject loveDevice = bleDeviceTypes.add(); + loveDevice["name"] = "Love"; + loveDevice["value"] = BLEDeviceType::LOVE; + JsonObject hcDevice = bleDeviceTypes.add(); + // HC has an unknown formatting. + hcDevice["name"] = "HC"; + hcDevice["value"] = BLEDeviceType::HC; + + JsonArray bleLoveDevices = doc["bleLoveDeviceTypes"].to(); + JsonObject defaultLoveDevice = bleLoveDevices.add(); + defaultLoveDevice["name"] = "Edge"; + defaultLoveDevice["value"] = BLELoveDeviceType::EDGE; + + JsonArray availableChannels = doc["availableChannels"].to(); + channelMap.serialize(availableChannels); + doc[MOTION_ENABLED] = getMotionEnabled(); + // int motionProfileSelectedIndex = MOTION_PROFILE_SELECTED_INDEX_DEFAULT; + // m_settingsFactory->getValue(MOTION_PROFILE_SELECTED_INDEX, motionProfileSelectedIndex); + doc[MOTION_PROFILE_SELECTED_INDEX] = motionSelectedProfileIndex; + + JsonArray availableTimers = doc["availableTimers"].to(); + JsonArray timerChannels = doc["timerChannels"].to(); + JsonObject timerChannelNoneObj = timerChannels.add(); + timerChannelNoneObj["name"] = "None"; + timerChannelNoneObj["value"] = ESPTimerChannelNum::NONE; + PinMap *pinMap = m_settingsFactory->getPins(); + for (size_t i = 0; i < MAX_TIMERS; i++) + { + JsonObject timerObj = availableTimers.add(); + ESPTimer *timer = pinMap->getTimer(i); + timerObj["id"] = timer->id; + timerObj["name"] = timer->name; + timerObj["value"] = i; + for (size_t j = 0; j < 2; j++) + { + JsonObject timerChannelObj = timerChannels.add(); + timerChannelObj["name"] = timer->channels[j].name; + timerChannelObj["value"] = timer->channels[j].channel; + } + } + + doc["localIP"] = currentIP; + doc["gateway"] = currentGateway; + doc["subnet"] = currentSubnet; + doc["dns1"] = currentDns1; + doc["dns2"] = currentDns2; // Not being used currently + char macTemp[18] = {0}; +#ifdef ESP_ARDUINO3 + strlcpy(macTemp, Network.macAddress().c_str(), sizeof(macTemp)); +#else + strlcpy(macTemp, WiFi.macAddress().c_str(), sizeof(macTemp)); +#endif + doc["mac"] = macTemp; + + doc["chipModel"] = ESP.getChipModel(); + doc["chipRevision"] = ESP.getChipRevision(); + doc["chipCores"] = ESP.getChipCores(); + uint32_t chipId = 0; + for (int i = 0; i < 17; i = i + 8) + { + chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; + } + doc["chipID"] = chipId; + + doc["decoyPass"] = DECOY_PASS; + doc["apMode"] = apMode; + doc["defaultIP"] = m_settingsFactory->getAPModeIP(); + // String output; + serializeJson(doc, buf); + doc.clear(); + if (LogHandler::getLogLevel() == LogLevel::VERBOSE) + Serial.printf("SystemInfo: %s\n", buf.c_str()); + // buf[0] = {0}; + // strcpy(buf, output.c_str()); + } + + static bool loadButtons(bool loadDefault, JsonObject json = JsonObject()) + { + LogHandler::info(Tags::Settings, "Loading buttons"); + return loadSettingsJson(BUTTON_SETTINGS_PATH, loadDefault, m_buttonsMutex, [](const JsonObject json, bool &mutableLoadDefault) -> bool + { + + // const bool bootButtonEnabled = SettingsHandler::getValue(BOOT_BUTTON_ENABLED); + // const bool buttonSetsEnabled = SettingsHandler::getValue(BUTTON_SETS_ENABLED);; + // const char* bootButtonCommand = SettingsHandler::getValue(BOOT_BUTTON_COMMAND);; + // const int buttonAnalogDebounce = SettingsHandler::getValue(BUTTON_ANALOG_DEBOUNCE); + // setValue(json, bootButtonEnabled, "buttonCommand", "bootButtonEnabled", BOOT_BUTTON_ENABLED_DEFAULT); + // setValue(json, buttonSetsEnabled, "buttonCommand", "buttonSetsEnabled", BOOT_BUTTON_COMMAND_DEFAULT); + // setValue(json, bootButtonCommand, "buttonCommand", "bootButtonCommand", BUTTON_SETS_ENABLED_DEFAULT); + // setValue(json, buttonAnalogDebounce, "buttonCommand", "buttonAnalogDebounce", BUTTON_ANALOG_DEBOUNCE_DEFAULT); + if(!json.isNull()) + { + bool bootButtonEnabled = json[BOOT_BUTTON_ENABLED] | BOOT_BUTTON_ENABLED_DEFAULT; + m_settingsFactory->setValue(BOOT_BUTTON_ENABLED, bootButtonEnabled); + bool buttonSetsEnabled = json[BUTTON_SETS_ENABLED] | BUTTON_SETS_ENABLED_DEFAULT; + m_settingsFactory->setValue(BUTTON_SETS_ENABLED, buttonSetsEnabled); + const char* bootButtonCommand = json[BOOT_BUTTON_COMMAND] | BOOT_BUTTON_COMMAND_DEFAULT; + m_settingsFactory->setValue(BOOT_BUTTON_COMMAND, bootButtonCommand); + int buttonAnalogDebounce = json[BUTTON_ANALOG_DEBOUNCE] | BUTTON_ANALOG_DEBOUNCE_DEFAULT; + m_settingsFactory->setValue(BUTTON_ANALOG_DEBOUNCE, buttonAnalogDebounce); + m_settingsFactory->saveCommon(); + } + + JsonArray buttonSetsObj = json["buttonSets"].as(); + if(buttonSetsObj.isNull()) { + LogHandler::info(Tags::Settings, "No button sets stored, loading default"); + mutableLoadDefault = true; + const PinMap* pinMap = m_settingsFactory->getPins(); + for(int i = 0; i < MAX_BUTTON_SETS; i++) { + buttonSets[i] = ButtonSet(); + buttonSets[i].pin = pinMap->buttonSetPin(i); + + sprintf(buttonSets[i].name, "Button set %u", i+1); + LogHandler::debug(Tags::Settings, "Default buttonset name: %s, index: %u, pin: %ld", buttonSets[i].name, i, buttonSets[i].pin); + for(int j = 0; j < MAX_BUTTONS; j++) { + buttonSets[i].buttons[j] = ButtonModel(); + buttonSets[i].buttons[j].loadDefault(j); + LogHandler::debug(Tags::Settings, "Default button name: %s, index: %u, command: %s", buttonSets[i].name, buttonSets[i].buttons[j].index, buttonSets[i].buttons[j].command); + + } + } + } else { + std::vector pins; + for(int i = 0; i < MAX_BUTTON_SETS; i++) { + auto set = ButtonSet(); + set.fromJson(buttonSetsObj[i].as()); + pins.push_back(set.pin); + LogHandler::debug(Tags::Settings, "Loaded button set '%s', pin: %ld", set.name, set.pin); + buttonSets[i] = set; + for(int j = 0; j < MAX_BUTTONS; j++) { + LogHandler::debug(Tags::Settings, "Loaded button, name: %s, index: %u, command: %s", buttonSets[i].name, buttonSets[i].buttons[j].index, buttonSets[i].buttons[j].command); + } + } + m_settingsFactory->setValue(BUTTON_SET_PINS, pins); + m_settingsFactory->savePins(); + } + + if(initialized) + sendMessage(SettingProfile::Button, "analogButtonCommands"); + + return true; }, saveButtons, json); + } + + static bool saveButtons(JsonObject json = JsonObject()) + { + LogHandler::info(Tags::Settings, "Save buttons file"); + uint16_t docSize = 2000; + // for(int i = 0; i < MAX_BUTTON_SETS; i++) { + // LogHandler::debug(Tags::Settings, "Save buttonSets[i] '%s', pin: %ld", buttonSets[i].name, buttonSets[i].pin); + // for(int j = 0; j < MAX_BUTTONS; j++) { + // LogHandler::debug(Tags::Settings, "Save buttonSets[i].buttons, name: %s, index: %ld, command: %s", buttonSets[i].name, buttonSets[i].buttons[j].index, buttonSets[i].buttons[j].command); + // } + // } + return saveSettingsJson(BUTTON_SETTINGS_PATH, m_buttonsMutex, docSize, [](JsonDocument &doc) -> bool + { + + bool bootButtonEnabled = BOOT_BUTTON_ENABLED_DEFAULT; + m_settingsFactory->getValue(BOOT_BUTTON_ENABLED, bootButtonEnabled); + doc[BOOT_BUTTON_ENABLED] = bootButtonEnabled; + bool buttonSetsEnabled = BUTTON_SETS_ENABLED_DEFAULT; + m_settingsFactory->getValue(BUTTON_SETS_ENABLED, buttonSetsEnabled); + doc[BUTTON_SETS_ENABLED] = buttonSetsEnabled; + // char bootButtonCommand[BOOT_BUTTON_COMMAND_LEN] = {0}; + // m_settingsFactory->getValue(BOOT_BUTTON_COMMAND, bootButtonCommand, BOOT_BUTTON_COMMAND_LEN); + const char* bootButtonCommand = m_settingsFactory->getBootButtonCommand(); + doc[BOOT_BUTTON_COMMAND] = bootButtonCommand; + int buttonAnalogDebounce = BUTTON_ANALOG_DEBOUNCE_DEFAULT; + m_settingsFactory->getValue(BUTTON_ANALOG_DEBOUNCE, buttonAnalogDebounce); + doc[BUTTON_ANALOG_DEBOUNCE] = buttonAnalogDebounce; + + //auto buttonSetArray = doc["buttonSets"].as(); + std::vector pins; + for (size_t i = 0; i < MAX_BUTTON_SETS; i++) + { + //JsonObject obj; + doc["buttonSets"][i]["name"] = buttonSets[i].name; + + doc["buttonSets"][i]["pin"] = buttonSets[i].pin; + pins.push_back(buttonSets[i].pin); + doc["buttonSets"][i]["pullMode"] = (uint8_t)buttonSets[i].pullMode; + LogHandler::debug(Tags::Settings, "Saving button set '%s' from settings, pin: %ld", doc["buttonSets"][i]["name"].as(), doc["buttonSets"][i]["pin"].as()); + for(size_t j = 0; j < MAX_BUTTONS; j++) { + doc["buttonSets"][i]["buttons"][j]["name"] = buttonSets[i].buttons[j].name; + doc["buttonSets"][i]["buttons"][j]["index"] = buttonSets[i].buttons[j].index; + doc["buttonSets"][i]["buttons"][j]["command"] = buttonSets[i].buttons[j].command; + LogHandler::debug(Tags::Settings, "Saving button, name: %s, index: %u, command: %s", doc["buttonSets"][i]["buttons"][j]["name"].as(), doc["buttonSets"][i]["buttons"][j]["index"].as(), doc["buttonSets"][i]["buttons"][j]["command"].as()); + } + //buttonSetArray.add(obj); + } + m_settingsFactory->setValue(BUTTON_SET_PINS, pins); + m_settingsFactory->savePins(); + return true; }, loadButtons, json); + } + + static bool loadMotionProfiles(bool loadDefault, JsonObject json = JsonObject()) + { + LogHandler::info(Tags::Settings, "Loading motion profiles"); + // bool mutableLoadDefault = loadDefault; + // JsonDocument doc; //deserializeSize + // if(mutableLoadDefault || json.isNull()) { + // xSemaphoreTake(m_motionMutex, portMAX_DELAY); + // if(!checkForFileAndLoad(MOTION_PROFILE_SETTINGS_PATH, doc, mutableLoadDefault)) { + // saving = false; + // xSemaphoreGive(m_motionMutex); + // return false; + // } + // json = doc.as(); + // } + return loadSettingsJson(MOTION_PROFILE_SETTINGS_PATH, loadDefault, m_motionMutex, [](const JsonObject json, bool &mutableLoadDefault) -> bool + { + motionDefaultProfileIndex = json[MOTION_PROFILE_DEFAULT_INDEX] | MOTION_PROFILE_SELECTED_INDEX_DEFAULT; + if(!initialized) + motionSelectedProfileIndex = motionDefaultProfileIndex; + + JsonArray motionProfilesObj = json[MOTION_PROFILES].as(); + if(motionProfilesObj.isNull()) { + LogHandler::info(Tags::Settings, "No motion profiles stored, loading default"); + mutableLoadDefault = true; + for(int i = 0; i < MAX_MOTION_PROFILE_COUNT; i++) { + motionProfiles[i] = MotionProfile(i + 1); + motionProfiles[i].addDefaultChannel("L0"); + LogHandler::debug(Tags::Settings, "Added new Motion profile for: %s", motionProfiles[i].channels.back().name); + } + } else { + int i = 0; + for (JsonObject profileObj : motionProfilesObj) { + auto profile = MotionProfile(); + profile.fromJson(profileObj); + LogHandler::debug(Tags::Settings, "Loading motion profile '%s' from settings", profile.motionProfileName); + motionProfiles[i] = profile; + i++; + } + } + //xSemaphoreGive(m_motionMutex); + // if(mutableLoadDefault) + // saveMotionProfiles(); + return true; }, saveMotionProfiles, json); + } + + static bool saveMotionProfiles(JsonObject json = JsonObject()) + { + LogHandler::info(Tags::Settings, "Save motion profiles file"); + saving = true; + xSemaphoreTake(m_motionMutex, portMAX_DELAY); + if (!LittleFS.exists(MOTION_PROFILE_SETTINGS_PATH)) + { + LogHandler::error(Tags::Settings, "Motion profile file did not exist whan saving."); + saving = false; + xSemaphoreGive(m_motionMutex); + return false; + } + if (!json.isNull()) + { // If passed in, load the json into memory before flushing it to disk. + // WARNING: watchout for the mutex taken in this method. Changing these parameters below may result in hard locks. + loadMotionProfiles(false, json); // DO NOT PASS loadDefault as true else infinit loop + } + JsonDocument doc; // serializeSize + doc[MOTION_PROFILE_DEFAULT_INDEX] = motionDefaultProfileIndex; + LogHandler::debug(Tags::Settings, "motion profiles index: %ld", motionDefaultProfileIndex); + + for (int i = 0; i < MAX_MOTION_PROFILE_COUNT; i++) + { + // if(motionProfiles[i].edited) { // TODO: this does not work because doc is empty and needs to be loaded from disk first bedore modifying sections of it. + + LogHandler::debug(Tags::Settings, "Edited motion profile name: %s", motionProfiles[i].motionProfileName); + doc[MOTION_PROFILES][i]["name"] = motionProfiles[i].motionProfileName; + for (size_t j = 0; j < motionProfiles[i].channels.size(); j++) + { + // if(motionProfiles[i].channels[j].edited) { + LogHandler::debug(Tags::Settings, "motion profile channel: %s", motionProfiles[i].channels[j].name); + doc[MOTION_PROFILES][i]["channels"][j]["name"] = motionProfiles[i].channels[j].name; + doc[MOTION_PROFILES][i]["channels"][j]["update"] = motionProfiles[i].channels[j].motionUpdateGlobal; + doc[MOTION_PROFILES][i]["channels"][j]["period"] = motionProfiles[i].channels[j].motionPeriodGlobal; + doc[MOTION_PROFILES][i]["channels"][j]["amp"] = motionProfiles[i].channels[j].motionAmplitudeGlobal; + doc[MOTION_PROFILES][i]["channels"][j]["offset"] = motionProfiles[i].channels[j].motionOffsetGlobal; + doc[MOTION_PROFILES][i]["channels"][j]["phase"] = motionProfiles[i].channels[j].motionPhaseGlobal; + doc[MOTION_PROFILES][i]["channels"][j]["reverse"] = motionProfiles[i].channels[j].motionReversedGlobal; + doc[MOTION_PROFILES][i]["channels"][j]["periodRan"] = motionProfiles[i].channels[j].motionPeriodGlobalRandom; + doc[MOTION_PROFILES][i]["channels"][j]["periodMin"] = motionProfiles[i].channels[j].motionPeriodGlobalRandomMin; + doc[MOTION_PROFILES][i]["channels"][j]["periodMax"] = motionProfiles[i].channels[j].motionPeriodGlobalRandomMax; + doc[MOTION_PROFILES][i]["channels"][j]["ampRan"] = motionProfiles[i].channels[j].motionAmplitudeGlobalRandom; + doc[MOTION_PROFILES][i]["channels"][j]["ampMin"] = motionProfiles[i].channels[j].motionAmplitudeGlobalRandomMin; + doc[MOTION_PROFILES][i]["channels"][j]["ampMax"] = motionProfiles[i].channels[j].motionAmplitudeGlobalRandomMax; + doc[MOTION_PROFILES][i]["channels"][j]["offsetRan"] = motionProfiles[i].channels[j].motionOffsetGlobalRandom; + doc[MOTION_PROFILES][i]["channels"][j]["offsetMin"] = motionProfiles[i].channels[j].motionOffsetGlobalRandomMin; + doc[MOTION_PROFILES][i]["channels"][j]["offsetMax"] = motionProfiles[i].channels[j].motionOffsetGlobalRandomMax; + doc[MOTION_PROFILES][i]["channels"][j]["phaseRan"] = motionProfiles[i].channels[j].motionPhaseRandom; + doc[MOTION_PROFILES][i]["channels"][j]["phaseMin"] = motionProfiles[i].channels[j].motionPhaseRandomMin; + doc[MOTION_PROFILES][i]["channels"][j]["phaseMax"] = motionProfiles[i].channels[j].motionPhaseRandomMax; + doc[MOTION_PROFILES][i]["channels"][j]["ranMin"] = motionProfiles[i].channels[j].motionRandomChangeMin; + doc[MOTION_PROFILES][i]["channels"][j]["ranMax"] = motionProfiles[i].channels[j].motionRandomChangeMax; + motionProfiles[i].channels[j].edited = false; + //} + } + if (initialized && motionSelectedProfileIndex == i) + { + sendMessage(SettingProfile::MotionProfile, MOTION_PROFILES); + } + motionProfiles[i].edited = false; + //} + } + File file = LittleFS.open(MOTION_PROFILE_SETTINGS_PATH, FILE_WRITE); + if (serializeJson(doc, file) == 0) + { + LogHandler::error(Tags::Settings, "Failed to write to motion profiles file"); + file.close(); + xSemaphoreGive(m_motionMutex); + saving = false; + return false; + } + + xSemaphoreGive(m_motionMutex); + saving = false; + return true; + } + + static bool loadChannels(bool loadDefault, JsonObject json = JsonObject()) + { + + MotorType motorType; + DeviceType deviceType; + m_settingsFactory->getValue(MOTOR_TYPE_SETTING, motorType); + m_settingsFactory->getValue(DEVICE_TYPE, deviceType); + // Init motor type BEFORE loading the channels else it will load the defaults + channelMap.init(m_settingsFactory->getTcodeVersion(), motorType, deviceType); + + LogHandler::info(Tags::Settings, "Loading channel profile"); + return loadSettingsJson(CHANNELS_SETTINGS_PATH, loadDefault, m_channelsMutex, [](const JsonObject json, bool &mutableLoadDefault) -> bool + { + + JsonArray channelProfileObj = json[CHANNEL_PROFILE].as(); + + if(channelProfileObj.isNull()) { + LogHandler::info(Tags::Settings, "No channel profile stored, loading default"); + mutableLoadDefault = true; + } else { + for (JsonObject profileObj : channelProfileObj) { + const char* name = profileObj[CHANNEL_NAME]; + Channel* channel = channelMap.get(name); + if(!channel) + { + LogHandler::error(Tags::Settings, "Channel missing from stored profile: %s", name); + continue; + } + channel->userMin = profileObj[CHANNEL_USER_MIN] | TCODE_MIN; + channel->userMid = profileObj[CHANNEL_USER_MID] | TCODE_MID; + channel->userMax = profileObj[CHANNEL_USER_MAX] | TCODE_MAX; + channel->rangeLimitEnabled = profileObj[CHANNEL_RANGE_LIMIT_ENABLED]; + LogHandler::debug(Tags::Settings, "Loading channel profile '%s' from settings", name); + } + } + return true; }, saveChannels, json); + } + + static bool saveChannels(JsonObject json = JsonObject()) + { + LogHandler::info(Tags::Settings, "Save channel profile file"); + saving = true; + xSemaphoreTake(m_channelsMutex, portMAX_DELAY); + if (!LittleFS.exists(CHANNELS_SETTINGS_PATH)) + { + LogHandler::error(Tags::Settings, "Channel profile file did not exist whan saving."); + saving = false; + xSemaphoreGive(m_channelsMutex); + return false; + } + if (!json.isNull()) + { // If passed in, load the json into memory before flushing it to disk. + // WARNING: watchout for the mutex taken in this method. Changing these parameters below may result in hard locks. + loadChannels(false, json); // DO NOT PASS loadDefault as true else infinit loop + } + JsonDocument doc; // serializeSize + for (int i = 0; i < channelMap.count(); i++) + { + Channel *channel = channelMap.get(i); + doc[CHANNEL_PROFILE][i][CHANNEL_NAME] = channel->Name; + doc[CHANNEL_PROFILE][i][CHANNEL_USER_MIN] = channel->userMin; + doc[CHANNEL_PROFILE][i][CHANNEL_USER_MID] = channel->userMid; + doc[CHANNEL_PROFILE][i][CHANNEL_USER_MAX] = channel->userMax; + doc[CHANNEL_PROFILE][i][CHANNEL_RANGE_LIMIT_ENABLED] = channel->rangeLimitEnabled; + if (initialized) + { + sendMessage(SettingProfile::ChannelRanges, CHANNEL_PROFILE); + } + } + File file = LittleFS.open(CHANNELS_SETTINGS_PATH, FILE_WRITE); + if (serializeJson(doc, file) == 0) + { + LogHandler::error(Tags::Settings, "Failed to write to channel profile file"); + file.close(); + xSemaphoreGive(m_channelsMutex); + saving = false; + return false; + } + + xSemaphoreGive(m_channelsMutex); + saving = false; + return true; + } + + static std::vector &getMotionChannels() + { + return motionProfiles[motionSelectedProfileIndex].channels; + } + + static bool getMotionEnabled() + { + return motionEnabled; + } + static void setMotionEnabled(const bool &newValue) + { + setValue(newValue, motionEnabled, SettingProfile::MotionProfile, MOTION_ENABLED); + } + static bool getMotionPaused() + { + return motionPaused; + } + static void setMotionPaused(const bool &newValue) + { + setValue(newValue, motionPaused, SettingProfile::MotionProfile, MOTION_PAUSED); + } + + static int getMotionDefaultProfileIndex() + { + return motionDefaultProfileIndex; + } + static void setMotionProfileName(const char newValue[MAX_MOTION_PROFILE_NAME_LENGTH]) + { + strcpy(motionProfiles[motionSelectedProfileIndex].motionProfileName, newValue); + } + + // static int getMotionUpdateGlobal(const char name[3]) + // { + // return motionProfiles[motionSelectedProfileIndex].motionUpdateGlobal; + // } + // static void setMotionUpdateGlobal(const int& newValue) + // { + // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionUpdateGlobal, "motionGenerator", "motionUpdateGlobal"); + // } + // static int getMotionPeriodGlobal() + // { + // return motionProfiles[motionSelectedProfileIndex].motionPeriodGlobal; + // } + // static void setMotionPeriodGlobal(const int& newValue) + // { + // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionPeriodGlobal, "motionGenerator", "motionPeriodGlobal"); + // } + // static int getMotionAmplitudeGlobal() + // { + // return motionProfiles[motionSelectedProfileIndex].motionAmplitudeGlobal; + // } + // static void setMotionAmplitudeGlobal(const int& newValue) + // { + // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionAmplitudeGlobal, "motionGenerator", "motionAmplitudeGlobal"); + // } + // static int getMotionOffsetGlobal() + // { + // return motionProfiles[motionSelectedProfileIndex].motionOffsetGlobal; + // } + // static void setMotionOffsetGlobal(const int& newValue) + // { + // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionOffsetGlobal, "motionGenerator", "motionOffsetGlobal"); + // } + // static float getMotionPhaseGlobal() + // { + // return motionProfiles[motionSelectedProfileIndex].motionPhaseGlobal; + // } + // static void setMotionPhaseGlobal(const float& newValue) + // { + // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionPhaseGlobal, "motionGenerator", "motionPhaseGlobal"); + // } + // static bool getMotionReversedGlobal() + // { + // return motionProfiles[motionSelectedProfileIndex].motionReversedGlobal; + // } + // static void setMotionReversedGlobal(const bool& newValue) + // { + // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionReversedGlobal, "motionGenerator", "motionReversedGlobal"); + // } + // static bool getMotionPeriodGlobalRandom() + // { + // return motionProfiles[motionSelectedProfileIndex].motionPeriodGlobalRandom; + // } + // static void setMotionPeriodGlobalRandom(const bool& newValue) + // { + // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionPeriodGlobalRandom, "motionGenerator", "motionPeriodGlobalRandom"); + // } + // static int getMotionPeriodGlobalRandomMin() + // { + // return motionProfiles[motionSelectedProfileIndex].motionPeriodGlobalRandomMin; + // } + // static void setMotionPeriodGlobalRandomMin(const int& newValue) + // { + // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionPeriodGlobalRandomMin, "motionGenerator", "motionPeriodGlobalRandomMin"); + // } + // static int getMotionPeriodGlobalRandomMax() + // { + // return motionProfiles[motionSelectedProfileIndex].motionPeriodGlobalRandomMax; + // } + // static void setMotionPeriodGlobalRandomMax(const int& newValue) + // { + // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionPeriodGlobalRandomMax, "motionGenerator", "motionPeriodGlobalRandomMax"); + // } + // static bool getMotionAmplitudeGlobalRandom() + // { + // return motionProfiles[motionSelectedProfileIndex].motionAmplitudeGlobalRandom ; + // } + // static void setMotionAmplitudeGlobalRandom(const bool& newValue) + // { + // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionAmplitudeGlobalRandom, "motionGenerator", "motionAmplitudeGlobalRandom"); + // } + // static int getMotionAmplitudeGlobalRandomMin() + // { + // return motionProfiles[motionSelectedProfileIndex].motionAmplitudeGlobalRandomMin; + // } + // static void setMotionAmplitudeGlobalRandomMin(const int& newValue) + // { + // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionAmplitudeGlobalRandomMin = newValue, "motionGenerator", "motionAmplitudeGlobalRandomMin"); + // } + // static int getMotionAmplitudeGlobalRandomMax() + // { + // return motionProfiles[motionSelectedProfileIndex].motionAmplitudeGlobalRandomMax; + // } + // static void setMotionAmplitudeGlobalRandomMax(const int& newValue) + // { + // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionAmplitudeGlobalRandomMax = newValue, "motionGenerator", "motionAmplitudeGlobalRandomMax"); + // } + // static bool getMotionOffsetGlobalRandom() + // { + // return motionProfiles[motionSelectedProfileIndex].motionOffsetGlobalRandom; + // } + // static void setMotionOffsetGlobalRandom(const bool& newValue) + // { + // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionOffsetGlobalRandom = newValue, "motionGenerator", "motionOffsetGlobalRandom"); + // } + // static int getMotionOffsetGlobalRandomMin() + // { + // return motionProfiles[motionSelectedProfileIndex].motionOffsetGlobalRandomMin; + // } + // static void setMotionOffsetGlobalRandomMin(const int& newValue) + // { + // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionOffsetGlobalRandomMin, "motionGenerator", "motionOffsetGlobalRandomMin"); + // } + // static int getMotionOffsetGlobalRandomMax() + // { + // return motionProfiles[motionSelectedProfileIndex].motionOffsetGlobalRandomMax; + // } + // static void setMotionOffsetGlobalRandomMax(const int& newValue) + // { + // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionOffsetGlobalRandomMax = newValue, "motionGenerator", "motionOffsetGlobalRandomMax"); + // } + // static int getMotionRandomChangeMin() + // { + // return motionProfiles[motionSelectedProfileIndex].motionRandomChangeMin; + // } + // static void setMotionRandomChangeMin(const int& newValue) + // { + // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionRandomChangeMin, "motionGenerator", "motionRandomChangeMin"); + // } + // static int getMotionRandomChangeMax() + // { + // return motionProfiles[motionSelectedProfileIndex].motionRandomChangeMax; + // } + // static void setMotionRandomChangeMax(const int& newValue) + // { + // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionRandomChangeMax, "motionGenerator", "motionRandomChangeMax"); + // } + + static int motionProfileExists(const char *profile) + { + for (size_t i = 0; i < MAX_MOTION_PROFILE_COUNT; i++) + { + if (strcmp(motionProfiles[i].motionProfileName, profile) == 0) + return (int)i; + } + return -1; + } + + static void setMotionDefaults() + { + setMotionEnabled(false); + auto motionProfile = MotionProfile(motionSelectedProfileIndex + 1); + setMotionProfile(motionProfile, motionSelectedProfileIndex); + } + + static void setMotionProfile(const char profile[MAX_MOTION_PROFILE_NAME_LENGTH]) + { + auto index = motionProfileExists(profile); + if (index < 0) + { + LogHandler::error(Tags::Settings, "Motion profile %s does not exist", profile); + return; + } + setMotionProfile(index); + } + + static void setMotionProfile(const int &index) + { + if (index < 0 || index > MAX_MOTION_PROFILE_COUNT - 1) + { + LogHandler::error(Tags::Settings, "Invalid motion profile index: %ld", index); + return; + } + auto newProfile = motionProfiles[index]; + setMotionProfile(newProfile, index); + } + + static void setMotionProfile(const MotionProfile &profile, int profileIndex) + { + if (profileIndex < 0 || profileIndex > MAX_MOTION_PROFILE_COUNT - 1) + { + LogHandler::error(Tags::Settings, "Invalid motion profile index: %ld", profileIndex); + return; + } + // m_settingsFactory->setValue(MOTION_PROFILE_SELECTED_INDEX, profileIndex); + setValue(profileIndex, motionSelectedProfileIndex, SettingProfile::MotionProfile, MOTION_PROFILE_SELECTED_INDEX); + } + + static void cycleMotionProfile() + { + if (!getMotionEnabled()) + { + setMotionEnabled(true); + return; + } + uint8_t newProfileIndex = motionSelectedProfileIndex + 1; + if (newProfileIndex > MAX_MOTION_PROFILE_COUNT - 1) + { + newProfileIndex = 0; + setMotionEnabled(false); + } + auto newProfile = motionProfiles[newProfileIndex]; + setMotionProfile(newProfile, newProfileIndex); + } + + static const bool readFile(char *&buf, const char *path) + { + if (!LittleFS.exists(path)) + { + LogHandler::error(Tags::Settings, "Path did not exist when reading contents: %s", path); + return false; + } + File file = LittleFS.open(path, "r"); + printFree(); + String fileStr = file.readString(); + // buf = static_cast(malloc(fileStr.length() + 1)); + printFree(); + LogHandler::info(Tags::Settings, "Create buffer: %u", fileStr.length()); + buf = new char[fileStr.length() + 1]; + strcpy(buf, fileStr.c_str()); + return true; + } + + static const int getDeserializeSize() + { + return deserializeSize; + } + + // static void processTCodeJson(char *outbuf, const char *tcodeJson) + // { + // StaticJsonDocument<512> doc; + // DeserializationError error = deserializeJson(doc, tcodeJson); + // if (error) + // { + // LogHandler::error(Tags::Settings, "Failed to read udp jsonobject, using default configuration"); + // outbuf[0] = {0}; + // return; + // } + // JsonArray arr = doc.as(); + // char buffer[MAX_COMMAND] = ""; + // for (JsonObject repo : arr) + // { + // const char *channel = repo["c"]; + // int value = repo["v"]; + // if (channel != nullptr && value > 0) + // { + // if (buffer[0] == '\0') + // { + // // Serial.println("tcode empty"); + // strcpy(buffer, channel); + // } + // else + // { + // strcat(buffer, channel); + // } + // // Serial.print("channel: "); + // // Serial.print(channel); + // // Serial.print(" value: "); + // // Serial.println(value); + // char integer_string[4]; + // sprintf(integer_string, SettingsHandler::TCodeVersionEnum == TCodeVersion::v0_2 ? "%03d" : "%04d", SettingsHandler::calculateRange(name, value)); + // // pad(integer_string); + // // sprintf(integer_string, "%d", SettingsHandler::calculateRange(name, value)); + // // Serial.print("integer_string"); + // // Serial.println(integer_string); + // strcat(buffer, integer_string); + // int speed = repo["s"]; + // int interval = repo["i"]; + // if (interval > 0) + // { + // char interval_string[5]; + // sprintf(interval_string, "%d", interval); + // strcat(buffer, "I"); + // strcat(buffer, interval_string); + // } + // else if (speed > 0) + // { + // char speed_string[5]; + // sprintf(speed_string, "%d", speed); + // strcat(buffer, "S"); + // strcat(buffer, speed_string); + // } + // strcat(buffer, " "); + // // Serial.print("buffer "); + // // Serial.println(buffer); + // } + // } + // strcpy(outbuf, buffer); + // strcat(outbuf, "\n"); + // // Serial.print("outbuf "); + // // Serial.println(outbuf); + // } + + static bool waitForI2CDevices(const int &i2cAddress = 0) + { + int tries = 0; + if (i2cAddress) + LogHandler::info(Tags::Settings, "Looking for I2c address: %ld", i2cAddress); + while ((systemI2CAddresses.size() == 0 || i2cAddress) && tries <= 3) + { + tries++; + I2CScan(); + if (i2cAddress && std::find(systemI2CAddresses.begin(), systemI2CAddresses.end(), i2cAddress) != systemI2CAddresses.end()) + { + return true; + } + else if (i2cAddress) + { + LogHandler::info(Tags::Settings, "I2c address: %ld not found. trying again...", i2cAddress); + } + else if (systemI2CAddresses.size() == 0) + { + LogHandler::info(Tags::Settings, "No I2C devices found in system, trying again..."); + } + if (tries >= 3) + { + if (i2cAddress) + { + LogHandler::error(Tags::Settings, "I2c address: %ld timed out.", i2cAddress); + } + else + { + LogHandler::error(Tags::Settings, "No I2C devices found in system"); + } + return false; + } + vTaskDelay(1000 / portTICK_PERIOD_MS); + } + return true; + } + + static bool I2CScan() + { + systemI2CAddresses.clear(); + byte error, address; + int nDevices; + LogHandler::info(Tags::Settings, "Scanning for I2C..."); + nDevices = 0; + int8_t sdaPin = I2C_SDA_PIN_DEFAULT; + m_settingsFactory->getValue(I2C_SDA_PIN, sdaPin); + int8_t sclPin = I2C_SCL_PIN_DEFAULT; + m_settingsFactory->getValue(I2C_SCL_PIN, sclPin); + if (sdaPin < 0 || sclPin < 0) + { + LogHandler::debug(Tags::Settings, "SDA or SCL is disabled when scaning for I2C devices sdaPin: %d, sclPin: %d", sdaPin, sclPin); + return false; + } + Wire.begin(sdaPin, sclPin); + for (address = 1; address < 127; address++) + { + Wire.beginTransmission(address); + error = Wire.endTransmission(); + if (error == 0) + { + // Serial.print("I2C device found at address 0x"); + // if (address<16) + // { + // Serial.print("0"); + // } + // Serial.println(address,HEX); + + // std::stringstream I2C_Address_String; + // I2C_Address_String << "0x" << std::hex << address; + // std::string foundAddress = I2C_Address_String.str(); + + char buf[10]; + hexToString(address, buf); + LogHandler::info(Tags::Settings, "I2C device found at address %s, byte %ld", buf, address); + + systemI2CAddresses.push_back((int)address); + nDevices++; + } + else if (error == 4) + { + Serial.print("Unknow error at address 0x"); + if (address < 16) + { + Serial.print("0"); + } + Serial.println(address, HEX); + // std::stringstream I2C_Address_String; + // I2C_Address_String << "0x" << std::hex << address; + // std::string foundAddress = I2C_Address_String.str(); + // LogHandler::error(Tags::Settings, "Unknow error at address %s", foundAddress); + } + } + if (nDevices == 0) + { + LogHandler::info(Tags::Settings, "No I2C devices found"); + return false; + } + return true; + } + + static Channel *getChannel(const char *name) + { + return channelMap.get(name); + } + + static uint16_t getChannelMin(const char *name) + { + Channel *channelProfile = channelMap.get(name); + if (!channelProfile) + { + LogHandler::error(Tags::Settings, "[getChannelMin] Invalid name for current map: %s", name); + return TCODE_MIN; + } + return channelProfile->min; + } + + static uint16_t getChannelMax(const char *name) + { + Channel *channelProfile = channelMap.get(name); + if (!channelProfile) + { + LogHandler::error(Tags::Settings, "[getChannelMax] Invalid name for current map: %s", name); + return TCODE_MAX; + } + return channelProfile->max; + } + + static uint16_t getChannelUserMin(const char *name) + { + Channel *channelProfile = channelMap.get(name); + if (!channelProfile) + { + LogHandler::error(Tags::Settings, "[getChannelUserMin] Invalid name for current map: %s", name); + return TCODE_MIN; + } + return channelProfile->userMin; + } + + static uint16_t getChannelUserMax(const char *name) + { + Channel *channelProfile = channelMap.get(name); + if (!channelProfile) + { + LogHandler::error(Tags::Settings, "[getChannelUserMax] Invalid name for current map: %s", name); + return TCODE_MAX; + } + return channelProfile->userMax; + } + + static void setChannelMin(const char *name, uint16_t value) + { + Channel *channelProfile = channelMap.get(name); + if (!channelProfile) + { + LogHandler::error(Tags::Settings, "[setChannelMin] Invalid name for current map: %s", name); + return; + } + channelProfile->userMin = value; + } + + static void setChannelMax(const char *name, uint16_t value) + { + Channel *channelProfile = channelMap.get(name); + if (!channelProfile) + { + LogHandler::error(Tags::Settings, "[setChannelMax] Invalid name for current map: %s", name); + return; + } + channelProfile->userMax = value; + } + + static void setChannelRangesEnabled(bool enabled) + { + channelRangesEnabled = enabled; + sendMessage(SettingProfile::ChannelRanges, "channelRangesEnabled"); + } + static bool getChannelRangesEnabled() + { + return channelRangesEnabled; + } + +private: + static SettingsFactory *m_settingsFactory; + static SemaphoreHandle_t m_motionMutex; + static SemaphoreHandle_t m_channelsMutex; + static SemaphoreHandle_t m_wifiMutex; + static SemaphoreHandle_t m_buttonsMutex; + static SemaphoreHandle_t m_settingsMutex; + static SETTING_STATE_FUNCTION_PTR_T message_callback; + // Use http://arduinojson.org/assistant to compute the capacity. + // static const size_t readCapacity = JSON_OBJECT_SIZE(100) + 2000; + // static const size_t saveCapacity = JSON_OBJECT_SIZE(100); + static const int deserializeSize = 32768; + static const int serializeSize = 24576; + // 3072 + + static bool motionEnabled; + static int motionSelectedProfileIndex; + static int motionDefaultProfileIndex; + // static MotionProfile motionProfiles[maxMotionProfileCount]; + + // static bool voiceEnabled; + // static bool voiceMuted; + // static int voiceWakeTime ; + // static int voiceVolume; + + // static bool update(JsonObject json) + // { + // logLevel = (LogLevel)(json["logLevel"] | 2); + // LogHandler::setLogLevel(logLevel); + // LogHandler::debug(Tags::Settings, "Load Json: Memory usage: %u bytes", json.memoryUsage()); + + // boardType = (BoardType)(json["boardType"] | (uint8_t)BoardType::DEVKIT); + + // if(isBoardType(BoardType::CRIMZZON) || isBoardType(BoardType::ISAAC)) { + // TCodeVersionEnum = TCodeVersion::v0_3; + // TCodeVersionName = TCodeVersionMapper(TCodeVersionEnum); + // } + // std::vector includesVec; + // setValue(json, includesVec, "log", "log-include-tags"); + // LogHandler::setIncludes(includesVec); + + // std::vector excludesVec; + // setValue(json, excludesVec, "log", "log-exclude-tags"); + // LogHandler::setExcludes(excludesVec); + + // if(!isBoardType(BoardType::CRIMZZON)) { + // TCodeVersionEnum = (TCodeVersion)(json["TCodeVersion"] | 1); + // TCodeVersionName = TCodeVersionMapper(TCodeVersionEnum); + // } + + // channelMap.init(TCodeVersionEnum, motorType); + + // #if MOTOR_TYPE == 1 + // for (auto x : ChannelMapBLDC) { + // currentChannels.push_back(x); + // } + // #else + // currentChannels.clear(); + // if(TCodeVersionEnum == TCodeVersion::v0_2) { + // // for (size_t i = 0; i < (sizeof(ChannelMapV2)/sizeof(Channel)); i++) { + // // currentChannels.push_back(ChannelMapV2[i]); + // // } + // for (auto x : ChannelMapV2) { + // currentChannels.push_back(x); + // } + // } else { + // for (auto x : ChannelMapV3) { + // currentChannels.push_back(x); + // } + // } + // #endif + + // int tcodeMax = TCodeVersionEnum == TCodeVersion::v0_2 ? 999 : 9999; + // for (size_t i = 0; i < currentChannels.size(); i++) + // { + // uint16_t min = json["channelRanges"][currentChannels[i].Name]["min"].as(); + // uint16_t max = json["channelRanges"][currentChannels[i].Name]["max"].as(); + // currentChannels[i].min = !min ? 1 : min; + // currentChannels[i].max = !max ? tcodeMax : max; + // } + + // sendMessage("channelRanges", "channelRanges");// TODO: channelranges should be in its own json + + // udpServerPort = json["udpServerPort"] | 8000; + // webServerPort = json["webServerPort"] | 80; + // const char *hostnameTemp = json["hostname"] | "tcode"; + // if (hostnameTemp != nullptr) + // strcpy(hostname, hostnameTemp); + // const char *friendlyNameTemp = json["friendlyName"] | "ESP32 TCode"; + // if (friendlyNameTemp != nullptr) + // strcpy(friendlyName, friendlyNameTemp); + + // bluetoothEnabled = json["bluetoothEnabled"] | false; + + // // Servo motors////////////////////////////////////////////////////////////////////////////////// + // pitchFrequencyIsDifferent = json["pitchFrequencyIsDifferent"]; + // msPerRad = json["msPerRad"] | 637; + // servoFrequency = json["servoFrequency"] | 50; + // pitchFrequency = json[pitchFrequencyIsDifferent ? "pitchFrequency" : "servoFrequency"] | servoFrequency; + // sr6Mode = json["sr6Mode"]; + + // RightServo_ZERO = json["RightServo_ZERO"] | 1500; + // LeftServo_ZERO = json["LeftServo_ZERO"] | 1500; + // RightUpperServo_ZERO = json["RightUpperServo_ZERO"] | 1500; + // LeftUpperServo_ZERO = json["LeftUpperServo_ZERO"] | 1500; + // PitchLeftServo_ZERO = json["PitchLeftServo_ZERO"] | 1500; + // PitchRightServo_ZERO = json["PitchRightServo_ZERO"] | 1500; + + // BLDC_UsePWM = json["BLDC_UsePWM"] | false; // Must be before pinout is set + // BLDC_UseMT6701 = json["BLDC_UseMT6701"] | true; + // BLDC_UseHallSensor = json["BLDC_UseHallSensor"] | false; + // BLDC_Pulley_Circumference = json["BLDC_Pulley_Circumference"] | 60; + // BLDC_MotorA_Voltage = round2(json["BLDC_MotorA_Voltage"] | 20.0); + // BLDC_MotorA_Current = round2(json["BLDC_MotorA_Current"] | 1.0); + // BLDC_MotorA_ParametersKnown = json["BLDC_MotorA_ParametersKnown"] | false; + // BLDC_MotorA_ZeroElecAngle = round2(json["BLDC_MotorA_ZeroElecAngle"] | 0.00); + // BLDC_RailLength = json["BLDC_RailLength"] | 125; + // BLDC_StrokeLength = json["BLDC_StrokeLength"] | 120; + + // setBoardPinout(json); + + // if(isBoardType(BoardType::CRIMZZON)) { + // heaterResolution = json["heaterResolution"] | 8; + // caseFanResolution = json["caseFanResolution"] | 10; + // caseFanFrequency = json["caseFanFrequency"] | 25; + // Display_Screen_Height = json["Display_Screen_Height"] | 32; + // } + + // twistFrequency = json["twistFrequency"] | 50; + // squeezeFrequency = json["squeezeFrequency"] | 50; + // valveFrequency = json["valveFrequency"] | 50; + // continuousTwist = json["continuousTwist"]; + // feedbackTwist = json["feedbackTwist"]; + // analogTwist = json["analogTwist"]; + // TwistServo_ZERO = json["TwistServo_ZERO"] | 1500; + // ValveServo_ZERO = json["ValveServo_ZERO"] | 1500; + // SqueezeServo_ZERO = json["Squeeze_ZERO"] | 1500; + + // staticIP = json["staticIP"]; + // const char *localIPTemp = json["localIP"] | "192.168.0.150"; + // if (localIPTemp != nullptr) + // strcpy(localIP, localIPTemp); + // const char *gatewayTemp = json["gateway"] | "192.168.0.1"; + // if (gatewayTemp != nullptr) + // strcpy(gateway, gatewayTemp); + // const char *subnetTemp = json["subnet"] | "255.255.255.0"; + // if (subnetTemp != nullptr) + // strcpy(subnet, subnetTemp); + // const char *dns1Temp = json["dns1"] | "8.8.8.8"; + // if (dns1Temp != nullptr) + // strcpy(dns1, dns1Temp); + // const char *dns2Temp = json["dns2"] | "8.8.4.4"; + // if (dns2Temp != nullptr) + // strcpy(dns2, dns2Temp); + + // autoValve = json["autoValve"]; + // inverseValve = json["inverseValve"]; + // valveServo90Degrees = json["valveServo90Degrees"]; + // inverseStroke = json["inverseStroke"]; + // inversePitch = json["inversePitch"]; + // lubeEnabled = json["lubeEnabled"]; + // lubeAmount = json["lubeAmount"] | 255; + // displayEnabled = json["displayEnabled"] | true; + // sleeveTempDisplayed = json["sleeveTempDisplayed"]; + // internalTempDisplayed = json["internalTempDisplayed"]; + // versionDisplayed = json["versionDisplayed"] | true; + // Display_Screen_Width = json["Display_Screen_Width"] | 128; + // if(!isBoardType(BoardType::CRIMZZON)) { + // Display_Screen_Height = json["Display_Screen_Height"] | 64; + // } + // const char *Display_I2C_AddressTemp = json["Display_I2C_Address"] | "0x3c"; + // if (Display_I2C_AddressTemp != nullptr) + // Display_I2C_Address = (int)strtol(Display_I2C_AddressTemp, NULL, 0); + // Display_Rst_PIN = json["Display_Rst_PIN"] | -1; + + // tempSleeveEnabled = json["tempSleeveEnabled"]; + // heaterThreshold = json["heaterThreshold"] | 5.0; + // heaterFrequency = json["heaterFrequency"] | 50; + // if(!isBoardType(BoardType::CRIMZZON)) { + // heaterResolution = json["heaterResolution"] | 8; + // } + // TargetTemp = json["TargetTemp"] | 40.0; + // HeatPWM = json["HeatPWM"] | 255; + // HoldPWM = json["HoldPWM"] | 110; + + // tempInternalEnabled = json["tempInternalEnabled"]; + // fanControlEnabled = json["fanControlEnabled"]; + // internalTempForFan = json["internalTempForFan"] | 30.0; + // internalMaxTemp = json["internalMaxTemp"] | 50.0; + + // batteryLevelEnabled = json["batteryLevelEnabled"]; + // batteryLevelNumeric = json["batteryLevelNumeric"]; + // batteryVoltageMax = json["batteryVoltageMax"] | 12.6; + // batteryCapacityMax = json["batteryCapacityMax"] | 3500; + + // if(!isBoardType(BoardType::CRIMZZON)) { + // caseFanFrequency = json["caseFanFrequency"] | 25; + // caseFanResolution = json["caseFanResolution"] | 10; + // } + // caseFanMaxDuty = pow(2, caseFanResolution) - 1; + + // lubeEnabled = json["lubeEnabled"]; + + // setValue(json, voiceEnabled, "voiceHandler", "voiceEnabled", false); + // setValue(json, voiceMuted, "voiceHandler", "voiceMuted", false); + // setValue(json, voiceVolume, "voiceHandler", "voiceVolume", 0); + // setValue(json, voiceWakeTime, "voiceHandler", "voiceWakeTime", 10); + + // lastRebootReason = machine_reset_cause(); + // LogHandler::debug(Tags::Settings, "Last reset reason: %s", SettingsHandler::lastRebootReason); + + // LogUpdateDebug(); + // return true; + // } + + // static bool compileCommonJsonDocument(DynamicJsonDocument& doc) + // { + // // LogHandler::info(Tags::Settings, "Save settings"); + // // Delete existing file, otherwise the configuration is appended to the file + // // Serial.print("LittleFS used: "); + // // Serial.println(LittleFS.usedBytes() + "/" + LittleFS.totalBytes()); + // // if (!LittleFS.remove(userSettingsFilePath)) + // // { + // // LogHandler::error(Tags::Settings, "Failed to remove settings file: %s", userSettingsFilePath); + // // } + // // File file = LittleFS.open(userSettingsFilePath, FILE_WRITE); + // // if (!file) + // // { + // // LogHandler::error(Tags::Settings, "Failed to create settings file: %s", userSettingsFilePath); + // // return false; + // // } + + // // // Allocate a temporary docdocument + // // DynamicdocDocument doc(serializeSize); + + // doc["boardType"] = (uint8_t)boardType; + // doc["logLevel"] = (int)logLevel; + // LogHandler::setLogLevel(logLevel); + // // Serial.println("logLevel: "); + // // Serial.println((int)logLevel); + // // Serial.println( doc["logLevel"] .as()); + // // Serial.println((int)doc["logLevel"]); + + // // std::vector tags; + // // tags.push_back(Tags::DisplayHandler); + // // tags.push_back(Tags::BLDCHandler); + // // tags.push_back(Tags::ServoHandler3); + // // LogHandler::setTags(tags); + + // for (size_t i = 0; i < currentChannels.size(); i++) + // { + // doc["channelRanges"][currentChannels[i].Name]["min"] = currentChannels[i].min; + // doc["channelRanges"][currentChannels[i].Name]["max"] = currentChannels[i].max; + // // LogHandler::debug(Tags::Settings, "save %s min: %i", currentChannels[i].Name, doc["channelRanges"][currentChannels[i].Name]["min"].as()); + // // LogHandler::debug(Tags::Settings, "save %s max: %i", currentChannels[i].Name, doc["channelRanges"][currentChannels[i].Name]["max"].as()); + // } + + // if (!tempInternalEnabled) + // { + // internalTempDisplayed = false; + // fanControlEnabled = false; + // } + // if (!tempSleeveEnabled) + // { + // sleeveTempDisplayed = false; + // } + // doc["fullBuild"] = fullBuild; + // doc["TCodeVersion"] = (int)TCodeVersionEnum; + + // doc["udpServerPort"] = udpServerPort; + // doc["webServerPort"] = webServerPort; + // doc["hostname"] = hostname; + // doc["friendlyName"] = friendlyName; + // doc["bluetoothEnabled"] = bluetoothEnabled; + // doc["pitchFrequencyIsDifferent"] = pitchFrequencyIsDifferent; + // doc["msPerRad"] = msPerRad; + // doc["servoFrequency"] = servoFrequency; + // doc["pitchFrequency"] = pitchFrequency; + // doc["valveFrequency"] = valveFrequency; + // doc["twistFrequency"] = twistFrequency; + // doc["squeezeFrequency"] = squeezeFrequency; + // doc["continuousTwist"] = continuousTwist; + // doc["feedbackTwist"] = feedbackTwist; + // doc["analogTwist"] = analogTwist; + // doc["TwistFeedBack_PIN"] = TwistFeedBack_PIN; + // doc["RightServo_PIN"] = RightServo_PIN; + // doc["LeftServo_PIN"] = LeftServo_PIN; + // doc["RightUpperServo_PIN"] = RightUpperServo_PIN; + // doc["LeftUpperServo_PIN"] = LeftUpperServo_PIN; + // doc["PitchLeftServo_PIN"] = PitchLeftServo_PIN; + // doc["PitchRightServo_PIN"] = PitchRightServo_PIN; + // doc["ValveServo_PIN"] = ValveServo_PIN; + // doc["TwistServo_PIN"] = TwistServo_PIN; + // doc["Squeeze_PIN"] = Squeeze_PIN; + // doc["Vibe0_PIN"] = Vibe0_PIN; + // doc["Vibe1_PIN"] = Vibe1_PIN; + // doc["Vibe2_PIN"] = Vibe2_PIN; + // doc["Vibe3_PIN"] = Vibe3_PIN; + // doc["Case_Fan_PIN"] = Case_Fan_PIN; + // doc["LubeButton_PIN"] = LubeButton_PIN; + // doc["Internal_Temp_PIN"] = Internal_Temp_PIN; + + // doc["BLDC_UsePWM"] = BLDC_UsePWM; + // doc["BLDC_UseMT6701"] = BLDC_UseMT6701; + // doc["BLDC_UseHallSensor"] = BLDC_UseHallSensor; + // doc["BLDC_Pulley_Circumference"] = BLDC_Pulley_Circumference; + // doc["BLDC_Encoder_PIN"] = BLDC_Encoder_PIN; + // doc["BLDC_ChipSelect_PIN"] = BLDC_ChipSelect_PIN; + // doc["BLDC_Enable_PIN"] = BLDC_Enable_PIN; + // doc["BLDC_HallEffect_PIN"] = BLDC_HallEffect_PIN; + // doc["BLDC_PWMchannel1_PIN"] = BLDC_PWMchannel1_PIN; + // doc["BLDC_PWMchannel2_PIN"] = BLDC_PWMchannel2_PIN; + // doc["BLDC_PWMchannel3_PIN"] = BLDC_PWMchannel3_PIN; + // doc["BLDC_MotorA_Voltage"] = round2(BLDC_MotorA_Voltage); + // doc["BLDC_MotorA_Current"] = round2(BLDC_MotorA_Current); + // doc["BLDC_MotorA_ParametersKnown"] = BLDC_MotorA_ParametersKnown; + // doc["BLDC_MotorA_ZeroElecAngle"] = round2(BLDC_MotorA_ZeroElecAngle); + // doc["BLDC_RailLength"] = BLDC_RailLength; + // doc["BLDC_StrokeLength"] = BLDC_StrokeLength; + + // LogHandler::debug(Tags::Settings, "save %s max: %f", "BLDC_MotorA_Voltage", doc["BLDC_MotorA_Current"].as()); + + // doc["staticIP"] = staticIP; + // doc["localIP"] = localIP; + // doc["gateway"] = gateway; + // doc["subnet"] = subnet; + // doc["dns1"] = dns1; + // doc["dns2"] = dns2; + + // doc["sr6Mode"] = sr6Mode; + // doc["RightServo_ZERO"] = RightServo_ZERO; + // doc["LeftServo_ZERO"] = LeftServo_ZERO; + // doc["RightUpperServo_ZERO"] = RightUpperServo_ZERO; + // doc["LeftUpperServo_ZERO"] = LeftUpperServo_ZERO; + // doc["PitchLeftServo_ZERO"] = PitchLeftServo_ZERO; + // doc["PitchRightServo_ZERO"] = PitchRightServo_ZERO; + // doc["TwistServo_ZERO"] = TwistServo_ZERO; + // doc["ValveServo_ZERO"] = ValveServo_ZERO; + // doc["Squeeze_ZERO"] = SqueezeServo_ZERO; + // doc["autoValve"] = autoValve; + // doc["inverseValve"] = inverseValve; + // doc["valveServo90Degrees"] = valveServo90Degrees; + // doc["inverseStroke"] = inverseStroke; + // doc["inversePitch"] = inversePitch; + // doc["lubeAmount"] = lubeAmount; + // doc["lubeEnabled"] = lubeEnabled; + // doc["displayEnabled"] = displayEnabled; + // doc["sleeveTempDisplayed"] = sleeveTempDisplayed; + // doc["versionDisplayed"] = versionDisplayed; + // doc["internalTempDisplayed"] = internalTempDisplayed; + // doc["tempSleeveEnabled"] = tempSleeveEnabled; + // doc["Display_Screen_Width"] = Display_Screen_Width; + // doc["Display_Screen_Height"] = Display_Screen_Height; + // doc["TargetTemp"] = TargetTemp; + // doc["HeatPWM"] = HeatPWM; + // doc["HoldPWM"] = HoldPWM; + // std::stringstream Display_I2C_Address_String; + // Display_I2C_Address_String << "0x" << std::hex << Display_I2C_Address; + // doc["Display_I2C_Address"] = Display_I2C_Address_String.str(); + // doc["Display_Rst_PIN"] = Display_Rst_PIN; + // doc["Temp_PIN"] = Sleeve_Temp_PIN; + // doc["Heater_PIN"] = Heater_PIN; + // // doc["heaterFailsafeTime"] = String(heaterFailsafeTime); + // doc["heaterThreshold"] = heaterThreshold; + // doc["heaterResolution"] = heaterResolution; + // doc["heaterFrequency"] = heaterFrequency; + // doc["fanControlEnabled"] = fanControlEnabled; + // doc["caseFanFrequency"] = caseFanFrequency; + // doc["caseFanResolution"] = caseFanResolution; + // doc["internalTempForFan"] = internalTempForFan; + // doc["internalMaxTemp"] = internalMaxTemp; + // doc["tempInternalEnabled"] = tempInternalEnabled; + + // doc["batteryLevelEnabled"] = batteryLevelEnabled; + // //doc["Battery_Voltage_PIN"] = Battery_Voltage_PIN; + // doc["batteryLevelNumeric"] = batteryLevelNumeric; + // doc["batteryVoltageMax"] = round2(batteryVoltageMax); + // doc["batteryCapacityMax"] = batteryCapacityMax; + + // doc["voiceEnabled"] = voiceEnabled; + // doc["voiceMuted"] = voiceMuted; + // doc["voiceWakeTime"] = voiceWakeTime; + // doc["voiceVolume"] = voiceVolume; + + // JsonArray includes = doc.createNestedArray("log-include-tags"); + // std::vector includesVec = LogHandler::getIncludes(); + // for (int i = 0; i < includesVec.size(); i++) + // { + // includes.add(includesVec[i]); + // } + + // JsonArray excludes = doc.createNestedArray("log-exclude-tags"); + // std::vector excludesVec = LogHandler::getExcludes(); + // for (int i = 0; i < excludesVec.size(); i++) + // { + // excludes.add(excludesVec[i]); + // } + + // LogHandler::debug(Tags::Settings, "isNull: %u", doc.isNull()); + // if (doc.isNull()) + // { + // LogHandler::error(Tags::Settings, "document is null!"); + // // file.close(); + // return false; + // } + + // LogHandler::debug(Tags::Settings, "Memory usage: %u bytes", doc.memoryUsage()); + // if (doc.memoryUsage() == 0) + // { + // LogHandler::error(Tags::Settings, "document is empty!"); + // // file.close(); + // return false; + // } + + // LogHandler::debug(Tags::Settings, "Is overflowed: %u", doc.overflowed()); + // if (doc.overflowed()) + // { + // LogHandler::error(Tags::Settings, "document is overflowed! Increase serialize size: %u", doc.memoryUsage()); + // // file.close(); + // return false; + // } + + // return true; + // } + + /// @brief Locks the mutex checks for an existing file and creates it if it doesnt exist. Calls the callback function and gives the mutex. + /// @param filepath + /// @param mutableLoadDefault + /// @param mutex + /// @param jsonSize + /// @param loadFunction + /// @param json + /// @return + static bool loadSettingsJson(const char *filepath, bool loadDefault, SemaphoreHandle_t &mutex, std::function loadFunction, std::function saveFunction, JsonObject json = JsonObject()) + { + JsonDocument doc; // jsonSize + bool mutableLoadDefault = loadDefault; + if (mutableLoadDefault || json.isNull()) + { + xSemaphoreTake(mutex, portMAX_DELAY); + if (!checkForFileAndLoad(filepath, doc, mutableLoadDefault)) + { + xSemaphoreGive(mutex); + return false; + } + json = doc.as(); + } + if (!loadFunction(json, mutableLoadDefault)) + { + xSemaphoreGive(mutex); + return false; + } + xSemaphoreGive(mutex); + if (mutableLoadDefault) + saveFunction(JsonObject()); + return true; + } + + /// @brief Locks the mutex and validates the file exists. calls the calback and serializes the data in a file to disk. Releases the mutex. + /// @param filepath + /// @param mutex + /// @param jsonSize + /// @param saveFunction + /// @param json + /// @return + static bool saveSettingsJson(const char *filepath, SemaphoreHandle_t &mutex, int jsonSize, std::function saveFunction, std::function loadFunction, JsonObject json = JsonObject()) + { + saving = true; + xSemaphoreTake(mutex, portMAX_DELAY); + LogHandler::debug(Tags::Settings, "Saving File: %s", filepath); + bool loadBeforeSetting = false; + if (!LittleFS.exists(filepath)) + { + LogHandler::error(Tags::Settings, "File did not exist whan saving: %s", filepath); + xSemaphoreGive(mutex); + saving = false; + return false; + } + else + { + if (!json.isNull()) + { + LogHandler::debug(Tags::Settings, "Loading from input json: %s", filepath); + xSemaphoreGive(mutex); + if (!loadFunction(false, json)) + { + LogHandler::error(Tags::Settings, "File loading input json failed: %s", filepath); + return false; + } + xSemaphoreTake(mutex, portMAX_DELAY); + } + LogHandler::debug(Tags::Settings, "jsonSize: %ld", jsonSize); + JsonDocument doc; // jsonSize + if (!saveFunction(doc)) + { + LogHandler::error(Tags::Settings, "Failed to compile JSON object: %s", filepath); + xSemaphoreGive(mutex); + saving = false; + return false; + } + LogHandler::debug(Tags::Settings, "Doc overflowed: %u", doc.overflowed()); + // LogHandler::debug(Tags::Settings, "Doc memory: %u", doc.memoryUsage()); + // LogHandler::debug(Tags::Settings, "Doc capacity: %u", doc.capacity()); + File file = LittleFS.open(filepath, FILE_WRITE); + if (serializeJson(doc, file) == 0) + { + LogHandler::error(Tags::Settings, "Failed to write to file: %s", filepath); + file.close(); + xSemaphoreGive(mutex); + saving = false; + return false; + } + LogHandler::debug(Tags::Settings, "File contents: %s", file.readString().c_str()); + file.close(); + printFree(); + } + saving = false; + xSemaphoreGive(mutex); + return true; + } + + static bool checkForFileAndLoad(const char *path, JsonDocument &doc, bool &loadDefault) + { + if (!LittleFS.exists(path)) + { + loadDefault = true; + } + if (loadDefault) + { + defaultJsonFile(path); + } + return loadJsonFromFile(path, doc); + } + + static bool defaultJsonFile(const char *path) + { + LogHandler::debug(Tags::Settings, "Defaulting file %s", path); + if (LittleFS.exists(path)) + { + LogHandler::debug(Tags::Settings, "Deleting file %s", path); + if (!LittleFS.remove(path)) + { + LogHandler::error(Tags::Settings, "Error deleting %s!", path); + return false; + } + } + LogHandler::debug(Tags::Settings, "Creating file %s", path); + File newFile = LittleFS.open(path, FILE_WRITE, true); + if (!newFile) + { + LogHandler::error(Tags::Settings, "Error creating %s!", path); + return false; + } + newFile.print("{}"); + newFile.flush(); + newFile.close(); + return true; + } + + static bool loadJsonFromFile(const char *path, JsonDocument &doc) + { + LogHandler::debug(Tags::Settings, "Loading json file %s", path); + if (!LittleFS.exists(path)) + { + LogHandler::error(Tags::Settings, "%s did not exist!", path); + return false; + } + + File file = LittleFS.open(path, FILE_READ); + if (!file) + { + LogHandler::error(Tags::Settings, "%s failed to open!", path); + return false; + } + if (LogDeserializationError(deserializeJson(doc, file), file.name())) + { + file.close(); + return false; + } + file.close(); + return true; + } + + static bool LogDeserializationError(DeserializationError error, const char *filename) + { + if (error) + { + LogHandler::error(Tags::Settings, "Error deserializing json: %s", filename); + switch (error.code()) + { + case DeserializationError::Code::Ok: + LogHandler::error(Tags::Settings, "Code: Ok"); + break; + case DeserializationError::Code::EmptyInput: + LogHandler::error(Tags::Settings, "Code: EmptyInput"); + break; + case DeserializationError::Code::IncompleteInput: + LogHandler::error(Tags::Settings, "Code: IncompleteInput"); + break; + case DeserializationError::Code::InvalidInput: + LogHandler::error(Tags::Settings, "Code: InvalidInput"); + break; + case DeserializationError::Code::NoMemory: + LogHandler::error(Tags::Settings, "Code: NoMemory"); + break; + case DeserializationError::Code::TooDeep: + LogHandler::error(Tags::Settings, "Code: TooDeep"); + break; + } + return true; + } + return false; + } + // /** If the parameter json is ommited or the pin value doesnt exist on the object then the pins are set to default. */ + // static void setBoardPinout(JsonObject json = JsonObject()) { + + // #if MOTOR_TYPE == 0 + // if(isBoardType(BoardType::ISAAC)) { + // // RightServo_PIN = 2; + // // LeftServo_PIN = 13; + // // PitchLeftServo_PIN = 14; + // // ValveServo_PIN = 5; + // // TwistServo_PIN = 27; + // // TwistFeedBack_PIN = 33; + // // Vibe0_PIN = 15; + // // Vibe1_PIN = 16; + // // LubeButton_PIN = 36; + // // RightUpperServo_PIN = 4; + // // LeftUpperServo_PIN = 12; + // // PitchRightServo_PIN = 17; + // // Sleeve_Temp_PIN = 25; + // // Heater_PIN = 19; + // // Squeeze_PIN = 26; + // TwistFeedBack_PIN = json["TwistFeedBack_PIN"] | 32; + // RightServo_PIN = json["RightServo_PIN"] | 4; + // LeftServo_PIN = json["LeftServo_PIN"] | 13; + // RightUpperServo_PIN = json["RightUpperServo_PIN"] | 16; + // LeftUpperServo_PIN = json["LeftUpperServo_PIN"] | 27; + // PitchLeftServo_PIN = json["PitchLeftServo_PIN"] | 26; + // PitchRightServo_PIN = json["PitchRightServo_PIN"] | 17; + // ValveServo_PIN = json["ValveServo_PIN"] | 18; + // TwistServo_PIN = json["TwistServo_PIN"] | 25; + // // Common motor + // Squeeze_PIN = json["Squeeze_PIN"] | 19; + // LubeButton_PIN = json["LubeButton_PIN"] | 34; + // // Internal_Temp_PIN = json["Internal_Temp_PIN"] | 34; + // Sleeve_Temp_PIN = json["Temp_PIN"] | 33; + // // Case_Fan_PIN = json["Case_Fan_PIN"] | 16; + // Vibe0_PIN = json["Vibe0_PIN"] | 15; + // Vibe1_PIN = json["Vibe1_PIN"] | 2; + // // Vibe2_PIN = json["Vibe2_PIN"] | 23; + // // Vibe3_PIN = json["Vibe3_PIN"] | 32; + // Heater_PIN = json["Heater_PIN"] | 5; + // } else if(isBoardType(BoardType::CRIMZZON)) { + + // Vibe3_PIN = json["Vibe3_PIN"] | 26; + // Internal_Temp_PIN = json["Internal_Temp_PIN"] | 32; + + // // EXT + // // EXT_Input2_PIN = 34; + // // EXT_Input3_PIN = 39; + // // EXT_Input4_PIN = 36; + + // heaterResolution = json["heaterResolution"] | 8; + // caseFanResolution = json["caseFanResolution"] | 10; + // caseFanFrequency = json["caseFanFrequency"] | 25; + // Display_Screen_Height = json["Display_Screen_Height"] | 32; + // TwistFeedBack_PIN = json["TwistFeedBack_PIN"] | 0; + // } + // if(!isBoardType(BoardType::ISAAC)) { // Devkit v1 pins + // if(!isBoardType(BoardType::CRIMZZON)) { + // TwistFeedBack_PIN = json["TwistFeedBack_PIN"] | 26; + // } + // // Common motor + // Squeeze_PIN = json["Squeeze_PIN"] | 17; + // LubeButton_PIN = json["LubeButton_PIN"] | 35; + // if(!isBoardType(BoardType::CRIMZZON)) { + // Internal_Temp_PIN = json["Internal_Temp_PIN"] | 34; + // } + // Case_Fan_PIN = json["Case_Fan_PIN"] | 16; + + // //Stock servo motors + // RightServo_PIN = json["RightServo_PIN"] | 13; + // LeftServo_PIN = json["LeftServo_PIN"] | 15; + // RightUpperServo_PIN = json["RightUpperServo_PIN"] | 12; + // LeftUpperServo_PIN = json["LeftUpperServo_PIN"] | 2; + // PitchLeftServo_PIN = json["PitchLeftServo_PIN"] | 4; + // PitchRightServo_PIN = json["PitchRightServo_PIN"] | 14; + + // Heater_PIN = json["Heater_PIN"] | 33; + // ValveServo_PIN = json["ValveServo_PIN"] | 25; + // TwistServo_PIN = json["TwistServo_PIN"] | 27; + // Sleeve_Temp_PIN = json["Temp_PIN"] | 5; + // Vibe0_PIN = json["Vibe0_PIN"] | 18; + // Vibe1_PIN = json["Vibe1_PIN"] | 19; + // Vibe2_PIN = json["Vibe2_PIN"] | 23; + // if(!isBoardType(BoardType::CRIMZZON)) { + // Vibe3_PIN = json["Vibe3_PIN"] | 32; + // } + // } + // #elif MOTOR_TYPE == 1 + // // BLDC motor + // BLDC_Encoder_PIN = json["BLDC_Encoder_PIN"] | 33; + // BLDC_ChipSelect_PIN = json["BLDC_ChipSelect_PIN"] | 5; + // BLDC_Enable_PIN = json["BLDC_Enable_PIN"] | 14; + // BLDC_PWMchannel1_PIN = json["BLDC_PWMchannel1_PIN"] | 27; + // BLDC_PWMchannel2_PIN = json["BLDC_PWMchannel2_PIN"] | 26; + // BLDC_PWMchannel3_PIN = json["BLDC_PWMchannel3_PIN"] | 25; + + // // PWM + // Heater_PIN = json["Heater_PIN"] | 15; + // Sleeve_Temp_PIN = json["Temp_PIN"] | 36; + // Vibe0_PIN = json["Vibe0_PIN"] | 2; + // Vibe1_PIN = json["Vibe1_PIN"] | 4; + // Vibe2_PIN = json["Vibe2_PIN"] | -1; + // Vibe3_PIN = json["Vibe3_PIN"] | -1; + // Case_Fan_PIN = json["Case_Fan_PIN"] | 16; + + // // PWM servo + // ValveServo_PIN = json["ValveServo_PIN"] | 12; + // TwistServo_PIN = json["TwistServo_PIN"] | 13; + // Squeeze_PIN = json["Squeeze_PIN"] | 17; + + // // Input + // TwistFeedBack_PIN = json["TwistFeedBack_PIN"] | 26; + // LubeButton_PIN = json["LubeButton_PIN"] | 35; + // Internal_Temp_PIN = json["Internal_Temp_PIN"] | 34; + // BLDC_HallEffect_PIN = json["BLDC_HallEffect_PIN"] | 35; + // #endif + // } + + static void setBuildFeatures() + { + int index = 0; +#if WIFI_TCODE + LOG_BUILD_FEATURE(WIFI_TCODE); + buildFeatures[index] = BuildFeature::WIFI; + index++; +#endif +#if BLUETOOTH_TCODE + LOG_BUILD_FEATURE(BLUETOOTH_TCODE); + buildFeatures[index] = BuildFeature::BLUETOOTH; + index++; +#endif +#if BLE_TCODE + LOG_BUILD_FEATURE(BLE_TCODE); + buildFeatures[index] = BuildFeature::BLE; + index++; +#endif +#if DEBUG_BUILD + LOG_BUILD_FEATURE(DEBUG_BUILD); + buildFeatures[index] = BuildFeature::DEBUG; + index++; +#endif +#ifdef ESP32_DA + LOG_BUILD_FEATURE(ESP32_DA); + buildFeatures[index] = BuildFeature::DA; + index++; +#endif +#if BUILD_TEMP + LOG_BUILD_FEATURE(BUILD_TEMP); + buildFeatures[index] = BuildFeature::TEMP; + index++; +#endif +#if BUILD_DISPLAY + LOG_BUILD_FEATURE(BUILD_DISPLAY); + buildFeatures[index] = BuildFeature::DISPLAY_; + index++; +#endif +// #if TCODE_V2 +// LogHandler::debug("setBuildFeatures", "TCODE_V2"); +// buildFeatures[index] = BuildFeature::HAS_TCODE_V2; +// index++; +// #endif +#if SECURE_WEB + LOG_BUILD_FEATURE(HTTPS); + buildFeatures[index] = BuildFeature::HTTPS; + index++; +#endif +#if COEXIST + LOG_BUILD_FEATURE(COEXIST); + buildFeatures[index] = BuildFeature::COEXIST_FEATURE; + index++; +#endif + buildFeatures[(int)BuildFeature::MAX_FEATURES - 1] = {}; + } + + static void setMotorType() + { +#if MOTOR_TYPE == 0 + m_settingsFactory->setValue(MOTOR_TYPE_SETTING, (int)MotorType::Servo); +#elif MOTOR_TYPE == 1 + m_settingsFactory->setValue(MOTOR_TYPE_SETTING, (int)MotorType::BLDC); +#endif + } + + static void sendMessage(const SettingProfile &profile, const char *message) + { + if (message_callback) + { + LogHandler::debug(Tags::Settings, "sendMessage: message_callback %s", message); + message_callback(profile, message); + } + else + { + LogHandler::debug(Tags::Settings, "sendMessage: message_callback null"); + } + } + + static void setValue(JsonObject json, bool &variable, const SettingProfile &profile, const char *propertyName, bool defaultValue) + { + bool newValue = json[propertyName] | defaultValue; + setValue(newValue, variable, profile, propertyName); + } + + template + static void setValue(JsonObject json, char (&variable)[n], const SettingProfile &profile, const char *propertyName, const char *defaultValue) + { + const char *newValue = json[propertyName] | defaultValue; + setValue(newValue, variable, profile, propertyName); + } + + static void setValue(JsonObject json, int &variable, const SettingProfile &profile, const char *propertyName, int defaultValue) + { + int newValue = json[propertyName] | defaultValue; + setValue(newValue, variable, profile, propertyName); + } + static void setValue(JsonObject json, uint8_t &variable, const SettingProfile &profile, const char *propertyName, uint8_t defaultValue) + { + uint8_t newValue = json[propertyName] | defaultValue; + setValue(newValue, variable, profile, propertyName); + } + static void setValue(JsonObject json, uint16_t &variable, const SettingProfile &profile, const char *propertyName, uint16_t defaultValue) + { + uint16_t newValue = json[propertyName] | defaultValue; + setValue(newValue, variable, profile, propertyName); + } + + static void setValue(JsonObject json, float &variable, const SettingProfile &profile, const char *propertyName, float defaultValue) + { + float newValue = json[propertyName] | defaultValue; + setValue(newValue, variable, profile, propertyName); + } + + static void setValue(JsonObject json, std::vector &variable, const SettingProfile &profile, const char *propertyName) + { + variable.clear(); + if (json[propertyName].isNull()) + { + return; + } + JsonArray jsonArray = json[propertyName].as(); + for (int i = 0; i < jsonArray.size(); i++) + { + variable.push_back(jsonArray[i]); + } + if (initialized) + sendMessage(profile, propertyName); + } + + static void setValue(JsonObject json, std::vector &variable, const SettingProfile &profile, const char *propertyName) + { + variable.clear(); + if (json[propertyName].isNull()) + { + return; + } + JsonArray jsonArray = json[propertyName].as(); + for (int i = 0; i < jsonArray.size(); i++) + { + variable.push_back(jsonArray[i]); + } + if (initialized) + sendMessage(profile, propertyName); + } + + static void setValue(bool newValue, bool &variable, const SettingProfile &profile, const char *propertyName) + { + bool valueChanged = initialized && variable != newValue; + LogHandler::debug(Tags::Settings, "Set bool '%s' oldValue '%ld' newValue '%ld' changed: '%ld'", propertyName, variable, newValue, valueChanged); + variable = newValue; + if (valueChanged) + sendMessage(profile, propertyName); + } + + template + static void setValue(const char *newValue, char (&variable)[n], const SettingProfile &profile, const char *propertyName) + { + bool valueChanged = initialized && strcmp(variable, newValue) != -1; + LogHandler::debug(Tags::Settings, "Set char* '%s' oldValue '%s' newValue '%s' changed: '%ld'", propertyName, variable, newValue, valueChanged); + strcpy(variable, newValue); + if (valueChanged) + sendMessage(profile, propertyName); + } + + static void setValue(int newValue, int &variable, const SettingProfile &profile, const char *propertyName) + { + bool valueChanged = initialized && variable != newValue; + LogHandler::debug(Tags::Settings, "Set int '%s' oldValue '%ld' newValue '%ld' changed: '%ld'", propertyName, variable, newValue, valueChanged); + variable = newValue; + if (valueChanged) + sendMessage(profile, propertyName); + } + + static void setValue(uint8_t newValue, uint8_t &variable, const SettingProfile &profile, const char *propertyName) + { + bool valueChanged = initialized && variable != newValue; + LogHandler::debug(Tags::Settings, "Set int '%s' oldValue '%u' newValue '%u' changed: '%ld'", propertyName, variable, newValue, valueChanged); + variable = newValue; + if (valueChanged) + sendMessage(profile, propertyName); + } + + static void setValue(uint16_t newValue, uint16_t &variable, const SettingProfile &profile, const char *propertyName) + { + bool valueChanged = initialized && variable != newValue; + LogHandler::debug(Tags::Settings, "Set int '%s' oldValue '%u' newValue '%u' changed: '%ld'", propertyName, variable, newValue, valueChanged); + variable = newValue; + if (valueChanged) + sendMessage(profile, propertyName); + } + + static void setValue(float newValue, float &variable, const SettingProfile &profile, const char *propertyName) + { + bool valueChanged = initialized && variable != newValue; + LogHandler::debug(Tags::Settings, "Set float '%s' oldValue '%f' newValue '%f' changed: '%ld'", propertyName, variable, newValue, valueChanged); + variable = newValue; + if (valueChanged) + sendMessage(profile, propertyName); + } + + static u_int16_t calculateRange(const char *channel, int value) + { + return constrain(value, getChannelMin(channel), getChannelMax(channel)); + } + + // Function that gets current epoch time + static unsigned long getTime() + { + time_t now; + struct tm timeinfo; + if (!getLocalTime(&timeinfo)) + { + // Serial.println("Failed to obtain time"); + return (0); + } + time(&now); + return now; + } + + static const char *machine_reset_cause() + { + switch (esp_reset_reason()) + { + case ESP_RST_POWERON: + return "Reset due to power-on event"; + break; + case ESP_RST_BROWNOUT: + return "Brownout reset (software or hardware)"; + break; + case ESP_RST_INT_WDT: + return "Reset (software or hardware) due to interrupt watchdog"; + break; + case ESP_RST_TASK_WDT: + return "Reset due to task watchdog"; + break; + case ESP_RST_WDT: + return "Reset due to other watchdogs"; + break; + case ESP_RST_DEEPSLEEP: + return "Reset after exiting deep sleep mode"; + break; + case ESP_RST_SW: + return "Software reset via esp_restart"; + break; + case ESP_RST_PANIC: + return "Software reset due to exception/panic"; + break; + case ESP_RST_EXT: // Comment in ESP-IDF: "For ESP32, ESP_RST_EXT is never returned" + return "Reset by external pin (not applicable for ESP32)"; + break; + case ESP_RST_SDIO: + return "Reset over SDIO"; + break; + case ESP_RST_UNKNOWN: + return "Reset reason can not be determined"; + break; + default: + return ""; + break; + } + } + + // static bool LogDeserializationError(DeserializationError error, const char* filename) { + // if (error) + // { + // LogHandler::error(Tags::Settings, "Error deserializing json: %s", filename); + // switch (error.code()) + // { + // case DeserializationError::Code::Ok: + // LogHandler::error(Tags::Settings, "Code: Ok"); + // break; + // case DeserializationError::Code::EmptyInput: + // LogHandler::error(Tags::Settings, "Code: EmptyInput"); + // break; + // case DeserializationError::Code::IncompleteInput: + // LogHandler::error(Tags::Settings, "Code: IncompleteInput"); + // break; + // case DeserializationError::Code::InvalidInput: + // LogHandler::error(Tags::Settings, "Code: InvalidInput"); + // break; + // case DeserializationError::Code::NoMemory: + // LogHandler::error(Tags::Settings, "Code: NoMemory"); + // break; + // case DeserializationError::Code::TooDeep: + // LogHandler::error(Tags::Settings, "Code: TooDeep"); + // break; + // } + // return true; + // } + // return false; + // } + + // static void LogSaveDebug(const DynamicJsonDocument doc) + // { + // Commented out due to error in logger on hostname. + // LogHandler::debug(Tags::Settings, "save TCodeVersionEnum: %i", doc["TCodeVersion"].as()); + // LogHandler::debug(Tags::Settings, "save ssid: %s", doc["ssid"].as()); + // // LogHandler::debug(Tags::Settings, "save pass: %s", doc["wifiPass"].as()); + // LogHandler::debug(Tags::Settings, "save udpServerPort: %i", doc["udpServerPort"].as()); + // LogHandler::debug(Tags::Settings, "save webServerPort: %i", doc["webServerPort"].as()); + // LogHandler::debug(Tags::Settings, "save hostname: %s", doc["hostname"].as()); + // LogHandler::debug(Tags::Settings, "save friendlyName: %s", doc["friendlyName"].as()); + // LogHandler::debug(Tags::Settings, "save pitchFrequencyIsDifferent ", doc["pitchFrequencyIsDifferent"].as()); + // LogHandler::debug(Tags::Settings, "save msPerRad: %i", doc["msPerRad"].as()); + // LogHandler::debug(Tags::Settings, "save servoFrequency: %i", doc["servoFrequency"].as()); + // LogHandler::debug(Tags::Settings, "save pitchFrequency: %i", doc["pitchFrequency"].as()); + // LogHandler::debug(Tags::Settings, "save valveFrequency: %i", doc["valveFrequency"].as()); + // LogHandler::debug(Tags::Settings, "save twistFrequency: %i", doc["twistFrequency"].as()); + // LogHandler::debug(Tags::Settings, "save continuousTwist: %i", doc["continuousTwist"].as()); + // LogHandler::debug(Tags::Settings, "save feedbackTwist: %i", doc["feedbackTwist"].as()); + // LogHandler::debug(Tags::Settings, "save analogTwist: %i", doc["analogTwist"].as()); + // LogHandler::debug(Tags::Settings, "save TwistFeedBack_PIN: %i", doc["TwistFeedBack_PIN"].as()); + // LogHandler::debug(Tags::Settings, "save RightServo_PIN: %i", doc["RightServo_PIN"].as()); + // LogHandler::debug(Tags::Settings, "save LeftServo_PIN: %i", doc["LeftServo_PIN"].as()); + // LogHandler::debug(Tags::Settings, "save RightUpperServo_PIN: %i", doc["RightUpperServo_PIN"].as()); + // LogHandler::debug(Tags::Settings, "save LeftUpperServo_PIN: %i", doc["LeftUpperServo_PIN"].as()); + // LogHandler::debug(Tags::Settings, "save PitchLeftServo_PIN: %i", doc["PitchLeftServo_PIN"].as()); + // LogHandler::debug(Tags::Settings, "save PitchRightServo_PIN: %i", doc["PitchRightServo_PIN"].as()); + // LogHandler::debug(Tags::Settings, "save ValveServo_PIN: %i", doc["ValveServo_PIN"].as()); + // LogHandler::debug(Tags::Settings, "save TwistServo_PIN: %i", doc["TwistServo_PIN"].as()); + // LogHandler::debug(Tags::Settings, "save Vibe0_PIN: %i", doc["Vibe0_PIN"].as()); + // LogHandler::debug(Tags::Settings, "save Vibe1_PIN: %i", doc["Vibe1_PIN"].as()); + // LogHandler::debug(Tags::Settings, "save Lube_Pin: %i", doc["Lube_Pin"].as()); + // LogHandler::debug(Tags::Settings, "save LubeButton_PIN: %i", doc["LubeButton_PIN"].as()); + // LogHandler::debug(Tags::Settings, "save staticIP: %i", doc["staticIP"].as()); + // LogHandler::debug(Tags::Settings, "save localIP: %s", doc["localIP"].as()); + // LogHandler::debug(Tags::Settings, "save gateway: %s", doc["gateway"].as()); + // LogHandler::debug(Tags::Settings, "save subnet: %s", doc["subnet"].as()); + // LogHandler::debug(Tags::Settings, "save dns1: %s", doc["dns1"].as()); + // LogHandler::debug(Tags::Settings, "save dns2: %s", doc["dns2"].as()); + // LogHandler::debug(Tags::Settings, "save sr6Mode: %i", doc["sr6Mode"].as()); + // LogHandler::debug(Tags::Settings, "save RightServo_ZERO: %i", doc["RightServo_ZERO"].as()); + // LogHandler::debug(Tags::Settings, "save LeftServo_ZERO: %i", doc["LeftServo_ZERO"].as()); + // LogHandler::debug(Tags::Settings, "save RightUpperServo_ZERO: %i", doc["RightUpperServo_ZERO"].as()); + // LogHandler::debug(Tags::Settings, "save LeftUpperServo_ZERO: %i", doc["LeftUpperServo_ZERO"].as()); + // LogHandler::debug(Tags::Settings, "save PitchLeftServo_ZERO: %i", doc["PitchLeftServo_ZERO"].as()); + // LogHandler::debug(Tags::Settings, "save PitchRightServo_ZERO: %i", doc["PitchRightServo_ZERO"].as()); + // LogHandler::debug(Tags::Settings, "save TwistServo_ZERO: %i", doc["TwistServo_ZERO"].as()); + // LogHandler::debug(Tags::Settings, "save ValveServo_ZERO: %i", doc["ValveServo_ZERO"].as()); + // LogHandler::debug(Tags::Settings, "save autoValve: %i", doc["autoValve"].as()); + // LogHandler::debug(Tags::Settings, "save inverseValve: %i", doc["inverseValve"].as()); + // LogHandler::debug(Tags::Settings, "save valveServo90Degrees: %i", doc["valveServo90Degrees"].as()); + // LogHandler::debug(Tags::Settings, "save inverseStroke: %i", doc["inverseStroke"].as()); + // LogHandler::debug(Tags::Settings, "save inversePitch: %i", doc["inversePitch"].as()); + // LogHandler::debug(Tags::Settings, "save lubeEnabled: %i", doc["lubeEnabled"].as()); + // LogHandler::debug(Tags::Settings, "save lubeAmount: %i", doc["lubeAmount"].as()); + // LogHandler::debug(Tags::Settings, "save Temp_PIN: %i", doc["Temp_PIN"].as()); + // LogHandler::debug(Tags::Settings, "save Heater_PIN: %i", doc["Heater_PIN"].as()); + // LogHandler::debug(Tags::Settings, "save displayEnabled: %i", doc["displayEnabled"].as()); + // LogHandler::debug(Tags::Settings, "save sleeveTempDisplayed: %i", doc["sleeveTempDisplayed"].as()); + // LogHandler::debug(Tags::Settings, "save internalTempDisplayed: %i", doc["internalTempDisplayed"].as()); + // LogHandler::debug(Tags::Settings, "save tempSleeveEnabled: %i", doc["tempSleeveEnabled"].as()); + // LogHandler::debug(Tags::Settings, "save tempInternalEnabled: %i", doc["tempInternalEnabled"].as()); + // LogHandler::debug(Tags::Settings, "save Display_Screen_Width: %i", doc["Display_Screen_Width"].as()); + // LogHandler::debug(Tags::Settings, "save internalMaxTemp: %f", doc["internalMaxTemp"].as()); + // LogHandler::debug(Tags::Settings, "save internalTempForFan: %f", doc["internalTempForFan"].as()); + // LogHandler::debug(Tags::Settings, "save Display_Screen_Height: %i", doc["Display_Screen_Height"].as()); + // LogHandler::debug(Tags::Settings, "save TargetTemp: %f", doc["TargetTemp"].as()); + // LogHandler::debug(Tags::Settings, "save HeatPWM: %i", doc["HeatPWM"].as()); + // LogHandler::debug(Tags::Settings, "save HoldPWM: %i", doc["HoldPWM"].as()); + // LogHandler::debug(Tags::Settings, "save Display_I2C_Address: %i", doc["Display_I2C_Address"].as()); + // LogHandler::debug(Tags::Settings, "save Display_Rst_PIN: %i", doc["Display_Rst_PIN"].as()); + // LogHandler::debug(Tags::Settings, "save heaterFailsafeTime: %ld", doc["heaterFailsafeTime"].as()); + // LogHandler::debug(Tags::Settings, "save heaterThreshold: %i", doc["heaterThreshold"].as()); + // LogHandler::debug(Tags::Settings, "save heaterResolution: %i", doc["heaterResolution"].as()); + // LogHandler::debug(Tags::Settings, "save heaterFrequency: %i", doc["heaterFrequency"].as()); + // LogHandler::debug(Tags::Settings, "save newtoungeHatExists: %i", doc["newtoungeHatExists"].as()); + // LogHandler::debug(Tags::Settings, "save logLevel: %i", doc["logLevel"].as()); + // LogHandler::debug(Tags::Settings, "save bluetoothEnabled: %i", doc["bluetoothEnabled"].as()); + // } + + // static void LogUpdateDebug() + // { + // LogHandler::debug(Tags::Settings, "update TCodeVersionEnum: %i", TCodeVersionEnum); + // LogHandler::debug(Tags::Settings, "update ssid: %s", ssid); + // //LogHandler::debug(Tags::Settings, "update wifiPass: %s", wifiPass); + // LogHandler::debug(Tags::Settings, "update udpServerPort: %i", udpServerPort); + // LogHandler::debug(Tags::Settings, "update webServerPort: %i", webServerPort); + // LogHandler::debug(Tags::Settings, "update hostname: %s", hostname); + // LogHandler::debug(Tags::Settings, "update friendlyName: %s", friendlyName); + // LogHandler::debug(Tags::Settings, "update pitchFrequencyIsDifferent: %i", pitchFrequencyIsDifferent); + // LogHandler::debug(Tags::Settings, "update msPerRad: %i", msPerRad); + // LogHandler::debug(Tags::Settings, "update servoFrequency: %i", servoFrequency); + // LogHandler::debug(Tags::Settings, "update pitchFrequency: %i", pitchFrequency); + // LogHandler::debug(Tags::Settings, "update valveFrequency: %i", valveFrequency); + // LogHandler::debug(Tags::Settings, "update twistFrequency: %i", twistFrequency); + // LogHandler::debug(Tags::Settings, "update continuousTwist: %i", continuousTwist); + // LogHandler::debug(Tags::Settings, "update feedbackTwist: %i", feedbackTwist);max + // LogHandler::debug(Tags::Settings, "update PitchRightServo_ZERO: %i", PitchRightServo_ZERO); + // LogHandler::debug(Tags::Settings, "update TwistServo_ZERO: %i", TwistServo_ZERO); + // LogHandler::debug(Tags::Settings, "update ValveServo_ZERO: %i", ValveServo_ZERO); + // LogHandler::debug(Tags::Settings, "update autoValve: %i", autoValve); + // LogHandler::debug(Tags::Settings, "update inverseValve: %i", inverseValve); + // LogHandler::debug(Tags::Settings, "update valveServo90Degrees: %i", valveServo90Degrees); + // LogHandler::debug(Tags::Settings, "update inverseStroke: %i", inverseStroke); + // LogHandler::debug(Tags::Settings, "update inversePitch: %i", inversePitch); + // LogHandler::debug(Tags::Settings, "update lubeEnabled: %i", lubeEnabled); + // LogHandler::debug(Tags::Settings, "update lubeAmount: %i", lubeAmount); + // LogHandler::debug(Tags::Settings, "update displayEnabled: %i", displayEnabled); + // LogHandler::debug(Tags::Settings, "update sleeveTempDisplayed: %i", sleeveTempDisplayed); + // LogHandler::debug(Tags::Settings, "update internalTempDisplayed: %i", internalTempDisplayed); + // LogHandler::debug(Tags::Settings, "update tempSleeveEnabled: %i", tempSleeveEnabled); + // LogHandler::debug(Tags::Settings, "update tempInternalEnabled: %i", tempInternalEnabled); + // LogHandler::debug(Tags::Settings, "update Display_Screen_Width: %i", Display_Screen_Width); + // LogHandler::debug(Tags::Settings, "update Display_Screen_Height: %i", Display_Screen_Height); + // LogHandler::debug(Tags::Settings, "update TargetTemp: %i", TargetTemp); + // LogHandler::debug(Tags::Settings, "update HeatPWM: %i", HeatPWM); + // LogHandler::debug(Tags::Settings, "update HoldPWM: %i", HoldPWM); + // LogHandler::debug(Tags::Settings, "update Display_I2C_Address: %i", Display_I2C_Address); + // LogHandler::debug(Tags::Settings, "update Display_Rst_PIN: %i", Display_Rst_PIN); + // LogHandler::debug(Tags::Settings, "update Sleeve_Temp_PIN: %i", Sleeve_Temp_PIN); + // LogHandler::debug(Tags::Settings, "update Heater_PIN: %i", Heater_PIN); + // LogHandler::debug(Tags::Settings, "update heaterThreshold: %d", heaterThreshold); + // LogHandler::debug(Tags::Settings, "update heaterResolution: %i", heaterResolution); + // LogHandler::debug(Tags::Settings, "update heaterFrequency: %i", heaterFrequency); + // LogHandler::debug(Tags::Settings, "update logLevel: %i", (int)logLevel); + // LogHandler::debug(Tags::Settings, "update bluetoothEnabled: %i", (int)bluetoothEnabled); + // } +}; + +SettingsFactory *SettingsHandler::m_settingsFactory; +SemaphoreHandle_t SettingsHandler::m_motionMutex = xSemaphoreCreateMutex(); +SemaphoreHandle_t SettingsHandler::m_channelsMutex = xSemaphoreCreateMutex(); +SemaphoreHandle_t SettingsHandler::m_wifiMutex = xSemaphoreCreateMutex(); +SemaphoreHandle_t SettingsHandler::m_buttonsMutex = xSemaphoreCreateMutex(); +SemaphoreHandle_t SettingsHandler::m_settingsMutex = xSemaphoreCreateMutex(); +bool SettingsHandler::initialized = false; +int SettingsHandler::restartRequired = -1; +bool SettingsHandler::saving = false; +bool SettingsHandler::motionPaused = false; +bool SettingsHandler::fullBuild = false; +bool SettingsHandler::apMode = false; + +// BoardType SettingsHandler::boardType = BoardType::DEVKIT; +BuildFeature SettingsHandler::buildFeatures[(int)BuildFeature::MAX_FEATURES]; +std::vector SettingsHandler::systemI2CAddresses; +SETTING_STATE_FUNCTION_PTR_T SettingsHandler::message_callback = 0; +ChannelMap SettingsHandler::channelMap; + +char SettingsHandler::currentIP[IP_ADDRESS_LEN] = LOCALIP_DEFAULT; +char SettingsHandler::currentGateway[IP_ADDRESS_LEN] = GATEWAY_DEFAULT; +char SettingsHandler::currentSubnet[IP_ADDRESS_LEN] = SUBNET_DEFAULT; +char SettingsHandler::currentDns1[IP_ADDRESS_LEN] = DNS1_DEFAULT; +char SettingsHandler::currentDns2[IP_ADDRESS_LEN] = DNS2_DEFAULT; + +// MotorType SettingsHandler::motorType = MotorType::Servo; +// const char SettingsHandler::HandShakeChannel[4] = "D1\n"; +// const char SettingsHandler::SettingsChannel[4] = "D2\n"; +// const char *SettingsHandler::userSettingsFilePath = "/userSettings.json"; +// const char *SettingsHandler::wifiPassFilePath = "/wifiInfo.json"; +// const char *SettingsHandler::buttonsFilePath = "/buttons.json"; +// const char *SettingsHandler::motionProfilesFilePath = "/motionProfiles.json"; +// const char *SettingsHandler::logPath = "/log.json"; +// const char *SettingsHandler::defaultWifiPass = "YOUR PASSWORD HERE"; +// const char *SettingsHandler::decoyPass = "Too bad haxor!"; +// const char SettingsHandler::defaultIP[15] = "192.168.69.1"; +// const char SettingsHandler::defaultGateWay[15] = "192.168.69.254"; +// const char SettingsHandler::defaultSubnet[15] = "255.255.255.0"; +// bool SettingsHandler::bluetoothEnabled = true; +// LogLevel SettingsHandler::logLevel = LogLevel::INFO; +// bool SettingsHandler::isTcp = true; +// char SettingsHandler::ssid[32]; +// char SettingsHandler::wifiPass[63]; +// char SettingsHandler::hostname[63]; +// char SettingsHandler::friendlyName[100]; +// int SettingsHandler::udpServerPort; +// int SettingsHandler::webServerPort; +// int SettingsHandler::PitchRightServo_PIN; +// int SettingsHandler::RightUpperServo_PIN; +// int SettingsHandler::RightServo_PIN; +// int SettingsHandler::PitchLeftServo_PIN; +// int SettingsHandler::LeftUpperServo_PIN; +// int SettingsHandler::LeftServo_PIN; +// int SettingsHandler::ValveServo_PIN; +// int SettingsHandler::TwistServo_PIN; +// int SettingsHandler::TwistFeedBack_PIN; +// int SettingsHandler::Vibe0_PIN; +// int SettingsHandler::Vibe1_PIN; +// int SettingsHandler::LubeButton_PIN; +// int SettingsHandler::Sleeve_Temp_PIN; +// int SettingsHandler::Heater_PIN; +// int SettingsHandler::I2C_SDA_PIN_obsolete = 21; +// int SettingsHandler::I2C_SCL_PIN_obsolete = 22; + +// int SettingsHandler::Internal_Temp_PIN; +// int SettingsHandler::Case_Fan_PIN; +// int SettingsHandler::Squeeze_PIN; +// int SettingsHandler::Vibe2_PIN; +// int SettingsHandler::Vibe3_PIN; + +// // int SettingsHandler::HeatLED_PIN = 32; +// // pin 25 cannot be servo. Throws error +// bool SettingsHandler::lubeEnabled = true; + +// bool SettingsHandler::pitchFrequencyIsDifferent; +// int SettingsHandler::msPerRad; +// int SettingsHandler::servoFrequency; +// int SettingsHandler::pitchFrequency; +// int SettingsHandler::valveFrequency; +// int SettingsHandler::twistFrequency; +// int SettingsHandler::squeezeFrequency; +// bool SettingsHandler::feedbackTwist = false; +// bool SettingsHandler::continuousTwist; +// bool SettingsHandler::analogTwist; +// bool SettingsHandler::staticIP; +// char SettingsHandler::localIP[15]; +// char SettingsHandler::gateway[15]; +// char SettingsHandler::subnet[15]; +// char SettingsHandler::dns1[15]; +// char SettingsHandler::dns2[15]; +// bool SettingsHandler::sr6Mode; +// int SettingsHandler::RightServo_ZERO = 1500; +// int SettingsHandler::LeftServo_ZERO = 1500; +// int SettingsHandler::RightUpperServo_ZERO = 1500; +// int SettingsHandler::LeftUpperServo_ZERO = 1500; +// int SettingsHandler::PitchLeftServo_ZERO = 1500; +// int SettingsHandler::PitchRightServo_ZERO = 1500; + +// bool SettingsHandler::BLDC_UsePWM = false; +// bool SettingsHandler::BLDC_UseMT6701 = true; +// bool SettingsHandler::BLDC_UseHallSensor = false; +// int SettingsHandler::BLDC_Pulley_Circumference = 60; +// int SettingsHandler::BLDC_Encoder_PIN = 33;// PWM feedback pin (if used) - P pad on AS5048a +// int SettingsHandler::BLDC_Enable_PIN = 14;// Motor enable - EN on SFOCMini +// int SettingsHandler::BLDC_HallEffect_PIN = 12; +// int SettingsHandler::BLDC_PWMchannel1_PIN = 27; +// int SettingsHandler::BLDC_PWMchannel2_PIN = 26; +// int SettingsHandler::BLDC_PWMchannel3_PIN = 25; +// float SettingsHandler::BLDC_MotorA_Voltage = 20.0; // BLDC Motor operating voltage (12-20V) +// float SettingsHandler::BLDC_MotorA_Current = 1.0; // BLDC Maximum operating current (Amps) +// int SettingsHandler::BLDC_ChipSelect_PIN = 5; // SPI chip select pin - CSn on AS5048a (By default on ESP32: MISO = D19, MOSI = D23, CLK = D18) +// bool SettingsHandler::BLDC_MotorA_ParametersKnown = false; // Once you know the zero elec angle for the motor enter it below and set this flag to true. +// float SettingsHandler::BLDC_MotorA_ZeroElecAngle = 0.00; // This number is the zero angle (in radians) for the motor relative to the encoder. +// int SettingsHandler::BLDC_RailLength; +// int SettingsHandler::BLDC_StrokeLength; + +// int SettingsHandler::TwistServo_ZERO = 1500; +// int SettingsHandler::ValveServo_ZERO = 1500; +// int SettingsHandler::SqueezeServo_ZERO = 1500; +// bool SettingsHandler::autoValve = false; +// bool SettingsHandler::inverseValve = false; +// bool SettingsHandler::valveServo90Degrees = false; +// bool SettingsHandler::inverseStroke = false; +// bool SettingsHandler::inversePitch = false; +// int SettingsHandler::lubeAmount = 255; + +// bool SettingsHandler::displayEnabled = false; +// bool SettingsHandler::sleeveTempDisplayed = false; +// bool SettingsHandler::internalTempDisplayed = false; +// bool SettingsHandler::versionDisplayed = true; +// bool SettingsHandler::tempSleeveEnabled = false; +// bool SettingsHandler::tempInternalEnabled = false; +// bool SettingsHandler::fanControlEnabled = false; +// bool SettingsHandler::batteryLevelEnabled = true; +// //int SettingsHandler::Battery_Voltage_PIN = 32; +// bool SettingsHandler::batteryLevelNumeric = false; +// double SettingsHandler::batteryVoltageMax = 12.6; +// int SettingsHandler::batteryCapacityMax; +// int SettingsHandler::Display_Screen_Width = 128; +// int SettingsHandler::Display_Screen_Height = 64; +// int SettingsHandler::caseFanMaxDuty = 255; +// double SettingsHandler::internalTempForFan = 20.0; +// double SettingsHandler::internalMaxTemp = 50.0; +// int SettingsHandler::TargetTemp = 40; +// int SettingsHandler::HeatPWM = 255; +// int SettingsHandler::HoldPWM = 110; +// int SettingsHandler::Display_I2C_Address = 0x3C; +// int SettingsHandler::Display_Rst_PIN = -1; +// // long SettingsHandler::heaterFailsafeTime = 60000; +// float SettingsHandler::heaterThreshold = 5.0; +// int SettingsHandler::heaterResolution = 8; +// int SettingsHandler::heaterFrequency = 5000; +// int SettingsHandler::caseFanFrequency = 25; +// int SettingsHandler::caseFanResolution = 10; +// const char *SettingsHandler::lastRebootReason; + +// bool SettingsHandler::voiceEnabled = false; +// bool SettingsHandler::voiceMuted = false; +// int SettingsHandler::voiceWakeTime = 10; +// int SettingsHandler::voiceVolume = 10; + +// bool SettingsHandler::bootButtonEnabled; +// bool SettingsHandler::buttonSetsEnabled; +// char SettingsHandler::bootButtonCommand[MAX_COMMAND]; + +bool SettingsHandler::motionEnabled = false; +// char SettingsHandler::motionSelectedProfileName[MAX_MOTION_PROFILE_NAME_LENGTH]; +int SettingsHandler::motionSelectedProfileIndex = 0; +int SettingsHandler::motionDefaultProfileIndex = 0; +// uint8_t SettingsHandler::defaultButtonSetPin; +// uint16_t SettingsHandler::buttonAnalogDebounce; +// std::map SettingsHandler::motionProfiles; +// int SettingsHandler::motionUpdateGlobal; +// int SettingsHandler::motionPeriodGlobal; +// int SettingsHandler::motionAmplitudeGlobal; +// int SettingsHandler::motionOffsetGlobal; +// float SettingsHandler::motionPhaseGlobal; +// bool SettingsHandler::motionReversedGlobal = false; +// bool SettingsHandler::motionPeriodGlobalRandom = false; +// bool SettingsHandler::motionAmplitudeGlobalRandom = false; +// bool SettingsHandler::motionOffsetGlobalRandom = false; +// int SettingsHandler::motionPeriodGlobalRandomMin; +// int SettingsHandler::motionPeriodGlobalRandomMax; +// int SettingsHandler::motionAmplitudeGlobalRandomMin; +// int SettingsHandler::motionAmplitudeGlobalRandomMax; +// int SettingsHandler::motionOffsetGlobalRandomMin; +// int SettingsHandler::motionOffsetGlobalRandomMax; +// int SettingsHandler::motionRandomChangeMin; +// int SettingsHandler:: motionRandomChangeMax; \ No newline at end of file diff --git a/ESP32/src/tasks/TaskHandler.h b/ESP32/src/tasks/TaskHandler.h new file mode 100644 index 0000000..21c5bcf --- /dev/null +++ b/ESP32/src/tasks/TaskHandler.h @@ -0,0 +1,298 @@ +#ifndef TASK_HANDLER_H_ +#define TASK_HANDLER_H_ + +#include + +#include +#include +#include +#include + +#include "logging/LogHandler.h" +#include "logging/TagHandler.h" + +namespace TaskHandler +{ + class Task; + using Task_t = std::function; + + // Rates are in microseconds + enum class Rates : int32_t + { + ONDEMAND = 0, + MAX = 10, + FAST = 1000, + SLOW = 10000, + }; + + enum class Core : uint8_t + { + PRO_CPU = PRO_CPU_NUM, + APP_CPU = APP_CPU_NUM, +}; + +enum class Priority : uint8_t +{ + CRITICAL = 4, + PRIORITY = 3, + AUXILIARY = 1, +}; + +// A task wraps a periodic routine and assigns it a tick rate. +class Task +{ +private: + Rates _tickRate; + uint64_t _lastTickUs = 0; + int64_t _sleepRemainingUs = 0; + +public: + explicit Task(Rates rate) : _tickRate(rate) {} + virtual ~Task() = default; + + virtual void initialize() + { + _lastTickUs = micros(); + setup(); + } + + void handle_tick() + { + if (_tickRate == Rates::ONDEMAND) + { + return; + } + + const uint64_t nowUs = micros(); + const uint64_t deltaUs = nowUs - _lastTickUs; + + if (_sleepRemainingUs > 0) + { + _sleepRemainingUs -= static_cast(deltaUs); + _lastTickUs = nowUs; + return; + } + + if (deltaUs >= static_cast(_tickRate)) + { + _lastTickUs = nowUs; + loop(); + } + } + + void sleep(long long ms) + { + _sleepRemainingUs = ms * 1000; + } + + void wait(long long ms) + { + vTaskDelay(ms / portTICK_PERIOD_MS); + } + + virtual void setup() = 0; + virtual void loop() = 0; +}; + +class FunctionalTask : public Task +{ +private: + Task_t _handler; + +public: + explicit FunctionalTask(Task_t handler) : Task(Rates::SLOW), _handler(std::move(handler)) {} + + void setup() override {} + + void loop() override + { + if (_handler) + { + _handler(this); + } + } +}; + +class TaskExecutor +{ +private: + static constexpr uint32_t STACK_SIZE = configMINIMAL_STACK_SIZE * 8; + static constexpr uint32_t INITIAL_CAPACITY = 8; + + std::vector _tasks; + size_t _initializedTaskCount = 0; + TaskHandle_t _threadHandle = nullptr; + const std::string _name; + const uint32_t _priority; + const BaseType_t _core; + SemaphoreHandle_t _tasksMutex; + bool _started = false; + + static void threadMain(void* context) + { + TaskExecutor* executor = static_cast(context); + if (executor) + { + executor->run(); + } + vTaskDelete(nullptr); + } + +public: + TaskExecutor(const char* name, Priority priority, Core core) + : _name(name), _priority(static_cast(priority)), _core(static_cast(core)) + { + _tasks.reserve(INITIAL_CAPACITY); + _tasksMutex = xSemaphoreCreateMutex(); + } + + void registerTask(Task* task) + { + if (task) + { + xSemaphoreTake(_tasksMutex, portMAX_DELAY); + _tasks.push_back(task); + xSemaphoreGive(_tasksMutex); + } + } + + void start() + { + if (_started) + { + return; + } + + _started = true; + + const BaseType_t status = xTaskCreatePinnedToCore( + threadMain, + _name.c_str(), + STACK_SIZE, + this, + tskIDLE_PRIORITY + _priority, + &_threadHandle, + _core); + + if (status != pdPASS) + { + LogHandler::error(Tags::Tasks, "Could not start task executor: %s", _name.c_str()); + _started = false; + return; + } + } + + void run() + { + // Persistent list of tasks that have completed setup() and are safe to tick. + // Avoids per-iteration heap allocation from rebuild and guarantees + // that handle_tick() / loop() is never called before setup() has returned. + std::vector readyTasks; + readyTasks.reserve(INITIAL_CAPACITY); + + for (;;) + { + Task* taskToInitialize = nullptr; + + xSemaphoreTake(_tasksMutex, portMAX_DELAY); + if (_initializedTaskCount < _tasks.size()) + { + taskToInitialize = _tasks[_initializedTaskCount++]; + } + xSemaphoreGive(_tasksMutex); + + // Run setup() on the executor thread; only add to readyTasks once done. + if (taskToInitialize) + { + taskToInitialize->initialize(); + readyTasks.push_back(taskToInitialize); + } + + for (Task* task : readyTasks) + { + task->handle_tick(); + } + + // Yield for one tick so the executor does not spin at 100 % CPU, + // which caused heap contention and potential corruption. + vTaskDelay(1 / portTICK_PERIOD_MS); + } + } +}; + +class TaskManager +{ +private: + inline static TaskManager* singleton = nullptr; + + TaskExecutor _criticalThread; + TaskExecutor _priorityThread; + TaskExecutor _auxiliaryThread; + + TaskManager() + : _criticalThread("critical", Priority::CRITICAL, Core::PRO_CPU), + _priorityThread("priority", Priority::PRIORITY, Core::APP_CPU), + _auxiliaryThread("aux", Priority::AUXILIARY, Core::APP_CPU) + { + } + +public: + static TaskManager& global() + { + if (!singleton) + { + singleton = new TaskManager(); + } + return *singleton; + } + + TaskExecutor* criticalTasks() { return &_criticalThread; } + TaskExecutor* priorityTasks() { return &_priorityThread; } + TaskExecutor* auxiliaryTasks() { return &_auxiliaryThread; } + + void start() + { + _criticalThread.start(); + _priorityThread.start(); + _auxiliaryThread.start(); + } + + void critical(Task* task) + { + _criticalThread.registerTask(task); + } + + void priority(Task* task) + { + _priorityThread.registerTask(task); + } + + void auxiliary(Task* task) + { + _auxiliaryThread.registerTask(task); + } + + // Backward-compatible aliases + void realtime(Task* task) { critical(task); } + void lazy(Task* task) { auxiliary(task); } + + void update() + { + // No-op: executors run on dedicated FreeRTOS threads. + } +}; + +using Manager = TaskManager; + +inline TaskManager& global() +{ + return TaskManager::global(); +} +} + +namespace Tasks +{ + using Rates = TaskHandler::Rates; +} + +#endif // TASK_HANDLER_H_ \ No newline at end of file diff --git a/ESP32/src/utils.h b/ESP32/src/utils.h index 94c62a6..61d99d5 100644 --- a/ESP32/src/utils.h +++ b/ESP32/src/utils.h @@ -56,7 +56,7 @@ void strtrim(char* buf) { char* buffer = buf; while (*buf && *buf++ == ' ') ++start; while (*buf++); // move to end of string - int end = buf - buffer - 1; + int end = buf - buffer - 1; while (end > 0 && buffer[end - 1] == ' ') --end; // backup over trailing spaces buffer[end] = 0; // remove trailing spaces if (end <= start || start == 0) return; // exit if no leading spaces or string is now empty @@ -76,38 +76,6 @@ double mapf(const double& x, const double& in_min, const double& in_max, const d return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; } -// #ifdef ESP32 -// adc1_channel_t gpioToADC1(const int& gpioPin) { -// switch(gpioPin) { -// case 36: -// return ADC1_CHANNEL_0; -// case 37: -// return ADC1_CHANNEL_1; -// case 38: -// return ADC1_CHANNEL_2; -// case 39: -// return ADC1_CHANNEL_3; -// case 32: -// return ADC1_CHANNEL_4; -// case 33: -// return ADC1_CHANNEL_5; -// case 34: -// return ADC1_CHANNEL_6; -// case 35: -// return ADC1_CHANNEL_7; -// default: return ADC1_CHANNEL_MAX; -// } -// } -// #endif - -bool contains_duplicate(const std::vector& values ) { - for( std::size_t i = 0 ; i < values.size() ; ++i ) - for( std::size_t j = i+1 ; j < values.size() ; ++j ) // for each number after the one at i - if( values[i] == values[j] ) return true ; // found a duplicate - - return false ; -} - void hexToString(const int &inByte, char* buf) { std::stringstream addressString; addressString << "0x" << std::hex << inByte; @@ -152,7 +120,6 @@ void appendNewline(char* out, const char* input) { } } - struct StrCompare { bool operator()(char const *a, char const *b) const @@ -161,28 +128,40 @@ struct StrCompare } }; -// adc2_channel_t gpioToADC2(int gpioPinc:\Users\jfain\AppData\Local\Programs\Microsoft VS Code\resources\app\out\vs\code\electron-sandbox\workbench\workbench.html) { -// switch(gpioPin) { -// case 4: -// return ADC2_CHANNEL_0; -// case 0: -// return ADC2_CHANNEL_1; -// case 2: -// return ADC2_CHANNEL_2; -// case 15: -// return ADC2_CHANNEL_3; -// case 13: -// return ADC2_CHANNEL_4; -// case 12: -// return ADC2_CHANNEL_5; -// case 14: -// return ADC2_CHANNEL_6; -// case 27: -// return ADC2_CHANNEL_7; -// case 25: -// return ADC2_CHANNEL_8; -// case 26: -// return ADC2_CHANNEL_9; -// default: return ADC2_CHANNEL_MAX; -// } -// } \ No newline at end of file +template +class Ringbuffer +{ + private: + T buffer[N]; + size_t head = 0; + size_t tail = 0; + size_t count = 0; + public: + bool push(const T& item) { + if(count == N) { + return false; // Buffer full + } + buffer[head] = item; + head = (head + 1) % N; + count++; + return true; + } + + bool pop(T& item) { + if(count == 0) { + return false; // Buffer empty + } + item = buffer[tail]; + tail = (tail + 1) % N; + count--; + return true; + } + + bool isEmpty() const { + return count == 0; + } + + bool isFull() const { + return count == N; + } +}; \ No newline at end of file From c710875bfdbea879f15aa6732d5edb9ca81f44c2 Mon Sep 17 00:00:00 2001 From: Millibyte Products Date: Sat, 28 Mar 2026 14:57:58 -0400 Subject: [PATCH 04/42] Millibyte Platforms - ESP32 Firmware 1) Consolidate tasks, no need to spawn a separate stack for each 2) Support pullup/down modes for lube button pin, default to pulldown 3) Add lube button pin mode setting to settings factory and UI 4) Remove old minified web assets that are no longer used 5) Update index.html and settings.js to reflect pin mode setting 6) Update platformio.ini to reflect new source files and dependencies --- ESP32/data/www/battery-min.js.gz | Bin 412 -> 0 bytes ESP32/data/www/bldc-motor-min.js.gz | Bin 1242 -> 0 bytes ESP32/data/www/buttons-min.js.gz | Bin 1663 -> 0 bytes ESP32/data/www/esp-timer-setup-min.js.gz | Bin 457 -> 0 bytes ESP32/data/www/index-min.html.gz | Bin 6010 -> 43467 bytes ESP32/data/www/modal-component-min.js.gz | Bin 1046 -> 0 bytes ESP32/data/www/motion-generator-min.js.gz | Bin 3838 -> 0 bytes ESP32/data/www/power-min.js | 1 - ESP32/data/www/range-slider-min.css.gz | Bin 601 -> 0 bytes ESP32/data/www/range-slider-min.js.gz | Bin 1922 -> 0 bytes ESP32/data/www/settings-min.js.gz | Bin 24989 -> 0 bytes ESP32/data/www/style-min.css.gz | Bin 2010 -> 0 bytes ESP32/data/www/utils-min.js.gz | Bin 1266 -> 0 bytes ESP32/dataEdit/www/index.html | 340 ++++++------ ESP32/dataEdit/www/settings.js | 350 ++++++------ ESP32/lib/enum.h | 8 + ESP32/lib/jsonConverters.h | 14 +- ESP32/lib/pinDefaultsWROOM32.h | 4 + ESP32/lib/pinMap.h | 19 +- ESP32/lib/settingConstants.h | 2 + ESP32/lib/settingsFactory.h | 424 +++++++------- ESP32/minify.js | 158 ++++-- ESP32/platformio.ini | 6 +- ESP32/src/HTTP/HTTPSHandler.hpp | 86 +-- ESP32/src/HTTP/SecureWebSocketClient.hpp | 12 +- ESP32/src/HTTP/SecureWebSocketHandler.hpp | 14 +- ESP32/src/HTTP/WebSocketBase.h | 22 +- ESP32/src/PowerHandler.h | 8 +- ESP32/src/TCode/v0.2/ServoHandler0_2.h | 174 +++--- ESP32/src/TCode/v0.3/BLDCHandler0_3.h | 350 +++++++----- ESP32/src/TCode/v0.3/MotorHandler0_3.h | 492 ++++++++++------- ESP32/src/TCode/v0.4/BLDCHandler0_4.h | 354 +++++++----- ESP32/src/TCode/v0.4/MotorHandler0_4.h | 516 ++++++++++-------- ESP32/src/UdpHandler.h | 8 + ESP32/src/WebHandler_psychic.h | 361 ++++++------ ESP32/src/WifiHandler.h | 4 +- .../bluetooth/BLE/BLEConfigurationHandler.h | 72 +-- .../src/bluetooth/BLE/BLEHCControlCallback.h | 41 +- ESP32/src/bluetooth/BLE/BLEHandler.hpp | 34 +- ESP32/src/main.cpp | 162 +++++- ESP32/src/network/WebHandler.h | 127 +++-- ESP32/src/serial/SerialHandler.h | 22 +- ESP32/src/settings/SettingsHandler.h | 39 +- 43 files changed, 2391 insertions(+), 1833 deletions(-) delete mode 100644 ESP32/data/www/battery-min.js.gz delete mode 100644 ESP32/data/www/bldc-motor-min.js.gz delete mode 100644 ESP32/data/www/buttons-min.js.gz delete mode 100644 ESP32/data/www/esp-timer-setup-min.js.gz delete mode 100644 ESP32/data/www/modal-component-min.js.gz delete mode 100644 ESP32/data/www/motion-generator-min.js.gz delete mode 100644 ESP32/data/www/power-min.js delete mode 100644 ESP32/data/www/range-slider-min.css.gz delete mode 100644 ESP32/data/www/range-slider-min.js.gz delete mode 100644 ESP32/data/www/settings-min.js.gz delete mode 100644 ESP32/data/www/style-min.css.gz delete mode 100644 ESP32/data/www/utils-min.js.gz diff --git a/ESP32/data/www/battery-min.js.gz b/ESP32/data/www/battery-min.js.gz deleted file mode 100644 index 55ca568aff0b4fc213fe558787f95aa09d3d5cb9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 412 zcmV;N0b~9jiwFP!000001D#VrZrd;r{EDHIhzLT?b%g*nY#>0I7HN_miXPU|SQbi= zD!Ft+82)?d)|T8vh?JZpkh3$hv)r{)IagXuR>TbBzk*#8i(R2}S3_}Hf)^5p-IG!4`6%XeCiZBF#6*f1=2og$Wc~snVuB&}03=^1XnK4tUU&W`CYv9mG6ZcApu8PXcoQi+5zK2bbs$%iA=9i3S3P*bIX-WQlW zSUkYYE3m|Om-na}n4oVk9qRUIq3x7L=hI5JwdU=$U3V>u8e}=^udVXk3|N|J@l=%@2(>V~{oC3eXAa5@ggUGR~wzk_0XApjc$a~-jCrwVL zQw)7dIHULxCFmJp#1{g8>DUr?HJL!jZLh`&S7R_rF^0ucA2H$I1Q?JI{5W6<3_nFJyOhrDO{sD;{IxejHV=NIl1m1mbo*dL*48>y*^XE^a!L1LvQBMcFA|Z`{l~JXkJ&@oTgNj&>XaS?o zH{mQG1n^lIwKH4;ZRw#c187?hZSRF{KLcf?I$Fd?pU&r$t?Rk zf_5pWL^>4v{3wH7hSe$xmRXkpPZ_bayK0(QDr%cs)lD~LEI~AMn;0=>TO%rDyYXZC zMO9%3Xj{k8wrXhG$I-U6v^z0r$f}~Yq{4U;xkbE5sckq`YtJ&TXIG3-mN<5tiKzS?TMSw^!|}kDsoyy}%iNArwdh}e zDhz(h$TdM_}H&kS4`W%)v9Y354Hy$*IMsXc3VDMNj|V2R4UW< z>VDVu^Ucbfq3KN4EYB__QEu!JL-W>tb8Q_iLE9T>%^AGW(9CkDOe<%4Wwu2EatFeEUW=cc{2q)o$wB=Mj3bWE}V>O2(Y@B$;(P zytk6z1tKd-!Q#|HB$pJwamt(L&#Y&!tN&V0oB-%)!JP1gIFFvp12#qEXABeGv0A^z z|8-A#_cO326l3}-7SD>MTEIZ)#2AHQN>N9$msvuJDeG9TGsK~5={W6E-u#D(rM7d= z+*AAf7eUZlU2TyDTOW7b%>mv!JTI2!D7VpJx5?I5hgUcAM^C{Lv6K%UH5YPtF1dTF z2k(Unv1CSH^Q!GoH(OWMOr}bHB=(o$(2~ZYz8ih**Pjr-nwo`$cT{(-;ZrPYSPym2 z8fu++jlzL0S#1c7T6NbMn|Z6wSdJGtIE>{-Mx3{P_@7??V=JC{c_k`No5yrBetm0Pv-+Bz zMoU^gt-ZCc*+aNFn;q5FXi0ZF?4uf+8s^rUnA>k)ZW}P``x`B>YMN!;@1a9eeuNeP E00U!PZvX%Q diff --git a/ESP32/data/www/buttons-min.js.gz b/ESP32/data/www/buttons-min.js.gz deleted file mode 100644 index d3e9de9b0b7c5401e022157764f479bc73b11c1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1663 zcmV-_27vh=iwFP!000001GQLfZ`(Ey{ws`%r4qsj?uvb}s|InFp#|D*XuJw&>zrF-q90C|u^Q_ZZ8l%s^;=4*E6)^Eh3-k6kB}QctUWZUqZdh#k+5 z5{SM*9E8>S9cazPT=_eq^sv=dQw>|SMUPnBIB+Agj0t0P!O{{Ad)f&41t5SCxDIis z^F=t`knh)mrE=cFd2pjWvb2CqE?~O4Ntsd|EI!L6?tAd~{#RMtvM6y~S5xqUw~rA9 zUlS4NMoLi`47hzGg)U^O9B-0>CS~xq=Be_d0x&sd@Lm5QiwqrcN7OM{glKF$sNOGt zYG2?J{q;Bc70;u&6t4Cjb;6ZjCmFu3{g8{;HH+x0Hz|EC@>2VlZ`WEE9E~E_xN9s$ zx^n2*sCJ0;9QuSuDR|WJ=`Sw2E}7TLqnrzR`3ik{ic_p{${Z)?8C4Et!ycoI<~l{) zSpo!IAjUMQ4G~l%_$GxjS+Ih{+tAd9Yog;iF<)RfVm~tZ@PRAsvls(b=R>8-2Xy(4 z?hT}AqpSv$)iLGtpt8!DFj<5!2`EhJ1WtX@@SskO3~Gfwj{|4gB7Ph5=02KehFz29 zD+iaPh*BPX3wELl8(lQC6J*MLmggYin}nyan|f6B7DBw}y#;PBD36wYA(u8v*0p6P z$n1pJ`d;=nu6DH-*}lcNj#V}g=*-?82_>5uPQ|@5U+I-etsw`jYhvr927L6+p-Dc7%LODtap#LUsb+E$9<#4fbYdllD@;6h}JzC^{^HR?N8=lhK6aXau?5JUfl^cLUBFs!X2B z-PKikCVwXl8y4_y$qTH~XQRrA`IGs2>&3G;d%o)kou5X-^sbjFNqv?2i}Px+iNbB~ za35F*Y3FEo98wdHsiu81x3w8e4A*4;A{b2;Z-k$MnCoP+xVZ3|;;n;4g`+$$6n3VW$0Gi#%7)WD&x3x*6=}^I*+U$khjxNdy zp-YWeBZ*Dx3jA*N@k4;!FzNig-%6|To3MD>|N0+Ul86a{J3>L2SeauX2+Sy$gy@}B z;qA;>!OUsH6+sUpKve2eSIMFX3$a=D`@3K&AWRI09vG9CFEUvf~rI=KZDY&(4QL;alMwU z2WOwXvu0&)jG6U{iZV^l0)6EkK0>d5(Z_kYs)tRzx*AT1rWUs2`)LY| zBaKphjH6|x+xZOmZ<-1wwtDL-`KR58 zZst_$$M${gmnE{j-QCqg4cO15=F{k}=02`Aa&3z;3;E(gDXg6+q6>XvQ|P diff --git a/ESP32/data/www/esp-timer-setup-min.js.gz b/ESP32/data/www/esp-timer-setup-min.js.gz deleted file mode 100644 index 48dca9fbe2a8808b3ea024013c27b56aab5f9c88..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 457 zcmV;)0XF_0iwFP!0000018q`GZ`&{o{VT2yfrJP`b{IC)^JQHNJyYV$m@H zS#%}3MH=|OPgcLyq9+-AkB^U!zT8~jVh^D{p+`pJ@Dc3!*%#H51$Ta$l!ZNwzJYkY z-Kp5ogODfgFzUoGPCr#96@kYgLD-6n{wF6x!h)|p*6XvCdQ++`HDeE+^#So(mihX9J75B7S=EzCm}i^hQo^U}$H_>3aeXVp5T z_|hd_=g8B2I;%|~tUX_l=Ri*IldBI)s#!)Z79t7VKx7lrb2nlDE2J7I^7T8uSV=j_ z^e47vbo}etjhyrzz0JmzS0t0{p6pSZVE}J0I&@Z$4)h?WX{!DK2SE{x>H+`&AS2^3 diff --git a/ESP32/data/www/index-min.html.gz b/ESP32/data/www/index-min.html.gz index 85c9230a94b3cdba498e9e1ea90b6a27f9bf9db4..90009d9a5b7b1541d9a9acd61183b72c8f8717d8 100644 GIT binary patch literal 43467 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 PTVFl`S0SfTB+LQ;3vYxR literal 6010 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 diff --git a/ESP32/data/www/modal-component-min.js.gz b/ESP32/data/www/modal-component-min.js.gz deleted file mode 100644 index f9b0d52189cbe5febb73db79afa93d8dcdb966eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1046 zcmV+x1nK)9iwFP!000001D#fFkK#5E{wv-{y-OsLh3j!`!*Xr4I_W<2`k{BKDs}t8 z!~=2H*pcl3Ta^FaabCb7xm{WWBAGYOJTrdmOi5$ORxv8Rz_t+7!jh@1+=8@8rUBnS z2yky(T z{ceMkJ#-sjg0f)s|UMRyYR}vNc0L;_;6LZPnkdpb-lkPdQc}ix} z>2B|dbKslYHuBt)TUDgQ_g~TMaidi!SxWR~6;6}+#QQ~~^<+v~$`P|`Dsr#!TBvsRYkEkNJTd-!ar-f#q&mR60T zfi()HQ?e2$@-38Rfbv;!AVvw!$Uf&51{SfVjF%=QZ|22e6vP}TyAHv>OJn)^sfHZ~ zIVGl`8N@5FdpJQc791yLt^5@%9BfPl-(cwi)^>D|zIh&}i%}5DU@E1xs<%Hn$Jx8$ za1}US4&P3D(yqA|V!xIQVF5jwpygHoITCW2TkHV96y9DJwRS|WHb*yFZ9hhrT`6|e zejoI9u&6)$UvIu)CuiS%I4Pn#bLu_)&T=>{+6ZgYDlPT_hE#11HSE7W8Q^aSj zaBV&*#^mvZM?ca=hCb;&EJejYmE`g5=pnP3|e=r2xVhYZUd~^E5rKl-E;T< zdQlfkKDk=&72YW%5)(lJ2gwX^_qq1!cg)-^QJ0xjI*g8{ECBx(byumiG|NBNgcTkU zH%J3jfDEcAp#?s$uzy)>2{6G?kx&oA%`}QCZhq&6uLLY_W(#AHFfxJ@L)6NE-U0jB zTFr4tz+eU%Y^mj+!LD%${;FuFzR#)L0K2C`xZ6l5Cor+m@npJO%2%&K`(vHF$ua3D zt)H7&j4I4eLT0@XQC+_hkO;L2gOfoJO+3#EkBb+5eVZVv5VYbM>sF11O`^vpX~%HO zpgWy3Ag|d)(B$I5IIWt*M!np%d&Fl}wK$zXCVi8t%J3jl+jcTc!eEV*8J;M#9iw2< QAFF8bFI=v)XKD-p05(Vf;s5{u diff --git a/ESP32/data/www/motion-generator-min.js.gz b/ESP32/data/www/motion-generator-min.js.gz deleted file mode 100644 index b5a15615dcffd1569968172bde1fd4c1a90951aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3838 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 A(EtDd diff --git a/ESP32/data/www/power-min.js b/ESP32/data/www/power-min.js deleted file mode 100644 index d818008..0000000 --- a/ESP32/data/www/power-min.js +++ /dev/null @@ -1 +0,0 @@ -function powerSetup(){isBoardType(BoardType.SR6PCB)?togglePowerSettings(!0):togglePowerSettings(!1)}function wsPowerStatus(e){var t=e.message,o=t.servoVoltage,n=t.inputVoltage;document.getElementById("currentServoVoltage").value=o,document.getElementById("currentInputVoltage").value=n}function togglePowerSettings(e){Utils.toggleControlVisibilityByClassName("powerOnly",e)} \ No newline at end of file diff --git a/ESP32/data/www/range-slider-min.css.gz b/ESP32/data/www/range-slider-min.css.gz deleted file mode 100644 index 9acba939c68bef4dc0ddda9173e7477fb1c4706e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 601 zcmV-f0;c^RiwFP!000001Km~8j+-zL{FPLvdpk!aCoVaoenlUvQ)FNX?it&%O+te1 z-@7JI5)!q^^`)wiNW6H*vpZvZZn@gPI@8K?p}?+7;5q)dI`z=_HoORzVxw3F%7aY` zoJyQH0BeY=xRf9B2}YX>XC&XVLPC>-7s-W%%nOYuTGo|Hb|UvBThYg);&vkxkx+QX zYp;iZ6`Qh;q}n1Ul4|dD#Uj(poR%V=J;cQ;a5`l4-&dx&pfsBX`a0XOnrI$Dm7LTY z7#*wKLC0lD=@npyOu)J>WVLLKcA~4)g5+LoA*qmCd4z~fGhrLit8@!(tKUGLb@)f^ zyeRf0Oh=8FGdu&5g5LqC>2ixdE_Wj2NjEwl~8PvK^ zXQUI;D|Cc1wg2w-2EKNm-WWqM(|3WIcd`**)>YaLL2e9ioS6YrT0t@$8!f^fAh5+g zJBH@(jU2t8u)W6J|CDF%8>?%TqbyQe_D~eXU}Vd4QFoLvg1e~Xx!y543r}$OL-rD$ zB&)5%)PB`Zm&Z!I#paEakjyeXpGV|buwi?`UML5?tKd0Q@Vr{4DLjSo>o`sZuzM44 zV_NA0=?4Fwtdr7af2rvO+&?9#LP}uY+M@_Dz1nz6X)PIqCY*~ko{Q?~ zu5?}d^Ym8Zkt_V3MK`qnW?S~g3%={^NYPrqE(4z8Er?8}_U&@dExCxpVOv;Ted~5{ n+e6djfW63}VT;QSd!&L_iAB&aqB7cguKdSOW7Go1F$e$v$4V-H diff --git a/ESP32/data/www/range-slider-min.js.gz b/ESP32/data/www/range-slider-min.js.gz deleted file mode 100644 index fec7197f1318e376c14267f8c74fe3fa4d69f466..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1922 zcmV-|2YvV-iwFP!000001HD;obK*7-{wu`oW$Z@rk#^b-A-+tG@`0Jl^(LYHl4Me0 z3(z2BMiK;<^546X{EmsC)9KC40V`>>e)icn->+m%L-K(6lsqyT5fN;|8RncYdHwW) z7L=Q{>mPsaB55}CV-|~RCyWK3u=tILFLr68-riNm@7;<5so#5zptgo>uM&kSQsrLW~mO< zqC>r>qp;|8MsSoHh7T%bcue%w*ayV#loE8jQp9L=kb@Z;a1C74pOJ7rjyH~nuus7Q zVnjA7a7OWT%18$`I!WKL7OOFt`zKIzDkxPaKh|v#C#eBO;*85%tJ=Qc`#Y= zP|;ZXbqlU8JEh=<{G5En3wX|yPKG0?Qn(qO-nGu?_S6mc8R1hk>#2(ik3Xpw-&jPV zfMnm0;i)oUq$*QwZiT`6Fo%m#eiA&YH>{QsK=4SEqI@dduDA2Yl-tvYS;S}yHE7L; ziYlNyaz>z|^F|r#D6&|_0t?ELjy1M(_vK5+_(}R5vl0Gc_J$MLQDQ|pG4G7ySoz?1 zdK~pq|3mOLVJm!PQ3x=d^?60JlMhBl3gk=P@dXrz)PJR0@mh`71yZk_K^ zotGBDOJcM-*Lw>Ql{WvB-6}k|E2EN3BzR}YC$ZmTC<4frOR&MDEth#U0rn8D2$c3$ zSrF9)qJD&W=(Qr?9`Lj*cr8LhLTJdF3c0f|kq{02>68- z*>`GpPg??KdQ#$A9@Hk6m;+VUt%O`_LWUQO*$1s_VWA;`xM<8&u(?j(bxx_+H_!2f zE~v_Av~D0b;@+0*?+y47*^O9_eN@q}IN9m!tTUp5yT_qr5w6+lUTXRRhln!R7 z-;_x-$R1i&2)wQf`W~8A1WRkdxP>y~6U#6fS8kcwTP7{HOzOATE=tyVhCwUg1||bS zLuUIl=ORRdy(bXyB3<@i)$+=)!hxS~Ht6b4ys1Xs%ra zd3aNpH>w&iG6r1f&$+xKVh1enxIxoHur0_};cF@A^zDI6p6M>^5!8Kq+^Was;EH z#{Zzn&2w2X3h$7d#c%FA8p^L@?&AnU^7QSQ9XiBThN!oNMX8=5Bwp-50gAt2A@y*L zuuYvZh%U@p_R^^*KUGp6;38|KsIp(b*U))W!eC{d=?@IiNL8Yb;(ZZxzBZLA`7QewK#(_>#fh3O}4O<^$eIk6A9RHtjJ`^7u6z32KByNHz^TNsTQb1YtUjcSKqH^~X4m{+A#xVI;bl;)o zmjwC^L$%5tDA-wjO~vK%yR|Q}k2?tES^E^5w-Z4CbqzPY;OLE^Yn|_QJ5)!-c)u_j zgVFdzqj6?5JJx7yjAn(=EPIU=K!26J-U?Lno0Q^Va&liA=z~vBxr4KUanYMq zzg4?V_d{Ls{WkhYf4J&)chx1_vLy1Q&Ys4@YaspL{pky`WH==5$ERohES}ZN$LRt& z=075&O2DyW9L}dAUhxQm2{pyMVft4yh-FZFH$nH7ryBkmLNm4re!Q*;y1QNP9}7$u IU``tV05%D&CjbBd diff --git a/ESP32/data/www/settings-min.js.gz b/ESP32/data/www/settings-min.js.gz deleted file mode 100644 index ebcd272dc44bdac0ccbfd421ef2d0e4828d1547f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24989 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
KgmyN<)N9s6ZBDA9K6D%r zOL!iuZ8gDdCj6SFoOVTlBifC|OziA*W!MC^e%uMe9*p&*d8^m9Q4rludB0e;&8?}o z^{%ep}sTiv5My2WsQe}x4o%Vc(f}mDYcH*Qq zfoP_#dgOd5%+Uz8urYzRm*j$|lejr!QPON~9SR<=oDGG56-$h`Ha(U@75GjC8Mtf4JbikmQFuz#$c7_CZmceSeOXP+< z8#(y6I+`{Xm4?%(rF+a?w@f5;X+5p4$(SFHMom#3lf9zI^EFZG@QX1WPd&L4ZA2tQ z6b`S|ri@D2Xp0!AbG6NDVb^qVray^wyc$G=y%0u2c5UKB4VwlFY1JESRA(WtxdF?g zdfDB8nVD==-fl|0`F7oKsQsQo&s1AJO88=1sPFt`9$S)3Z}#*%&C`C3Hzg9jQudIM zv9s1i#{$8+-5DzJ8sM@@I(mnp{`Vk0KZ-V*Op&F~_st#&%aZ}qmXxyAQQ&rEvcQy9 z*we8E)Yq8lM(`SzvS`)8IH;mMNoJx(rT1VfmV-O-qGD4mxF!yJN^>R(R<+ZPlc8)` zA=&QNr%8EBqp08OqG6B4g(ZzS3%}SM`$e;kxcw^c&6#P>84n^gf?I;rg+0Vxy+lGIG-WhfL`M~j$%F1hZ_M#(BpdBj>n?oEk%53 zbhUvcov;GQ>w+S>yIz%XIZccAESSyL_EsWD#6S1~3 z*^cTE^1o>fJ~~YHErRFu5$7!3j>Mb2%_S^i1tqlW|c|osEdkWooS8cB&T?6hGHt|DGMZGEfF9cRXRb5x< z4$M*HO!f(9yzDMEi+0%bqmG4J%a~gmT}r7;Y-7~Zk*J{tR5?cDfnH%zwXs`Kc+{Kd z%Wjg`vn^g=`O$I>6ROIeK9`c5}*VQ^AKiWt%1Ga&_nTjat-^ zoZ&(qc9!E2qxq9gB6VxrY*F!=;W#8CynCSJ*wwmoJpeu9+uw~hiG>E5L zEr)F%qd8>%VLg<>d92r-!foK#DuItqfLDz0vxFPxVhb zNYrCgn(l1TZr`Pf16##K7V~k95UXkz?{95?BvcZk6OY8D4iJ_sH8J}NuWc(89iNcq zh8IZFQs+`R>JMuA#%j_Vu4wl*qgHVym?NWw}}I?A8ui_k6Rq-ULw+)i+DAzXF~&%TCoD1CJbd92<0stxJ&AA;6YK1?du7 zm1hPMWWncbf-p*8XX1B498~@n+u8gvMOiyqK^)emJxal-gf^`no)kMht~(qH$huc_ z$jZDNGh%t+&7hcS3%r=nBLfOsWyBeGA+H@zYK^j1tE?cxBPMhltww-ngX2(SS~L3U zltk&ixAw8#9JM=Bd{QjOd<%0zN<~M5R@@IEdmiuLRyYWHWz!?*J~VCC2~OLYA<=S> z-eM%Sw{^H;5#0Vcg}KN;CMs0Os1eF~&gJ43Reo2F&Wvy`R#wV?k4-ooBb4;4o~6g3 z40_*$ai=^Z`8C^YgZawIGBRqIlCw01hPw17E5?{iM`U1D_*hYPjIs$eoovk~_C$*U zda>wvp0%uOdc%o0tGCb*aK7d>Lk|IWD&#mB!@HICVop>ycpt6~IJkz)y^%_+Sasgw zWe8exo;lWrlaXstOU#K|Bo)Yvz2%Tag<45bVkomP!`7Lm*os_=?I7>Ya+WEGMjv2% z+6(LQ0~Q!<=D$&Av!ggQv1L7hvqeYrXrCi&UupBUPt+^~1Q%@RVM2#p05Y(3G7pG$MgFXyGG^tcv?%XPLBbR;M!W_3EF8a zWVRJ?OC#8bY}?m&P|fes<5sg_?kEp6tGF|l*1bL#hh?j*TEa$%pr|t*B}{##?e}XI zqhX3INi;VNfNeFWx8!g)P7THBX3y}y8PoR%%yEOMfm*=*MRBV!A?#6evGD_?rm>RA z#TLV0-K{j%44_r`(yMw=XI+cR@pd_m+an)Y;eCADQg&*wN8;5Lg4pJCrO#*(_G0|B zf>Q}gY-y*rsMnG1q%)4@5elodsS0(4_PDAdz{l<3Ws29xvM+3Q8$;FtscMi`4b|Jq za8gSU4|upKwjAG0u~muYt}$Tq*v{eMw({?UExc{7^gc~fYiv*xY>K2%+>|ZU`0&CRJ#+KhQ z;{0^Z_G=^pU@J*P6a#GC9JWR@*ma|-`EP@5CUS>h#{3Mb;;rpyK?!S~lcynCtcIP& z6y?{;<+>R+c4CMLqoQEaaFvv*MS41KFie+JMq(jmT{}XuLItXeMEgad$42Y6DBe92W#D2Mq zQ{&i~Vq!E#mKrx4mzSM}wXK93c&5#|OM>m9zAQNe+2>}nK;p^9vRVVkjaLo@PdI$D zhW!o1+Vdi+!wbJwk^2pq+}5`!-y5trTb__I9mYnB=pkDi5$atKP0c~@J8cGe@c_2% z)c|1ICf2pyWQPSgY`ZQIIg3HZ{Wrz-bQF7bW8B39j_@j|*&4tyg}Er=wATY9Tyz#I zsJ+2=<7$uhU>mmac28k?ihygiMn!0jI`UvJgVx<4@a%BO7SJ76G(&!FN#>w6Ept<0 zH^Q~ybQ6~OK$H#1iF!4`Mb-g}tdxedyBheXdtpp1#a1#K^kg@$MJH@KixgW1$r*}& zA8f5eUJLvtj#0wf;UvPZ4VsPp*#sN**AgwOF%&MkYA0Uwro-Wc;)+I?1Ou0miH6s= zsYo^mRfei@JCe7uHQ=`b1VOy-nXtNm2sfY*hE*UKGvGZ4P`5D1%X912=ldNuj}^c zZqx3e^>8tRR=bA8(!_GUP$)mx^ksC)jR$eJ#+A*Y=^zZEtb^c+OzECNgs1y8qCF5~ zHW;K2-5r)LY{fNbWgBgBfd99n_UWTJE|vws&{0D0F??2?M8sgO^>lKZL}P8lS>GL&XKivyc%z@xWbSICkXij^Rp}lth zCt!!gSA6u6P{Utj!ZIt%pi4EZO6NMf2w)2V7E30%%LMhLSy%iyNjnFW4H2sQK z$KQ>l5j<4Jk-G91wSgteF=d zQjtlNCKqjDLEhG6qrVtY?50^~79Qh{R~y}uDB+VkD(Sms6qMRqg==p;t47i}y_MHu zcfy#I{XY>~w1;6RSvAT2q}|3h2tloSBdce3X<^dF$4z|-o9(UBR~ERVw3}>M?g*`_ zOQ*{PQs$l4LpLjVw-arajX? zM`F7_nnJ{q#K`~2*nZ;p7xgLKuPIU)wre!Sc7GhO?V~8aJ6`+0 z7uz-ZFFE$_L#Z@~%{ncU=p3&fmag>w)Vn>rIGF+duMy<`!Xw3&8)|xrE$|kY)C0rO zzp(vh9EGRn^n1YBQQK=4q~p zESc>C$}n&FQNI7DC(f-1P&m#p*Fjn(e9*HG(?2Mlp0RM1?Sp6RQ&xmV)~0!2A3n%d z`z$5kKA%jMEwga=&hjx+Zk9;lWuIc@O0rn=kSumM5X3^M03B)-vb3?F&^;6P-&3WdHVfl>PYG=3^J4J$_#< zGIJF8w`3EyvP3-n1hOBJ%w8nmIwr0@CZpSfc1&6Epr+*~%#3T885fxCfyp>Sg(8|k zXM6%W;~I3vhtTblm)%D&6Xs#xto~)6MJS(}bdM;1p45F0obfI@Z;IO~%k~Z6_Uijz zLT=Cd-vzq8H2*B@e2>qi@=3AVgL7;qJk#YDG))p*d_~VS-guu72!7e8D7gZ1d;#*I z>V8VcS2ID!A3zSQUW$60L;VR{1F)wT_ozSpIPpH%n?m8`EN9RG3ScJz)_ZKv(xa+Q zV#K~5&)+g(=aT)nzMeRHsbAT*^JkeK4A@Hfn4Wl_b2y*Jr}$bB=f=a@P#^ZWYxcHR zH6D(66u`NrB$4s(*8am>iNICEdocErJpWw{|1rrdJE(iK%LfLzdMiswTd@@9A&Jz7 zLzzX7AGwfmI;Q4)>tUVk6CalV&l_^ z^&BXBD!G2_P^ym6GwA8fcHZW~LA_@d@Le(_eq|E!Ea# z(wDr^*aMjL#ZS|a_^H{rNd1=f%K!OPLX7Iu&oouqJNx|8aKIFQZ;X_{0*Ha0%oNK3 zCg~@zhL={{ir!Ma;m2HJz%1@<&X+G&bH<%aCD59;>d%8A+0Qc;`1%{ChqL4<`rDHG z`g&S^pFxFu{ei&$x%!~OS^ENy{7C^ojXZ}lm+BfH^+=Gqi}{)-GoGt@!{lF7jQ#Ll zy+iRuHH;x}VMyKRZd&>~>1n=hgs-pZufvahuZPqhfBXr0R;)MR98T|?)LbUDQ^NXx zAn^AyUsryg8towlGev&@b0wX3L&s2nUV{mj043f6q8Xr@=d94h%my<#TZ4Shu{`5B z^MS7B?)y`@1W#Qk7O$rm%un&0YXR0o;ces|fr|nraoMQkb4F&1S=$fy=z%(CxrcJK z0j6(1?j&{I69I!e^HUe(W=f*fJ{RU&jq*FK+yLF3bZfz< zcpO*cn|w@QiPp;YK9>X0Z7xxwdU?#RG1=4H<^k6G`wRd9awk*X+{*GkI@hYc72dDU z8)@p@>tmZQzt08t!MNOFkrm#rPZ75&Q$#7&PU%>qeEC_y`z%?v6KlPMhU>2k;7s6i z%KQJg#wMmxx`}E zxc+MV_MEwc@ik@65oM(yo>u}8*65k&kMYyb`{}FRr`WpIeGpjX(}}Kt2ax)qY2vJB zfD5T;^DGV5kA1N9i9YYUf|l~VM?qY8A^ai3b9`U^vrJ{z8vT{xVU@k15kpW6@bMnA0E{Qz*De4nd5u+DmL zM*qJl{XzB5`mmof)NZ2=09dm+}E$bjYxu=_m*w)q}Sej+qWyZu7XRkZ<>ygrK=w76XLyH zd~4%WkTFP|pzxSA{KSGGa~n*DYsYPc{MOTl8zMp_jpQ9mAIfZCdC@W5-#*te@`C=o zT75x*DbEuf^NPM$X>!kj=WO>?tM%VXimoBU>=#lhmnx-dsaC3&8l`5bRYKsbynHA} zN=UhcR7yy-gw#q%y@WJMNV9~rN@e)BpSNwD5`jQ14LEt=%wAuWFKNzvz_VQSPd{#Z zFOj9cF+bnv0428<>y&h;MA7xS$I{#NFE_RTpF?WM*Si~F_c$6K@-jZdB|qb@b?%Jh zq^#%D;Ln(q8;zWkof^6@FZUWdZdTnwym!f&A!9plRowx4xeza zkvrAzyVsz!e7|z}Fgn5k&$pR(7416%mgvE0yYPB*XC!wfXny1I#wCxJ!AR z$=e?0?U?Vhn|VtQb0oL<%t8yScFwkcQ`8?m#f3i!@nOBl7;Ezw=Zs%v_(`?}B1oD3$QvEj}gR|D3+rm@#G_Gdm-? z%`0nD0?uLhIq}X(@5}f(w(lu#a~)c-Hvd%j-&20T^>agXg|xI+qr-eT8*99a_WLAL zDBT|D1Ig1Rmy_tXn7pHS$KwKnTa2!gz2kN{{C7L|p6+T7Kf&%g+4a7D0{RK5@s98|&ynJ+j1RT#oazG!Kjd~!bw@T!HQ>8q9SQF9KtAGue9WW# z5s&i6JSrdYsC;0`5)}lgaqN3{&Da^$ZEnIcF^rvYk5!K3&X^l+aCfv1<+nL>)9r-t z9*+~nZ7#SH8NaucyV&GuZgZkM(3qzL2v~R`v7ZceUTN{#N}=GOAVOawl%TeQ>;b;}-XkI9>eG`=f`kIS7ANX_hB>+pJS!faLai4Rv3biF}Cs zo=U*`1_E^q&j^)+AATqkvInZ}?yDU4C9lKvR{VB*D1V#&?JT>MKg6-8m)pAm`JJ2N z15-D+;XK7or-S~}AAejYpac}fK`DB>WBHrv`4;a~ag|LrhxqiVwd;7Ck=%*7^8=d; zQg4s`o%?&f2j~+&iSBZ{AiEPIg4Mz|@AAr#lt^KiZ8YzryUUH^+dJVGVIiov_Xwi5 z-=B#PrzAhPJ`xv1pNSA}GdrjGe1!Occo&qPju7t)`f08g!OU&0R|r2JA%4L2)8mv| z32(E!Ao^4oaGTo&(Z`PJZB}`TJKMs*F@C?Ncg843a-R?CrVq(nm*!M{XM6VjR{D(< z?T#%;Vm}*_o0jBAaL1wa(#K&s1Dxc!l&s357WH?TW0PiFXM-F%HZ?n8Ydau9xNas_W zuM&UwfYyg>FX?V`1WRNfWZnA!)^~*Wc^>1vPjI~;z0LRdgoU7idEosnTY5adZWFFi zewuIBNSA_7G2WAwD&^NtkbV7nd$akVv{Y0n4>vim(l;1xuN_JVE~Tn-W7&8szrUd{ zl-cVH4uyAW`;LCQ_P+3A+|Qnvxrcg=@K165JvKHU4Ctp=&$HlBoTty5g3ev%ctP|L zH{OVRCy8$cf|pc(dU~XfWv9+rm% z`K+(?4F}kDd>_@_c66vAvkyH8H(Uih0KMB&K&=%-aL* z13&tsvCh%lQJs{EP2vyuWE3B88G%{j-thc$1Rrpb728O4^bVgR$^MzO!-k;XSUPZ} zayp#8r;x47r+vR`4;iN~5TZ}%_`aaeGb@bZtnlS2eS0`1-#^udW>0x#=TDamvFtgA z1FtXW;eEfC{gcaAx*NS!NS|Xg4vMsw!T1&a?d2)`_c>62gmK^3jWbEovZt5qjiGc0 z&3BCFwGw)7?|grI==%>q1zph4!wf`h`u;tOj7R#8fp+NpgSvg#KTh(#LpbPd^rw21 z)%bVmmgZy+gq->{SE4ppo|Ev#YdLEAnB;*XU0JeengVcQaqW4^AN!X{bKJ2Uqi2z1 zhfgn3Y&pPx&Z-|4)sYohs^wVmHV<4Olj8T((+)ERg%bKy`k*>R+z4cNe+}w?{rZs? z|EU(P#8o%>pT!;@a(~zTgM(ct-9nY1gdu}Yet$Z?&Hc&E`alP$@Uh$e;&>BO(#F~2(!1VzkR-9y3hJn@16di-X%N9{^xk7 zE>r1DYwo|U2I&8c79HNCE0l6C)#bX81UeYlv)eyt35;XOJKY7^FC8EX^b*JU@(Psv zaFV{@`dTq6PbCAK8m|Q?IxtDy7;q^*Td$r<`+BcQK!8RMzSqU;YiDIorL&)BZ>D80 zpWb+TI`@qYCAEh4Kdn)+UVr?&_`ZhmU;V5GdU|aiOoD^e|9){blQ)vBc>Y-UQF!`p zz0$&-<=U_CFSXzPcr1gz)!+U= z{!{XHpT3iD`99VApgN-j8m*-7 zdwxW>f6f5Nl(|=3k!7+s=Aw8D0)y4t%Kuu(&K+Pm4;4rI-)C`OitbU;JE)$Ec%`lR z{t@waO(F06I{@ojbZ{4ZFgSNWI~$(TomVn5LT$g2WM1lNDEAT_NDe(nKels&A|2I~ zz_>!Nm*kfqd;KxhEA+#5Zc-fD$*oYSQw(I?c&ND0Mkv-l;8;C~$Vp01*F3b}!W zhF`qDAD4v}FWY6xN22ksUOq~FdU?FdaxV%6j5b)~4_tvKcwcfBAgE-IgzS;Ju(_J! zN53_sxSz_w>Ol#=5mios{(JM%kyXQlIekfBhX?&bAj` z|Mnk0jGuq|$AA1lfBqc;SO4+9QUwJD42^*|19S>tnC(N9@=zWltHad)OqVa>u0IU` zat=V|JfcS*>5eX1*|!2-jEncG_x3~c9Y1_6tmjs26kguNnP>VWD&}6L+|&et`RoG! zcR!=gUxknJ@4W-{kF%H2_vQ%rt_P95*X(*USAdhi`+ock9M5#KH{G91o#E$$mLIbpGzHINS)U!Fn8tSH;VB6 z=gIc==lVB}zMnp5l*K`R`%yT2)U5EW@V6pb{9B>)02Z75GzY=n---sf_VaHbTD$z> z&AAlWzEQUO`KgA{qL77U&$G~XfioRu)e-pVa}ABsjil2i#7ThkmhgLm9EI4ed^>p{H+E*9k(Qa;D>#CR!Q$W8L9rFCw`zWX-~iu zGKwkjzRkn1?04DFbJnFVNC`12;15_iz$)Y&wD=cf|JlOAL*XfV>;4U0;~dDdCiX~{ ztUZ>|YhZ*Lm@7b!L|@eQwT}o|-!F$DdiwV6QYrqPT>z+n$n3DQMfF9JAN~LBU2SvQ zMw0$35bjPUPcS1LAxJu|@cU}gX+ z-m84rt=gEv^z^*H_4GVXc-pH5jV@14j#(8Dr{(HxN8J`vw8b^s#%I@XXoRgd8>6`n zwc7%j%h!0-CSoC_WT;ViymU!hHyq>huJ zrE|Xzdqi~_g6i-!#ErPEL5U>oiX~S^sxTHPs}u+<LA)4C#+JJCPf@l4OH>SO)_zCRZ&`v~qM1AlLK z_don#-pLU>b%`G5?9X^QV0nff&l?ZE&0}^5f`7+f20#ADeyCG6RzCa2r*muxI@8Xf zI42KaCue1YHv%$GL18J@)*@|wh1pwNpF|SB_d^y9X-0 zosI93IA5{0Y7M^|wLQ!pA8SuF2|U>ncp~kyXa^7E!$|+|q&MjUFX6DZNr$4z(9nL9 ze({50;ZR}e#cdhOZ%?2HQ!ZQ^PQnJwMmG{G;L5V zrnOD4YpUwn(WW;wac^wZe51Ch$f-A%?aJHm(yBb4GToOp-It_2-nD~k^5I7RaH%)p zH80$eHGOF=?H|>&hJL_-swVE0t-7vUyEm49pGnBw zUYGq=YwD;SJdzJrdR;!Y16~36_>8Z2#bYYN8$kG89!N~*i01~5RAm^s4&vvfeG3Av z>dJ92Q2*+D+$VlMJgi&lvH6j_4@bXUA6*=u{PwYHI8>jFzmz(DWdAdW#7QZ#OTyLd zu)*J`z(bU1Ysw7*oG-7_%i_qXT#Zdqcz%&%bG3E@lGR{0V0$^$W2t&-wQ6E9b#cyElQ{S$Z{d^kTu$G@hym_@pumLxXf?pw` z{4q5w$HJw@o^kCZE%6@;^ibiixV6_e7R=BIc5(JsuRyra5q$&Oz~EwgTiXv0tL!|( zTSTkpOb%Wd*UKUDugO{X-zVPa!_|p*4bB!`$o_wR;r%kaIvIIK7w6|EN7pCE-t~ot zo*6%#o{Zj~4u5`s?Hz@qlPy8$gP{AM5V5u&SuJG9<--~nt-9=N^!sZ0gmUR*vRZb2 zO+fok!>coA!iRqQkiI6@ia!XABd*X1{9$U{Ga2W;|oFaLYeW^C%EaD&KqqflSE_A}C^j&4Xu;pMOzkzsuTJ zV6%^>u(ROIP50yF^l`joA19M^2^ZY6r;gutA#d3W)O}N5ASP2+AkI?7O3{6@S*Gn% zT9`~fiitH>9q zK5BBqW<*BB?Ad&;*6ZqQ&W}o9i-mN(c{EP4l8xm2;u@HJaHeM+nl-;CI9%RJ|EvR! z;mLZsO529BtSuj`uQXe{;WmnUwAq}=9E-0JIFfh=^ZG8lx!TXd^Hmnj?E9*PvJ*c2 zVDNucyhI8>OfjChhc>^-w7=!kKs5VknAUyK+D8Am4JwgTqoR&Ml}nV6f=2uh!43)F zA17H0fpCsffE6U6gpIDC2P(LJI-WRM!Sbec;F6T7=)Ivl?n@_^zZ7Vc<58mxG%5-x z<)^s<1T$D@dJ9oFSbgR9RJPOrHdr{a7U8v8@Ys?x|5 zqj{jIghFaEs$*K!<3rX1%8pk=1|2z zsyW zuDuT<772`3-t}wDpm#bvMuV7$$j2I6mf(^n#)5J47<_<5>;_DkY}jp8j9iAiZ*J&v zLrYbRU8Xea4PIrx;3!g-j==+Kdxj2I)wok4?vy2M4699AL$_+2vb51yF|2A+*333F zDU0>}GEU0F9ZP;o$Gxfuv4e4SC6FY(z|wv>Jog?7@vQS}@-8Y_-Tz3^NAK}|j8?H% z?Bg_LNkP}GO&K_OTMeXKyc~uIq%+qUr!s==iax)v9N1eYEMHT-|m$lCW9hU{Tij4|HpsS)a?BzIOdJ7~%?7cd*ET7x@(ZV`F^a zTp*8x1#Za@P)R;3_@=xP4qor6PgHn=a&89W=VrP`ZHD_U z1@_FC-lah0!luBw7Xtg0UO+JHp$8NnEZEmv3~@>nyRAJcn~%TvK{1y{ zm5ot;7AIjcEtF^FG)n^QDO#6AJu#zROf+<7S}Z_p?uF-4R5DH!Nx4+*>?tW5ZrJr# zB4FzAIjn#`7=YJXdJ9jmTXy2f5PW16JABO6l6JthFbVs~bFp5>eEG)>L7YFeK}OG7f?&b+{E5v z-?GC)-!EPx2I|W{S!F-X((zxh(lB3t-D>0Sjs8mYw!NKqSc>zOe4Klj_a;1l!{g5b zq2w=Dk!&t6J*|R_{@V zrNf!l-^o9v+|&VGhl$7A+pqW7KPtCoflC**dFX>m$EgNpBngCMfoC-M@flL-MKIg< z!msMqu_)p%XQY&DGQ^Cx40X^BoEeD?88OU*V!KM=MijUux?`1{NX9PrK7DcxxuEAB z@y6}b=OBXrz*CtZ!6y)~GvSA%71&5%G}IovWG0fnR&7bSi{&Wt+^*pVSYz#=A}%+x zhz(yST<$kHJi9k*5J}kqi!80+k0u!tu`p|mA}Kpuk;A2<62#^dBXhu~B*-Pf@~6+x z5>oV>%EbXb1e5Rj^Ws-50q%qsq;(P=;11!~*iBEA#*m^8xqF>AbyJ9{6&i zk?FCqknIS~=Z@c@RNvFRPb$(A-Rr{E;_Kwe|D|s;;#AP1!>|%uAPvl%ql`qM5?9O&x z=Cu`78}nNi!DD?1Kfb6exw|Zgxlpb^m|N*LAg7J$Bv>5_Vu>arELV?c+|f%NTS;X| z=JWt284l_qDW^M5*iah*k8jALnEKs;jp5xuLNQD~|%jzg}CRWVIj@dk}6=a8`9BQ7qXK&|359 zC5e{8AK|MmYRE(xLy#Y_^$X#P;b|ZqG2iaO#*oVlyGP#W=u~bUqzKzgz@`a8IiYzb zJawlWIgXFb0K5$oaki~!Jr1eX>39RJ>OM$D52RjfaFfhr7RVBD8GeZzXT|QOrR{TA zZIxJwXccT+M;jIWo0X`oF4$rdQ5sW_Q_vIdfN)rB?ZRfD@o+_jXf8n_K&K83O;y)I zY$F8;pO?GT5(i7z#}kW8C7;AxBW7x;LQO-mC9$@~Uo1W8)yED@(g8Fc?DpfgN#~}J zk+$R*Y{@k3Qs`6sIq}Ks7=)(DBj8KZ|N$^U_3v&Jrsl?=_6P zXNufy7`bbTyx%bLz8)DiJq_Yk2~!b0uGs4|P+?+A1tol)X`rA=Xey&mW(}iLCQ}^V z#mdxVd>iR-K@BV!O$LHS?}`Ob7NG_ztW>d>4RyI`IxtO7L>%bd#Vu#CzQFDk5opR4 z2MsI=WDuP92M5op2(dKV{T`J>*pW+S?`!qt(EmisG%J1Ezagr#@VM&!u)7;X1NL8P zVftT|aK*%w1Jz3c)evvC8mgRg?S5-3X6N67kp1T^Aken(C6zE9s+J{>$CD^Hn=GDs zirIRdL5i6n#nL)O5^OT>y2unE*>t^5A=%VgP{s5_Qg1SOI4Bn(@$q^+hQ!CVGTYK_ zvMq2BGeX+S^?C_uFAZty<~@>jlUdJ2-w27%*6SH0KC>piv~;rRW^1jUFgvVHA>pYZ zVa3!yl3Z=J*U*z(vGXw`xiTcF*icB4lMQwydU})fdIIT94CxUAj7%}eHD@-h;RZ21 zi_ktb$bxEQt$TOhpX6cd#^w=y{z{8LcY}rZiYPJ}XWL!UJDcn)+drrrrbUSHnzji`5aK8qIccm_F4UOy- zAMnAe34A20T4|)M5~KoC2+0Mhj*7t6AOgtTu;SSE?2b2xiaJ3Iu0{IT>IUFI+RqX1 zHRQK}${`1#2jI-IEVRI-T@)Cz4KG3Zxt^^=) zWw|BLdcZ3HSQ@Mq^_&7b+GDz$)-&R;8t@lF+8+!C5h9o#MgqtbH{3n>S0Pw|nPb^F zP4D7FS??-DaE{X#3oQ?$VSp zE**`;v+$s(s36?h@K*JbMhL_^FX1)?J3rTlb zd!G`~lvSUPjgbgY#&BRx!~AkA0>^@gLoN+3?HLdP1a~rsI(Mu#9>-7ictSvrm#uD+ z$g$nv;d-oTlI=MxJ}lG_qw9>()no8p;;ITMQHeCFDU>QziTrb2O%kypx*P=~+U8N# zLR&bUwuB5ja+B|}@r!OigJl~RtxbbPQ_41_f!Jca;VJ=L9?&(-378xC(y&iX!t39z^SET_W;&q=Buls60dv!1p9cTd?RqWValIG?+o3a9;jJV_H z8@qvXJ<*@{W_LQ_D@UkY@%@JPM8e~4SKm+?7R@^PZZLU?5!#fHUUY$`;=GlcO2bgt zKo*?8D<=RBmm@LFHrxEC*|I>rtQ8g{H@=Z~5}777h|IyDg!1W(uHLYTVT*hkUF~Tw znvi#{JyNwtb=ogRSM}QUnmbPu1nhj+FZthP<=G&El=^t+it1=k(JYBg&_xv_Q9}iB z9igfttkHpydt6oDg`aBo-gSHS{&T@k-?2^7b(-e)n_Ph0ut+&XDr{{9V3pi`11I42 zHhDk?ZtU^}o&4STy%z#RMy0fZv^-;BEIinF$YR5!n1aVJ8O@VuIb0U@JL(iEhwuB3gDl znAv|>rs;gY>rL+Cg*QzjzKYzlRfJ_IUESS#^JtzfpU`+!)FBoIL4`_nzb3yX|IqRA zWZGp+_A!f;%q+%tkvG0$WkL;SDY}w~6%By9ZxBEHHojZ2{hap@k2#TmCnyoUe-qLy z`iCsSTxkOL(QJYFX9a;pi~XGADhdgHIKK2akrHNnu}p7Rj-S|427f-L%RhL@R-$AI z(yhShF7r1Qcr3hhSh5upnIV$RGP+?EfcZm&yN=oQF z`i)fU(`M17mB9!Y__jNvcc&jtt}iaG-?!`VV3Y~7$S$8Z`{le`A@)S}PK|U#k~UXR zUf#{3&E}BkpE`TcHu={a@~=7M8_XfmKV$aTTxnkd@1CeQp0?$)GRvHv(D}tay_~cK zMs?%ag~j=1^NO*Qb;y%6FXa?d?%gZcla`9x-gZ^YAplh>h2W5tWjT6UI4vZ(8Bq(U zo1$;zF@!?hx~A1*6}?v8T*|6EWtk+^`Qo+8plhS1f!JDCY0x%GWeiBgbxtonp8P*H z87;h_*{DFXrlWEG1ociPsfD@Yn11!AhBfhJR>bu_WjN}+&`G5dr9iz)iZ0q1DYEpaE#6>OU21F7SA@QWlgnF!_zFelaEdPk;GvFcY(*tnw6@9`Qp-?71W^N5qDo|ca@f^}W z@foNJ);6Gj1_j%DCR7%Jmha*`pj9!T1`T}HKgYfCf`gW@Tz>aEYtuec#2O_Ztn%B} z{}uQ<9Q6Lf)$k~tFVZAJXty94_-Z@-=I>ZqWykM;T0gK8hL4diJR@Lh_si(khgZ~k zz?5~*8Jom>fO3dCNHD1mEUtSvhaM+$hZk=$x%P7_wuaUbmA1u&K2$L)l2uPykv2C_CBQL8MP_ngX&MpCg>7g0is3`6rc1~V3HOznw*O>3M_?lHwy5Wx|I!D(<__PbaPmpsc^A(cW_aj3s@?N7NnMQ2i zlChD^27@vkh3)>u&F|49@B9%x0j6y^XR3VLSoG41Vhf~ zb`ZYMr5wKKc*CxZKzMqp?FHc82rk&zL^BODfVTtJDP<O^4lZ$ z@;Hk)YX^d)gui`EXF#Fq_aqt|Sb|6^8l+F-d7KY`uLRV=F7Irdtj7FpIML(=iIp1Y zvR!Csw8#%44iwc$nC{2eUS}HTxEoiNdLpBUfWrXEuVJG?3%}_$r>GBgIw2}Ob*hZa zQzdI}I!|ieA}~LJ9iuD``n8dqSDoTy)(U?mhrIMgk#Xepsy#~Ks3Ky>8&-Ra!godH zi(j%jawVKaggjoe+NHUBODWrR%XSM{%8P4{MEQ1$h9?BJ_SlpdSH>>L-|P_!t~VKn zaH84c5rQ=t7tD}+G#_ybDOEr>+ntJZsfP%n_P5Auh)C@K3rrAXLPnaf){zsyf2B?qC<_ztHUDq z^lzifp_ati^_#!#?!7z}9^aABJiCmRS7WW~ec%0ot)2Q-hwvy5x_^TyolWrU(EUrC s-?w~@-%$bB$G!ocOp4^u`|$kyA zrLQ}|4=f8)PPvG`iXIlr)jF}?@;p!Y5sRGiQ+SE@B+D?n&dvXIMyiscOUyXKUy!2Y zLLw%w^8%g2Jtn)o45dJu{x0!GNGoFEFpFFg?Y7tv7Dt`bNr|$Iu${Z+VHAA*Z?P)T4ucVxM1*0{>NcnN zoTy)+<_UH=6fEiVc--5odyF!;H)+um$>iQ>e#-?M4YyoMUc?orBujeyem_)_AnFwtmhl>t4Cz#Md z^0B`_^k8-fw+FBdtF5I3%E&=UQJ$?GH}~+-dckU`co0@eaY&Vuh7zBp3HyqC#PR%5 z;KC60`9~CiM}Us@Ci}D<(f50q=g0D9BPivgrsg=-KhF3n~&E4RzT@8n)4Uxq2 zoRZrdkdrf<_y}R2;dA`3NhB}hhX{@sO|I0(CUO>$da~A(%|%m+toHoW+ijDG-nQoJ z`|H@lM-kY+9$0fZsX$xT#@xeNNh`4S!cgEp4n*LrMvN3lzuj*A#KEN?H|s>Rr~<-r z!a$7cyH|Y41uAeAc#hT(RJG57k6QV{wO)UzzY>m5UB_a_d*#DpKiUbbln%$Sd{61G zSW2)|RXder?0b(}@V;n#Yl?{ysdPMhF<&d!CagS2*hE>#Aw}IO|3~n(Tk`mQnxY^g^X?^oXA`+e?lt+*7GfYBub(n;JKr${%8TfQ+f{>rg z(n+8)R==#GzBiRsd)>)Ac*|*~vO?>K=W|G*&|{lBDopcO{j!zEf&i1YG|_K;3Epo4 zrS!fvnljW`K!0|4J5|r&ZBuD4@1iXLfP-wj;zL_H`cG9M6>0|Yn5@INDd1-(N{wd5 zL{-tsdELt&)un>G`va^8bNLHDJ!bLWzBM7RU62W#U(#;MJ7 zLglI}T`dLY@`H*q50+^?WD>vRTohBcJD0O)ZmK3XxrL&#!r)FN5=i1^LxD1lA_MI< zdNEe+t^r-}|J8Nv60UnRRolTYt4Qg%)l#ruxdyUD*Re@xLZ`j(BhL_Zi=IksF2-$O zcL%nwb7>>_O{_bzpz)}BL|e;s-#)d0)Av0&1LKHyBXX_`&iUtz9G~?GT@~gP7DsNx zo_U=54X|sa(n(iI>7d!wst3aVBKr{%w@`k7@}~f@s~lao?}iJ%$xDg9VTWRi#mmHQ ziK^-ZUf0`)1O$QQt~5YkoM%U3=m#6(=^m%A;9D=V|3o&wDc+DOChQ1-lD#e6Cr-6e z`p&fg%I@`nj;f>`9@E#pZ2+*2IvoR)Lla~I&W)Y6i&{lGq_vB=v0_hylX^DI3+xfX z_E-=(IQH4jJME#)&0*c}mNDnNW#qwTD}0YO%Qhc*leQi-K$-_}+B7dO6q_bJFz9W} z%^=7wFU&?IvQ5!uZqQW;jYmBc9Yq;!atX?A5ijIEomzT1zc*JHZF|{tGEZ^Q62HM_ zw11u3+}bn~V?&PX%SvA?PsMxX@8C#36XwSeO@FU$E!UrU>luv797I$Vh*FTigU1A> z(A6Dre_CN$NXj)>2@75Q#!c-M28YPgAUrY{Jn9PN@zk3oIZz}PyY20Keb@Y*b?mz- z)mW!*&#nB$*4J=bsdT%bMT34pw+o#ZQ0L^qZ7`j!k}!9QMi>Qo!!70Mt9|^V)4x1+ zh(+7gxY2-EjB*gHZbbhw__BShOV^#u=+A@F)ApG0rWb`tzn6p+HyZ@Qg`}Gv4CD+J z^JNvN_af1a5SpG-^4*SN&>b(wNqwq8_cgB$p{85!J;2`dweZQH`yWyS_%a~&6k+`2 zA*P##q<6?m&MTpRBw<B|j+NRCi^$jHEbcwb}>G|geG;z@H7mq=%&{0sH#8o z&Jc_EHkw^kksoiYHc^Imv+MO;0nb5!8p#AzsJCmNaXek`q>86IkODw svIhPZ2P#QP@yo0^vRbu+IrmnhZ#9Rt!nb(q5pG=n1dfK}+7KH60516R*Z=?k diff --git a/ESP32/data/www/utils-min.js.gz b/ESP32/data/www/utils-min.js.gz deleted file mode 100644 index 42bd3248ad6009940ab151d81e2de618a0da41d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1266 zcmV!=zwby%)|(ZlShsz$d8Y54=eaxb_&Z8fW;<&(TD(Xg`}vw<5&LEMd_MmH zq95sl{V8BUwj9djcBlCYlHhf1c^)v7D=-^O=1hR%Tl!pc%U9(~YiV_7q?pb3A6erg z1cpqrIF5O=!$OvE>;4u#Ui`ZF3v!GCAF(6< z_Z*!hdxmDYRPaW@3N(Hw%)PF16Ya?3jAub1g#aC-`s^$`EYHz$T}qTj2Mpp|@v^*< zCC0hY$fYjBV8tKK3y{kKyPKmgU%K155R*G2rZ-A`m*E9obpK;vW$qy4Q8M&2FjdsZ z+OWJNaMq|lWGNR?N!-5LzI$DlBGus?w=g|>+hqSPySL@?_GWBlxl zhxV~(@2S|@cvLmvWX!VJ9Wj;7Q}vUL73d`vse1M-nnZG|K6b&-@-aM@POGm2)%nXe zK$ctJ2)`J+`pw*v#~syAi8E}2S37D~VIbsP5TzDyWAzM&m4xIah!{|*Vk|{Q5V0HN z5|nDGR zb`4p8FKYo46iwP%n+gQ(lfFi$SYnVW%pV@c+v@!!RMSi}Dl>$ZLVbLMqL9wE+5 zLw!WCrhc5Cr=CwxFT3_$H?&b3?s<<{Nm37~%Aq=*yR5mFEqBr3p6`c8!-#y_kjIIS zYM~{5yc`Bv_0Oa#WW#zya&)(-VF`HFs%9-()~3T+9#~)T1*i_?hOC|-JGx?9vJaB zf)LHL(U8vWrNAPY(c}61G)P^f6LT%oCy?mTAo>=#pi9o-=xU>f28=v_=}Wx|z#pT^ zPg3O@H0f-`?NaJwHcw}3aw7H8eXkTaHEkbeib1AvSPPVTgYMz(z)`LjMr>(HEW94o z^PSu#cu4%U5nT1M@4Lgja;IglYg)(ja({_vB@LcHW@Tsy3s9mLMt2ALw0bvpFR@<9 cH55-IjK>v~vE31OQ`0{E8=Pxj>ADdB0In!`pa1{> diff --git a/ESP32/dataEdit/www/index.html b/ESP32/dataEdit/www/index.html index 9b666bf..60bdb4e 100644 --- a/ESP32/dataEdit/www/index.html +++ b/ESP32/dataEdit/www/index.html @@ -7,18 +7,9 @@ TCode ESP32 - - - - - - - - - - - - + + + \n" -// "\n" -// "\n"); -// } - - static void handleFavicon(HTTPRequest * req, HTTPResponse * res) { - // // Set Content-Type - // res->setHeader("Content-Type", "image/vnd.microsoft.icon"); - // // Write data from header file - // res->write(FAVICON_DATA, FAVICON_LENGTH); - } - - -private: - - static const char* Tags::Https; - // Create an SSL certificate object from the files included above - SSLCert cert = SSLCert( - example_crt_DER, example_crt_DER_len, - example_key_DER, example_key_DER_len - ); - bool m_connected = false; - // Create an SSL-enabled server that uses the certificate - // The contstructor takes some more parameters, but we go for default values here. - HTTPSServer secureServer = HTTPSServer(&cert); - - static const char contentTypes[7][6][32]; - - static void beginResponse(HTTPResponse * res, int code, const char* contentType, const char* message) { - res->setStatusCode(code); - res->setHeader("Content-Type", contentType); - res->setHeader("Content-Length", httpsserver::intToString(strlen(message))); - //res->setStatusText(message); - res->println(message); - } - - void setupHandlers(WebSocketBase* webSocketHandler, bool apMode) { - - // For every resource available on the server, we need to create a ResourceNode - // The ResourceNode links URL and HTTP method to a handler function - ResourceNode * nodeRoot = new ResourceNode("/", "GET", &handleSPIFFS); - ResourceNode * nodeFavicon = new ResourceNode("/favicon.ico", "GET", &handleFavicon); - ResourceNode * spiffsNode = new ResourceNode("", "", &handleSPIFFS); - - secureServer.registerNode(nodeRoot); - secureServer.setDefaultNode(spiffsNode); - secureServer.registerNode(nodeFavicon); - - secureServer.registerNode(new ResourceNode("/userSettings", "GET", [](HTTPRequest * req, HTTPResponse * res) - { - LogHandler::verbose(Tags::Https, "Get userSettings"); - SettingsHandler::printFree(); - ////req->send(SPIFFS, "/userSettings.json"); - // DynamicJsonDocument doc(SettingsHandler::deserialize); - // File file = SPIFFS.open(SettingsHandler::userSettingsFilePath, "r"); - // DeserializationError error = deserializeJson(doc, file); - char settings[40000]; - SettingsHandler::serialize(settings); - if (strlen(settings) == 0) { - //AsyncWebServerResponse *response = req->beginResponse(504, "application/text", "Error getting user settings"); - HTTPSHandler::beginResponse(res, 504, "application/text", "Error getting user settings"); - return; - } - // if(strcmp(doc["wifiPass"], SettingsHandler::defaultWifiPass) != 0 ) - // doc["wifiPass"] = "Too bad haxor!";// Do not send password if its not default - - // doc["lastRebootReason"] = SettingsHandler::lastRebootReason; - // String output; - // serializeJson(doc, output); - //AsyncWebServerResponse *response = req->beginResponse(200, "application/json", settings); - HTTPSHandler::beginResponse(res, 200, "application/json", settings); - })); - secureServer.registerNode(new ResourceNode("/systemInfo", "GET", [](HTTPRequest * req, HTTPResponse * res) - { - LogHandler::verbose(Tags::Https, "systemInfo"); - SettingsHandler::printFree(); - char systemInfo[1024]; - SettingsHandler::getSystemInfo(systemInfo); - if (strlen(systemInfo) == 0) { - //AsyncWebServerResponse *response = req->beginResponse(504, "application/text", "Error getting user settings"); - HTTPSHandler::beginResponse(res, 504, "application/text", "Error getting user settings"); - return; - } - //AsyncWebServerResponse *response = req->beginResponse(200, "application/json", systemInfo); - HTTPSHandler::beginResponse(res, 200, "application/json", systemInfo); - })); - - // secureServer.registerNode(new ResourceNode("/log", "GET", [](HTTPRequest * req, HTTPResponse * res) - // { - // Serial.println("Get log..."); - // req->send(SPIFFS, SettingsHandler::logPath); - // })); - - // secureServer.registerNode(new ResourceNode("/connectWifi", "POST", [](HTTPRequest * req, HTTPResponse * res) - // { - // WifiHandler wifi; - // const size_t capacity = JSON_OBJECT_SIZE(2); - // DynamicJsonDocument doc(capacity); - // if (wifi.connect(SettingsHandler::ssid, SettingsHandler::wifiPass)) - // { - - // doc["connected"] = true; - // doc["IPAddress"] = wifi.ip().toString(); - // } - // else - // { - // doc["connected"] = false; - // doc["IPAddress"] = "0.0.0.0"; - - // } - // String output; - // serializeJson(doc, output); - // AsyncWebServerResponse *response = req->beginResponse(200, "application/json", output); - // req->send(response); - // })); - - // secureServer.registerNode(new ResourceNode("/toggleContinousTwist", "POST", [](HTTPRequest * req, HTTPResponse * res) - // { - // SettingsHandler::continuousTwist = !SettingsHandler::continuousTwist; - // if (SettingsHandler::save()) - // { - // char returnJson[45]; - // sprintf(returnJson, "{\"msg\":\"done\", \"continousTwist\":%s }", SettingsHandler::continuousTwist ? "true" : "false"); - // AsyncWebServerResponse *response = req->beginResponse(200, "application/json", returnJson); - // req->send(response); - // } - // else - // { - // AsyncWebServerResponse *response = req->beginResponse(200, "application/json", "{\"msg\":\"Error saving settings\"}"); - // req->send(response); - // } - // }); - - // secureServer.registerNode(new ResourceNode("^\\/sensor\\/([0-9]+)$", "GET", [] (HTTPRequest * req, HTTPResponse * res) - // { - // String sensorId = req->pathArg(0); - // })); - - secureServer.registerNode(new ResourceNode("/restart", "POST", [](HTTPRequest * req, HTTPResponse * res) - { - LogHandler::verbose(Tags::Https, "restart"); - SettingsHandler::printFree(); - //if(apMode) { - //req->send(200, "text/plain",String("Restarting device, wait about 10-20 seconds and navigate to ") + (SettingsHandler::hostname) + ".local or the network IP address in your browser address bar."); - //} - String message = "{\"msg\":\"restarting\",\"apMode\":false}"; - //message += apMode ? "true}" : "false}"; - //AsyncWebServerResponse *response = req->beginResponse(200, "application/json", message); - HTTPSHandler::beginResponse(res, 200, "application/json", message.c_str()); - delay(2000); - //webSocketHandler->closeAll(); - SystemCommandHandler::restart(); - })); - - secureServer.registerNode(new ResourceNode("/default", "POST", [](HTTPRequest * req, HTTPResponse * res) - { - Serial.println("Settings default"); - SettingsHandler::defaultAll(); - })); - secureServer.registerNode(new ResourceNode("/settings", "POST", [](HTTPRequest * req, HTTPResponse * res) - { - LogHandler::verbose(Tags::Https, "Puot settings"); - SettingsHandler::printFree(); - const int capacity = SettingsHandler::getDeserializeSize(); - DynamicJsonDocument doc(capacity); - - // Create buffer to read request - char buffer[capacity + 1]; - memset(buffer, 0, capacity+1); - - // Try to read request into buffer - size_t idx = 0; - while (!req->requestComplete() && idx < capacity) { - idx += req->readChars(buffer + idx, capacity-idx); - } - // If the request is still not read completely, we cannot process it. - if (!req->requestComplete()) { - // res->setStatusCode(413); - // res->setStatusText("Request entity too large"); - // res->println("413 Request entity too large"); - HTTPSHandler::beginResponse(res, 413, "application/text", "413 Request entity too large"); - // Clean up - return; - } - - DeserializationError error = deserializeJson(doc, buffer); - if (error) - { - HTTPSHandler::beginResponse(res, 504, "application/text", "Error deserializing settings json"); - return; - } - - JsonObject reqObj = doc.as(); - if(SettingsHandler::update(reqObj)) - { - if (SettingsHandler::save()) - { - // AsyncWebServerResponse *response = req->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); - HTTPSHandler::beginResponse(res, 200, "application/json", "{\"msg\":\"done\"}"); - } - else - { - // AsyncWebServerResponse *response = req->beginResponse(200, "application/json", "{\"msg\":\"Error saving settings\"}"); - HTTPSHandler::beginResponse(res, 200, "application/json", "{\"msg\":\"Error saving settings\"}"); - } - } - else - { - // AsyncWebServerResponse *response = req->beginResponse(400, "application/json", "{\"msg\":\"Could not parse JSON\"}"); - HTTPSHandler::beginResponse(res, 400, "application/json", "{\"msg\":\"Could not parse JSON\"}"); - } - })); - - // AsyncCallbackJsonWebHandler* settingsUpdateHandler = new AsyncCallbackJsonWebHandler("/settings", [](HTTPRequest * req, HTTPResponse * res, JsonVariant &json) - // { - // Serial.println("API save settings..."); - // JsonObject jsonObj = json.as(); - // if(SettingsHandler::update(jsonObj)) - // { - // if (SettingsHandler::save()) - // { - // AsyncWebServerResponse *response = req->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); - // req->send(response); - // } - // else - // { - // AsyncWebServerResponse *response = req->beginResponse(200, "application/json", "{\"msg\":\"Error saving settings\"}"); - // req->send(response); - // } - // } - // else - // { - // AsyncWebServerResponse *response = req->beginResponse(400, "application/json", "{\"msg\":\"Could not parse JSON\"}"); - // req->send(response); - // } - // }, 6000U );//Bad req? increase the size. - } - /** - * This handler function will try to load the requested resource from SPIFFS's /public folder. - * - * If the method is not GET, it will throw 405, if the file is not found, it will throw 404. - */ - static void handleSPIFFS(HTTPRequest * req, HTTPResponse * res) { - LogHandler::verbose(Tags::Https, "Spiffs: %s", req->getRequestString().c_str()); - SettingsHandler::printFree(); - // We only handle GET here - if (req->getMethod() == "GET") { - // Redirect / to /index.html - std::string reqFile = req->getRequestString()=="/" ? "/index-min.html" : req->getRequestString(); - LogHandler::verbose(Tags::Https, "Get File: %s", reqFile.c_str()); - - // Try to open the file - std::string filename = std::string("/www") + reqFile; - - // Check if the file exists - if (!SPIFFS.exists(filename.c_str())) { - LogHandler::error(Tags::Https, "Spiffs file not found: %s", filename.c_str()); - HTTPSHandler::beginResponse(res, 404, "application/text", filename.c_str()); - return; - } - - File file = SPIFFS.open(filename.c_str()); - - // Set length - res->setHeader("Content-Length", httpsserver::intToString(file.size())); - - // Content-Type is guessed using the definition of the contentTypes-table defined above - int cTypeIdx = 0; - do { - if(reqFile.rfind(contentTypes[cTypeIdx][0])!=std::string::npos) { - res->setHeader("Content-Type", contentTypes[cTypeIdx][1]); - break; - } - cTypeIdx+=1; - } while(strlen(contentTypes[cTypeIdx][0])>0); - - // Read the file and write it to the response - uint8_t buffer[256]; - size_t length = 0; - do { - length = file.read(buffer, 256); - res->write(buffer, length); - } while (length > 0); - - file.close(); - } else { - // If there's any body, discard it - req->discardRequestBody(); - // Send "405 Method not allowed" as response - res->setStatusCode(405); - res->setStatusText("Method not allowed"); - res->println("405 Method not allowed"); - } - } -}; -const char* HTTPSHandler::Tags::Https = Tags::HTTPSHandler; -const char HTTPSHandler::contentTypes[7][6][32] = { - {".html", "text/html"}, - {".css", "text/css"}, - {".js", "application/javascript"}, - {".json", "application/json"}, - {".png", "image/png"}, - {".jpg", "image/jpg"}, - {"", ""} - }; diff --git a/ESP32/src/HTTP/SecureWebSocketClient.hpp b/ESP32/src/HTTP/SecureWebSocketClient.hpp deleted file mode 100644 index 9d7b17b..0000000 --- a/ESP32/src/HTTP/SecureWebSocketClient.hpp +++ /dev/null @@ -1,79 +0,0 @@ -#pragma once - -#include -#include -#include "LogHandler.h" -#include "logging/TagHandler.h" - -// The HTTPS Server comes in a separate namespace. For easier use, include it here. -using namespace httpsserver; - -#define MAX_CLIENTS 4 - -using SECURE_WEBSOCKET_MESSAGE_FUNCTION_PTR_T = void (*)(const char* message); - -SECURE_WEBSOCKET_MESSAGE_FUNCTION_PTR_T secure_websocket_message_callback = 0; - -void setSecureWebSocketMessageCallback(SECURE_WEBSOCKET_MESSAGE_FUNCTION_PTR_T f) -{ - if (f == nullptr) { - secure_websocket_message_callback = 0; - } else { - secure_websocket_message_callback = f; - } -} - -class SecureWebSocketClient : public WebsocketHandler { -public: - static WebsocketHandler * create(); - void onMessage(WebsocketInputStreambuf * inbuf) override; - void onClose() override; -private: - static const char* Tags::SecureWebSocketClient; -}; - -const char* SecureWebSocketClient::Tags::SecureWebSocketClient = Tags::SecureWebsocketClient; -SecureWebSocketClient* activeClients[MAX_CLIENTS]; - -WebsocketHandler * SecureWebSocketClient::create() { - LogHandler::debug(Tags::SecureWebSocketClient, "Creating new chat client!"); - SettingsHandler::printFree(); - SecureWebSocketClient* handler = new SecureWebSocketClient(); - for(int i = 0; i < MAX_CLIENTS; i++) { - if (activeClients[i] == nullptr) { - activeClients[i] = handler; - break; - } - } - SettingsHandler::printFree(); - return handler; -} - -void SecureWebSocketClient::onClose() { - SettingsHandler::printFree(); - LogHandler::info(Tags::SecureWebSocketClient, "Close wss client!"); - for(int i = 0; i < MAX_CLIENTS; i++) { - if (activeClients[i] == this) { - LogHandler::info(Tags::SecureWebSocketClient, "Delete client!"); - activeClients[i] = nullptr; - SettingsHandler::printFree(); - } - } -} - -void SecureWebSocketClient::onMessage(WebsocketInputStreambuf * inbuf) { - // Get the input message - std::ostringstream ss; - ss << inbuf; - const char* msg = ss.str().c_str(); - //LogHandler::verbose(Tags::SecureWebSocketClient, "Secure message recieved: %s", msg); - if(secure_websocket_message_callback) { - secure_websocket_message_callback(msg); - // // Send it back to every client - // for(int i = 0; i < MAX_CLIENTS; i++) { - // if (activeClients[i] != nullptr) { - // activeClients[i]->send(msg, SEND_TYPE_TEXT); - // } - // } - } -} diff --git a/ESP32/src/HTTP/SecureWebSocketHandler.hpp b/ESP32/src/HTTP/SecureWebSocketHandler.hpp deleted file mode 100644 index 09fb4dc..0000000 --- a/ESP32/src/HTTP/SecureWebSocketHandler.hpp +++ /dev/null @@ -1,99 +0,0 @@ - -#pragma once - -#include -#include -#include "WebSocketBase.h" -#include "settings/SettingsHandler.h" -#include "logging/LogHandler.h" -#include "logging/TagHandler.h" -#include "sensors/BatteryHandler.h" -#include "SecureWebSocketClient.hpp" - - -// As websockets are more complex, they need a custom class that is derived from WebsocketHandler -class SecureWebSocketHandler : public WebSocketBase { -public: - void setup(HTTPSServer& httpsServer) { - LogHandler::info(Tags::SecureWebSocketServer, "Setting up secure webSocket"); - tCodeInQueue = xQueueCreate(5, sizeof(char[255])); - if (!tCodeInQueue || tCodeInQueue == NULL) { - LogHandler::error(Tags::SecureWebSocketServer, "Error creating the tcode queue"); - } - // Initialize the slots - for (int i = 0; i < MAX_CLIENTS; i++) activeClients[i] = nullptr; - instanceRef = this; - setSecureWebSocketMessageCallback(onMessage); - isInitialized = true; - httpsServer.registerNode(new WebsocketNode("/ws", &SecureWebSocketClient::create)); - LogHandler::info(Tags::SecureWebSocketServer, "Ready"); - } - - static void onMessage(const char* message) { - instance()->processWebSocketTextMessage(message); - } - - void setMessageCallback(SECURE_WEBSOCKET_MESSAGE_FUNCTION_PTR_T f) // Sets the callback function used by TCode - { - if (f == nullptr) { - secure_websocket_message_callback = 0; - } - else { - secure_websocket_message_callback = f; - } - } - - void CommandCallback(const char* in) override - { //This overwrites the callback for message return - if (isInitialized && hasClients()) - sendCommand(in); - } - - void sendCommand(const char* command, const char* message = 0) override - { - if (isInitialized && command_mtx.try_lock()) { - std::lock_guard lck(command_mtx, std::adopt_lock); - m_lastSend = millis(); - - char commandJson[255]; - compileCommand(commandJson, command, message); - // if(client) - // send(commandJson, SEND_TYPE_TEXT); - // else - sendText(commandJson); - } - } - - void closeAll() override - { - for (int i = 0; i < MAX_CLIENTS; i++) { - activeClients[i]->close(); - } - } -private: - static const char* Tags::SecureWebSocketServer; - static int m_lastSend; - static SecureWebSocketHandler* instanceRef; - - void sendText(const char* message) { - for (int i = 0; i < MAX_CLIENTS; i++) { - if (activeClients[i] != nullptr) { - activeClients[i]->send(message, activeClients[i]->SEND_TYPE_TEXT); - } - } - } - bool hasClients() { - for (int i = 0; i < MAX_CLIENTS; i++) { - if (activeClients[i] != nullptr) - return true; - } - return false; - } - - static SecureWebSocketHandler* instance() { - return instanceRef; - } -}; -const char* SecureWebSocketHandler::Tags::SecureWebSocketServer = Tags::SecureWebsocketsHandler; -SecureWebSocketHandler* SecureWebSocketHandler::instanceRef; -int SecureWebSocketHandler::m_lastSend = 0; diff --git a/ESP32/src/HTTP/WebSocketBase.h b/ESP32/src/HTTP/WebSocketBase.h deleted file mode 100644 index 4a86a61..0000000 --- a/ESP32/src/HTTP/WebSocketBase.h +++ /dev/null @@ -1,165 +0,0 @@ -#pragma once - -#include -#include -// #include "LogHandler.h" -#include "sensors/BatteryHandler.h" -#include "TCode/MotorHandler.h" -#include "PowerHandler.h" -#include "settings/SettingsHandler.h" -#include "TCodeInterface.h" - -class WebSocketBase : public TCodeInterface { -public: - virtual void sendCommand(const char* command, const char* message = 0) = 0; - virtual void closeAll() = 0; - - size_t available() override - { - return m_TCodeQueue && uxQueueMessagesWaiting(m_TCodeQueue); - } - - size_t read(char* buf) override - { - if (!m_TCodeQueue || m_TCodeQueue == NULL) - { - if (millis() >= lastMessage + messageLimit) { - lastMessage = millis(); - LogHandler::error(Tags::WebSocketServer, "Websocket TCode queue was null"); - } - return 0; - } - if (!xQueueReceive(m_TCodeQueue, buf, 0)) - { - buf[0] = { 0 }; - return 0; - } - //tcode->toCharArray(webSocketData, tcode->length() + 1); - // Serial.print("Top tcode: "); - // Serial.println(webSocketData); - return strnlen(buf, MAX_COMMAND); - } - -protected: - bool isInitialized = false; - QueueHandle_t m_TCodeQueue; - std::mutex command_mtx; - - void compileCommand(char* buf, size_t bufSize, const char* command, const char* message = 0) { - if (!buf || bufSize == 0 || !command) { - return; - } - if (LogHandler::getLogLevel() == LogLevel::DEBUG) { - if (message) - Serial.printf("Sending WS commands: %s, Message: %s\n", command, message); - else - Serial.printf("Sending WS commands: %s\n", command); - } - int written = 0; - if (!message) - written = snprintf(buf, bufSize, "{ \"command\": \"%s\" }", command); - else if (strpbrk(message, "{") != nullptr) - written = snprintf(buf, bufSize, "{ \"command\": \"%s\" , \"message\": %s }", command, message); - else - written = snprintf(buf, bufSize, "{ \"command\": \"%s\" , \"message\": \"%s\" }", command, message); - - if (written < 0 || static_cast(written) >= bufSize) { - LogHandler::warning(Tags::WebSocketServer, "WebSocket payload truncated for command '%s'", command); - buf[bufSize - 1] = '\0'; - } - } - void processWebSocketTextMessage(const char* msg) - { - if (strpbrk(msg, "{") == nullptr) - { - LogHandler::verbose(Tags::WebSocketServer, "Websocket tcode in: %s", msg); - extern void feedMotorCommand(const char* cmd, size_t len); - feedMotorCommand(msg, strlen(msg)); - } - else - { - JsonDocument doc; //255 - DeserializationError error = deserializeJson(doc, msg); - if (error) - { - LogHandler::error(Tags::WebSocketServer, "Failed to read websocket json"); - return; - } - JsonObject jsonObj = doc.as(); - - if (!jsonObj["command"].isNull()) - { - String command = jsonObj["command"].as(); - String message = jsonObj["message"].as(); - if (command == "setBatteryFull") { - BatteryHandler::setBatteryToFull(); - } - else if (command == "identifyServo") { - extern MotorHandler* motorHandler; - extern PowerHandler* powerHandler; - bool shouldRestoreServoPower = false; - if (powerHandler && !powerHandler->isServoVoltageEnabled()) { - powerHandler->setServoVoltageEnabled(true); - shouldRestoreServoPower = true; - } - - if (motorHandler) - motorHandler->identifyServo(message.c_str()); - - if (shouldRestoreServoPower && powerHandler) { - struct RestoreServoPowerParams { - PowerHandler* power; - }; - auto* params = new RestoreServoPowerParams{ powerHandler }; - xTaskCreate([](void* arg) { - auto* p = static_cast(arg); - vTaskDelay(pdMS_TO_TICKS(2600)); - p->power->setServoVoltageEnabled(false); - delete p; - vTaskDelete(nullptr); - }, "idPwrR", 2048, params, 1, nullptr); - } - } - else if (command == "setServoVoltageEnabled") { - extern PowerHandler* powerHandler; - if (powerHandler) { - bool enabled = (message == "true"); - powerHandler->setServoVoltageEnabled(enabled); - // Persist the state to settings - SettingsFactory* settingsFactory = SettingsFactory::getInstance(); - if (settingsFactory) { - settingsFactory->setValue(SERVO_VOLTAGE_ENABLE_STATE, enabled); - } - } - } - // String* message = jsonObj["message"]; - // Serial.print("Recieved websocket tcode message: "); - // Serial.println(message->c_str()); - // if(m_TCodeQueue == NULL)return; - // xQueueSend(m_TCodeQueue, &message, 0); - } - // else - // { - // LogHandler::verbose(Tags::WebSocketServer, "Websocket tcode in JSON: %s", msg); - // char tcode[MAX_COMMAND]; - // SettingsHandler::processTCodeJson(tcode, msg); - // // Serial.print("tcode JSON converted:"); - // // Serial.println(tcode); - // xQueueSend(m_TCodeQueue, tcode, 0); - // } - } - } - -private: - // std::mutex serial_mtx; - // static QueueHandle_t debugInQueue; - // static TaskHandle_t* emptyQueueHandle; - // static bool emptyQueueRunning; - int messageLimit = 5000; - unsigned long lastMessage = millis(); -}; - - -// bool WebSocketBase::emptyQueueRunning = false; -// QueueHandle_t WebSocketBase::debugInQueue; -// TaskHandle_t* WebSocketBase::emptyQueueHandle = NULL; \ No newline at end of file diff --git a/ESP32/src/HTTP/cert.h b/ESP32/src/HTTP/cert.h deleted file mode 100644 index 9ac2dc4..0000000 --- a/ESP32/src/HTTP/cert.h +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef CERT_H_ -#define CERT_H_ -unsigned char example_crt_DER[] = { - 0x30, 0x82, 0x02, 0x23, 0x30, 0x82, 0x01, 0x8c, 0x02, 0x01, 0x02, 0x30, - 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, - 0x05, 0x00, 0x30, 0x5e, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, - 0x06, 0x13, 0x02, 0x43, 0x49, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, - 0x04, 0x08, 0x0c, 0x02, 0x53, 0x54, 0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, - 0x55, 0x04, 0x07, 0x0c, 0x07, 0x43, 0x4f, 0x75, 0x6e, 0x74, 0x72, 0x79, - 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x09, 0x4d, - 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x31, 0x1c, 0x30, 0x1a, - 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x13, 0x74, 0x63, 0x6f, 0x64, 0x65, - 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x6c, 0x6f, 0x63, - 0x61, 0x6c, 0x30, 0x1e, 0x17, 0x0d, 0x32, 0x33, 0x30, 0x35, 0x31, 0x32, - 0x32, 0x31, 0x33, 0x34, 0x33, 0x39, 0x5a, 0x17, 0x0d, 0x33, 0x33, 0x30, - 0x35, 0x30, 0x39, 0x32, 0x31, 0x33, 0x34, 0x33, 0x39, 0x5a, 0x30, 0x56, - 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x43, - 0x49, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x08, 0x0c, 0x02, - 0x53, 0x54, 0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x04, 0x07, 0x0c, - 0x07, 0x43, 0x4f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x31, 0x12, 0x30, 0x10, - 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x09, 0x4d, 0x79, 0x43, 0x6f, 0x6d, - 0x70, 0x61, 0x6e, 0x79, 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, - 0x03, 0x0c, 0x0b, 0x74, 0x63, 0x6f, 0x64, 0x65, 0x2e, 0x6c, 0x6f, 0x63, - 0x61, 0x6c, 0x30, 0x81, 0x9f, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, - 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x81, 0x8d, 0x00, - 0x30, 0x81, 0x89, 0x02, 0x81, 0x81, 0x00, 0xea, 0x16, 0xfe, 0x83, 0xf2, - 0x30, 0xa5, 0xde, 0x3a, 0x5f, 0xd9, 0x00, 0xff, 0x61, 0x4c, 0x6e, 0x96, - 0xe0, 0xaa, 0xf8, 0x66, 0x0f, 0x8d, 0x0c, 0x3b, 0x31, 0x2f, 0x95, 0x07, - 0x96, 0xca, 0x28, 0x11, 0xdd, 0x67, 0x60, 0xe2, 0x26, 0xaa, 0x5c, 0xfb, - 0x58, 0xde, 0x47, 0xdb, 0x2b, 0x8a, 0xb9, 0x23, 0xaa, 0xe7, 0x89, 0x58, - 0x3b, 0x01, 0x77, 0x74, 0x2b, 0xec, 0x90, 0x4d, 0xd9, 0x24, 0x5e, 0x34, - 0x67, 0x46, 0x02, 0x8f, 0x34, 0x69, 0xdd, 0x65, 0xae, 0x13, 0x7f, 0xa4, - 0xe3, 0x9d, 0x0f, 0xce, 0x56, 0x73, 0x79, 0x6f, 0xa1, 0x25, 0x77, 0x4b, - 0xac, 0x24, 0x03, 0x5f, 0x9b, 0x7f, 0xe0, 0x4f, 0x08, 0x29, 0x5a, 0x23, - 0x5b, 0x02, 0xcc, 0x64, 0x02, 0x84, 0x71, 0x9e, 0x88, 0x0e, 0xa7, 0xc9, - 0xf9, 0x80, 0x2a, 0xb8, 0x64, 0x89, 0xf5, 0xed, 0x1e, 0x73, 0x1a, 0xd0, - 0x77, 0x1f, 0x9b, 0x02, 0x03, 0x01, 0x00, 0x01, 0x30, 0x0d, 0x06, 0x09, - 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, - 0x81, 0x81, 0x00, 0x91, 0xba, 0xa1, 0x51, 0x20, 0x27, 0x40, 0xe9, 0xe2, - 0x7b, 0x0a, 0xab, 0xaf, 0xd8, 0xbb, 0xa7, 0x22, 0xea, 0xbc, 0x48, 0xc8, - 0x17, 0xf4, 0x52, 0xe5, 0x76, 0x60, 0xf6, 0xa1, 0x37, 0x41, 0x32, 0xd9, - 0xcf, 0xee, 0x57, 0x99, 0x08, 0x98, 0x50, 0xc7, 0x06, 0xe6, 0xd3, 0xbc, - 0xd7, 0x9f, 0xcb, 0xf0, 0x54, 0xc7, 0xb6, 0xb5, 0x36, 0xad, 0xf4, 0x8c, - 0x03, 0x61, 0x46, 0x25, 0xab, 0xf0, 0x95, 0x88, 0x1b, 0xe3, 0xa9, 0xbe, - 0x96, 0x2a, 0x2b, 0x3c, 0x6d, 0x56, 0xa7, 0x2f, 0xf8, 0xbd, 0xc3, 0xdb, - 0x3d, 0xb6, 0x5b, 0x45, 0x61, 0x6f, 0xad, 0xf9, 0x95, 0x35, 0xf6, 0x6b, - 0x41, 0xc0, 0xdd, 0x6e, 0x15, 0xb3, 0x79, 0x6d, 0x59, 0x81, 0xfe, 0x16, - 0x1a, 0x80, 0x2f, 0x55, 0xdf, 0xcd, 0x40, 0xa8, 0x75, 0xbc, 0xe2, 0x0d, - 0x9a, 0x7a, 0x35, 0x95, 0x76, 0x57, 0x25, 0xdc, 0xb9, 0x46, 0x3a -}; -unsigned int example_crt_DER_len = 551; -#endif diff --git a/ESP32/src/HTTP/private_key.h b/ESP32/src/HTTP/private_key.h deleted file mode 100644 index d37bc7c..0000000 --- a/ESP32/src/HTTP/private_key.h +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef PRIVATE_KEY_H_ -#define PRIVATE_KEY_H_ -unsigned char example_key_DER[] = { - 0x30, 0x82, 0x02, 0x5f, 0x02, 0x01, 0x00, 0x02, 0x81, 0x81, 0x00, 0xea, - 0x16, 0xfe, 0x83, 0xf2, 0x30, 0xa5, 0xde, 0x3a, 0x5f, 0xd9, 0x00, 0xff, - 0x61, 0x4c, 0x6e, 0x96, 0xe0, 0xaa, 0xf8, 0x66, 0x0f, 0x8d, 0x0c, 0x3b, - 0x31, 0x2f, 0x95, 0x07, 0x96, 0xca, 0x28, 0x11, 0xdd, 0x67, 0x60, 0xe2, - 0x26, 0xaa, 0x5c, 0xfb, 0x58, 0xde, 0x47, 0xdb, 0x2b, 0x8a, 0xb9, 0x23, - 0xaa, 0xe7, 0x89, 0x58, 0x3b, 0x01, 0x77, 0x74, 0x2b, 0xec, 0x90, 0x4d, - 0xd9, 0x24, 0x5e, 0x34, 0x67, 0x46, 0x02, 0x8f, 0x34, 0x69, 0xdd, 0x65, - 0xae, 0x13, 0x7f, 0xa4, 0xe3, 0x9d, 0x0f, 0xce, 0x56, 0x73, 0x79, 0x6f, - 0xa1, 0x25, 0x77, 0x4b, 0xac, 0x24, 0x03, 0x5f, 0x9b, 0x7f, 0xe0, 0x4f, - 0x08, 0x29, 0x5a, 0x23, 0x5b, 0x02, 0xcc, 0x64, 0x02, 0x84, 0x71, 0x9e, - 0x88, 0x0e, 0xa7, 0xc9, 0xf9, 0x80, 0x2a, 0xb8, 0x64, 0x89, 0xf5, 0xed, - 0x1e, 0x73, 0x1a, 0xd0, 0x77, 0x1f, 0x9b, 0x02, 0x03, 0x01, 0x00, 0x01, - 0x02, 0x81, 0x81, 0x00, 0xa9, 0xf9, 0x86, 0x57, 0x82, 0xad, 0x66, 0x53, - 0x45, 0xe9, 0xc0, 0xe5, 0x63, 0x8a, 0x5f, 0xf8, 0x51, 0x1f, 0xd3, 0xa5, - 0x48, 0x5e, 0x74, 0x59, 0x74, 0x45, 0x93, 0xba, 0x4f, 0xe7, 0x62, 0xe4, - 0xd3, 0x8c, 0x03, 0x7b, 0xaa, 0xda, 0xce, 0x8b, 0x73, 0x8a, 0xa4, 0xe4, - 0x62, 0x35, 0x6c, 0xa6, 0x60, 0x4a, 0xc1, 0x92, 0xcd, 0xf9, 0x12, 0x68, - 0x8d, 0x77, 0x33, 0x6f, 0xd8, 0xc7, 0x1a, 0x0a, 0xd5, 0x87, 0xa4, 0xe8, - 0xf9, 0xb5, 0x32, 0x2c, 0xfd, 0x25, 0xbf, 0xed, 0x55, 0x82, 0xeb, 0x1e, - 0x46, 0x27, 0x4c, 0xa4, 0xac, 0xe9, 0xdd, 0x83, 0x50, 0xd9, 0x63, 0x73, - 0x1c, 0x10, 0x41, 0x97, 0xb5, 0xe3, 0x45, 0x68, 0xb5, 0xf2, 0x1c, 0x17, - 0x99, 0x36, 0x31, 0x06, 0xd2, 0x02, 0x61, 0x16, 0x83, 0x7b, 0x3c, 0x93, - 0x45, 0x6c, 0x76, 0xa6, 0xba, 0xc0, 0x28, 0xda, 0x15, 0xee, 0x16, 0xe1, - 0x02, 0x41, 0x00, 0xff, 0x72, 0xdd, 0xa6, 0x88, 0x2d, 0x14, 0x69, 0xbf, - 0x3d, 0xd9, 0xc7, 0x8e, 0x17, 0x28, 0x26, 0xb0, 0x19, 0x6b, 0x2a, 0x0b, - 0x3c, 0x5a, 0x42, 0xb6, 0xe6, 0xb3, 0xde, 0x97, 0xe3, 0xdd, 0x15, 0x0b, - 0xe4, 0xe7, 0x4b, 0xbc, 0xc6, 0x91, 0x29, 0x11, 0x40, 0x3d, 0x82, 0xe7, - 0x37, 0x84, 0xfa, 0x1f, 0xda, 0xbd, 0x19, 0x38, 0xd7, 0xfd, 0x35, 0x0b, - 0xa9, 0xdf, 0x33, 0xd2, 0x94, 0x5c, 0x91, 0x02, 0x41, 0x00, 0xea, 0x98, - 0x53, 0xe4, 0x5e, 0xe0, 0x7e, 0x5a, 0x1d, 0xe2, 0x2d, 0x1a, 0x7a, 0xde, - 0x76, 0xda, 0x04, 0x1c, 0xa4, 0x92, 0x74, 0xf7, 0x26, 0x41, 0x26, 0xbf, - 0xb3, 0xa7, 0xbd, 0x26, 0x58, 0x50, 0x36, 0x2c, 0x45, 0xce, 0x52, 0x77, - 0x67, 0x8d, 0x81, 0xdd, 0x2d, 0xe5, 0x4a, 0x70, 0xd6, 0x26, 0x77, 0x5a, - 0xa1, 0xa8, 0x7e, 0x51, 0x8a, 0x54, 0xe1, 0xf4, 0xce, 0xfa, 0xe0, 0x40, - 0xff, 0x6b, 0x02, 0x41, 0x00, 0x9e, 0x20, 0x04, 0x84, 0xa9, 0x96, 0xfe, - 0x23, 0xd7, 0x75, 0xf9, 0xf1, 0x45, 0x4b, 0xa0, 0x57, 0x12, 0x7b, 0x29, - 0x93, 0x05, 0x11, 0x7e, 0xed, 0xfd, 0x3a, 0x21, 0xed, 0x90, 0x28, 0x45, - 0x1a, 0x5a, 0x1a, 0x7f, 0xf2, 0xaa, 0x10, 0x60, 0x9b, 0x03, 0x4a, 0xb8, - 0xc8, 0xe7, 0x47, 0xbe, 0xd0, 0xf6, 0x16, 0xf9, 0x27, 0x3b, 0xc0, 0xb7, - 0xc4, 0xb6, 0x4b, 0x99, 0x17, 0x03, 0x2b, 0x43, 0x81, 0x02, 0x41, 0x00, - 0xbb, 0x01, 0x26, 0x8e, 0xbb, 0x1a, 0xd5, 0x5d, 0xdc, 0xc8, 0x79, 0x0f, - 0xcc, 0xb6, 0x1d, 0xa3, 0xf8, 0xf7, 0x24, 0x31, 0x23, 0x50, 0x08, 0x8c, - 0x92, 0xe8, 0xe9, 0xbb, 0x62, 0xca, 0x78, 0x47, 0xa8, 0x87, 0x6e, 0x35, - 0xe4, 0x03, 0x0e, 0xe6, 0xfc, 0x88, 0x65, 0x97, 0x8b, 0xd9, 0x9a, 0xbc, - 0x1b, 0x14, 0x82, 0x1d, 0x20, 0x64, 0xbb, 0x92, 0xa2, 0x74, 0x55, 0xb2, - 0x22, 0xa5, 0x6d, 0x75, 0x02, 0x41, 0x00, 0x91, 0xd3, 0x8a, 0xca, 0x3d, - 0xde, 0x65, 0x1e, 0x1e, 0x03, 0x86, 0xe4, 0x32, 0xba, 0xb7, 0x91, 0x19, - 0xc1, 0x8a, 0xfd, 0x35, 0x56, 0xcd, 0x95, 0xf8, 0x86, 0x5d, 0x49, 0x93, - 0xb2, 0x72, 0x64, 0x28, 0xe7, 0xd3, 0xd7, 0x5f, 0x5d, 0x03, 0x83, 0x5d, - 0x7c, 0x82, 0x2d, 0x34, 0xaa, 0xe3, 0x7a, 0x36, 0x88, 0x85, 0x74, 0xfe, - 0xc6, 0xa3, 0xec, 0x37, 0xc9, 0xe3, 0x15, 0xfd, 0x27, 0x4d, 0xe3 -}; -unsigned int example_key_DER_len = 611; -#endif diff --git a/ESP32/src/InitHandler.hpp b/ESP32/src/InitHandler.hpp deleted file mode 100644 index 652359a..0000000 --- a/ESP32/src/InitHandler.hpp +++ /dev/null @@ -1,826 +0,0 @@ -#pragma once -#include "InstanceHandler.h" - -class InitHandler -{ -public: - static InitHandler* getInstance() - { - static InitHandler instance; - return &instance; - } - bool init() - { - taskHandler = TaskHandler::getInstance(); - - - // setCpuFrequencyMhz(240); - - // see if we can use the onboard led for status - // https://github.com/kriswiner/ESP32/blob/master/PWM/ledcWrite_demo_ESP32.ino - // digitalWrite(5, LOW);// Turn off on-board blue led - serialHandler = new SerialHandler(); - serialHandler->setup(); - Serial.printf("Startup DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - - #if DEBUG_BUILD == 1 - LogHandler::setLogLevel(LogLevel::DEBUG); - #else - LogHandler::setLogLevel(LogLevel::INFO); - #endif - LogHandler::setMessageCallback(logCallBack); - - Serial.println(); - LogHandler::info(TagHandler::Main, "Firmware version: %s", FIRMWARE_VERSION_NAME); - // LogHandler::info(TagHandler::Main, "Esp arduino version: %s", ESP_ARDUINO_VERSION_STR); - LogHandler::info(TagHandler::Main, "ESP IDF version: %s", esp_get_idf_version()); - uint32_t chipId = 0; - for (int i = 0; i < 17; i = i + 8) - { - chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; - } - LogHandler::info(TagHandler::Main, "ESP32 Chip model = %s Rev %d", ESP.getChipModel(), ESP.getChipRevision()); - LogHandler::info(TagHandler::Main, "This chip has %d cores", ESP.getChipCores()); - LogHandler::info(TagHandler::Main, "Chip ID: %u", chipId); - Serial.println(); - - // esp_log_level_set("*", ESP_LOG_VERBOSE); - // LogHandler::debug("main", "this is verbose"); - // LogHandler::debug("main", "this is debug"); - // LogHandler::info("main", "this is info"); - // LogHandler::warning("main", "this is warning"); - // LogHandler::error("main", "this is error"); - - if (!LittleFS.begin(true)) - { - LogHandler::error(TagHandler::Main, "An Error has occurred while mounting LittleFS"); - return false; - } - LogHandler::debug(TagHandler::Main, "LittleFS DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - - settingsFactory = SettingsFactory::getInstance(); - settingsFactory->setMessageCallback(settingChangeCallback); - if (!settingsFactory->init()) - { - LogHandler::error(TagHandler::Main, "Failed to load settings..."); - return false; - } - LogHandler::debug(TagHandler::Main, "Settings factory DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - // LogHandler::setLogLevel(LogLevel::DEBUG); - - const PinMap *pinMap = settingsFactory->getPins(); - if(!pinMap) - { - LogHandler::warning(TagHandler::Main, "No pin map defined"); - return false; - } - - SettingsHandler::init(); - SettingsHandler::setMessageCallback(settingChangeCallback); - LogHandler::debug(TagHandler::Main, "Settings handler DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - - #if BLE_TCODE - settingsFactory->getValue(BLE_ENABLED, bleEnabled); - - //bleEnabled = true; - #endif - #if BLUETOOTH_TCODE - settingsFactory->getValue(BLUETOOTH_ENABLED, bluetoothEnabled); - - //bluetoothEnabled = true; - #endif - - #if WIFI_TCODE - if ((!bluetoothEnabled && !bleEnabled) || COEXIST) - wifi.setWiFiStatusCallback(std::bind(&InitHandler::wifiStatusCallBack, this, std::placeholders::_1, std::placeholders::_2)); - #endif - - // Get ConfigurationSettings - bool fanControlEnabled = FAN_CONTROL_ENABLED_DEFAULT; - settingsFactory->getValue(FAN_CONTROL_ENABLED, fanControlEnabled); - - // Cached (Requires reboot) - MotorType motorType; - BoardType boardType; - DeviceType deviceType; - settingsFactory->getValue(MOTOR_TYPE_SETTING, motorType); - settingsFactory->getValue(BOARD_TYPE_SETTING, boardType); - settingsFactory->getValue(DEVICE_TYPE, deviceType); - SettingsHandler::channelMap.init(settingsFactory->getTcodeVersion(), motorType, deviceType); - - // bool lubeEnabled; - // bool feedbackTwist; - // bool analogTwist; - bool bootButtonEnabled = BOOT_BUTTON_ENABLED_DEFAULT; - bool buttonSetsEnabled = BUTTON_SETS_ENABLED_DEFAULT; - - // settingsFactory->getValue(FEEDBACK_TWIST, feedbackTwist); - // settingsFactory->getValue(ANALOG_TWIST, analogTwist); - settingsFactory->getValue(BOOT_BUTTON_ENABLED, bootButtonEnabled); - settingsFactory->getValue(BUTTON_SETS_ENABLED, buttonSetsEnabled); - - bool batteryLevelEnabled = BATTERY_LEVEL_ENABLED_DEFAULT; - bool voiceEnabled = VOICE_ENABLED_DEFAULT; - settingsFactory->getValue(BATTERY_LEVEL_ENABLED, batteryLevelEnabled); - settingsFactory->getValue(VOICE_ENABLED, voiceEnabled); - - bool displayEnabled = DISPLAY_ENABLED_DEFAULT; - settingsFactory->getValue(DISPLAY_ENABLED, displayEnabled); - char Display_I2C_AddressString[DISPLAY_I2C_ADDRESS_LEN] = {0}; - settingsFactory->getValue(DISPLAY_I2C_ADDRESS, Display_I2C_AddressString, DISPLAY_I2C_ADDRESS_LEN); - int Display_I2C_Address = (int)strtol(Display_I2C_AddressString, NULL, 0); - - systemCommandHandler = new SystemCommandHandler(); - systemCommandHandler->registerExternalCommandCallback(tcodePassthroughCommandCallback); - LogHandler::debug(TagHandler::Main, "System command handler DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - - #ifdef MOTOR_TYPE_SERVO - if (settingsFactory->getTcodeVersion() == TCodeVersion::v0_3) - { - motorHandler = new ServoHandler0_3(); - } - else if (settingsFactory->getTcodeVersion() == TCodeVersion::v0_4) - { - motorHandler = new ServoHandler0_4(); - } - #if !DEBUG_BUILD && TCODE_V2 - // else if(settingsFactory->getTcodeVersion() == TCodeVersion::v0_2) - // motorHandler = new ServoHandler0_2(); - #endif - else - { - LogHandler::error(TagHandler::Main, "Invalid TCode version: %ld", settingsFactory->getTcodeVersion()); - return false; // TODO: this stops apmode and not what we want - } - #elif defined MOTOR_TYPE_BLDC - if (settingsFactory->getTcodeVersion() == TCodeVersion::v0_3) - { - motorHandler = new BLDCHandler0_3(); - } - else if (settingsFactory->getTcodeVersion() == TCodeVersion::v0_4) - { - motorHandler = new BLDCHandler0_4(); - } - #else - #error "Build error! Invalid motor type defined!" - #endif - - motorHandler->setMessageCallback(tcodeCommandCallback); - LogHandler::debug(TagHandler::Main, "Motor handler DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - // SystemCommandHandler::registerOtherCommandCallback(TCodeCommandCallback); - - #if BUILD_TEMP - bool sleeveTempEnabled; - bool internalTempEnabled; - int8_t heaterChannel = pinMap->heaterChannel(); - int heaterFrequency = heaterChannel > -1 ? pinMap->getChannelFrequency(pinMap->heaterChannel()) : ESP_TIMER_FREQUENCY_DEFAULT; - int heaterResolution; - float heaterThreshold; - int8_t caseFanChannel = pinMap->caseFanChannel(); - int caseFanFrequency = caseFanChannel > -1 ? pinMap->getChannelFrequency(pinMap->caseFanChannel()) : ESP_TIMER_FREQUENCY_DEFAULT; - int caseFanResolution; - int caseFanMaxPWM; - settingsFactory->getValue(TEMP_SLEEVE_ENABLED, sleeveTempEnabled); - settingsFactory->getValue(TEMP_INTERNAL_ENABLED, internalTempEnabled); - settingsFactory->getValue(HEATER_RESOLUTION, heaterResolution); - settingsFactory->getValue(HEATER_THRESHOLD, heaterThreshold); - settingsFactory->getValue(CASE_FAN_RESOLUTION, caseFanResolution); - settingsFactory->getValue(CASE_FAN_MAX_PWM, caseFanMaxPWM); - if (sleeveTempEnabled || internalTempEnabled || fanControlEnabled) - { - temperatureHandler = new TemperatureHandler(); - temperatureHandler->setup(internalTempEnabled, - sleeveTempEnabled, - pinMap->sleeveTemp(), - pinMap->internalTemp(), - pinMap->heater(), - heaterChannel, - pinMap->caseFan(), - caseFanChannel, - heaterFrequency, - heaterResolution, - fanControlEnabled, - caseFanFrequency, - caseFanResolution, - caseFanMaxPWM); - temperatureHandler->setMessageCallback(tempChangeCallBack); - temperatureHandler->setStateChangeCallback(tempStateChangeCallBack); - LogHandler::debug(TagHandler::Main, "Start temperature task"); - taskHandler->startTemperatureTask(temperatureHandler); - LogHandler::debug(TagHandler::Main, "Temp DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - } - - #endif - #if BUILD_DISPLAY - if (displayEnabled) - { - displayHandler = new DisplayHandler(); - displayHandler->setup(Display_I2C_Address, fanControlEnabled, pinMap->displayReset()); - // #if ISAAC_NEWTONGUE_BUILD - // xTaskCreatePinnedToCore( - // DisplayHandler::startAnimationDontPanic,/* Function to implement the task */ - // "DisplayTask", /* Name of the task */ - // 10000, /* Stack size in words */ - // displayHandler, /* Task input parameter */ - // 25, /* Priority of the task */ - // &animationTask, /* Task handle. */ - // APP_CPU_NUM); /* Core where the task should run */ - // #endif - } - LogHandler::debug(TagHandler::Main, "Display DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - #endif - - #if BLE_TCODE - if (bleEnabled) - { - startBLETCode(); - } - else - { - BLEHandler::disable(); - } - LogHandler::debug(TagHandler::Main, "BLE DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - #else - esp_bt_controller_mem_release(ESP_BT_MODE_BTDM) - #endif - #if BLUETOOTH_TCODE - if (bluetoothEnabled) - { - startBlueTooth(); - } - else - { - BluetoothHandler::disable(); - } - LogHandler::debug(TagHandler::Main, "Bluetooth DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - #else - esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); - #endif - - #if BLE_TCODE || BLUETOOTH_TCODE - if (WIFI_TCODE && !COEXIST && (bluetoothEnabled || bleEnabled)) - { - WifiHandler::disable(); - LogHandler::debug(TagHandler::Main, "Wifi disable DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - } - #endif - - #if WIFI_TCODE - if ((!bluetoothEnabled && !bleEnabled) || COEXIST) - { - char ssid[SSID_LEN]; - char wifiPass[WIFI_PASS_LEN]; - bool staticIP; - char localIP[IP_ADDRESS_LEN]; - char gateway[IP_ADDRESS_LEN]; - char subnet[IP_ADDRESS_LEN]; - char dns1[IP_ADDRESS_LEN]; - char dns2[IP_ADDRESS_LEN]; - - settingsFactory->getValue(SSID_SETTING, ssid, SSID_LEN); - settingsFactory->getValue(WIFI_PASS_SETTING, wifiPass, WIFI_PASS_LEN); - settingsFactory->getValue(STATICIP, staticIP); - settingsFactory->getValue(LOCALIP, localIP, IP_ADDRESS_LEN); - settingsFactory->getValue(GATEWAY, gateway, IP_ADDRESS_LEN); - settingsFactory->getValue(SUBNET, subnet, IP_ADDRESS_LEN); - settingsFactory->getValue(DNS1, dns1, IP_ADDRESS_LEN); - settingsFactory->getValue(DNS2, dns2, IP_ADDRESS_LEN); - if (strcmp(wifiPass, WIFI_PASS_DONOTCHANGE_DEFAULT) != 0 && strlen(ssid)) - { - displayPrint("Setting up wifi..."); - LogHandler::info(TagHandler::Main, "Setting up wifi..."); - displayPrint("Connecting to: "); - LogHandler::info(TagHandler::Main, "Connecting to: %s", ssid); - displayPrint(ssid); - if (wifi.connect(settingsFactory->getHostname(), ssid, wifiPass)) - { - // String ipaddress = wifi.ip().toString(); - // displayPrint("Connected IP: " + ipaddress); - // LogHandler::info(TagHandler::Main, "Connected IP: %s", ipaddress.c_str()); - // #if BUILD_DISPLAY - // displayHandler->setLocalIPAddress(wifi.ip()); - // #endif - if (!startUDPTCode(settingsFactory->getUdpServerPort())) - { - LogHandler::error(TagHandler::Main, "Error starting UDP server!"); - return false; - } - LogHandler::debug(TagHandler::Main, "UDP DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - startNetworking(false, - settingsFactory->getWebServerPort(), - settingsFactory->getUdpServerPort(), - settingsFactory->getHostname(), - settingsFactory->getFriendlyName()); - } - } - else - { - startConfigMode( - settingsFactory->getWebServerPort(), - settingsFactory->getUdpServerPort(), - settingsFactory->getHostname(), - settingsFactory->getFriendlyName()); - } - } - #endif - motionHandler = new MotionHandler(); - motionHandler->setup(settingsFactory->getTcodeVersion()); - LogHandler::debug(TagHandler::Main, "Motion handler DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - loadI2CModules(displayEnabled, batteryLevelEnabled, voiceEnabled); - LogHandler::debug(TagHandler::Main, "I2C DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - - if (bootButtonEnabled || buttonSetsEnabled) - { - buttonHandler = new ButtonHandler(); - buttonHandler->init(settingsFactory->getButtonAnalogDebounce(), - settingsFactory->getBootButtonCommand(), - settingsFactory->getButtonSets()); - } - - // otaHandler.setup(); - displayPrint("Setting up motor"); - if(!motorHandler->setup()) - { - return false; - } - LogHandler::debug(TagHandler::Main, "Motor DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - - LogHandler::debug(TagHandler::Main, "Setup finished"); - SettingsHandler::printFree(); - m_initialized = true; - return true; - } - - -private: - static inline SettingsFactory *settingsFactory = SettingsFactory::getInstance(); - TaskHandler* taskHandler; - //CallbackHandler* callbackHandler; - bool m_initialized = false; - bool bluetoothEnabled = BLUETOOTH_ENABLED_DEFAULT; - bool bleEnabled = BLE_ENABLED_DEFAULT; - // BLEConfigurationHandler* bleConfigurationHandler; - // TcpHandler tcpHandler; - void loadI2CModules(bool displayEnabled, bool batteryEnabled, bool voiceEnabled) - { -#if BUILD_DISPLAY - if (displayEnabled) - { - taskHandler->startDisplayTask(displayHandler); - } -#endif - if (batteryEnabled) - { - batteryHandler = new BatteryHandler(); - if (batteryHandler->setup()) - { - taskHandler->startBatteryTask(batteryHandler); - batteryHandler->setMessageCallback(batteryVoltageCallback); - } - } - if (voiceEnabled) - { - voiceHandler = new VoiceHandler(); - if (voiceHandler->setup()) - { - taskHandler->startVoiceTask(voiceHandler); - voiceHandler->setMessageCallback(tcodeCommandCallback); - } - } - } -#if WIFI_TCODE - void startNetworking(const bool &apMode, const int &port, const int &udpPort, const char *hostname, const char *friendlyName) - { - if((MODULE_CURRENT != ModuleType::WROOM32 || (!bluetoothEnabled && !bleEnabled)) && !webHandler) - { - displayPrint("Starting web server"); - #if !SECURE_WEB - webHandler = new WebHandler(); - webSocketHandler = new WebSocketHandler(); - #else - webHandler = new HTTPSHandler(); - webSocketHandler = new SecureWebSocketHandler(); - taskHandler->startHTTPSTask(webHandler); - #endif - webHandler->setup(port, webSocketHandler, apMode); - LogHandler::debug(TagHandler::Main, "Web DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - } else { - displayPrint("WebServer disabled"); - LogHandler::info(TagHandler::Main, "WebServer disabled due to bluetooth and chip model"); - } - if (!apMode) {// mdns breaks apmode? - bool mdnsEnabled = MDNS_ENABLED_DEFAULT; - settingsFactory->getValue(MDNS_ENABLED, mdnsEnabled); - if(mdnsEnabled) - { - mdnsHandler.setup(hostname, friendlyName, port, udpPort); - LogHandler::debug(TagHandler::Main, "MDNS DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - } - } - } -#endif - - void startBLEConfig() - { - // Disabled. Android Application needs maintenance - // if(!bleConfigurationHandler) { - // displayPrint("Starting BLE config"); - // bleConfigurationHandler = new BLEConfigurationHandler(); - // bleConfigurationHandler->setup(); - // } - } - -#if BLE_TCODE - void startBLETCode() - { - if (!bleHandler) - { - displayPrint("Starting BLE"); - bleHandler = new BLEHandler(); - bleHandler->setup(); - } - } -#endif - -#if BLUETOOTH_TCODE - void startBlueTooth() - { - if (bluetoothEnabled && !bluetoothHandler) - { - displayPrint("Starting Bluetooth serial"); - bluetoothHandler = new BluetoothHandler(); - bluetoothHandler->setup(); - } - } -#endif - -#if WIFI_TCODE - bool startUDPTCode(int port) - { - if (!udpHandler) - { - displayPrint("Starting UDP"); - udpHandler = new Udphandler(); - if (!udpHandler->setup(port)) - return false; - } - return true; - } -#endif - - void startConfigMode(const int &webPort, const int &udpPort, const char *hostname, const char *friendlyName) - { -#if WIFI_TCODE - SettingsHandler::apMode = true; - displayPrint("Starting in APMode"); - - char pass[WIFI_PASS_LEN]; - bool hidden = AP_MODE_HIDDEN_DEFAULT; - uint8_t channel = AP_MODE_CHANNEL_DEFAULT; - - char subnet[IP_ADDRESS_LEN]; - char gateway[IP_ADDRESS_LEN]; - - settingsFactory->getValue(AP_MODE_PASS, pass, WIFI_PASS_LEN); - settingsFactory->getValue(AP_MODE_SUBNET, subnet, IP_ADDRESS_LEN); - settingsFactory->getValue(AP_MODE_GATEWAY, gateway, IP_ADDRESS_LEN); - settingsFactory->getValue(AP_MODE_HIDDEN, hidden); - settingsFactory->getValue(AP_MODE_CHANNEL, channel); - if (wifi.startAp(hostname, settingsFactory->getAPModeSSID(), pass, channel, hidden, settingsFactory->getAPModeIP(), subnet, gateway)) - { - displayPrint("APMode started"); - startNetworking(SettingsHandler::apMode, webPort, udpPort, hostname, friendlyName); - } - else - { - displayPrint("APMode start failed"); - } -#endif - -#if BLE_TCODE || BLUETOOTH_TCODE - if(bleEnabled || bluetoothEnabled) { - startBLEConfig(); - } -#endif - } - -#if WIFI_TCODE - void wifiStatusCallBack(WiFiStatus status, WiFiReason reason) - { - if (status == WiFiStatus::CONNECTED) - { - LogHandler::debug(TagHandler::Main, "wifiStatusCallBack WiFiStatus::CONNECTED"); - if (reason == WiFiReason::AP_MODE) - { - LogHandler::debug(TagHandler::Main, "wifiStatusCallBack WiFiReason::AP_MODE"); - // if(bleConfigurationHandler) - // bleConfigurationHandler->stop(); // If a client connects to the ap stop the BLE to save memory. - } - } - else if(status == WiFiStatus::DISCONNECTED) - { - // wifi.dispose(); - // startApMode(); - LogHandler::debug(TagHandler::Main, "wifiStatusCallBack Not connected"); - if (reason == WiFiReason::NO_AP || reason == WiFiReason::UNKNOWN) - { - LogHandler::debug(TagHandler::Main, "wifiStatusCallBack WiFiReason::NO_AP || WiFiReason::UNKNOWN"); - startConfigMode( - settingsFactory->getWebServerPort(), - settingsFactory->getUdpServerPort(), - settingsFactory->getHostname(), - settingsFactory->getFriendlyName()); - } - else if (reason == WiFiReason::AUTH) - { - LogHandler::debug(TagHandler::Main, "wifiStatusCallBack WiFiReason::AUTH"); - LogHandler::warning(TagHandler::Main, "Connection auth failed: Resetting wifi password and restarting"); - settingsFactory->defaultValue(WIFI_PASS_SETTING); - ESP.restart(); - } - else if (reason == WiFiReason::AP_MODE) - { - LogHandler::debug(TagHandler::Main, "wifiStatusCallBack WiFiReason::AP_MODE"); - // #ifdef !ESP32_DA - // if(bleConfigurationHandler) - // bleConfigurationHandler->setup(); - // #endif - } - } - else if(status == WiFiStatus::IP) - { - #if BUILD_DISPLAY - if(displayHandler) - { - String ipaddress = wifi.ip().toString(); - displayPrint("Connected IP: " + ipaddress); - displayHandler->setLocalIPAddress(wifi.ip()); - } - #endif - } - } - void displayPrint(String text) - { - #if BUILD_DISPLAY - if(displayHandler) - displayHandler->println(text); - #endif - } - bool initialized() { - return m_initialized; - } -#endif - -// TODO move to CallbackHandler or freertos queues/////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////////////////////////// - // void TCodeCommandCallback(const char *in) - // { - - // if (systemCommandHandler->isCommand(in)) - // { - // systemCommandHandler->process(in); - // } - // else - // { - // #if BLUETOOTH_TCODE - // if (bluetoothHandler && bluetoothHandler->isConnected()) - // bluetoothHandler->CommandCallback(in); - // #endif - // #if BLE_TCODE - // if (bleHandler && bleHandler->isConnected()) - // bleHandler->send(in); - // #endif - // #if WIFI_TCODE - // if (webSocketHandler) - // webSocketHandler->send(in); - // if (udpHandler) - // udpHandler->send(in); - // #endif - // serialHandler->send(in); - // } - // } - - // void tcodePassthroughCommandCallback(const char *in) - // { - // if (systemCommandHandler->isCommand(in)) - // { - // // This seems wrong but since we are only calling this from one place its fine for now. - // char temp[strlen(in) + 2]; - // temp[0] = {0}; - // strcpy(temp, in); - // strcat(temp, "\n"); - // ////////////////////////////////////////////////////////////////////////////////////// - // #if BLUETOOTH_TCODE - // if (bluetoothHandler && bluetoothHandler->isConnected()) - // bluetoothHandler->send(temp); - // #endif - // #if BLE_TCODE - - // #endif - // #if WIFI_TCODE - // if (webSocketHandler) - // webSocketHandler->send(temp); - // if (udpHandler) - // udpHandler->send(temp); - // #endif - // serialHandler->send(temp); - // } - // } - - // void profileChangeCallback(uint8_t profile) - // { - // } - - // void logCallBack(const char *input, size_t length, LogLevel level) - // { - // #if WIFI_TCODE - // // if(webSocketHandler) { - // // webSocketHandler->sendDebug(in, level); - // // } - // #endif - // } - - // #if BUILD_TEMP - // void tempChangeCallBack(TemperatureType type, const char *message, float temp) - // { - // #if WIFI_TCODE - // if (webSocketHandler) - // { - // if (strpbrk(message, "{") == nullptr) - // { - // webSocketHandler->sendCommand(message); - // } - // else - // { - // if (type == TemperatureType::SLEEVE) - // { - // webSocketHandler->sendCommand("sleeveTempStatus", message); - // } - // else - // { - // webSocketHandler->sendCommand("internalTempStatus", message); - // } - // } - // } - // #endif - // #if BUILD_DISPLAY - // if (displayHandler) - // { - // if (type == TemperatureType::SLEEVE) - // { - // displayHandler->setSleeveTemp(temp); - // } - // else - // { - // displayHandler->setInternalTemp(temp); - // } - // } - // #endif - // } - - // void tempStateChangeCallBack(TemperatureType type, const char *state) - // { - // #if BUILD_DISPLAY - // if (displayHandler) - // { - // if (type == TemperatureType::SLEEVE) - // { - // LogHandler::verbose(TagHandler::Main, "tempStateChangeCallBack heat: %s", state); - // displayHandler->setHeateState(state); - // if (temperatureHandler) - // displayHandler->setHeateStateShort(temperatureHandler->getShortSleeveControlStatus(state)); - // } - // else - // { - // LogHandler::verbose(TagHandler::Main, "tempStateChangeCallBack fan: %s", state); - // displayHandler->setFanState(state); - // } - // } - // #endif - // } - // #endif - - // void batteryVoltageCallback(float capacityRemainingPercentage, float capacityRemaining, float voltage, float temperature) - // { - // #if BUILD_DISPLAY - // if (displayHandler) - // { - // displayHandler->setBatteryInformation(capacityRemainingPercentage, voltage, temperature); - // } - // #endif - // #if WIFI_TCODE - // if (webSocketHandler) - // { - // String statusJson("{\"batteryCapacityRemaining\":\"" + String(capacityRemaining) + "\", \"batteryCapacityRemainingPercentage\":\"" + String(capacityRemainingPercentage) + "\", \"batteryVoltage\":\"" + String(voltage) + "\", \"batteryTemperature\":\"" + String(temperature) + "\"}"); - // webSocketHandler->sendCommand("batteryStatus", statusJson.c_str()); - // } - // #endif - // } - - // void settingChangeCallback(const SettingProfile &profile, const char *settingThatChanged) - // { - // LogHandler::verbose(TagHandler::Main, "settingChangeCallback: %s", settingThatChanged); - // if (profile == SettingProfile::System) - // { - // if (!strcmp(settingThatChanged, LOG_LEVEL_SETTING)) - // { - - // #if DEBUG_BUILD != 1 - // LogHandler::setLogLevel(settingsFactory->getLogLevel()); - // #endif - // } - // else if (!strcmp(settingThatChanged, LOG_INCLUDETAGS)) - // { - // LogHandler::setIncludes(settingsFactory->getLogIncludes()); - // } - // else if (!strcmp(settingThatChanged, LOG_EXCLUDETAGS)) - // { - // LogHandler::setExcludes(settingsFactory->getLogExcludes()); - // } - // } - // else if (profile == SettingProfile::MotionProfile) - // { - // if (strcmp(settingThatChanged, MOTION_PROFILE_SELECTED_INDEX) == 0 || strcmp(settingThatChanged, MOTION_PROFILES) == 0) { - // motionHandler->setMotionChannels(SettingsHandler::getMotionChannels()); - // //} else if(strcmp(settingThatChanged, "motionChannels") == 0) { - // // motionHandler->setMotionChannels(SettingsHandler::getGetMotionChannels()()); - // } else if (strcmp(settingThatChanged, MOTION_ENABLED) == 0) { - // LogHandler::verbose(TagHandler::Main, "MOTION_ENABLED: %d", SettingsHandler::getMotionEnabled()); - // motionHandler->setEnabled(SettingsHandler::getMotionEnabled()); - // } - // // else if(strcmp(settingThatChanged, "motionAmplitudeGlobal") == 0) - // // motionHandler->setAmplitude(SettingsHandler::getGetMotionAmplitudeGlobal()()); - // // else if(strcmp(settingThatChanged, "motionOffsetGlobal") == 0) - // // motionHandler->setOffset(SettingsHandler::getGetMotionOffsetGlobal()()); - // // else if(strcmp(settingThatChanged, "motionPeriodGlobal") == 0) - // // motionHandler->setPeriod(SettingsHandler::getGetMotionPeriodGlobal()()); - // // else if(strcmp(settingThatChanged, "motionUpdateGlobal") == 0) - // // motionHandler->setUpdate(SettingsHandler::getGetMotionUpdateGlobal()()); - // // else if(strcmp(settingThatChanged, "motionPhaseGlobal") == 0) - // // motionHandler->setPhase(SettingsHandler::getGetMotionPhaseGlobal()()); - // // else if(strcmp(settingThatChanged, "motionReversedGlobal") == 0) - // // motionHandler->setReverse(SettingsHandler::getGetMotionReversedGlobal()()); - // // else if(strcmp(settingThatChanged, "motionAmplitudeGlobalRandom") == 0) - // // motionHandler->setAmplitudeRandom(SettingsHandler::getGetMotionAmplitudeGlobalRandom()()); - // // else if(strcmp(settingThatChanged, "motionAmplitudeGlobalRandomMin") == 0) - // // motionHandler->setAmplitudeRandomMin(SettingsHandler::getGetMotionAmplitudeGlobalRandomMin()()); - // // else if(strcmp(settingThatChanged, "motionAmplitudeGlobalRandomMax") == 0) - // // motionHandler->setAmplitudeRandomMax(SettingsHandler::getGetMotionAmplitudeGlobalRandomMax()()); - // // else if(strcmp(settingThatChanged, "motionPeriodGlobalRandom") == 0) - // // motionHandler->setPeriodRandom(SettingsHandler::getGetMotionPeriodGlobalRandom()()); - // // else if(strcmp(settingThatChanged, "motionPeriodGlobalRandomMin") == 0) - // // motionHandler->setPeriodRandomMin(SettingsHandler::getGetMotionPeriodGlobalRandomMin()()); - // // else if(strcmp(settingThatChanged, "motionPeriodGlobalRandomMax") == 0) - // // motionHandler->setPeriodRandomMax(SettingsHandler::getGetMotionPeriodGlobalRandomMax()()); - // // else if(strcmp(settingThatChanged, "motionOffsetGlobalRandom") == 0) - // // motionHandler->setOffsetRandom(SettingsHandler::getGetMotionOffsetGlobalRandom()()); - // // else if(strcmp(settingThatChanged, "motionOffsetGlobalRandomMin") == 0) - // // motionHandler->setOffsetRandomMin(SettingsHandler::getGetMotionOffsetGlobalRandomMin()()); - // // else if(strcmp(settingThatChanged, "motionOffsetGlobalRandomMax") == 0) - // // motionHandler->setOffsetRandomMax(SettingsHandler::getGetMotionOffsetGlobalRandomMax()()); - // // else if(strcmp(settingThatChanged, "motionRandomChangeMin") == 0) - // // motionHandler->setMotionRandomChangeMin(SettingsHandler::getGetMotionRandomChangeMin()()); - // // else if(strcmp(settingThatChanged, "motionRandomChangeMax") == 0) - // // motionHandler->setMotionRandomChangeMax(SettingsHandler::getGetMotionRandomChangeMax()()); - // } - // else if (voiceHandler && profile == SettingProfile::Voice) - // { - // if (strcmp(settingThatChanged, "voiceMuted") == 0) - // { - // voiceHandler->setMuteMode(settingsFactory->getVoiceMuted()); - // } - // else if (strcmp(settingThatChanged, "voiceVolume") == 0) - // { - // voiceHandler->setVolume(settingsFactory->getVoiceVolume()); - // } - // else if (strcmp(settingThatChanged, "voiceWakeTime") == 0) - // { - // voiceHandler->setWakeTime(settingsFactory->getVoiceWakeTime()); - // } - // } - // else if (buttonHandler && profile == SettingProfile::Button) - // { - // if (strcmp(settingThatChanged, "bootButtonCommand") == 0) - // buttonHandler->updateBootButtonCommand(settingsFactory->getBootButtonCommand()); - // else if (strcmp(settingThatChanged, "analogButtonCommands") == 0) - // { - // buttonHandler->updateAnalogButtonCommands(settingsFactory->getButtonSets()); - // } - // else if (strcmp(settingThatChanged, "buttonAnalogDebounce") == 0) - // { - // buttonHandler->updateAnalogDebounce(settingsFactory->getButtonAnalogDebounce()); - // } - // } - // else if (profile == SettingProfile::ChannelRanges) - // { - // if (strcmp(settingThatChanged, CHANNEL_PROFILE) == 0) { - // // TODO add channe; specific updates when moving to its own save...maybe... - // motionHandler->updateChannelRanges(); - // } else if (strcmp(settingThatChanged, "channelRangesEnabled") == 0) { - // webSocketHandler->sendCommand("channelRangesEnabled", SettingsHandler::getChannelRangesEnabled() ? "true" : "false"); - // } - - // } - // } -}; \ No newline at end of file diff --git a/ESP32/src/InstanceHandler.h b/ESP32/src/InstanceHandler.h deleted file mode 100644 index 761bb57..0000000 --- a/ESP32/src/InstanceHandler.h +++ /dev/null @@ -1,809 +0,0 @@ -/* MIT License - -Copyright (c) 2026 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once -#include "constants.h" -#include "SerialHandler.h" -#include -#include "LogHandler.h" -#include "SettingsHandler.h" -#include "SystemCommandHandler.h" -#if WIFI_TCODE -#include "WifiHandler.h" -#endif - -#if BUILD_TEMP -#include "TemperatureHandler.h" -#endif -#if BUILD_DISPLAY -#include "DisplayHandler.h" -#endif -#if BLUETOOTH_TCODE -#include "BluetoothHandler.h" -#endif -#include "TCode/MotorHandler.h" -// #include "BLEConfigurationHandler.h" - -#ifdef MOTOR_TYPE_SERVO -#include "ServoHandler0_3.h" -#include "ServoHandler0_4.h" -#elif defined MOTOR_TYPE_BLDC -#include "BLDCHandler0_3.h" -#include "BLDCHandler0_4.h" -#endif - -#if WIFI_TCODE -#include "UdpHandler.h" -// #include "TcpHandler.h" -#include "HTTP/HTTPBase.h" -#include "HTTP/WebSocketBase.h" -#if !SECURE_WEB -#include "WebHandler.h" -//#include "WebHandler_psychic.h" -#else -#include "HTTP\HTTPSHandler.hpp" -#endif -#include "MDNSHandler.hpp" -#endif -// #include "OTAHandler.h" -#if BLE_TCODE -#include "BLEHandler.hpp" -#endif - -#if WIFI_TCODE -#if !SECURE_WEB -#include "WebSocketHandler.h" -//#include "WebSocketHandler_psychic.h" -#else -#include "HTTP/SecureWebSocketHandler.hpp" -#endif -#endif - -#include "BatteryHandler.h" -#include "MotionHandler.hpp" -#include "VoiceHandler.hpp" -#include "ButtonHandler.hpp" - -#include "TaskHandler.hpp" - -SerialHandler *serialHandler; -SystemCommandHandler *systemCommandHandler = 0; -MotorHandler *motorHandler = 0; -BatteryHandler *batteryHandler = 0; -MotionHandler *motionHandler = 0; -VoiceHandler *voiceHandler; -ButtonHandler *buttonHandler = 0; -#if WIFI_TCODE - Udphandler *udpHandler = 0; - WifiHandler wifi; - MDNSHandler mdnsHandler; - HTTPBase *webHandler = 0; - WebSocketBase *webSocketHandler = 0; -#endif -#if BUILD_TEMP - TemperatureHandler *temperatureHandler = 0; -#endif -#if BLE_TCODE - BLEHandler *bleHandler = 0; -#endif -#if BLUETOOTH_TCODE - BluetoothHandler *bluetoothHandler = 0; -#endif -#if BUILD_DISPLAY - DisplayHandler *displayHandler = 0; -#endif - - -void tcodeCommandCallback(const char *in) -{ - - if (systemCommandHandler->isCommand(in)) - { - systemCommandHandler->process(in); - } - else - { -#if BLUETOOTH_TCODE - if (bluetoothHandler && bluetoothHandler->isConnected()) - bluetoothHandler->send(in); -#endif -#if BLE_TCODE - if (bleHandler && bleHandler->isConnected()) - bleHandler->send(in); -#endif -#if WIFI_TCODE - if (webSocketHandler) - webSocketHandler->send(in); - if (udpHandler) - udpHandler->send(in); -#endif - serialHandler->send(in); - } -} - -void tcodePassthroughCommandCallback(const char *in) -{ - if (systemCommandHandler->isCommand(in)) - { - // This seems wrong but since we are only calling this from one place its fine for now. - char temp[strlen(in) + 2]; - temp[0] = {0}; - strcpy(temp, in); - strcat(temp, "\n"); -////////////////////////////////////////////////////////////////////////////////////// -#if BLUETOOTH_TCODE - if (bluetoothHandler && bluetoothHandler->isConnected()) - bluetoothHandler->send(temp); -#endif -#if BLE_TCODE - -#endif -#if WIFI_TCODE - if (webSocketHandler) - webSocketHandler->send(temp); - if (udpHandler) - udpHandler->send(temp); -#endif - serialHandler->send(temp); - } -} - -void profileChangeCallback(uint8_t profile) -{ -} - -void logCallBack(const char *input, const size_t& length, const LogLevel& level) -{ -#if WIFI_TCODE - // if(webSocketHandler) { - // webSocketHandler->sendDebug(in, level); - // } -#endif -} - -#if BUILD_TEMP -void tempChangeCallBack(const TemperatureType& type, const char *message, const float& temp) -{ -#if WIFI_TCODE - if (webSocketHandler) - { - if (strpbrk(message, "{") == nullptr) - { - webSocketHandler->sendCommand(message); - } - else - { - if (type == TemperatureType::SLEEVE) - { - webSocketHandler->sendCommand("sleeveTempStatus", message); - } - else - { - webSocketHandler->sendCommand("internalTempStatus", message); - } - } - } -#endif -#if BUILD_DISPLAY - if (displayHandler) - { - if (type == TemperatureType::SLEEVE) - { - displayHandler->setSleeveTemp(temp); - } - else - { - displayHandler->setInternalTemp(temp); - } - } -#endif -} - -void tempStateChangeCallBack(const TemperatureType& type, const char *state) -{ -#if BUILD_DISPLAY - if (displayHandler) - { - if (type == TemperatureType::SLEEVE) - { - LogHandler::verbose(TagHandler::Main, "tempStateChangeCallBack heat: %s", state); - displayHandler->setHeateState(state); - if (temperatureHandler) - displayHandler->setHeateStateShort(temperatureHandler->getShortSleeveControlStatus(state)); - } - else - { - LogHandler::verbose(TagHandler::Main, "tempStateChangeCallBack fan: %s", state); - displayHandler->setFanState(state); - } - } -#endif -} -#endif - -void batteryVoltageCallback(const float& capacityRemainingPercentage, const float& capacityRemaining, const float& voltage, const float& temperature) -{ -#if BUILD_DISPLAY - if (displayHandler) - { - displayHandler->setBatteryInformation(capacityRemainingPercentage, voltage, temperature); - } -#endif -#if WIFI_TCODE - if (webSocketHandler) - { - String statusJson("{\"batteryCapacityRemaining\":\"" + String(capacityRemaining) + "\", \"batteryCapacityRemainingPercentage\":\"" + String(capacityRemainingPercentage) + "\", \"batteryVoltage\":\"" + String(voltage) + "\", \"batteryTemperature\":\"" + String(temperature) + "\"}"); - webSocketHandler->sendCommand("batteryStatus", statusJson.c_str()); - } -#endif -} - -void settingChangeCallback(const SettingProfile &profile, const char *settingThatChanged) -{ - LogHandler::verbose(TagHandler::Main, "settingChangeCallback: %s", settingThatChanged); - SettingsFactory* settingsFactory = SettingsFactory::getInstance(); - if (profile == SettingProfile::System) - { - if (!strcmp(settingThatChanged, LOG_LEVEL_SETTING)) - { - - #if DEBUG_BUILD != 1 - LogHandler::setLogLevel(settingsFactory->getLogLevel()); - #endif - } - else if (!strcmp(settingThatChanged, LOG_INCLUDETAGS)) - { - LogHandler::setIncludes(settingsFactory->getLogIncludes()); - } - else if (!strcmp(settingThatChanged, LOG_EXCLUDETAGS)) - { - LogHandler::setExcludes(settingsFactory->getLogExcludes()); - } - } - else if (profile == SettingProfile::MotionProfile) - { - if (strcmp(settingThatChanged, MOTION_PROFILE_SELECTED_INDEX) == 0 || strcmp(settingThatChanged, MOTION_PROFILES) == 0) { - motionHandler->setMotionChannels(SettingsHandler::getMotionChannels()); - //} else if(strcmp(settingThatChanged, "motionChannels") == 0) { - // motionHandler->setMotionChannels(SettingsHandler::getGetMotionChannels()()); - } else if (strcmp(settingThatChanged, MOTION_ENABLED) == 0) { - LogHandler::verbose(TagHandler::Main, "MOTION_ENABLED: %d", SettingsHandler::getMotionEnabled()); - motionHandler->setEnabled(SettingsHandler::getMotionEnabled()); - } - // else if(strcmp(settingThatChanged, "motionAmplitudeGlobal") == 0) - // motionHandler->setAmplitude(SettingsHandler::getGetMotionAmplitudeGlobal()()); - // else if(strcmp(settingThatChanged, "motionOffsetGlobal") == 0) - // motionHandler->setOffset(SettingsHandler::getGetMotionOffsetGlobal()()); - // else if(strcmp(settingThatChanged, "motionPeriodGlobal") == 0) - // motionHandler->setPeriod(SettingsHandler::getGetMotionPeriodGlobal()()); - // else if(strcmp(settingThatChanged, "motionUpdateGlobal") == 0) - // motionHandler->setUpdate(SettingsHandler::getGetMotionUpdateGlobal()()); - // else if(strcmp(settingThatChanged, "motionPhaseGlobal") == 0) - // motionHandler->setPhase(SettingsHandler::getGetMotionPhaseGlobal()()); - // else if(strcmp(settingThatChanged, "motionReversedGlobal") == 0) - // motionHandler->setReverse(SettingsHandler::getGetMotionReversedGlobal()()); - // else if(strcmp(settingThatChanged, "motionAmplitudeGlobalRandom") == 0) - // motionHandler->setAmplitudeRandom(SettingsHandler::getGetMotionAmplitudeGlobalRandom()()); - // else if(strcmp(settingThatChanged, "motionAmplitudeGlobalRandomMin") == 0) - // motionHandler->setAmplitudeRandomMin(SettingsHandler::getGetMotionAmplitudeGlobalRandomMin()()); - // else if(strcmp(settingThatChanged, "motionAmplitudeGlobalRandomMax") == 0) - // motionHandler->setAmplitudeRandomMax(SettingsHandler::getGetMotionAmplitudeGlobalRandomMax()()); - // else if(strcmp(settingThatChanged, "motionPeriodGlobalRandom") == 0) - // motionHandler->setPeriodRandom(SettingsHandler::getGetMotionPeriodGlobalRandom()()); - // else if(strcmp(settingThatChanged, "motionPeriodGlobalRandomMin") == 0) - // motionHandler->setPeriodRandomMin(SettingsHandler::getGetMotionPeriodGlobalRandomMin()()); - // else if(strcmp(settingThatChanged, "motionPeriodGlobalRandomMax") == 0) - // motionHandler->setPeriodRandomMax(SettingsHandler::getGetMotionPeriodGlobalRandomMax()()); - // else if(strcmp(settingThatChanged, "motionOffsetGlobalRandom") == 0) - // motionHandler->setOffsetRandom(SettingsHandler::getGetMotionOffsetGlobalRandom()()); - // else if(strcmp(settingThatChanged, "motionOffsetGlobalRandomMin") == 0) - // motionHandler->setOffsetRandomMin(SettingsHandler::getGetMotionOffsetGlobalRandomMin()()); - // else if(strcmp(settingThatChanged, "motionOffsetGlobalRandomMax") == 0) - // motionHandler->setOffsetRandomMax(SettingsHandler::getGetMotionOffsetGlobalRandomMax()()); - // else if(strcmp(settingThatChanged, "motionRandomChangeMin") == 0) - // motionHandler->setMotionRandomChangeMin(SettingsHandler::getGetMotionRandomChangeMin()()); - // else if(strcmp(settingThatChanged, "motionRandomChangeMax") == 0) - // motionHandler->setMotionRandomChangeMax(SettingsHandler::getGetMotionRandomChangeMax()()); - } - else if (voiceHandler && profile == SettingProfile::Voice) - { - if (strcmp(settingThatChanged, "voiceMuted") == 0) - { - voiceHandler->setMuteMode(settingsFactory->getVoiceMuted()); - } - else if (strcmp(settingThatChanged, "voiceVolume") == 0) - { - voiceHandler->setVolume(settingsFactory->getVoiceVolume()); - } - else if (strcmp(settingThatChanged, "voiceWakeTime") == 0) - { - voiceHandler->setWakeTime(settingsFactory->getVoiceWakeTime()); - } - } - else if (buttonHandler && profile == SettingProfile::Button) - { - if (strcmp(settingThatChanged, "bootButtonCommand") == 0) - buttonHandler->updateBootButtonCommand(settingsFactory->getBootButtonCommand()); - else if (strcmp(settingThatChanged, "analogButtonCommands") == 0) - { - buttonHandler->updateAnalogButtonCommands(settingsFactory->getButtonSets()); - } - else if (strcmp(settingThatChanged, "buttonAnalogDebounce") == 0) - { - buttonHandler->updateAnalogDebounce(settingsFactory->getButtonAnalogDebounce()); - } - } - else if (profile == SettingProfile::ChannelRanges) - { - if (strcmp(settingThatChanged, CHANNEL_PROFILE) == 0) { - // TODO add channe; specific updates when moving to its own save...maybe... - motionHandler->updateChannelRanges(); - } else if (strcmp(settingThatChanged, "channelRangesEnabled") == 0) { - webSocketHandler->sendCommand("channelRangesEnabled", SettingsHandler::getChannelRangesEnabled() ? "true" : "false"); - } - - } -}; - - - -/// Functor /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// class TCodeCommandCallback -// { -// public: -// TCodeCommandCallback( -// SystemCommandHandler* systemCommandHandler, -// WebSocketHandler* webSocketHandler, -// Udphandler* udpHandler, -// BLEHandler* bleHandler) -// : systemCommandHandler(systemCommandHandler), -// udpHandler(udpHandler), -// bleHandler(bleHandler), -// webSocketHandler(webSocketHandler) {} -// void operator()(const char* in) const { - -// if (systemCommandHandler->isCommand(in)) -// { -// systemCommandHandler->process(in); -// } -// else -// { -// #if BLUETOOTH_TCODE -// if (btHandler && btHandler->isConnected()) -// btHandler->send(in); -// #endif -// #if BLE_TCODE -// if (bleHandler && bleHandler->isConnected()) -// bleHandler->send(in); -// #endif -// #if WIFI_TCODE -// if (webSocketHandler) -// webSocketHandler->send(in); -// if (udpHandler) -// udpHandler->send(in); -// #endif -// if (Serial) -// Serial.println(in); -// } -// } -// private: -// SystemCommandHandler* systemCommandHandler; -// Udphandler* udpHandler; -// BLEHandler* bleHandler; -// DisplayHandler* displayHandler; -// WebSocketHandler* webSocketHandler; -// TemperatureHandler* temperatureHandler; -// MotionHandler* motionHandler; -// ButtonHandler* buttonHandler; -// VoiceHandler* voiceHandler; -// }; - -// class LogCallback -// { -// public: -// LogCallback(WebSocketHandler* webSocketHandler) : webSocketHandler(webSocketHandler) {} -// void operator()(const char* in, LogLevel level) const { -// #if WIFI_TCODE -// // if(webSocketHandler) { -// // webSocketHandler->sendDebug(in, level); -// // } -// #endif -// } -// private: -// WebSocketHandler* webSocketHandler; -// }; - -// class TempChangeCallback -// { -// public: -// TempChangeCallback(DisplayHandler* displayHandler, WebSocketHandler* webSocketHandler) : -// displayHandler(displayHandler), -// webSocketHandler(webSocketHandler) {} -// void operator()(const char* message, TemperatureType type, float temp) const -// { -// #if WIFI_TCODE -// if (webSocketHandler) -// { -// if (strpbrk(message, "{") == nullptr) -// { -// webSocketHandler->sendCommand(message); -// } -// else -// { -// if (type == TemperatureType::SLEEVE) -// { -// webSocketHandler->sendCommand("sleeveTempStatus", message); -// } -// else -// { -// webSocketHandler->sendCommand("internalTempStatus", message); -// } -// } -// } -// #endif -// #if BUILD_DISPLAY -// if (displayHandler) -// { -// if (type == TemperatureType::SLEEVE) -// { -// displayHandler->setSleeveTemp(temp); -// } -// else -// { -// displayHandler->setInternalTemp(temp); -// } -// } -// #endif -// } -// private: -// WebSocketHandler* webSocketHandler; -// DisplayHandler* displayHandler; -// }; - -// class TempChangeStateCallback -// { -// public: -// TempChangeStateCallback(DisplayHandler* displayHandler, TemperatureHandler* temperatureHandler) : -// displayHandler(displayHandler), -// temperatureHandler(temperatureHandler) {} -// void operator()(TemperatureType type, const char *state) const -// { -// #if BUILD_DISPLAY -// if (displayHandler) -// { -// if (type == TemperatureType::SLEEVE) -// { -// LogHandler::verbose(TagHandler::Main, "tempStateChangeCallBack heat: %s", state); -// displayHandler->setHeateState(state); -// if (temperatureHandler) -// displayHandler->setHeateStateShort(temperatureHandler->getShortSleeveControlStatus(state)); -// } -// else -// { -// LogHandler::verbose(TagHandler::Main, "tempStateChangeCallBack fan: %s", state); -// displayHandler->setFanState(state); -// } -// } -// #endif -// } -// private: -// DisplayHandler* displayHandler; -// TemperatureHandler* temperatureHandler; -// }; - -// class BatteryVoltageCallback -// { -// public: -// BatteryVoltageCallback(DisplayHandler* displayHandler, WebSocketHandler* webSocketHandler) : -// displayHandler(displayHandler), -// webSocketHandler(webSocketHandler) {} -// void operator()(float capacityRemainingPercentage, float capacityRemaining, float voltage, float temperature) const -// { -// #if BUILD_DISPLAY -// if (displayHandler) -// { -// displayHandler->setBatteryInformation(capacityRemainingPercentage, voltage, temperature); -// } -// #endif -// #if WIFI_TCODE -// if (webSocketHandler) -// { -// String statusJson("{\"batteryCapacityRemaining\":\"" + String(capacityRemaining) + "\", \"batteryCapacityRemainingPercentage\":\"" + String(capacityRemainingPercentage) + "\", \"batteryVoltage\":\"" + String(voltage) + "\", \"batteryTemperature\":\"" + String(temperature) + "\"}"); -// webSocketHandler->sendCommand("batteryStatus", statusJson.c_str()); -// } -// #endif -// } -// private: -// DisplayHandler* displayHandler; -// WebSocketHandler* webSocketHandler; -// }; - -// // #if WIFI_TCODE -// // class WifiStatusCallBack -// // { -// // public: -// // WifiStatusCallBack( -// // SettingsFactory* settingsFactory, -// // DisplayHandler* displayHandler, -// // MotionHandler* motionHandler, -// // ButtonHandler* buttonHandler, -// // VoiceHandler* voiceHandler) : -// // settingsFactory(settingsFactory), -// // displayHandler(displayHandler) {} -// // void operator()(WiFiStatus status, WiFiReason reason) const -// // { -// // if (status == WiFiStatus::CONNECTED) -// // { -// // LogHandler::debug(TagHandler::Main, "wifiStatusCallBack WiFiStatus::CONNECTED"); -// // if (reason == WiFiReason::AP_MODE) -// // { -// // LogHandler::debug(TagHandler::Main, "wifiStatusCallBack WiFiReason::AP_MODE"); -// // // if(bleConfigurationHandler) -// // // bleConfigurationHandler->stop(); // If a client connects to the ap stop the BLE to save memory. -// // } -// // } -// // else if(status == WiFiStatus::DISCONNECTED) -// // { -// // // wifi.dispose(); -// // // startApMode(); -// // LogHandler::debug(TagHandler::Main, "wifiStatusCallBack Not connected"); -// // if (reason == WiFiReason::NO_AP || reason == WiFiReason::UNKNOWN) -// // { -// // LogHandler::debug(TagHandler::Main, "wifiStatusCallBack WiFiReason::NO_AP || WiFiReason::UNKNOWN"); -// // startConfigMode( -// // settingsFactory->getWebServerPort(), -// // settingsFactory->getUdpServerPort(), -// // settingsFactory->getHostname(), -// // settingsFactory->getFriendlyName()); -// // } -// // else if (reason == WiFiReason::AUTH) -// // { -// // LogHandler::debug(TagHandler::Main, "wifiStatusCallBack WiFiReason::AUTH"); -// // LogHandler::warning(TagHandler::Main, "Connection auth failed: Resetting wifi password and restarting"); -// // settingsFactory->defaultValue(WIFI_PASS_SETTING); -// // ESP.restart(); -// // } -// // else if (reason == WiFiReason::AP_MODE) -// // { -// // LogHandler::debug(TagHandler::Main, "wifiStatusCallBack WiFiReason::AP_MODE"); -// // // #ifdef !ESP32_DA -// // // if(bleConfigurationHandler) -// // // bleConfigurationHandler->setup(); -// // // #endif -// // } -// // } -// // else if(status == WiFiStatus::IP) -// // { -// // #if BUILD_DISPLAY -// // if(displayHandler) -// // { -// // String ipaddress = wifi.ip().toString(); -// // displayPrint("Connected IP: " + ipaddress); -// // displayHandler->setLocalIPAddress(wifi.ip()); -// // } -// // #endif -// // } -// // } -// // private: - -// // DisplayHandler* displayHandler; -// // SettingsFactory* settingsFactory; -// // }; - -// class SettingsChangeCallback -// { -// public: -// SettingsChangeCallback( -// SettingsFactory* settingsFactory, -// WebSocketHandler* webSocketHandler, -// MotionHandler* motionHandler, -// ButtonHandler* buttonHandler, -// VoiceHandler* voiceHandler) : -// settingsFactory(settingsFactory), -// webSocketHandler(webSocketHandler), -// motionHandler(motionHandler), -// buttonHandler(buttonHandler), -// voiceHandler(voiceHandler) {} -// void operator()(const SettingProfile &profile, const char *settingThatChanged) const -// { -// LogHandler::verbose(TagHandler::Main, "settingChangeCallback: %s", settingThatChanged); -// if (profile == SettingProfile::System) -// { -// if (!strcmp(settingThatChanged, LOG_LEVEL_SETTING)) -// { -// LogHandler::setLogLevel(settingsFactory->getLogLevel()); -// } -// else if (!strcmp(settingThatChanged, LOG_INCLUDETAGS)) -// { -// LogHandler::setIncludes(settingsFactory->getLogIncludes()); -// } -// else if (!strcmp(settingThatChanged, LOG_EXCLUDETAGS)) -// { -// LogHandler::setExcludes(settingsFactory->getLogExcludes()); -// } -// } -// else if (profile == SettingProfile::MotionProfile) -// { -// if (strcmp(settingThatChanged, MOTION_PROFILE_SELECTED_INDEX) == 0 || strcmp(settingThatChanged, MOTION_PROFILES) == 0) { -// motionHandler->setMotionChannels(SettingsHandler::getMotionChannels()); -// //} else if(strcmp(settingThatChanged, "motionChannels") == 0) { -// // motionHandler->setMotionChannels(SettingsHandler::getGetMotionChannels()()); -// } else if (strcmp(settingThatChanged, MOTION_ENABLED) == 0) { -// LogHandler::verbose(TagHandler::Main, "MOTION_ENABLED: %d", SettingsHandler::getMotionEnabled()); -// motionHandler->setEnabled(SettingsHandler::getMotionEnabled()); -// } -// // else if(strcmp(settingThatChanged, "motionAmplitudeGlobal") == 0) -// // motionHandler->setAmplitude(SettingsHandler::getGetMotionAmplitudeGlobal()()); -// // else if(strcmp(settingThatChanged, "motionOffsetGlobal") == 0) -// // motionHandler->setOffset(SettingsHandler::getGetMotionOffsetGlobal()()); -// // else if(strcmp(settingThatChanged, "motionPeriodGlobal") == 0) -// // motionHandler->setPeriod(SettingsHandler::getGetMotionPeriodGlobal()()); -// // else if(strcmp(settingThatChanged, "motionUpdateGlobal") == 0) -// // motionHandler->setUpdate(SettingsHandler::getGetMotionUpdateGlobal()()); -// // else if(strcmp(settingThatChanged, "motionPhaseGlobal") == 0) -// // motionHandler->setPhase(SettingsHandler::getGetMotionPhaseGlobal()()); -// // else if(strcmp(settingThatChanged, "motionReversedGlobal") == 0) -// // motionHandler->setReverse(SettingsHandler::getGetMotionReversedGlobal()()); -// // else if(strcmp(settingThatChanged, "motionAmplitudeGlobalRandom") == 0) -// // motionHandler->setAmplitudeRandom(SettingsHandler::getGetMotionAmplitudeGlobalRandom()()); -// // else if(strcmp(settingThatChanged, "motionAmplitudeGlobalRandomMin") == 0) -// // motionHandler->setAmplitudeRandomMin(SettingsHandler::getGetMotionAmplitudeGlobalRandomMin()()); -// // else if(strcmp(settingThatChanged, "motionAmplitudeGlobalRandomMax") == 0) -// // motionHandler->setAmplitudeRandomMax(SettingsHandler::getGetMotionAmplitudeGlobalRandomMax()()); -// // else if(strcmp(settingThatChanged, "motionPeriodGlobalRandom") == 0) -// // motionHandler->setPeriodRandom(SettingsHandler::getGetMotionPeriodGlobalRandom()()); -// // else if(strcmp(settingThatChanged, "motionPeriodGlobalRandomMin") == 0) -// // motionHandler->setPeriodRandomMin(SettingsHandler::getGetMotionPeriodGlobalRandomMin()()); -// // else if(strcmp(settingThatChanged, "motionPeriodGlobalRandomMax") == 0) -// // motionHandler->setPeriodRandomMax(SettingsHandler::getGetMotionPeriodGlobalRandomMax()()); -// // else if(strcmp(settingThatChanged, "motionOffsetGlobalRandom") == 0) -// // motionHandler->setOffsetRandom(SettingsHandler::getGetMotionOffsetGlobalRandom()()); -// // else if(strcmp(settingThatChanged, "motionOffsetGlobalRandomMin") == 0) -// // motionHandler->setOffsetRandomMin(SettingsHandler::getGetMotionOffsetGlobalRandomMin()()); -// // else if(strcmp(settingThatChanged, "motionOffsetGlobalRandomMax") == 0) -// // motionHandler->setOffsetRandomMax(SettingsHandler::getGetMotionOffsetGlobalRandomMax()()); -// // else if(strcmp(settingThatChanged, "motionRandomChangeMin") == 0) -// // motionHandler->setMotionRandomChangeMin(SettingsHandler::getGetMotionRandomChangeMin()()); -// // else if(strcmp(settingThatChanged, "motionRandomChangeMax") == 0) -// // motionHandler->setMotionRandomChangeMax(SettingsHandler::getGetMotionRandomChangeMax()()); -// } -// else if (voiceHandler && profile == SettingProfile::Voice) -// { -// if (strcmp(settingThatChanged, "voiceMuted") == 0) -// { -// voiceHandler->setMuteMode(settingsFactory->getVoiceMuted()); -// } -// else if (strcmp(settingThatChanged, "voiceVolume") == 0) -// { -// voiceHandler->setVolume(settingsFactory->getVoiceVolume()); -// } -// else if (strcmp(settingThatChanged, "voiceWakeTime") == 0) -// { -// voiceHandler->setWakeTime(settingsFactory->getVoiceWakeTime()); -// } -// } -// else if (buttonHandler && profile == SettingProfile::Button) -// { -// if (strcmp(settingThatChanged, "bootButtonCommand") == 0) -// buttonHandler->updateBootButtonCommand(settingsFactory->getBootButtonCommand()); -// else if (strcmp(settingThatChanged, "analogButtonCommands") == 0) -// { -// buttonHandler->updateAnalogButtonCommands(settingsFactory->getButtonSets()); -// } -// else if (strcmp(settingThatChanged, "buttonAnalogDebounce") == 0) -// { -// buttonHandler->updateAnalogDebounce(settingsFactory->getButtonAnalogDebounce()); -// } -// } -// else if (profile == SettingProfile::ChannelRanges) -// { -// if (strcmp(settingThatChanged, CHANNEL_PROFILE) == 0) { -// // TODO add channe; specific updates when moving to its own save...maybe... -// motionHandler->updateChannelRanges(); -// } else if (strcmp(settingThatChanged, "channelRangesEnabled") == 0) { -// webSocketHandler->sendCommand("channelRangesEnabled", SettingsHandler::getChannelRangesEnabled() ? "true" : "false"); -// } - -// } -// } -// private: -// SettingsFactory* settingsFactory; -// WebSocketHandler* webSocketHandler; -// MotionHandler* motionHandler; -// ButtonHandler* buttonHandler; -// VoiceHandler* voiceHandler; -// }; -// // class TCodeCommandCallbackPassthrough -// // { -// // public: -// // TCodeCommandCallbackPassthrough( -// // SystemCommandHandler* systemCommandHandler, -// // WebSocketHandler* webSocketHandler, -// // Udphandler* udpHandler, -// // BLEHandler* bleHandler) -// // : systemCommandHandler(systemCommandHandler), -// // udpHandler(udpHandler), -// // bleHandler(bleHandler), -// // webSocketHandler(webSocketHandler) {} -// // void operator()(const char* in) const { - -// // if (systemCommandHandler->isCommand(in)) -// // { -// // // This seems wrong but since we are only calling this from one place its fine for now. -// // char temp[strlen(in) + 2]; -// // temp[0] = {0}; -// // strcpy(temp, in); -// // strcat(temp, "\n"); -// // ////////////////////////////////////////////////////////////////////////////////////// -// // #if BLUETOOTH_TCODE -// // if (btHandler && btHandler->isConnected()) -// // btHandler->send(temp); -// // #endif -// // #if BLE_TCODE - -// // #endif -// // #if WIFI_TCODE -// // if (webSocketHandler) -// // webSocketHandler->send(temp); -// // if (udpHandler) -// // udpHandler->send(temp); -// // #endif -// // if (Serial) -// // Serial.println(temp); -// // } -// // if (systemCommandHandler->isCommand(in)) -// // { -// // systemCommandHandler->process(in); -// // } -// // else -// // { -// // #if BLUETOOTH_TCODE -// // if (btHandler && btHandler->isConnected()) -// // btHandler->send(in); -// // #endif -// // #if BLE_TCODE -// // if (bleHandler && bleHandler->isConnected()) -// // bleHandler->send(in); -// // #endif -// // #if WIFI_TCODE -// // if (webSocketHandler) -// // webSocketHandler->send(in); -// // if (udpHandler) -// // udpHandler->send(in); -// // #endif -// // if (Serial) -// // Serial.println(in); -// // } -// // } -// // private: -// // SystemCommandHandler* systemCommandHandler; -// // Udphandler* udpHandler; -// // BLEHandler* bleHandler; -// // DisplayHandler* displayHandler; -// // WebSocketHandler* webSocketHandler; -// // TemperatureHandler* temperatureHandler; -// // MotionHandler* motionHandler; -// // ButtonHandler* buttonHandler; -// // VoiceHandler* voiceHandler; -// // }; - -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// \ No newline at end of file diff --git a/ESP32/src/MDNSHandler.hpp b/ESP32/src/MDNSHandler.hpp deleted file mode 100644 index e27699b..0000000 --- a/ESP32/src/MDNSHandler.hpp +++ /dev/null @@ -1,80 +0,0 @@ -/* MIT License - -Copyright (c) 2026 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once -#if ESP8266 == 1 -#include -#include -#include -#include -#include -#else -#include -#endif -#include "settings/SettingsHandler.h" -// #include "LogHandler.h" -class MDNSHandler -{ -public: - void setup(const char* hostName, const char* friendlyName, const int udpPort, const uint8_t webPort = 0, const uint8_t securePort = 0) - { - if (!MDNSInitialized) - startMDNS(hostName, friendlyName, udpPort, webPort, securePort); - } - void stop() - { - if (MDNSInitialized) - { - MDNS.end(); - MDNSInitialized = false; - } - } - -private: - static constexpr Tags::tag_t _TAG = Tags::Mdns; - bool MDNSInitialized = false; - void startMDNS(const char* hostName, const char* friendlyName, const int udpPort, const uint8_t webPort = 0, const uint8_t securePort = 0) - { - LogHandler::info(_TAG, "Setting up MDNS"); - if (MDNSInitialized) - MDNS.end(); - if (!MDNS.begin(hostName)) - { - printf("MDNS Init failed"); - return; - } - MDNS.setInstanceName(friendlyName); - if (webPort) - MDNS.addService("http", "tcp", webPort); - if (securePort) - MDNS.addService("https", "tcp", securePort); - if (webPort || securePort) - { - char hostLen = strlen(hostName) + 7; - char domainName[hostLen]; - sprintf(domainName, "%s.local", hostName); - SettingsHandler::printWebAddress(domainName); - } - MDNS.addService("tcode", "udp", udpPort); - MDNSInitialized = true; - } -}; \ No newline at end of file diff --git a/ESP32/src/MessageHandler.h b/ESP32/src/MessageHandler.h deleted file mode 100644 index 78f1e46..0000000 --- a/ESP32/src/MessageHandler.h +++ /dev/null @@ -1,72 +0,0 @@ -#ifndef _MESSAGE_HANDLER_H_ -#define _MESSAGE_HANDLER_H_ - -#include - -#include "logging/TagHandler.h" - -namespace Messages { -struct message_t -{ - uint8_t id; - const char* message; - union - { - /* data */ - uint32_t u_data; - int32_t s_data; - float f_data; - }; -}; - -class MessageSink -{ - protected: - virtual void sink(message_t) = 0; - private: - uint32_t _tags; - public: - MessageSink(uint32_t tags = ALL_TAGS) : _tags(tags) {} - - void handle_msg(uint32_t tags, message_t msg) - { - if (_tags & tags) - { - this->sink(msg); - } - } -}; - -class MessageHandler -{ - private: - std::vector> _sinks; - public: - void push(std::unique_ptr sink) - { - _sinks.push_back(std::move(sink)); - } - - void send(uint32_t tags, const char* msg) - { - message_t _msg{msg, {{0}}}; - send(tags, _msg); - } - - void send(uint32_t tags, message_t msg) - { - for(auto&& sink : _sinks) - { - sink->handle_msg(tags, msg); - } - } - - MessageHandler* getInstance() - { - static MessageHandler instance; - return &instance; - } -}; -}; - -#endif // _MESSAGE_HANDLER_H_ diff --git a/ESP32/src/NetworkHandler.h b/ESP32/src/NetworkHandler.h deleted file mode 100644 index 8f6f1bf..0000000 --- a/ESP32/src/NetworkHandler.h +++ /dev/null @@ -1,72 +0,0 @@ - - -class NetworkHandler : public Task -{ -private: - const bool &apMode; - const int &port; - const int &udpPort; - const char *hostname; - const char *friendlyName; - MDNSHandler mdnsHandler; - HTTPBase *webHandler = nullptr; - WebSocketBase *webSocketHandler = nullptr; - TaskHandle_t httpsTask; - static constexpr Tags::tag_t _TAG = Tags::Network; - -public: - NetworkHandler(const bool &apMode, const int &port, const int &udpPort, const char *hostname, const char *friendlyName) - : apMode(apMode), port(port), udpPort(udpPort), hostname(hostname), friendlyName(friendlyName) - { - } - void setup() override - { - LogHandler::info(_TAG, "Setting up network handler"); - // Network setup code here - } - - void loop() override - { - // Network handling code here - } - - void start() - { - if ((MODULE_CURRENT != ModuleType::WROOM32 || (!bluetoothEnabled && !bleEnabled)) && !webHandler) - { - displayPrint("Starting web server"); -#if !SECURE_WEB - webHandler = new WebHandler(); - webSocketHandler = new WebSocketHandler(); -#else - LogHandler::debug(Tags::Main, "Start https task"); - webHandler = new HTTPSHandler(); - webSocketHandler = new SecureWebSocketHandler(); - auto httpsStatus = xTaskCreateUniversal( - HTTPSHandler::startLoop, /* Function to implement the task */ - "HTTPSTask", /* Name of the task */ - 8192 * 3, /* Stack size in words */ - webHandler, /* Task input parameter */ - 3, /* Priority of the task */ - &httpsTask, /* Task handle. */ - -1); /* Core where the task should run */ - if (httpsStatus != pdPASS) - { - LogHandler::error(Tags::Main, "Could not start https task."); - } -#endif - webHandler->setup(port, webSocketHandler, apMode); - LogHandler::debug(Tags::Main, "Web DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - } - else - { - displayPrint("WebServer disabled"); - LogHandler::info(Tags::Main, "WebServer disabled due to bluetooth and chip model"); - } - if (!apMode) - { // mdns breaks apmode? - mdnsHandler.setup(hostname, friendlyName, port, udpPort); - LogHandler::debug(Tags::Main, "MDNS DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - } - } -} \ No newline at end of file diff --git a/ESP32/src/OTAHandler.h b/ESP32/src/OTAHandler.h deleted file mode 100644 index 95134e3..0000000 --- a/ESP32/src/OTAHandler.h +++ /dev/null @@ -1,87 +0,0 @@ -/* MIT License - -Copyright (c) 2026 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - - -#include -#include - -class OTAHandler -{ - public: - void setup() - { - // Port defaults to 3232 - // ArduinoOTA.setPort(3232); - - // Hostname defaults to esp3232-[MAC] - // ArduinoOTA.setHostname("myesp32"); - - // No authentication by default - // ArduinoOTA.setPassword("admin"); - - // Password can be set with it's md5 value as well - // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3 - // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3"); - - ArduinoOTA - .onStart([]() - { - String type; - if (ArduinoOTA.getCommand() == U_FLASH) - type = "sketch"; - else // U_SPIFFS - type = "filesystem"; - - // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() - Serial.println("Start updating " + type); - SPIFFS.end(); - }) - .onEnd([]() - { - if(!SPIFFS.begin(true)) - { - Serial.println("An Error has occurred while mounting SPIFFS"); - } - Serial.println("\nEnd"); - }) - .onProgress([](unsigned int progress, unsigned int total) - { - Serial.printf("Progress: %u%%\r", (progress / (total / 100))); - }) - .onError([](ota_error_t error) - { - Serial.printf("Error[%u]: ", error); - if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); - else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); - else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); - else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); - else if (error == OTA_END_ERROR) Serial.println("End Failed"); - }); - - ArduinoOTA.begin(); - } - - void handle() - { - ArduinoOTA.handle(); - } -}; \ No newline at end of file diff --git a/ESP32/src/SerialHandler.h b/ESP32/src/SerialHandler.h deleted file mode 100644 index 6c20bff..0000000 --- a/ESP32/src/SerialHandler.h +++ /dev/null @@ -1,122 +0,0 @@ -/* MIT License - -Copyright (c) 2026 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once -#include -#include -#include "SettingsHandler.h" -#include "logging/LogHandler.h" -#include "TagHandler.h" -#include "TCodeInterface.h" - -#if ARDUINO_USB_CDC_ON_BOOT //Serial used from Native_USB_CDC | HW_CDC_JTAG -#if ARDUINO_USB_MODE // Hardware CDC mode -// Arduino Serial is the HW JTAG CDC device -#define SerialType HWCDC -#else // !ARDUINO_USB_MODE -- Native USB Mode -// Arduino Serial is the Native USB CDC device -#define SerialType USBCDC -#endif // ARDUINO_USB_MODE -#else // !ARDUINO_USB_CDC_ON_BOOT -- Serial is used from UART0 -// if not using CDC on Boot, Arduino Serial is the UART0 device -#define SerialType HardwareSerial -#endif // ARDUINO_USB_CDC_ON_BOOT - -class SerialHandler : public TCodeInterface -{ - public: - bool setup(SerialType& serial = Serial, int baud = 115200, int rxpin = -1, int txpin = -1) - { - LogHandler::info(m_TAG, "Starting Serial baud: %i txpin: %i rxpin: %i", baud, txpin, rxpin); - m_serial = serial; -#if ARDUINO_USB_CDC_ON_BOOT //Serial used from Native_USB_CDC | HW_CDC_JTAG -#if ARDUINO_USB_MODE // Hardware CDC mode -// Arduino Serial is the HW JTAG CDC device - m_serial.begin(baud); -#else // !ARDUINO_USB_MODE -- Native USB Mode - m_serial.begin(baud, 134217756UL, rxpin, txpin); -#endif // ARDUINO_USB_MODE -#else // !ARDUINO_USB_CDC_ON_BOOT -- Serial is used from UART0 - m_serial.begin(baud, 134217756UL, rxpin, txpin); -#endif // ARDUINO_USB_CDC_ON_BOOT - if(!m_serial.availableForWrite()) - { - LogHandler::error(m_TAG, "Serial not available"); - return false; - } - LogHandler::info(m_TAG, "Serial Listening"); - // SettingsFactory* m_settingsFactory = SettingsFactory::getInstance(); - if(!m_serial) - return false; - initialized = true; - return true; - } - - size_t available() override - { - if(!initialized) - { - return 0; - } - return m_serial.available(); - } - - void send(const char* in) override - { - if(initialized) - { - LogHandler::verbose(m_TAG, "[send] %s", in); - m_serial.println(in); - } - } - - size_t read(char* buf) override - { - if (!initialized) - { - buf[0] = {0}; - return 0; - } - size_t len = m_serial.readBytesUntil('\n', buf, MAX_COMMAND); - if(len < MAX_COMMAND) - { - buf[len] = '\n'; - return len +1; - } - return len; - } -private: - bool initialized = false; - const char* m_TAG = TagHandler::SerialHandler; - SerialType& m_serial = Serial; - HardwareSerial& getSerial(uint8_t index) - { - switch(index) - { - case 0: - return Serial0; - case 1: - return Serial1; - } - return Serial0; - } -}; \ No newline at end of file diff --git a/ESP32/src/SettingsHandler.h b/ESP32/src/SettingsHandler.h deleted file mode 100644 index 5f031d5..0000000 --- a/ESP32/src/SettingsHandler.h +++ /dev/null @@ -1,2652 +0,0 @@ -/* MIT License - -Copyright (c) 2026 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include "soc/rtc.h" -// // #include "LogHandler.h" -#include "utils.h" -#include "logging/TagHandler.h" -#include "struct/voice.h" -#include "struct/motionProfile.h" -#include "struct/channel.h" -#include "struct/motionChannel.h" -#include "struct/buttonSet.h" -#include "enum.h" -#include "constants.h" -#include "channelMap.hpp" -#include "settingConstants.h" -#include "settingsFactory.h" -#include "callback.h" - -#define DESERIALIZE_SIZE 32768 -#define SERIALIZE_SIZE 24576 - -// using SETTING_STATE_FUNCTION_PTR_T = void (*)(const char *group, const char *settingNameThatChanged); - -class SettingsHandler -{ -public: - static bool initialized; - static int restartInSecs; - static inline const char* errorInfo[10]; - static inline size_t errorInfoIndex = 0; - static bool saving; - static bool motionPaused; - static bool fullBuild; - static inline bool channelRangesEnabled = true; - static LogLevel logLevel; - static std::vector systemI2CAddresses; - - static ChannelMap channelMap; - static BuildFeature buildFeatures[(int)BuildFeature::MAX_FEATURES]; - - static inline MotionProfile *motionProfiles; - static inline ButtonSet *buttonSets; - - // static bool staticIP; - static char currentIP[IP_ADDRESS_LEN]; - static char currentGateway[IP_ADDRESS_LEN]; - static char currentSubnet[IP_ADDRESS_LEN]; - static char currentDns1[IP_ADDRESS_LEN]; - static char currentDns2[IP_ADDRESS_LEN]; - - static bool apMode; - - // template::value || std::is_integral::value || std::is_enum::value || std::is_floating_point::value || std::is_same::value>> - // static void getValue(const char* name, T &value) - // { - // m_settingsFactory->getValue(name, value); - // } - - // static void getValue(const char* name, char* value, size_t len) - // { - // m_settingsFactory->getValue(name, value, len); - // } - - // static void defaultValue(const char* name) - // { - // m_settingsFactory->defaultValue(name); - // } - - static void init() - { - m_settingsFactory = SettingsFactory::getInstance(); - motionProfiles = m_settingsFactory->getMotionProfiles(); - buttonSets = m_settingsFactory->getButtonSets(); - setBuildFeatures(); - setMotorType(); - - // loadWifiInfo(false); - // loadSettings(false); - loadChannels(false); - loadMotionProfiles(false); - loadButtons(false); - - LogHandler::debug(_TAG, "Last reset reason: %s", machine_reset_cause()); - initialized = true; - } - - static void setMessageCallback(SettingsChangeCallback f) - { - LogHandler::debug(_TAG, "setMessageCallback"); - if (f == nullptr) - { - message_callback = 0; - } - else - { - message_callback = f; - } - } - - /// The errors added will only print if the main setup fails for some reason. - /// Otherwise use LogHandler::error - static void addPersistentError(const char* value) - { - if(errorInfoIndex >= 10) - { - LogHandler::error("Too many errors in buffer! The last added erro is: %s", value); - return; - } - //LogHandler::debug("Add persistent error: %s", value); - errorInfo[errorInfoIndex] = value; - errorInfoIndex++; - } - - static void printFree(bool forcePrint = false) - { - if (forcePrint || LogHandler::getLogLevel() == LogLevel::DEBUG) - { - uint32_t freeHEap = ESP.getFreeHeap(); - uint32_t heapSize = ESP.getHeapSize(); - // https://esp32.com/viewtopic.php?t=27780 - // https://github.com/espressif/esp-idf/blob/master/components/heap/include/esp_heap_caps.h#L20-L37 - // esp_get_free_internal_heap_size - Serial.printf("Used heap INTERNAL: %u/%u Free: %u\n", heapSize - freeHEap, heapSize, freeHEap); - Serial.printf("Free psram: %u\n", ESP.getFreePsram()); - Serial.printf("Total Psram: %u\n", ESP.getPsramSize()); - Serial.printf("LittleFS used: %i\n", LittleFS.usedBytes()); - Serial.printf("LittleFS total: %i\n", LittleFS.totalBytes()); - // LogHandler::debug(_TAG, "Used Psram: %u/%u", ESP.getPsramSize() - ESP.getFreePsram(), ESP.getPsramSize()); - Serial.printf("Sketch size: %u\n", ESP.getSketchSize()); - Serial.printf("Sketch free space: %u\n", ESP.getFreeSketchSpace()); - Serial.printf("DRAM heaps free %u\n", heap_caps_get_free_size(MALLOC_CAP_8BIT)); - Serial.printf("IRAM %u\n", heap_caps_get_free_size(MALLOC_CAP_32BIT)); - Serial.printf("FREE_HEAP Default %u\n", esp_get_free_heap_size()); - Serial.printf("MIN_FREE_HEAP %u\n", esp_get_minimum_free_heap_size()); - // uxTaskGetStackHighWaterMark - } - } - - static void restart(const int delayInSec = 0) - { - LogHandler::info(_TAG, "Schedule device restart in %ld seconds", delayInSec); - // Restart in main task loop - restartRequired = delayInSec; - } - - static void printWebAddress(const char *hostAddress) - { - char webServerportString[6]; - int webServerPort = 0; - m_settingsFactory->getValue(WEBSERVER_PORT, webServerPort); - sprintf(webServerportString, ":%d", webServerPort); - LogHandler::info(_TAG, "Web address: http://%s%s", hostAddress, webServerPort == 80 ? "" : webServerportString); - } - - static bool saveAll(JsonObject obj = JsonObject()) - { - if (!m_settingsFactory->saveAllToDisk(obj) || !saveMotionProfiles(obj) || !saveButtons(obj)) - return false; - return true; - } - - static bool saveAll(const String &data) - { - LogHandler::debug(_TAG, "Save frome string"); - printFree(); - JsonDocument doc; - - DeserializationError error = deserializeJson(doc, data); - if (error) - { - LogHandler::error(_TAG, "Settings save: Deserialize error: %s", error.c_str()); - return false; - } - printFree(); - JsonObject obj = doc.as(); - if (!saveAll(obj)) - { - LogHandler::error(_TAG, "Settings save: save error"); - return false; - } - return true; - } - - static void getWifiInfo(char *buf) - { - JsonDocument doc; // 100 - - JsonDocument wifiDoc = m_settingsFactory->getNetworkSettings(); - - doc.set(wifiDoc); - const char *wifiPass = doc[WIFI_PASS_SETTING]; - if (strcmp(wifiPass, WIFI_PASS_DONOTCHANGE_DEFAULT)) - { - doc[WIFI_PASS_SETTING] = DECOY_PASS; // Never set to actual password - } - else - { - doc[WIFI_PASS_SETTING] = WIFI_PASS_DONOTCHANGE_DEFAULT; - } - const char *apPass = doc[AP_MODE_PASS]; - if (strcmp(apPass, AP_MODE_PASS_DEFAULT)) - { - doc[AP_MODE_PASS] = DECOY_PASS; // Never set to actual password - } - else - { - doc[AP_MODE_PASS] = AP_MODE_PASS_DEFAULT; - } - - String output; - serializeJson(doc, output); - doc.clear(); - if (LogHandler::getLogLevel() == LogLevel::VERBOSE) - Serial.printf("Network Info: %s\n", output.c_str()); - buf[0] = {0}; - strcpy(buf, output.c_str()); - } - - static void getSystemInfo(String &buf) - { - JsonDocument doc; // 3500 - - doc["esp32Version"] = FIRMWARE_VERSION_NAME; - doc["esp32VersionNum"] = FIRMWARE_VERSION; - doc["TCodeVersion"] = m_settingsFactory->getTcodeVersion(); - doc["lastRebootReason"] = machine_reset_cause(); - doc["channelRangesEnabled"] = getChannelRangesEnabled(); - - JsonArray logLevels = doc["logLevels"].to(); - JsonObject logLevelNone = logLevels.add(); - logLevelNone["name"] = "None"; - logLevelNone["value"] = LogLevel::NONE; - JsonObject logLevelError = logLevels.add(); - logLevelError["name"] = "Error"; - logLevelError["value"] = LogLevel::ERROR; - JsonObject logLevelWarning = logLevels.add(); - logLevelWarning["name"] = "Warning"; - logLevelWarning["value"] = LogLevel::WARNING; - JsonObject logLevelInfo = logLevels.add(); - logLevelInfo["name"] = "info"; - logLevelInfo["value"] = LogLevel::INFO; - JsonObject logLevelDebug = logLevels.add(); - logLevelDebug["name"] = "Debug"; - logLevelDebug["value"] = LogLevel::DEBUG; - JsonObject logLevelVerbose = logLevels.add(); - logLevelVerbose["name"] = "Verbose"; - logLevelVerbose["value"] = LogLevel::VERBOSE; - - JsonArray tcodeVersions = doc["tcodeVersions"].to(); - JsonObject v03 = tcodeVersions.add(); - v03["name"] = "v0.3"; - v03["value"] = TCodeVersion::v0_3; - // JsonObject v04 = tcodeVersions.add(); - // v04["name"] = "v0.4 (Experimental)"; - // v04["value"] = TCodeVersion::v0_4; - JsonArray boardTypes = doc["boardTypes"].to(); -#if CONFIG_IDF_TARGET_ESP32 - JsonObject devkit = boardTypes.add(); - devkit["name"] = "Devkit"; - devkit["value"] = (uint8_t)BoardType::DEVKIT; -#if MOTOR_TYPE == 0 - JsonObject SR6MB = boardTypes.add(); - SR6MB["name"] = "SR6MB"; - SR6MB["value"] = (uint8_t)BoardType::CRIMZZON; - JsonObject INControl = boardTypes.add(); - INControl["name"] = "IN-Control"; - INControl["value"] = (uint8_t)BoardType::ISAAC; -#elif MOTOR_TYPE == 1 - JsonObject SSR1PCB = boardTypes.add(); - SSR1PCB["name"] = "SSR1PCB"; - SSR1PCB["value"] = (uint8_t)BoardType::SSR1PCB; -#endif -#elif CONFIG_IDF_TARGET_ESP32S3 -#ifdef S3_ZERO - JsonObject S3_Zero = boardTypes.add(); - S3_Zero["name"] = "S3 Zero"; - S3_Zero["value"] = (uint8_t)BoardType::ZERO; -#else - JsonObject N8R8 = boardTypes.add(); - N8R8["name"] = "S3 N8R8"; - N8R8["value"] = (uint8_t)BoardType::N8R8; -#endif -#if MOTOR_TYPE == 0 - JsonObject SR6PCB = boardTypes.add(); - SR6PCB["name"] = "SR6PCB"; - SR6PCB["value"] = (uint8_t)BoardType::SR6PCB; -#endif -#endif - JsonArray wifiBands = doc["wifiBands"].to(); - JsonObject modeAuto = wifiBands.add(); - modeAuto["name"] = "Auto"; - modeAuto["value"] = (uint8_t)WifiBand::AUTO; - JsonObject mode24g = wifiBands.add(); - mode24g["name"] = "2.4ghz"; - mode24g["value"] = (uint8_t)WifiBand::MODE24ghz; - - #if SOC_WIFI_SUPPORT_5G - JsonObject mode5g = wifiBands.add(); - mode5g["name"] = "5ghz"; - mode5g["value"] = (uint8_t)WifiBand::MODE5ghz; - #endif - #if SOC_WIFI_SUPPORT_6G - JsonObject mode6g = wifiBands.add(); - mode6g["name"] = "56ghz"; - c5Demode6gvkit["value"] = (uint8_t)WifiMode::MODE6ghz; - #endif - int motorType = MOTOR_TYPE_DEFAULT; - m_settingsFactory->getValue(MOTOR_TYPE_SETTING, motorType); - doc["motorType"] = motorType; - JsonArray buildFeaturesJsonArray = doc["buildFeatures"].to(); - for (BuildFeature value : buildFeatures) - { - buildFeaturesJsonArray.add((int)value); - } - doc["moduleType"] = (int)MODULE_CURRENT; - - JsonArray availableTagsJsonArray = doc["availableTags"].to(); - for (const char *tag : Tags::AvailableTags) - { - availableTagsJsonArray.add(tag); - } - JsonArray systemI2CAddressesJsonArray = doc["systemI2CAddresses"].to(); - systemI2CAddressesJsonArray.add("0x0"); - for (int value : systemI2CAddresses) - { - char buf[10]; - hexToString(value, buf); - systemI2CAddressesJsonArray.add(buf); - } - - int deviceType = DEVICE_TYPE_DEFAULT; - m_settingsFactory->getValue(DEVICE_TYPE, deviceType); - JsonArray deviceTypes = doc["deviceTypes"].to(); - JsonObject defaultDevice = deviceTypes.add(); -#if MOTOR_TYPE == 0 - defaultDevice["name"] = "OSR"; - defaultDevice["value"] = DeviceType::OSR; - JsonObject SR6 = deviceTypes.add(); - SR6["name"] = "SR6"; - SR6["value"] = DeviceType::SR6; - JsonObject TVIBE = deviceTypes.add(); - TVIBE["name"] = "TVIBE"; - TVIBE["value"] = DeviceType::TVIBE; -#elif MOTOR_TYPE == 1 - defaultDevice["name"] = "SSR1"; - defaultDevice["value"] = DeviceType::SSR1; - JsonArray encoderTypes = doc["encoderTypes"].to(); - JsonObject defaultEncoder = encoderTypes.add(); - defaultEncoder["name"] = "NONE"; - defaultEncoder["value"] = BLDCEncoderType::NONE; - if(static_cast(deviceType) != DeviceType::SSR2) - { - JsonObject MT6701 = encoderTypes.add(); - MT6701["name"] = "MT6701 SSI"; - MT6701["value"] = BLDCEncoderType::MT6701; - JsonObject PWM = encoderTypes.add(); - PWM["name"] = "PWM"; - PWM["value"] = BLDCEncoderType::PWM; - } - JsonObject SPI = encoderTypes.add(); - SPI["name"] = "SPI"; - SPI["value"] = BLDCEncoderType::SPI; -#endif - - JsonArray bleDeviceTypes = doc["bleDeviceTypes"].to(); - JsonObject defaultBleDevice = bleDeviceTypes.add(); - defaultBleDevice["name"] = "TCode"; - defaultBleDevice["value"] = BLEDeviceType::TCODE; - JsonObject loveDevice = bleDeviceTypes.add(); - loveDevice["name"] = "Love"; - loveDevice["value"] = BLEDeviceType::LOVE; - JsonObject hcDevice = bleDeviceTypes.add(); - // HC has an unknown formatting. - hcDevice["name"] = "HC"; - hcDevice["value"] = BLEDeviceType::HC; - - JsonArray bleLoveDevices = doc["bleLoveDeviceTypes"].to(); - JsonObject defaultLoveDevice = bleLoveDevices.add(); - defaultLoveDevice["name"] = "Edge"; - defaultLoveDevice["value"] = BLELoveDeviceType::EDGE; - - JsonArray availableChannels = doc["availableChannels"].to(); - channelMap.serialize(availableChannels); - doc[MOTION_ENABLED] = getMotionEnabled(); - // int motionProfileSelectedIndex = MOTION_PROFILE_SELECTED_INDEX_DEFAULT; - // m_settingsFactory->getValue(MOTION_PROFILE_SELECTED_INDEX, motionProfileSelectedIndex); - doc[MOTION_PROFILE_SELECTED_INDEX] = motionSelectedProfileIndex; - - JsonArray availableTimers = doc["availableTimers"].to(); - JsonArray timerChannels = doc["timerChannels"].to(); - JsonObject timerChannelNoneObj = timerChannels.add(); - timerChannelNoneObj["name"] = "None"; - timerChannelNoneObj["value"] = ESPTimerChannelNum::NONE; - PinMap *pinMap = m_settingsFactory->getPins(); - for (size_t i = 0; i < MAX_TIMERS; i++) - { - JsonObject timerObj = availableTimers.add(); - ESPTimer *timer = pinMap->getTimer(i); - timerObj["id"] = timer->id; - timerObj["name"] = timer->name; - timerObj["value"] = i; - timerObj["pwmDriver"] = (int8_t)timer->pwmDriver; - String driverKey = String(timer->id); - driverKey.replace("FREQUENCY", "DRIVER"); - timerObj["driverKey"] = driverKey; - for (size_t j = 0; j < 2; j++) - { - JsonObject timerChannelObj = timerChannels.add(); - timerChannelObj["name"] = timer->channels[j].name; - timerChannelObj["value"] = timer->channels[j].channel; - } - } - doc["mcpwmMaxOutputs"] = 12; -#if CONFIG_IDF_TARGET_ESP32 - doc["ledcMaxOutputs"] = 16; -#else - doc["ledcMaxOutputs"] = 8; -#endif - - doc["localIP"] = currentIP; - doc["gateway"] = currentGateway; - doc["subnet"] = currentSubnet; - doc["dns1"] = currentDns1; - doc["dns2"] = currentDns2; // Not being used currently - char macTemp[18] = {0}; -#ifdef ESP_ARDUINO3 - strlcpy(macTemp, Network.macAddress().c_str(), sizeof(macTemp)); -#else - strlcpy(macTemp, WiFi.macAddress().c_str(), sizeof(macTemp)); -#endif - doc["mac"] = macTemp; - - doc["chipModel"] = ESP.getChipModel(); - doc["chipRevision"] = ESP.getChipRevision(); - doc["chipCores"] = ESP.getChipCores(); - uint32_t chipId = 0; - for (int i = 0; i < 17; i = i + 8) - { - chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; - } - doc["chipID"] = chipId; - - doc["maxPWMResolution"] = MAX_PWM_RESOLUTION; - doc["apbClockFrequency"] = rtc_clk_apb_freq_get(); - doc["decoyPass"] = DECOY_PASS; - doc["apMode"] = apMode; - doc["defaultIP"] = m_settingsFactory->getAPModeIP(); - // String output; - serializeJson(doc, buf); - doc.clear(); - if (LogHandler::getLogLevel() == LogLevel::VERBOSE) - Serial.printf("SystemInfo: %s\n", buf.c_str()); - // buf[0] = {0}; - // strcpy(buf, output.c_str()); - } - - static bool loadButtons(bool loadDefault, JsonObject json = JsonObject()) - { - LogHandler::info(_TAG, "Loading buttons"); - return loadSettingsJson(BUTTON_SETTINGS_PATH, loadDefault, m_buttonsMutex, [](const JsonObject json, bool &mutableLoadDefault) -> bool - { - - // const bool bootButtonEnabled = SettingsHandler::getValue(BOOT_BUTTON_ENABLED); - // const bool buttonSetsEnabled = SettingsHandler::getValue(BUTTON_SETS_ENABLED);; - // const char* bootButtonCommand = SettingsHandler::getValue(BOOT_BUTTON_COMMAND);; - // const int buttonAnalogDebounce = SettingsHandler::getValue(BUTTON_ANALOG_DEBOUNCE); - // setValue(json, bootButtonEnabled, "buttonCommand", "bootButtonEnabled", BOOT_BUTTON_ENABLED_DEFAULT); - // setValue(json, buttonSetsEnabled, "buttonCommand", "buttonSetsEnabled", BOOT_BUTTON_COMMAND_DEFAULT); - // setValue(json, bootButtonCommand, "buttonCommand", "bootButtonCommand", BUTTON_SETS_ENABLED_DEFAULT); - // setValue(json, buttonAnalogDebounce, "buttonCommand", "buttonAnalogDebounce", BUTTON_ANALOG_DEBOUNCE_DEFAULT); - if(!json.isNull()) - { - bool bootButtonEnabled = json[BOOT_BUTTON_ENABLED] | BOOT_BUTTON_ENABLED_DEFAULT; - m_settingsFactory->setValue(BOOT_BUTTON_ENABLED, bootButtonEnabled); - bool buttonSetsEnabled = json[BUTTON_SETS_ENABLED] | BUTTON_SETS_ENABLED_DEFAULT; - m_settingsFactory->setValue(BUTTON_SETS_ENABLED, buttonSetsEnabled); - const char* bootButtonCommand = json[BOOT_BUTTON_COMMAND] | BOOT_BUTTON_COMMAND_DEFAULT; - m_settingsFactory->setValue(BOOT_BUTTON_COMMAND, bootButtonCommand); - int buttonAnalogDebounce = json[BUTTON_ANALOG_DEBOUNCE] | BUTTON_ANALOG_DEBOUNCE_DEFAULT; - m_settingsFactory->setValue(BUTTON_ANALOG_DEBOUNCE, buttonAnalogDebounce); - m_settingsFactory->saveCommon(); - } - - JsonArray buttonSetsObj = json["buttonSets"].as(); - if(buttonSetsObj.isNull()) { - LogHandler::info(_TAG, "No button sets stored, loading default"); - mutableLoadDefault = true; - const PinMap* pinMap = m_settingsFactory->getPins(); - for(int i = 0; i < MAX_BUTTON_SETS; i++) { - buttonSets[i] = ButtonSet(); - buttonSets[i].pin = pinMap->buttonSetPin(i); - - sprintf(buttonSets[i].name, "Button set %u", i+1); - LogHandler::debug(_TAG, "Default buttonset name: %s, index: %u, pin: %ld", buttonSets[i].name, i, buttonSets[i].pin); - for(int j = 0; j < MAX_BUTTONS; j++) { - buttonSets[i].buttons[j] = ButtonModel(); - buttonSets[i].buttons[j].loadDefault(j); - LogHandler::debug(_TAG, "Default button name: %s, index: %u, command: %s", buttonSets[i].name, buttonSets[i].buttons[j].index, buttonSets[i].buttons[j].command); - - } - } - } else { - std::vector pins; - for(int i = 0; i < MAX_BUTTON_SETS; i++) { - auto set = ButtonSet(); - set.fromJson(buttonSetsObj[i].as()); - pins.push_back(set.pin); - LogHandler::debug(_TAG, "Loaded button set '%s', pin: %ld", set.name, set.pin); - buttonSets[i] = set; - for(int j = 0; j < MAX_BUTTONS; j++) { - LogHandler::debug(_TAG, "Loaded button, name: %s, index: %u, command: %s", buttonSets[i].name, buttonSets[i].buttons[j].index, buttonSets[i].buttons[j].command); - } - } - m_settingsFactory->setValue(BUTTON_SET_PINS, pins); - m_settingsFactory->savePins(); - } - - if(initialized) - sendMessage(SettingProfile::Button, "analogButtonCommands"); - - return true; }, saveButtons, json); - } - - static bool saveButtons(JsonObject json = JsonObject()) - { - LogHandler::info(_TAG, "Save buttons file"); - uint16_t docSize = 2000; - // for(int i = 0; i < MAX_BUTTON_SETS; i++) { - // LogHandler::debug(_TAG, "Save buttonSets[i] '%s', pin: %ld", buttonSets[i].name, buttonSets[i].pin); - // for(int j = 0; j < MAX_BUTTONS; j++) { - // LogHandler::debug(_TAG, "Save buttonSets[i].buttons, name: %s, index: %ld, command: %s", buttonSets[i].name, buttonSets[i].buttons[j].index, buttonSets[i].buttons[j].command); - // } - // } - return saveSettingsJson(BUTTON_SETTINGS_PATH, m_buttonsMutex, docSize, [](JsonDocument &doc) -> bool - { - - bool bootButtonEnabled = BOOT_BUTTON_ENABLED_DEFAULT; - m_settingsFactory->getValue(BOOT_BUTTON_ENABLED, bootButtonEnabled); - doc[BOOT_BUTTON_ENABLED] = bootButtonEnabled; - bool buttonSetsEnabled = BUTTON_SETS_ENABLED_DEFAULT; - m_settingsFactory->getValue(BUTTON_SETS_ENABLED, buttonSetsEnabled); - doc[BUTTON_SETS_ENABLED] = buttonSetsEnabled; - // char bootButtonCommand[BOOT_BUTTON_COMMAND_LEN] = {0}; - // m_settingsFactory->getValue(BOOT_BUTTON_COMMAND, bootButtonCommand, BOOT_BUTTON_COMMAND_LEN); - const char* bootButtonCommand = m_settingsFactory->getBootButtonCommand(); - doc[BOOT_BUTTON_COMMAND] = bootButtonCommand; - int buttonAnalogDebounce = BUTTON_ANALOG_DEBOUNCE_DEFAULT; - m_settingsFactory->getValue(BUTTON_ANALOG_DEBOUNCE, buttonAnalogDebounce); - doc[BUTTON_ANALOG_DEBOUNCE] = buttonAnalogDebounce; - - //auto buttonSetArray = doc["buttonSets"].as(); - std::vector pins; - for (size_t i = 0; i < MAX_BUTTON_SETS; i++) - { - //JsonObject obj; - doc["buttonSets"][i]["name"] = buttonSets[i].name; - - doc["buttonSets"][i]["pin"] = buttonSets[i].pin; - pins.push_back(buttonSets[i].pin); - doc["buttonSets"][i]["pullMode"] = (uint8_t)buttonSets[i].pullMode; - LogHandler::debug(_TAG, "Saving button set '%s' from settings, pin: %ld", doc["buttonSets"][i]["name"].as(), doc["buttonSets"][i]["pin"].as()); - for(size_t j = 0; j < MAX_BUTTONS; j++) { - doc["buttonSets"][i]["buttons"][j]["name"] = buttonSets[i].buttons[j].name; - doc["buttonSets"][i]["buttons"][j]["index"] = buttonSets[i].buttons[j].index; - doc["buttonSets"][i]["buttons"][j]["command"] = buttonSets[i].buttons[j].command; - LogHandler::debug(_TAG, "Saving button, name: %s, index: %u, command: %s", doc["buttonSets"][i]["buttons"][j]["name"].as(), doc["buttonSets"][i]["buttons"][j]["index"].as(), doc["buttonSets"][i]["buttons"][j]["command"].as()); - } - //buttonSetArray.add(obj); - } - m_settingsFactory->setValue(BUTTON_SET_PINS, pins); - m_settingsFactory->savePins(); - return true; }, loadButtons, json); - } - - static bool loadMotionProfiles(bool loadDefault, JsonObject json = JsonObject()) - { - LogHandler::info(_TAG, "Loading motion profiles"); - // bool mutableLoadDefault = loadDefault; - // JsonDocument doc; //deserializeSize - // if(mutableLoadDefault || json.isNull()) { - // xSemaphoreTake(m_motionMutex, portMAX_DELAY); - // if(!checkForFileAndLoad(MOTION_PROFILE_SETTINGS_PATH, doc, mutableLoadDefault)) { - // saving = false; - // xSemaphoreGive(m_motionMutex); - // return false; - // } - // json = doc.as(); - // } - return loadSettingsJson(MOTION_PROFILE_SETTINGS_PATH, loadDefault, m_motionMutex, [](const JsonObject json, bool &mutableLoadDefault) -> bool - { - motionDefaultProfileIndex = json[MOTION_PROFILE_DEFAULT_INDEX] | MOTION_PROFILE_SELECTED_INDEX_DEFAULT; - if(!initialized) - motionSelectedProfileIndex = motionDefaultProfileIndex; - - JsonArray motionProfilesObj = json[MOTION_PROFILES].as(); - if(motionProfilesObj.isNull()) { - LogHandler::info(_TAG, "No motion profiles stored, loading default"); - mutableLoadDefault = true; - for(int i = 0; i < MAX_MOTION_PROFILE_COUNT; i++) { - motionProfiles[i] = MotionProfile(i + 1); - motionProfiles[i].addDefaultChannel("L0"); - LogHandler::debug(_TAG, "Added new Motion profile for: %s", motionProfiles[i].channels.back().name); - } - } else { - int i = 0; - for (JsonObject profileObj : motionProfilesObj) { - auto profile = MotionProfile(); - profile.fromJson(profileObj); - LogHandler::debug(_TAG, "Loading motion profile '%s' from settings", profile.motionProfileName); - motionProfiles[i] = profile; - i++; - } - } - //xSemaphoreGive(m_motionMutex); - // if(mutableLoadDefault) - // saveMotionProfiles(); - return true; }, saveMotionProfiles, json); - } - - static bool saveMotionProfiles(JsonObject json = JsonObject()) - { - LogHandler::info(_TAG, "Save motion profiles file"); - saving = true; - xSemaphoreTake(m_motionMutex, portMAX_DELAY); - if (!LittleFS.exists(MOTION_PROFILE_SETTINGS_PATH)) - { - LogHandler::error(_TAG, "Motion profile file did not exist whan saving."); - saving = false; - xSemaphoreGive(m_motionMutex); - return false; - } - if (!json.isNull()) - { // If passed in, load the json into memory before flushing it to disk. - // WARNING: watchout for the mutex taken in this method. Changing these parameters below may result in hard locks. - loadMotionProfiles(false, json); // DO NOT PASS loadDefault as true else infinit loop - } - JsonDocument doc; // serializeSize - doc[MOTION_PROFILE_DEFAULT_INDEX] = motionDefaultProfileIndex; - LogHandler::debug(_TAG, "motion profiles index: %ld", motionDefaultProfileIndex); - - for (int i = 0; i < MAX_MOTION_PROFILE_COUNT; i++) - { - // if(motionProfiles[i].edited) { // TODO: this does not work because doc is empty and needs to be loaded from disk first bedore modifying sections of it. - - LogHandler::debug(_TAG, "Edited motion profile name: %s", motionProfiles[i].motionProfileName); - doc[MOTION_PROFILES][i]["name"] = motionProfiles[i].motionProfileName; - for (size_t j = 0; j < motionProfiles[i].channels.size(); j++) - { - // if(motionProfiles[i].channels[j].edited) { - LogHandler::debug(_TAG, "motion profile channel: %s", motionProfiles[i].channels[j].name); - doc[MOTION_PROFILES][i]["channels"][j]["name"] = motionProfiles[i].channels[j].name; - doc[MOTION_PROFILES][i]["channels"][j]["update"] = motionProfiles[i].channels[j].motionUpdateGlobal; - doc[MOTION_PROFILES][i]["channels"][j]["period"] = motionProfiles[i].channels[j].motionPeriodGlobal; - doc[MOTION_PROFILES][i]["channels"][j]["amp"] = motionProfiles[i].channels[j].motionAmplitudeGlobal; - doc[MOTION_PROFILES][i]["channels"][j]["offset"] = motionProfiles[i].channels[j].motionOffsetGlobal; - doc[MOTION_PROFILES][i]["channels"][j]["phase"] = motionProfiles[i].channels[j].motionPhaseGlobal; - doc[MOTION_PROFILES][i]["channels"][j]["reverse"] = motionProfiles[i].channels[j].motionReversedGlobal; - doc[MOTION_PROFILES][i]["channels"][j]["periodRan"] = motionProfiles[i].channels[j].motionPeriodGlobalRandom; - doc[MOTION_PROFILES][i]["channels"][j]["periodMin"] = motionProfiles[i].channels[j].motionPeriodGlobalRandomMin; - doc[MOTION_PROFILES][i]["channels"][j]["periodMax"] = motionProfiles[i].channels[j].motionPeriodGlobalRandomMax; - doc[MOTION_PROFILES][i]["channels"][j]["ampRan"] = motionProfiles[i].channels[j].motionAmplitudeGlobalRandom; - doc[MOTION_PROFILES][i]["channels"][j]["ampMin"] = motionProfiles[i].channels[j].motionAmplitudeGlobalRandomMin; - doc[MOTION_PROFILES][i]["channels"][j]["ampMax"] = motionProfiles[i].channels[j].motionAmplitudeGlobalRandomMax; - doc[MOTION_PROFILES][i]["channels"][j]["offsetRan"] = motionProfiles[i].channels[j].motionOffsetGlobalRandom; - doc[MOTION_PROFILES][i]["channels"][j]["offsetMin"] = motionProfiles[i].channels[j].motionOffsetGlobalRandomMin; - doc[MOTION_PROFILES][i]["channels"][j]["offsetMax"] = motionProfiles[i].channels[j].motionOffsetGlobalRandomMax; - doc[MOTION_PROFILES][i]["channels"][j]["phaseRan"] = motionProfiles[i].channels[j].motionPhaseRandom; - doc[MOTION_PROFILES][i]["channels"][j]["phaseMin"] = motionProfiles[i].channels[j].motionPhaseRandomMin; - doc[MOTION_PROFILES][i]["channels"][j]["phaseMax"] = motionProfiles[i].channels[j].motionPhaseRandomMax; - doc[MOTION_PROFILES][i]["channels"][j]["ranMin"] = motionProfiles[i].channels[j].motionRandomChangeMin; - doc[MOTION_PROFILES][i]["channels"][j]["ranMax"] = motionProfiles[i].channels[j].motionRandomChangeMax; - motionProfiles[i].channels[j].edited = false; - //} - } - if (initialized && motionSelectedProfileIndex == i) - { - sendMessage(SettingProfile::MotionProfile, MOTION_PROFILES); - } - motionProfiles[i].edited = false; - //} - } - File file = LittleFS.open(MOTION_PROFILE_SETTINGS_PATH, FILE_WRITE); - if (serializeJson(doc, file) == 0) - { - LogHandler::error(_TAG, "Failed to write to motion profiles file"); - file.close(); - xSemaphoreGive(m_motionMutex); - saving = false; - return false; - } - - xSemaphoreGive(m_motionMutex); - saving = false; - return true; - } - - static bool loadChannels(bool loadDefault, JsonObject json = JsonObject()) - { - - MotorType motorType; - DeviceType deviceType; - m_settingsFactory->getValue(MOTOR_TYPE_SETTING, motorType); - m_settingsFactory->getValue(DEVICE_TYPE, deviceType); - // Init motor type BEFORE loading the channels else it will load the defaults - channelMap.init(m_settingsFactory->getTcodeVersion(), motorType, deviceType); - - LogHandler::info(_TAG, "Loading channel profile"); - return loadSettingsJson(CHANNELS_SETTINGS_PATH, loadDefault, m_channelsMutex, [](const JsonObject json, bool &mutableLoadDefault) -> bool - { - - JsonArray channelProfileObj = json[CHANNEL_PROFILE].as(); - - if(channelProfileObj.isNull()) { - LogHandler::info(_TAG, "No channel profile stored, loading default"); - mutableLoadDefault = true; - } else { - for (JsonObject profileObj : channelProfileObj) { - const char* name = profileObj[CHANNEL_NAME]; - Channel* channel = channelMap.get(name); - if(!channel) - { - LogHandler::error(_TAG, "Channel missing from stored profile: %s", name); - continue; - } - channel->userMin = profileObj[CHANNEL_USER_MIN] | TCODE_MIN; - channel->userMid = profileObj[CHANNEL_USER_MID] | TCODE_MID; - channel->userMax = profileObj[CHANNEL_USER_MAX] | TCODE_MAX; - channel->rangeLimitEnabled = profileObj[CHANNEL_RANGE_LIMIT_ENABLED]; - LogHandler::debug(_TAG, "Loading channel profile '%s' from settings", name); - } - } - return true; }, saveChannels, json); - } - - static bool saveChannels(JsonObject json = JsonObject()) - { - LogHandler::info(_TAG, "Save channel profile file"); - saving = true; - xSemaphoreTake(m_channelsMutex, portMAX_DELAY); - if (!LittleFS.exists(CHANNELS_SETTINGS_PATH)) - { - LogHandler::error(_TAG, "Channel profile file did not exist whan saving."); - saving = false; - xSemaphoreGive(m_channelsMutex); - return false; - } - if (!json.isNull()) - { // If passed in, load the json into memory before flushing it to disk. - // WARNING: watchout for the mutex taken in this method. Changing these parameters below may result in hard locks. - loadChannels(false, json); // DO NOT PASS loadDefault as true else infinit loop - } - JsonDocument doc; // serializeSize - for (int i = 0; i < channelMap.count(); i++) - { - Channel *channel = channelMap.get(i); - doc[CHANNEL_PROFILE][i][CHANNEL_NAME] = channel->Name; - doc[CHANNEL_PROFILE][i][CHANNEL_USER_MIN] = channel->userMin; - doc[CHANNEL_PROFILE][i][CHANNEL_USER_MID] = channel->userMid; - doc[CHANNEL_PROFILE][i][CHANNEL_USER_MAX] = channel->userMax; - doc[CHANNEL_PROFILE][i][CHANNEL_RANGE_LIMIT_ENABLED] = channel->rangeLimitEnabled; - if (initialized) - { - sendMessage(SettingProfile::ChannelRanges, CHANNEL_PROFILE); - } - } - File file = LittleFS.open(CHANNELS_SETTINGS_PATH, FILE_WRITE); - if (serializeJson(doc, file) == 0) - { - LogHandler::error(_TAG, "Failed to write to channel profile file"); - file.close(); - xSemaphoreGive(m_channelsMutex); - saving = false; - return false; - } - - xSemaphoreGive(m_channelsMutex); - saving = false; - return true; - } - - static std::vector &getMotionChannels() - { - return motionProfiles[motionSelectedProfileIndex].channels; - } - - static bool getMotionEnabled() - { - return motionEnabled; - } - static void setMotionEnabled(const bool &newValue) - { - setValue(newValue, motionEnabled, SettingProfile::MotionProfile, MOTION_ENABLED); - } - static bool getMotionPaused() - { - return motionPaused; - } - static void setMotionPaused(const bool &newValue) - { - setValue(newValue, motionPaused, SettingProfile::MotionProfile, MOTION_PAUSED); - } - - static int getMotionDefaultProfileIndex() - { - return motionDefaultProfileIndex; - } - static void setMotionProfileName(const char newValue[MAX_MOTION_PROFILE_NAME_LENGTH]) - { - strcpy(motionProfiles[motionSelectedProfileIndex].motionProfileName, newValue); - } - - // static int getMotionUpdateGlobal(const char name[3]) - // { - // return motionProfiles[motionSelectedProfileIndex].motionUpdateGlobal; - // } - // static void setMotionUpdateGlobal(const int& newValue) - // { - // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionUpdateGlobal, "motionGenerator", "motionUpdateGlobal"); - // } - // static int getMotionPeriodGlobal() - // { - // return motionProfiles[motionSelectedProfileIndex].motionPeriodGlobal; - // } - // static void setMotionPeriodGlobal(const int& newValue) - // { - // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionPeriodGlobal, "motionGenerator", "motionPeriodGlobal"); - // } - // static int getMotionAmplitudeGlobal() - // { - // return motionProfiles[motionSelectedProfileIndex].motionAmplitudeGlobal; - // } - // static void setMotionAmplitudeGlobal(const int& newValue) - // { - // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionAmplitudeGlobal, "motionGenerator", "motionAmplitudeGlobal"); - // } - // static int getMotionOffsetGlobal() - // { - // return motionProfiles[motionSelectedProfileIndex].motionOffsetGlobal; - // } - // static void setMotionOffsetGlobal(const int& newValue) - // { - // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionOffsetGlobal, "motionGenerator", "motionOffsetGlobal"); - // } - // static float getMotionPhaseGlobal() - // { - // return motionProfiles[motionSelectedProfileIndex].motionPhaseGlobal; - // } - // static void setMotionPhaseGlobal(const float& newValue) - // { - // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionPhaseGlobal, "motionGenerator", "motionPhaseGlobal"); - // } - // static bool getMotionReversedGlobal() - // { - // return motionProfiles[motionSelectedProfileIndex].motionReversedGlobal; - // } - // static void setMotionReversedGlobal(const bool& newValue) - // { - // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionReversedGlobal, "motionGenerator", "motionReversedGlobal"); - // } - // static bool getMotionPeriodGlobalRandom() - // { - // return motionProfiles[motionSelectedProfileIndex].motionPeriodGlobalRandom; - // } - // static void setMotionPeriodGlobalRandom(const bool& newValue) - // { - // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionPeriodGlobalRandom, "motionGenerator", "motionPeriodGlobalRandom"); - // } - // static int getMotionPeriodGlobalRandomMin() - // { - // return motionProfiles[motionSelectedProfileIndex].motionPeriodGlobalRandomMin; - // } - // static void setMotionPeriodGlobalRandomMin(const int& newValue) - // { - // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionPeriodGlobalRandomMin, "motionGenerator", "motionPeriodGlobalRandomMin"); - // } - // static int getMotionPeriodGlobalRandomMax() - // { - // return motionProfiles[motionSelectedProfileIndex].motionPeriodGlobalRandomMax; - // } - // static void setMotionPeriodGlobalRandomMax(const int& newValue) - // { - // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionPeriodGlobalRandomMax, "motionGenerator", "motionPeriodGlobalRandomMax"); - // } - // static bool getMotionAmplitudeGlobalRandom() - // { - // return motionProfiles[motionSelectedProfileIndex].motionAmplitudeGlobalRandom ; - // } - // static void setMotionAmplitudeGlobalRandom(const bool& newValue) - // { - // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionAmplitudeGlobalRandom, "motionGenerator", "motionAmplitudeGlobalRandom"); - // } - // static int getMotionAmplitudeGlobalRandomMin() - // { - // return motionProfiles[motionSelectedProfileIndex].motionAmplitudeGlobalRandomMin; - // } - // static void setMotionAmplitudeGlobalRandomMin(const int& newValue) - // { - // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionAmplitudeGlobalRandomMin = newValue, "motionGenerator", "motionAmplitudeGlobalRandomMin"); - // } - // static int getMotionAmplitudeGlobalRandomMax() - // { - // return motionProfiles[motionSelectedProfileIndex].motionAmplitudeGlobalRandomMax; - // } - // static void setMotionAmplitudeGlobalRandomMax(const int& newValue) - // { - // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionAmplitudeGlobalRandomMax = newValue, "motionGenerator", "motionAmplitudeGlobalRandomMax"); - // } - // static bool getMotionOffsetGlobalRandom() - // { - // return motionProfiles[motionSelectedProfileIndex].motionOffsetGlobalRandom; - // } - // static void setMotionOffsetGlobalRandom(const bool& newValue) - // { - // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionOffsetGlobalRandom = newValue, "motionGenerator", "motionOffsetGlobalRandom"); - // } - // static int getMotionOffsetGlobalRandomMin() - // { - // return motionProfiles[motionSelectedProfileIndex].motionOffsetGlobalRandomMin; - // } - // static void setMotionOffsetGlobalRandomMin(const int& newValue) - // { - // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionOffsetGlobalRandomMin, "motionGenerator", "motionOffsetGlobalRandomMin"); - // } - // static int getMotionOffsetGlobalRandomMax() - // { - // return motionProfiles[motionSelectedProfileIndex].motionOffsetGlobalRandomMax; - // } - // static void setMotionOffsetGlobalRandomMax(const int& newValue) - // { - // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionOffsetGlobalRandomMax = newValue, "motionGenerator", "motionOffsetGlobalRandomMax"); - // } - // static int getMotionRandomChangeMin() - // { - // return motionProfiles[motionSelectedProfileIndex].motionRandomChangeMin; - // } - // static void setMotionRandomChangeMin(const int& newValue) - // { - // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionRandomChangeMin, "motionGenerator", "motionRandomChangeMin"); - // } - // static int getMotionRandomChangeMax() - // { - // return motionProfiles[motionSelectedProfileIndex].motionRandomChangeMax; - // } - // static void setMotionRandomChangeMax(const int& newValue) - // { - // setValue(newValue, motionProfiles[motionSelectedProfileIndex].motionRandomChangeMax, "motionGenerator", "motionRandomChangeMax"); - // } - - static int motionProfileExists(const char *profile) - { - for (size_t i = 0; i < MAX_MOTION_PROFILE_COUNT; i++) - { - if (strcmp(motionProfiles[i].motionProfileName, profile) == 0) - return (int)i; - } - return -1; - } - - static void setMotionDefaults() - { - setMotionEnabled(false); - auto motionProfile = MotionProfile(motionSelectedProfileIndex + 1); - setMotionProfile(motionProfile, motionSelectedProfileIndex); - } - - static void setMotionProfile(const char profile[MAX_MOTION_PROFILE_NAME_LENGTH]) - { - auto index = motionProfileExists(profile); - if (index < 0) - { - LogHandler::error(_TAG, "Motion profile %s does not exist", profile); - return; - } - setMotionProfile(index); - } - - static void setMotionProfile(const int &index) - { - if (index < 0 || index > MAX_MOTION_PROFILE_COUNT - 1) - { - LogHandler::error(_TAG, "Invalid motion profile index: %ld", index); - return; - } - auto newProfile = motionProfiles[index]; - setMotionProfile(newProfile, index); - } - - static void setMotionProfile(const MotionProfile &profile, int profileIndex) - { - if (profileIndex < 0 || profileIndex > MAX_MOTION_PROFILE_COUNT - 1) - { - LogHandler::error(_TAG, "Invalid motion profile index: %ld", profileIndex); - return; - } - // m_settingsFactory->setValue(MOTION_PROFILE_SELECTED_INDEX, profileIndex); - setValue(profileIndex, motionSelectedProfileIndex, SettingProfile::MotionProfile, MOTION_PROFILE_SELECTED_INDEX); - } - - static void cycleMotionProfile() - { - if (!getMotionEnabled()) - { - setMotionEnabled(true); - return; - } - uint8_t newProfileIndex = motionSelectedProfileIndex + 1; - if (newProfileIndex > MAX_MOTION_PROFILE_COUNT - 1) - { - newProfileIndex = 0; - setMotionEnabled(false); - } - auto newProfile = motionProfiles[newProfileIndex]; - setMotionProfile(newProfile, newProfileIndex); - } - - static const bool readFile(char *&buf, const char *path) - { - if (!LittleFS.exists(path)) - { - LogHandler::error(_TAG, "Path did not exist when reading contents: %s", path); - return false; - } - File file = LittleFS.open(path, "r"); - printFree(); - String fileStr = file.readString(); - // buf = static_cast(malloc(fileStr.length() + 1)); - printFree(); - LogHandler::info(_TAG, "Create buffer: %u", fileStr.length()); - buf = new char[fileStr.length() + 1]; - strcpy(buf, fileStr.c_str()); - return true; - } - - static const int getDeserializeSize() - { - return deserializeSize; - } - - // static void processTCodeJson(char *outbuf, const char *tcodeJson) - // { - // StaticJsonDocument<512> doc; - // DeserializationError error = deserializeJson(doc, tcodeJson); - // if (error) - // { - // LogHandler::error(_TAG, "Failed to read udp jsonobject, using default configuration"); - // outbuf[0] = {0}; - // return; - // } - // JsonArray arr = doc.as(); - // char buffer[MAX_COMMAND] = ""; - // for (JsonObject repo : arr) - // { - // const char *channel = repo["c"]; - // int value = repo["v"]; - // if (channel != nullptr && value > 0) - // { - // if (buffer[0] == '\0') - // { - // // Serial.println("tcode empty"); - // strcpy(buffer, channel); - // } - // else - // { - // strcat(buffer, channel); - // } - // // Serial.print("channel: "); - // // Serial.print(channel); - // // Serial.print(" value: "); - // // Serial.println(value); - // char integer_string[4]; - // sprintf(integer_string, SettingsHandler::TCodeVersionEnum == TCodeVersion::v0_2 ? "%03d" : "%04d", SettingsHandler::calculateRange(name, value)); - // // pad(integer_string); - // // sprintf(integer_string, "%d", SettingsHandler::calculateRange(name, value)); - // // Serial.print("integer_string"); - // // Serial.println(integer_string); - // strcat(buffer, integer_string); - // int speed = repo["s"]; - // int interval = repo["i"]; - // if (interval > 0) - // { - // char interval_string[5]; - // sprintf(interval_string, "%d", interval); - // strcat(buffer, "I"); - // strcat(buffer, interval_string); - // } - // else if (speed > 0) - // { - // char speed_string[5]; - // sprintf(speed_string, "%d", speed); - // strcat(buffer, "S"); - // strcat(buffer, speed_string); - // } - // strcat(buffer, " "); - // // Serial.print("buffer "); - // // Serial.println(buffer); - // } - // } - // strcpy(outbuf, buffer); - // strcat(outbuf, "\n"); - // // Serial.print("outbuf "); - // // Serial.println(outbuf); - // } - - static bool waitForI2CDevices(const int &i2cAddress = 0) - { - int tries = 0; - if (i2cAddress) - LogHandler::info(_TAG, "Looking for I2c address: %ld", i2cAddress); - while ((systemI2CAddresses.size() == 0 || i2cAddress) && tries <= 3) - { - tries++; - I2CScan(); - if (i2cAddress && std::find(systemI2CAddresses.begin(), systemI2CAddresses.end(), i2cAddress) != systemI2CAddresses.end()) - { - return true; - } - else if (i2cAddress) - { - LogHandler::info(_TAG, "I2c address: %ld not found. trying again...", i2cAddress); - } - else if (systemI2CAddresses.size() == 0) - { - LogHandler::info(_TAG, "No I2C devices found in system, trying again..."); - } - if (tries >= 3) - { - if (i2cAddress) - { - LogHandler::error(_TAG, "I2c address: %ld timed out.", i2cAddress); - } - else - { - LogHandler::error(_TAG, "No I2C devices found in system"); - } - return false; - } - vTaskDelay(1000 / portTICK_PERIOD_MS); - } - return true; - } - - static bool I2CScan() - { - systemI2CAddresses.clear(); - byte error, address; - int nDevices; - LogHandler::info(_TAG, "Scanning for I2C..."); - nDevices = 0; - int8_t sdaPin = I2C_SDA_PIN_DEFAULT; - m_settingsFactory->getValue(I2C_SDA_PIN, sdaPin); - int8_t sclPin = I2C_SCL_PIN_DEFAULT; - m_settingsFactory->getValue(I2C_SCL_PIN, sclPin); - if (sdaPin < 0 || sclPin < 0) - { - LogHandler::debug(_TAG, "SDA or SCL is disabled when scaning for I2C devices sdaPin: %d, sclPin: %d", sdaPin, sclPin); - return false; - } - Wire.begin(sdaPin, sclPin); - for (address = 1; address < 127; address++) - { - Wire.beginTransmission(address); - error = Wire.endTransmission(); - if (error == 0) - { - // Serial.print("I2C device found at address 0x"); - // if (address<16) - // { - // Serial.print("0"); - // } - // Serial.println(address,HEX); - - // std::stringstream I2C_Address_String; - // I2C_Address_String << "0x" << std::hex << address; - // std::string foundAddress = I2C_Address_String.str(); - - char buf[10]; - hexToString(address, buf); - LogHandler::info(_TAG, "I2C device found at address %s, byte %ld", buf, address); - - systemI2CAddresses.push_back((int)address); - nDevices++; - } - else if (error == 4) - { - Serial.print("Unknow error at address 0x"); - if (address < 16) - { - Serial.print("0"); - } - Serial.println(address, HEX); - // std::stringstream I2C_Address_String; - // I2C_Address_String << "0x" << std::hex << address; - // std::string foundAddress = I2C_Address_String.str(); - // LogHandler::error(_TAG, "Unknow error at address %s", foundAddress); - } - } - if (nDevices == 0) - { - LogHandler::info(_TAG, "No I2C devices found"); - return false; - } - return true; - } - - static Channel *getChannel(const char *name) - { - return channelMap.get(name); - } - - static uint16_t getChannelMin(const char *name) - { - Channel *channelProfile = channelMap.get(name); - if (!channelProfile) - { - LogHandler::error(_TAG, "[getChannelMin] Invalid name for current map: %s", name); - return TCODE_MIN; - } - return channelProfile->min; - } - - static uint16_t getChannelMax(const char *name) - { - Channel *channelProfile = channelMap.get(name); - if (!channelProfile) - { - LogHandler::error(_TAG, "[getChannelMax] Invalid name for current map: %s", name); - return TCODE_MAX; - } - return channelProfile->max; - } - - static uint16_t getChannelUserMin(const char *name) - { - Channel *channelProfile = channelMap.get(name); - if (!channelProfile) - { - LogHandler::error(_TAG, "[getChannelUserMin] Invalid name for current map: %s", name); - return TCODE_MIN; - } - return channelProfile->userMin; - } - - static uint16_t getChannelUserMax(const char *name) - { - Channel *channelProfile = channelMap.get(name); - if (!channelProfile) - { - LogHandler::error(_TAG, "[getChannelUserMax] Invalid name for current map: %s", name); - return TCODE_MAX; - } - return channelProfile->userMax; - } - - static void setChannelMin(const char *name, uint16_t value) - { - Channel *channelProfile = channelMap.get(name); - if (!channelProfile) - { - LogHandler::error(_TAG, "[setChannelMin] Invalid name for current map: %s", name); - return; - } - channelProfile->userMin = value; - } - - static void setChannelMax(const char *name, uint16_t value) - { - Channel *channelProfile = channelMap.get(name); - if (!channelProfile) - { - LogHandler::error(_TAG, "[setChannelMax] Invalid name for current map: %s", name); - return; - } - channelProfile->userMax = value; - } - - static void setChannelRangesEnabled(bool enabled) - { - channelRangesEnabled = enabled; - sendMessage(SettingProfile::ChannelRanges, "channelRangesEnabled"); - } - static bool getChannelRangesEnabled() - { - return channelRangesEnabled; - } - -private: - static constexpr Tags::tag_t _TAG = Tags::Settings; - - static SettingsFactory *m_settingsFactory; - static SemaphoreHandle_t m_motionMutex; - static SemaphoreHandle_t m_channelsMutex; - static SemaphoreHandle_t m_wifiMutex; - static SemaphoreHandle_t m_buttonsMutex; - static SemaphoreHandle_t m_settingsMutex; - static SETTING_STATE_FUNCTION_PTR_T message_callback; - // Use http://arduinojson.org/assistant to compute the capacity. - static const int deserializeSize = 32768; - static const int serializeSize = 24576; - - static bool motionEnabled; - static int motionSelectedProfileIndex; - static int motionDefaultProfileIndex; - // static MotionProfile motionProfiles[maxMotionProfileCount]; - - // static bool voiceEnabled; - // static bool voiceMuted; - // static int voiceWakeTime ; - // static int voiceVolume; - - // static bool update(JsonObject json) - // { - // logLevel = (LogLevel)(json["logLevel"] | 2); - // LogHandler::setLogLevel(logLevel); - // LogHandler::debug(_TAG, "Load Json: Memory usage: %u bytes", json.memoryUsage()); - - // boardType = (BoardType)(json["boardType"] | (uint8_t)BoardType::DEVKIT); - - // if(isBoardType(BoardType::CRIMZZON) || isBoardType(BoardType::ISAAC)) { - // TCodeVersionEnum = TCodeVersion::v0_3; - // TCodeVersionName = TCodeVersionMapper(TCodeVersionEnum); - // } - // std::vector includesVec; - // setValue(json, includesVec, "log", "log-include-tags"); - // LogHandler::setIncludes(includesVec); - - // std::vector excludesVec; - // setValue(json, excludesVec, "log", "log-exclude-tags"); - // LogHandler::setExcludes(excludesVec); - - // if(!isBoardType(BoardType::CRIMZZON)) { - // TCodeVersionEnum = (TCodeVersion)(json["TCodeVersion"] | 1); - // TCodeVersionName = TCodeVersionMapper(TCodeVersionEnum); - // } - - // channelMap.init(TCodeVersionEnum, motorType); - - // #if MOTOR_TYPE == 1 - // for (auto x : ChannelMapBLDC) { - // currentChannels.push_back(x); - // } - // #else - // currentChannels.clear(); - // if(TCodeVersionEnum == TCodeVersion::v0_2) { - // // for (size_t i = 0; i < (sizeof(ChannelMapV2)/sizeof(Channel)); i++) { - // // currentChannels.push_back(ChannelMapV2[i]); - // // } - // for (auto x : ChannelMapV2) { - // currentChannels.push_back(x); - // } - // } else { - // for (auto x : ChannelMapV3) { - // currentChannels.push_back(x); - // } - // } - // #endif - - // int tcodeMax = TCodeVersionEnum == TCodeVersion::v0_2 ? 999 : 9999; - // for (size_t i = 0; i < currentChannels.size(); i++) - // { - // uint16_t min = json["channelRanges"][currentChannels[i].Name]["min"].as(); - // uint16_t max = json["channelRanges"][currentChannels[i].Name]["max"].as(); - // currentChannels[i].min = !min ? 1 : min; - // currentChannels[i].max = !max ? tcodeMax : max; - // } - - // sendMessage("channelRanges", "channelRanges");// TODO: channelranges should be in its own json - - // udpServerPort = json["udpServerPort"] | 8000; - // webServerPort = json["webServerPort"] | 80; - // const char *hostnameTemp = json["hostname"] | "tcode"; - // if (hostnameTemp != nullptr) - // strcpy(hostname, hostnameTemp); - // const char *friendlyNameTemp = json["friendlyName"] | "ESP32 TCode"; - // if (friendlyNameTemp != nullptr) - // strcpy(friendlyName, friendlyNameTemp); - - // bluetoothEnabled = json["bluetoothEnabled"] | false; - - // // Servo motors////////////////////////////////////////////////////////////////////////////////// - // pitchFrequencyIsDifferent = json["pitchFrequencyIsDifferent"]; - // msPerRad = json["msPerRad"] | 637; - // servoFrequency = json["servoFrequency"] | 50; - // pitchFrequency = json[pitchFrequencyIsDifferent ? "pitchFrequency" : "servoFrequency"] | servoFrequency; - // sr6Mode = json["sr6Mode"]; - - // RightServo_ZERO = json["RightServo_ZERO"] | 1500; - // LeftServo_ZERO = json["LeftServo_ZERO"] | 1500; - // RightUpperServo_ZERO = json["RightUpperServo_ZERO"] | 1500; - // LeftUpperServo_ZERO = json["LeftUpperServo_ZERO"] | 1500; - // PitchLeftServo_ZERO = json["PitchLeftServo_ZERO"] | 1500; - // PitchRightServo_ZERO = json["PitchRightServo_ZERO"] | 1500; - - // BLDC_UsePWM = json["BLDC_UsePWM"] | false; // Must be before pinout is set - // BLDC_UseMT6701 = json["BLDC_UseMT6701"] | true; - // BLDC_UseHallSensor = json["BLDC_UseHallSensor"] | false; - // BLDC_Pulley_Circumference = json["BLDC_Pulley_Circumference"] | 60; - // BLDC_MotorA_Voltage = round2(json["BLDC_MotorA_Voltage"] | 20.0); - // BLDC_MotorA_Current = round2(json["BLDC_MotorA_Current"] | 1.0); - // BLDC_MotorA_ParametersKnown = json["BLDC_MotorA_ParametersKnown"] | false; - // BLDC_MotorA_ZeroElecAngle = round2(json["BLDC_MotorA_ZeroElecAngle"] | 0.00); - // BLDC_RailLength = json["BLDC_RailLength"] | 125; - // BLDC_StrokeLength = json["BLDC_StrokeLength"] | 120; - - // setBoardPinout(json); - - // if(isBoardType(BoardType::CRIMZZON)) { - // heaterResolution = json["heaterResolution"] | 8; - // caseFanResolution = json["caseFanResolution"] | 10; - // caseFanFrequency = json["caseFanFrequency"] | 25; - // Display_Screen_Height = json["Display_Screen_Height"] | 32; - // } - - // twistFrequency = json["twistFrequency"] | 50; - // squeezeFrequency = json["squeezeFrequency"] | 50; - // valveFrequency = json["valveFrequency"] | 50; - // continuousTwist = json["continuousTwist"]; - // feedbackTwist = json["feedbackTwist"]; - // analogTwist = json["analogTwist"]; - // TwistServo_ZERO = json["TwistServo_ZERO"] | 1500; - // ValveServo_ZERO = json["ValveServo_ZERO"] | 1500; - // SqueezeServo_ZERO = json["Squeeze_ZERO"] | 1500; - - // staticIP = json["staticIP"]; - // const char *localIPTemp = json["localIP"] | "192.168.0.150"; - // if (localIPTemp != nullptr) - // strcpy(localIP, localIPTemp); - // const char *gatewayTemp = json["gateway"] | "192.168.0.1"; - // if (gatewayTemp != nullptr) - // strcpy(gateway, gatewayTemp); - // const char *subnetTemp = json["subnet"] | "255.255.255.0"; - // if (subnetTemp != nullptr) - // strcpy(subnet, subnetTemp); - // const char *dns1Temp = json["dns1"] | "8.8.8.8"; - // if (dns1Temp != nullptr) - // strcpy(dns1, dns1Temp); - // const char *dns2Temp = json["dns2"] | "8.8.4.4"; - // if (dns2Temp != nullptr) - // strcpy(dns2, dns2Temp); - - // autoValve = json["autoValve"]; - // inverseValve = json["inverseValve"]; - // valveServo90Degrees = json["valveServo90Degrees"]; - // inverseStroke = json["inverseStroke"]; - // inversePitch = json["inversePitch"]; - // lubeEnabled = json["lubeEnabled"]; - // lubeAmount = json["lubeAmount"] | 255; - // displayEnabled = json["displayEnabled"] | true; - // sleeveTempDisplayed = json["sleeveTempDisplayed"]; - // internalTempDisplayed = json["internalTempDisplayed"]; - // versionDisplayed = json["versionDisplayed"] | true; - // Display_Screen_Width = json["Display_Screen_Width"] | 128; - // if(!isBoardType(BoardType::CRIMZZON)) { - // Display_Screen_Height = json["Display_Screen_Height"] | 64; - // } - // const char *Display_I2C_AddressTemp = json["Display_I2C_Address"] | "0x3c"; - // if (Display_I2C_AddressTemp != nullptr) - // Display_I2C_Address = (int)strtol(Display_I2C_AddressTemp, NULL, 0); - // Display_Rst_PIN = json["Display_Rst_PIN"] | -1; - - // tempSleeveEnabled = json["tempSleeveEnabled"]; - // heaterThreshold = json["heaterThreshold"] | 5.0; - // heaterFrequency = json["heaterFrequency"] | 50; - // if(!isBoardType(BoardType::CRIMZZON)) { - // heaterResolution = json["heaterResolution"] | 8; - // } - // TargetTemp = json["TargetTemp"] | 40.0; - // HeatPWM = json["HeatPWM"] | 255; - // HoldPWM = json["HoldPWM"] | 110; - - // tempInternalEnabled = json["tempInternalEnabled"]; - // fanControlEnabled = json["fanControlEnabled"]; - // internalTempForFan = json["internalTempForFan"] | 30.0; - // internalMaxTemp = json["internalMaxTemp"] | 50.0; - - // batteryLevelEnabled = json["batteryLevelEnabled"]; - // batteryLevelNumeric = json["batteryLevelNumeric"]; - // batteryVoltageMax = json["batteryVoltageMax"] | 12.6; - // batteryCapacityMax = json["batteryCapacityMax"] | 3500; - - // if(!isBoardType(BoardType::CRIMZZON)) { - // caseFanFrequency = json["caseFanFrequency"] | 25; - // caseFanResolution = json["caseFanResolution"] | 10; - // } - // caseFanMaxDuty = pow(2, caseFanResolution) - 1; - - // lubeEnabled = json["lubeEnabled"]; - - // setValue(json, voiceEnabled, "voiceHandler", "voiceEnabled", false); - // setValue(json, voiceMuted, "voiceHandler", "voiceMuted", false); - // setValue(json, voiceVolume, "voiceHandler", "voiceVolume", 0); - // setValue(json, voiceWakeTime, "voiceHandler", "voiceWakeTime", 10); - - // lastRebootReason = machine_reset_cause(); - // LogHandler::debug(_TAG, "Last reset reason: %s", SettingsHandler::lastRebootReason); - - // LogUpdateDebug(); - // return true; - // } - - // static bool compileCommonJsonDocument(DynamicJsonDocument& doc) - // { - // // LogHandler::info(_TAG, "Save settings"); - // // Delete existing file, otherwise the configuration is appended to the file - // // Serial.print("LittleFS used: "); - // // Serial.println(LittleFS.usedBytes() + "/" + LittleFS.totalBytes()); - // // if (!LittleFS.remove(userSettingsFilePath)) - // // { - // // LogHandler::error(_TAG, "Failed to remove settings file: %s", userSettingsFilePath); - // // } - // // File file = LittleFS.open(userSettingsFilePath, FILE_WRITE); - // // if (!file) - // // { - // // LogHandler::error(_TAG, "Failed to create settings file: %s", userSettingsFilePath); - // // return false; - // // } - - // // // Allocate a temporary docdocument - // // DynamicdocDocument doc(serializeSize); - - // doc["boardType"] = (uint8_t)boardType; - // doc["logLevel"] = (int)logLevel; - // LogHandler::setLogLevel(logLevel); - // // Serial.println("logLevel: "); - // // Serial.println((int)logLevel); - // // Serial.println( doc["logLevel"] .as()); - // // Serial.println((int)doc["logLevel"]); - - // // std::vector tags; - // // tags.push_back(TagHandler::DisplayHandler); - // // tags.push_back(TagHandler::BLDCHandler); - // // tags.push_back(TagHandler::ServoHandler3); - // // LogHandler::setTags(tags); - - // for (size_t i = 0; i < currentChannels.size(); i++) - // { - // doc["channelRanges"][currentChannels[i].Name]["min"] = currentChannels[i].min; - // doc["channelRanges"][currentChannels[i].Name]["max"] = currentChannels[i].max; - // // LogHandler::debug(_TAG, "save %s min: %i", currentChannels[i].Name, doc["channelRanges"][currentChannels[i].Name]["min"].as()); - // // LogHandler::debug(_TAG, "save %s max: %i", currentChannels[i].Name, doc["channelRanges"][currentChannels[i].Name]["max"].as()); - // } - - // if (!tempInternalEnabled) - // { - // internalTempDisplayed = false; - // fanControlEnabled = false; - // } - // if (!tempSleeveEnabled) - // { - // sleeveTempDisplayed = false; - // } - // doc["fullBuild"] = fullBuild; - // doc["TCodeVersion"] = (int)TCodeVersionEnum; - - // doc["udpServerPort"] = udpServerPort; - // doc["webServerPort"] = webServerPort; - // doc["hostname"] = hostname; - // doc["friendlyName"] = friendlyName; - // doc["bluetoothEnabled"] = bluetoothEnabled; - // doc["pitchFrequencyIsDifferent"] = pitchFrequencyIsDifferent; - // doc["msPerRad"] = msPerRad; - // doc["servoFrequency"] = servoFrequency; - // doc["pitchFrequency"] = pitchFrequency; - // doc["valveFrequency"] = valveFrequency; - // doc["twistFrequency"] = twistFrequency; - // doc["squeezeFrequency"] = squeezeFrequency; - // doc["continuousTwist"] = continuousTwist; - // doc["feedbackTwist"] = feedbackTwist; - // doc["analogTwist"] = analogTwist; - // doc["TwistFeedBack_PIN"] = TwistFeedBack_PIN; - // doc["RightServo_PIN"] = RightServo_PIN; - // doc["LeftServo_PIN"] = LeftServo_PIN; - // doc["RightUpperServo_PIN"] = RightUpperServo_PIN; - // doc["LeftUpperServo_PIN"] = LeftUpperServo_PIN; - // doc["PitchLeftServo_PIN"] = PitchLeftServo_PIN; - // doc["PitchRightServo_PIN"] = PitchRightServo_PIN; - // doc["ValveServo_PIN"] = ValveServo_PIN; - // doc["TwistServo_PIN"] = TwistServo_PIN; - // doc["Squeeze_PIN"] = Squeeze_PIN; - // doc["Vibe0_PIN"] = Vibe0_PIN; - // doc["Vibe1_PIN"] = Vibe1_PIN; - // doc["Vibe2_PIN"] = Vibe2_PIN; - // doc["Vibe3_PIN"] = Vibe3_PIN; - // doc["Case_Fan_PIN"] = Case_Fan_PIN; - // doc["LubeButton_PIN"] = LubeButton_PIN; - // doc["Internal_Temp_PIN"] = Internal_Temp_PIN; - - // doc["BLDC_UsePWM"] = BLDC_UsePWM; - // doc["BLDC_UseMT6701"] = BLDC_UseMT6701; - // doc["BLDC_UseHallSensor"] = BLDC_UseHallSensor; - // doc["BLDC_Pulley_Circumference"] = BLDC_Pulley_Circumference; - // doc["BLDC_Encoder_PIN"] = BLDC_Encoder_PIN; - // doc["BLDC_ChipSelect_PIN"] = BLDC_ChipSelect_PIN; - // doc["BLDC_Enable_PIN"] = BLDC_Enable_PIN; - // doc["BLDC_HallEffect_PIN"] = BLDC_HallEffect_PIN; - // doc["BLDC_PWMchannel1_PIN"] = BLDC_PWMchannel1_PIN; - // doc["BLDC_PWMchannel2_PIN"] = BLDC_PWMchannel2_PIN; - // doc["BLDC_PWMchannel3_PIN"] = BLDC_PWMchannel3_PIN; - // doc["BLDC_MotorA_Voltage"] = round2(BLDC_MotorA_Voltage); - // doc["BLDC_MotorA_Current"] = round2(BLDC_MotorA_Current); - // doc["BLDC_MotorA_ParametersKnown"] = BLDC_MotorA_ParametersKnown; - // doc["BLDC_MotorA_ZeroElecAngle"] = round2(BLDC_MotorA_ZeroElecAngle); - // doc["BLDC_RailLength"] = BLDC_RailLength; - // doc["BLDC_StrokeLength"] = BLDC_StrokeLength; - - // LogHandler::debug(_TAG, "save %s max: %f", "BLDC_MotorA_Voltage", doc["BLDC_MotorA_Current"].as()); - - // doc["staticIP"] = staticIP; - // doc["localIP"] = localIP; - // doc["gateway"] = gateway; - // doc["subnet"] = subnet; - // doc["dns1"] = dns1; - // doc["dns2"] = dns2; - - // doc["sr6Mode"] = sr6Mode; - // doc["RightServo_ZERO"] = RightServo_ZERO; - // doc["LeftServo_ZERO"] = LeftServo_ZERO; - // doc["RightUpperServo_ZERO"] = RightUpperServo_ZERO; - // doc["LeftUpperServo_ZERO"] = LeftUpperServo_ZERO; - // doc["PitchLeftServo_ZERO"] = PitchLeftServo_ZERO; - // doc["PitchRightServo_ZERO"] = PitchRightServo_ZERO; - // doc["TwistServo_ZERO"] = TwistServo_ZERO; - // doc["ValveServo_ZERO"] = ValveServo_ZERO; - // doc["Squeeze_ZERO"] = SqueezeServo_ZERO; - // doc["autoValve"] = autoValve; - // doc["inverseValve"] = inverseValve; - // doc["valveServo90Degrees"] = valveServo90Degrees; - // doc["inverseStroke"] = inverseStroke; - // doc["inversePitch"] = inversePitch; - // doc["lubeAmount"] = lubeAmount; - // doc["lubeEnabled"] = lubeEnabled; - // doc["displayEnabled"] = displayEnabled; - // doc["sleeveTempDisplayed"] = sleeveTempDisplayed; - // doc["versionDisplayed"] = versionDisplayed; - // doc["internalTempDisplayed"] = internalTempDisplayed; - // doc["tempSleeveEnabled"] = tempSleeveEnabled; - // doc["Display_Screen_Width"] = Display_Screen_Width; - // doc["Display_Screen_Height"] = Display_Screen_Height; - // doc["TargetTemp"] = TargetTemp; - // doc["HeatPWM"] = HeatPWM; - // doc["HoldPWM"] = HoldPWM; - // std::stringstream Display_I2C_Address_String; - // Display_I2C_Address_String << "0x" << std::hex << Display_I2C_Address; - // doc["Display_I2C_Address"] = Display_I2C_Address_String.str(); - // doc["Display_Rst_PIN"] = Display_Rst_PIN; - // doc["Temp_PIN"] = Sleeve_Temp_PIN; - // doc["Heater_PIN"] = Heater_PIN; - // // doc["heaterFailsafeTime"] = String(heaterFailsafeTime); - // doc["heaterThreshold"] = heaterThreshold; - // doc["heaterResolution"] = heaterResolution; - // doc["heaterFrequency"] = heaterFrequency; - // doc["fanControlEnabled"] = fanControlEnabled; - // doc["caseFanFrequency"] = caseFanFrequency; - // doc["caseFanResolution"] = caseFanResolution; - // doc["internalTempForFan"] = internalTempForFan; - // doc["internalMaxTemp"] = internalMaxTemp; - // doc["tempInternalEnabled"] = tempInternalEnabled; - - // doc["batteryLevelEnabled"] = batteryLevelEnabled; - // //doc["Battery_Voltage_PIN"] = Battery_Voltage_PIN; - // doc["batteryLevelNumeric"] = batteryLevelNumeric; - // doc["batteryVoltageMax"] = round2(batteryVoltageMax); - // doc["batteryCapacityMax"] = batteryCapacityMax; - - // doc["voiceEnabled"] = voiceEnabled; - // doc["voiceMuted"] = voiceMuted; - // doc["voiceWakeTime"] = voiceWakeTime; - // doc["voiceVolume"] = voiceVolume; - - // JsonArray includes = doc.createNestedArray("log-include-tags"); - // std::vector includesVec = LogHandler::getIncludes(); - // for (int i = 0; i < includesVec.size(); i++) - // { - // includes.add(includesVec[i]); - // } - - // JsonArray excludes = doc.createNestedArray("log-exclude-tags"); - // std::vector excludesVec = LogHandler::getExcludes(); - // for (int i = 0; i < excludesVec.size(); i++) - // { - // excludes.add(excludesVec[i]); - // } - - // LogHandler::debug(_TAG, "isNull: %u", doc.isNull()); - // if (doc.isNull()) - // { - // LogHandler::error(_TAG, "document is null!"); - // // file.close(); - // return false; - // } - - // LogHandler::debug(_TAG, "Memory usage: %u bytes", doc.memoryUsage()); - // if (doc.memoryUsage() == 0) - // { - // LogHandler::error(_TAG, "document is empty!"); - // // file.close(); - // return false; - // } - - // LogHandler::debug(_TAG, "Is overflowed: %u", doc.overflowed()); - // if (doc.overflowed()) - // { - // LogHandler::error(_TAG, "document is overflowed! Increase serialize size: %u", doc.memoryUsage()); - // // file.close(); - // return false; - // } - - // return true; - // } - - /// @brief Locks the mutex checks for an existing file and creates it if it doesnt exist. Calls the callback function and gives the mutex. - /// @param filepath - /// @param mutableLoadDefault - /// @param mutex - /// @param jsonSize - /// @param loadFunction - /// @param json - /// @return - static bool loadSettingsJson(const char *filepath, bool loadDefault, SemaphoreHandle_t &mutex, std::function loadFunction, std::function saveFunction, JsonObject json = JsonObject()) - { - JsonDocument doc; // jsonSize - bool mutableLoadDefault = loadDefault; - if (mutableLoadDefault || json.isNull()) - { - xSemaphoreTake(mutex, portMAX_DELAY); - if (!checkForFileAndLoad(filepath, doc, mutableLoadDefault)) - { - xSemaphoreGive(mutex); - return false; - } - json = doc.as(); - } - if (!loadFunction(json, mutableLoadDefault)) - { - xSemaphoreGive(mutex); - return false; - } - xSemaphoreGive(mutex); - if (mutableLoadDefault) - saveFunction(JsonObject()); - return true; - } - - /// @brief Locks the mutex and validates the file exists. calls the calback and serializes the data in a file to disk. Releases the mutex. - /// @param filepath - /// @param mutex - /// @param jsonSize - /// @param saveFunction - /// @param json - /// @return - static bool saveSettingsJson(const char *filepath, SemaphoreHandle_t &mutex, int jsonSize, std::function saveFunction, std::function loadFunction, JsonObject json = JsonObject()) - { - saving = true; - xSemaphoreTake(mutex, portMAX_DELAY); - LogHandler::debug(_TAG, "Saving File: %s", filepath); - bool loadBeforeSetting = false; - if (!LittleFS.exists(filepath)) - { - LogHandler::error(_TAG, "File did not exist whan saving: %s", filepath); - xSemaphoreGive(mutex); - saving = false; - return false; - } - else - { - if (!json.isNull()) - { - LogHandler::debug(_TAG, "Loading from input json: %s", filepath); - xSemaphoreGive(mutex); - if (!loadFunction(false, json)) - { - LogHandler::error(_TAG, "File loading input json failed: %s", filepath); - return false; - } - xSemaphoreTake(mutex, portMAX_DELAY); - } - LogHandler::debug(_TAG, "jsonSize: %ld", jsonSize); - JsonDocument doc; // jsonSize - if (!saveFunction(doc)) - { - LogHandler::error(_TAG, "Failed to compile JSON object: %s", filepath); - xSemaphoreGive(mutex); - saving = false; - return false; - } - LogHandler::debug(_TAG, "Doc overflowed: %u", doc.overflowed()); - // LogHandler::debug(_TAG, "Doc memory: %u", doc.memoryUsage()); - // LogHandler::debug(_TAG, "Doc capacity: %u", doc.capacity()); - File file = LittleFS.open(filepath, FILE_WRITE); - if (serializeJson(doc, file) == 0) - { - LogHandler::error(_TAG, "Failed to write to file: %s", filepath); - file.close(); - xSemaphoreGive(mutex); - saving = false; - return false; - } - LogHandler::debug(_TAG, "File contents: %s", file.readString().c_str()); - file.close(); - printFree(); - } - saving = false; - xSemaphoreGive(mutex); - return true; - } - - static bool checkForFileAndLoad(const char *path, JsonDocument &doc, bool &loadDefault) - { - if (!LittleFS.exists(path)) - { - loadDefault = true; - } - if (loadDefault) - { - defaultJsonFile(path); - } - return loadJsonFromFile(path, doc); - } - - static bool defaultJsonFile(const char *path) - { - LogHandler::debug(_TAG, "Defaulting file %s", path); - if (LittleFS.exists(path)) - { - LogHandler::debug(_TAG, "Deleting file %s", path); - if (!LittleFS.remove(path)) - { - LogHandler::error(_TAG, "Error deleting %s!", path); - return false; - } - } - LogHandler::debug(_TAG, "Creating file %s", path); - File newFile = LittleFS.open(path, FILE_WRITE, true); - if (!newFile) - { - LogHandler::error(_TAG, "Error creating %s!", path); - return false; - } - newFile.print("{}"); - newFile.flush(); - newFile.close(); - return true; - } - - static bool loadJsonFromFile(const char *path, JsonDocument &doc) - { - LogHandler::debug(_TAG, "Loading json file %s", path); - if (!LittleFS.exists(path)) - { - LogHandler::error(_TAG, "%s did not exist!", path); - return false; - } - - File file = LittleFS.open(path, FILE_READ); - if (!file) - { - LogHandler::error(_TAG, "%s failed to open!", path); - return false; - } - if (LogDeserializationError(deserializeJson(doc, file), file.name())) - { - file.close(); - return false; - } - file.close(); - return true; - } - - static bool LogDeserializationError(DeserializationError error, const char *filename) - { - if (error) - { - LogHandler::error(_TAG, "Error deserializing json: %s", filename); - switch (error.code()) - { - case DeserializationError::Code::Ok: - LogHandler::error(_TAG, "Code: Ok"); - break; - case DeserializationError::Code::EmptyInput: - LogHandler::error(_TAG, "Code: EmptyInput"); - break; - case DeserializationError::Code::IncompleteInput: - LogHandler::error(_TAG, "Code: IncompleteInput"); - break; - case DeserializationError::Code::InvalidInput: - LogHandler::error(_TAG, "Code: InvalidInput"); - break; - case DeserializationError::Code::NoMemory: - LogHandler::error(_TAG, "Code: NoMemory"); - break; - case DeserializationError::Code::TooDeep: - LogHandler::error(_TAG, "Code: TooDeep"); - break; - } - return true; - } - return false; - } - // /** If the parameter json is ommited or the pin value doesnt exist on the object then the pins are set to default. */ - // static void setBoardPinout(JsonObject json = JsonObject()) { - - // #if MOTOR_TYPE == 0 - // if(isBoardType(BoardType::ISAAC)) { - // // RightServo_PIN = 2; - // // LeftServo_PIN = 13; - // // PitchLeftServo_PIN = 14; - // // ValveServo_PIN = 5; - // // TwistServo_PIN = 27; - // // TwistFeedBack_PIN = 33; - // // Vibe0_PIN = 15; - // // Vibe1_PIN = 16; - // // LubeButton_PIN = 36; - // // RightUpperServo_PIN = 4; - // // LeftUpperServo_PIN = 12; - // // PitchRightServo_PIN = 17; - // // Sleeve_Temp_PIN = 25; - // // Heater_PIN = 19; - // // Squeeze_PIN = 26; - // TwistFeedBack_PIN = json["TwistFeedBack_PIN"] | 32; - // RightServo_PIN = json["RightServo_PIN"] | 4; - // LeftServo_PIN = json["LeftServo_PIN"] | 13; - // RightUpperServo_PIN = json["RightUpperServo_PIN"] | 16; - // LeftUpperServo_PIN = json["LeftUpperServo_PIN"] | 27; - // PitchLeftServo_PIN = json["PitchLeftServo_PIN"] | 26; - // PitchRightServo_PIN = json["PitchRightServo_PIN"] | 17; - // ValveServo_PIN = json["ValveServo_PIN"] | 18; - // TwistServo_PIN = json["TwistServo_PIN"] | 25; - // // Common motor - // Squeeze_PIN = json["Squeeze_PIN"] | 19; - // LubeButton_PIN = json["LubeButton_PIN"] | 34; - // // Internal_Temp_PIN = json["Internal_Temp_PIN"] | 34; - // Sleeve_Temp_PIN = json["Temp_PIN"] | 33; - // // Case_Fan_PIN = json["Case_Fan_PIN"] | 16; - // Vibe0_PIN = json["Vibe0_PIN"] | 15; - // Vibe1_PIN = json["Vibe1_PIN"] | 2; - // // Vibe2_PIN = json["Vibe2_PIN"] | 23; - // // Vibe3_PIN = json["Vibe3_PIN"] | 32; - // Heater_PIN = json["Heater_PIN"] | 5; - // } else if(isBoardType(BoardType::CRIMZZON)) { - - // Vibe3_PIN = json["Vibe3_PIN"] | 26; - // Internal_Temp_PIN = json["Internal_Temp_PIN"] | 32; - - // // EXT - // // EXT_Input2_PIN = 34; - // // EXT_Input3_PIN = 39; - // // EXT_Input4_PIN = 36; - - // heaterResolution = json["heaterResolution"] | 8; - // caseFanResolution = json["caseFanResolution"] | 10; - // caseFanFrequency = json["caseFanFrequency"] | 25; - // Display_Screen_Height = json["Display_Screen_Height"] | 32; - // TwistFeedBack_PIN = json["TwistFeedBack_PIN"] | 0; - // } - // if(!isBoardType(BoardType::ISAAC)) { // Devkit v1 pins - // if(!isBoardType(BoardType::CRIMZZON)) { - // TwistFeedBack_PIN = json["TwistFeedBack_PIN"] | 26; - // } - // // Common motor - // Squeeze_PIN = json["Squeeze_PIN"] | 17; - // LubeButton_PIN = json["LubeButton_PIN"] | 35; - // if(!isBoardType(BoardType::CRIMZZON)) { - // Internal_Temp_PIN = json["Internal_Temp_PIN"] | 34; - // } - // Case_Fan_PIN = json["Case_Fan_PIN"] | 16; - - // //Stock servo motors - // RightServo_PIN = json["RightServo_PIN"] | 13; - // LeftServo_PIN = json["LeftServo_PIN"] | 15; - // RightUpperServo_PIN = json["RightUpperServo_PIN"] | 12; - // LeftUpperServo_PIN = json["LeftUpperServo_PIN"] | 2; - // PitchLeftServo_PIN = json["PitchLeftServo_PIN"] | 4; - // PitchRightServo_PIN = json["PitchRightServo_PIN"] | 14; - - // Heater_PIN = json["Heater_PIN"] | 33; - // ValveServo_PIN = json["ValveServo_PIN"] | 25; - // TwistServo_PIN = json["TwistServo_PIN"] | 27; - // Sleeve_Temp_PIN = json["Temp_PIN"] | 5; - // Vibe0_PIN = json["Vibe0_PIN"] | 18; - // Vibe1_PIN = json["Vibe1_PIN"] | 19; - // Vibe2_PIN = json["Vibe2_PIN"] | 23; - // if(!isBoardType(BoardType::CRIMZZON)) { - // Vibe3_PIN = json["Vibe3_PIN"] | 32; - // } - // } - // #elif MOTOR_TYPE == 1 - // // BLDC motor - // BLDC_Encoder_PIN = json["BLDC_Encoder_PIN"] | 33; - // BLDC_ChipSelect_PIN = json["BLDC_ChipSelect_PIN"] | 5; - // BLDC_Enable_PIN = json["BLDC_Enable_PIN"] | 14; - // BLDC_PWMchannel1_PIN = json["BLDC_PWMchannel1_PIN"] | 27; - // BLDC_PWMchannel2_PIN = json["BLDC_PWMchannel2_PIN"] | 26; - // BLDC_PWMchannel3_PIN = json["BLDC_PWMchannel3_PIN"] | 25; - - // // PWM - // Heater_PIN = json["Heater_PIN"] | 15; - // Sleeve_Temp_PIN = json["Temp_PIN"] | 36; - // Vibe0_PIN = json["Vibe0_PIN"] | 2; - // Vibe1_PIN = json["Vibe1_PIN"] | 4; - // Vibe2_PIN = json["Vibe2_PIN"] | -1; - // Vibe3_PIN = json["Vibe3_PIN"] | -1; - // Case_Fan_PIN = json["Case_Fan_PIN"] | 16; - - // // PWM servo - // ValveServo_PIN = json["ValveServo_PIN"] | 12; - // TwistServo_PIN = json["TwistServo_PIN"] | 13; - // Squeeze_PIN = json["Squeeze_PIN"] | 17; - - // // Input - // TwistFeedBack_PIN = json["TwistFeedBack_PIN"] | 26; - // LubeButton_PIN = json["LubeButton_PIN"] | 35; - // Internal_Temp_PIN = json["Internal_Temp_PIN"] | 34; - // BLDC_HallEffect_PIN = json["BLDC_HallEffect_PIN"] | 35; - // #endif - // } - - static void setBuildFeatures() - { - int index = 0; -#if WIFI_TCODE - LogHandler::debug(Tags::Settings, "WIFI_TCODE"); - buildFeatures[index] = BuildFeature::WIFI; - index++; -#endif -#if BLUETOOTH_TCODE - LogHandler::debug(Tags::Settings, "BLUETOOTH_TCODE"); - buildFeatures[index] = BuildFeature::BLUETOOTH; - index++; -#endif -#if BLE_TCODE - LogHandler::debug(Tags::Settings, "BLE_TCODE"); - buildFeatures[index] = BuildFeature::BLE; - index++; -#endif -#if DEBUG_BUILD - LogHandler::debug(Tags::Settings, "DEBUG_BUILD"); - buildFeatures[index] = BuildFeature::DEBUG; - index++; -#endif -#ifdef ESP32_DA - LogHandler::debug(Tags::Settings, "ESP32_DA"); - buildFeatures[index] = BuildFeature::DA; - index++; -#endif -#if BUILD_TEMP - LogHandler::debug(Tags::Settings, "BUILD_TEMP"); - buildFeatures[index] = BuildFeature::TEMP; - index++; -#endif -#if BUILD_DISPLAY - LogHandler::debug(Tags::Settings, "BUILD_DISPLAY"); - buildFeatures[index] = BuildFeature::DISPLAY_; - index++; -#endif -// #if TCODE_V2 -// LogHandler::debug(Tags::Settings, "TCODE_V2"); -// buildFeatures[index] = BuildFeature::HAS_TCODE_V2; -// index++; -// #endif -#if SECURE_WEB - LogHandler::debug(Tags::Settings, "HTTPS"); - buildFeatures[index] = BuildFeature::HTTPS; - index++; -#endif -#if COEXIST - LogHandler::debug(Tags::Settings, "COEXIST"); - buildFeatures[index] = BuildFeature::COEXIST_FEATURE; - index++; -#endif - buildFeatures[(int)BuildFeature::MAX_FEATURES - 1] = {}; - } - - static void setMotorType() - { -#if MOTOR_TYPE == 0 - m_settingsFactory->setValue(MOTOR_TYPE_SETTING, (int)MotorType::Servo); -#elif MOTOR_TYPE == 1 - m_settingsFactory->setValue(MOTOR_TYPE_SETTING, (int)MotorType::BLDC); -#endif - } - - static void sendMessage(const SettingProfile &profile, const char *message) - { - if (message_callback) - { - LogHandler::debug(_TAG, "sendMessage: message_callback %s", message); - message_callback(profile, message); - } - else - { - LogHandler::debug(_TAG, "sendMessage: message_callback 0"); - } - } - - static void setValue(JsonObject json, bool &variable, const SettingProfile &profile, const char *propertyName, bool defaultValue) - { - bool newValue = json[propertyName] | defaultValue; - setValue(newValue, variable, profile, propertyName); - } - - template - static void setValue(JsonObject json, char (&variable)[n], const SettingProfile &profile, const char *propertyName, const char *defaultValue) - { - const char *newValue = json[propertyName] | defaultValue; - setValue(newValue, variable, profile, propertyName); - } - - static void setValue(JsonObject json, int &variable, const SettingProfile &profile, const char *propertyName, int defaultValue) - { - int newValue = json[propertyName] | defaultValue; - setValue(newValue, variable, profile, propertyName); - } - static void setValue(JsonObject json, uint8_t &variable, const SettingProfile &profile, const char *propertyName, uint8_t defaultValue) - { - uint8_t newValue = json[propertyName] | defaultValue; - setValue(newValue, variable, profile, propertyName); - } - static void setValue(JsonObject json, uint16_t &variable, const SettingProfile &profile, const char *propertyName, uint16_t defaultValue) - { - uint16_t newValue = json[propertyName] | defaultValue; - setValue(newValue, variable, profile, propertyName); - } - - static void setValue(JsonObject json, float &variable, const SettingProfile &profile, const char *propertyName, float defaultValue) - { - float newValue = json[propertyName] | defaultValue; - setValue(newValue, variable, profile, propertyName); - } - - static void setValue(JsonObject json, std::vector &variable, const SettingProfile &profile, const char *propertyName) - { - variable.clear(); - if (json[propertyName].isNull()) - { - return; - } - JsonArray jsonArray = json[propertyName].as(); - for (int i = 0; i < jsonArray.size(); i++) - { - variable.push_back(jsonArray[i]); - } - if (initialized) - sendMessage(profile, propertyName); - } - - static void setValue(JsonObject json, std::vector &variable, const SettingProfile &profile, const char *propertyName) - { - variable.clear(); - if (json[propertyName].isNull()) - { - return; - } - JsonArray jsonArray = json[propertyName].as(); - for (int i = 0; i < jsonArray.size(); i++) - { - variable.push_back(jsonArray[i]); - } - if (initialized) - sendMessage(profile, propertyName); - } - - static void setValue(bool newValue, bool &variable, const SettingProfile &profile, const char *propertyName) - { - bool valueChanged = initialized && variable != newValue; - LogHandler::debug(Tags::Settings, "Set bool '%s' oldValue '%ld' newValue '%ld' changed: '%ld'", propertyName, variable, newValue, valueChanged); - variable = newValue; - if (valueChanged) - sendMessage(profile, propertyName); - } - - template - static void setValue(const char *newValue, char (&variable)[n], const SettingProfile &profile, const char *propertyName) - { - bool valueChanged = initialized && strcmp(variable, newValue) != -1; - LogHandler::debug(Tags::Settings, "Set char* '%s' oldValue '%s' newValue '%s' changed: '%ld'", propertyName, variable, newValue, valueChanged); - strcpy(variable, newValue); - if (valueChanged) - sendMessage(profile, propertyName); - } - - static void setValue(int newValue, int &variable, const SettingProfile &profile, const char *propertyName) - { - bool valueChanged = initialized && variable != newValue; - LogHandler::debug(Tags::Settings, "Set int '%s' oldValue '%ld' newValue '%ld' changed: '%ld'", propertyName, variable, newValue, valueChanged); - variable = newValue; - if (valueChanged) - sendMessage(profile, propertyName); - } - - static void setValue(uint8_t newValue, uint8_t &variable, const SettingProfile &profile, const char *propertyName) - { - bool valueChanged = initialized && variable != newValue; - LogHandler::debug(Tags::Settings, "Set int '%s' oldValue '%u' newValue '%u' changed: '%ld'", propertyName, variable, newValue, valueChanged); - variable = newValue; - if (valueChanged) - sendMessage(profile, propertyName); - } - - static void setValue(uint16_t newValue, uint16_t &variable, const SettingProfile &profile, const char *propertyName) - { - bool valueChanged = initialized && variable != newValue; - LogHandler::debug(Tags::Settings, "Set int '%s' oldValue '%u' newValue '%u' changed: '%ld'", propertyName, variable, newValue, valueChanged); - variable = newValue; - if (valueChanged) - sendMessage(profile, propertyName); - } - - static void setValue(float newValue, float &variable, const SettingProfile &profile, const char *propertyName) - { - bool valueChanged = initialized && variable != newValue; - LogHandler::debug(Tags::Settings, "Set float '%s' oldValue '%f' newValue '%f' changed: '%ld'", propertyName, variable, newValue, valueChanged); - variable = newValue; - if (valueChanged) - sendMessage(profile, propertyName); - } - - static u_int16_t calculateRange(const char *channel, int value) - { - return constrain(value, getChannelMin(channel), getChannelMax(channel)); - } - - // Function that gets current epoch time - static unsigned long getTime() - { - time_t now; - struct tm timeinfo; - if (!getLocalTime(&timeinfo)) - { - // Serial.println("Failed to obtain time"); - return (0); - } - time(&now); - return now; - } - - static const char *machine_reset_cause() - { - switch (esp_reset_reason()) - { - case ESP_RST_POWERON: - return "Reset due to power-on event"; - break; - case ESP_RST_BROWNOUT: - return "Brownout reset (software or hardware)"; - break; - case ESP_RST_INT_WDT: - return "Reset (software or hardware) due to interrupt watchdog"; - break; - case ESP_RST_TASK_WDT: - return "Reset due to task watchdog"; - break; - case ESP_RST_WDT: - return "Reset due to other watchdogs"; - break; - case ESP_RST_DEEPSLEEP: - return "Reset after exiting deep sleep mode"; - break; - case ESP_RST_SW: - return "Software reset via esp_restart"; - break; - case ESP_RST_PANIC: - return "Software reset due to exception/panic"; - break; - case ESP_RST_EXT: // Comment in ESP-IDF: "For ESP32, ESP_RST_EXT is never returned" - return "Reset by external pin (not applicable for ESP32)"; - break; - case ESP_RST_SDIO: - return "Reset over SDIO"; - break; - case ESP_RST_UNKNOWN: - return "Reset reason can not be determined"; - break; - default: - return ""; - break; - } - } - - // static bool LogDeserializationError(DeserializationError error, const char* filename) { - // if (error) - // { - // LogHandler::error(_TAG, "Error deserializing json: %s", filename); - // switch (error.code()) - // { - // case DeserializationError::Code::Ok: - // LogHandler::error(_TAG, "Code: Ok"); - // break; - // case DeserializationError::Code::EmptyInput: - // LogHandler::error(_TAG, "Code: EmptyInput"); - // break; - // case DeserializationError::Code::IncompleteInput: - // LogHandler::error(_TAG, "Code: IncompleteInput"); - // break; - // case DeserializationError::Code::InvalidInput: - // LogHandler::error(_TAG, "Code: InvalidInput"); - // break; - // case DeserializationError::Code::NoMemory: - // LogHandler::error(_TAG, "Code: NoMemory"); - // break; - // case DeserializationError::Code::TooDeep: - // LogHandler::error(_TAG, "Code: TooDeep"); - // break; - // } - // return true; - // } - // return false; - // } - - // static void LogSaveDebug(const DynamicJsonDocument doc) - // { - // Commented out due to error in logger on hostname. - // LogHandler::debug(_TAG, "save TCodeVersionEnum: %i", doc["TCodeVersion"].as()); - // LogHandler::debug(_TAG, "save ssid: %s", doc["ssid"].as()); - // // LogHandler::debug(_TAG, "save pass: %s", doc["wifiPass"].as()); - // LogHandler::debug(_TAG, "save udpServerPort: %i", doc["udpServerPort"].as()); - // LogHandler::debug(_TAG, "save webServerPort: %i", doc["webServerPort"].as()); - // LogHandler::debug(_TAG, "save hostname: %s", doc["hostname"].as()); - // LogHandler::debug(_TAG, "save friendlyName: %s", doc["friendlyName"].as()); - // LogHandler::debug(_TAG, "save pitchFrequencyIsDifferent ", doc["pitchFrequencyIsDifferent"].as()); - // LogHandler::debug(_TAG, "save msPerRad: %i", doc["msPerRad"].as()); - // LogHandler::debug(_TAG, "save servoFrequency: %i", doc["servoFrequency"].as()); - // LogHandler::debug(_TAG, "save pitchFrequency: %i", doc["pitchFrequency"].as()); - // LogHandler::debug(_TAG, "save valveFrequency: %i", doc["valveFrequency"].as()); - // LogHandler::debug(_TAG, "save twistFrequency: %i", doc["twistFrequency"].as()); - // LogHandler::debug(_TAG, "save continuousTwist: %i", doc["continuousTwist"].as()); - // LogHandler::debug(_TAG, "save feedbackTwist: %i", doc["feedbackTwist"].as()); - // LogHandler::debug(_TAG, "save analogTwist: %i", doc["analogTwist"].as()); - // LogHandler::debug(_TAG, "save TwistFeedBack_PIN: %i", doc["TwistFeedBack_PIN"].as()); - // LogHandler::debug(_TAG, "save RightServo_PIN: %i", doc["RightServo_PIN"].as()); - // LogHandler::debug(_TAG, "save LeftServo_PIN: %i", doc["LeftServo_PIN"].as()); - // LogHandler::debug(_TAG, "save RightUpperServo_PIN: %i", doc["RightUpperServo_PIN"].as()); - // LogHandler::debug(_TAG, "save LeftUpperServo_PIN: %i", doc["LeftUpperServo_PIN"].as()); - // LogHandler::debug(_TAG, "save PitchLeftServo_PIN: %i", doc["PitchLeftServo_PIN"].as()); - // LogHandler::debug(_TAG, "save PitchRightServo_PIN: %i", doc["PitchRightServo_PIN"].as()); - // LogHandler::debug(_TAG, "save ValveServo_PIN: %i", doc["ValveServo_PIN"].as()); - // LogHandler::debug(_TAG, "save TwistServo_PIN: %i", doc["TwistServo_PIN"].as()); - // LogHandler::debug(_TAG, "save Vibe0_PIN: %i", doc["Vibe0_PIN"].as()); - // LogHandler::debug(_TAG, "save Vibe1_PIN: %i", doc["Vibe1_PIN"].as()); - // LogHandler::debug(_TAG, "save Lube_Pin: %i", doc["Lube_Pin"].as()); - // LogHandler::debug(_TAG, "save LubeButton_PIN: %i", doc["LubeButton_PIN"].as()); - // LogHandler::debug(_TAG, "save staticIP: %i", doc["staticIP"].as()); - // LogHandler::debug(_TAG, "save localIP: %s", doc["localIP"].as()); - // LogHandler::debug(_TAG, "save gateway: %s", doc["gateway"].as()); - // LogHandler::debug(_TAG, "save subnet: %s", doc["subnet"].as()); - // LogHandler::debug(_TAG, "save dns1: %s", doc["dns1"].as()); - // LogHandler::debug(_TAG, "save dns2: %s", doc["dns2"].as()); - // LogHandler::debug(_TAG, "save sr6Mode: %i", doc["sr6Mode"].as()); - // LogHandler::debug(_TAG, "save RightServo_ZERO: %i", doc["RightServo_ZERO"].as()); - // LogHandler::debug(_TAG, "save LeftServo_ZERO: %i", doc["LeftServo_ZERO"].as()); - // LogHandler::debug(_TAG, "save RightUpperServo_ZERO: %i", doc["RightUpperServo_ZERO"].as()); - // LogHandler::debug(_TAG, "save LeftUpperServo_ZERO: %i", doc["LeftUpperServo_ZERO"].as()); - // LogHandler::debug(_TAG, "save PitchLeftServo_ZERO: %i", doc["PitchLeftServo_ZERO"].as()); - // LogHandler::debug(_TAG, "save PitchRightServo_ZERO: %i", doc["PitchRightServo_ZERO"].as()); - // LogHandler::debug(_TAG, "save TwistServo_ZERO: %i", doc["TwistServo_ZERO"].as()); - // LogHandler::debug(_TAG, "save ValveServo_ZERO: %i", doc["ValveServo_ZERO"].as()); - // LogHandler::debug(_TAG, "save autoValve: %i", doc["autoValve"].as()); - // LogHandler::debug(_TAG, "save inverseValve: %i", doc["inverseValve"].as()); - // LogHandler::debug(_TAG, "save valveServo90Degrees: %i", doc["valveServo90Degrees"].as()); - // LogHandler::debug(_TAG, "save inverseStroke: %i", doc["inverseStroke"].as()); - // LogHandler::debug(_TAG, "save inversePitch: %i", doc["inversePitch"].as()); - // LogHandler::debug(_TAG, "save lubeEnabled: %i", doc["lubeEnabled"].as()); - // LogHandler::debug(_TAG, "save lubeAmount: %i", doc["lubeAmount"].as()); - // LogHandler::debug(_TAG, "save Temp_PIN: %i", doc["Temp_PIN"].as()); - // LogHandler::debug(_TAG, "save Heater_PIN: %i", doc["Heater_PIN"].as()); - // LogHandler::debug(_TAG, "save displayEnabled: %i", doc["displayEnabled"].as()); - // LogHandler::debug(_TAG, "save sleeveTempDisplayed: %i", doc["sleeveTempDisplayed"].as()); - // LogHandler::debug(_TAG, "save internalTempDisplayed: %i", doc["internalTempDisplayed"].as()); - // LogHandler::debug(_TAG, "save tempSleeveEnabled: %i", doc["tempSleeveEnabled"].as()); - // LogHandler::debug(_TAG, "save tempInternalEnabled: %i", doc["tempInternalEnabled"].as()); - // LogHandler::debug(_TAG, "save Display_Screen_Width: %i", doc["Display_Screen_Width"].as()); - // LogHandler::debug(_TAG, "save internalMaxTemp: %f", doc["internalMaxTemp"].as()); - // LogHandler::debug(_TAG, "save internalTempForFan: %f", doc["internalTempForFan"].as()); - // LogHandler::debug(_TAG, "save Display_Screen_Height: %i", doc["Display_Screen_Height"].as()); - // LogHandler::debug(_TAG, "save TargetTemp: %f", doc["TargetTemp"].as()); - // LogHandler::debug(_TAG, "save HeatPWM: %i", doc["HeatPWM"].as()); - // LogHandler::debug(_TAG, "save HoldPWM: %i", doc["HoldPWM"].as()); - // LogHandler::debug(_TAG, "save Display_I2C_Address: %i", doc["Display_I2C_Address"].as()); - // LogHandler::debug(_TAG, "save Display_Rst_PIN: %i", doc["Display_Rst_PIN"].as()); - // LogHandler::debug(_TAG, "save heaterFailsafeTime: %ld", doc["heaterFailsafeTime"].as()); - // LogHandler::debug(_TAG, "save heaterThreshold: %i", doc["heaterThreshold"].as()); - // LogHandler::debug(_TAG, "save heaterResolution: %i", doc["heaterResolution"].as()); - // LogHandler::debug(_TAG, "save heaterFrequency: %i", doc["heaterFrequency"].as()); - // LogHandler::debug(_TAG, "save newtoungeHatExists: %i", doc["newtoungeHatExists"].as()); - // LogHandler::debug(_TAG, "save logLevel: %i", doc["logLevel"].as()); - // LogHandler::debug(_TAG, "save bluetoothEnabled: %i", doc["bluetoothEnabled"].as()); - // } - - // static void LogUpdateDebug() - // { - // LogHandler::debug(_TAG, "update TCodeVersionEnum: %i", TCodeVersionEnum); - // LogHandler::debug(_TAG, "update ssid: %s", ssid); - // //LogHandler::debug(_TAG, "update wifiPass: %s", wifiPass); - // LogHandler::debug(_TAG, "update udpServerPort: %i", udpServerPort); - // LogHandler::debug(_TAG, "update webServerPort: %i", webServerPort); - // LogHandler::debug(_TAG, "update hostname: %s", hostname); - // LogHandler::debug(_TAG, "update friendlyName: %s", friendlyName); - // LogHandler::debug(_TAG, "update pitchFrequencyIsDifferent: %i", pitchFrequencyIsDifferent); - // LogHandler::debug(_TAG, "update msPerRad: %i", msPerRad); - // LogHandler::debug(_TAG, "update servoFrequency: %i", servoFrequency); - // LogHandler::debug(_TAG, "update pitchFrequency: %i", pitchFrequency); - // LogHandler::debug(_TAG, "update valveFrequency: %i", valveFrequency); - // LogHandler::debug(_TAG, "update twistFrequency: %i", twistFrequency); - // LogHandler::debug(_TAG, "update continuousTwist: %i", continuousTwist); - // LogHandler::debug(_TAG, "update feedbackTwist: %i", feedbackTwist);max - // LogHandler::debug(_TAG, "update PitchRightServo_ZERO: %i", PitchRightServo_ZERO); - // LogHandler::debug(_TAG, "update TwistServo_ZERO: %i", TwistServo_ZERO); - // LogHandler::debug(_TAG, "update ValveServo_ZERO: %i", ValveServo_ZERO); - // LogHandler::debug(_TAG, "update autoValve: %i", autoValve); - // LogHandler::debug(_TAG, "update inverseValve: %i", inverseValve); - // LogHandler::debug(_TAG, "update valveServo90Degrees: %i", valveServo90Degrees); - // LogHandler::debug(_TAG, "update inverseStroke: %i", inverseStroke); - // LogHandler::debug(_TAG, "update inversePitch: %i", inversePitch); - // LogHandler::debug(_TAG, "update lubeEnabled: %i", lubeEnabled); - // LogHandler::debug(_TAG, "update lubeAmount: %i", lubeAmount); - // LogHandler::debug(_TAG, "update displayEnabled: %i", displayEnabled); - // LogHandler::debug(_TAG, "update sleeveTempDisplayed: %i", sleeveTempDisplayed); - // LogHandler::debug(_TAG, "update internalTempDisplayed: %i", internalTempDisplayed); - // LogHandler::debug(_TAG, "update tempSleeveEnabled: %i", tempSleeveEnabled); - // LogHandler::debug(_TAG, "update tempInternalEnabled: %i", tempInternalEnabled); - // LogHandler::debug(_TAG, "update Display_Screen_Width: %i", Display_Screen_Width); - // LogHandler::debug(_TAG, "update Display_Screen_Height: %i", Display_Screen_Height); - // LogHandler::debug(_TAG, "update TargetTemp: %i", TargetTemp); - // LogHandler::debug(_TAG, "update HeatPWM: %i", HeatPWM); - // LogHandler::debug(_TAG, "update HoldPWM: %i", HoldPWM); - // LogHandler::debug(_TAG, "update Display_I2C_Address: %i", Display_I2C_Address); - // LogHandler::debug(_TAG, "update Display_Rst_PIN: %i", Display_Rst_PIN); - // LogHandler::debug(_TAG, "update Sleeve_Temp_PIN: %i", Sleeve_Temp_PIN); - // LogHandler::debug(_TAG, "update Heater_PIN: %i", Heater_PIN); - // LogHandler::debug(_TAG, "update heaterThreshold: %d", heaterThreshold); - // LogHandler::debug(_TAG, "update heaterResolution: %i", heaterResolution); - // LogHandler::debug(_TAG, "update heaterFrequency: %i", heaterFrequency); - // LogHandler::debug(_TAG, "update logLevel: %i", (int)logLevel); - // LogHandler::debug(_TAG, "update bluetoothEnabled: %i", (int)bluetoothEnabled); - // } -}; - -SettingsFactory *SettingsHandler::m_settingsFactory; -SemaphoreHandle_t SettingsHandler::m_motionMutex = xSemaphoreCreateMutex(); -SemaphoreHandle_t SettingsHandler::m_channelsMutex = xSemaphoreCreateMutex(); -SemaphoreHandle_t SettingsHandler::m_wifiMutex = xSemaphoreCreateMutex(); -SemaphoreHandle_t SettingsHandler::m_buttonsMutex = xSemaphoreCreateMutex(); -SemaphoreHandle_t SettingsHandler::m_settingsMutex = xSemaphoreCreateMutex(); -bool SettingsHandler::initialized = false; -int SettingsHandler::restartInSecs = -1; -bool SettingsHandler::saving = false; -bool SettingsHandler::motionPaused = false; -bool SettingsHandler::fullBuild = false; -bool SettingsHandler::apMode = false; - -BuildFeature SettingsHandler::buildFeatures[(int)BuildFeature::MAX_FEATURES]; -std::vector SettingsHandler::systemI2CAddresses; -ChannelMap SettingsHandler::channelMap; - -char SettingsHandler::currentIP[IP_ADDRESS_LEN] = LOCALIP_DEFAULT; -char SettingsHandler::currentGateway[IP_ADDRESS_LEN] = GATEWAY_DEFAULT; -char SettingsHandler::currentSubnet[IP_ADDRESS_LEN] = SUBNET_DEFAULT; -char SettingsHandler::currentDns1[IP_ADDRESS_LEN] = DNS1_DEFAULT; -char SettingsHandler::currentDns2[IP_ADDRESS_LEN] = DNS2_DEFAULT; - -// MotorType SettingsHandler::motorType = MotorType::Servo; -// const char SettingsHandler::HandShakeChannel[4] = "D1\n"; -// const char SettingsHandler::SettingsChannel[4] = "D2\n"; -// const char *SettingsHandler::userSettingsFilePath = "/userSettings.json"; -// const char *SettingsHandler::wifiPassFilePath = "/wifiInfo.json"; -// const char *SettingsHandler::buttonsFilePath = "/buttons.json"; -// const char *SettingsHandler::motionProfilesFilePath = "/motionProfiles.json"; -// const char *SettingsHandler::logPath = "/log.json"; -// const char *SettingsHandler::defaultWifiPass = "YOUR PASSWORD HERE"; -// const char *SettingsHandler::decoyPass = "Too bad haxor!"; -// const char SettingsHandler::defaultIP[15] = "192.168.69.1"; -// const char SettingsHandler::defaultGateWay[15] = "192.168.69.254"; -// const char SettingsHandler::defaultSubnet[15] = "255.255.255.0"; -// bool SettingsHandler::bluetoothEnabled = true; -// LogLevel SettingsHandler::logLevel = LogLevel::INFO; -// bool SettingsHandler::isTcp = true; -// char SettingsHandler::ssid[32]; -// char SettingsHandler::wifiPass[63]; -// char SettingsHandler::hostname[63]; -// char SettingsHandler::friendlyName[100]; -// int SettingsHandler::udpServerPort; -// int SettingsHandler::webServerPort; -// int SettingsHandler::PitchRightServo_PIN; -// int SettingsHandler::RightUpperServo_PIN; -// int SettingsHandler::RightServo_PIN; -// int SettingsHandler::PitchLeftServo_PIN; -// int SettingsHandler::LeftUpperServo_PIN; -// int SettingsHandler::LeftServo_PIN; -// int SettingsHandler::ValveServo_PIN; -// int SettingsHandler::TwistServo_PIN; -// int SettingsHandler::TwistFeedBack_PIN; -// int SettingsHandler::Vibe0_PIN; -// int SettingsHandler::Vibe1_PIN; -// int SettingsHandler::LubeButton_PIN; -// int SettingsHandler::Sleeve_Temp_PIN; -// int SettingsHandler::Heater_PIN; -// int SettingsHandler::I2C_SDA_PIN_obsolete = 21; -// int SettingsHandler::I2C_SCL_PIN_obsolete = 22; - -// int SettingsHandler::Internal_Temp_PIN; -// int SettingsHandler::Case_Fan_PIN; -// int SettingsHandler::Squeeze_PIN; -// int SettingsHandler::Vibe2_PIN; -// int SettingsHandler::Vibe3_PIN; - -// // int SettingsHandler::HeatLED_PIN = 32; -// // pin 25 cannot be servo. Throws error -// bool SettingsHandler::lubeEnabled = true; - -// bool SettingsHandler::pitchFrequencyIsDifferent; -// int SettingsHandler::msPerRad; -// int SettingsHandler::servoFrequency; -// int SettingsHandler::pitchFrequency; -// int SettingsHandler::valveFrequency; -// int SettingsHandler::twistFrequency; -// int SettingsHandler::squeezeFrequency; -// bool SettingsHandler::feedbackTwist = false; -// bool SettingsHandler::continuousTwist; -// bool SettingsHandler::analogTwist; -// bool SettingsHandler::staticIP; -// char SettingsHandler::localIP[15]; -// char SettingsHandler::gateway[15]; -// char SettingsHandler::subnet[15]; -// char SettingsHandler::dns1[15]; -// char SettingsHandler::dns2[15]; -// bool SettingsHandler::sr6Mode; -// int SettingsHandler::RightServo_ZERO = 1500; -// int SettingsHandler::LeftServo_ZERO = 1500; -// int SettingsHandler::RightUpperServo_ZERO = 1500; -// int SettingsHandler::LeftUpperServo_ZERO = 1500; -// int SettingsHandler::PitchLeftServo_ZERO = 1500; -// int SettingsHandler::PitchRightServo_ZERO = 1500; - -// bool SettingsHandler::BLDC_UsePWM = false; -// bool SettingsHandler::BLDC_UseMT6701 = true; -// bool SettingsHandler::BLDC_UseHallSensor = false; -// int SettingsHandler::BLDC_Pulley_Circumference = 60; -// int SettingsHandler::BLDC_Encoder_PIN = 33;// PWM feedback pin (if used) - P pad on AS5048a -// int SettingsHandler::BLDC_Enable_PIN = 14;// Motor enable - EN on SFOCMini -// int SettingsHandler::BLDC_HallEffect_PIN = 12; -// int SettingsHandler::BLDC_PWMchannel1_PIN = 27; -// int SettingsHandler::BLDC_PWMchannel2_PIN = 26; -// int SettingsHandler::BLDC_PWMchannel3_PIN = 25; -// float SettingsHandler::BLDC_MotorA_Voltage = 20.0; // BLDC Motor operating voltage (12-20V) -// float SettingsHandler::BLDC_MotorA_Current = 1.0; // BLDC Maximum operating current (Amps) -// int SettingsHandler::BLDC_ChipSelect_PIN = 5; // SPI chip select pin - CSn on AS5048a (By default on ESP32: MISO = D19, MOSI = D23, CLK = D18) -// bool SettingsHandler::BLDC_MotorA_ParametersKnown = false; // Once you know the zero elec angle for the motor enter it below and set this flag to true. -// float SettingsHandler::BLDC_MotorA_ZeroElecAngle = 0.00; // This number is the zero angle (in radians) for the motor relative to the encoder. -// int SettingsHandler::BLDC_RailLength; -// int SettingsHandler::BLDC_StrokeLength; - -// int SettingsHandler::TwistServo_ZERO = 1500; -// int SettingsHandler::ValveServo_ZERO = 1500; -// int SettingsHandler::SqueezeServo_ZERO = 1500; -// bool SettingsHandler::autoValve = false; -// bool SettingsHandler::inverseValve = false; -// bool SettingsHandler::valveServo90Degrees = false; -// bool SettingsHandler::inverseStroke = false; -// bool SettingsHandler::inversePitch = false; -// int SettingsHandler::lubeAmount = 255; - -// bool SettingsHandler::displayEnabled = false; -// bool SettingsHandler::sleeveTempDisplayed = false; -// bool SettingsHandler::internalTempDisplayed = false; -// bool SettingsHandler::versionDisplayed = true; -// bool SettingsHandler::tempSleeveEnabled = false; -// bool SettingsHandler::tempInternalEnabled = false; -// bool SettingsHandler::fanControlEnabled = false; -// bool SettingsHandler::batteryLevelEnabled = true; -// //int SettingsHandler::Battery_Voltage_PIN = 32; -// bool SettingsHandler::batteryLevelNumeric = false; -// double SettingsHandler::batteryVoltageMax = 12.6; -// int SettingsHandler::batteryCapacityMax; -// int SettingsHandler::Display_Screen_Width = 128; -// int SettingsHandler::Display_Screen_Height = 64; -// int SettingsHandler::caseFanMaxDuty = 255; -// double SettingsHandler::internalTempForFan = 20.0; -// double SettingsHandler::internalMaxTemp = 50.0; -// int SettingsHandler::TargetTemp = 40; -// int SettingsHandler::HeatPWM = 255; -// int SettingsHandler::HoldPWM = 110; -// int SettingsHandler::Display_I2C_Address = 0x3C; -// int SettingsHandler::Display_Rst_PIN = -1; -// // long SettingsHandler::heaterFailsafeTime = 60000; -// float SettingsHandler::heaterThreshold = 5.0; -// int SettingsHandler::heaterResolution = 8; -// int SettingsHandler::heaterFrequency = 5000; -// int SettingsHandler::caseFanFrequency = 25; -// int SettingsHandler::caseFanResolution = 10; -// const char *SettingsHandler::lastRebootReason; - -// bool SettingsHandler::voiceEnabled = false; -// bool SettingsHandler::voiceMuted = false; -// int SettingsHandler::voiceWakeTime = 10; -// int SettingsHandler::voiceVolume = 10; - -// bool SettingsHandler::bootButtonEnabled; -// bool SettingsHandler::buttonSetsEnabled; -// char SettingsHandler::bootButtonCommand[MAX_COMMAND]; - -bool SettingsHandler::motionEnabled = false; -// char SettingsHandler::motionSelectedProfileName[MAX_MOTION_PROFILE_NAME_LENGTH]; -int SettingsHandler::motionSelectedProfileIndex = 0; -int SettingsHandler::motionDefaultProfileIndex = 0; -// uint8_t SettingsHandler::defaultButtonSetPin; -// uint16_t SettingsHandler::buttonAnalogDebounce; -// std::map SettingsHandler::motionProfiles; -// int SettingsHandler::motionUpdateGlobal; -// int SettingsHandler::motionPeriodGlobal; -// int SettingsHandler::motionAmplitudeGlobal; -// int SettingsHandler::motionOffsetGlobal; -// float SettingsHandler::motionPhaseGlobal; -// bool SettingsHandler::motionReversedGlobal = false; -// bool SettingsHandler::motionPeriodGlobalRandom = false; -// bool SettingsHandler::motionAmplitudeGlobalRandom = false; -// bool SettingsHandler::motionOffsetGlobalRandom = false; -// int SettingsHandler::motionPeriodGlobalRandomMin; -// int SettingsHandler::motionPeriodGlobalRandomMax; -// int SettingsHandler::motionAmplitudeGlobalRandomMin; -// int SettingsHandler::motionAmplitudeGlobalRandomMax; -// int SettingsHandler::motionOffsetGlobalRandomMin; -// int SettingsHandler::motionOffsetGlobalRandomMax; -// int SettingsHandler::motionRandomChangeMin; -// int SettingsHandler:: motionRandomChangeMax; diff --git a/ESP32/src/SystemCommandHandler.h b/ESP32/src/SystemCommandHandler.h deleted file mode 100644 index 6b83189..0000000 --- a/ESP32/src/SystemCommandHandler.h +++ /dev/null @@ -1,964 +0,0 @@ -/* MIT License - -Copyright (c) 2026 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - - -#pragma once - -#include -#if ESP8266 == 1 -#include -#else -#include -#endif -#include "settings/SettingsHandler.h" -#include "utils.h" -#include "logging/LogHandler.h" -#include "logging/TagHandler.h" -#include "struct/command.hpp" -#include "settingsFactory.h" -#include "TCodeInterface.h" - -class SystemCommandHandler { -public: - SystemCommandHandler() { - tCodeQueue = xQueueCreate(10, sizeof(char[MAX_COMMAND])); - if(tCodeQueue == NULL) { - LogHandler::error(Tags::SystemCommand, "Error creating the tcode queue"); - } - m_settingsFactory = SettingsFactory::getInstance(); - } - bool process(const char* in) { - xSemaphoreTake(xMutex, portMAX_DELAY); - if(isSaveCommand(in)) { - LogHandler::debug(Tags::SystemCommand, "Enter process save command: %s", in); - for(Command command : saveCommands) { - if(match(in, command.command)) { - command.callback(); - xSemaphoreGive(xMutex); - return true; - } - } - - LogHandler::error(Tags::SystemCommand, "Unknown save command: %s\n", in); - xSemaphoreGive(xMutex); - return false; - - } else if(isOtherCommand(in)) { - LogHandler::debug(Tags::SystemCommand, "Enter process other command: %s", in); - - for(auto command : commands) { - if(match(in, command.command)) { - command.callback(); - xSemaphoreGive(xMutex); - return true; - } - } - - for(auto command : commandExternal) { - if(startsWith(in, command.command)) { - command.callback(in); - xSemaphoreGive(xMutex); - return true; - } - } - CommandValuePair valuePair; - if(!getCommandValue(in, valuePair)) - return false; - - LogHandler::debug(Tags::SystemCommand, "Value command: %s:%s", valuePair.command, valuePair.value); - for(auto command : commandCharValues) { - if(match(valuePair.command, command.command)) { - command.callback(valuePair.value); - xSemaphoreGive(xMutex); - return true; - } - } - - for(auto command : commandNumberValues) { - if(match(valuePair.command, command.command)) { - bool error = false; - int valueInt = getInt(valuePair.value, error); - if(error) - return false; - command.callback(valueInt); - xSemaphoreGive(xMutex); - return true; - } - } - - LogHandler::error(Tags::SystemCommand, "Unknown command: %s", in); - xSemaphoreGive(xMutex); - return false; - } - xSemaphoreGive(xMutex); - return false; - } - - // bool process(ButtonModel* button, char buf[MAX_COMMAND]) { - // buf[0] = {0}; - // for(auto command : commandExternal) { - // if(match(button->command, command.command)) { - // strlcpy(buf, button->command, MAX_COMMAND); - // button->isPressed() ? strcat(buf, ":1\n") : strcat(buf, ":0\n"); - // return true; - // } - // } - // if(!button->isPressed()) {// Filter out other commands button release event for now. - // strlcpy(buf, button->command, MAX_COMMAND); - // } - // return false; - // } - - /// @brief This function is mainly for concatenating the button state for commands sent externally. - /// @param button - /// @param buf - /// @return true if any commands where added. - bool process(ButtonModel* button, char buf[MAX_COMMAND]) { - LogHandler::debug(Tags::SystemCommand, "Enter process button command: %s", button->command); - char temp[MAX_COMMAND]; - strlcpy(temp, button->command, MAX_COMMAND); - char *token = strtok(temp, " ");//Split incoming at TCode delemiter "space" - buf[0] = {0}; - while( token != NULL ) {// Specify if the button is pressed or released only for externaly sent commands. - LogHandler::debug(Tags::SystemCommand, "Searching command: %s", token); - bool externalFound = false; - for(auto command : commandExternal) { - if(match(command.command, token)) { - strcat(buf, token); - button->isPressed() ? strcat(buf, ":1") : strcat(buf, ":0"); - externalFound = true; - break; - } - } - if(!externalFound && !button->isPressed()) {// Add other commands only if the button has been released for now. - strcat(buf, token); - } - strcat(buf, " "); - token = strtok(NULL, " "); - } - if(strlen(buf)) { - strcat(buf, "\n"); - LogHandler::debug(Tags::SystemCommand, "Finish process button command: %s", buf); - return true; - } - return false; - } - - bool isCommand(const char* in) { - return isSaveCommand(in) || isOtherCommand(in); - //return strpbrk(DELEMITER_SAVE, in) != nullptr || strpbrk(DELEMITER, in) != nullptr; - } - - bool isSaveCommand(const char* in) { - return startsWith(in, DELEMITER_SAVE); - } - - bool isOtherCommand(const char* in) { - return startsWith(in, DELEMITER); - } - bool isValueCommand(const char* in) { - return contains(in, (&DELEMITER_VALUE)); - } - - bool isSettingCommand(const char* in) { - return startsWith(in, DELEMITER); - } - - - void registerExternalCommandCallback(std::function callback) { - m_externalCommandCallback = callback; - } - - bool getTCode(char* buf) - { - if(!tCodeQueue) { - return false; - } - if(!xQueueReceive(tCodeQueue, buf, 0)) { - buf[0] = {0}; - return 0; - } - return strnlen(buf, MAX_COMMAND); - } - -private: - SemaphoreHandle_t xMutex = xSemaphoreCreateMutex(); - QueueHandle_t tCodeQueue; - SettingsFactory* m_settingsFactory; - std::function m_externalCommandCallback = 0; - std::function m_otherCommandCallback = 0; - - const char* DELEMITER = "#"; - const char* DELEMITER_SAVE = "$"; - const char DELEMITER_VALUE = ':'; - const Command HELP{{"Help", "#help", "Print the help screen", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { - return execute([this]() -> bool { - printCommandHelp(); - return true; - }); - }}; - const Command AVAILABLE_SETTINGS{{"List settings", "#list-settings", "Print the available settings for the #setting command", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { - return execute([this]() -> bool { - printAvailableSettings(); - return true; - }); - }}; - const Command PRINT_MEMORY{{"Print memory", "#print-mem", "Print the system memory info to serial", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { - return execute([this]() -> bool { - SettingsHandler::printFree(true); - return true; - }); - }}; - const Command SAVE{{"Save", "$save", "Saves all settings", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { - return execute([this]() -> bool { - SettingsHandler::saveAll(); - Serial.println("Settings saved!"); - return true; - }); - }}; - const Command DEFAULT_ALL{{"Default all", "$defaultAll", "Saves all settings to default", SaveRequired::NO, RestartRequired::YES, SettingType::NONE}, [this]() -> bool { - return execute([this]() -> bool { - if(!m_settingsFactory->resetAll()) { - LogHandler::error(Tags::SystemCommand, "Error resetting all to default"); - return false; - } - LogHandler::info(Tags::SystemCommand, "All settings reset to default!"); - return true; - }, SaveRequired::NO, RestartRequired::YES); - }}; - const Command RESTART{{"Restart", "#restart", "Restart the system", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { - return execute([]() -> bool { - SettingsHandler::restart(); - return true; - }); - }}; - const Command PRINT_IP{ {"Print IP", "#ip", "Print current STA or AP IP address", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { - return execute([]() -> bool { - IPAddress ip = (WiFi.isConnected()) ? WiFi.localIP() : WiFi.softAPIP(); - Serial.printf("IP Address: %s\n", ip.toString().c_str()); - return true; - }); - } }; - const Command CLEAR_LOGS_INCLUDE{{"Clear log include", "#clear-log-include", "Clears all the log included tags", SaveRequired::YES, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { - return execute([this]() -> bool { - LogHandler::clearIncludes(); - LogHandler::debug(Tags::SystemCommand, "Tags cleared"); - return m_settingsFactory->setValue(LOG_INCLUDETAGS, "") != SettingFile::NONE; - }, SaveRequired::YES); - }}; - const Command CLEAR_LOGS_EXCLUDE{{"Clear log exclude", "#clear-log-exclude", "Clears all the log excluded tags", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { - return execute([this]() -> bool { - LogHandler::clearExcludes(); - LogHandler::debug(Tags::SystemCommand, "Filters cleared"); - return m_settingsFactory->setValue(LOG_EXCLUDETAGS, "") != SettingFile::NONE; - }, SaveRequired::NO); - }}; - const Command CHANNEL_RANGES_ENABLE{{"Channel ranges enable", "#channel-ranges-enable", "Enables the channel range limits temporarily", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { - return validateBool("Channel ranges", true, SettingsHandler::getChannelRangesEnabled(), [this](bool value) -> bool { - SettingsHandler::setChannelRangesEnabled(true); - LogHandler::debug(Tags::SystemCommand, "Channel ranges enabled"); - return true; - }); - }}; - const Command CHANNEL_RANGES_DISABLE{{"Channel ranges disable", "#channel-ranges-disable", "Disables the channel range limits temporarily", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { - return validateBool("Channel ranges", false, SettingsHandler::getChannelRangesEnabled(), [this](bool value) -> bool { - SettingsHandler::setChannelRangesEnabled(false); - LogHandler::debug(Tags::SystemCommand, "Channel ranges disabled"); - return true; - }); - }}; - const Command CHANNEL_RANGES_TOGGLE{{"Channel ranges toggle", "#channel-ranges-toggle", "Toggles the channel range limits temporarily", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { - return execute([this]() -> bool { - bool enabled = SettingsHandler::getChannelRangesEnabled(); - SettingsHandler::setChannelRangesEnabled(!enabled); - LogHandler::debug(Tags::SystemCommand, !enabled ? "Channel ranges enabled" : "Channel ranges disabled"); - return true; - }); - }}; - const Command MOTION_ENABLE{{"Motion enable", "#motion-enable", "Enables the motion generator", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { - return validateBool("Motion", true, SettingsHandler::getMotionEnabled(), [this](bool value) -> bool { - SettingsHandler::setMotionEnabled(value); - LogHandler::debug(Tags::SystemCommand, "Motion enabled"); - return true; - }); - }}; - const Command MOTION_DISABLE{{"Motion disable", "#motion-disable", "Disables the motion generator", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { - return validateBool("Motion", false, SettingsHandler::getMotionEnabled(), [this](bool value) -> bool { - SettingsHandler::setMotionEnabled(value); - LogHandler::debug(Tags::SystemCommand, "Motion disabled"); - writeTCode("DSTOP\n"); - return true; - }); - }}; - const Command MOTION_TOGGLE{{"Motion toggle", "#motion-toggle", "Toggles the motion generator", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { - return execute([this]() -> bool { - bool enabled = SettingsHandler::getMotionEnabled(); - SettingsHandler::setMotionEnabled(!enabled); - LogHandler::debug(Tags::SystemCommand, !enabled ? "Motion enabled" : "Motion disabled"); - if(!enabled) { - send("DSTOP\n"); - } - return true; - }); - }}; - const Command MOTION_HOME{{"Motion home", "#device-home", "Sends all axis' to its home position", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { - char buf[MAX_COMMAND]; - SettingsHandler::channelMap.tCodeHome(buf); - LogHandler::debug(Tags::SystemCommand, "Device home: %s", buf); - writeTCode(buf); - return true; - }}; - const Command MOTION_PROFILE_CYCLE{{"Motion profile cycle", "#motion-profile-cycle", "Cycles the motion generator profiles stopping after last profile", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this]() -> bool { - return execute([this]() -> bool { - SettingsHandler::cycleMotionProfile(); - return true; - }); - }}; - const Command PAUSE{{"Pause", "#pause", "Pauses all motion of the device", SaveRequired::YES, RestartRequired::YES, SettingType::NONE}, [this]() -> bool { - return execute([this]() -> bool { - SettingsHandler::motionPaused = true; - LogHandler::debug(Tags::SystemCommand, "Device paused"); - return true; - }); - return true; - }}; - const Command RESUME{{"Resume", "#resume", "Resumes all motion of the device", SaveRequired::YES, RestartRequired::YES, SettingType::NONE}, [this]() -> bool { - return execute([this]() -> bool { - SettingsHandler::motionPaused = false; - LogHandler::debug(Tags::SystemCommand, "Device resumed"); - return true; - }); - return true; - }}; - const Command PAUSE_TOGGLE{{"Pause toggle", "#pause-toggle", "Pauses all motion of the device", SaveRequired::YES, RestartRequired::YES, SettingType::NONE}, [this]() -> bool { - return execute([this]() -> bool { - SettingsHandler::motionPaused = !SettingsHandler::motionPaused; - LogHandler::debug(Tags::SystemCommand, SettingsHandler::motionPaused ? "Device paused" : "Device resumed"); - return true; - }); - return true; - }}; - const CommandValue MOTION_HOME_SPEED{{"Motion home", "#device-home", "Sends all axis' to its home position at specified speed (S)", SaveRequired::NO, RestartRequired::NO, SettingType::Number}, [this](const int value) -> bool { - char buf[MAX_COMMAND]; - SettingsHandler::channelMap.tCodeHome(buf, value); - LogHandler::debug(Tags::SystemCommand, "Device home speed: %s", buf); - writeTCode(buf); - return true; - }}; - const CommandValueWIFI_SSID{{"Wifi ssid", "#wifi-ssid", "Sets the ssid of the wifi AP", SaveRequired::YES, RestartRequired::YES, SettingType::String}, [this](const char* value) -> bool { - return validateMaxLength("Wifi SSID", value, SSID_LEN, false, [this](const char* value) -> bool { - m_settingsFactory->setValue(SSID_SETTING, value); - //strcpy(SettingsHandler::ssid, value); - return true; - }, SaveRequired::YES, RestartRequired::YES); - }}; - const CommandValueWIFI_PASS{{"Wifi pass", "#wifi-pass", "Sets the password of the wifi AP", SaveRequired::YES, RestartRequired::YES, SettingType::String}, [this](const char* value) -> bool { - return validateMaxLength("Wifi password", value, WIFI_PASS_LEN, true, [this](const char* value) -> bool { - m_settingsFactory->setValue(WIFI_PASS_SETTING, value); - //strcpy(SettingsHandler::wifiPass, value); - return true; - }, SaveRequired::YES, RestartRequired::YES); - }}; - const CommandValueBOARD_TYPE{{"Board type", "#board-type", BOARD_TYPES_HELP, SaveRequired::YES, RestartRequired::YES, SettingType::Number}, [this](const int value) -> bool { - return executeValue(value, [this](const int value) -> bool { - return m_settingsFactory->changeBoardType(value); - }, SaveRequired::YES); - }}; - const CommandValueDEVICE_TYPE_COMMAND{{"Device type", "#device-type", DEVICE_TYPES_HELP, SaveRequired::YES, RestartRequired::YES, SettingType::Number}, [this](const int value) -> bool { - return executeValue(value, [this](const int value) -> bool { - return m_settingsFactory->changeDeviceType(value); - }, SaveRequired::YES); - }}; - const CommandValueLOG_LEVEL{{"Log level", "#log-level", LOG_LEVEL_HELP, SaveRequired::YES, RestartRequired::NO, SettingType::Number}, [this](const int value) -> bool { - return executeValue(value, [this](const int value) -> bool { - if(value > (int)LogLevel::VERBOSE || value < 0) { - LogHandler::error(Tags::SystemCommand, "Invalid value: %ld. Valid log levels are %s", value, LOG_LEVEL_HELP); - return false; - } - return m_settingsFactory->setValue(LOG_LEVEL_SETTING, value) != SettingFile::NONE; - }, SaveRequired::YES); - }}; - const CommandValueADD_LOG_INCLUDE{{"Add log include", "#add-log-include", "Adds a tag to the log includes", SaveRequired::YES, RestartRequired::NO, SettingType::String}, [this](const char* value) -> bool { - return executeValue(value, [this](const char* value) -> bool { - uint32_t tag_masks = 0; - if (Tags::from_str(value, tag_masks)) - { - LogHandler::error(Tags::SystemCommand, "Invalid value(s): %s", value); - return false; - } - LogHandler::addIncludes(tag_masks); - return m_settingsFactory->setValue(LOG_INCLUDETAGS, Tags::as_str(LogHandler::getIncludes()).c_str()) != SettingFile::NONE; - }, SaveRequired::YES); - }}; - const CommandValueREMOVE_LOG_INCLUDE{{"Remove log include", "#remove-log-include", "Removes a tag from the log includes", SaveRequired::YES, RestartRequired::NO, SettingType::String}, [this](const char* value) -> bool { - return executeValue(value, [this](const char* value) -> bool { - uint32_t tag_masks = 0; - if (Tags::from_str(value, tag_masks)) - { - LogHandler::error(Tags::SystemCommand, "Invalid value(s): %s", value); - return false; - } - LogHandler::removeIncludes(tag_masks); - return m_settingsFactory->setValue(LOG_INCLUDETAGS, Tags::as_str(LogHandler::getIncludes()).c_str()) != SettingFile::NONE; - }, SaveRequired::YES); - }}; - const CommandValueADD_LOG_EXCLUDE{{"Add log exclude", "#add-log-exclude", "Adds a tag to the log excludes", SaveRequired::YES, RestartRequired::NO, SettingType::String}, [this](const char* value) -> bool { - return executeValue(value, [this](const char* value) -> bool { - uint32_t tag_masks = 0; - if (Tags::from_str(value, tag_masks)) - { - LogHandler::error(Tags::SystemCommand, "Invalid value(s): %s", value); - return false; - } - LogHandler::addExcludes(tag_masks); - return m_settingsFactory->setValue(LOG_EXCLUDETAGS, Tags::as_str(LogHandler::getExcludes()).c_str()) != SettingFile::NONE; - }, SaveRequired::YES); - }}; - const CommandValueREMOVE_LOG_EXCLUDE{{"Remove log exclude", "#remove-log-exclude", "Removes a tag from the log excludes", SaveRequired::YES, RestartRequired::NO, SettingType::String}, [this](const char* value) -> bool { - return executeValue(value, [this](const char* value) -> bool { - uint32_t tag_masks = 0; - if (Tags::from_str(value, tag_masks)) - { - LogHandler::error(Tags::SystemCommand, "Invalid value(s): %s", value); - return false; - } - LogHandler::removeExclude(tag_masks); - return m_settingsFactory->setValue(LOG_EXCLUDETAGS, Tags::as_str(LogHandler::getExcludes()).c_str()) != SettingFile::NONE; - }, SaveRequired::YES); - }}; - const CommandValueMOTION_PROFILE_NAME{{"Motion profile set by name", "#motion-profile-name", "Sets the current running profile by name", SaveRequired::NO, RestartRequired::NO, SettingType::String}, [this](const char* value) -> bool { - return validateMaxLength("Motion profile name", value, MAX_MOTION_PROFILE_NAME_LENGTH, false, [](const char* value) -> bool { - SettingsHandler::setMotionProfileName(value); - return true; - }); - }}; - const CommandValueMOTION_PROFILE_SET{{"Motion profile set by number", "#motion-profile-set", "Sets the current running profile by number", SaveRequired::NO, RestartRequired::NO, SettingType::Number}, [this](const int value) -> bool { - return validateGreaterThanZero("Motion profile", value, [this](int value) -> bool { - int profileAsIndex = value - 1; - if(profileAsIndex > MAX_MOTION_PROFILE_COUNT) { - LogHandler::error(Tags::SystemCommand, "Motion profile %ld does not exist", profileAsIndex); - return false; - } - SettingsHandler::setMotionProfile(profileAsIndex); - return true; - }); - }}; - const CommandValue EDGE{{"Edge", "#edge", "Outputs the edge pressed command to external application", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this](const char* in) -> bool { - if(m_externalCommandCallback) { - m_externalCommandCallback(in); - } - return true; - }}; - const CommandValue LEFT{{"Left", "#left", "Outputs the left pressed command to external application", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this](const char* in) -> bool { - if(m_externalCommandCallback) { - m_externalCommandCallback(in); - } - return true; - }}; - const CommandValue RIGHT{{"Right", "#right", "Outputs the right pressed command to external application", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this](const char* in) -> bool { - if(m_externalCommandCallback) { - m_externalCommandCallback(in); - } - return true; - }}; - const CommandValue OK{{"Ok", "#ok", "Outputs the ok pressed command to external application", SaveRequired::NO, RestartRequired::NO, SettingType::NONE}, [this](const char* in) -> bool { - if(m_externalCommandCallback) { - m_externalCommandCallback(in); - } - return true; - }}; - const CommandValue SETTING{{"Setting", "#setting", "Modify a setting ex. #setting::", SaveRequired::YES, RestartRequired::NO, SettingType::String}, [this](const char* value) -> bool { - CommandValuePair valuePair; - if(!getCommandValue(value, valuePair)) - return false; - const Setting* setting = m_settingsFactory->getSetting(valuePair.command); - if(!setting) { - return false; - } - return executeValue(value, [this, setting, valuePair](const char* value) -> bool { - - LogHandler::debug(Tags::SystemCommand, "Searching for setting command '%s' value: '%s'", valuePair.command, valuePair.value); - bool error = false; - switch(setting->type) { - case SettingType::String: { - if(m_settingsFactory->setValue(setting->name, valuePair.value) != SettingFile::NONE) { - return true; - } - } - break; - case SettingType::Number: { - int value = getInt(valuePair.value, error); - if(error) - return false; - if(m_settingsFactory->setValue(setting->name, value) != SettingFile::NONE) { - return true; - } - LogHandler::debug(Tags::SystemCommand, "value: %d", value); - } - break; - case SettingType::Float: { - float value = getFloat(valuePair.value, error); - if(error) - return false; - if(m_settingsFactory->setValue(setting->name, value) != SettingFile::NONE) { - return true; - } - LogHandler::debug(Tags::SystemCommand, "value: %f", value); - } - break; - case SettingType::Double: { - double value = getDouble(valuePair.value, error); - if(error) - return false; - if(m_settingsFactory->setValue(setting->name, value) != SettingFile::NONE) { - return true; - } - LogHandler::debug(Tags::SystemCommand, "value: %f", value); - } - break; - case SettingType::Boolean: { - bool value = getBoolean(valuePair.value, error); - if(error) - return false; - if(m_settingsFactory->setValue(setting->name, value) != SettingFile::NONE) { - return true; - } - LogHandler::debug(Tags::SystemCommand, "value: %d", value); - } - break; - case SettingType::ArrayString: { - } - break; - case SettingType::ArrayInt: { - } - break; - default: - LogHandler::error(Tags::SystemCommand, "Invalid setting type: %ld", (int)setting->type); - } - return false; - }, SaveRequired::YES, setting->isRestartRequired); - }}; - - - Command saveCommands[2] { - SAVE, - DEFAULT_ALL, - }; - - Command commands[18] = { - HELP, - AVAILABLE_SETTINGS, - PRINT_MEMORY, - RESTART, - PRINT_IP, - CLEAR_LOGS_INCLUDE, - CLEAR_LOGS_EXCLUDE, - CHANNEL_RANGES_ENABLE, - CHANNEL_RANGES_DISABLE, - CHANNEL_RANGES_TOGGLE, - MOTION_ENABLE, - MOTION_DISABLE, - MOTION_TOGGLE, - MOTION_PROFILE_CYCLE, - PAUSE, - RESUME, - PAUSE_TOGGLE, - MOTION_HOME - }; - - CommandValue commandNumberValues[5] = { - LOG_LEVEL, - BOARD_TYPE, - DEVICE_TYPE_COMMAND, - MOTION_PROFILE_SET, - MOTION_HOME_SPEED - }; - - CommandValue commandCharValues[8] = { - WIFI_SSID, - WIFI_PASS, - ADD_LOG_INCLUDE, - REMOVE_LOG_INCLUDE, - ADD_LOG_EXCLUDE, - REMOVE_LOG_EXCLUDE, - MOTION_PROFILE_NAME, - SETTING - }; - CommandValue commandExternal[4] = { - EDGE, - LEFT, - RIGHT, - OK - }; - // std::vector> commandStringSetting; - // std::vector> commandIntSetting; - // std::vector> commandFloatSetting; - // std::vector> commandDoubleSetting; - // std::vector> commandBooleanSetting; - - // void setupSettingsCommands() { - - // auto allSettings = m_settingsFactory->AllSettings; - - // for(SettingFileInfo* settingsInfo : allSettings) - // { - // for(const Setting& setting : settingsInfo->settings) - // { - // switch(setting.type) { - // case SettingType::String: { - // // const CommandValue command{{setting.friendlyName, commandValue, setting.description, SaveRequired::YES, setting.isRestartRequired, setting.type}, [this, setting](const char* in) -> bool { - // // return m_settingsFactory->setValue(setting.name, in) != SettingFile::NONE; - // // }}; - // auto command = setupSettingsCommand(setting); - // commandStringSetting.push_back(command); - // } - // break; - // case SettingType::Number: { - // auto command = setupSettingsCommand(setting); - // commandIntSetting.push_back(command); - // } - // break; - // case SettingType::Float: { - // auto command = setupSettingsCommand(setting); - // commandFloatSetting.push_back(command); - // } - // break; - // case SettingType::Double: { - // auto command = setupSettingsCommand(setting); - // commandDoubleSetting.push_back(command); - // } - // break; - // case SettingType::Boolean: { - // auto command = setupSettingsCommand(setting); - // commandBooleanSetting.push_back(command); - // } - // break; - // case SettingType::ArrayString: { - // } - // break; - // case SettingType::ArrayInt: { - // } - // break; - // default: - // LogHandler::error(Tags::SystemCommand, "Invalid setting type: %ld", (int)setting.type); - // } - // } - // } - // } - template - const CommandValue setupSettingsCommand(const Setting &setting) { - const CommandValue command{setting, [this, setting](T in) -> bool { - return executeValue(in, [this, setting](T value) -> bool { - if(m_settingsFactory->setValue(setting.name, value) != SettingFile::NONE) { - return true; - } - return false; - }, SaveRequired::YES); - }}; - return command; - } - - void send(const char* tcode) override - { - if(tCodeQueue) - xQueueSend(tCodeQueue, tcode, 0); - } - - struct CommandValuePair { - const char* command; - const char* value; - }; - - bool getCommandValue(const char* in, CommandValuePair &valuePair) { - // Commands with values - int indexofDelim = getposition(in, strlen(in), DELEMITER_VALUE); - if(indexofDelim == -1) { - LogHandler::error(Tags::SystemCommand, "Invalid command format: '%s' missing colon, correct format is #:", in); - xSemaphoreGive(xMutex); - return false; - } - const char* commandAlone = substr(in, 0, indexofDelim); - if(!strlen(commandAlone)) { - LogHandler::error(Tags::SystemCommand, "Invalid command format: '%s' missing command, correct format is #:", in); - xSemaphoreGive(xMutex); - return false; - } - valuePair.command = commandAlone; - const char* valueAlone = substr(in, indexofDelim +1, strlen(in)); - if(!strlen(valueAlone)) { - LogHandler::error(Tags::SystemCommand, "Invalid command format: '%s' missing value, correct format is #:", in); - xSemaphoreGive(xMutex); - return false; - } - valuePair.value = valueAlone; - return true; - } - - int getInt(const char* value, bool &error) { - if(!isStringIntegral(value)) { - LogHandler::debug(Tags::SystemCommand, "getInt '%s' not integral", value); - error = true; - return false; - } - return (int)(String(value).toInt()); - } - float getFloat(const char* value, bool &error) { - if(!isStringIntegral(value)) { - LogHandler::debug(Tags::SystemCommand, "getFloat '%s' not integral", value); - error = true; - return false; - } - return String(value).toFloat(); - } - double getDouble(const char* value, bool &error) { - if(!isStringIntegral(value)) { - LogHandler::debug(Tags::SystemCommand, "getDouble '%s' not integral", value); - error = true; - return false; - } - return String(value).toDouble(); - } - bool getBoolean(const char* value, bool &error) { - if(!strcmp(value, "true")) { - return true; - } - if(!strcmp(value, "false")) { - return false; - } - uint8_t valueInt = getInt(value, error); - if(error) { - LogHandler::debug(Tags::SystemCommand, "getBoolean '%s' not integral", value); - return false; - } - if(valueInt == 0 || valueInt == 1) - return (bool)valueInt; - error = true; - return false; - } - - bool isStringIntegral(const char* value) { - int len = strlen(value); - if(!len) - return false; - char firstChar = value[0]; - bool firstCharIsNegative = firstChar == '-'; - bool firstCharIsDecimal = firstChar == '.'; - if((firstCharIsNegative || firstCharIsDecimal) && len > 1) { - firstChar = value[1]; - } - return firstChar == '0' || firstChar == '1' || firstChar == '2' || firstChar == '3' || firstChar == '4' || firstChar == '5' || firstChar == '6' || firstChar == '7' || firstChar == '8' || firstChar == '9'; - } - - - bool execute(std::function function, SaveRequired isSaveRequired = SaveRequired::NO, RestartRequired isRestartRequired = RestartRequired::NO) { - bool subValidate = function(); - if(subValidate) { - completeCommand(isRestartRequired, isSaveRequired); - } - xSemaphoreGive(xMutex); - return subValidate; - } - - template - bool executeValue(T value, std::function function, SaveRequired isSaveRequired = SaveRequired::NO, RestartRequired isRestartRequired = RestartRequired::NO) { - bool subValidate = function(value); - if(subValidate) { - completeCommand(isRestartRequired, isSaveRequired); - } - xSemaphoreGive(xMutex); - return subValidate; - } - - bool validateBool(const char* name, bool value, bool currentValue, std::function function, SaveRequired isSaveRequired = SaveRequired::NO, RestartRequired isRestartRequired = RestartRequired::NO) { - if(value && currentValue) { - Serial.println("Already on!"); - xSemaphoreGive(xMutex); - return false; - } - if(!value && !currentValue) { - Serial.println("Already off!"); - xSemaphoreGive(xMutex); - return false; - } - bool subValidate = function(value); - if(subValidate) { - printNewState(name, value); - completeCommand(isRestartRequired, isSaveRequired); - } - xSemaphoreGive(xMutex); - return subValidate; - } - - bool validateGreaterThanZero(const char* name, int value, std::function function, SaveRequired isSaveRequired = SaveRequired::NO, RestartRequired isRestartRequired = RestartRequired::NO) { - if(value < 1) { - Serial.printf("Invalid value: %d.", value); - xSemaphoreGive(xMutex); - return false; - } - bool subValidate = function(value); - if(subValidate) { - printNewState(name, value); - completeCommand(isRestartRequired, isSaveRequired); - } - xSemaphoreGive(xMutex); - return subValidate; - } - bool validateGreaterThanNegativeOne(const char* name, int value, std::function function, SaveRequired isSaveRequired = SaveRequired::NO, RestartRequired isRestartRequired = RestartRequired::NO) { - if(value < 0) { - Serial.printf("Invalid value: %d.", value); - xSemaphoreGive(xMutex); - return false; - } - bool subValidate = function(value); - if(subValidate) { - printNewState(name, value); - completeCommand(isRestartRequired, isSaveRequired); - } - xSemaphoreGive(xMutex); - return subValidate; - } - - bool validateMaxLength(const char* name, const char* value, int maxLen, bool valueSensitive, std::function function, SaveRequired isSaveRequired = SaveRequired::NO, RestartRequired isRestartRequired = RestartRequired::NO) { - if(strlen(value) > maxLen) { - Serial.printf("Invalid command: %s max length is: %d\n", name, maxLen); - xSemaphoreGive(xMutex); - return false; - } - bool subValidate = function(value); - if(subValidate) { - if(!valueSensitive) - printNewState(name, value); - else - Serial.printf("%s changed to a value of %d length\n", name, strlen(value)); - completeCommand(isRestartRequired, isSaveRequired); - } - xSemaphoreGive(xMutex); - return subValidate; - } - - void printNewState(const char* name, const char* newValue) { - Serial.printf("%s changed to: %s\n", name, newValue); - } - void printNewState(const char* name, int newValue) { - Serial.printf("%s changed to: %d\n", name, newValue); - } - void printNewState(const char* name, bool newValue) { - Serial.printf("%s %s\n", name, newValue ? "enabled" : "disabled"); - } - void printNewState(const char* name, float newValue) { - Serial.printf("%s changed to: %f\n", name, newValue); - } - void completeCommand(RestartRequired isRestartRequired, SaveRequired isSaveRequired) { - if((int)isSaveRequired) - Serial.println("Execute the command '$save' to store the new value otherwise the value will reset upon reboot."); - if((int)isRestartRequired) { - Serial.println("Restart is required after save"); - } - } - void printCommandHelp() { - char buf[MAX_COMMAND] = {0}; - Serial.println(); - Serial.println(); - Serial.println(); - Serial.println(); - Serial.println("Available commands:"); - Serial.println(); - for(Command command : saveCommands) { - formatPrintCommand(command, buf, sizeof(buf)); - } - strcat(buf, "\n"); - for(Command command : commands) { - formatPrintCommand(command, buf, sizeof(buf)); - } - for(auto command : commandExternal) { - formatPrintCommand(command, buf, sizeof(buf)); - } - for(auto command : commandCharValues) { - formatPrintCommand(command, buf, sizeof(buf)); - } - for(auto command : commandNumberValues) { - formatPrintCommand(command, buf, sizeof(buf)); - } - } - - void printAvailableSettings() { - Serial.println(); - Serial.println(); - Serial.println(); - Serial.println(); - Serial.println("Available settings:"); - Serial.println(); - char buf[MAX_COMMAND] = {0}; - - auto allSettings = m_settingsFactory->AllSettings; - - for(SettingFileInfo* settingsInfo : allSettings) - { - for(const Setting& setting : settingsInfo->settings) - { - formatPrintCommand(setting, buf, sizeof(buf)); - } - } - } - - void formatPrintCommand(const Setting& setting, char* buf, const size_t& len) { - buf[0] = {0}; - formatCommand(setting.name, setting.friendlyName, setting.type, buf); - Serial.print(buf); - } - - void formatPrintCommand(const CommandBase& command, char* buf, const size_t& len) { - buf[0] = {0}; - formatCommand(command.command, command.description, command.valueType, buf); - Serial.print(buf); - } - - void formatCommand(const char* command, const char* description, const SettingType& valueType, char* buf) { - char temp[MAX_COMMAND] = {0}; - switch(valueType) - { - case SettingType::Number: - snprintf (temp, MAX_COMMAND, "%s%s", command, ":"); - break; - case SettingType::Boolean: - snprintf (temp, MAX_COMMAND, "%s%s", command, ":"); - break; - case SettingType::String: - snprintf (temp, MAX_COMMAND, "%s%s", command, ":"); - break; - case SettingType::Double: - snprintf (temp, MAX_COMMAND, "%s%s", command, ":"); - break; - case SettingType::Float: - snprintf (temp, MAX_COMMAND, "%s%s", command, ":"); - break; - default: - snprintf (temp, MAX_COMMAND, "%s%s", command, ""); - break; - } - sprintf(temp, "%-40s", temp); - std::replace(temp, temp + strlen(temp), ' ', '-'); - strcat(buf, temp); - sprintf(temp, "%s\n", description); - strcat(buf, temp); - } -}; \ No newline at end of file diff --git a/ESP32/src/TaskHandler.h b/ESP32/src/TaskHandler.h deleted file mode 100644 index 0451d12..0000000 --- a/ESP32/src/TaskHandler.h +++ /dev/null @@ -1,245 +0,0 @@ -#ifndef TASK_HANDLER_H_ -#define TASK_HANDLER_H_ - -#include - -#include -#include -#include - -#include -#include - -namespace TaskHandler -{ - - class Task; - - // Context is encoded in the task class, can be cast to appropriate structure - using Task_t = std::function; - - // Rates are in microseconds - enum Rates : int32_t - { - ONDEMAND = 0, - MAX = 100, - FAST = 1000, - SLOW = 25000, - }; - - // A task wraps a periodic task and assigns it a tick rate (software timer) - class Task - { - private: - Rates _tick_rate; - long long _last_tick; - long long _sleep_target; - - public: - Task(Rates rate) : _tick_rate(rate) - { - _last_tick = 0; - } - - Task(const Task &rhs) - { - _tick_rate = rhs._tick_rate; - _last_tick = rhs._last_tick; - } - - // Perform setup, grab peripherals, etc - virtual void initialize() - { - _last_tick = micros(); - this->setup(); - } - // Invoke update routine (handle_tick) - void handle_tick() - { - long long us = micros(); - long long delta = abs(us - _last_tick); - if (_sleep_target > 0) - { - _sleep_target -= delta; - return; - } - if (delta >= _tick_rate) - { - _last_tick = micros(); - loop(); - } - } - - void sleep(long long ms) - { - _sleep_target = ms * 1000; - } - - void wait(long long ms) - { - vTaskDelay(ms / portTICK_PERIOD_MS); - } - - virtual void setup() = 0; - virtual void loop() = 0; - }; - - // Task wraps a simple std::function - class FunctionalTask : public Task - { - private: - Task_t _handler; - - public: - FunctionalTask(Task_t handler) : Task(Rates::SLOW), _handler(handler) {} - - void setup() - { - // No setup needed - } - void loop() override - { - if (_handler) - { - _handler(this); - } - } - }; - - enum Core - { - PRO_CPU = PRO_CPU_NUM, - APP_CPU = APP_CPU_NUM, - }; - - enum Priority - { - IDLE = 0, - REALTIME = 0, - PRIORITY = 1, - LAZY = 2, - }; - - // Per-core (per-thread) executor/task queue - class TaskExecutor - { - private: - static constexpr uint32_t STACK_SIZE = configMINIMAL_STACK_SIZE * 8; - static constexpr uint32_t INITIAL_CAPACITY = 8; - std::vector> _tasks; - TaskHandle_t _thread_handle; - const std::string _name; - const uint32_t _priority; - const uint32_t _core; - - public: - TaskExecutor(std::string name, uint32_t priority = IDLE, uint32_t core = APP_CPU) : _name(name), _priority(priority), _core(core) - { - _tasks.reserve(INITIAL_CAPACITY); - } - - TaskExecutor(const char *name, uint32_t priority = IDLE, uint32_t core = APP_CPU) : _name(name), _priority(priority), _core(core) - { - _tasks.reserve(INITIAL_CAPACITY); - } - - // Add a task to the list - void registerTask(std::unique_ptr task) - { - if (task) - { - _tasks.push_back(std::move(task)); - } - } - - // Run setup code, acquire hardware locks, etc - void initialize() - { - for (auto &&task : _tasks) - { - task->initialize(); - } - } - - // Start the execution thread (TODO: Do we need this? Can we use a lambda?) - void start() - { - auto status = xTaskCreatePinnedToCore( - [](void *context) - { - TaskHandler::TaskExecutor *manager = static_cast(context); - if (manager) - { - manager->run(); - } - }, - _name.c_str(), - STACK_SIZE, - reinterpret_cast(this), - tskIDLE_PRIORITY + _priority, - &_thread_handle, - _core); - - if (status != pdPASS) - { - LogHandler::error(_name.c_str(), "Could not start task."); - } - } - - void run() - { - for (;;) - { - for (auto &&task : _tasks) - { - task->handle_tick(); - } - taskYIELD(); - } - } - }; - - class TaskManager - { - private: - TaskExecutor realtimeThread; - TaskExecutor priorityThread; - TaskExecutor lazyThread; - - public: - TaskManager() : realtimeThread("realtime", Priority::REALTIME, Core::PRO_CPU), - priorityThread("priority", Priority::PRIORITY, Core::APP_CPU), - lazyThread("lazy", Priority::LAZY, Core::APP_CPU) {} - - TaskExecutor *realtimeTasks() { return &realtimeThread; } - TaskExecutor *priorityTasks() { return &priorityThread; } - TaskExecutor *lazyTasks() { return &lazyThread; } - - void start() - { - realtimeThread.start(); - priorityThread.start(); - lazyThread.start(); - } - - void realtime(Task *t, Rates rate = Rates::FAST) - { - realtimeThread.registerTask(std::make_unique(t)); - } - - void priority(Task *t, Rates rate = Rates::FAST) - { - priorityThread.registerTask(std::make_unique(t)); - } - - void lazy(Task *t, Rates rate = Rates::FAST) - { - lazyThread.registerTask(std::make_unique(t)); - } - }; - - // Global instance - static TaskManager taskManager; -}; - -#endif // TASK_HANDLER_H_ \ No newline at end of file diff --git a/ESP32/src/TaskHandler.hpp b/ESP32/src/TaskHandler.hpp deleted file mode 100644 index eb3b1ec..0000000 --- a/ESP32/src/TaskHandler.hpp +++ /dev/null @@ -1,162 +0,0 @@ -#pragma once - -#include -#include "LogHandler.h" -#include "DisplayHandler.h" -#include "VoiceHandler.hpp" -#if SECURE_WEB -#include "HTTPSHandler.hpp" -#include "WebHandler.h" -#endif - - -class TaskHandler -{ -public: - static TaskHandler* getInstance() { - static TaskHandler instance; - return &instance; - } - void init() - { - - } -#if BUILD_DISPLAY - void startDisplayTask(DisplayHandler * displayHandler) - { - LogHandler::debug(TagHandler::Main, "Start Display task"); - auto displayStatus = xTaskCreatePinnedToCore( - DisplayHandler::startLoop, /* Function to implement the task */ - "DisplayTask", /* Name of the task */ - configMINIMAL_STACK_SIZE * 4, /* Stack size in words used to be 5000 */ - displayHandler, /* Task input parameter */ - 1, /* Priority of the task */ - &displayTask, /* Task handle. */ - TASK_CPU_NUM); /* Core where the task should run */ - if (displayStatus != pdPASS) - { - LogHandler::error(TagHandler::Main, "Could not start display task."); - } - } -#endif -#if BUILD_TEMP - void startTemperatureTask(TemperatureHandler* temperatureHandler) - { - LogHandler::debug(TagHandler::Main, "Start temperature task"); - auto tempStartStatus = xTaskCreatePinnedToCore( - TemperatureHandler::startLoop, /* Function to implement the task */ - "TempTask", /* Name of the task */ - static_cast(configMINIMAL_STACK_SIZE * 3), /* Stack size in words used to be 5000 */ - temperatureHandler, /* Task input parameter */ - 1, /* Priority of the task */ - &temperatureTask, /* Task handle. */ - TASK_CPU_NUM); /* Core where the task should run */ - if (tempStartStatus != pdPASS) - { - LogHandler::error(TagHandler::Main, "Could not start temperature task."); - } - } -#endif - - void startBatteryTask(BatteryHandler* batteryHandler) - { - LogHandler::debug(TagHandler::Main, "Start Battery task"); - auto batteryStatus = xTaskCreatePinnedToCore( - BatteryHandler::startLoop, /* Function to implement the task */ - "BatteryTask", /* Name of the task */ - configMINIMAL_STACK_SIZE, /* Stack size in words used to be 4028 */ - batteryHandler, /* Task input parameter */ - 1, /* Priority of the task */ - &batteryTask, /* Task handle. */ - TASK_CPU_NUM); /* Core where the task should run */ - if (batteryStatus != pdPASS) - { - LogHandler::error(TagHandler::Main, "Could not start battery task."); - } - } - // void startBatteryTask(BatteryHandler* batteryHandler) - // { - // LogHandler::debug(TagHandler::Main, "Start Battery task"); - // auto batteryStatus = xTaskCreatePinnedToCore( - // BatteryHandler::startLoop, /* Function to implement the task */ - // "BatteryTask", /* Name of the task */ - // configMINIMAL_STACK_SIZE, /* Stack size in words used to be 4028 */ - // batteryHandler, /* Task input parameter */ - // 1, /* Priority of the task */ - // &batteryTask, /* Task handle. */ - // APP_CPU_NUM); /* Core where the task should run */ - // if (batteryStatus != pdPASS) - // { - // LogHandler::error(TagHandler::Main, "Could not start battery task."); - // } - // } - - void startVoiceTask(VoiceHandler* voiceHandler) - { - LogHandler::debug(TagHandler::Main, "Start Voice task"); - auto voiceStatus = xTaskCreatePinnedToCore( - VoiceHandler::startLoop, /* Function to implement the task */ - "VoiceTask", /* Name of the task */ - configMINIMAL_STACK_SIZE, /* Stack size in words used to be 4028 */ - voiceHandler, /* Task input parameter */ - 1, /* Priority of the task */ - &voiceTask, /* Task handle. */ - TASK_CPU_NUM); /* Core where the task should run */ - if (voiceStatus != pdPASS) - { - LogHandler::error(TagHandler::Main, "Could not start voice task."); - } - } - -#if SECURE_WEB - void startHTTPSTask(WebHandler* webHandler) - { - LogHandler::debug(TagHandler::Main, "Start https task"); - auto httpsStatus = xTaskCreateUniversal( - HTTPSHandler::startLoop, /* Function to implement the task */ - "HTTPSTask", /* Name of the task */ - 8192 * 3, /* Stack size in words */ - webHandler, /* Task input parameter */ - 3, /* Priority of the task */ - &httpsTask, /* Task handle. */ - -1); /* Core where the task should run */ - if (httpsStatus != pdPASS) - { - LogHandler::error(TagHandler::Main, "Could not start https task."); - } - } -#endif - - void startDisplayAnimationTask(DisplayHandler* displayHandler) - { - // LogHandler::debug(TagHandler::Main, "Start Display Animation task"); - // auto status = xTaskCreatePinnedToCore( - // DisplayHandler::startAnimationDontPanic,/* Function to implement the task */ - // "DisplayTask", /* Name of the task */ - // 10000, /* Stack size in words */ - // displayHandler, /* Task input parameter */ - // 25, /* Priority of the task */ - // &animationTask, /* Task handle. */ - // APP_CPU_NUM); /* Core where the task should run */ - // if (status != pdPASS) - // { - // LogHandler::error(TagHandler::Main, "Could not start Display Animation task."); - // } - } -private: - TaskHandle_t batteryTask; - TaskHandle_t httpsTask; - - TaskHandle_t voiceTask; - - -#if BUILD_DISPLAY - TaskHandle_t displayTask; - // #if ISAAC_NEWTONGUE_BUILD - // TaskHandle_t animationTask; - // #endif -#endif -#if BUILD_TEMP - TaskHandle_t temperatureTask; -#endif -}; \ No newline at end of file diff --git a/ESP32/src/Tcphandler.h b/ESP32/src/Tcphandler.h deleted file mode 100644 index e0dda69..0000000 --- a/ESP32/src/Tcphandler.h +++ /dev/null @@ -1,124 +0,0 @@ -/* MIT License - -Copyright (c) 2026 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -// #pragma once - - -// #include -// #include -// #include -// #include -// #include "SettingsHandler.h" -// #include "TagHandler.h" - - -// class TcpHandler -// { -// public: -// void setup(int localPort) -// { -// m_server.begin(localPort); -// Serial.println("UDP Listening"); -// initialized = true; -// } - -// void CommandCallback(const char* in) { //This overwrites the callback for message return -// if(initialized && _lastConnectedPort > 0) { -// LogHandler::debug(_TAG, "Sending udp to client: %s", in); -// m_server.beginPacket(_lastConnectedIP, _lastConnectedPort); -// int i = 0; -// while (in[i] != 0) -// m_server.write((uint8_t)in[i++]); -// m_server.endPacket(); -// } -// } - -// void read(char* buf) -// { -// WiFiClient client = m_server.available(); -// if (!initialized || !client || !client.connected()) -// { -// buf[0] = {0}; -// return; -// } -// if (!initialized) -// { -// buf[0] = {0}; -// return; -// } -// // if(xQueueReceive(m_TCodeQueue, buf, 0)) { -// // //LogHandler::verbose(_TAG, "Recieve tcode: %s", buf); -// // } else { -// // //LogHandler::error(_TAG, "Failed to read from queue"); -// // buf[0] = {0}; -// // return; -// // } -// // // if there's data available, read a packet -// int packetSize = m_server.parsePacket(); -// if (!packetSize) -// { -// buf[0] = {0}; -// return; -// } -// _lastConnectedPort = m_server.remotePort(); -// _lastConnectedIP = m_server.remoteIP(); -// // // Serial.print("Received packet of size "); -// // // Serial.println(packetSize); -// // // Serial.print("From "); -// // // Serial.print(_lastConnectedIP); -// // // Serial.print(", port "); -// // // Serial.println(_lastConnectedPort); - -// // read the packet into packetBufffer -// int len = m_server.read(packetBuffer, MAX_COMMAND); -// if (len > 0) -// { -// packetBuffer[len] = 0; -// //LogHandler::verbose(_TAG, "Udp in: %s", packetBuffer); -// } -// if (m_tcodeVersion >= TCodeVersion::v0_3 && (strpbrk(packetBuffer, "$") != nullptr || strpbrk(packetBuffer, "#") != nullptr)) -// { -// // strcpy(buf, packetBuffer); -// LogHandler::debug(_TAG, "System command received: %s", buf); -// CommandCallback("OK"); -// // } else if (strpbrk(packetBuffer, jsonIdentifier) != nullptr) { -// // SettingsHandler::getProcessTCodeJson()(udpData, packetBuffer); -// // //LogHandler::verbose(_TAG, "json processed: %s", udpData); -// } -// else -// { -// //udpData[strlen(packetBuffer) + 1]; -// strncpy(buf, packetBuffer, len); -// //LogHandler::verbose(_TAG, "Udp tcode in: %s", udpData); -// } -// } - -// private: -// const char* _TAG = TagHandler::TcpHandler; -// WiFiServer m_server; -// TCodeVersion m_tcodeVersion; -// IPAddress _lastConnectedIP; -// int _lastConnectedPort = 0; -// bool initialized = false; -// char packetBuffer[MAX_COMMAND]; //buffer to hold incoming packet -// char jsonIdentifier[2] = "{"; -// }; diff --git a/ESP32/src/TemperatureHandler.h b/ESP32/src/TemperatureHandler.h deleted file mode 100644 index 975bb99..0000000 --- a/ESP32/src/TemperatureHandler.h +++ /dev/null @@ -1,725 +0,0 @@ -/* MIT License - -Copyright (c) 2026 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include -#include -#include -// #include -#include "TCode/Global.h" -#include "SettingsHandler.h" -// #include "LogHandler.h" -#include "logging/TagHandler.h" -#include "TaskHandler.h" -#include "MessageHandler.h" - -enum class TemperatureType -{ - INTERNAL, - SLEEVE -}; - -class TemperatureState -{ -public: - // Global state - static const int MAX_LEN; - static const char *UNKNOWN; - static const char *DISABLED_STATE; - static const char *RESTART_REQUIRED; - // Heater state - static const char *FAIL_SAFE; - static const char *ERROR; - static const char *MAX_TEMP_ERROR; - static const char *HOLD; - static const char *HEAT; - // Fan states - static const char *COOLING; - static const char *OFF; -}; -// Global state -const int TemperatureState::MAX_LEN = 10; -const char *TemperatureState::UNKNOWN = "Unknown"; -const char *TemperatureState::DISABLED_STATE = "Disabled"; -const char *TemperatureState::RESTART_REQUIRED = "Restart"; -// Heater state -const char *TemperatureState::FAIL_SAFE = "Fail safe"; -const char *TemperatureState::ERROR = "Error"; -const char *TemperatureState::MAX_TEMP_ERROR = "Max temp"; -const char *TemperatureState::HOLD = "Holding"; -const char *TemperatureState::HEAT = "Heating"; -// Fan states -const char *TemperatureState::COOLING = "Cooling"; -const char *TemperatureState::OFF = "Off"; - -class TemperatureHandler : public Task -{ - -public: - void setup(bool internalTempEnabled, - bool sleeveTempEnabled, - int8_t sleeveTempPin, - int8_t internalTempPin, - int8_t heaterPin, - int8_t heaterChannel, - int8_t caseFanPin, - int8_t caseFanChannel, - int heaterFrequency, - int heaterResolution, - bool fanControlEnabled, - int fanFrequency, - int fanResolution, - int maxFanPWM) - { - m_settingsFactory = SettingsFactory::getInstance(); - m_internalTempEnabled = internalTempEnabled; - m_sleeveTempEnabled = sleeveTempEnabled; - m_sleeveTempPin = sleeveTempPin; - m_internalTempPin = internalTempPin; - m_heaterPin = heaterPin; - m_heatChannel = heaterChannel; - m_caseFanPin = caseFanPin; - m_caseFanChannel = caseFanChannel; - m_heaterFrequency = heaterFrequency; - m_heaterResolution = heaterResolution; - m_fanControlEnabled = fanControlEnabled; - m_fanFrequency = fanFrequency; - m_fanResolution = fanResolution; - m_fanMaxDuty = maxFanPWM; - if (m_internalTempEnabled) - { - setupInternalTemp(); - } - if (m_sleeveTempEnabled) - { - setupSleeveTemp(); - } - if (m_fanControlEnabled) - { - setupInternalFan(); - } - } - - void setupSleeveTemp() - { - if (!m_sleeveTempEnabled || m_sleeveTempPin < 0) - { - return; - } - LogHandler::info(_TAG, "Starting sleeve temp on pin: %d", m_sleeveTempPin); - oneWireSleeve.begin(m_sleeveTempPin); - sensorsSleeve.setOneWire(&oneWireSleeve); - sensorsSleeve.begin(); - if (!sensorsSleeve.getAddress(sleeveDeviceAddress, 0)) - { - LogHandler::error(_TAG, "No temp sensor found on sleeve bus (index 0)."); - sleeveTempInitialized = false; - return; - } - - sensorsSleeve.setResolution(sleeveDeviceAddress, resolution); - sensorsSleeve.setWaitForConversion(false); - requestSleeveTemp(); - - // bootTime = true; - // bootTimer = millis() + SettingsHandler::getWarmUpTime(); - if (m_heaterPin > -1 && m_heatChannel > -1) - { - LogHandler::debug(_TAG, "Starting heat on pin: %d", m_heaterPin); -#ifdef ESP_ARDUINO3 - ledcAttachChannel(m_heaterPin, m_heaterFrequency, m_heaterResolution, m_heatChannel); -#else - ledcSetup(m_heatChannel, m_heaterFrequency, m_heaterResolution); - ledcAttachPin(m_heaterPin, m_heatChannel); -#endif - } - sleeveTempInitialized = sensorsSleeve.isConnected(sleeveDeviceAddress); - if (!sleeveTempInitialized) - LogHandler::error(_TAG, "Failed to initialize sleeve temp sensor."); - } - - void setupInternalTemp() - { - if (!m_internalTempEnabled || m_internalTempPin < 0) - { - return; - } - LogHandler::info(_TAG, "Starting internal temp on pin: %d", m_internalTempPin); - oneWireInternal.begin(m_internalTempPin); - sensorsInternal.setOneWire(&oneWireInternal); - sensorsInternal.begin(); - if (!sensorsInternal.getAddress(internalDeviceAddress, 0)) - { - LogHandler::error(_TAG, "No temp sensor found on internal bus (index 0)."); - sleeveTempInitialized = false; - return; - } - sensorsInternal.setResolution(internalDeviceAddress, resolution); - sensorsInternal.setWaitForConversion(false); - // internalPID.setGains(0.12f, 0.0003f, 0); - // internalPID.setOutputRange(); - requestInternalTemp(); - internalTempInitialized = sensorsInternal.isConnected(internalDeviceAddress); - if (!internalTempInitialized) - LogHandler::error(_TAG, "Failed to initialize internal temp sensor."); - } - - void setupInternalFan() - { - if (!m_fanControlEnabled || m_caseFanPin < 0 || m_caseFanChannel < 0) - { - return; - } - LogHandler::debug(_TAG, "Setting up fan, PIN: %i, hz: %i, resolution: %i, MAX PWM: %i", m_caseFanPin, m_fanFrequency, m_fanResolution, m_fanMaxDuty); -#ifdef ESP_ARDUINO3 - ledcAttachChannel(m_caseFanPin, m_fanFrequency, m_fanResolution, m_caseFanChannel); -#else - ledcSetup(m_caseFanChannel, m_fanFrequency, m_fanResolution); - ledcAttachPin(m_caseFanPin, m_caseFanChannel); -#endif - // LogHandler::debug(_TAG, "Setting up PID: Output max: %i", m_fanMaxDuty); - // internalPID = new AutoPID(&_currentInternalTemp, &SettingsHandler::getInternalTempForFan(), &m_currentInternalTempDuty, m_fanMaxDuty, 0, 0.12, 0.0003, 0.0); - // //if temperature is more than 4 degrees below or above setpoint, OUTPUT will be set to min or max respectively - // internalPID->setBangBang(2, 0); - // //set PID update interval to 4000ms - // internalPID->setTimeStep(4000); - fanControlInitialized = true; - } - - void loop() - { - getInternalTemp(); - getSleeveTemp(); - chackFailSafe(); - this->sleep(5000); - } - - bool isMaxTempTriggered() - { - return maxTempTriggerInternal; - } - - void getInternalTemp() - { - if (m_internalTempEnabled && internalTempInitialized && millis() - lastInternalTempRequest >= delayInMillis) - { - - long start = micros(); - - float currentInternalTemp = sensorsInternal.getTempC(internalDeviceAddress); - bool tempChanged = false; - - if (!essentiallyEqual(_currentInternalTemp, m_lastInternalTemp)) - { - tempChanged = true; - m_lastInternalTemp = _currentInternalTemp; - LogHandler::debug(_TAG, "Last internal temp: %f", m_lastInternalTemp); - // LogHandler::debug(_TAG, "Current internal temp: %f", _currentInternalTemp); - } - _currentInternalTemp = currentInternalTemp; - LogHandler::verbose(_TAG, "Current internal temp: %f", _currentInternalTemp); - - LogHandler::verbose(_TAG, "internal getTempC duration: %d", micros() - start); - - String statusJson("{\"temp\":\"" + String(_currentInternalTemp) + "\", \"status\":\"" + m_lastInternalStatus + "\"}"); - if (tempChanged && message_callback) - { - message_callback(TemperatureType::INTERNAL, statusJson.c_str(), _currentInternalTemp); - } - requestInternalTemp(); - } - } - - void getSleeveTemp() - { - if (m_sleeveTempEnabled && sleeveTempInitialized && millis() - lastSleeveTempRequest >= delayInMillis) - { - long start = micros(); - - float currentSleeveTemp = sensorsSleeve.getTempC(sleeveDeviceAddress); - bool tempChanged = false; - - if (!essentiallyEqual(_currentSleeveTemp, m_lastSleeveTemp)) - { - tempChanged = true; - m_lastSleeveTemp = _currentSleeveTemp; - LogHandler::debug(_TAG, "Last sleeve temp: %f", m_lastSleeveTemp); - } - _currentSleeveTemp = currentSleeveTemp; - LogHandler::verbose(_TAG, "Current sleeve temp: %f", _currentSleeveTemp); - - long duration = micros() - start; - - LogHandler::verbose(_TAG, "sleeve getTempC duration: %d", micros() - start); - - String statusJson("{\"temp\":\"" + String(_currentSleeveTemp) + "\", \"status\":\"" + m_lastSleeveStatus + "\"}"); - if (tempChanged) - { - Messages::message_t m = { - .message = statusJson.c_str(); - .data_f = _currentSleeveTemp - }; - Messages::MessageHandler::getInstance()->send(m) - } - requestSleeveTemp(); - } -} - -void -setFanState() -{ - if (m_caseFanPin < 0 || m_caseFanChannel < 0 || !m_fanControlEnabled || !_isRunning || !fanControlInitialized) - { - return; - } - String currentState; - if (fanControlInitialized) - { - if (!internalTempInitialized) - { - currentState = TemperatureState::COOLING; - m_lastInternalTempDuty = m_fanMaxDuty; - } - else if (failsafeTriggerInternal) - { - currentState = TemperatureState::FAIL_SAFE; - } - else if (maxTempTriggerInternal) - { - currentState = TemperatureState::MAX_TEMP_ERROR; - } - else - { - double currentTemp = _currentInternalTemp; - // LogHandler::debug(_TAG, "Current global temp: %f", _currentInternalTemp); - if (currentTemp == -127.0) - { - currentState = TemperatureState::ERROR; - } - else - { - // if(definitelyLessThanOREssentiallyEqual(currentTemp, SettingsHandler::getInternalTempForFan() + 3) && - // definitelyGreaterThanOREssentiallyEqual(currentTemp, SettingsHandler::getInternalTempForFan() - 3)) { - // currentState = TemperatureState::COOLING; - // currentDuty = m_fanMaxDuty * 0.8; - // } else - if (definitelyGreaterThanOREssentiallyEqual(currentTemp, m_settingsFactory->getInternalTempForFanOn()) || - (definitelyGreaterThanOREssentiallyEqual(currentTemp, m_settingsFactory->getInternalMaxTemp() - 5) && m_lastInternalTempDuty > 0)) - { - // LogHandler::debug(_TAG, "definitelyGreaterThanOREssentiallyEqual: %f >= %f", currentTemp, SettingsHandler::getInternalTempForFan()); - currentState = TemperatureState::COOLING; - m_lastInternalTempDuty = m_fanMaxDuty; - // Calculate pwm based on user entered values. - // double maxTemp = SettingsHandler::getInternalTempForFan() + SettingsHandler::getInternalTempForFan() * 0.50; - // if(definitelyGreaterThanOREssentiallyEqual(currentTemp, maxTemp)) - // maxTemp = currentTemp; - // m_currentInternalTempDuty = map(currentTemp, - // SettingsHandler::getInternalTempForFan(), - // maxTemp, - // 50, // Min duty. - // m_fanMaxDuty); - // // https://sciencing.com/calculate-pulse-width-8618299.html - // // Period = 1/Frequency - // // Frequency = 1/Period - // // duty cycle = PulseWidth/Period - // // duty percentage = (duty/SettingsHandler::getCaseFanPWM()) * 100 - - // if (currentTemp < SettingsHandler::getInternalTempForFan()) { // Fan at 10% over 27 degree - // m_lastInternalTempDuty = 0; - // } else if (currentTemp > SettingsHandler::getInternalTempForFan() + round(SettingsHandler::getInternalTempForFan() * 0.50)){ - // m_lastInternalTempDuty = 800; - // } else if (currentTemp > SettingsHandler::getInternalTempForFan() + round(SettingsHandler::getInternalTempForFan() * 0.40)){ - // m_lastInternalTempDuty = 500; - // } else if (currentTemp > SettingsHandler::getInternalTempForFan() + round(SettingsHandler::getInternalTempForFan() * 0.30)){ - // m_lastInternalTempDuty = 200; - // } else if (currentTemp > SettingsHandler::getInternalTempForFan()){ - // m_lastInternalTempDuty = 50; - // } - // if(m_lastInternalTempDuty != m_currentInternalTempDuty) { - // m_lastInternalTempDuty = m_currentInternalTempDuty; - // // m_lastInternalTempDuty = constrain(duty, 50, SettingsHandler::getCaseFanPWM()); - // LogHandler::debug(_TAG, "Setting fan duty: %f, Max duty: %i", m_lastInternalTempDuty, m_fanMaxDuty); - // // LogHandler::debug(_TAG, "Current temp: %f, maxTemp: %f, fan on temp: %f", currentTemp, maxTemp, SettingsHandler::getInternalTempForFan()); - // LogHandler::debug(_TAG, "Current temp: %f, fan on temp: %f", _currentInternalTemp, SettingsHandler::getInternalTempForFan()); - // } - } - else - { - currentState = TemperatureState::OFF; - m_lastInternalTempDuty = 0; - } - } - } -// if (m_lastInternalTempDuty != m_fanMaxDuty) { -// m_lastInternalTempDuty = m_fanMaxDuty; -// LogHandler::debug(_TAG, "Setting fan duty: %f", m_lastInternalTempDuty); -// } -#ifdef ESP_ARDUINO3 - ledcWrite(m_caseFanPin, m_lastInternalTempDuty); -#else - ledcWrite(m_caseFanChannel, m_lastInternalTempDuty); -#endif - } - else - { - if (m_fanControlEnabled && !fanControlInitialized) - currentState = TemperatureState::RESTART_REQUIRED; - else - currentState = TemperatureState::DISABLED_STATE; - } - if (!currentState.isEmpty()) - { - if (m_lastInternalStatus != currentState) - { - LogHandler::debug(_TAG, "Setting fan state: %s", currentState); - } - setState(TemperatureType::INTERNAL, currentState.c_str()); - } -} - -void setHeaterState() -{ - // Serial.println(_currentTemp); - if (m_heaterPin < 0 || m_heatChannel < 0 || !m_sleeveTempEnabled || !_isRunning || !sleeveTempInitialized) - { - return; - } - String currentState; - if (m_sleeveTempEnabled && sleeveTempInitialized) - { - if (failsafeTriggerSleeve) - { -#ifdef ESP_ARDUINO3 - ledcWrite(m_heaterPin, 0); -#else - ledcWrite(m_heatChannel, 0); -#endif - } - else - { - double currentTemp = _currentSleeveTemp; - if (currentTemp == -127.0) - { - currentState = TemperatureState::ERROR; - } - else - { - // if(currentTemp >= SettingsHandler::getTargetTemp() || millis() >= bootTimer) - // bootTime = false; - if (definitelyLessThan(currentTemp, m_settingsFactory->getTargetTemp()) - //|| (currentTemp > 0 && bootTime) - ) - { -#ifdef ESP_ARDUINO3 - ledcWrite(m_heaterPin, m_settingsFactory->getHeatPWM()); -#else - ledcWrite(m_heatChannel, m_settingsFactory->getHeatPWM()); -#endif - - // if(bootTime) - // { - // long time = bootTimer - millis(); - // int tseconds = time / 1000; - // int tminutes = tseconds / 60; - // int seconds = tseconds % 60; - // currentState = "Warm up time: " + String(tminutes) + ":" + (seconds < 10 ? "0" : "") + String(seconds); - // } - // else - // { - currentState = TemperatureState::HEAT; - // } - if (targetSleeveTempReached && m_settingsFactory->getTargetTemp() - currentTemp >= 5) - targetSleeveTempReached = false; - } - else if (definitelyLessThanOREssentiallyEqual(currentTemp, (m_settingsFactory->getTargetTemp() + m_settingsFactory->getHeaterThreshold()))) - { - if (!targetSleeveTempReached) - { - targetSleeveTempReached = true; - String *command = new String("tempReached"); - auto msgs = Messages::MessageHandler::getInstance(); - msgs->send() if (message_callback) - message_callback(TemperatureType::SLEEVE, "tempReached", _currentSleeveTemp); - } -#ifdef ESP_ARDUINO3 - ledcWrite(m_heaterPin, m_settingsFactory->getHoldPWM()); -#else - ledcWrite(m_heatChannel, m_settingsFactory->getHoldPWM()); -#endif - currentState = TemperatureState::HOLD; - } - else - { -#ifdef ESP_ARDUINO3 - ledcWrite(m_heaterPin, 0); -#else - ledcWrite(m_heatChannel, 0); -#endif - currentState = TemperatureState::OFF; - } - } - } - } - else - { - if (m_sleeveTempEnabled && !sleeveTempInitialized) - currentState = TemperatureState::RESTART_REQUIRED; - else - currentState = TemperatureState::DISABLED_STATE; - } - if (!currentState.isEmpty()) - setState(TemperatureType::SLEEVE, currentState.c_str()); -} - -const char *getShortSleeveControlStatus(const char *state) -{ - if (strcmp(state, TemperatureState::FAIL_SAFE) == 0) - { - return "F"; - } - else if (strcmp(state, TemperatureState::ERROR) == 0) - { - return "E"; - } - else if (strcmp(state, TemperatureState::MAX_TEMP_ERROR) == 0) - { - return "M"; - } - else if (strcmp(state, TemperatureState::HOLD) == 0) - { - return "S"; - } - else if (strcmp(state, TemperatureState::HEAT) == 0) - { - return "H"; - } - else if (strcmp(state, TemperatureState::UNKNOWN) == 0) - { - return "U"; - } - // TemperatureState::OFF - return "O"; -} - -void stop() -{ - _isRunning = false; -} - -bool isRunning() -{ - return _isRunning; -} - -void chackFailSafe() -{ - if (millis() >= failSafeFrequency) - { - failSafeFrequency = millis() + failSafeFrequencyLimiter; - if (m_sleeveTempEnabled) - { - if (errorCountSleeve > 10) - { - if (!failsafeTriggerSleeve) - { - failsafeTriggerSleeve = true; - if (message_callback) - message_callback(TemperatureType::SLEEVE, "failSafeTriggered", _currentSleeveTemp); - setState(TemperatureType::SLEEVE, TemperatureState::FAIL_SAFE); - } - } - else if (m_lastSleeveStatus == TemperatureState::ERROR) - { - errorCountSleeve++; - } - else - { - errorCountSleeve = 0; - } - } - - if (m_internalTempEnabled) - { - if (definitelyGreaterThanOREssentiallyEqual(_currentInternalTemp, m_settingsFactory->getInternalMaxTemp())) - { - if (!failsafeTriggerInternal && !maxTempTriggerInternal) - { - maxTempTriggerInternal = true; - if (message_callback) - message_callback(TemperatureType::INTERNAL, "failSafeTriggered", _currentInternalTemp); - setState(TemperatureType::INTERNAL, TemperatureState::MAX_TEMP_ERROR); - } - } - if (errorCountInternal > 10) - { - if (!failsafeTriggerInternal && !maxTempTriggerInternal) - { - failsafeTriggerInternal = true; - if (message_callback) - message_callback(TemperatureType::INTERNAL, "failSafeTriggered", _currentInternalTemp); - setState(TemperatureType::INTERNAL, TemperatureState::FAIL_SAFE); - } - } - else if (m_lastInternalStatus == TemperatureState::ERROR) - { - errorCountInternal++; - } - else - { - errorCountInternal = 0; - } - } - } -} - -private: -static constexpr Tags::tag_t _TAG = Tags::Temperature; -SettingsFactory *m_settingsFactory; - -bool _isRunning = false; - -// Boot value -bool m_internalTempEnabled = TEMP_INTERNAL_ENABLED_DEFAULT; -bool m_sleeveTempEnabled = TEMP_SLEEVE_ENABLED_DEFAULT; -int8_t m_sleeveTempPin = SLEEVE_TEMP_DISPLAYED_DEFAULT; -int8_t m_internalTempPin = INTERNAL_TEMP_PIN_DEFAULT; -int8_t m_heaterPin = HEATER_PIN_DEFAULT; -int8_t m_caseFanPin = CASE_FAN_PIN_DEFAULT; -int m_heaterFrequency = ESP_TIMER_FREQUENCY_DEFAULT; -int m_heaterResolution = HEATER_RESOLUTION_DEFAULT; -bool m_fanControlEnabled = FAN_CONTROL_ENABLED_DEFAULT; -int m_fanFrequency = ESP_TIMER_FREQUENCY_DEFAULT; -int m_fanResolution = CASE_FAN_RESOLUTION_DEFAULT; -int m_fanMaxDuty = CASE_FAN_MAX_PWM_DEFAULT; // pow(2, m_fanResolution) - 1; -float m_internalTempForFanOn = INTERNAL_TEMP_FOR_FAN_DEFAULT; - -int8_t m_caseFanChannel = -1; -int8_t m_heatChannel = -1; - -const int resolution = 9; -const int delayInMillis = 750 / (1 << (12 - resolution)); - -TEMP_CHANGE_FUNCTION_PTR_T message_callback = 0; -STATE_CHANGE_FUNCTION_PTR_T state_change_callback = 0; - -long failSafeFrequency; -int failSafeFrequencyLimiter = 10000; - -// Internal temp/Fan control -OneWire oneWireInternal; -DallasTemperature sensorsInternal; -DeviceAddress internalDeviceAddress; -// AutoPID* internalPID; -double _currentInternalTemp = 0.0f; -String m_lastInternalStatus = TemperatureState::UNKNOWN; -double m_lastInternalTemp = -127.0; -double m_currentInternalTempDuty = 0.0; -double m_lastInternalTempDuty = 0.0; -unsigned long lastInternalTempRequest = 0; - -int errorCountInternal = 0; -bool internalTempInitialized = false; - -bool failsafeTriggerInternal = false; -bool maxTempTriggerInternal = false; -bool fanControlInitialized = false; -///////////////////////////////////////// - -// Sleeve temp/Heater -OneWire oneWireSleeve; -DallasTemperature sensorsSleeve; -DeviceAddress sleeveDeviceAddress; -double _currentSleeveTemp = 0.0f; -String m_lastSleeveStatus = TemperatureState::UNKNOWN; -double m_lastSleeveTemp = -127.0; -unsigned long lastSleeveTempRequest = 0; - -int errorCountSleeve = 0; -bool sleeveTempInitialized = false; - -bool targetSleeveTempReached = false; -bool failsafeTriggerSleeve = false; -/////////////////////////////////////////// - -bool definitelyGreaterThan(float a, float b) -{ - return (a - b) > ((fabs(a) < fabs(b) ? fabs(b) : fabs(a)) * __FLT_EPSILON__); -} - -bool definitelyLessThan(float a, float b) -{ - return (b - a) > ((fabs(a) < fabs(b) ? fabs(b) : fabs(a)) * __FLT_EPSILON__); -} -bool essentiallyEqual(float a, float b) -{ - return fabs(a - b) <= ((fabs(a) > fabs(b) ? fabs(b) : fabs(a)) * __FLT_EPSILON__); -} -bool approximatelyEqual(float a, float b) -{ - return fabs(a - b) <= ((fabs(a) < fabs(b) ? fabs(b) : fabs(a)) * __FLT_EPSILON__); -} -bool definitelyLessThanOREssentiallyEqual(float a, float b) -{ - return definitelyLessThan(a, b) || essentiallyEqual(a, b); -} -bool definitelyLessThanORApproximatelyEqual(float a, float b) -{ - return definitelyLessThan(a, b) || approximatelyEqual(a, b); -} -bool definitelyGreaterThanOREssentiallyEqual(float a, float b) -{ - return definitelyGreaterThan(a, b) || essentiallyEqual(a, b); -} -void setState(TemperatureType type, const char *state) -{ - bool stateChanged = false; - if (type == TemperatureType::INTERNAL) - { - stateChanged = strcmp(state, m_lastInternalStatus.c_str()) != 0; - if (stateChanged) - m_lastInternalStatus = state; - } - else - { - stateChanged = strcmp(state, m_lastSleeveStatus.c_str()) != 0; - if (stateChanged) - m_lastSleeveStatus = state; - } - if (state_change_callback && stateChanged) - { - state_change_callback(type, state); - } -} -void requestInternalTemp() -{ - sensorsInternal.requestTemperaturesByIndex(0); - lastInternalTempRequest = millis(); -} - -void requestSleeveTemp() -{ - sensorsSleeve.requestTemperaturesByIndex(0); - lastSleeveTempRequest = millis(); -} -} -; diff --git a/ESP32/src/UdpHandler.h b/ESP32/src/UdpHandler.h deleted file mode 100644 index cdfe7d2..0000000 --- a/ESP32/src/UdpHandler.h +++ /dev/null @@ -1,149 +0,0 @@ -/* MIT License - -Copyright (c) 2026 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include -#include -#include -#include -#include "settings/SettingsHandler.h" -#include "logging/LogHandler.h" -#include "logging/TagHandler.h" -#include "tasks/TaskHandler.h" - -class UdpHandler : public TaskHandler::Task -{ -public: - UdpHandler() : Task(TaskHandler::Rates::SLOW) {} - void setup() override - { - // Defer UDP initialization until WiFi/lwip is ready - // Don't initialize here - let loop() handle it on first real call - udpInitialized = false; - m_TCodeQueue = xQueueCreate(25, sizeof(char[MAX_COMMAND])); - SettingsFactory* m_settingsFactory = SettingsFactory::getInstance(); - m_tcodeVersion = m_settingsFactory->getTcodeVersion(); - } - - bool initializeUdp() - { - if (udpInitialized) - return true; - - int localPort = SettingsFactory::getInstance()->getUdpServerPort(); - LogHandler::info(Tags::Udp, "Starting UDP on port: %ld", localPort); - if (!m_udp.listen(localPort)) - { - LogHandler::error(Tags::Udp, "UDP Error Listening"); - return false; - } - LogHandler::info(Tags::Udp, "UDP Listening"); - m_udp.onPacket(udpCallback, static_cast(this)); - udpInitialized = true; - return true; - } - - void loop() override - { - // Lazy-initialize UDP once WiFi/lwip is ready - if (!udpInitialized) - { - // Do not call AsyncUDP until WiFi stack has been enabled. - if (WiFi.getMode() == WIFI_OFF) - { - return; - } - - // Try initialize; if it fails, retry on a later tick. - initializeUdp(); - } - - // Drain queued UDP TCode commands and forward to motor task - char buf[MAX_COMMAND]; - while (xQueueReceive(m_TCodeQueue, buf, 0) == pdTRUE) - { - extern void feedMotorCommand(const char* cmd, size_t len); - feedMotorCommand(buf, strlen(buf)); - } - } - - static void udpCallback(void* arg, AsyncUDPPacket& packet) - { - UdpHandler* udp = static_cast(arg); - // LogHandler::verbose(udp->Tags::Udp, "UDP recieve: %s", packet.data()); - udp->_lastConnectedPort = packet.remotePort(); - udp->_lastConnectedIP = packet.remoteIP(); - udp->packetBuffer[0] = { 0 }; - - memcpy(udp->packetBuffer, packet.data(), packet.length()); - // size_t len = packet.readBytes(udp->packetBuffer, sizeof(packetBuffer)); - udp->packetBuffer[packet.length()] = '\0'; - if (xQueueSend(udp->m_TCodeQueue, udp->packetBuffer, 0) != pdTRUE) - LogHandler::error(Tags::Udp, "UDP queue full"); - } - - void CommandCallback(const char* in) - { // This overwrites the callback for message return - if (udpInitialized && _lastConnectedPort > 0) - { - LogHandler::debug(Tags::Udp, "Sending udp to client: %s", in); - int i = 0; - AsyncUDPMessage message; - while (in[i] != 0) - message.write((uint8_t)in[i++]); - m_udp.sendTo(message, _lastConnectedIP, _lastConnectedPort); - // m_udp.endPacket(); - } - } - - void read(char* buf) - { - if (!udpInitialized) - { - buf[0] = { 0 }; - return; - } - if (xQueueReceive(m_TCodeQueue, buf, 0)) - { - // LogHandler::verbose(Tags::Udp, "Recieve tcode: %s", buf); - } - else - { - // LogHandler::error(Tags::Udp, "Failed to read from queue"); - buf[0] = { 0 }; - return; - } - } - -private: - TCodeVersion m_tcodeVersion; - TaskHandle_t m_task; - QueueHandle_t m_TCodeQueue; - - AsyncUDP m_udp; - IPAddress _lastConnectedIP; - int _lastConnectedPort = 0; - bool udpInitialized = false; - char packetBuffer[MAX_COMMAND] = { 0 }; // buffer to hold incoming packet - char jsonIdentifier[2] = "{"; -}; diff --git a/ESP32/src/UdpHandler2.h b/ESP32/src/UdpHandler2.h deleted file mode 100644 index 08adfe6..0000000 --- a/ESP32/src/UdpHandler2.h +++ /dev/null @@ -1,158 +0,0 @@ -/* MIT License - -Copyright (c) 2026 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -// #pragma once - - -// #include -// #include -// #include -// #include "SettingsHandler.h" -// // #include "LogHandler.h" -// #include "TagHandler.h" - - -// class Udphandler -// { -// public: -// bool setup(int localPort) -// { -// LogHandler::info(_TAG, "Starting UDP2"); -// if(!m_server.begin(localPort)) { -// return false; -// } -// LogHandler::info(_TAG, "UDP2 Listening"); -// SettingsFactory* m_settingsFactory = SettingsFactory::getInstance(); -// m_tcodeVersion = m_settingsFactory->getTcodeVersion(); -// // m_TCodeQueue = xQueueCreate(25, sizeof(char[MAX_COMMAND])); -// // if(xTaskCreatePinnedToCore( -// // handlerTask,/* Function to implement the task */ -// // "UDPTask", /* Name of the task */ -// // configMINIMAL_STACK_SIZE*4, /* Stack size in words */ -// // static_cast(this), /* Task input parameter */ -// // tskIDLE_PRIORITY, /* Priority of the task */ -// // &m_task, /* Task handle. */ -// // WIFI_TASK_CORE_ID) == pdFALSE) /* Core where the task should run */ -// // return; -// initialized = true; -// return true; -// } - -// // static void handlerTask(void* arg) { -// // Udphandler* handler = static_cast(arg); -// // char packetBuffer[MAX_COMMAND];; //buffer to hold incoming packet -// // TickType_t pxPreviousWakeTime = millis(); -// // while(1) { -// // int packetSize = handler->m_server.parsePacket(); -// // if(packetSize) { -// // int len = handler->m_server.read(packetBuffer, MAX_COMMAND); -// // if (len > 0) { -// // packetBuffer[len] = 0; -// // //LogHandler::verbose(_TAG, "Udp in: %s", packetBuffer); -// // } -// // LogHandler::verbose(handler->_TAG, "Recieve: %s", packetBuffer); -// // if(xQueueSend(handler->m_TCodeQueue, packetBuffer, 0) != pdTRUE) { -// // //LogHandler::error(_TAG, "Failed to write to queue"); -// // } -// // } -// // xTaskDelayUntil(&pxPreviousWakeTime, 10/portTICK_PERIOD_MS); -// // } -// // } - -// void CommandCallback(const char* in) { //This overwrites the callback for message return -// if(initialized && _lastConnectedPort > 0) { -// LogHandler::debug(_TAG, "Sending udp to client: %s", in); -// m_server.beginPacket(_lastConnectedIP, _lastConnectedPort); -// int i = 0; -// while (in[i] != 0) -// m_server.write((uint8_t)in[i++]); -// m_server.endPacket(); -// } -// } - -// void read(char* buf) -// { -// if (!initialized) -// { -// buf[0] = {0}; -// return; -// } -// // if(xQueueReceive(m_TCodeQueue, buf, 0)) { -// // //LogHandler::verbose(_TAG, "Recieve tcode: %s", buf); -// // } else { -// // //LogHandler::error(_TAG, "Failed to read from queue"); -// // buf[0] = {0}; -// // return; -// // } -// // // if there's data available, read a packet -// int packetSize = m_server.parsePacket(); -// if (!packetSize) -// { -// buf[0] = {0}; -// return; -// } -// _lastConnectedPort = m_server.remotePort(); -// _lastConnectedIP = m_server.remoteIP(); -// // // Serial.print("Received packet of size "); -// // // Serial.println(packetSize); -// // // Serial.print("From "); -// // // Serial.print(_lastConnectedIP); -// // // Serial.print(", port "); -// // // Serial.println(_lastConnectedPort); - -// // read the packet into packetBufffer -// int len = m_server.read(packetBuffer, MAX_COMMAND); -// if (len > 0) -// { -// packetBuffer[len] = 0; -// //LogHandler::verbose(_TAG, "Udp in: %s", packetBuffer); -// } -// if (m_tcodeVersion >= TCodeVersion::v0_3 && (strpbrk(packetBuffer, "$") != nullptr || strpbrk(packetBuffer, "#") != nullptr)) -// { -// // strcpy(buf, packetBuffer); -// LogHandler::debug(_TAG, "System command received: %s", buf); -// CommandCallback("OK"); -// // } else if (strpbrk(packetBuffer, jsonIdentifier) != nullptr) { -// // SettingsHandler::getProcessTCodeJson()(udpData, packetBuffer); -// // //LogHandler::verbose(_TAG, "json processed: %s", udpData); -// } -// else -// { -// //udpData[strlen(packetBuffer) + 1]; -// strncpy(buf, packetBuffer, len); -// //LogHandler::verbose(_TAG, "Udp tcode in: %s", udpData); -// } -// } - -// private: -// const char* _TAG = TagHandler::UdpHandler; -// TCodeVersion m_tcodeVersion; -// //TaskHandle_t m_task; -// //QueueHandle_t m_TCodeQueue; - -// WiFiUDP m_server; -// IPAddress _lastConnectedIP; -// int _lastConnectedPort = 0; -// bool initialized = false; -// char packetBuffer[MAX_COMMAND];; //buffer to hold incoming packet -// char jsonIdentifier[2] = "{"; -// }; diff --git a/ESP32/src/VoiceHandler.hpp b/ESP32/src/VoiceHandler.hpp deleted file mode 100644 index 8beaaf7..0000000 --- a/ESP32/src/VoiceHandler.hpp +++ /dev/null @@ -1,206 +0,0 @@ -/* MIT License - -Copyright (c) 2026 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include -#include "DFRobot_DF2301Q.h" -#include -#include "logging/LogHandler.h" -#include "logging/TagHandler.h" -#include "settings/SettingsHandler.h" -#include "tasks/TaskHandler.h" - -using VOICE_COMMAND_FUNCTION_PTR_T = void (*)(const char* tcodeCommand); - -class VoiceHandler : public TaskHandler::Task -{ -public: - VoiceHandler() : Task(TaskHandler::Rates::ONDEMAND) {} - void setup() override - { - LogHandler::info(Tags::Voice, "Setup voice"); - if (!SettingsHandler::waitForI2CDevices(DF2301Q_I2C_ADDR)) - { - return; - } - int tries = 0; - while (!asr.begin() && tries <= 3) - { // Returns true no matter what right now. - LogHandler::error(Tags::Voice, "Could not connect, trying again."); - if (tries >= 3) - { - LogHandler::error(Tags::Voice, "Communication with device failed, please check connection"); - return; - } - tries++; - this->wait(1000); - } - _isConnected = true; - LogHandler::info(Tags::Voice, "Begin ok!"); - SettingsFactory* settingsFactory = SettingsFactory::getInstance(); - if (settingsFactory->getVoiceVolume() > 0) - { - setVolume(settingsFactory->getVoiceVolume()); - } - setMuteMode(settingsFactory->getVoiceMuted()); - setWakeTime(settingsFactory->getVoiceWakeTime()); - /** - @brief Get wake-up duration - @return The currently-set wake-up period - */ - // uint8_t wakeTime = 0; - // wakeTime = asr.getWakeTime(); - // LogHandler::info(Tags::Voice, "wakeTime: %ld", wakeTime); - - } - bool isConnected() - { - return _isConnected; - } - - /** - * @brief Set voice volume - * @param value - Volume value(1~7) - */ - void setVolume(int value) - { - asr.setVolume(value); - } - /** - @brief Set mute mode - @param value - Mute mode; set value 1: mute, 0: unmute - */ - void setMuteMode(bool value) - { - asr.setMuteMode(value); - } - /** - @brief Set wake-up duration - @param value - Wake-up duration (0-255) - */ - void setWakeTime(int value) - { - asr.setWakeTime(value); - } - /** - @brief Play the corresponding reply audio according to the ID - @param CMDID - command word ID - */ - void playByCMDID(int value) - { - asr.playByCMDID(value); - } - - static void startLoop(void* parameter) - { - ((VoiceHandler*)parameter)->loop(); - } - - void setMessageCallback(VOICE_COMMAND_FUNCTION_PTR_T f) - { - if (f == nullptr) - { - message_callback = 0; - } - else - { - message_callback = f; - } - } - -private: - // I2C communication - DFRobot_DF2301Q_I2C asr; - VOICE_COMMAND_FUNCTION_PTR_T message_callback = 0; - bool _isRunning = false; - bool _isConnected = false; - - void loop() override - { /** -@brief Get the ID corresponding to the command word -@return Return the obtained command word ID, returning 0 means no valid ID is obtained -*/ - toTCode(asr.getCMDID()); - this->sleep(1000); - } - - void toTCode(uint8_t voiceCommand) - { - - switch (voiceCommand) - { - case 5: - LogHandler::verbose(Tags::Voice, "Custom Command: %ld", voiceCommand); - sendMessage("#motion-enable"); - char command[32]; - sprintf(command, "#motion-profile-set:%d", SettingsHandler::getMotionDefaultProfileIndex() + 1); - sendMessage(command); - break; - case 6: - LogHandler::verbose(Tags::Voice, "Custom Command: %d", voiceCommand); - sendMessage("#motion-enable"); - sendMessage("#motion-profile-set:1"); - break; - case 7: - LogHandler::verbose(Tags::Voice, "Custom Command: %d", voiceCommand); - sendMessage("#motion-enable"); - sendMessage("#motion-profile-set:2"); - break; - case 8: - LogHandler::verbose(Tags::Voice, "Custom Command: %d", voiceCommand); - sendMessage("#motion-enable"); - sendMessage("#motion-profile-set:3"); - break; - case 9: - LogHandler::verbose(Tags::Voice, "Custom Command: %d", voiceCommand); - sendMessage("#motion-enable"); - sendMessage("#motion-profile-set:4"); - break; - case 10: - LogHandler::verbose(Tags::Voice, "Custom Command: %d", voiceCommand); - sendMessage("#motion-enable"); - sendMessage("#motion-profile-set:5"); - break; - case 11: - LogHandler::verbose(Tags::Voice, "Custom Command: %d", voiceCommand); - sendMessage("#motion-disable"); - break; - - default: - if (voiceCommand == 1 || voiceCommand == 2) - { - LogHandler::verbose(Tags::Voice, "Wakup command: %d", voiceCommand); - } - else if (voiceCommand != 0) - { - LogHandler::verbose(Tags::Voice, "Command not used: %d", voiceCommand); - } - } - } - - void sendMessage(const char* message) - { - if (message_callback) - message_callback(message); - } -}; diff --git a/ESP32/src/WebHandler.h b/ESP32/src/WebHandler.h deleted file mode 100644 index 469e17d..0000000 --- a/ESP32/src/WebHandler.h +++ /dev/null @@ -1,572 +0,0 @@ -/* MIT License - -Copyright (c) 2026 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include -#include -#include -#if ESP8266 == 1 -#include -#else -#include -#endif -#include -#include "HTTP/HTTPBase.h" -#include "WifiHandler.h" -#include "WebSocketHandler.h" -#include "logging/TagHandler.h" -#include "SystemCommandHandler.h" -#if !CONFIG_HTTPD_WS_SUPPORT -#error This example cannot be used unless HTTPD_WS_SUPPORT is enabled in esp-http-server component configuration -#endif -class WebHandler : public HTTPBase -{ -public: - // bool MDNSInitialized = false; - void setup_http(uint16_t port, WebSocketBase *webSocketHandler, bool apMode) override - { - stop(); - if (port < 1 || port > 65535) - port = 80; - LogHandler::info(_TAG, "Starting web server on port: %i", port); - server = new AsyncWebServer(port); - ((WebSocketHandler *)webSocketHandler)->setup(server); - m_settingsFactory = SettingsFactory::getInstance(); - server->on("/wifiSettings", HTTP_GET, [](AsyncWebServerRequest *request) - { - char info[700]; - SettingsHandler::getWifiInfo(info); - if (strlen(info) == 0) { - AsyncWebServerResponse *response = request->beginResponse(504, "application/text", "Error getting wifi settings"); - request->send(response); - return; - } - AsyncWebServerResponse *response = request->beginResponse(200, "application/json", info); - request->send(response); }); - - server->on("/settings", HTTP_GET, [this](AsyncWebServerRequest *request) - { - // request->send(LittleFS, COMMON_SETTINGS_PATH, "application/json"); - sendChunked(request, COMMON_SETTINGS_PATH); }); - - server->on("/pins", HTTP_GET, [this](AsyncWebServerRequest *request) - { - request->send(LittleFS, PIN_SETTINGS_PATH, "application/json"); - // sendChunked(request, PIN_SETTINGS_PATH); - }); - - server->on("/systemInfo", HTTP_GET, [](AsyncWebServerRequest *request) - { - if(SettingsHandler::restartRequired > -1 || !SettingsHandler::initialized) { - AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"status\": \"restarting\"}"); - request->send(response); - return; - } - String systemInfo; - SettingsHandler::getSystemInfo(systemInfo); - if (!systemInfo.length()) { - AsyncWebServerResponse *response = request->beginResponse(504, "application/text", "Error getting user settings"); - request->send(response); - return; - } - AsyncWebServerResponse *response = request->beginResponse(200, "application/json", systemInfo.c_str()); - request->send(response); }); - - server->on("/motionProfiles", HTTP_GET, [this](AsyncWebServerRequest *request) - { - // request->send(LittleFS, MOTION_PROFILE_SETTINGS_PATH, "application/json"); - sendChunked(request, MOTION_PROFILE_SETTINGS_PATH); }); - - server->on("/channelsProfile", HTTP_GET, [this](AsyncWebServerRequest *request) - { - // request->send(LittleFS, CHANNELS_SETTINGS_PATH, "application/json"); - sendChunked(request, CHANNELS_SETTINGS_PATH); }); - - server->on("/buttonSettings", HTTP_GET, [this](AsyncWebServerRequest *request) - { - // request->send(LittleFS, BUTTON_SETTINGS_PATH, "application/json"); - sendChunked(request, BUTTON_SETTINGS_PATH); }); - - // server->on("/log", HTTP_GET, [this](AsyncWebServerRequest *request) - // { - // Serial.println("Get log..."); - // //request->send(LittleFS, LOG_PATH); - // sendChunked(request, LOG_PATH); - // }); - - // server->on("/connectWifi", HTTP_POST, [this](AsyncWebServerRequest *request) - // { - // WifiHandler wifi; - // //const size_t capacity = JSON_OBJECT_SIZE(2); - // JsonDocument doc; - // char ssid[SSID_LEN] = {0}; - // char pass[WIFI_PASS_LEN] = {0}; - // m_settingsFactory->getValue(SSID_SETTING, ssid, SSID_LEN); - // m_settingsFactory->getValue(WIFI_PASS_SETTING, pass, WIFI_PASS_LEN); - // if (wifi.connect(ssid, pass)) - // { - - // doc["connected"] = true; - // doc["IPAddress"] = wifi.ip().toString(); - // } - // else - // { - // doc["connected"] = false; - // doc["IPAddress"] = "0.0.0.0"; - - // } - // String output; - // serializeJson(doc, output); - // AsyncWebServerResponse *response = request->beginResponse(200, "application/json", output); - // request->send(response); - // }); - - // server->on("/toggleContinousTwist", HTTP_POST, [this](AsyncWebServerRequest *request) - // { - // m_settingsFactory->setValue(CONTINUOUS_TWIST, !m_settingsFactory->getContinuousTwist()); - // if (m_settingsFactory->saveCommon()) - // { - // char returnJson[45]; - // sprintf(returnJson, "{\"msg\":\"done\", \"continousTwist\":%s }", m_settingsFactory->getContinuousTwist() ? "true" : "false"); - // AsyncWebServerResponse *response = request->beginResponse(200, "application/json", returnJson); - // request->send(response); - // } - // else - // { - // AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"Error saving settings\"}"); - // request->send(response); - // } - // }); - - server->on("^\\/sensor\\/([0-9]+)$", HTTP_GET, [](AsyncWebServerRequest *request) - { String sensorId = request->pathArg(0); }); - - server->on("^\\/changeBoard\\/([0-9]+)$", HTTP_POST, [this](AsyncWebServerRequest *request) - { - auto boardTypeString = request->pathArg(0); - int boardType = boardTypeString.isEmpty() ? (int)BoardType::DEVKIT : boardTypeString.toInt(); - if(m_settingsFactory->changeBoardType(boardType)) - { - AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); - request->send(response); - SettingsHandler::restart(5); - } - else - { - AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"msg\":\"Error changing board type\"}"); - request->send(response); - } }); - server->on("^\\/changeDevice\\/([0-9]+)$", HTTP_POST, [this](AsyncWebServerRequest *request) - { - auto deviceTypeString = request->pathArg(0); - int deviceType = deviceTypeString.isEmpty() ? - #ifdef MOTOR_TYPE_SERVO - (int)DeviceType::OSR - #else - (int)DeviceType::NONE - #endif - : deviceTypeString.toInt(); - if (m_settingsFactory->changeDeviceType(deviceType)) - { - AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); - request->send(response); - SettingsHandler::restart(5); - } - else - { - AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"msg\":\"Error changing device type\"}"); - request->send(response); - } }); - - // upload a file to /upload - // server->on("/upload", HTTP_POST, [](AsyncWebServerRequest *request){ - // request->send(200); - // }, handleUpload);server->on("/reset", HTTP_POST, [](AsyncWebServerRequest *request){ - - server->on("/restart", HTTP_POST, [webSocketHandler, apMode](AsyncWebServerRequest *request) - { - //if(apMode) { - //request->send(200, "text/plain",String("Restarting device, wait about 10-20 seconds and navigate to ") + (SettingsHandler::getHostname()) + ".local or the network IP address in your browser address bar."); - //} - String message = "{\"msg\":\"restarting\",\"apMode\":"; - message += apMode ? "true}" : "false}"; - AsyncWebServerResponse *response = request->beginResponse(200, "application/json", message); - request->send(response); - webSocketHandler->closeAll(); - SettingsHandler::restart(2); }); - - server->on("/default", HTTP_POST, [this](AsyncWebServerRequest *request) - { - Serial.println("Settings default"); - if(m_settingsFactory->resetAll()) { - String message = "{\"msg\":\"restarting\",\"apMode\":"; - message += SettingsHandler::restart ? "true}" : "false}"; - AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); - request->send(response); - SettingsHandler::restart(5); - } else { - sendError(request); - } }); - - AsyncCallbackJsonWebHandler *settingsUpdateHandler = new AsyncCallbackJsonWebHandler("/settings", [this](AsyncWebServerRequest *request, JsonVariant &json) - { - Serial.println("API save settings..."); - JsonObject jsonObj = json.as(); - if (m_settingsFactory->saveCommon(jsonObj)) - { - AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); - request->send(response); - } - else - { - AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"msg\":\"Error saving settings\"}"); - request->send(response); - } }); //, 32768U );//Bad request? increase the size. - - AsyncCallbackJsonWebHandler *pinsHandler = new AsyncCallbackJsonWebHandler("/pins", [this](AsyncWebServerRequest *request, JsonVariant &json) - { - Serial.println("API save pins..."); - JsonObject jsonObj = json.as(); - if (m_settingsFactory->savePins(jsonObj)) - { - AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); - request->send(response); - } - else - { - AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"msg\":\"Error saving pins\"}"); - request->send(response); - } }); //, 1000U );//Bad request? increase the size. - - AsyncCallbackJsonWebHandler *wifiUpdateHandler = new AsyncCallbackJsonWebHandler("/wifiSettings", [this](AsyncWebServerRequest *request, JsonVariant &json) - { - Serial.println("API save wifi settings..."); - JsonObject jsonObj = json.as(); - if (m_settingsFactory->saveWifi(jsonObj)) - { - AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); - request->send(response); - } - else - { - AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"msg\":\"Error saving wifi settings\"}"); - request->send(response); - } }); //, 500U );//Bad request? increase the size. - - AsyncCallbackJsonWebHandler *motionProfileUpdateHandler = new AsyncCallbackJsonWebHandler("/motionProfiles", [](AsyncWebServerRequest *request, JsonVariant &json) - { - Serial.println("API save motion profiles..."); - JsonObject jsonObj = json.as(); - if (SettingsHandler::saveMotionProfiles(jsonObj)) - { - AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); - request->send(response); - } - else - { - AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"msg\":\"Error saving motion profiles\"}"); - request->send(response); - } }); //, 30000U );//Bad request? increase the size. - - AsyncCallbackJsonWebHandler *channelsProfileUpdateHandler = new AsyncCallbackJsonWebHandler("/channelsProfile", [](AsyncWebServerRequest *request, JsonVariant &json) - { - Serial.println("API save channels profile..."); - JsonObject jsonObj = json.as(); - if (SettingsHandler::saveChannels(jsonObj)) - { - AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); - request->send(response); - } - else - { - AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"msg\":\"Error saving channels profile\"}"); - request->send(response); - } }); //, 5000U );//Bad request? increase the size. - - AsyncCallbackJsonWebHandler *buttonsUpdateHandler = new AsyncCallbackJsonWebHandler("/buttonSettings", [](AsyncWebServerRequest *request, JsonVariant &json) - { - Serial.println("API save button settings..."); - JsonObject jsonObj = json.as(); - if (SettingsHandler::saveButtons(jsonObj)) - { - AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"done\"}"); - request->send(response); - } - else - { - AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"msg\":\"Error saving button settings\"}"); - request->send(response); - } }); //, 10000U );//Bad request? increase the size. - - // //To upload through terminal you can use: curl -F "image=@firmware.bin" esp8266-webupdate.local/update - // server->on("/update", HTTP_POST, [this](AsyncWebServerRequest *request){ - // // the request handler is triggered after the upload has finished... - // // create the response, add header, and send response - // AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", (Update.hasError())?"FAIL":"OK"); - // response->addHeader("Connection", "close"); - // response->addHeader("Access-Control-Allow-Origin", "*"); - // SettingsHandler::getRestartRequired() = true; - // request->send(response); - // },[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { - // //Upload handler chunks in data - - // if(!index){ // if index == 0 then this is the first frame of data - // Serial.printf("UploadStart: %s\n", filename.c_str()); - // Serial.setDebugOutput(true); - - // // calculate sketch space required for the update - // uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; - // if(!Update.begin(maxSketchSpace)){//start with max available size - // Update.printError(Serial); - // } - // Update.runAsync(true); // tell the updaterClass to run in async mode - // } - - // //Write chunked data to the free sketch space - // if(Update.write(data, len) != len){ - // Update.printError(Serial); - // } - - // if(final){ // if the final flag is set then this is the last frame of data - // if(Update.end(true)){ //true to set the size to the current progress - // Serial.printf("Update Success: %u B\nRebooting...\n", index+len); - // } else { - // Update.printError(Serial); - // } - // Serial.setDebugOutput(false); - // } - // }); - - server->addHandler(settingsUpdateHandler); - server->addHandler(pinsHandler); - server->addHandler(wifiUpdateHandler); - server->addHandler(motionProfileUpdateHandler); - server->addHandler(channelsProfileUpdateHandler); - server->addHandler(buttonsUpdateHandler); - - server->onNotFound([this](AsyncWebServerRequest *request) - { - if (handleStaticFile(request)) return; - Serial.printf("AsyncWebServerRequest Not found: %s\n", request->url().c_str()); - if (request->method() == AsyncWebRequestMethod::AsyncWebRequestMethodType::HTTP_OPTIONS) { - //if (request->method() == HTTP_OPTIONS) { - request->send(200); - } else { - AsyncWebServerResponse *response = request->beginResponse(404, "application/text", String("AsyncWebServerRequest Not found") + request->url()); - request->send(response); - } }); - // server->on("/", HTTP_GET, [this](AsyncWebServerRequest *request) - // { - // // request->send(LittleFS, COMMON_SETTINGS_PATH, "application/json"); - // Serial.println("index"); - // sendChunked(request, "/www/index-min.html", "text/html"); - // }); - //"^\\/pinoutDefault\\/([0-9]+)$" - // server->on("\\/.*\\.js", HTTP_GET, [this](AsyncWebServerRequest *request) - // { - // // request->send(LittleFS, COMMON_SETTINGS_PATH, "application/json"); - // const char* filename = request->pathArg(0).c_str(); - // Serial.printf("JS file: %s\n", filename); - // sendChunked(request, filename, "application/javascript"); - // }); - // server->on("/settings-min.js", HTTP_GET, [this](AsyncWebServerRequest *request) - // { - // sendChunked(request, "/www/settings-min.js", 4096, "text/javascript"); - // }); - // server->on("/motion-generator-min.js", HTTP_GET, [this](AsyncWebServerRequest *request) - // { - // sendChunked(request, "/www/motion-generator-min.js", 1024, "text/javascript"); - // }); - - // server->rewrite("/", "/wifiSettings.htm").setFilter(ON_AP_FILTER); - server->serveStatic("/", LittleFS, "/www/"); - // .setDefaultFile("index-min.html"); - // //.setCacheControl("max-age=60000"); - // //.setCacheControl("no-cache"); - server->begin(); - initialized = true; - } - void stop() override - { - if (initialized) - { - initialized = false; - server->end(); - } - // if(MDNSInitialized) - // { - // MDNS.end(); - // MDNSInitialized = false; - // } - } - bool isRunning() override - { - return initialized; - } - - void setup() override - { - } - - void loop() override - { - } - -private: - bool initialized = false; - static constexpr Tags::tag_t _TAG = Tags::Web; - SettingsFactory *m_settingsFactory; - AsyncWebServer *server; - - void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) - { - if (!index) - { - Serial.printf("UploadStart: %s\n", filename.c_str()); - } - for (size_t i = 0; i < len; i++) - { - Serial.write(data[i]); - } - if (final) - { - Serial.printf("UploadEnd: %s, %u B\n", filename.c_str(), index + len); - } - } - void sendError(AsyncWebServerRequest *request, int code = 500) - { - const char *lastError = LogHandler::getLastError(); - char responseMessage[1057]; - sprintf(responseMessage, "{\"msg\":\"Error: %s\"}", strlen(lastError) > 0 ? lastError : "Unknown error"); - AsyncWebServerResponse *response = request->beginResponse(code, "application/json", responseMessage); - request->send(response); - } - - void sendChunked(AsyncWebServerRequest *request, const char *filePath, const char *mimeType = "application/json", const bool &isGZip = false) - { - LogHandler::debug(_TAG, "[sendChunked] Open file: %s\n", filePath); - File file{LittleFS.open(filePath, FILE_READ)}; - - AsyncWebServerResponse *response = request->beginChunkedResponse( - mimeType, - [this, file]( - uint8_t *buffer, - const size_t max_len, - const size_t index) mutable -> size_t - { - LogHandler::debug(_TAG, "[beginChunkedResponse] Enter chunked file: %s\n", file.name()); - size_t length; - - // Restrict chunk size so we don't run out of RAM - // static const size_t max_chunk{chunkSize}; - // if (max_chunk < max_len) - // { - // LogHandler::debug(_TAG,"Max chunk %u Max len %u for: %s\n", chunkSize, max_len, file.name()); - // length = file.read(buffer, max_chunk); - // } - // else - // { - // LogHandler::debug(_TAG,"Max len %u exceded max chunk %u for: %s\n", max_len, chunkSize, file.name()); - length = file.read(buffer, max_len); - // } - - if (length == 0) - { - LogHandler::debug(_TAG, "[beginChunkedResponse] Close file: %s\n", file.name()); - file.close(); - } - - return length; - }); - - // Force download - // response->addHeader("Content-Disposition", "attachment; filename=\"userSettings.json\""); - if (isGZip) - response->addHeader("Content-Encoding", "gzip"); - request->send(response); - } - - bool handleStaticFile(AsyncWebServerRequest *request) - { - String requestUrl = request->url(); - LogHandler::debug(_TAG, "[handleStaticFile] requet url: %s", requestUrl); - String path = "/www" + requestUrl; - LogHandler::debug(_TAG, "[handleStaticFile] static path: %s", path); - - if (path.endsWith("/")) - path += F("index-min.html"); - String mimeType; - String pathWithGz = path + ".gz"; - - if (LittleFS.exists(pathWithGz) || LittleFS.exists(path)) - { - bool gzipped = false; - if (LittleFS.exists(pathWithGz)) - { - gzipped = true; - path += ".gz"; - } - // else - // { - if (path.endsWith(".html")) - { - mimeType = "text/html"; - } - else if (path.endsWith(".js")) - { - mimeType = "text/javascript"; - } - else if (path.endsWith(".json")) - { - mimeType = "application/json"; - } - else if (path.endsWith(".css")) - { - mimeType = "text/css"; - } - // } - sendChunked(request, path.c_str(), mimeType.c_str(), gzipped); - - return true; - } - - return false; - } - // void startMDNS(char* hostName, char* friendlyName) - // { - // if(MDNSInitialized) - // MDNS.end(); - // Serial.print("hostName: "); - // Serial.println(hostName); - // Serial.print("friendlyName: "); - // Serial.println(friendlyName); - // if (!MDNS.begin(hostName)) { - // printf("MDNS Init failed"); - // return; - // } - // MDNS.setInstanceName(friendlyName); - // MDNS.addService("http", "tcp", 80); - // MDNS.addService("tcode", "udp", SettingsHandler::getUdpServerPort()); - // MDNSInitialized = true; - // } -}; diff --git a/ESP32/src/WebHandler_psychic.h b/ESP32/src/WebHandler_psychic.h deleted file mode 100644 index 238a1b7..0000000 --- a/ESP32/src/WebHandler_psychic.h +++ /dev/null @@ -1,337 +0,0 @@ -/* MIT License - -Copyright (c) 2026 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#include -#include -#include -// #if ESP8266 == 1 -// #include -// #else -// #include -// #endif -// #include -#include "HTTP/HTTPBase.h" -#include "network/WifiHandler.h" -#include "WebSocketHandler_psychic.h" -#include "logging/TagHandler.h" -#include "messages/SystemCommandHandler.h" -// #if !CONFIG_HTTPD_WS_SUPPORT -// #error This example cannot be used unless HTTPD_WS_SUPPORT is enabled in esp-http-server component configuration -// #endif -class WebHandler : public HTTPBase, public Task -{ -public: - WebHandler() : Task(TaskHandler::Rates::ONDEMAND) {} - // bool MDNSInitialized = false; - void setup_http(int port, WebSocketBase *webSocketHandler, bool apMode) override - { - stop(); - if (port < 1) - port = 80; - LogHandler::info(Tags::Web, "Starting psychic web server on port: %i", port); - server = new PsychicHttpServer(); - m_settingsFactory = SettingsFactory::getInstance(); - - server->listen(port); - ((WebSocketHandler *)webSocketHandler)->setup(server); - - server->on("/wifiSettings", HTTP_GET, [](PsychicRequest *request) - { - char info[1024]; - SettingsHandler::getWifiInfo(info, sizeof(info)); - if (strlen(info) == 0) { - return request->reply(504, "application/text", "Error getting wifi settings"); - } - return request->reply(200, "application/json", info); }); - - server->on("/pins", HTTP_GET, [](PsychicRequest *request) - { - PsychicFileResponse response(request, LittleFS, PIN_SETTINGS_PATH, "application/json"); - return response.send(); }); - - server->on("/settings", HTTP_GET, [](PsychicRequest *request) - { - PsychicFileResponse response(request, LittleFS, COMMON_SETTINGS_PATH, "application/json"); - return response.send(); }); - - server->on("/systemInfo", HTTP_GET, [](PsychicRequest *request) - { - String systemInfo; - SettingsHandler::getSystemInfo(systemInfo); - if (systemInfo.length() == 0) { - return request->reply(504, "application/text", "Error getting user settings"); - } - return request->reply(200, "application/json", systemInfo.c_str()); - // systemInfo String is freed here after reply copies the data - }); - - server->on("/motionProfiles", HTTP_GET, [](PsychicRequest *request) - { - PsychicFileResponse response(request, LittleFS, MOTION_PROFILE_SETTINGS_PATH, "application/json"); - return response.send(); }); - - server->on("/buttonSettings", HTTP_GET, [](PsychicRequest *request) - { - PsychicFileResponse response(request, LittleFS, BUTTON_SETTINGS_PATH, "application/json"); - return response.send(); }); - - server->on("/log", HTTP_GET, [](PsychicRequest *request) - { - Serial.println("Get log..."); - PsychicFileResponse response(request, LittleFS, LOG_PATH); - return response.send(); }); - - server->on("/connectWifi", HTTP_POST, [this](PsychicRequest *request) - { - WifiHandler wifi; - //const size_t capacity = JSON_OBJECT_SIZE(2); - JsonDocument doc; - char ssid[SSID_LEN] = {0}; - char pass[WIFI_PASS_LEN] = {0}; - m_settingsFactory->getValue(SSID_SETTING, ssid, SSID_LEN); - m_settingsFactory->getValue(WIFI_PASS_SETTING, pass, WIFI_PASS_LEN); - if (wifi.connect(ssid, pass)) - { - - doc["connected"] = true; - doc["IPAddress"] = wifi.ip().toString(); - } - else - { - doc["connected"] = false; - doc["IPAddress"] = "0.0.0.0"; - - } - String output; - serializeJson(doc, output); - //PsychicResponse *response = request->beginResponse(200, "application/json", output); - return request->reply(200, "application/json", output.c_str()); }); - - server->on("/toggleContinousTwist", HTTP_POST, [this](PsychicRequest *request) - { - m_settingsFactory->setValue(CONTINUOUS_TWIST, !m_settingsFactory->getContinuousTwist()); - if (m_settingsFactory->saveCommon()) - { - char returnJson[45]; - sprintf(returnJson, "{\"msg\":\"done\", \"continousTwist\":%s }", m_settingsFactory->getContinuousTwist() ? "true" : "false"); - //PsychicResponse *response = request->beginResponse(200, "application/json", returnJson); - return request->reply(200, "application/json", returnJson); - } - else - { - //PsychicResponse *response = request->beginResponse(200, "application/json", "{\"msg\":\"Error saving settings\"}"); - return request->reply(200, "application/json", "{\"msg\":\"Error saving settings\"}"); - } }); - - // server->on("^\\/sensor\\/([0-9]+)$", HTTP_GET, [] (PsychicRequest *request) - // { - // String sensorId = request->pathArg(0); - // }); - - server->on("^\\/pinoutDefault\\/([0-9]+)$", HTTP_POST, [this](PsychicRequest *request) - { - auto boardType = request->getParam(""); - String boardTypeString = boardType->value(); - int boardTypeInt = boardTypeString.isEmpty() ? (int)BoardType::DEVKIT : boardTypeString.toInt(); - Serial.println("Settings pinout default"); - m_settingsFactory->setValue(BOARD_TYPE_SETTING, boardTypeInt); - if(SettingsHandler::defaultPinout()) - //if (m_settingsFactory->resetPins()) // Settings handler executes resetPins - { - return request->reply(200, "application/json", "{\"msg\":\"done\"}"); - } - else - { - //PsychicResponse *response = request->beginResponse(500, "application/json", "{\"msg\":\"Error defaulting pinout settings\"}"); - return request->reply(500, "application/json", "{\"msg\":\"Error defaulting pinout settings\"}"); - } }); - - // upload a file to /upload - // server->on("/upload", HTTP_POST, [](PsychicRequest *request){ - // request->send(200); - // }, handleUpload);server->on("/reset", HTTP_POST, [](PsychicRequest *request){ - - server->on("/restart", HTTP_POST, [webSocketHandler, apMode](PsychicRequest *request) - { - //if(apMode) { - //request->send(200, "text/plain",String("Restarting device, wait about 10-20 seconds and navigate to ") + (SettingsHandler::hostname) + ".local or the network IP address in your browser address bar."); - //} - String message = "{\"msg\":\"restarting\",\"apMode\":"; - message += apMode ? "true}" : "false}"; - //PsychicResponse *response = request->beginResponse(200, "application/json", message); - auto value = request->reply(200, "application/json", message.c_str()); - delay(2000); - webSocketHandler->closeAll(); - SettingsHandler::restart(); - return value; }); - - server->on("/default", HTTP_POST, [this](PsychicRequest *request) - { - Serial.println("Settings default"); - if(m_settingsFactory->resetAll()) { - esp_err_t ret = request->reply(200, "application/json", "{\"msg\":\"done\"}"); - SettingsHandler::restart(5); - return ret; - } else { - return sendError(request); - } }); - - server->on("/settings", HTTP_POST, [this](PsychicRequest *request, JsonVariant &json) - { - Serial.println("API save settings..."); - JsonObject jsonObj = json.as(); - if (m_settingsFactory->saveCommon(jsonObj)) - { - return request->reply(200, "application/json", "{\"msg\":\"done\"}"); - } - return request->reply(500, "application/json", "{\"msg\":\"Error saving settings\"}"); }); - - server->on("/pins", [this](PsychicRequest *request, JsonVariant &json) - { - Serial.println("API save pins..."); - JsonObject jsonObj = json.as(); - if (m_settingsFactory->savePins(jsonObj)) - { - return request->reply(200, "application/json", "{\"msg\":\"done\"}"); - } - return request->reply(500, "application/json", "{\"msg\":\"Error saving pin settings\"}"); }); - - server->on("/wifiSettings", [this](PsychicRequest *request, JsonVariant &json) - { - Serial.println("API save wifi settings..."); - JsonObject jsonObj = json.as(); - if (m_settingsFactory->saveWifi(jsonObj)) - { - return request->reply(200, "application/json", "{\"msg\":\"done\"}"); - } - return request->reply(500, "application/json", "{\"msg\":\"Error saving wifi settings\"}"); }); - - server->on("/motionProfiles", [this](PsychicRequest *request, JsonVariant &json) - { - Serial.println("API save motion profiles..."); - JsonObject jsonObj = json.as(); - if (SettingsHandler::saveMotionProfiles(jsonObj)) - { - return request->reply(200, "application/json", "{\"msg\":\"done\"}"); - } - return request->reply(500, "application/json", "{\"msg\":\"Error saving motion profiles settings\"}"); }); - - server->on("/buttonSettings", [this](PsychicRequest *request, JsonVariant &json) - { - Serial.println("API save button settings..."); - JsonObject jsonObj = json.as(); - if (SettingsHandler::saveButtons(jsonObj)) - { - return request->reply(200, "application/json", "{\"msg\":\"done\"}"); - } - return request->reply(500, "application/json", "{\"msg\":\"Error saving button settings\"}"); }); - - server->onNotFound([](PsychicRequest *request) - { - Serial.printf("PsychicRequest Not found: %s", request->url()); - if (request->method() == HTTP_OPTIONS) { - return request->reply(200); - } else { - //PsychicResponse *response = request->beginResponse(404, "application/text", String("PsychicRequest Not found") + request->url()); - return request->reply(404, "application/text", (String("PsychicRequest Not found") + request->url()).c_str()); - } }); - // server->rewrite("/", "/wifiSettings.htm").setFilter(ON_AP_FILTER); - server->serveStatic("/", LittleFS, "/www/")->setDefaultFile("index-min.html"); - // server->s; - initialized = true; - } - - void stop() override - { - if (initialized) - { - initialized = false; - server->stop(); - } - // if(MDNSInitialized) - // { - // MDNS.end(); - // MDNSInitialized = false; - // } - } - - bool isRunning() override - { - return initialized; - } - - bool setup() override - { - } - - void loop() override - { - } - -private: - bool initialized = false; - SettingsFactory *m_settingsFactory; - PsychicHttpServer *server; - - void handleUpload(PsychicRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) - { - if (!index) - { - Serial.printf("UploadStart: %s\n", filename.c_str()); - } - for (size_t i = 0; i < len; i++) - { - Serial.write(data[i]); - } - if (final) - { - Serial.printf("UploadEnd: %s, %u B\n", filename.c_str(), index + len); - } - } - esp_err_t sendError(PsychicRequest *request, int code = 500) - { - const char *lastError = LogHandler::getLastError(); - char responseMessage[1024]; - sprintf(responseMessage, "{\"msg\":\"Error setting default: %s\"}", strlen(lastError) > 0 ? lastError : "Unknown error"); - // PsychicResponse *response = request->beginResponse(code, "application/json", responseMessage); - return request->reply(code, "application/json", responseMessage); - } - // void startMDNS(char* hostName, char* friendlyName) - // { - // if(MDNSInitialized) - // MDNS.end(); - // Serial.print("hostName: "); - // Serial.println(hostName); - // Serial.print("friendlyName: "); - // Serial.println(friendlyName); - // if (!MDNS.begin(hostName)) { - // printf("MDNS Init failed"); - // return; - // } - // MDNS.setInstanceName(friendlyName); - // MDNS.addService("http", "tcp", 80); - // MDNS.addService("tcode", "udp", SettingsHandler::udpServerPort); - // MDNSInitialized = true; - // } -}; diff --git a/ESP32/src/WebSocketHandler.h b/ESP32/src/WebSocketHandler.h deleted file mode 100644 index 58733d3..0000000 --- a/ESP32/src/WebSocketHandler.h +++ /dev/null @@ -1,308 +0,0 @@ -/* MIT License - -Copyright (c) 2026 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -// #include -#include -#include -#include -#include -#include "HTTP/WebSocketBase.h" -#include "SettingsHandler.h" -// #include "LogHandler.h" -#include "logging/TagHandler.h" -#include "BatteryHandler.h" - -AsyncWebSocket ws("/ws"); - -struct WebSocketCommand -{ - const char *command; - const char *message; -}; - -class WebSocketHandler : public WebSocketBase -{ -public: - void setup(AsyncWebServer *server) - { - LogHandler::info(_TAG, "Setting up webSocket"); - ws.onEvent([&](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) - { onWsEvent(server, client, type, arg, data, len); }); - server->addHandler(&ws); - tCodeInQueue = xQueueCreate(25, sizeof(char[MAX_COMMAND])); - if (tCodeInQueue == NULL) - { - LogHandler::error(_TAG, "Error creating the tcode queue"); - } - // debugInQueue = xQueueCreate(10, sizeof(char[MAX_COMMAND])); - // if(debugInQueue == NULL) { - // LogHandler::error(_TAG, "Error creating the debug queue"); - // } - // xTaskCreate(this->emptyQueue, "emptyDebugQueue", 2042, this, tskIDLE_PRIORITY, emptyQueueHandle); - isInitialized = true; - } - - void CommandCallback(const char *in) override - { // This overwrites the callback for message return - if (isInitialized && ws.count() > 0) - sendCommand(in); - } - - // void sendDebug(const char* message, LogLevel level) { - // if (level != LogLevel::VERBOSE && isInitialized && debugInQueue != NULL && uxQueueMessagesWaiting(debugInQueue) < 10 && serial_mtx.try_lock()) { - // std::lock_guard lck(serial_mtx, std::adopt_lock); - // // char messageToSend[MAX_COMMAND]; - // // if(sizeof(message) > 253) { - // // strncpy(messageToSend, message, 253); - // // messageToSend[254] = '\0'; - // // Serial.println("truncated"); - // // } else { - // // strcpy(messageToSend, message); - // // messageToSend[strlen(message)] = '\0'; - // // } - // // if(level >= LogLevel::DEBUG) { - // // Serial.print("insert to q: "); - // // Serial.println(message); - // // } - // xQueueSend(debugInQueue, message, 0); - // } - // } - - void sendCommand(const char *command, const char *message = 0) override - { - if (isInitialized && command_mtx.try_lock()) - { - std::lock_guard lck(command_mtx, std::adopt_lock); - m_lastSend = millis(); - - const size_t messageLen = message ? strlen(message) : 0; - const size_t required = strlen(command) + messageLen + 64; - std::string commandJson(required, '\0'); - compileCommand(commandJson.data(), commandJson.size(), command, message); - // if(client) - // client->text(commandJson); - // else - ws.textAll(commandJson.c_str()); - } - } - - // // Did not work last I tried it. Gave up. - // // template - // // void sendCommands(WebSocketCommand (&commands)[N], AsyncWebSocketClient* client = 0) - // // { - // // if(isInitialized && command_mtx.try_lock()) { - // // std::lock_guard lck(command_mtx, std::adopt_lock); - // // m_lastSend = millis(); - - // // char commandsJson[MAX_COMMAND]; - // // std::strcat(commandsJson, "["); - // // for (int i = 0; i < N; i++) - // // { - // // if(commands[i].command) { - // // char commandJson[128]; - // // compileCommand(commandJson, commands[i].command, commands[i].message); - // // Serial.print("compileCommand: "); - // // Serial.println(commandJson); - // // std::strcat(commandsJson, commandJson); - // // if(i < N-1) - // // std::strcat(commandsJson, ","); - // // } - // // } - // // std::strcat(commandsJson, "]"); - // // Serial.print("commandsJson: "); - // // Serial.println(commandsJson); - // // if(client) - // // client->text(commandsJson); - // // else - // // ws.textAll(commandsJson); - // // } - // // } - - // void getTCode(char* webSocketData) - // { - // if(tCodeInQueue == NULL) - // { - // LogHandler::error(_TAG, "TCode queue was null"); - // return; - // } - // if(xQueueReceive(tCodeInQueue, webSocketData, 0)) - // { - // //tcode->toCharArray(webSocketData, tcode->length() + 1); - // // Serial.print("Top tcode: "); - // // Serial.println(webSocketData); - // } - // else - // { - // webSocketData[0] = {0}; - // } - // ws.cleanupClients(); - // } - - void closeAll() override - { - for (AsyncWebSocketClient *pClient : m_clients) - pClient->close(); - } - -private: - bool isInitialized = false; - // std::mutex serial_mtx; - // std::mutex command_mtx; - static constexpr Tags::tag_t _TAG = Tags::WebSocketServer; - // unsigned long lastCall; - std::list m_clients; - // QueueHandle_t tCodeInQueue; - // static QueueHandle_t debugInQueue; - static int m_lastSend; - // static TaskHandle_t* emptyQueueHandle; - // static bool emptyQueueRunning; - - // static void emptyQueue(void *webSocketHandler) { - // while (true) { - // if(ws.count() > 0 && millis() - m_lastSend > 50 && uxQueueMessagesWaiting(debugInQueue)) { - // char lastMessage[MAX_COMMAND]; - // if(xQueueReceive(debugInQueue, lastMessage, 0)) { - // if(LogHandler::getLogLevel() == LogLevel::VERBOSE) - // Serial.printf("read from q: %s\n", lastMessage); - // ((WebSocketHandler*)webSocketHandler)->sendCommand("debug", lastMessage); - // } - // } - // vTaskDelay(100/portTICK_PERIOD_MS); - // } - // vTaskDelete(NULL); - // } - - void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) - { - if (type == WS_EVT_CONNECT) - { - LogHandler::debug(_TAG, "ws[%s][%u] connect\n", server->url(), client->id()); - // client->printf("Hello Client %u :)", client->id()); - // client->ping(); - // client->client()->setNoDelay(true); - m_clients.push_back(client); - } - else if (type == WS_EVT_DISCONNECT) - { - LogHandler::debug(_TAG, "ws[%s][%u] disconnect\n", server->url(), client->id()); - client->close(); - m_clients.remove(client); - } - else if (type == WS_EVT_ERROR) - { - LogHandler::debug(_TAG, "ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t *)arg), (char *)data); - } - else if (type == WS_EVT_PONG) - { - LogHandler::debug(_TAG, "ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len) ? (char *)data : ""); - } - else if (type == WS_EVT_DATA) - { - AwsFrameInfo *info = (AwsFrameInfo *)arg; - // String msg = ""; - if (info->final && info->index == 0 && info->len == len) - { - // the whole message is in a single frame and we got all of it's data - // Serial.printf("ws[%s][%u] %s-message[%llu]: ", server->url(), client->id(), (info->opcode == WS_TEXT)?"text":"binary", info->len); - - // if(info->opcode == WS_TEXT) - // { - // for(size_t i=0; i < info->len; i++) - // { - // msg += (char) data[i]; - // } - // } - // else - // { - // char buff[3]; - // for(size_t i=0; i < info->len; i++) - // { - // sprintf(buff, "%02x ", (uint8_t) data[i]); - // msg += buff ; - // } - // } - // Serial.printf("%s\n",msg.c_str()); - - if (info->opcode == WS_TEXT) - { - data[len] = 0; - processWebSocketTextMessage((char *)data); - } - else - client->binary("I got your binary message"); - } - else - { - // message is comprised of multiple frames or the frame is split into multiple packets - // if(info->index == 0) - // { - // if(info->num == 0) - // Serial.printf("ws[%s][%u] %s-message start\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary"); - // Serial.printf("ws[%s][%u] frame[%u] start[%llu]\n", server->url(), client->id(), info->num, info->len); - // } - - // Serial.printf("ws[%s][%u] frame[%u] %s[%llu - %llu]: ", server->url(), client->id(), info->num, (info->message_opcode == WS_TEXT)?"text":"binary", info->index, info->index + len); - - // if(info->opcode == WS_TEXT) - // { - // for(size_t i=0; i < len; i++) - // { - // msg += (char) data[i]; - // } - // } - // else - // { - // char buff[3]; - // for(size_t i=0; i < len; i++) - // { - // sprintf(buff, "%02x ", (uint8_t) data[i]); - // msg += buff ; - // } - // } - // Serial.printf("%s\n",msg.c_str()); - - if ((info->index + len) == info->len) - { - // Serial.printf("ws[%s][%u] frame[%u] end[%llu]\n", server->url(), client->id(), info->num, info->len); - if (info->final) - { - // Serial.printf("ws[%s][%u] %s-message end\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary"); - if (info->message_opcode == WS_TEXT) - { - data[len] = 0; - processWebSocketTextMessage((char *)data); - } - else - client->binary("I got your binary message"); - } - } - } - } - } -}; - -// bool WebSocketHandler::emptyQueueRunning = false; -// QueueHandle_t WebSocketHandler::debugInQueue; -int WebSocketHandler::m_lastSend = 0; -// TaskHandle_t* WebSocketHandler::emptyQueueHandle = NULL; \ No newline at end of file diff --git a/ESP32/src/WebSocketHandler_psychic.h b/ESP32/src/WebSocketHandler_psychic.h deleted file mode 100644 index 52352f5..0000000 --- a/ESP32/src/WebSocketHandler_psychic.h +++ /dev/null @@ -1,215 +0,0 @@ -/* MIT License - -Copyright (c) 2026 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -// #include -#include -#include -#include -#include -// #include "HTTP/WebSocketBase.h" -#include "settings/SettingsHandler.h" -// #include "LogHandler.h" -#include "logging/TagHandler.h" -#include "sensors/BatteryHandler.h" - -PsychicWebSocketHandler ws; - -struct WebSocketCommand -{ - const char *command; - const char *message; -}; -//("/ws") -class WebSocketHandler : public WebSocketBase -{ -public: - void setup(PsychicHttpServer *server) - { - LogHandler::info(_TAG, "Setting up webSocket"); - // ws.onEvent([&](AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len) { - // onWsEvent(server, client, type, arg, data, len); - // }); - tCodeInQueue = xQueueCreate(5, sizeof(char[MAX_COMMAND])); - if (tCodeInQueue == NULL) - { - LogHandler::error(_TAG, "Error creating the tcode queue"); - } - ws.onOpen([this](PsychicWebSocketClient *client) - { - LogHandler::info(_TAG, "Connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str()); - client->sendMessage("Hello!"); - m_clients.push_back(client); }); - ws.onFrame([this](PsychicWebSocketRequest *request, httpd_ws_frame *frame) - { - LogHandler::info(_TAG, "[socket] #%d sent: %s\n", request->client()->socket(), (char *)frame->payload); - processWebSocketTextMessage((char *)frame->payload); - return request->reply(frame); - }); - ws.onClose([this](PsychicWebSocketClient *client) - { - LogHandler::info(_TAG, "Connection #%u closed from %s\n", client->socket(), client->remoteIP().toString().c_str()); - m_clients.remove(client); }); - server->on("/ws")->setHandler(&ws); - // debugInQueue = xQueueCreate(10, sizeof(char[MAX_COMMAND])); - // if(debugInQueue == NULL) { - // LogHandler::error(_TAG, "Error creating the debug queue"); - // } - // xTaskCreate(this->emptyQueue, "emptyDebugQueue", 2042, this, tskIDLE_PRIORITY, emptyQueueHandle); - isInitialized = true; - } - - void CommandCallback(const char *in) override - { // This overwrites the callback for message return - if (isInitialized && ws.count() > 0) - sendCommand(in); - } - - // void sendDebug(const char* message, LogLevel level) { - // if (level != LogLevel::VERBOSE && isInitialized && debugInQueue != NULL && uxQueueMessagesWaiting(debugInQueue) < 10 && serial_mtx.try_lock()) { - // std::lock_guard lck(serial_mtx, std::adopt_lock); - // // char messageToSend[MAX_COMMAND]; - // // if(sizeof(message) > 253) { - // // strncpy(messageToSend, message, 253); - // // messageToSend[254] = '\0'; - // // Serial.println("truncated"); - // // } else { - // // strcpy(messageToSend, message); - // // messageToSend[strlen(message)] = '\0'; - // // } - // // if(level >= LogLevel::DEBUG) { - // // Serial.print("insert to q: "); - // // Serial.println(message); - // // } - // xQueueSend(debugInQueue, message, 0); - // } - // } - - void sendCommand(const char *command, const char *message = 0) override - { - if (isInitialized && command_mtx.try_lock()) - { - std::lock_guard lck(command_mtx, std::adopt_lock); - m_lastSend = millis(); - - const size_t messageLen = message ? strlen(message) : 0; - const size_t required = strlen(command) + messageLen + 64; - std::string commandJson(required, '\0'); - compileCommand(commandJson.data(), commandJson.size(), command, message); - // if(client) - // client->text(commandJson); - // else - ws.sendAll(commandJson.c_str()); - } - } - - // // Did not work last I tried it. Gave up. - // // template - // // void sendCommands(WebSocketCommand (&commands)[N], AsyncWebSocketClient* client = 0) - // // { - // // if(isInitialized && command_mtx.try_lock()) { - // // std::lock_guard lck(command_mtx, std::adopt_lock); - // // m_lastSend = millis(); - - // // char commandsJson[MAX_COMMAND]; - // // std::strcat(commandsJson, "["); - // // for (int i = 0; i < N; i++) - // // { - // // if(commands[i].command) { - // // char commandJson[128]; - // // compileCommand(commandJson, commands[i].command, commands[i].message); - // // Serial.print("compileCommand: "); - // // Serial.println(commandJson); - // // std::strcat(commandsJson, commandJson); - // // if(i < N-1) - // // std::strcat(commandsJson, ","); - // // } - // // } - // // std::strcat(commandsJson, "]"); - // // Serial.print("commandsJson: "); - // // Serial.println(commandsJson); - // // if(client) - // // client->text(commandsJson); - // // else - // // ws.textAll(commandsJson); - // // } - // // } - - // void getTCode(char* webSocketData) - // { - // if(tCodeInQueue == NULL) - // { - // LogHandler::error(_TAG, "TCode queue was null"); - // return; - // } - // if(xQueueReceive(tCodeInQueue, webSocketData, 0)) - // { - // //tcode->toCharArray(webSocketData, tcode->length() + 1); - // // Serial.print("Top tcode: "); - // // Serial.println(webSocketData); - // } - // else - // { - // webSocketData[0] = {0}; - // } - // ws.cleanupClients(); - // } - - void closeAll() override - { - for (PsychicWebSocketClient *pClient : m_clients) - pClient->close(); - } - -private: - bool isInitialized = false; - // std::mutex serial_mtx; - // std::mutex command_mtx; - static constexpr Tags::tag_t _TAG = Tags::WebSocketServer; - // unsigned long lastCall; - std::list m_clients; - // QueueHandle_t tCodeInQueue; - // static QueueHandle_t debugInQueue; - static int m_lastSend; - // static TaskHandle_t* emptyQueueHandle; - // static bool emptyQueueRunning; - - // static void emptyQueue(void *webSocketHandler) { - // while (true) { - // if(ws.count() > 0 && millis() - m_lastSend > 50 && uxQueueMessagesWaiting(debugInQueue)) { - // char lastMessage[MAX_COMMAND]; - // if(xQueueReceive(debugInQueue, lastMessage, 0)) { - // if(LogHandler::getLogLevel() == LogLevel::VERBOSE) - // Serial.printf("read from q: %s\n", lastMessage); - // ((WebSocketHandler*)webSocketHandler)->sendCommand("debug", lastMessage); - // } - // } - // vTaskDelay(100/portTICK_PERIOD_MS); - // } - // vTaskDelete(NULL); - // } -}; -// bool WebSocketHandler::emptyQueueRunning = false; -// QueueHandle_t WebSocketHandler::debugInQueue; -int WebSocketHandler::m_lastSend = 0; -// TaskHandle_t* WebSocketHandler::emptyQueueHandle = NULL; \ No newline at end of file diff --git a/ESP32/src/WifiHandler.h b/ESP32/src/WifiHandler.h deleted file mode 100644 index 621c482..0000000 --- a/ESP32/src/WifiHandler.h +++ /dev/null @@ -1,432 +0,0 @@ -/* MIT License - -Copyright (c) 2026 Jason C. Fain - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -#pragma once - -#if ESP8266 == 1 -#include -#else -#include -#include -#endif -// // #include "LogHandler.h" -#include "settings/SettingsHandler.h" -#include "logging/TagHandler.h" -#include "tasks/TaskHandler.h" - -enum class WiFiStatus -{ - CONNECTED, - DISCONNECTED, - IP -}; -enum class WiFiReason -{ - UNKNOWN, - AUTH, - NO_AP, - AP_MODE -}; -using WIFI_STATUS_FUNCTION_PTR_T = void (*)(WiFiStatus status, WiFiReason reason); -class WifiHandler : public TaskHandler::Task -{ -public: - WifiHandler() : Task(TaskHandler::Rates::ONDEMAND) {} - ~WifiHandler() - { - if (onApEventID != 0) - WiFi.removeEvent(onApEventID); - } - static bool isConnected() - { - return WiFi.isConnected(); - } - - IPAddress ip() - { - return WiFi.localIP(); - } - int8_t RSSI() - { - return WiFi.RSSI(); - } - static int8_t getRSSI() - { - return WiFi.RSSI(); - } - static bool apMode() - { - return _apMode; - } - bool connect(const char* ssid, const char* pass) - { - LogHandler::info(Tags::Wifi, "Setting up wifi"); - m_settingsFactory = SettingsFactory::getInstance(); - _apMode = false; - m_connectingInProgress = true; - // Serial.println("Setting mode"); - // if (onApEventID != 0) - // WiFi.removeEvent(onApEventID); - if (onApEventID != 0) - WiFi.removeEvent(onApEventID); - onApEventID = WiFi.onEvent([this](arduino_event_id_t event, arduino_event_info_t info) - { this->WiFiEvent(event, info); }); - WiFi.mode(WIFI_STA); - WifiBand band = (WifiBand)WIFI_BAND_SETTING_DEFAULT; - m_settingsFactory->getValue(WIFI_BAND_SETTING, band); - WiFi.setBandMode(toWiFiBandMode(band)); - WiFi.setSleep(false); - const char* hostname = m_settingsFactory->getHostname(); - if (!WiFi.setHostname(hostname)) - { - LogHandler::warning(Tags::Wifi, "Failed to apply hostname '%s'; using existing/default hostname", hostname); - } - else - { - LogHandler::info(Tags::Wifi, "Using hostname: %s", hostname); - } - bool isStatic = false; - m_settingsFactory->getValue(STATICIP, isStatic); - if (isStatic) - { - const char* ipAddressString = m_settingsFactory->getValue(LOCALIP); - LogHandler::info(Tags::Wifi, "Setting static IP settings: %s", ipAddressString); - IPAddress ipAddress; - if (!ipAddress.fromString(ipAddressString)) - { - LogHandler::error(Tags::Wifi, "Invalid static IP address: %s", ipAddressString); - return false; - } - IPAddress gateway; - const char* gatewayString = m_settingsFactory->getValue(GATEWAY); - if (!gateway.fromString(gatewayString)) - { - LogHandler::error(Tags::Wifi, "Invalid static gateway address: %s", gatewayString); - return false; - } - IPAddress subnet; - const char* subnetString = m_settingsFactory->getValue(SUBNET); - if (!subnet.fromString(subnetString)) - { - LogHandler::error(Tags::Wifi, "Invalid static subnet address: %s", subnetString); - return false; - } - IPAddress dns1 = (uint32_t)0; - const char* dns1String = m_settingsFactory->getValue(DNS1); - if (strlen(dns1String) > 0 && !dns1.fromString(dns1String)) - { - LogHandler::error(Tags::Wifi, "Invalid static dns1 address: %s", dns1String); - return false; - } - IPAddress dns2 = (uint32_t)0; - const char* dns2String = m_settingsFactory->getValue(DNS2); - if (strlen(dns2String) > 0 && !dns2.fromString(dns2String)) - { - LogHandler::error(Tags::Wifi, "Invalid static dns2 address: %s", dns2String); - return false; - } - - WiFi.config(ipAddress, gateway, subnet, dns1, dns2); - } - printMac(); - LogHandler::info(Tags::Wifi, "Establishing connection to %s", ssid); - if (WiFi.status() == WL_CONNECTED && strcmp(WiFi.SSID().c_str(), ssid) == 0) - { - LogHandler::info(Tags::Wifi, "Already connected to %s, reconnecting to renew DHCP hostname", ssid); - WiFi.disconnect(false, false); - } - if (pass[0] == '\0') - WiFi.begin(ssid); - else - WiFi.begin(ssid, pass); - int connectStartTimeout = millis() + connectTimeOut; - while (!isConnected() && millis() < connectStartTimeout) - { - vTaskDelay(250 / portTICK_PERIOD_MS); - } - if (millis() >= connectStartTimeout) - { - LogHandler::error(Tags::Wifi, "Wifi timed out connection to AP"); - m_connectingInProgress = false; - WiFi.disconnect(true, true); - return false; - } - - WiFi.setSleep(false); - _apMode = false; - m_connectingInProgress = false; - return true; - } - - void dispose() - { - // WiFi.disconnect(true, true); - // esp_http_client_cleanup( WiFi ); // dismiss the TCP stack - // esp_wifi_disconnect(); // break connection to AP - WiFi.disconnect(true, true); - WiFi.mode(WIFI_OFF); - - // esp_wifi_stop(); // shut down the wifi radio - // esp_wifi_deinit(); // release wifi resources - } - - void WiFiEvent(arduino_event_id_t event, arduino_event_info_t info) - { - switch (event) - { - case ARDUINO_EVENT_WIFI_STA_START: - LogHandler::info(Tags::Wifi, "Station Mode Started"); - break; - case ARDUINO_EVENT_WIFI_STA_GOT_IP: - m_connectingInProgress = false; - strncpy(SettingsHandler::currentIP, WiFi.localIP().toString().c_str(), IP4ADDR_STRLEN_MAX); - strncpy(SettingsHandler::currentGateway, WiFi.subnetMask().toString().c_str(), IP4ADDR_STRLEN_MAX); - strncpy(SettingsHandler::currentSubnet, WiFi.gatewayIP().toString().c_str(), IP4ADDR_STRLEN_MAX); - strncpy(SettingsHandler::currentDns1, WiFi.dnsIP().toString().c_str(), IP4ADDR_STRLEN_MAX); - LogHandler::info(Tags::Wifi, "Connected to: %s", WiFi.SSID().c_str()); - LogHandler::info(Tags::Wifi, "IP Address: %s", SettingsHandler::currentIP); - if (wifiStatus_callback) - wifiStatus_callback(WiFiStatus::IP, WiFiReason::UNKNOWN); - // SettingsHandler::printWebAddress(SettingsHandler::currentIP); - break; - case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: - { - uint8_t reason = info.wifi_sta_disconnected.reason; - LogHandler::warning(Tags::Wifi, "Disconnected from station, reason: %u", reason); - if (reason == WIFI_REASON_NO_AP_FOUND) - { - LogHandler::info(Tags::Wifi, "WIFI_REASON_NO_AP_FOUND"); - lastReason = reason; - } - else if (reason == WIFI_REASON_AUTH_FAIL || reason == WIFI_REASON_CONNECTION_FAIL - || reason == WIFI_REASON_NOT_AUTHED) // 15: AP deauthed the station (often after wrong password) - { - lastReason = reason; - } - else if (reason == WIFI_REASON_BEACON_TIMEOUT || reason == WIFI_REASON_HANDSHAKE_TIMEOUT) - { - LogHandler::info(Tags::Wifi, "WIFI_REASON_BEACON_TIMEOUT or WIFI_REASON_HANDSHAKE_TIMEOUT"); - } - else if (reason == WIFI_REASON_AUTH_EXPIRE) - { - LogHandler::info(Tags::Wifi, "WIFI_REASON_AUTH_EXPIRE"); - } - else if (reason == 201) // WIFI_REASON_ROAMING (ESP-IDF 5.x) - { - LogHandler::info(Tags::Wifi, "WIFI_REASON_ROAMING"); - } - else - { - LogHandler::info(Tags::Wifi, "Unknown reason %u", reason); - } - - // Avoid reconnect races while the initial WiFi.begin() connection is still in progress. - if (m_connectingInProgress) - { - LogHandler::debug(Tags::Wifi, "Initial STA connect still in progress; skipping immediate reconnect"); - break; - } - - const unsigned long now = millis(); - if (!_apMode && WiFi.getMode() == WIFI_STA && (now - m_lastReconnectAttemptMs) > 2000) - { - m_lastReconnectAttemptMs = now; - WiFi.reconnect(); - } - break; - } - case ARDUINO_EVENT_WIFI_STA_STOP: - LogHandler::error(Tags::Wifi, "Station Mode Stopped: %u", info.wifi_sta_disconnected.reason); - if (lastReason == WIFI_REASON_NO_AP_FOUND) - { - LogHandler::info(Tags::Wifi, "WIFI_REASON_NO_AP_FOUND"); - if (wifiStatus_callback) - wifiStatus_callback(WiFiStatus::DISCONNECTED, WiFiReason::NO_AP); - } - else if (lastReason == WIFI_REASON_AUTH_FAIL || lastReason == WIFI_REASON_CONNECTION_FAIL - || lastReason == WIFI_REASON_NOT_AUTHED) - { - if (wifiStatus_callback) - wifiStatus_callback(WiFiStatus::DISCONNECTED, WiFiReason::AUTH); - } - else - { - if (wifiStatus_callback) - wifiStatus_callback(WiFiStatus::DISCONNECTED, WiFiReason::UNKNOWN); - } - lastReason = 0; - break; - case ARDUINO_EVENT_WPS_ER_SUCCESS: - LogHandler::info(Tags::Wifi, "WPS Successfull, stopping WPS and connecting to: %s", WiFi.SSID().c_str()); - WiFi.begin(); - break; - case ARDUINO_EVENT_WPS_ER_FAILED: - LogHandler::error(Tags::Wifi, "WPS Failed, retrying"); - WiFi.reconnect(); - break; - case ARDUINO_EVENT_WPS_ER_TIMEOUT: - LogHandler::error(Tags::Wifi, "WPS Timedout, retrying"); - WiFi.reconnect(); - break; - case ARDUINO_EVENT_WPS_ER_PIN: - LogHandler::debug(Tags::Wifi, "ARDUINO_EVENT_WPS_ER_PIN"); - break; - case ARDUINO_EVENT_WIFI_AP_STACONNECTED: - LogHandler::debug(Tags::Wifi, "ARDUINO_EVENT_WIFI_AP_STACONNECTED"); - if (wifiStatus_callback) - wifiStatus_callback(WiFiStatus::CONNECTED, WiFiReason::AP_MODE); - // if(_apMode) - // { - // if(_bleHandler) - // _bleHandler->stop(); // If a client connects to the ap stop the BLE to save memory. - // if(_btHandler) - // _btHandler->stop(); - // } - break; - case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: - LogHandler::debug(Tags::Wifi, "ARDUINO_EVENT_WIFI_AP_STADISCONNECTED"); - if (wifiStatus_callback) - wifiStatus_callback(WiFiStatus::DISCONNECTED, WiFiReason::AP_MODE); - if (_apMode) - { - // _bleHandler->setup(); //Didnt get called for some reason. No time to debug. Just restart the esp. - } - break; - default: - break; - } - } - - bool startAp(const char* ssid, const char* pass, const uint8_t& channel, const bool& hidden, const char* ip, const char* subnet, const char* gateway) - { - // WiFi.disconnect(true, true); - LogHandler::info(Tags::Wifi, "Starting in APMode: SSID: %s, Hidden: %u, Channel: %u, IP: %s, Subnet: %s, Gateway: %s", ssid, hidden, channel, ip, subnet, gateway); - // LogHandler::info(Tags::Wifi, "Password: %s", pass); - WiFi.mode(WIFI_AP); - WiFi.setHostname(SettingsFactory::getInstance()->getHostname()); - - WiFi.softAP(ssid, pass, channel, hidden, 1); - printMac(); - delay(100); - if (onApEventID != 0) - WiFi.removeEvent(onApEventID); - onApEventID = WiFi.onEvent([this](arduino_event_id_t event, arduino_event_info_t info) - { this->WiFiEvent(event, info); }); - IPAddress ip_; - ip_.fromString(ip); - IPAddress subnet_; - subnet_.fromString(subnet); - IPAddress gateway_; - gateway_.fromString(ip); - if (!WiFi.softAPConfig(ip_, gateway_, subnet_)) - { - LogHandler::error(Tags::Wifi, "AP Mode Failed to configure"); - return false; - } - _apMode = true; - m_connectingInProgress = false; - LogHandler::info(Tags::Wifi, "APMode started"); - SettingsHandler::printWebAddress(WiFi.softAPIP().toString().c_str()); - return true; - } - void setWiFiStatusCallback(std::function f) - { - wifiStatus_callback = f == nullptr ? 0 : f; - } - static void disable() - { - LogHandler::info(Tags::Wifi, "Disable WiFi"); - WiFi.disconnect(true, true); - WiFi.mode(WIFI_OFF); - esp_err_t disable = esp_wifi_deinit(); - if (disable != ESP_OK) - { - LogHandler::error(Tags::Wifi, "Disable fail: %s", esp_err_to_name(disable)); - } - }; - - void setup() override - { - } - - void loop() override - { - } - -private: - WIFI_STATUS_FUNCTION_PTR_T wifiStatus_callback; - SettingsFactory* m_settingsFactory; - int connectTimeOut = 10000; - int onApEventID = 0; - static int8_t _rssi; - static bool _apMode; - uint8_t lastReason; - bool m_connectingInProgress = false; - unsigned long m_lastReconnectAttemptMs = 0; - // String translateEncryptionType(wifi_auth_mode_t encryptionType) { - // switch (encryptionType) { - // case (WIFI_AUTH_OPEN): - // return "Open"; - // case (WIFI_AUTH_WEP): - // return "WEP"; - // case (WIFI_AUTH_WPA_PSK): - // return "WPA_PSK"; - // case (WIFI_AUTH_WPA2_PSK): - // return "WPA2_PSK"; - // case (WIFI_AUTH_WPA_WPA2_PSK): - // return "WPA_WPA2_PSK"; - // case (WIFI_AUTH_WPA2_ENTERPRISE): - // return "WPA2_ENTERPRISE"; - // } - // } - void printMac() - { - // char macAddress[18] = {0}; -#ifdef ESP_ARDUINO3 - // strlcpy(macAddress, Network.macAddress().c_str(), sizeof(macAddress)); - LogHandler::info(Tags::Wifi, "Mac: %s", Network.macAddress().c_str()); -#else - // strlcpy(macTemp, WiFi.macAddress().c_str(), sizeof(macTemp)); - LogHandler::info(Tags::Wifi, "Mac: %s", WiFi.macAddress().c_str()); -#endif - } - wifi_band_mode_t toWiFiBandMode(WifiBand band) - { - switch (band) - { - case WifiBand::AUTO: - return wifi_band_mode_t::WIFI_BAND_MODE_AUTO; - case WifiBand::MODE24ghz: - return wifi_band_mode_t::WIFI_BAND_MODE_2G_ONLY; -#if SOC_WIFI_SUPPORT_5G - case WifiBand::MODE5ghz: - return wifi_band_mode_t::WIFI_BAND_MODE_5G_ONLY; -#endif -#if SOC_WIFI_SUPPORT_6G - case WifiMode::MODE6ghz: - return wifi_band_mode_t::WIFI_BAND_MODE_6G_ONLY; -#endif - } - return wifi_band_mode_t::WIFI_BAND_MODE_AUTO; - } -}; -bool WifiHandler::_apMode = false; \ No newline at end of file From 126fe7441b847dde3f4cb47dc1c2502f936527a9 Mon Sep 17 00:00:00 2001 From: Millibyte Products Date: Thu, 7 May 2026 12:55:05 -0400 Subject: [PATCH 25/42] Guard MCPWMServo::attachV5 against resolution_hz exceeding source clock High-frequency vibe attaches (e.g. 8 kHz x 15-bit = 262 MHz) caused IDF mcpwm_new_timer to compute prescale = src_clk / resolution_hz = 0, then later divide by prescale -> IntegerDivideByZero panic during boot. Reject configurations where resolution_hz exceeds the MCPWM source clock so callers get a clean failure instead of a hardware panic. --- ESP32/src/TCode/MCPWMServo.h | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ESP32/src/TCode/MCPWMServo.h b/ESP32/src/TCode/MCPWMServo.h index 996a2d0..862ddc4 100644 --- a/ESP32/src/TCode/MCPWMServo.h +++ b/ESP32/src/TCode/MCPWMServo.h @@ -176,6 +176,19 @@ class MCPWMServo uint32_t period_ticks = 1u << resolution_bits; uint32_t resolution_hz = freq_hz * period_ticks; + // MCPWM_TIMER_CLK_SRC_DEFAULT is the PLL/APB-derived 160 MHz clock on + // classic ESP32 and 80 MHz on S3/C3. If `resolution_hz` exceeds the + // source clock, IDF's `mcpwm_new_timer` computes prescale = src/res = 0 + // and the timer init divides by prescale → IntegerDivideByZero panic. + // Servo-style configs (50 Hz @ 14-bit ≈ 819 kHz) sit far below this + // limit; this guard blocks misuse like vibe @ 8 kHz / 15-bit + // (262 MHz) from triggering an unhandled exception. + constexpr uint32_t MCPWM_MAX_RESOLUTION_HZ = 80'000'000u; + if (resolution_hz == 0 || resolution_hz > MCPWM_MAX_RESOLUTION_HZ) + { + return false; + } + // Find a group whose timer matches this frequency (or is free) AND still has // an operator slot available. A group that matches frequency but is fully // occupied must be skipped so we can try the other group. From 4fb952d96e35a0976c924a3b44fe02530162798a Mon Sep 17 00:00:00 2001 From: Millibyte Products Date: Thu, 7 May 2026 15:35:40 -0400 Subject: [PATCH 26/42] Honor DEFAULT_BOARD build flag in cfg//config.h The per-chip config headers were unconditionally redefining BOARD_TYPE_DEFAULT, clobbering the BoardType passed via -D DEFAULT_BOARD in platformio.ini. As a result ssr1_pcb (and other PCB envs) booted with DEVKIT pins instead of their actual board pinout. Guard each cfg with #ifndef DEFAULT_BOARD so the build flag wins. --- ESP32/lib/cfg/C5/config.h | 2 ++ ESP32/lib/cfg/C6/config.h | 2 ++ ESP32/lib/cfg/E22/config.h | 2 ++ ESP32/lib/cfg/ESP32/config.h | 2 ++ ESP32/lib/cfg/S3/config.h | 2 ++ 5 files changed, 10 insertions(+) diff --git a/ESP32/lib/cfg/C5/config.h b/ESP32/lib/cfg/C5/config.h index 0052643..6fb9b31 100644 --- a/ESP32/lib/cfg/C5/config.h +++ b/ESP32/lib/cfg/C5/config.h @@ -4,6 +4,8 @@ #define MAX_PWM_RESOLUTION 16 #define MAX_TIMERS 4 #define MAX_CHANNELS 6 +#ifndef DEFAULT_BOARD #define BOARD_TYPE_DEFAULT (uint8_t)BoardType::DEVKIT_C5 +#endif #define TASK_CPU_NUM PRO_CPU_NUM // #define CONFIG_PREFIX ="C5" \ No newline at end of file diff --git a/ESP32/lib/cfg/C6/config.h b/ESP32/lib/cfg/C6/config.h index 9824e3a..f3fb4e8 100644 --- a/ESP32/lib/cfg/C6/config.h +++ b/ESP32/lib/cfg/C6/config.h @@ -4,5 +4,7 @@ #define MAX_PWM_RESOLUTION 14 #define MAX_TIMERS 6 #define MAX_CHANNELS 6 +#ifndef DEFAULT_BOARD #define BOARD_TYPE_DEFAULT (uint8_t)BoardType::DEVKIT_C6 +#endif #define TASK_CPU_NUM PRO_CPU_NUM \ No newline at end of file diff --git a/ESP32/lib/cfg/E22/config.h b/ESP32/lib/cfg/E22/config.h index f2a36eb..273ba1f 100644 --- a/ESP32/lib/cfg/E22/config.h +++ b/ESP32/lib/cfg/E22/config.h @@ -5,5 +5,7 @@ #define MAX_PWM_RESOLUTION 16 #define MAX_TIMERS 6 #define MAX_CHANNELS 6 +#ifndef DEFAULT_BOARD #define BOARD_TYPE_DEFAULT (uint8_t)BoardType::DEVKIT_E22 +#endif #define TASK_CPU_NUM PRO_CPU_NUM \ No newline at end of file diff --git a/ESP32/lib/cfg/ESP32/config.h b/ESP32/lib/cfg/ESP32/config.h index 7d49fc6..dd00489 100644 --- a/ESP32/lib/cfg/ESP32/config.h +++ b/ESP32/lib/cfg/ESP32/config.h @@ -4,6 +4,8 @@ #define MAX_PWM_RESOLUTION 16 #define MAX_TIMERS 8 #define MAX_CHANNELS (MAX_TIMERS << 1) +#ifndef DEFAULT_BOARD #define BOARD_TYPE_DEFAULT (uint8_t)BoardType::DEVKIT +#endif #define TASK_CPU_NUM APP_CPU_NUM diff --git a/ESP32/lib/cfg/S3/config.h b/ESP32/lib/cfg/S3/config.h index 8239762..88719e6 100644 --- a/ESP32/lib/cfg/S3/config.h +++ b/ESP32/lib/cfg/S3/config.h @@ -4,10 +4,12 @@ #define MAX_PWM_RESOLUTION 14 #define MAX_TIMERS 4 #define MAX_CHANNELS (MAX_TIMERS << 1) +#ifndef DEFAULT_BOARD #ifdef S3_ZERO #define BOARD_TYPE_DEFAULT (uint8_t)BoardType::ZERO #else #define BOARD_TYPE_DEFAULT (uint8_t)BoardType::N8R8 #endif +#endif #define TASK_CPU_NUM APP_CPU_NUM From bfa77061763acb40ba62a2d15703ba44f6f1737a Mon Sep 17 00:00:00 2001 From: Millibyte Products Date: Thu, 7 May 2026 15:35:47 -0400 Subject: [PATCH 27/42] Restore canonical BLDC_MotorA_* setting keys lib/settingConstants.h had a duplicate #define block that re-declared BLDC_MOTORA_VOLTAGE/SUPPLY/CURRENT/ZEROELECANGLE etc. with the truncated string keys 'BLDC_Motor_VoltageLimit'... overriding the earlier canonical 'BLDC_MotorA_VoltageLimit' names. Saved JSON written via SettingsFactory then used the wrong keys and the BLDC handler always read defaults (ZeroElecAngle = -12345 sentinel), which broke FOC initialization. --- ESP32/lib/settingConstants.h | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ESP32/lib/settingConstants.h b/ESP32/lib/settingConstants.h index 142a4c2..d5ca604 100644 --- a/ESP32/lib/settingConstants.h +++ b/ESP32/lib/settingConstants.h @@ -240,19 +240,19 @@ #define BLDC_LOWPASS_FILTER "BLDC_LowPassFilter" #define BLDC_TWIST_LIMIT "BLDC_TwistLimit" -#define BLDC_MOTORA_ENCODER "BLDC_Encoder" -#define BLDC_MOTORA_PULLEY_CIRCUMFERENCE "BLDC_Pulley_Circumference" -#define BLDC_MOTORA_VOLTAGE "BLDC_Motor_VoltageLimit" -#define BLDC_MOTORA_SUPPLY "BLDC_Motor_SupplyVoltage" -#define BLDC_MOTORA_CURRENT "BLDC_Motor_Current" -#define BLDC_MOTORA_ZEROELECANGLE "BLDC_Motor_ZeroElecAngle" +// MotorA aliases follow the pre-merge MillibyteProducts naming convention so +// existing saved settings load correctly. Do not redefine BLDC_MOTORA_VOLTAGE, +// BLDC_MOTORA_SUPPLY, BLDC_MOTORA_CURRENT, or BLDC_MOTORA_ZEROELECANGLE here; +// those are already defined above. +#define BLDC_MOTORA_ENCODER BLDC_ENCODER +#define BLDC_MOTORA_PULLEY_CIRCUMFERENCE BLDC_PULLEY_CIRCUMFERENCE #define BLDC_MOTORB_ENCODER "BLDC_BEncoder" #define BLDC_MOTORB_PULLEY_CIRCUMFERENCE "BLDC_BPulley_Circumference" -#define BLDC_MOTORB_VOLTAGE "BLDC_BMotor_VoltageLimit" -#define BLDC_MOTORB_SUPPLY "BLDC_BMotor_SupplyVoltage" -#define BLDC_MOTORB_CURRENT "BLDC_BMotor_Current" -#define BLDC_MOTORB_ZEROELECANGLE "BLDC_BMotor_ZeroElecAngle" +#define BLDC_MOTORB_VOLTAGE "BLDC_MotorB_VoltageLimit" +#define BLDC_MOTORB_SUPPLY "BLDC_MotorB_SupplyVoltage" +#define BLDC_MOTORB_CURRENT "BLDC_MotorB_Current" +#define BLDC_MOTORB_ZEROELECANGLE "BLDC_MotorB_ZeroElecAngle" #define STATICIP "staticIP" #define LOCALIP "localIP" From 1e87ce3c5fe81b4137b7c54558f15a00b5a479cd Mon Sep 17 00:00:00 2001 From: Millibyte Products Date: Thu, 7 May 2026 15:35:53 -0400 Subject: [PATCH 28/42] Add /debugInfo GET/POST endpoints to active WebHandler The AsyncWebServer-based WebHandler (the one main.cpp actually instantiates) was missing /debugInfo, so the settings UI got a 404 and the polling chain faulted. GET streams /debugInfo.json (or returns an empty doc when absent), POST clears the file. --- ESP32/src/network/WebHandler.h | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/ESP32/src/network/WebHandler.h b/ESP32/src/network/WebHandler.h index a2a059a..5a5bec8 100644 --- a/ESP32/src/network/WebHandler.h +++ b/ESP32/src/network/WebHandler.h @@ -105,6 +105,28 @@ class WebHandler : public HTTPBase server->on("/buttonSettings", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(LittleFS, BUTTON_SETTINGS_PATH, "application/json"); }); + server->on("/debugInfo", HTTP_GET, [](AsyncWebServerRequest* request) + { + if (LittleFS.exists(DEBUG_INFO_PATH)) { + request->send(LittleFS, DEBUG_INFO_PATH, "application/json"); + } + else { + // No debug info recorded yet; return an empty doc so the + // client can populate its UI without treating it as a fault. + request->send(200, "application/json", "{\"" DEBUG_INFO_LAST_BOOT_REASONS "\":[]}"); + } + }); + + server->on("/debugInfo", HTTP_POST, [](AsyncWebServerRequest* request) + { + // Clear the persisted debug info file so the client can reset + // recorded boot reasons. + if (LittleFS.exists(DEBUG_INFO_PATH)) { + LittleFS.remove(DEBUG_INFO_PATH); + } + request->send(200, "application/json", "{\"msg\":\"done\"}"); + }); + // server->on("/log", HTTP_GET, [this](AsyncWebServerRequest *request) // { // Serial.println("Get log..."); From 8bd83913334b941c54880190e755e8b47b9dc305 Mon Sep 17 00:00:00 2001 From: Millibyte Products Date: Thu, 7 May 2026 15:36:00 -0400 Subject: [PATCH 29/42] Hoist polling to module scope and tolerate /debugInfo 404 pingDevice() declared 'let polling = false' as a function-local variable, so every other function (getSystemInfo/getDebugInfo/...) referencing bare 'polling' threw ReferenceError on the first call from onDocumentLoad. Move the declaration to module scope. Also stop treating a missing /debugInfo endpoint as a fatal error so older firmware keeps loading. --- ESP32/data/www/index-min.html.gz | Bin 0 -> 50533 bytes ESP32/dataEdit/www/settings.js | 15 +++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 ESP32/data/www/index-min.html.gz diff --git a/ESP32/data/www/index-min.html.gz b/ESP32/data/www/index-min.html.gz new file mode 100644 index 0000000000000000000000000000000000000000..5999a9002ae62865692425ba7101f6fee492ab20 GIT binary patch literal 50533 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 Od;bd?$k^USQ3C*|JDNTK literal 0 HcmV?d00001 diff --git a/ESP32/dataEdit/www/settings.js b/ESP32/dataEdit/www/settings.js index 0ae0f76..33deb81 100644 --- a/ESP32/dataEdit/www/settings.js +++ b/ESP32/dataEdit/www/settings.js @@ -138,6 +138,7 @@ var testDeviceDisableModifier = false; var testDeviceModifierValue = "1000"; var restartClicked = false; var serverPollingTimeOut = null; +var polling = false; var staticIPAddressTimeout = null; var hostnameTimeout = null; var serverPollRetryCount = 0; @@ -219,7 +220,7 @@ function onDocumentLoad() { // debugTextElement.scrollTop = debugTextElement.scrollHeight; } function pingDevice() { - let polling = false; + polling = false; if(serverPollingTimeOut) { polling = true; clearTimeout(serverPollingTimeOut); @@ -278,8 +279,18 @@ function getDebugInfo(chain) { else hideLoading(); }, function(xhr) { + // Debug info is non-essential; if the endpoint is missing or fails, + // just continue the load chain instead of looping into a poll. + if(xhr && xhr.status === 404) { + if(chain) { + getPinSettings(chain); + return; + } + hideLoading(); + return; + } if(!polling) - showError("Error getting system info!"); + showError("Error getting debug info!"); startServerPoll(); }); } From 38402736e656ddd4abcb20607e94b7ec100cf2b3 Mon Sep 17 00:00:00 2001 From: Millibyte Products Date: Thu, 7 May 2026 15:43:47 -0400 Subject: [PATCH 30/42] BLDCHandler0_3: log driver init result and explicitly enable driver Makes it possible to tell from the boot log whether BLDCDriver3PWM->init() succeeded and whether the EN pin was actually asserted before initFOC() attempts sensor alignment. Calls driverA->enable() right after init so the gate-driver chip is unambiguously enabled when the alignment voltage is applied. --- ESP32/src/TCode/v0.3/BLDCHandler0_3.h | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/ESP32/src/TCode/v0.3/BLDCHandler0_3.h b/ESP32/src/TCode/v0.3/BLDCHandler0_3.h index 53ba8c5..4c2d37d 100644 --- a/ESP32/src/TCode/v0.3/BLDCHandler0_3.h +++ b/ESP32/src/TCode/v0.3/BLDCHandler0_3.h @@ -187,14 +187,23 @@ class BLDCHandler0_3 : public MotorHandler0_3 // Max DC voltage allowed - default voltage_limit double motorAVoltage = BLDC_MOTORA_VOLTAGE_DEFAULT; m_settingsFactory->getValue(BLDC_MOTORA_VOLTAGE, motorAVoltage); - LogHandler::debug(Tags::Motor, "Voltage: %f", motorAVoltage); + LogHandler::info(Tags::Motor, "BLDC voltage_limit: %f", motorAVoltage); driverA->voltage_limit = motorAVoltage; // power supply voltage [V] double supplyAVoltage = BLDC_MOTORA_SUPPLY_DEFAULT; m_settingsFactory->getValue(BLDC_MOTORA_SUPPLY, supplyAVoltage); + LogHandler::info(Tags::Motor, "BLDC voltage_power_supply: %f", supplyAVoltage); driverA->voltage_power_supply = supplyAVoltage; // driver init - driverA->init(); + int driverInitStatus = driverA->init(); + LogHandler::info(Tags::Motor, "BLDCDriver3PWM->init() returned: %d (1=ok, 0=fail)", driverInitStatus); + // Force the driver into the enabled state so the gate-driver chip + // is actually receiving its EN signal during alignment. Some boards + // (and some SimpleFOC code paths) leave this de-asserted until the + // first move(); without it the alignment voltage is never reflected + // on the motor windings and we get "Failed to notice movement". + driverA->enable(); + LogHandler::info(Tags::Motor, "BLDCDriver3PWM enabled (EN pin %d driven HIGH)", pinMap->enable()); // limiting motor movements double motorACurrent = BLDC_MOTORA_CURRENT_DEFAULT; From 28df60968d7bf86b28a22465293ff4a270044679 Mon Sep 17 00:00:00 2001 From: Millibyte Products Date: Thu, 7 May 2026 15:48:26 -0400 Subject: [PATCH 31/42] Revert bldc-motor.js to pre-merge singleton The class-based rewrite was incomplete: it generated DOM elements with IDs like BLDC_Motor_VoltageLimit (no 'A') that don't match the static HTML form (BLDC_MotorA_VoltageLimit), it expected motorA/motorB instances that were never constructed, and it left BLDCMotor.setup() calls hanging on a class with no static method. Restoring the prior object-literal singleton fixes the 'BLDCMotor.setup is not a function' TypeError thrown from setUserSettings during settings load. --- ESP32/data/www/index-min.html.gz | Bin 50533 -> 50471 bytes ESP32/dataEdit/www/bldc-motor.js | 353 +++++++++++++++++-------------- 2 files changed, 200 insertions(+), 153 deletions(-) diff --git a/ESP32/data/www/index-min.html.gz b/ESP32/data/www/index-min.html.gz index 5999a9002ae62865692425ba7101f6fee492ab20..72b428a8b5dee596ee024348b280f79d29c1fe83 100644 GIT binary patch literal 50471 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 Od;bd?$k^USQ3C*|JDNTK diff --git a/ESP32/dataEdit/www/bldc-motor.js b/ESP32/dataEdit/www/bldc-motor.js index ea3df02..ff4c004 100644 --- a/ESP32/dataEdit/www/bldc-motor.js +++ b/ESP32/dataEdit/www/bldc-motor.js @@ -1,6 +1,6 @@ -/* MIT License +/* MIT License -Copyright (c) 2026 Jason C. Fain +Copyright (c) 2024 Jason C. Fain Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -20,163 +20,210 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -class BLDCMotor { - name = ""; - Names = {}; - deviceType; - ModalNode; - ParentNode; - initialized = false; - initializedPins = false; - constructor(deviceType, name = "") { - this.name = name; - this.deviceType = deviceType; - this.ModalNode = document.getElementById(this.name + "MotorSettings"); - if(!this.ModalNode) - { - this.ModalNode = document.createElement("modal-component"); - this.ModalNode.id = "motor" + this.name + "Settings"; - const header = document.createElement("span"); - // This is like this because the settings for the A motor does not have 'A' in it for SSR1. - header.innerText = isSSR1() ? "Motor Settings" : "Motor " + (this.name.length ? this.name : "A") + " Settings" - header.setAttribute("slot", "title"); - this.ModalNode.appendChild(header); - document.body.appendChild(this.ModalNode); - - const ParentTable = document.createElement("div"); - ParentTable.id = this.name + "MotorSettingsTable"; - ParentTable.setAttribute("name", this.name + "MotorSettingsTable"); - ParentTable.classList.add("formTable"); - this.ParentNode = document.createElement("div"); - ParentTable.appendChild(this.ParentNode); - this.ModalNode.appendChild(ParentTable); - } - - // this.Names.BLDC_Encoder = "BLDC_" + name + "Encoder"; - // this.Names.BLDC_UseHallSensor = "BLDC_" + name + "UseHallSensor"; - this.Names.BLDC_Encoder = "BLDC_Encoder"; - // this.Names.BLDC_UseHallSensor = "BLDC_UseHallSensor"; - this.Names.BLDC_Pulley_Circumference = "BLDC_" + name + "Pulley_Circumference"; - this.Names.BLDC_Motor_VoltageLimit = "BLDC_" + name + "Motor_VoltageLimit"; - this.Names.BLDC_Motor_SupplyVoltage = "BLDC_" + name + "Motor_SupplyVoltage"; - this.Names.BLDC_Motor_Current = "BLDC_" + name + "Motor_Current"; - this.Names.BLDC_Motor_ZeroElecAngle = "BLDC_" + name + "Motor_ZeroElecAngle"; - // this.Names.BLDC_RailLength = "BLDC_" + name + "RailLength"; - // this.Names.BLDC_Range = "BLDC_" + (name.length == 0 ? "Stroke" : name) + "Length"; - this.Names.BLDC_ChipSelect_PIN = "BLDC_" + name + "ChipSelect_PIN"; - this.Names.BLDC_Encoder_PIN = "BLDC_" + name + "Encoder_PIN"; - this.Names.BLDC_Enable_PIN = "BLDC_" + name + "Enable_PIN"; - this.Names.BLDC_PWMchannel1_PIN = "BLDC_" + name + "PWMchannel1_PIN"; - this.Names.BLDC_PWMchannel2_PIN = "BLDC_" + name + "PWMchannel2_PIN"; - this.Names.BLDC_PWMchannel3_PIN = "BLDC_" + name + "PWMchannel3_PIN"; - // this.Names.BLDC_HallEffect_PIN = "BLDC_" + name + "HallEffect_PIN"; - // this.Names.HallEffect_Row = name + "HallEffect", - this.Names.ZeroElecAngle_Row = name + "ZeroElecAngle"; - } - +BLDCMotor = { setup() { - if(this.initialized) - return; - - // let channelRow = Utils.createNumericFormRow(0, "Update rate (ms)", 'motionUpdate'+profileIndex+channelIndex, motionChannel ? motionChannel.update : 100, 0, 2147483647, 1, - // function(profileIndex, channelIndex, name) {setMotionGeneratorSettings(profileIndex, channelIndex, name)}.bind(this, profileIndex, channelIndex, name)); - // channelRow.title = `This is the time in between updates that gives the system time to process other tasks. (DO NOT SET TOO LOW ON ESP32!) - // It may be best to just leave at default.` - // channelTableDiv.appendChild(channelRow.row); - - //ToDo create combo box maybe - // const encoderNode = Utils.createNumericFormRow(null, "Encoder", this.Names.BLDC_Encoder, userSettings[this.Names.BLDC_Encoder], null, null, null, this.setEncoderType); - // motorSettingsTable.appendChild(encoderNode.row); - // // document.getElementById(this.Names.BLDC_Encoder).value = userSettings[this.Names.BLDC_Encoder]; - // this.createBLDCCheckboxFormNode(this.Names.BLDC_UseHallSensor, "Use hall sensor", userSettings[this.Names.BLDC_UseHallSensor], () => this.updateBLDCSettings(0)); - if(this.deviceType == DeviceType.SSR1) - { - this.createBLDCNumericFormNode(this.Names.BLDC_Pulley_Circumference, "Pulley Circumference (mm)", userSettings[this.Names.BLDC_Pulley_Circumference], () => this.updateBLDCSettings(), 0, 2147483647, 1); - } - this.createBLDCNumericFormNode(this.Names.BLDC_Motor_VoltageLimit, "Voltage limit (v)", userSettings[this.Names.BLDC_Motor_VoltageLimit], () => this.updateBLDCSettings(), 0.0, 2147483647.0, 0.01); - this.createBLDCNumericFormNode(this.Names.BLDC_Motor_SupplyVoltage, "Supply voltage (v)", userSettings[this.Names.BLDC_Motor_SupplyVoltage], () => this.updateBLDCSettings(), 0.0, 2147483647.0, 0.01); - this.createBLDCNumericFormNode(this.Names.BLDC_Motor_Current, "Motor current (a)", userSettings[this.Names.BLDC_Motor_Current], () => this.updateBLDCSettings(), 0.0, 2147483647.0, 0.01); - this.createBLDCNumericFormNode(this.Names.BLDC_Motor_ZeroElecAngle, "Zero elec angle (rad)", userSettings[this.Names.BLDC_Motor_ZeroElecAngle], () => this.updateBLDCSettings(), -2147483647.0, 2147483647.0, 0.01, this.Names.ZeroElecAngle_Row); - // this.createBLDCNumericFormNode(this.Names.BLDC_RailLength, "Rail length (mm)", userSettings[this.Names.BLDC_RailLength], () => this.updateBLDCSettings(), 0, 2147483647); - // this.createBLDCNumericFormNode(this.Names.BLDC_Range, (this.name.length == 0 ? "Stroke" : this.name) + " length (mm)", userSettings[this.Names.BLDC_Range], () => this.updateBLDCSettings(), 0, 2147483647, 1); - - // this.toggleBLDCEncoderOptions(); - // Utils.toggleControlVisibilityByID(this.Names.HallEffect_Row, userSettings[this.Names.BLDC_UseHallSensor]); - this.initialized = true; - } - - createBLDCNumericFormNode(key, label, value, callback, min = undefined, max = undefined, step = undefined, rowName = undefined, classList = []) { - const node = Utils.createNumericFormRow(rowName, label, key, value, min, max, step, callback); - if(classList.length > 0) { - node.row.classList.add(...classList); - } - this.ParentNode.appendChild(node.row); - } - createBLDCCheckboxFormNode(key, label, value, callback, rowName = undefined) { - const node = Utils.createCheckboxFormRow(rowName, label, key, value, callback); - this.ParentNode.appendChild(node.row); - } - + document.getElementById("BLDC_Encoder").value = userSettings["BLDC_Encoder"]; + document.getElementById("BLDC_UseHallSensor").checked = userSettings["BLDC_UseHallSensor"]; + document.getElementById("BLDC_Pulley_Circumference").value = userSettings["BLDC_Pulley_Circumference"]; + document.getElementById("BLDC_MotorA_VoltageLimit").value = Utils.round2(userSettings["BLDC_MotorA_VoltageLimit"]); + document.getElementById("BLDC_MotorA_SupplyVoltage").value = Utils.round2(userSettings["BLDC_MotorA_SupplyVoltage"]); + document.getElementById("BLDC_MotorA_Current").value = Utils.round2(userSettings["BLDC_MotorA_Current"]); + document.getElementById("BLDC_MotorA_ZeroElecAngle").value = Utils.round2(userSettings["BLDC_MotorA_ZeroElecAngle"]); + document.getElementById("BLDC_MotorA_ParametersKnown").checked = userSettings["BLDC_MotorA_ParametersKnown"]; + document.getElementById("BLDC_RailLength").value = userSettings["BLDC_RailLength"]; + document.getElementById("BLDC_StrokeLength").value = userSettings["BLDC_StrokeLength"]; + + toggleBLDCEncoderOptions(); + Utils.toggleControlVisibilityByID("HallEffect", userSettings["BLDC_UseHallSensor"]); + Utils.toggleControlVisibilityByID("ZeroElecAngle", userSettings["BLDC_MotorA_ParametersKnown"]); + }, setupPins() { - if(this.initializedPins) - return; - this.createBLDCNumericFormNode(this.Names.BLDC_ChipSelect_PIN, "Chip select PIN", pinoutSettings[this.Names.BLDC_ChipSelect_PIN], () => this.updateBLDCPins(), -1, 2147483647, 1, null, ["BLDCSPI"]); - this.createBLDCNumericFormNode(this.Names.BLDC_Encoder_PIN, "Encoder PIN", pinoutSettings[this.Names.BLDC_Encoder_PIN], () => this.updateBLDCPins(), -1, 2147483647, 1, null, ["BLDCPWM"]); - this.createBLDCNumericFormNode(this.Names.BLDC_Enable_PIN, "Enable PIN", pinoutSettings[this.Names.BLDC_Enable_PIN], () => this.updateBLDCPins(), -1, 2147483647); - this.createBLDCNumericFormNode(this.Names.BLDC_PWMchannel1_PIN, "PWM channel 1 PIN", pinoutSettings[this.Names.BLDC_PWMchannel1_PIN], () => this.updateBLDCPins(), -1, 2147483647); - this.createBLDCNumericFormNode(this.Names.BLDC_PWMchannel2_PIN, "PWM channel 2 PIN", pinoutSettings[this.Names.BLDC_PWMchannel2_PIN], () => this.updateBLDCPins(), -1, 2147483647); - this.createBLDCNumericFormNode(this.Names.BLDC_PWMchannel3_PIN, "PWM channel 3 PIN", pinoutSettings[this.Names.BLDC_PWMchannel3_PIN], () => this.updateBLDCPins(), -1, 2147483647); - // this.createBLDCNumericFormNode(this.Names.BLDC_HallEffect_PIN, "Hall effect PIN", pinoutSettings[this.Names.BLDC_HallEffect_PIN], () => this.updateBLDCPins(), -1, 2147483647, this.Names.HallEffect_Row); - this.initializedPins = true; + document.getElementById("BLDC_ChipSelect_PIN").value = pinoutSettings["BLDC_ChipSelect_PIN"]; + document.getElementById("BLDC_Encoder_PIN").value = pinoutSettings["BLDC_Encoder_PIN"]; + document.getElementById("BLDC_Enable_PIN").value = pinoutSettings["BLDC_Enable_PIN"]; + document.getElementById("BLDC_PWMchannel1_PIN").value = pinoutSettings["BLDC_PWMchannel1_PIN"]; + document.getElementById("BLDC_PWMchannel2_PIN").value = pinoutSettings["BLDC_PWMchannel2_PIN"]; + document.getElementById("BLDC_PWMchannel3_PIN").value = pinoutSettings["BLDC_PWMchannel3_PIN"]; + document.getElementById("BLDC_HallEffect_PIN").value = pinoutSettings["BLDC_HallEffect_PIN"]; } - // TODO: move bldc stuff in to here. Follow this pattern moving forward. - updateBLDCSettings(delay = defaultDebounce) { - Utils.debounce("updateBLDCSettings", () => { - if(this.deviceType == DeviceType.SSR1) - { - userSettings[this.Names.BLDC_Pulley_Circumference] = parseInt(document.getElementById(this.Names.BLDC_Pulley_Circumference).value); - } - userSettings[this.Names.BLDC_Motor_VoltageLimit] = Utils.round2(parseFloat(document.getElementById(this.Names.BLDC_Motor_VoltageLimit).value)); - userSettings[this.Names.BLDC_Motor_SupplyVoltage] = Utils.round2(parseFloat(document.getElementById(this.Names.BLDC_Motor_SupplyVoltage).value)); - userSettings[this.Names.BLDC_Motor_Current] = Utils.round2(parseFloat(document.getElementById(this.Names.BLDC_Motor_Current).value)); - userSettings[this.Names.BLDC_Motor_ZeroElecAngle] = Utils.round2(parseFloat(document.getElementById(this.Names.BLDC_Motor_ZeroElecAngle).value)); - // userSettings[this.Names.BLDC_RailLength] = parseInt(document.getElementById(this.Names.BLDC_RailLength).value); - // userSettings[this.Names.BLDC_Range] = parseInt(document.getElementById(this.Names.BLDC_Range).value); - setRestartRequired(); - updateUserSettings(0); - }, delay); +} +function updateBLDCSettings() { + userSettings["BLDC_UseHallSensor"] = document.getElementById('BLDC_UseHallSensor').checked; + Utils.toggleControlVisibilityByID("HallEffect", userSettings["BLDC_UseHallSensor"]); + userSettings["BLDC_Pulley_Circumference"] = parseInt(document.getElementById('BLDC_Pulley_Circumference').value); + userSettings["BLDC_MotorA_VoltageLimit"] = Utils.round2(parseFloat(document.getElementById('BLDC_MotorA_VoltageLimit').value)); + userSettings["BLDC_MotorA_SupplyVoltage"] = Utils.round2(parseFloat(document.getElementById('BLDC_MotorA_SupplyVoltage').value)); + userSettings["BLDC_MotorA_Current"] = Utils.round2(parseFloat(document.getElementById('BLDC_MotorA_Current').value)); + userSettings["BLDC_MotorA_ZeroElecAngle"] = Utils.round2(parseFloat(document.getElementById('BLDC_MotorA_ZeroElecAngle').value)); + userSettings["BLDC_MotorA_ParametersKnown"] = document.getElementById("BLDC_MotorA_ParametersKnown").checked; + userSettings["BLDC_RailLength"] = parseInt(document.getElementById('BLDC_RailLength').value); + userSettings["BLDC_StrokeLength"] = parseInt(document.getElementById('BLDC_StrokeLength').value); + Utils.toggleControlVisibilityByID("ZeroElecAngle", userSettings["BLDC_MotorA_ParametersKnown"]); + setRestartRequired(); + updateUserSettings(); +} + +function updateBLDCPins() { + if(upDateTimeout !== null) + { + clearTimeout(upDateTimeout); } + upDateTimeout = setTimeout(() => + { + var pinValues = validateBLDCPins(); + if(pinValues) { + pinoutSettings["BLDC_ChipSelect_PIN"] = pinValues.BLDC_ChipSelect_PIN; + pinoutSettings["BLDC_Encoder_PIN"] = pinValues.BLDC_Encoder_PIN; + pinoutSettings["BLDC_Enable_PIN"] = pinValues.BLDC_Enable_PIN; + pinoutSettings["BLDC_PWMchannel1_PIN"] = pinValues.BLDC_PWMchannel1_PIN; + pinoutSettings["BLDC_PWMchannel2_PIN"] = pinValues.BLDC_PWMchannel2_PIN; + pinoutSettings["BLDC_PWMchannel3_PIN"] = pinValues.BLDC_PWMchannel3_PIN; + pinoutSettings["BLDC_HallEffect_PIN"] = pinValues.BLDC_HallEffect_PIN; + updateCommonPins(pinValues); + setRestartRequired(); + postPinoutSettings(); + } + }, 2000); +} + +function getBLDCPinValues() { + var pinValues = {}; + pinValues.BLDC_ChipSelect_PIN = parseInt(document.getElementById('BLDC_ChipSelect_PIN').value); + pinValues.BLDC_Encoder_PIN = parseInt(document.getElementById('BLDC_Encoder_PIN').value); + pinValues.BLDC_Enable_PIN = parseInt(document.getElementById('BLDC_Enable_PIN').value); + pinValues.BLDC_PWMchannel1_PIN = parseInt(document.getElementById('BLDC_PWMchannel1_PIN').value); + pinValues.BLDC_PWMchannel2_PIN = parseInt(document.getElementById('BLDC_PWMchannel2_PIN').value); + pinValues.BLDC_PWMchannel3_PIN = parseInt(document.getElementById('BLDC_PWMchannel3_PIN').value); + pinValues.BLDC_HallEffect_PIN = parseInt(document.getElementById('BLDC_HallEffect_PIN').value); + getCommonPinValues(pinValues); + return pinValues; +} + +function validateBLDCPins() { + clearErrors("pinValidation"); + var assignedPins = []; + var duplicatePins = []; + var pwmErrors = []; + var pinValues = getBLDCPinValues(); + if(userSettings["disablePinValidation"]) + return pinValues; - updateBLDCPins(pinValues) { - pinoutSettings[this.Names.BLDC_ChipSelect_PIN] = pinValues[this.Names.BLDC_ChipSelect_PIN]; - pinoutSettings[this.Names.BLDC_Encoder_PIN] = pinValues[this.Names.BLDC_Encoder_PIN]; - pinoutSettings[this.Names.BLDC_Enable_PIN] = pinValues[this.Names.BLDC_Enable_PIN]; - pinoutSettings[this.Names.BLDC_PWMchannel1_PIN] = pinValues[this.Names.BLDC_PWMchannel1_PIN]; - pinoutSettings[this.Names.BLDC_PWMchannel2_PIN] = pinValues[this.Names.BLDC_PWMchannel2_PIN]; - pinoutSettings[this.Names.BLDC_PWMchannel3_PIN] = pinValues[this.Names.BLDC_PWMchannel3_PIN]; + if(isModuleType(ModuleType.S3)) + { + if(isBoardType(BoardType.ZERO)) { + if(isBLDCSPI()) { + assignedPins.push({name:"SPI MOSI", pin:11}); + } + } else { + // TODO validate this for N8R8 + //assignedPins.push({name:"SPI1", pin:5}); + assignedPins.push({name:"SPI CLK", pin:18}); + assignedPins.push({name:"SPI MISO", pin:19}); + if(isBLDCSPI()) { + assignedPins.push({name:"SPI MOSI", pin:23}); + } + } } - - getBLDCPinValues(pinValues = {}) { - pinValues[this.Names.BLDC_ChipSelect_PIN] = parseInt(document.getElementById(this.Names.BLDC_ChipSelect_PIN).value); - pinValues[this.Names.BLDC_Encoder_PIN] = parseInt(document.getElementById(this.Names.BLDC_Encoder_PIN).value); - pinValues[this.Names.BLDC_Enable_PIN] = parseInt(document.getElementById(this.Names.BLDC_Enable_PIN).value); - pinValues[this.Names.BLDC_PWMchannel1_PIN] = parseInt(document.getElementById(this.Names.BLDC_PWMchannel1_PIN).value); - pinValues[this.Names.BLDC_PWMchannel2_PIN] = parseInt(document.getElementById(this.Names.BLDC_PWMchannel2_PIN).value); - pinValues[this.Names.BLDC_PWMchannel3_PIN] = parseInt(document.getElementById(this.Names.BLDC_PWMchannel3_PIN).value); - return pinValues; + else + { + //assignedPins.push({name:"SPI1", pin:5}); + assignedPins.push({name:"SPI CLK", pin:18}); + assignedPins.push({name:"SPI MISO", pin:19}); + if(isBLDCSPI()) { + assignedPins.push({name:"SPI MOSI", pin:23}); + } } - - validateBLDCPins(pinValues, assignedPins, duplicatePins, pwmErrors, invalidPins) { - const name = "Motor " + (this.name.length ? this.name : "A"); - if(!isBLDCSPI()) - validatePin(pinValues[this.Names.BLDC_Encoder_PIN], name+" Encoder", assignedPins, duplicatePins, false, invalidPins); - else - validatePin(pinValues[this.Names.BLDC_ChipSelect_PIN], name+" Chip select", assignedPins, duplicatePins, false, invalidPins); - validatePin(pinValues[this.Names.BLDC_Enable_PIN], name+" Enable", assignedPins, duplicatePins, false, invalidPins); - validatePWMPin(pinValues[this.Names.BLDC_PWMchannel1_PIN], name+" PWMchannel1", assignedPins, duplicatePins, pwmErrors, invalidPins); - validatePWMPin(pinValues[this.Names.BLDC_PWMchannel2_PIN], name+" PWMchannel2", assignedPins, duplicatePins, pwmErrors, invalidPins); - validatePWMPin(pinValues[this.Names.BLDC_PWMchannel3_PIN], name+" PWMchannel3", assignedPins, duplicatePins, pwmErrors, invalidPins); - return pinValues; + // var pinDupeIndex = -1; + // if(pinValues.BLDC_Encoder_PIN > -1) { + // pinDupeIndex = assignedPins.findIndex(x => x.pin === pinValues.BLDC_Encoder_PIN); + // if(pinDupeIndex > -1) + // duplicatePins.push("Encoder pin and "+assignedPins[pinDupeIndex].name); + // assignedPins.push({name:"Encoder", pin:pinValues.BLDC_Encoder_PIN}); + // } + validatePin(pinValues.BLDC_Encoder_PIN, "Encoder", assignedPins, duplicatePins); + + + // if(pinValues.BLDC_ChipSelect_PIN > -1) { + // pinDupeIndex = assignedPins.findIndex(x => x.pin === pinValues.BLDC_ChipSelect_PIN); + // if(pinDupeIndex > -1) + // duplicatePins.push("Chip select and "+assignedPins[pinDupeIndex].name); + // assignedPins.push({name:"Chip select", pin:pinValues.BLDC_ChipSelect_PIN}); + // } + validatePin(pinValues.BLDC_ChipSelect_PIN, "Chip select", assignedPins, duplicatePins); + + // if(pinValues.BLDC_Enable_PIN > -1) { + // pinDupeIndex = assignedPins.findIndex(x => x.pin === pinValues.BLDC_Enable_PIN); + // if(pinDupeIndex > -1) + // duplicatePins.push("Enable pin and "+assignedPins[pinDupeIndex].name); + // assignedPins.push({name:"Enable", pin:pinValues.BLDC_Enable_PIN}); + // } + validatePin(pinValues.BLDC_Enable_PIN, "Enable", assignedPins, duplicatePins); + + // if(pinValues.BLDC_PWMchannel1_PIN > -1) { + // pinDupeIndex = assignedPins.findIndex(x => x.pin === pinValues.BLDC_PWMchannel1_PIN); + // if(pinDupeIndex > -1) + // duplicatePins.push("PWMchannel1 pin and "+assignedPins[pinDupeIndex].name); + // if(validPWMpins.indexOf(pinValues.BLDC_PWMchannel1_PIN) == -1) + // pwmErrors.push("PWMchannel1 pin: "+pinValues.BLDC_PWMchannel1_PIN); + // assignedPins.push({name:"PWMchannel1", pin:pinValues.BLDC_PWMchannel1_PIN}); + // } + validatePWMPin(pinValues.rightPin, "PWMchannel1", assignedPins, duplicatePins, pwmErrors); + + // if(pinValues.BLDC_PWMchannel2_PIN > -1) { + // pinDupeIndex = assignedPins.findIndex(x => x.pin === pinValues.BLDC_PWMchannel2_PIN); + // if(pinDupeIndex > -1) + // duplicatePins.push("PWMchannel2 pin and "+assignedPins[pinDupeIndex].name); + // if(validPWMpins.indexOf(pinValues.BLDC_PWMchannel2_PIN) == -1) + // pwmErrors.push("PWMchannel2 pin: "+pinValues.BLDC_PWMchannel2_PIN); + // assignedPins.push({name:"PWMchannel2", pin:pinValues.BLDC_PWMchannel2_PIN}); + // } + validatePWMPin(pinValues.rightPin, "PWMchannel2", assignedPins, duplicatePins, pwmErrors); + + // if(pinValues.BLDC_PWMchannel3_PIN > -1) { + // pinDupeIndex = assignedPins.findIndex(x => x.pin === pinValues.BLDC_PWMchannel3_PIN); + // if(pinDupeIndex > -1) + // duplicatePins.push("PWMchannel3 pin and "+assignedPins[pinDupeIndex].name); + // if(validPWMpins.indexOf(pinValues.BLDC_PWMchannel3_PIN) == -1) + // pwmErrors.push("PWMchannel3pin: "+pinValues.BLDC_PWMchannel3_PIN); + // assignedPins.push({name:"PWMchannel3", pin:pinValues.BLDC_PWMchannel3_PIN}); + // } + validatePWMPin(pinValues.BLDC_PWMchannel3_PIN, "PWMchannel3", assignedPins, duplicatePins, pwmErrors); + + if(userSettings["BLDC_UseHallSensor"]) { + // if(pinValues.BLDC_HallEffect_PIN > -1) { + // pinDupeIndex = assignedPins.findIndex(x => x.pin === pinValues.BLDC_HallEffect_PIN); + // if(pinDupeIndex > -1) + // duplicatePins.push("Hall effect pin and "+assignedPins[pinDupeIndex].name); + // assignedPins.push({name:"HallEffect", pin:pinValues.BLDC_HallEffect_PIN}); + // } + validatePin(pinValues.BLDC_HallEffect_PIN, "HallEffect", assignedPins, duplicatePins); + } + + validateCommonPWMPins(assignedPins, duplicatePins, pinValues, pwmErrors); + + var invalidPins = []; + validateNonPWMPins(assignedPins, duplicatePins, invalidPins, pinValues); + + if (duplicatePins.length || pwmErrors.length || invalidPins.length) { + var errorString = "
Pins NOT saved due to invalid input.
"; + if(duplicatePins.length ) + errorString += "
The following pins are duplicated:
"+duplicatePins.join("
")+"
"; + if(invalidPins.length) { + if(duplicatePins.length) + errorString += "
"; + errorString += "
The following pins are invalid:
"+invalidPins.join("
")+"
"; + } + if (pwmErrors.length) { + if(duplicatePins.length || invalidPins.length) { + errorString += "
"; + } + errorString += "
The following pins are invalid PWM pins:
"+pwmErrors.join("
")+"
"; + } + + errorString += "
"; + showError(errorString); + return undefined; } -} \ No newline at end of file + return pinValues; +} From 9431743da8c61cd19f8f297a9d7ed12290f3f453 Mon Sep 17 00:00:00 2001 From: Millibyte Products Date: Thu, 7 May 2026 15:56:32 -0400 Subject: [PATCH 32/42] Wire up BLDCMotor.setupPins() in setPinoutSettings The BLDC branch in setPinoutSettings was a no-op with a comment claiming pins were populated from user settings, but they weren't populated anywhere else either - the BLDC pin inputs in the web UI were always blank. Call BLDCMotor.setupPins() to populate them from /pins like the pre-merge build. --- ESP32/data/www/index-min.html.gz | Bin 50471 -> 50480 bytes ESP32/dataEdit/www/settings.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/ESP32/data/www/index-min.html.gz b/ESP32/data/www/index-min.html.gz index 72b428a8b5dee596ee024348b280f79d29c1fe83..f29568f9235050ed0a74abfdbebfbd9c8d5abc18 100644 GIT binary patch delta 24915 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 T({U2cvc3NW#jzim0!{+}U1se( delta 24934 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
6i3hGVCpRY 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 i-LwC)dqtk_UV&X^|K-o=I0 Date: Thu, 7 May 2026 17:24:47 -0400 Subject: [PATCH 33/42] SSR1PCB: populate BLDC PID/LowPass, lock MT6701 SSI encoder, hide unused PWM resolution UI - settingsFactory: register BLDC_PIDProportionalConstant and BLDC_LowPassFilter so they appear in /settings JSON and persist. - BLDCHandler0_3: load PID P-const and low-pass filter from settings (m_pConst/m_lowPass) replacing compile-time P_CONST/LOW_PASS macros; force encoder type to MT6701 SSI (SSR1PCB always ships with MT6701, ignore stale BLDC_Encoder=0/NONE in saved settings). - bldc-motor.js: populate and persist BLDC_PIDProportionalConstant and BLDC_LowPassFilter; for SSR1PCB lock the BLDC_Encoder dropdown to MT6701 (=1) and force-save corrected value. - settings.js: declare module-level upDateTimeout to fix ReferenceError in updateBLDCPins/updatePins; remove servo/vibe/lube/heater/case-fan resolution setters that were assigning undefined into the inputs. - index.html: hide servo/vibe/lube/heater/case-fan resolution rows (PwmManager auto-derives resolution per timer). - scripts/tcode-stroke-test.ps1: helper script that drives the L0 axis Max/Min/Center over T-Code on a serial port. --- ESP32/data/www/index-min.html.gz | Bin 50480 -> 50589 bytes ESP32/dataEdit/www/bldc-motor.js | 44 ++++++++++++++++++++------ ESP32/dataEdit/www/index.html | 10 +++--- ESP32/dataEdit/www/settings.js | 19 +++-------- ESP32/lib/settingsFactory.h | 4 ++- ESP32/scripts/tcode-stroke-test.ps1 | 37 ++++++++++++++++++++++ ESP32/src/TCode/v0.3/BLDCHandler0_3.h | 28 +++++++++++----- 7 files changed, 104 insertions(+), 38 deletions(-) create mode 100644 ESP32/scripts/tcode-stroke-test.ps1 diff --git a/ESP32/data/www/index-min.html.gz b/ESP32/data/www/index-min.html.gz index f29568f9235050ed0a74abfdbebfbd9c8d5abc18..c87ed03dafaa3eeeeb7f9e978dde2e1c70c18494 100644 GIT binary patch literal 50589 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⋘r9 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
Z4 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 S9Vg*DJN#cws@%VNPXho-gW%=> literal 50480 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 N{{_XdADIG90|3-Q;@AKH diff --git a/ESP32/dataEdit/www/bldc-motor.js b/ESP32/dataEdit/www/bldc-motor.js index ff4c004..08b0131 100644 --- a/ESP32/dataEdit/www/bldc-motor.js +++ b/ESP32/dataEdit/www/bldc-motor.js @@ -22,7 +22,23 @@ SOFTWARE. */ BLDCMotor = { setup() { - document.getElementById("BLDC_Encoder").value = userSettings["BLDC_Encoder"]; + var encoderEl = document.getElementById("BLDC_Encoder"); + encoderEl.value = userSettings["BLDC_Encoder"]; + // SSR1PCB always ships with an MT6701 SSI encoder; lock the dropdown + // to that selection (BLDCEncoderType::MT6701 = 1). + if (typeof isBoardType === "function" && isBoardType(BoardType.SSR1PCB)) { + encoderEl.value = 1; // MT6701 SSI + encoderEl.disabled = true; + encoderEl.title = "SSR1PCB ships with an MT6701 SSI encoder; this is fixed."; + // Persist if the saved value is wrong so other consumers see MT6701 + if (userSettings["BLDC_Encoder"] !== 1) { + userSettings["BLDC_Encoder"] = 1; + if (typeof updateUserSettings === "function") updateUserSettings(); + } + } else { + encoderEl.disabled = false; + encoderEl.title = ""; + } document.getElementById("BLDC_UseHallSensor").checked = userSettings["BLDC_UseHallSensor"]; document.getElementById("BLDC_Pulley_Circumference").value = userSettings["BLDC_Pulley_Circumference"]; document.getElementById("BLDC_MotorA_VoltageLimit").value = Utils.round2(userSettings["BLDC_MotorA_VoltageLimit"]); @@ -32,6 +48,10 @@ BLDCMotor = { document.getElementById("BLDC_MotorA_ParametersKnown").checked = userSettings["BLDC_MotorA_ParametersKnown"]; document.getElementById("BLDC_RailLength").value = userSettings["BLDC_RailLength"]; document.getElementById("BLDC_StrokeLength").value = userSettings["BLDC_StrokeLength"]; + if (userSettings["BLDC_PIDProportionalConstant"] !== undefined) + document.getElementById("BLDC_PIDProportionalConstant").value = userSettings["BLDC_PIDProportionalConstant"]; + if (userSettings["BLDC_LowPassFilter"] !== undefined) + document.getElementById("BLDC_LowPassFilter").value = userSettings["BLDC_LowPassFilter"]; toggleBLDCEncoderOptions(); Utils.toggleControlVisibilityByID("HallEffect", userSettings["BLDC_UseHallSensor"]); @@ -59,17 +79,23 @@ function updateBLDCSettings() { userSettings["BLDC_MotorA_ParametersKnown"] = document.getElementById("BLDC_MotorA_ParametersKnown").checked; userSettings["BLDC_RailLength"] = parseInt(document.getElementById('BLDC_RailLength').value); userSettings["BLDC_StrokeLength"] = parseInt(document.getElementById('BLDC_StrokeLength').value); + var pidEl = document.getElementById('BLDC_PIDProportionalConstant'); + if (pidEl && pidEl.value !== "") + userSettings["BLDC_PIDProportionalConstant"] = parseFloat(pidEl.value); + var lpEl = document.getElementById('BLDC_LowPassFilter'); + if (lpEl && lpEl.value !== "") + userSettings["BLDC_LowPassFilter"] = parseFloat(lpEl.value); Utils.toggleControlVisibilityByID("ZeroElecAngle", userSettings["BLDC_MotorA_ParametersKnown"]); setRestartRequired(); updateUserSettings(); } function updateBLDCPins() { - if(upDateTimeout !== null) + if(upDateTimeout !== null) { clearTimeout(upDateTimeout); } - upDateTimeout = setTimeout(() => + upDateTimeout = setTimeout(() => { var pinValues = validateBLDCPins(); if(pinValues) { @@ -101,7 +127,7 @@ function getBLDCPinValues() { } function validateBLDCPins() { - clearErrors("pinValidation"); + clearErrors("pinValidation"); var assignedPins = []; var duplicatePins = []; var pwmErrors = []; @@ -125,7 +151,7 @@ function validateBLDCPins() { } } } - else + else { //assignedPins.push({name:"SPI1", pin:5}); assignedPins.push({name:"SPI CLK", pin:18}); @@ -143,7 +169,7 @@ function validateBLDCPins() { // } validatePin(pinValues.BLDC_Encoder_PIN, "Encoder", assignedPins, duplicatePins); - + // if(pinValues.BLDC_ChipSelect_PIN > -1) { // pinDupeIndex = assignedPins.findIndex(x => x.pin === pinValues.BLDC_ChipSelect_PIN); // if(pinDupeIndex > -1) @@ -199,7 +225,7 @@ function validateBLDCPins() { // } validatePin(pinValues.BLDC_HallEffect_PIN, "HallEffect", assignedPins, duplicatePins); } - + validateCommonPWMPins(assignedPins, duplicatePins, pinValues, pwmErrors); var invalidPins = []; @@ -217,10 +243,10 @@ function validateBLDCPins() { if (pwmErrors.length) { if(duplicatePins.length || invalidPins.length) { errorString += "
"; - } + } errorString += "
The following pins are invalid PWM pins:
"+pwmErrors.join("
")+"
"; } - + errorString += ""; showError(errorString); return undefined; diff --git a/ESP32/dataEdit/www/index.html b/ESP32/dataEdit/www/index.html index 10552f4..d40a365 100644 --- a/ESP32/dataEdit/www/index.html +++ b/ESP32/dataEdit/www/index.html @@ -444,7 +444,7 @@

General

-
+
-
+
-
+
-
+
--> -
+