diff --git a/pio-scripts/inject_syslog_ui.py b/pio-scripts/inject_syslog_ui.py new file mode 100644 index 0000000000..08077cf636 --- /dev/null +++ b/pio-scripts/inject_syslog_ui.py @@ -0,0 +1,153 @@ +# pio-scripts/inject_syslog_ui.py + +""" +PlatformIO build script to conditionally inject Syslog UI elements into the settings HTML file. + +This script: +1. Injects Syslog UI elements when WLED_ENABLE_SYSLOG is defined in build flags +2. Restores the original HTML file after build completion +3. Tracks state between builds to force UI rebuilds when necessary +""" + +import os, re, shutil +from SCons.Script import Import + +Import("env") + +# detect full vs. partial compile +is_full_build = env.get("PIOENV") is not None + +# Track the state between builds +def get_previous_syslog_state(project_dir): + state_file = os.path.join(project_dir, "wled00/data/.syslog_state") + if os.path.exists(state_file): + with open(state_file, 'r') as f: + return f.read().strip() == "1" + return None # None means no previous state recorded + +def set_syslog_state(project_dir, enabled): + state_file = os.path.join(project_dir, "wled00/data/.syslog_state") + with open(state_file, 'w') as f: + f.write("1" if enabled else "0") + +# This is the HTML we want to inject +SYSLOG_HTML = """

Syslog

