diff --git a/CHANGELOG.md b/CHANGELOG.md index cf2e4df4b..de07e9c3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. ## [14.6.0.1] ### Added +- Command `JsonPP 0..7` to enable (>0) JSON Pretty Print on user interfaces and set number of indents ### Breaking Changed diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c51e5a4a9..f727c6625 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -116,6 +116,7 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm ## Changelog v14.6.0.1 ### Added +- Command `JsonPP 0..7` to enable (>0) JSON Pretty Print on user interfaces and set number of indents ### Breaking Changed diff --git a/tasmota/include/i18n.h b/tasmota/include/i18n.h index a99ae590b..b9f96eccd 100644 --- a/tasmota/include/i18n.h +++ b/tasmota/include/i18n.h @@ -272,6 +272,7 @@ // Commands tasmota.ino #define D_CMND_BACKLOG "Backlog" #define D_CMND_JSON "Json" +#define D_CMND_JSON_PP "JsonPP" #define D_CMND_DELAY "Delay" #define D_CMND_NODELAY "NoDelay" #define D_CMND_STATUS "Status" diff --git a/tasmota/include/tasconsole.h b/tasmota/include/tasconsole.h index f42b588eb..62aaf103f 100644 --- a/tasmota/include/tasconsole.h +++ b/tasmota/include/tasconsole.h @@ -16,9 +16,10 @@ class TASCONSOLE { virtual void begin(uint32_t) = 0; virtual void flush() = 0; virtual size_t println() = 0; - virtual size_t print(char *) = 0; - virtual size_t printf(const char*, char *, const char*&, const char*&, const char*&) = 0; - virtual size_t printf(char *) = 0; + virtual size_t println(const char*) = 0; + virtual size_t print(char*) = 0; + virtual size_t printf(const char*, char*, const char*&, const char*&, const char*&) = 0; + virtual size_t printf(char*) = 0; virtual size_t read() = 0; virtual size_t write(uint8_t) = 0; virtual size_t write(const uint8_t *buf, size_t size) = 0; @@ -49,7 +50,11 @@ public: return object->println(); } - size_t print(char * string) { + size_t println(const char *string) { + return object->println(string); + } + + size_t print(char *string) { return object->print(string); } @@ -96,10 +101,15 @@ public: object.flush(); } - size_t println() override { + size_t println() override { return object.println(); } - size_t print(char * string) override { + + size_t println(const char *string) override { + return object.println(string); + } + + size_t print(char *string) override { return object.print(string); } diff --git a/tasmota/include/tasmota_types.h b/tasmota/include/tasmota_types.h index d553242ad..da7f9446e 100644 --- a/tasmota/include/tasmota_types.h +++ b/tasmota/include/tasmota_types.h @@ -252,9 +252,7 @@ typedef union { uint32_t spare13 : 1; // bit 13 uint32_t spare14 : 1; // bit 14 uint32_t spare15 : 1; // bit 15 - uint32_t spare16 : 1; // bit 16 - uint32_t spare17 : 1; // bit 17 - uint32_t spare18 : 1; // bit 18 + uint32_t json_pretty_print : 3; // bit 16.18 (v14.6.0.1) - JSON pretty print log data no or indent uint32_t dali_group_sliders : 5; // bit 19.23 (v14.3.0.3) - (DALI) Number of group sliders 0 to 16 uint32_t FTP_Mode : 2; // bit 24/25 uint32_t tariff_forced : 2; // bit 26/27 (v12.4.0.2) - Energy forced tariff : 0=tariff change on time, 1|2=tariff forced diff --git a/tasmota/tasmota_support/support.ino b/tasmota/tasmota_support/support.ino index d7cc80ac5..4f9e151ed 100755 --- a/tasmota/tasmota_support/support.ino +++ b/tasmota/tasmota_support/support.ino @@ -2611,6 +2611,95 @@ bool GetLog(uint32_t req_loglevel, uint32_t* index_p, char** entry_pp, size_t* l return false; } +bool LogDataJsonPrettyPrint(const char *log_line, uint32_t log_data_len, std::function println) { + // log_line: + // 14:49:36.123 MQTT: stat/wemos5/RESULT = {"POWER":"OFF"} + // 14:30:16.749-172/38 MQT: tele/atomlite3/INFO3 = {"Info3":{"RestartReason":"Vbat power on reset","BootCount":74}} + + if (!Settings->mbflag2.json_pretty_print) { return false; } // [JsonPP] Number of indents + char *bch = (char*)memchr(log_line, '{', log_data_len); + if (!bch) { return false; } // No JSON data + + uint32_t pos_brace = bch - log_line; + uint32_t cnt_brace = 0; // {} + uint32_t len_mxtime = strchr(log_line, ' ') - log_line +2; + uint32_t pos_value_pair = pos_brace; + uint32_t cnt_bracket = 0; // [] + uint32_t cnt_Indent = 0; // indent + bool quotes = false; // "" + bool bracket_comma = false; + bool pls_print = false; + for (uint32_t i = pos_brace; i < log_data_len; i++) { + char curchar = log_line[i]; + char nxtchar = log_line[i +1]; + cnt_Indent = cnt_brace + cnt_bracket; + if (curchar == '{') { + cnt_brace++; + pls_print = true; + } + else if (cnt_brace) { + if (nxtchar == '}') { + pls_print = true; + } + if (curchar == '}') { + cnt_brace--; + if (cnt_brace) { + if (nxtchar != ',') { + pls_print = true; + cnt_Indent = cnt_brace + cnt_bracket; + } + } else { + pls_print = true; + cnt_Indent = 0; + } + } + else if (curchar == '[') { + cnt_bracket++; + if (nxtchar == '[') { + pls_print = true; + } + } + else if (curchar == ']') { + cnt_bracket--; + if (nxtchar == ',') { + bracket_comma = true; + } + else { + pls_print = true; + if ((nxtchar == ']') || (nxtchar == '}')) { + cnt_Indent = cnt_brace + cnt_bracket; + } + } + } + else if (curchar == '"') { + quotes ^= 1; + } + else if (curchar == ',') { + if (!quotes && (!cnt_bracket || bracket_comma)) { + bracket_comma = false; + pls_print = true; + } + } + } + + if (pls_print) { + pls_print = false; + uint32_t len_id = (pos_brace == i) ? pos_brace +1 : len_mxtime; + uint32_t len_indent = cnt_Indent * Settings->mbflag2.json_pretty_print; + uint32_t len_value_pair = (i - pos_value_pair) +1; + uint32_t len_full = len_id + len_indent + len_value_pair +1; + + char line[len_full]; // Known max value pair size is 152 + strlcpy(line, log_line, len_id); // Repeat mxtime + sprintf(line, "%s%*s", line, len_indent, ""); // Add space indent + strncat(line, log_line + pos_value_pair, len_value_pair); + println(line, strlen(line)); // Callback for output + pos_value_pair = i +1; + } + } + return true; +} + uint32_t HighestLogLevel(void) { uint32_t highest_loglevel = TasmotaGlobal.seriallog_level; if (Settings->seriallog_level > highest_loglevel) { highest_loglevel = Settings->seriallog_level; } @@ -2660,7 +2749,9 @@ void AddLogData(uint32_t loglevel, const char* log_data, const char* log_data_pa if ((loglevel <= TasmotaGlobal.seriallog_level) && (TasmotaGlobal.masterlog_level <= TasmotaGlobal.seriallog_level)) { - TasConsole.printf("%s%s%s%s\r\n", mxtime, log_data, log_data_payload, log_data_retained); + if (!Settings->mbflag2.json_pretty_print || !strchr(log_data_payload, '{')) { + TasConsole.printf("%s%s%s%s\r\n", mxtime, log_data, log_data_payload, log_data_retained); + } } if (!TasmotaGlobal.log_buffer) { return; } // Leave now if there is no buffer available @@ -2707,6 +2798,14 @@ void AddLogData(uint32_t loglevel, const char* log_data, const char* log_data_pa // These calls fail to show initial logging log_line += 2; // Skip log_buffer_pointer and loglevel + // 14:49:36.123 MQTT: stat/wemos5/RESULT = {"POWER":"OFF"} + // 14:30:16.749-172/38 MQT: tele/atomlite3/INFO3 = {"Info3":{"RestartReason":"Vbat power on reset","BootCount":74}} + + if ((loglevel <= TasmotaGlobal.seriallog_level) && + (TasmotaGlobal.masterlog_level <= TasmotaGlobal.seriallog_level)) { + LogDataJsonPrettyPrint(log_line, log_data_len, TasConsoleLDJsonPPCb); + } + #ifdef USE_SERIAL_BRIDGE if (loglevel <= TasmotaGlobal.seriallog_level) { SerialBridgeWrite(log_line, log_data_len); @@ -2723,6 +2822,10 @@ void AddLogData(uint32_t loglevel, const char* log_data, const char* log_data_pa } } +void TasConsoleLDJsonPPCb(const char* line, uint32_t len) { + TasConsole.println(line); +} + void AddLog(uint32_t loglevel, PGM_P formatP, ...) { #ifdef ESP32 if (xPortInIsrContext()) { diff --git a/tasmota/tasmota_support/support_command.ino b/tasmota/tasmota_support/support_command.ino index 1c28dc2f6..1bb925120 100644 --- a/tasmota/tasmota_support/support_command.ino +++ b/tasmota/tasmota_support/support_command.ino @@ -29,18 +29,18 @@ const char kTasmotaCommands[] PROGMEM = "|" // No prefix D_CMND_VOLTAGE_RESOLUTION "|" D_CMND_FREQUENCY_RESOLUTION "|" D_CMND_CURRENT_RESOLUTION "|" D_CMND_ENERGY_RESOLUTION "|" D_CMND_WEIGHT_RESOLUTION "|" D_CMND_MODULE "|" D_CMND_MODULES "|" D_CMND_GPIO "|" D_CMND_GPIOREAD "|" D_CMND_GPIOS "|" D_CMND_TEMPLATE "|" D_CMND_PWM "|" D_CMND_PWMFREQUENCY "|" D_CMND_PWMRANGE "|" D_CMND_BUTTONDEBOUNCE "|" D_CMND_SWITCHDEBOUNCE "|" D_CMND_SYSLOG "|" D_CMND_LOGHOST "|" D_CMND_LOGPORT "|" -#ifdef USE_UFILESYS - D_CMND_FILELOG "|" -#endif // USE_UFILESYS D_CMND_SERIALBUFFER "|" D_CMND_SERIALSEND "|" D_CMND_BAUDRATE "|" D_CMND_SERIALCONFIG "|" D_CMND_SERIALDELIMITER "|" D_CMND_IPADDRESS "|" D_CMND_NTPSERVER "|" D_CMND_AP "|" D_CMND_SSID "|" D_CMND_PASSWORD "|" D_CMND_HOSTNAME "|" D_CMND_WIFICONFIG "|" D_CMND_WIFI "|" D_CMND_DNSTIMEOUT "|" - D_CMND_DEVICENAME "|" D_CMND_FN "|" D_CMND_FRIENDLYNAME "|" D_CMND_SWITCHMODE "|" D_CMND_INTERLOCK "|" D_CMND_TELEPERIOD "|" D_CMND_RESET "|" D_CMND_TIME "|" D_CMND_TIMEZONE "|" D_CMND_TIMESTD "|" - D_CMND_TIMEDST "|" D_CMND_ALTITUDE "|" D_CMND_LEDPOWER "|" D_CMND_LEDSTATE "|" D_CMND_LEDMASK "|" D_CMND_LEDPWM_ON "|" D_CMND_LEDPWM_OFF "|" D_CMND_LEDPWM_MODE "|" - D_CMND_WIFIPOWER "|" D_CMND_TEMPOFFSET "|" D_CMND_HUMOFFSET "|" D_CMND_SPEEDUNIT "|" D_CMND_GLOBAL_TEMP "|" D_CMND_GLOBAL_HUM"|" D_CMND_GLOBAL_PRESS "|" D_CMND_SWITCHTEXT "|" D_CMND_WIFISCAN "|" D_CMND_WIFITEST "|" - D_CMND_ZIGBEE_BATTPERCENT "|" + D_CMND_DEVICENAME "|" D_CMND_FN "|" D_CMND_FRIENDLYNAME "|" D_CMND_SWITCHMODE "|" D_CMND_INTERLOCK "|" D_CMND_TELEPERIOD "|" D_CMND_RESET "|" + D_CMND_TIME "|" D_CMND_TIMEZONE "|" D_CMND_TIMESTD "|" D_CMND_TIMEDST "|" D_CMND_ALTITUDE "|" + D_CMND_LEDPOWER "|" D_CMND_LEDSTATE "|" D_CMND_LEDMASK "|" D_CMND_LEDPWM_ON "|" D_CMND_LEDPWM_OFF "|" D_CMND_LEDPWM_MODE "|" + D_CMND_WIFIPOWER "|" D_CMND_TEMPOFFSET "|" D_CMND_HUMOFFSET "|" D_CMND_SPEEDUNIT "|" D_CMND_GLOBAL_TEMP "|" D_CMND_GLOBAL_HUM"|" D_CMND_GLOBAL_PRESS "|" D_CMND_SWITCHTEXT "|" + D_CMND_WIFISCAN "|" D_CMND_WIFITEST "|" D_CMND_ZIGBEE_BATTPERCENT "|" + #ifdef USE_I2C D_CMND_I2CSCAN "|" D_CMND_I2CDRIVER "|" #endif + #ifdef USE_DEVICE_GROUPS D_CMND_DEVGROUP_NAME "|" #ifdef USE_DEVICE_GROUPS_SEND @@ -48,14 +48,20 @@ const char kTasmotaCommands[] PROGMEM = "|" // No prefix #endif // USE_DEVICE_GROUPS_SEND D_CMND_DEVGROUP_SHARE "|" D_CMND_DEVGROUPSTATUS "|" D_CMND_DEVGROUP_TIE "|" #endif // USE_DEVICE_GROUPS - D_CMND_SETSENSOR "|" D_CMND_SENSOR "|" D_CMND_DRIVER "|" D_CMND_JSON + +#ifdef USE_UFILESYS + D_CMND_FILELOG "|" +#endif // USE_UFILESYS + #ifdef ESP32 - "|Info|" + "|Info|" #if defined(SOC_TOUCH_VERSION_1) || defined(SOC_TOUCH_VERSION_2) D_CMND_TOUCH_CAL "|" D_CMND_TOUCH_THRES "|" #endif // ESP32 SOC_TOUCH_VERSION_1 or SOC_TOUCH_VERSION_2 D_CMND_CPU_FREQUENCY #endif // ESP32 + + D_CMND_SETSENSOR "|" D_CMND_SENSOR "|" D_CMND_DRIVER "|" D_CMND_JSON "|" D_CMND_JSON_PP #endif //FIRMWARE_MINIMAL ; @@ -72,18 +78,18 @@ void (* const TasmotaCommand[])(void) PROGMEM = { &CmndVoltageResolution, &CmndFrequencyResolution, &CmndCurrentResolution, &CmndEnergyResolution, &CmndWeightResolution, &CmndModule, &CmndModules, &CmndGpio, &CmndGpioRead, &CmndGpios, &CmndTemplate, &CmndPwm, &CmndPwmfrequency, &CmndPwmrange, &CmndButtonDebounce, &CmndSwitchDebounce, &CmndSyslog, &CmndLoghost, &CmndLogport, -#ifdef USE_UFILESYS - &CmndFilelog, -#endif // USE_UFILESYS &CmndSerialBuffer, &CmndSerialSend, &CmndBaudrate, &CmndSerialConfig, &CmndSerialDelimiter, &CmndIpAddress, &CmndNtpServer, &CmndAp, &CmndSsid, &CmndPassword, &CmndHostname, &CmndWifiConfig, &CmndWifi, &CmndDnsTimeout, - &CmndDevicename, &CmndFriendlyname, &CmndFriendlyname, &CmndSwitchMode, &CmndInterlock, &CmndTeleperiod, &CmndReset, &CmndTime, &CmndTimezone, &CmndTimeStd, - &CmndTimeDst, &CmndAltitude, &CmndLedPower, &CmndLedState, &CmndLedMask, &CmndLedPwmOn, &CmndLedPwmOff, &CmndLedPwmMode, - &CmndWifiPower, &CmndTempOffset, &CmndHumOffset, &CmndSpeedUnit, &CmndGlobalTemp, &CmndGlobalHum, &CmndGlobalPress, &CmndSwitchText, &CmndWifiScan, &CmndWifiTest, - &CmndBatteryPercent, + &CmndDevicename, &CmndFriendlyname, &CmndFriendlyname, &CmndSwitchMode, &CmndInterlock, &CmndTeleperiod, &CmndReset, + &CmndTime, &CmndTimezone, &CmndTimeStd, &CmndTimeDst, &CmndAltitude, + &CmndLedPower, &CmndLedState, &CmndLedMask, &CmndLedPwmOn, &CmndLedPwmOff, &CmndLedPwmMode, + &CmndWifiPower, &CmndTempOffset, &CmndHumOffset, &CmndSpeedUnit, &CmndGlobalTemp, &CmndGlobalHum, &CmndGlobalPress, &CmndSwitchText, + &CmndWifiScan, &CmndWifiTest, &CmndBatteryPercent, + #ifdef USE_I2C &CmndI2cScan, &CmndI2cDriver, #endif + #ifdef USE_DEVICE_GROUPS &CmndDevGroupName, #ifdef USE_DEVICE_GROUPS_SEND @@ -91,14 +97,20 @@ void (* const TasmotaCommand[])(void) PROGMEM = { #endif // USE_DEVICE_GROUPS_SEND &CmndDevGroupShare, &CmndDevGroupStatus, &CmndDevGroupTie, #endif // USE_DEVICE_GROUPS - &CmndSetSensor, &CmndSensor, &CmndDriver, &CmndJson + +#ifdef USE_UFILESYS + &CmndFilelog, +#endif // USE_UFILESYS + #ifdef ESP32 - , &CmndInfo, + &CmndInfo, #if defined(SOC_TOUCH_VERSION_1) || defined(SOC_TOUCH_VERSION_2) &CmndTouchCal, &CmndTouchThres, #endif // ESP32 SOC_TOUCH_VERSION_1 or SOC_TOUCH_VERSION_2 - &CmndCpuFrequency + &CmndCpuFrequency, #endif // ESP32 + + &CmndSetSensor, &CmndSensor, &CmndDriver, &CmndJson, &CmndJsonPP #endif //FIRMWARE_MINIMAL }; @@ -1255,6 +1267,13 @@ void CmndOtaUrl(void) ResponseCmndChar(SettingsText(SET_OTAURL)); } +void CmndJsonPP(void) { + if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 7)) { + Settings->mbflag2.json_pretty_print = XdrvMailbox.payload; + } + ResponseCmndNumber(Settings->mbflag2.json_pretty_print); +} + void CmndSeriallog(void) { if ((XdrvMailbox.payload >= LOG_LEVEL_NONE) && (XdrvMailbox.payload <= LOG_LEVEL_DEBUG_MORE)) { diff --git a/tasmota/tasmota_xdrv_driver/xdrv_01_9_webserver.ino b/tasmota/tasmota_xdrv_driver/xdrv_01_9_webserver.ino index 49ca5cbd5..fb2954826 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_01_9_webserver.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_01_9_webserver.ino @@ -500,6 +500,7 @@ struct WEB { bool upload_services_stopped = false; bool reset_web_log_flag = false; // Reset web console log bool initial_config = false; + bool cflg; } Web; /*********************************************************************************************/ @@ -3737,18 +3738,24 @@ void HandleConsoleRefresh(void) { index = 0; Web.reset_web_log_flag = true; } - bool cflg = (index); + Web.cflg = (index); char* line; size_t len; while (GetLog(Settings->weblog_level, &index, &line, &len)) { - if (cflg) { WSContentSend_P(PSTR("\n")); } - WSContentSend(line, len -1); - cflg = true; + if (!LogDataJsonPrettyPrint(line, len -1, WSContentSendLDJsonPPCb)) { + WSContentSendLDJsonPPCb(line, len -1); + } } WSContentSend_P(PSTR("}1")); WSContentEnd(); } +void WSContentSendLDJsonPPCb(const char* line, uint32_t len) { + if (Web.cflg) { WSContentSend_P(PSTR("\n")); } + WSContentSend(line, len); + Web.cflg = true; +} + /********************************************************************************************/ void HandleNotFound(void) { diff --git a/tasmota/tasmota_xdrv_driver/xdrv_78_telnet.ino b/tasmota/tasmota_xdrv_driver/xdrv_78_telnet.ino index 0c7cfb508..287ce1c27 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_78_telnet.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_78_telnet.ino @@ -97,6 +97,25 @@ void TelnetWriteColor(uint32_t color) { } } +void TelnetLDJsonPPCb(const char* line, uint32_t len) { + uint32_t textcolor = Telnet.color[Telnet.prompt]; + uint32_t diffcolor = textcolor; + if ((textcolor >= 30) && (textcolor <= 37)) { + diffcolor += 60; // Highlight color + } + else if ((textcolor >= 90) && (textcolor <= 97)) { + diffcolor -= 60; // Lowlight color + } + char* time_end = (char*)memchr(line, ' ', len); // Find first word (usually 14:49:36.123-017) + uint32_t time_len = time_end - line; + TelnetWriteColor(diffcolor); + Telnet.client.write(line, time_len); + TelnetWriteColor(textcolor); + Telnet.client.write(time_end, len - time_len); + TelnetWriteColor(0); + Telnet.client.println(); +} + void TelnetWrite(char *line, uint32_t len) { if (Telnet.client) { if (3 == Telnet.prompt) { // Print linefeed for non-requested data @@ -104,22 +123,10 @@ void TelnetWrite(char *line, uint32_t len) { Telnet.client.println(); } // line = 14:49:36.123-017 MQTT: stat/wemos5/RESULT = {"POWER":"OFF"} - uint32_t textcolor = Telnet.color[Telnet.prompt]; - uint32_t diffcolor = textcolor; - if ((textcolor >= 30) && (textcolor <= 37)) { - diffcolor += 60; // Highlight color + + if (!LogDataJsonPrettyPrint(line, len, TelnetLDJsonPPCb)) { + TelnetLDJsonPPCb(line, len); } - else if ((textcolor >= 90) && (textcolor <= 97)) { - diffcolor -= 60; // Lowlight color - } - char* time_end = (char*)memchr(line, ' ', len); // Find first word (usually 14:49:36.123-017) - uint32_t time_len = time_end - line; - TelnetWriteColor(diffcolor); - Telnet.client.write(line, time_len); - TelnetWriteColor(textcolor); - Telnet.client.write(time_end, len - time_len); - TelnetWriteColor(0); - Telnet.client.println(); } } @@ -182,10 +189,24 @@ void TelnetLoop(void) { } // Input keyboard data + bool telnet_iac = false; while (Telnet.client.available()) { yield(); uint8_t in_byte = Telnet.client.read(); + if (telnet_iac) { + telnet_iac = false; + if (in_byte != 0xFF) { + // Process telnet Interpret as Command (IAC) codes + AddLog(LOG_LEVEL_DEBUG_MORE, PSTR(D_LOG_TELNET "IAC %d"), in_byte); + continue; + } + } + else if (0xFF == in_byte) { // Telnet Interpret as Command (IAC) + telnet_iac = true; + continue; + } + #ifdef USE_XYZMODEM if (XYZModemWifiClientStart(&Telnet.client, in_byte)) { return; } #endif // USE_XYZMODEM