diff --git a/CHANGELOG.md b/CHANGELOG.md index de53ec5f5..abbff6c0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,9 @@ All notable changes to this project will be documented in this file. ## [13.3.0.2] ### Added - HASPmota type `chart` (#20372) -- Berry add support for `tcpclientasync` in `tcpserver` -- Berry add `tasmota.urlbecload(url:string) -> bool` +- Berry add support for `tcpclientasync` in `tcpserver` (#20401) +- Berry add `tasmota.urlbecload(url:string) -> bool` (#20412) +- GPIO Viewer to see realtime GPIO states. Enable with define USE_GPIO_VIEWER ### Breaking Changed diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6198b8991..de5928873 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -121,10 +121,14 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm - Support for CST816S touch interface [#20213](https://github.com/arendst/Tasmota/issues/20213) - Support for Sonoff Basic R4 Magic Switch [#20247](https://github.com/arendst/Tasmota/issues/20247) - Display of active drivers using command ``status 4`` +- GPIO Viewer to see realtime GPIO states. Enable with define USE_GPIO_VIEWER - NeoPool hydrolysis FL1 and Redox flag [#20258](https://github.com/arendst/Tasmota/issues/20258) - Support negative power on BL0942 using index 5..8 [#20322](https://github.com/arendst/Tasmota/issues/20322) - ESP32 used UART information +- ESP32 experimental support GPIOViewer when ``define USE_ESP32_GPIO_VIEWER`` is enabled - Berry `introspect.set()` for class attributes [#20339](https://github.com/arendst/Tasmota/issues/20339) +- Berry add support for `tcpclientasync` in `tcpserver` [#20401](https://github.com/arendst/Tasmota/issues/20401) +- Berry add `tasmota.urlbecload(url:string) -> bool` [#20412](https://github.com/arendst/Tasmota/issues/20412) - HASPmota `haspmota.page_show()` to change page [#20333](https://github.com/arendst/Tasmota/issues/20333) - HASPmota type `chart` [#20372](https://github.com/arendst/Tasmota/issues/20372) - Matter support for password for remote Tasmota devices [#20296](https://github.com/arendst/Tasmota/issues/20296) diff --git a/tasmota/tasmota_xdrv_driver/xdrv_121_gpioviewer.ino b/tasmota/tasmota_xdrv_driver/xdrv_121_gpioviewer.ino new file mode 100644 index 000000000..e2295044d --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_121_gpioviewer.ino @@ -0,0 +1,351 @@ +/* + xdrv_121_gpioviewer.ino - GPIOViewer for Tasmota + + SPDX-FileCopyrightText: 2024 Theo Arends + + SPDX-License-Identifier: GPL-3.0-only +*/ + +#ifdef USE_GPIO_VIEWER +/*********************************************************************************************\ + * GPIOViewer support + * + * Open webpage :8080 and watch realtime GPIO states +\*********************************************************************************************/ + +#define XDRV_121 121 + +#define GV_PORT 8080 +#define GV_SAMPLING_INTERVAL 100 // Relates to FUNC_EVERY_100_MSECOND + +const char *GVRelease = "1.0.5"; + +#define GV_BASE_URL "https://thelastoutpostworkshop.github.io/microcontroller_devkit/gpio_viewer/assets/" + +#ifdef ESP32 +const int GVMaxGPIOPins = 49; +// Global variables to capture PMW pins +const int GVMaxChannels = 64; +#endif // ESP32 +#ifdef ESP8266 +const int GVMaxGPIOPins = 18; +// Global variables to capture PMW pins +const int GVMaxChannels = MAX_PWMS; +#endif // ESP8266 + +const char HTTP_GV_PAGE[] PROGMEM = + "" + "" + "" + "Tasmota GPIO State" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "
" + "
" + // Image + "
" + "
" + "Board Image" + "
" + "
" + "
" + "
" + "" + ""; + +enum GVPinTypes { + digitalPin = 0, + PWMPin = 1, + analogPin = 2 +}; + +struct { + WiFiClient WebClient; + ESP8266WebServer *WebServer; + int freeHeap; + uint32_t lastPinStates[GVMaxGPIOPins]; + int ledcChannelPin[GVMaxChannels][2]; + int ledcChannelPinCount; + int ledcChannelResolution[GVMaxChannels][2]; + int ledcChannelResolutionCount; + bool first; + bool active; +} GV; + +String GVFormatBytes(size_t bytes) { + if (bytes < 1024) { + return String(bytes) + " B"; + } + else if (bytes < (1024 * 1024)) { + return String(bytes / 1024.0, 2) + " KB"; + } + else { + return String(bytes / 1024.0 / 1024.0, 2) + " MB"; + } +} + +void GVPrintPWNTraps(void) { +#ifdef ESP32 + for (uint32_t pin = 0; pin < GVMaxChannels; pin++) { + int32_t channel = analogGetChannel2(pin); + if (channel > -1) { + GV.ledcChannelPin[GV.ledcChannelPinCount][0] = pin; + GV.ledcChannelPin[GV.ledcChannelPinCount++][1] = channel; + uint8_t resolution = ledcReadResolution(channel); + GV.ledcChannelResolution[GV.ledcChannelResolutionCount][0] = channel; + GV.ledcChannelResolution[GV.ledcChannelResolutionCount++][1] = resolution; + } + } +#endif // ESP32 +#ifdef ESP8266 + uint32_t pwm_range = Settings->pwm_range + 1; + uint32_t resolution = 0; + while (pwm_range) { + resolution++; + pwm_range >>= 1; + } + for (uint32_t i = 0; i < MAX_PWMS; i++) { // Basic PWM control only + if (PinUsed(GPIO_PWM1, i)) { + int32_t channel = i; + GV.ledcChannelPin[GV.ledcChannelPinCount][0] = Pin(GPIO_PWM1, i); + GV.ledcChannelPin[GV.ledcChannelPinCount++][1] = channel; + GV.ledcChannelResolution[GV.ledcChannelResolutionCount][0] = channel; + GV.ledcChannelResolution[GV.ledcChannelResolutionCount++][1] = resolution; + } + } +#endif // ESP8266 + + AddLog(LOG_LEVEL_DEBUG, "IOV: %d pins are PWM", GV.ledcChannelPinCount); + for (int i = 0; i < GV.ledcChannelPinCount; i++) { + AddLog(LOG_LEVEL_DEBUG, "IOV: pin %d is using channel %d", GV.ledcChannelPin[i][0], GV.ledcChannelPin[i][1]); + } + AddLog(LOG_LEVEL_DEBUG, "IOV: %d channels are used", GV.ledcChannelResolutionCount); + for (int i = 0; i < GV.ledcChannelResolutionCount; i++) { + AddLog(LOG_LEVEL_DEBUG, "IOV: channel %d resolution is %d bits", GV.ledcChannelResolution[i][0], GV.ledcChannelResolution[i][1]); + } +} + +int GVGetLedcChannelForPin(int pin) { + for (int i = 0; i < GV.ledcChannelPinCount; i++) { + if (GV.ledcChannelPin[i][0] == pin) { + return GV.ledcChannelPin[i][1]; + } + } + return -1; // Pin not found, return -1 to indicate no channel is associated +} + +int GVGetChannelResolution(int channel) { + for (int i = 0; i < GV.ledcChannelResolutionCount; i++) { + if (GV.ledcChannelResolution[i][0] == channel) { + return GV.ledcChannelResolution[i][1]; + } + } + return -1; // Pin not found, return -1 to indicate no channel is associated +} + +int GVMapLedcReadTo8Bit(int channel, uint32_t *originalValue) { + uint32_t maxDutyCycle = (1 << GVGetChannelResolution(channel)) - 1; + +#ifdef ESP32 + *originalValue = ledcRead(channel); +#endif // ESP32 +#ifdef ESP8266 + if (17 == channel) { + maxDutyCycle = (1 << 10) - 1; // 10 = ANALOG_RESOLUTION + *originalValue = AdcRead(channel, 2); + } else { + *originalValue = (channel < MAX_PWMS_LEGACY) ? Settings->pwm_value[channel] : Settings->pwm_value_ext[channel - MAX_PWMS_LEGACY]; + } +#endif + + return map(*originalValue, 0, maxDutyCycle, 0, 255); +} + +int GVReadGPIO(int gpioNum, uint32_t *originalValue, uint32_t *pintype) { + int channel = GVGetLedcChannelForPin(gpioNum); + int value; + if (channel != -1) { + // This is a PWM Pin + value = GVMapLedcReadTo8Bit(channel, originalValue); + *pintype = PWMPin; + return value; + } +#ifdef ESP32 + uint8_t analogChannel = analogGetChannel2(gpioNum); + if (analogChannel != 0 && analogChannel != 255) { +#endif // ESP32 +#ifdef ESP8266 + uint8_t analogChannel = gpioNum; + if (17 == analogChannel) { +#endif // ESP8266 + // This is an analog pin + // Serial.printf("A Pin %d value=%d,channel=%d\n", gpioNum, value,analogChannel); + + value = GVMapLedcReadTo8Bit(analogChannel, originalValue); + *pintype = analogPin; + return value; + } + else { + // This is a digital pin + *pintype = digitalPin; + value = digitalRead(gpioNum); + *originalValue = value; + if (value == 1) { + return 256; + } + return 0; + } +} + +void GVResetStatePins(void) { + uint32_t originalValue; + uint32_t pintype; + AddLog(LOG_LEVEL_INFO, "IOV: GPIOViewer Connected, sampling interval is " STR(GV_SAMPLING_INTERVAL) "ms"); + + for (int i = 0; i < GVMaxGPIOPins; i++) { + GV.lastPinStates[i] = GVReadGPIO(i, &originalValue, &pintype); + } +} + +//void GVEventsSend(const char *message, const char *event=NULL, uint32_t id=0, uint32_t reconnect=0); +void GVEventsSend(const char *message, const char *event, uint32_t id) { + if (GV.WebClient.connected()) { + // generateEventMessage() in AsyncEventSource.cpp +// GV.WebClient.printf_P(PSTR("retry: 0\r\nid: %u\r\nevent: %s\r\ndata: %s\r\n\r\n"), id, event, message); + GV.WebClient.printf_P(PSTR("id: %u\r\nevent: %s\r\ndata: %s\r\n\r\n"), id, event, message); + } +} + +// Monitor GPIO Values +void GVMonitorTask(void) { + uint32_t originalValue; + uint32_t pintype; + + String jsonMessage = "{"; + bool hasChanges = false; + + for (int i = 0; i < GVMaxGPIOPins; i++) { + int currentState = GVReadGPIO(i, &originalValue, &pintype); + + if (originalValue != GV.lastPinStates[i]) { + if (hasChanges) { + jsonMessage += ", "; + } + jsonMessage += "\"" + String(i) + "\": {\"s\": " + currentState + ", \"v\": " + originalValue + ", \"t\": " + pintype + "}"; + GV.lastPinStates[i] = currentState; + hasChanges = true; + } + } + + jsonMessage += "}"; + + if (hasChanges) { +// events->send(jsonMessage.c_str(), "gpio-state", millis()); + GVEventsSend(jsonMessage.c_str(), "gpio-state", millis()); + } + + uint32_t heap = ESP_getFreeHeap(); + if (heap != GV.freeHeap) { + GV.freeHeap = heap; +// events->send(GVFormatBytes(GV.freeHeap).c_str(), "free_heap", millis()); + GVEventsSend(GVFormatBytes(GV.freeHeap).c_str(), "free_heap", millis()); + } +} + +void GVBegin(void) { + GVPrintPWNTraps(); + + GV.WebServer = new ESP8266WebServer(GV_PORT); + +// GV.WebServer->setContentLength(CONTENT_LENGTH_UNKNOWN); // the payload can go on forever + + // Set CORS headers for global responses + GV.WebServer->sendHeader("Access-Control-Allow-Origin", "*"); + GV.WebServer->sendHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + GV.WebServer->sendHeader("Access-Control-Allow-Headers", "Content-Type"); + + GV.WebServer->on("/events", GVHandleEvents); + GV.WebServer->on("/", GVHandleRoot); + GV.WebServer->on("/release", GVHandleRelease); + + GV.WebServer->begin(); +} + +void GVHandleEvents(void) { + if (!GV.first) { + GVResetStatePins(); + GV.first = true; + + GV.WebClient = GV.WebServer->client(); + GV.WebClient.setNoDelay(true); +// GV.WebClient.setSync(true); + + GV.WebServer->setContentLength(CONTENT_LENGTH_UNKNOWN); // The payload can go on forever + GV.WebServer->sendContent_P(PSTR("HTTP/1.1 200 OK\nContent-Type: text/event-stream;\nConnection: keep-alive\nCache-Control: no-cache\nAccess-Control-Allow-Origin: *\n\n")); + } +} + +void GVHandleRoot(void) { + char* content = ext_snprintf_malloc_P(HTTP_GV_PAGE, + WiFi.localIP().toString().c_str(), + WiFi.localIP().toString().c_str(), + GVFormatBytes(ESP.getFreeSketchSpace()).c_str()); + if (content == nullptr) { return; } // Avoid crash + + GV.WebServer->send_P(200, "text/html", content); + free(content); +} + +void GVHandleRelease(void) { + String jsonResponse = "{\"release\": \"" + String(GVRelease) + "\"}"; + + GV.WebServer->send(200, "application/json", jsonResponse); +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xdrv121(uint32_t function) { + bool result = false; + + if (GV.active) { + switch (function) { + case FUNC_LOOP: + if (GV.WebServer) { GV.WebServer->handleClient(); } + break; + case FUNC_EVERY_100_MSECOND: + if (GV.first) { GVMonitorTask(); } + break; + case FUNC_ACTIVE: + result = true; + break; + } + } else { + switch (function) { + case FUNC_EVERY_SECOND: + if (!TasmotaGlobal.global_state.network_down) { + GVBegin(); + GV.active = true; + } + break; + } + + } + return result; +} + +#endif // USE_GPIO_VIEWER