+
+ Enable Syslog:
+ Host:
+ Port:
+
""" + +def inject_syslog_ui(source, target, env, retry_count=0): + print("\033[44m==== inject_syslog_ui.py (PRE BUILD) ====\033[0m") + if not is_full_build: + print("\033[43mNot a full build, skipping Syslog UI operations.\033[0m") + return + + # Check for the define in BUILD_FLAGS + build_flags = env.get("BUILD_FLAGS", "") + if isinstance(build_flags, list): + build_flags = " ".join(build_flags) + has_syslog = bool(re.search(r'-D\s*WLED_ENABLE_SYSLOG\b', build_flags)) + + project_dir = env.subst("$PROJECT_DIR") + html_path = os.path.join(project_dir, "wled00/data/settings_sync.htm") + bak = html_path + ".backup" + + # Detect state change → touch to force rebuild + prev = get_previous_syslog_state(project_dir) + if prev is not None and prev != has_syslog: + print(f"\033[43mSYSLOG state changed from {prev} to {has_syslog}, forcing UI rebuild.\033[0m") + if os.path.exists(html_path): + with open(html_path, 'a'): + os.utime(html_path, None) + + set_syslog_state(project_dir, has_syslog) + + if not has_syslog: + print("\033[43mWLED_ENABLE_SYSLOG not defined, skipping injection.\033[0m") + # restore if backup exists + if os.path.exists(bak): + print("Restoring original file from backup...") + shutil.copy2(bak, html_path) + os.remove(bak) + return + + # backup + inject only once + if not os.path.exists(bak): + print("Backing up and injecting Syslog UI...") + shutil.copyfile(html_path, bak) + try: + with open(html_path, 'r', encoding='utf8') as f: + original = f.read() + modified = original + + # replace the single comment with HTML + if '' in modified: + modified = modified.replace('', SYSLOG_HTML) + else: + # insert before last
+ idx = modified.rfind('
') + if idx == -1: + print("\033[41mCould not find
to insert Syslog UI!\033[0m") + # Clean up backup since injection failed + if os.path.exists(bak): + os.remove(bak) + return + modified = ( + modified[:idx] + + SYSLOG_HTML + '\n' + + modified[idx:] + ) + + with open(html_path, 'w', encoding='utf8') as f: + f.write(modified) + print("\033[42mSyslog UI injected successfully!\033[0m") + + except (IOError, OSError) as e: + print(f"\033[41mFile operation error during injection: {e}\033[0m") + # injection failed → remove backup so we'll retry next time + if os.path.exists(bak): + os.remove(bak) + except Exception as e: + print(f"\033[41mUnexpected error during injection: {e}\033[0m") + # injection failed → remove backup so we’ll retry next time + if os.path.exists(bak): + os.remove(bak) + else: + print("Backup exists; assume already injected.") + # verify that SYSLOG markers really are in the file + with open(html_path, 'r', encoding='utf8') as f: + content = f.read() + if '

Syslog

' not in content: + print("Backup exists but SYSLOG markers missing—forcing re-injection.") + os.remove(bak) + # only retry up to 3 times + if retry_count < 3: + # Add a small delay before retrying + import time + time.sleep(0.5 * (retry_count + 1)) # Increasing delay with each retry + inject_syslog_ui(source, target, env, retry_count + 1) + else: + print("\033[41mToo many retry attempts. Manual intervention required.\033[0m") + else: + print("Backup exists and markers found; already injected.") + +def restore_syslog_ui(source, target, env): + print("\033[44m==== inject_syslog_ui.py (POST BUILD) ====\033[0m") + project_dir = env.subst("$PROJECT_DIR") + html_path = os.path.join(project_dir, "wled00/data/settings_sync.htm") + bak = html_path + ".backup" + + # restore only if backup file is present + if os.path.exists(bak): + print("Restoring original file from backup...") + shutil.copy2(bak, html_path) + os.remove(bak) + +# always register the post-action on checkprogsize so it runs every build +env.AddPostAction("checkprogsize", restore_syslog_ui) + +# only inject on full build +if is_full_build: + inject_syslog_ui(None, None, env) \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index a7485244cd..8cee7ea9b8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -115,6 +115,7 @@ extra_scripts = post:pio-scripts/strip-floats.py pre:pio-scripts/user_config_copy.py pre:pio-scripts/load_usermods.py + pre:pio-scripts/inject_syslog_ui.py pre:pio-scripts/build_ui.py ; post:pio-scripts/obj-dump.py ;; convenience script to create a disassembly dump of the firmware (hardcore debugging) diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 0570cc2d6c..b72560cddd 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -24,7 +24,10 @@ make_unique(Args&&... args) #endif // enable additional debug output -#if defined(WLED_DEBUG_HOST) +#if defined(WLED_ENABLE_SYSLOG) + #include "syslog.h" + #define DEBUGOUT Syslog +#elif defined(WLED_DEBUG_HOST) #include "net_debug.h" #define DEBUGOUT NetDebug #else diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index fa0397fc65..5e06944920 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -536,6 +536,13 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(hueIP[i], if_hue_ip[i]); #endif +#ifdef WLED_ENABLE_SYSLOG + JsonObject if_syslog = interfaces["syslog"]; + CJSON(syslogEnabled, if_syslog["en"]); + getStringFromJson(syslogHost, if_syslog[F("host")], 33); + CJSON(syslogPort, if_syslog["port"]); +#endif + JsonObject if_ntp = interfaces[F("ntp")]; CJSON(ntpEnabled, if_ntp["en"]); getStringFromJson(ntpServerName, if_ntp[F("host")], 33); // "1.wled.pool.ntp.org" @@ -1051,6 +1058,13 @@ void serializeConfig(JsonObject root) { } #endif +#ifdef WLED_ENABLE_SYSLOG + JsonObject if_syslog = interfaces.createNestedObject("syslog"); + if_syslog["en"] = syslogEnabled; + if_syslog["host"] = syslogHost; + if_syslog["port"] = syslogPort; +#endif + JsonObject if_ntp = interfaces.createNestedObject("ntp"); if_ntp["en"] = ntpEnabled; if_ntp[F("host")] = ntpServerName; diff --git a/wled00/data/settings_sync.htm b/wled00/data/settings_sync.htm index ca6c0fb59c..983d0a6cbc 100644 --- a/wled00/data/settings_sync.htm +++ b/wled00/data/settings_sync.htm @@ -237,6 +237,7 @@

Serial


Keep at 115200 to use Improv. Some boards may not support high rates. +
diff --git a/wled00/json.cpp b/wled00/json.cpp index c09b543f1c..ce9b3e7b8c 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -298,7 +298,11 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) bool stateResponse = root[F("v")] | false; #if defined(WLED_DEBUG) && defined(WLED_DEBUG_HOST) - netDebugEnabled = root[F("debug")] | netDebugEnabled; + netDebugEnabled = root[F("debug")] | netDebugEnabled; + #elif defined(WLED_DEBUG) && defined(WLED_ENABLE_SYSLOG) + syslogEnabled = root[F("debug")] | syslogEnabled; + configNeedsWrite = true; + DEBUG_PRINTF_P(PSTR("Syslog: %s\n"), syslogEnabled ? PSTR("ENABLED") : PSTR("DISABLED") ); #endif bool onBefore = bri; @@ -781,6 +785,9 @@ void serializeInfo(JsonObject root) #ifdef WLED_DEBUG_HOST os |= 0x0100; if (!netDebugEnabled) os &= ~0x0080; + #elif defined(WLED_ENABLE_SYSLOG) + os |= 0x0100; + if (!syslogEnabled) os &= ~0x0080; #endif #endif #ifndef WLED_DISABLE_ALEXA diff --git a/wled00/set.cpp b/wled00/set.cpp index 725875023e..1db411af47 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -472,6 +472,19 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) t = request->arg(F("BD")).toInt(); if (t >= 96 && t <= 15000) serialBaud = t; updateBaudRate(serialBaud *100); + + #ifdef WLED_ENABLE_SYSLOG + syslogEnabled = request->hasArg(F("SL_en")); + strlcpy(syslogHost, request->arg(F("SL_host")).c_str(), sizeof(syslogHost)); + + t = request->arg(F("SL_port")).toInt(); + if (t > 0) syslogPort = t; + + Syslog.begin(syslogHost, syslogPort, + syslogFacility, syslogSeverity, syslogProtocol); + + Syslog.setAppName("WLED"); + #endif } //TIME diff --git a/wled00/syslog.cpp b/wled00/syslog.cpp new file mode 100644 index 0000000000..384a937e4e --- /dev/null +++ b/wled00/syslog.cpp @@ -0,0 +1,195 @@ +#include "wled.h" +#ifdef WLED_ENABLE_SYSLOG + +#include "syslog.h" + +SyslogPrinter::SyslogPrinter() : + _lastOperationSucceeded(true), + _lastErrorMessage(""), + _facility(SYSLOG_LOCAL0), + _severity(SYSLOG_DEBUG), + _protocol(SYSLOG_PROTO_BSD), + _appName("WLED"), + _bufferIndex(0) {} + +void SyslogPrinter::begin(const char* host, uint16_t port, + uint8_t facility, uint8_t severity, uint8_t protocol) { + + DEBUG_PRINTF_P(PSTR("===== WLED SYSLOG CONFIGURATION =====\n")); + DEBUG_PRINTF_P(PSTR(" Hostname: %s\n"), host); + DEBUG_PRINTF_P(PSTR(" Cached IP: %s\n"), syslogHostIP.toString().c_str()); + DEBUG_PRINTF_P(PSTR(" Port: %u\n"), (unsigned)port); + DEBUG_PRINTF_P(PSTR("======================================\n")); + + strlcpy(syslogHost, host, sizeof(syslogHost)); + syslogPort = port; + _facility = facility; + _severity = severity; + _protocol = protocol; + + // clear any cached IP so resolveHostname() will run next write() + syslogHostIP = IPAddress(0,0,0,0); +} + +void SyslogPrinter::setAppName(const String &appName) { + _appName = appName; +} + +bool SyslogPrinter::resolveHostname() { + if (!WLED_CONNECTED || !syslogEnabled) return false; + + // If we already have an IP or can parse the hostname as an IP, use that + if (syslogHostIP || syslogHostIP.fromString(syslogHost)) { + return true; + } + + // Otherwise resolve the hostname + #ifdef ESP8266 + WiFi.hostByName(syslogHost, syslogHostIP, 750); + #else + #ifdef WLED_USE_ETHERNET + ETH.hostByName(syslogHost, syslogHostIP); + #else + WiFi.hostByName(syslogHost, syslogHostIP); + #endif + #endif + + return syslogHostIP != IPAddress(0, 0, 0, 0); +} + +void SyslogPrinter::flushBuffer() { + if (_bufferIndex == 0) return; + + // Skip pure "#015" lines + if (_bufferIndex == 4 && memcmp(_buffer, "#015", 4) == 0) { + _bufferIndex = 0; + return; + } + + // Check if the message contains only whitespace + bool onlyWhitespace = true; + for (size_t i = 0; i < _bufferIndex; i++) { + if (_buffer[i] != ' ' && _buffer[i] != '\t') { + onlyWhitespace = false; + break; + } + } + if (onlyWhitespace) { + _bufferIndex = 0; + return; + } + + // Null-terminate + _buffer[_bufferIndex] = '\0'; + + // Send the buffer with default severity + write((const uint8_t*)_buffer, _bufferIndex, _severity); + + // Reset buffer index + _bufferIndex = 0; +} + +size_t SyslogPrinter::write(uint8_t c) { + // Store in buffer regardless of connection status + if (_bufferIndex < sizeof(_buffer) - 1) { + _buffer[_bufferIndex++] = c; + } + + // If newline or buffer full, flush buffer + if (c == '\n' || _bufferIndex >= sizeof(_buffer) - 1) { + flushBuffer(); + } + + return 1; +} + +size_t SyslogPrinter::write(const uint8_t *buf, size_t size) { + return write(buf, size, _severity); +} + +size_t SyslogPrinter::write(const uint8_t *buf, size_t size, uint8_t severity) { + _lastOperationSucceeded = true; + if (!WLED_CONNECTED) { + _lastOperationSucceeded = false; + _lastErrorMessage = F("Network not connected"); + return 0; + } + if (buf == nullptr) { + _lastOperationSucceeded = false; + _lastErrorMessage = F("Null buffer provided"); + return 0; + } + if (!syslogEnabled) { + _lastOperationSucceeded = false; + _lastErrorMessage = F("Syslog is disabled"); + return 0; + } + if (!resolveHostname()) { + _lastOperationSucceeded = false; + _lastErrorMessage = F("Failed to resolve hostname"); + return 0; + } + + // Check for special case - literal "#015" string + if (size >= 4 && buf[0] == '#' && buf[1] == '0' && buf[2] == '1' && buf[3] == '5') { + return size; // Skip sending this message + } + + // Skip empty messages + if (size == 0) return 0; + + // Check if the message contains only whitespace + bool onlyWhitespace = true; + for (size_t i = 0; i < size; i++) { + if (buf[i] != ' ' && buf[i] != '\t' && buf[i] != '\r' && buf[i] != '\n') { + onlyWhitespace = false; + break; + } + } + if (onlyWhitespace) return size; // Skip sending this message + + syslogUdp.beginPacket(syslogHostIP, syslogPort); + + // Calculate priority value + uint8_t pri = (_facility << 3) | severity; + + // Add hostname (replacing spaces with underscores) and app name + String cleanHostname = String(serverDescription); + cleanHostname.replace(' ', '_'); + + // Note: Only BSD protocol is currently implemented + syslogUdp.printf("<%d>", pri); + + if (ntpEnabled && ntpConnected) { + // Month abbreviation + static const char* const months[] = { + "Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec" + }; + + syslogUdp.printf("%s %2d %02d:%02d:%02d ", + months[month(localTime) - 1], + day(localTime), + hour(localTime), + minute(localTime), + second(localTime)); + } else { + // No valid time available + syslogUdp.print(F("Jan 01 00:00:00 ")); + } + + // Add hostname and app name + syslogUdp.print(cleanHostname); + syslogUdp.print(" "); + syslogUdp.print(_appName); + syslogUdp.print(": "); + + // Add message content + size = syslogUdp.write(buf, size); + + syslogUdp.endPacket(); + return size; +} + +SyslogPrinter Syslog; +#endif // WLED_ENABLE_SYSLOG \ No newline at end of file diff --git a/wled00/syslog.h b/wled00/syslog.h new file mode 100644 index 0000000000..98b4e39134 --- /dev/null +++ b/wled00/syslog.h @@ -0,0 +1,59 @@ +#ifndef WLED_SYSLOG_H +#define WLED_SYSLOG_H +#include +#include + +// Buffer management +#ifndef SYSLOG_BUFFER_SIZE + #define SYSLOG_BUFFER_SIZE 128 +#endif + +#define SYSLOG_LOCAL0 16 // local use 0 +#define SYSLOG_DEBUG 7 // Debug: debug-level messages + +// Syslog protocol formats - commented out but preserved +#define SYSLOG_PROTO_BSD 0 // Legacy BSD format (RFC 3164) + +class SyslogPrinter : public Print { + private: + WiFiUDP syslogUdp; // needs to be here otherwise UDP messages get truncated upon destruction + IPAddress syslogHostIP; + bool resolveHostname(); + bool _lastOperationSucceeded; + String _lastErrorMessage; + + // Syslog configuration - using globals from wled.h + uint8_t _facility; // Internal copy of syslogFacility (from wled.h), fixed to SYSLOG_LOCAL0 + uint8_t _severity; // Internal copy of syslogSeverity (from wled.h), fixed to SYSLOG_DEBUG + uint8_t _protocol; // Internal copy of syslogProtocol (from wled.h), fixed to SYSLOG_PROTO_BSD + String _appName; + + char _buffer[SYSLOG_BUFFER_SIZE]; // Buffer for collecting characters + + size_t _bufferIndex; + void flushBuffer(); + + public: + SyslogPrinter(); + void begin(const char* host, uint16_t port, + uint8_t facility = SYSLOG_LOCAL0, + uint8_t severity = SYSLOG_DEBUG, + uint8_t protocol = SYSLOG_PROTO_BSD); + void setAppName(const String &appName); + + // Print interface implementation + virtual size_t write(uint8_t c); + virtual size_t write(const uint8_t *buf, size_t size); + + // Severity override for specific messages + size_t write(const uint8_t *buf, size_t size, uint8_t severity); + + // Error handling + bool lastOperationSucceeded() const { return _lastOperationSucceeded; } + String getLastErrorMessage() const { return _lastErrorMessage; } +}; + +// Default instance +extern SyslogPrinter Syslog; + +#endif \ No newline at end of file diff --git a/wled00/wled.cpp b/wled00/wled.cpp index cc338d23f2..97d092ba61 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -329,7 +329,7 @@ void WLED::setup() #if defined(WLED_DEBUG) && defined(ARDUINO_ARCH_ESP32) && (defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || ARDUINO_USB_CDC_ON_BOOT) delay(2500); // allow CDC USB serial to initialise #endif - #if !defined(WLED_DEBUG) && defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DEBUG_HOST) && ARDUINO_USB_CDC_ON_BOOT + #if !defined(WLED_DEBUG) && defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DEBUG_HOST) && !defined(WLED_ENABLE_SYSLOG) && ARDUINO_USB_CDC_ON_BOOT Serial.setDebugOutput(false); // switch off kernel messages when using USBCDC #endif DEBUG_PRINTLN(); @@ -387,7 +387,7 @@ void WLED::setup() usePWMFixedNMI(); // link the NMI fix #endif -#if defined(WLED_DEBUG) && !defined(WLED_DEBUG_HOST) +#if defined(WLED_DEBUG) && !defined(WLED_DEBUG_HOST) && !defined(WLED_ENABLE_SYSLOG) PinManager::allocatePin(hardwareTX, true, PinOwner::DebugOut); // TX (GPIO1 on ESP32) reserved for debug output #endif #ifdef WLED_ENABLE_DMX //reserve GPIO2 as hardcoded DMX pin @@ -422,6 +422,13 @@ void WLED::setup() WLED_SET_AP_SSID(); // otherwise it is empty on first boot until config is saved multiWiFi.push_back(WiFiConfig(CLIENT_SSID,CLIENT_PASS)); // initialise vector with default WiFi +#ifdef WLED_ENABLE_SYSLOG + // Configure and initialize Syslog client + Syslog.begin(syslogHost, syslogPort, + syslogFacility, syslogSeverity, syslogProtocol); + Syslog.setAppName("WLED"); +#endif + DEBUG_PRINTLN(F("Reading config")); deserializeConfigFromFS(); DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); diff --git a/wled00/wled.h b/wled00/wled.h index f8dc1252a8..e7a3c7a2fa 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -964,7 +964,24 @@ WLED_GLOBAL JsonDocument *pDoc _INIT(&gDoc); WLED_GLOBAL volatile uint8_t jsonBufferLock _INIT(0); // enable additional debug output -#if defined(WLED_DEBUG_HOST) +#if defined(WLED_ENABLE_SYSLOG) + #include "syslog.h" + // On the host side, use a standard syslog server or tools like rsyslog + // use -D WLED_ENABLE_SYSLOG and -D WLED_DEBUG + #define DEBUGOUT Syslog + WLED_GLOBAL bool syslogEnabled _INIT(true); + #ifndef WLED_SYSLOG_HOST + #define WLED_SYSLOG_HOST "" + #endif + WLED_GLOBAL char syslogHost[33] _INIT(WLED_SYSLOG_HOST); + #ifndef WLED_SYSLOG_PORT + #define WLED_SYSLOG_PORT 514 + #endif + WLED_GLOBAL int syslogPort _INIT(WLED_SYSLOG_PORT); + WLED_GLOBAL uint8_t syslogProtocol _INIT(SYSLOG_PROTO_BSD); // Direct initialization with hardcoded BSD protocol value + WLED_GLOBAL uint8_t syslogFacility _INIT(SYSLOG_LOCAL0); // Direct initialization with hardcoded LOCAL0 facility value + WLED_GLOBAL uint8_t syslogSeverity _INIT(SYSLOG_DEBUG); // Direct initialization with hardcoded DEBUG severity value +#elif defined(WLED_DEBUG_HOST) #include "net_debug.h" // On the host side, use netcat to receive the log statements: nc -l 7868 -u // use -D WLED_DEBUG_HOST='"192.168.xxx.xxx"' or FQDN within quotes diff --git a/wled00/xml.cpp b/wled00/xml.cpp index de2f5590df..17303bf240 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -105,7 +105,7 @@ void appendGPIOinfo(Print& settingsScript) { settingsScript.print(2); // DMX hardcoded pin firstPin = false; #endif - #if defined(WLED_DEBUG) && !defined(WLED_DEBUG_HOST) + #if defined(WLED_DEBUG) && !defined(WLED_DEBUG_HOST) && !defined(WLED_ENABLE_SYSLOG) if (!firstPin) settingsScript.print(','); settingsScript.print(hardwareTX); // debug output (TX) pin firstPin = false; @@ -516,6 +516,13 @@ void getSettingsJS(byte subPage, Print& settingsScript) settingsScript.print(F("toggle('Hue');")); // hide Hue Sync settings #endif printSetFormValue(settingsScript,PSTR("BD"),serialBaud); + + #ifdef WLED_ENABLE_SYSLOG + printSetFormCheckbox(settingsScript,PSTR("SL_en"), syslogEnabled); // enable/disable + printSetFormValue (settingsScript,PSTR("SL_host"), syslogHost); // host + printSetFormValue (settingsScript,PSTR("SL_port"), syslogPort); // port + #endif + #ifndef WLED_ENABLE_ADALIGHT settingsScript.print(F("toggle('Serial');")); #endif