├── Wiring.jpg ├── License.md ├── README .txt ├── HomeAssistant ├── instruction.txt └── homeassistant.txt └── LightTrack-PRO V2.ino /Wiring.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DiyYari/LightTrack-PRO/HEAD/Wiring.jpg -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | Non-Commercial Use License 2 | Copyright (c) [2025] [DIY Yari] 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software (the "Software") to use, copy, modify, merge, publish, and distribute the Software for non-commercial purposes only, provided that the following conditions are met: 5 | 6 | Non-Commercial Use 7 | The Software may be used, copied, modified, merged, published, and distributed free of charge solely for personal, educational, and non-commercial purposes. Any use, publication, or distribution of this Software must include proper attribution to the original author. 8 | 9 | Commercial Use 10 | Any use of the Software for commercial purposes (including, but not limited to, selling, incorporating it into commercial products, or providing services) is allowed only after obtaining prior written permission from the author. For commercial licensing, please contact me at: [your contact email or other contact information]. 11 | 12 | Disclaimer of Warranty 13 | The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to warranties of merchantability or fitness for a particular purpose. The author shall not be liable for any damages arising from the use of this Software. 14 | 15 | Attribution 16 | When using, modifying, or distributing the Software or any derivative works, you must retain attribution to the original author and include this license text. 17 | 18 | Third-Party Libraries 19 | This Software makes use of third-party libraries (such as ArduinoOTA, FastLED, WiFi, SPIFFS, EEPROM, etc.). These libraries are distributed under their own respective licenses, and you must comply with their terms. It is recommended to include a list of these libraries and their licenses in your repository's documentation (e.g., in the README). 20 | 21 | License Modifications 22 | The author reserves the right to modify the terms of this license for future versions of the Software without prior notice. 23 | -------------------------------------------------------------------------------- /README .txt: -------------------------------------------------------------------------------- 1 | Project "Your_Device" – Third-Party Component License Notices 2 | ======================================================================= 3 | 4 | This project uses the following third-party libraries and components. 5 | The use of each component is subject to the terms of its respective license. 6 | 7 | 1. Arduino Core Libraries 8 | --------------------------- 9 | - Files: Arduino.h, WebServer.h, EEPROM.h, and other files included in the Arduino Core. 10 | - License: GNU Lesser General Public License v2.1 (LGPL v2.1) 11 | - More info: https://www.gnu.org/licenses/lgpl-2.1.html 12 | 13 | 2. FastLED Library 14 | ------------------ 15 | - File: FastLED.h 16 | - License: MIT License 17 | --- 18 | MIT License 19 | 20 | Copyright (c) [Year] [Author or group of developers] 21 | 22 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation (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: 23 | 24 | [Full text of the MIT License] 25 | 26 | 3. ESP-IDF Components (WiFi.h, esp_wifi.h, SPIFFS.h) 27 | ---------------------------------------------------- 28 | - Files: WiFi.h, esp_wifi.h, SPIFFS.h (included as part of ESP32 Arduino/ESP-IDF) 29 | - License: Apache License 2.0 30 | - More info: http://www.apache.org/licenses/LICENSE-2.0 31 | 32 | 4. Standard C/C++ Libraries 33 | --------------------------- 34 | - Files: math.h, time.h 35 | - These header files are part of the standard C/C++ library provided by your compiler/platform. 36 | - The usage terms are governed by the corresponding documentation of your system. 37 | 38 | ======================================================================= 39 | This LICENSES.txt file should accompany the source code and be included in the documentation distributed with the product. 40 | If necessary, supplement the information or include the full text of the licenses in accordance with the requirements of each specific licensing agreement. 41 | -------------------------------------------------------------------------------- /HomeAssistant/instruction.txt: -------------------------------------------------------------------------------- 1 | 2 | **Step-by-Step Integration Guide: ESP32 LightTrack with Home Assistant** 3 | 4 | **Phase 1: ESP32 Code Configuration (`main.cpp`)** 5 | 6 | 1. **Open your `main.cpp` file** in your code editor (e.g., VS Code with PlatformIO). 7 | 2. **Locate the MQTT & Home Assistant Configuration Block:** 8 | It's near the top, marked with: 9 | ```cpp 10 | // ############################################################################ 11 | // ## MQTT & HOME ASSISTANT CONFIGURATION ## 12 | // ############################################################################ 13 | ``` 14 | 3. **Configure Wi-Fi Credentials:** 15 | * Modify `main_wifi_ssid`: Replace `"YOUR_WIFI_SSID"` with the **name (SSID) of your home Wi-Fi network** where your Home Assistant server is also connected. 16 | ```cpp 17 | const char* main_wifi_ssid = "MyHomeWiFi"; // Example 18 | ``` 19 | * Modify `main_wifi_password`: Replace `"YOUR_WIFI_PASSWORD"` with the **password for your home Wi-Fi network**. 20 | ```cpp 21 | const char* main_wifi_password = "MySecurePassword123"; // Example 22 | ``` 23 | 4. **Configure MQTT Broker (Home Assistant Server):** 24 | * Modify `mqtt_server`: Replace `"IP_OF_YOUR_HA_MACHINE"` with the **actual IP address of your Home Assistant machine** on your local network. 25 | * *How to find HA IP:* Check your router's connected clients list, or use a network scanning tool. If you access HA via `http://homeassistant.local:8123`, your computer can resolve this; the ESP32 needs the direct IP. 26 | ```cpp 27 | const char* mqtt_server = "192.168.1.100"; // Example: Replace with YOUR HA's IP 28 | ``` 29 | * `mqtt_port`: Usually remains `1883`. Only change if you've configured Mosquitto on a different port. 30 | 31 | 5. **Configure MQTT User Credentials:** 32 | * **Important:** You will create this user in Home Assistant in Phase 2. For now, decide on a username and a strong password. 33 | * Modify `mqtt_user`: Replace `"HA_MQTT_USER"` with the **username you will create in Home Assistant** specifically for MQTT access. 34 | ```cpp 35 | const char* mqtt_user = "lighttrack_mqtt"; // Example username 36 | ``` 37 | * Modify `mqtt_pass`: Replace `"HA_MQTT_PASSWORD"` with the **password for that dedicated HA user**. 38 | ```cpp 39 | const char* mqtt_pass = "aVeryStrongMqttPassword!"; // Example password 40 | ``` 41 | 6. **Save the `main.cpp` file.** 42 | 7. **Compile and Upload the Code to your ESP32:** 43 | * Connect the ESP32 board to your computer. 44 | * Use PlatformIO's "Upload" command. 45 | 8. **Open Serial Monitor:** 46 | * Use PlatformIO's "Serial Monitor" (baud rate 115200). This is crucial for debugging. You should see it trying to connect to your Wi-Fi and then to the MQTT broker (which will likely fail until Phase 2 is complete, but Wi-Fi should connect). 47 | 48 | **Phase 2: Home Assistant Configuration** 49 | 50 | 1. **Install Mosquitto Broker Add-on (if not already installed):** 51 | * In Home Assistant, go to "Settings" > "Add-ons" > "Add-on store". 52 | * Search for "Mosquitto broker" and install it. 53 | * Once installed, **Start** the Mosquitto broker add-on. 54 | * Check its "Log" tab to ensure it starts without errors. 55 | 56 | 2. **Configure Mosquitto Add-on to Use Home Assistant Users:** 57 | * Go to the "Configuration" tab of the Mosquitto broker add-on. 58 | * Find the `logins:` section. **Ensure this section is empty:** 59 | ```yaml 60 | logins: [] 61 | ``` 62 | (If it has entries, delete them. This tells Mosquitto to use Home Assistant users for authentication). 63 | * Find the `anonymous:` setting. Ensure it is set to `false` (or that the line is deleted/commented out, as `false` is the default when `logins: []` is used). 64 | ```yaml 65 | anonymous: false 66 | ``` 67 | * Click "SAVE" at the bottom of the Mosquitto add-on configuration. 68 | * **Restart** the Mosquitto broker add-on (go to its "Info" tab and click "RESTART"). Check the "Log" tab again for errors. 69 | 70 | 3. **Create a Dedicated Home Assistant User for MQTT:** 71 | * In Home Assistant, go to "Settings" > "People" > "Users" tab. 72 | * Click the "+ ADD USER" button (usually bottom right). 73 | * **Display Name:** Enter a descriptive name (e.g., `MQTT LightTrack User`). 74 | * **Username:** Enter the **exact same username** you configured in the ESP32's `main.cpp` for `mqtt_user` (e.g., `lighttrack_mqtt`). 75 | * **Password:** Enter the **exact same password** you configured in the ESP32's `main.cpp` for `mqtt_pass`. Confirm the password. 76 | * Ensure "User can log in" is enabled. 77 | * You can choose not to allow this user to administer Home Assistant if it's solely for MQTT. 78 | * Click "CREATE". 79 | 80 | 4. **Configure/Check Home Assistant's MQTT Integration:** 81 | * Go to "Settings" > "Devices & Services". 82 | * Look for an existing "MQTT" integration card. 83 | * **If it's not there:** Click "+ ADD INTEGRATION", search for "MQTT", and select it. It should automatically discover your Mosquitto add-on. You likely won't need to enter broker details manually if the add-on is running correctly and configured as above. Click "SUBMIT". 84 | * **If it is there:** Click "CONFIGURE" on the MQTT card. It should show that it's connected to your broker. If it shows errors or asks for broker details, it might not be correctly configured with the Mosquitto add-on. You might need to re-configure it (often involves deleting and re-adding if it can't find the broker). 85 | * **Important for Discovery:** The ESP32 code uses the default discovery prefix `homeassistant`. Ensure your MQTT integration in HA is also using this (it's the default). You can check this in the MQTT integration's configuration options if needed. 86 | 87 | **Phase 3: Verification and Troubleshooting** 88 | 89 | 1. **Restart your ESP32** (or it might have reconnected already). 90 | 2. **Check ESP32 Serial Monitor Logs:** 91 | * Look for: 92 | * Successful Wi-Fi connection: `WiFi connected! IP address: ...` 93 | * Attempts to connect to MQTT: `MQTT Attempting connection to [Your_HA_IP_Address] (Client ID: lighttrack_XXXXXX)...` 94 | * Successful MQTT connection: `MQTT connected!` 95 | * Subscription to command topic: `MQTT Subscribed to command topic: lighttrack/lighttrack_XXXXXX/set` 96 | * Discovery message publication: `MQTT Pub Discovery to homeassistant/light/lighttrack_XXXXXX/config` and `Discovery published successfully.` 97 | * Availability and State publications. 98 | 99 | 3. **Check Mosquitto Broker Add-on Logs in Home Assistant:** 100 | * Go to "Settings" > "Add-ons" > "Mosquitto broker" > "Log" tab. 101 | * You should see connection attempts from the ESP32's client ID (e.g., `lighttrack_XXXXXX`). 102 | * Look for lines like: `New client connected from [ESP32_IP_Address] as lighttrack_XXXXXX (p2, c1, k60, u'lighttrack_mqtt').` (The username `lighttrack_mqtt` should match what you configured). 103 | 104 | 4. **Check Home Assistant for the Device:** 105 | * Go to "Settings" > "Devices & Services". 106 | * Click on the "MQTT" integration. You should see a link like "1 device" (or more if you have other MQTT devices). 107 | * Your `LightTrack XXXXXX` device (where XXXXXX are the last 3 bytes of its MAC address) should appear. 108 | * Click on the device name. 109 | * You should see one entity (e.g., `light.lighttrack_xxxxxx`). 110 | * Click on this entity to see its controls. 111 | 112 | 5. **Test Control:** 113 | * Add the `light.lighttrack_xxxxxx` entity to one of your Home Assistant dashboards (Lovelace). 114 | * Try controlling it: 115 | * Turn On/Off. 116 | * Change brightness. 117 | * Change color. 118 | * Select an effect from the "Effect" dropdown list (e.g., "Solid", "Background", "Schedule", "Stationary"). 119 | * Observe the ESP32's physical LEDs and the Serial Monitor logs for corresponding actions and MQTT messages. 120 | 121 | **Common Troubleshooting Steps:** 122 | 123 | * **ESP32 Not Connecting to Wi-Fi:** 124 | * Double-check Wi-Fi SSID and password in `main.cpp` (case-sensitive). 125 | * Ensure ESP32 has good Wi-Fi signal. 126 | * Check router for MAC filtering or other restrictions. 127 | * **ESP32 Not Connecting to MQTT (e.g., `MQTT connect failed, rc=-2`, `rc=3`, `rc=4`, `rc=5`):** 128 | * `rc=-2` (Network error/DNS fail): Verify `mqtt_server` IP address in `main.cpp` is correct and reachable from ESP32. Ensure Mosquitto add-on is running. Check firewall on HA machine (though unlikely for local connections if HA itself is accessible). 129 | * `rc=3` (Server unavailable): Mosquitto broker add-on might not be running or accessible. 130 | * `rc=4` (Bad username/password): MQTT username/password in `main.cpp` **do not exactly match** the user created in HA. Double-check case sensitivity. 131 | * `rc=5` (Not authorized): 132 | * Mosquitto add-on "Configuration" tab: `logins: []` is not empty, or `anonymous: true` is set when it shouldn't be. Restart Mosquitto after changes. 133 | * The HA user for MQTT might not have been created correctly. 134 | * The MQTT integration in HA might not be correctly configured to use the add-on. 135 | * **Device Not Appearing in Home Assistant (but ESP32 logs say MQTT connected):** 136 | * MQTT Integration in HA: Is it running and connected to the broker? 137 | * ESP32 Logs: Was the "Discovery" message published successfully? (e.g., `Discovery published successfully.`) 138 | * Mosquitto Logs: Did Mosquitto receive the discovery message on a topic like `homeassistant/light/lighttrack_XXXXXX/config`? 139 | * HA MQTT Integration Settings: Ensure "Discovery prefix" is `homeassistant` (default). 140 | * Wait a minute or two; sometimes discovery takes a short while. 141 | * **"nan%" for Intensity in Serial Logs on First Boot:** 142 | * This is often due to an empty or freshly formatted EEPROM/SPIFFS. The code now has safeguards to load defaults if `nan` is read. Subsequent reboots should show correct values. If it persists, ensure `loadSettings()` is called correctly and SPIFFS is mounted. 143 | 144 | --- 145 | 146 | Follow these steps carefully, paying close attention to the Serial Monitor and Mosquitto logs, and you should have your LightTrack integrated with Home Assistant! Good luck! 147 | -------------------------------------------------------------------------------- /LightTrack-PRO V2.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "esp_wifi.h" 9 | #include // Used for time management 10 | #include 11 | #include 12 | 13 | // ------------------------- LED Configuration ------------------------- 14 | #define LED_PIN 2 15 | #define NUM_LEDS 300 16 | #define CHIPSET WS2812B 17 | #define COLOR_ORDER GRB 18 | CRGB leds[NUM_LEDS]; 19 | 20 | // ------------------------- Sensor Parameters ------------------------- 21 | #define SENSOR_HEADER 0xAA 22 | #define MIN_DISTANCE 20 23 | #define MAX_DISTANCE 1000 24 | #define DEFAULT_DISTANCE 1000 25 | #define NOISE_THRESHOLD 5 26 | 27 | // ------------------------- Display Parameters ------------------------- 28 | int updateInterval = 20; 29 | float movingIntensity = 0.3; // Stored as 0.0-1.0 30 | float stationaryIntensity = 0.03; // Stored as 0.0-0.1 (0-10%) 31 | int movingLength = 33; 32 | int centerShift = 0; 33 | int additionalLEDs = 0; 34 | CRGB baseColor = CRGB(255, 200, 50); 35 | int ledOffDelay = 5; 36 | int gradientSoftness = 7; // Gradient configuration 37 | 38 | // Global sensor distance 39 | volatile unsigned int g_sensorDistance = DEFAULT_DISTANCE; 40 | 41 | // Background Light Mode 42 | volatile bool backgroundModeActive = false; 43 | 44 | // ------------------------- Time and Schedule Parameters (Local Time Logic) ------------------------- 45 | int startHour = 20; // Start time (local) 46 | int startMinute = 0; 47 | int endHour = 8; // End time (local) 48 | int endMinute = 30; 49 | bool lightOn = true; // Current light state (based on schedule or manual override) 50 | unsigned long lastTimeCheck = 0; // For periodic time checks 51 | volatile bool smarthomeOverride = false; // Flag for manual control 52 | 53 | // Variables for local time 54 | volatile int clientTimezoneOffsetMinutes = 0; // Client offset from UTC in minutes (East +, West -) 55 | volatile bool isTimeOffsetSet = false; // Flag indicating offset has been set 56 | // ------------------------------------------------- 57 | 58 | // ------------------------- EEPROM ------------------------- 59 | #define EEPROM_SIZE 132 // was 128, +4 bytes for int offset 60 | 61 | // Load settings from EEPROM 62 | void loadSettings() { 63 | Serial.println("Loading settings from EEPROM..."); 64 | EEPROM.begin(EEPROM_SIZE); 65 | int offset = 0; 66 | 67 | EEPROM.get(offset, reinterpret_cast(updateInterval)); offset += sizeof(updateInterval); 68 | EEPROM.get(offset, reinterpret_cast(ledOffDelay)); offset += sizeof(ledOffDelay); 69 | EEPROM.get(offset, reinterpret_cast(movingIntensity)); offset += sizeof(movingIntensity); 70 | EEPROM.get(offset, reinterpret_cast(stationaryIntensity)); offset += sizeof(stationaryIntensity); 71 | EEPROM.get(offset, reinterpret_cast(movingLength)); offset += sizeof(movingLength); 72 | EEPROM.get(offset, reinterpret_cast(centerShift)); offset += sizeof(centerShift); 73 | { 74 | int temp = additionalLEDs; 75 | EEPROM.get(offset, temp); 76 | additionalLEDs = temp; 77 | offset += sizeof(temp); 78 | } 79 | EEPROM.get(offset, baseColor); offset += sizeof(baseColor); 80 | offset += sizeof(float); // Skip speedMultiplier placeholder 81 | EEPROM.get(offset, startHour); offset += sizeof(startHour); 82 | EEPROM.get(offset, startMinute); offset += sizeof(startMinute); 83 | EEPROM.get(offset, endHour); offset += sizeof(endHour); 84 | EEPROM.get(offset, endMinute); offset += sizeof(endMinute); 85 | EEPROM.get(offset, gradientSoftness); offset += sizeof(gradientSoftness); 86 | { 87 | int temp_tz = 0; 88 | EEPROM.get(offset, temp_tz); 89 | if (temp_tz >= -720 && temp_tz <= 840) { 90 | clientTimezoneOffsetMinutes = temp_tz; 91 | isTimeOffsetSet = true; 92 | } else { 93 | clientTimezoneOffsetMinutes = 0; 94 | isTimeOffsetSet = false; 95 | } 96 | offset += sizeof(temp_tz); 97 | } 98 | EEPROM.end(); 99 | 100 | // Validate loaded values 101 | if (updateInterval < 10 || updateInterval > 200) updateInterval = 20; 102 | if (ledOffDelay < 1 || ledOffDelay > 60) ledOffDelay = 5; 103 | if (movingIntensity < 0.0 || movingIntensity > 1.0) movingIntensity = 0.3; 104 | if (stationaryIntensity < 0.0 || stationaryIntensity > 0.1) stationaryIntensity = 0.03; // Max 10% 105 | if (movingLength < 1 || movingLength > NUM_LEDS) movingLength = 33; 106 | if (abs(centerShift) > NUM_LEDS/2) centerShift = 0; 107 | if (additionalLEDs < 0 || additionalLEDs > NUM_LEDS/2) additionalLEDs = 0; 108 | gradientSoftness = constrain(gradientSoftness, 0, 10); 109 | startHour = constrain(startHour, 0, 23); startMinute = constrain(startMinute, 0, 59); 110 | endHour = constrain(endHour, 0, 23); endMinute = constrain(endMinute, 0, 59); 111 | 112 | Serial.println("Settings loaded and validated:"); 113 | Serial.print("Update interval: "); Serial.println(updateInterval); 114 | Serial.print("LED off delay: "); Serial.println(ledOffDelay); 115 | Serial.print("Moving intensity: "); Serial.print(movingIntensity * 100.0, 0); Serial.println("%"); 116 | Serial.print("Stationary intensity: "); Serial.print(stationaryIntensity * 100.0, 1); Serial.println("%"); 117 | Serial.print("Moving length: "); Serial.println(movingLength); 118 | Serial.print("Center shift: "); Serial.println(centerShift); 119 | Serial.print("Additional LEDs: "); Serial.println(additionalLEDs); 120 | Serial.print("Gradient Softness: "); Serial.println(gradientSoftness); 121 | Serial.print("Base color RGB: "); Serial.print(baseColor.r); Serial.print(", "); 122 | Serial.print(baseColor.g); Serial.print(", "); Serial.println(baseColor.b); 123 | Serial.printf("Schedule: %02d:%02d - %02d:%02d (Local Time)\n", startHour, startMinute, endHour, endMinute); 124 | Serial.print("Client Timezone Offset: "); Serial.print(clientTimezoneOffsetMinutes); Serial.print(" minutes from UTC"); 125 | if (!isTimeOffsetSet) Serial.print(" (Default/Not Set)"); 126 | Serial.println(); 127 | } 128 | 129 | // Save settings to EEPROM 130 | void saveSettings() { 131 | Serial.println("Saving settings to EEPROM..."); 132 | EEPROM.begin(EEPROM_SIZE); 133 | int offset = 0; 134 | 135 | EEPROM.put(offset, updateInterval); offset += sizeof(updateInterval); 136 | EEPROM.put(offset, ledOffDelay); offset += sizeof(ledOffDelay); 137 | EEPROM.put(offset, movingIntensity); offset += sizeof(movingIntensity); 138 | EEPROM.put(offset, stationaryIntensity); offset += sizeof(stationaryIntensity); 139 | EEPROM.put(offset, movingLength); offset += sizeof(movingLength); 140 | EEPROM.put(offset, centerShift); offset += sizeof(centerShift); 141 | { 142 | int temp = additionalLEDs; 143 | EEPROM.put(offset, temp); offset += sizeof(temp); 144 | } 145 | EEPROM.put(offset, baseColor); offset += sizeof(baseColor); 146 | offset += sizeof(float); // Skip speedMultiplier placeholder 147 | EEPROM.put(offset, startHour); offset += sizeof(startHour); 148 | EEPROM.put(offset, startMinute); offset += sizeof(startMinute); 149 | EEPROM.put(offset, endHour); offset += sizeof(endHour); 150 | EEPROM.put(offset, endMinute); offset += sizeof(endMinute); 151 | EEPROM.put(offset, gradientSoftness); offset += sizeof(gradientSoftness); 152 | { 153 | int temp_tz = clientTimezoneOffsetMinutes; 154 | EEPROM.put(offset, temp_tz); 155 | offset += sizeof(temp_tz); 156 | } 157 | 158 | boolean result = EEPROM.commit(); 159 | EEPROM.end(); 160 | 161 | Serial.print("Settings saved to EEPROM: "); 162 | Serial.println(result ? "OK" : "FAILED"); 163 | } 164 | 165 | // ------------------------- Web Server ------------------------- 166 | WebServer server(80); 167 | 168 | // HTTP handler prototypes 169 | void handleRoot(); 170 | void handleSetInterval(); 171 | void handleSetLedOffDelay(); 172 | void handleSetBaseColor(); 173 | void handleSetMovingIntensity(); 174 | void handleSetStationaryIntensity(); 175 | void handleSetMovingLength(); 176 | void handleSetAdditionalLEDs(); 177 | void handleSetCenterShift(); 178 | void handleSetGradientSoftness(); 179 | void handleSetTime(); 180 | void handleSetSchedule(); 181 | void handleNotFound(); 182 | void handleSmartHomeOn(); 183 | void handleSmartHomeOff(); 184 | void handleSmartHomeClear(); 185 | void handleToggleBackgroundMode(); 186 | void handleGetCurrentTime(); // NEW: Handler for getting current time 187 | void updateTime(); 188 | 189 | // OTA setup prototype 190 | void setupOTA(); 191 | 192 | // ------------------------- Smart Home Integration Endpoints ------------------------- 193 | void handleSmartHomeOn() { 194 | lightOn = true; 195 | smarthomeOverride = true; 196 | server.send(200, "text/plain", "Smart Home Override: ON"); 197 | } 198 | void handleSmartHomeOff() { 199 | lightOn = false; 200 | smarthomeOverride = true; 201 | server.send(200, "text/plain", "Smart Home Override: OFF"); 202 | } 203 | 204 | // Modified to use local time for recalculation 205 | void handleSmartHomeClear() { 206 | smarthomeOverride = false; 207 | Serial.println("Smart Home Override Cleared."); 208 | // Immediately check schedule after override is cleared, USING LOCAL TIME 209 | time_t nowUtc = time(nullptr); 210 | if (nowUtc < 1000000000UL || !isTimeOffsetSet) { // If time/offset not set 211 | Serial.println("Time or TZ Offset not set. Defaulting light ON."); 212 | lightOn = true; 213 | } else { 214 | // Calculate client's local time 215 | time_t clientLocalEpoch = nowUtc + (clientTimezoneOffsetMinutes * 60); 216 | struct tm timeinfo_local; 217 | gmtime_r(&clientLocalEpoch, &timeinfo_local); // Use gmtime_r to convert calculated local epoch 218 | 219 | int currentTotalMinutes = timeinfo_local.tm_hour * 60 + timeinfo_local.tm_min; 220 | int startTotalMinutes = startHour * 60 + startMinute; 221 | int endTotalMinutes = endHour * 60 + endMinute; 222 | 223 | // Schedule checking logic (same, but with local time) 224 | if (startTotalMinutes <= endTotalMinutes) 225 | lightOn = (currentTotalMinutes >= startTotalMinutes && currentTotalMinutes < endTotalMinutes); 226 | else // Schedule crosses midnight 227 | lightOn = (currentTotalMinutes >= startTotalMinutes || currentTotalMinutes < endTotalMinutes); 228 | 229 | Serial.print("Schedule re-evaluated using local time. Light should be: "); 230 | Serial.println(lightOn ? "ON" : "OFF"); 231 | Serial.printf(" (Based on Est. Local Time: %02d:%02d)\n", timeinfo_local.tm_hour, timeinfo_local.tm_min); 232 | } 233 | server.send(200, "text/plain", "Smart Home Override: CLEARED"); 234 | } 235 | 236 | 237 | // ------------------------- Mode Handlers ------------------------- 238 | void handleToggleBackgroundMode() { 239 | backgroundModeActive = !backgroundModeActive; 240 | Serial.print("Background mode toggled: "); Serial.println(backgroundModeActive ? "ON" : "OFF"); 241 | server.sendHeader("Location", "/"); 242 | server.send(303); 243 | } 244 | 245 | // ------------------------- Sensor Reading Function ------------------------- 246 | unsigned int readSensorData() { 247 | #ifndef SIMULATE_SENSOR 248 | if (Serial1.available() < 7) return g_sensorDistance; 249 | if (Serial1.read() != SENSOR_HEADER) { 250 | while (Serial1.available()) Serial1.read(); 251 | return g_sensorDistance; 252 | } 253 | if (Serial1.read() != SENSOR_HEADER) { 254 | while (Serial1.available()) Serial1.read(); 255 | return g_sensorDistance; 256 | } 257 | byte buf[5]; 258 | size_t bytesRead = Serial1.readBytes(buf, 5); 259 | if (bytesRead < 5) { 260 | while (Serial1.available()) Serial1.read(); 261 | return g_sensorDistance; 262 | } 263 | unsigned int distance = (buf[2] << 8) | buf[1]; 264 | if (distance < MIN_DISTANCE || distance > MAX_DISTANCE) return g_sensorDistance; 265 | return distance; 266 | #else 267 | static unsigned int simulatedDistance = MIN_DISTANCE; 268 | simulatedDistance += 10; 269 | if (simulatedDistance > MAX_DISTANCE) simulatedDistance = MIN_DISTANCE; 270 | return simulatedDistance; 271 | #endif 272 | } 273 | 274 | // ------------------------- RTOS Tasks ------------------------- 275 | void sensorTask(void * parameter) { 276 | Serial.println("Sensor Task started"); 277 | for (;;) { 278 | unsigned int newDistance = readSensorData(); 279 | g_sensorDistance = newDistance; 280 | vTaskDelay(pdMS_TO_TICKS(5)); 281 | } 282 | } 283 | 284 | void ledTask(void * parameter) { 285 | static unsigned int lastSensor = g_sensorDistance; 286 | static int lastMovementDirection = 0; 287 | static unsigned long lastMovementTime = millis(); 288 | 289 | FastLED.clear(); 290 | FastLED.show(); 291 | vTaskDelay(pdMS_TO_TICKS(1000)); // Initial delay 292 | 293 | Serial.println("LED Task initialized and starting main loop"); 294 | 295 | for (;;) { 296 | unsigned long currentMillis = millis(); 297 | unsigned int currentDistance = g_sensorDistance; 298 | int diff = (int)currentDistance - (int)lastSensor; 299 | int absDiff = abs(diff); 300 | 301 | // Movement detection 302 | if (absDiff >= NOISE_THRESHOLD) { 303 | if (currentMillis - lastMovementTime > 50 || (diff > 0 && lastMovementDirection < 0) || (diff < 0 && lastMovementDirection > 0)) { 304 | lastMovementTime = currentMillis; 305 | lastMovementDirection = (diff > 0) ? 1 : -1; 306 | } 307 | } 308 | lastSensor = currentDistance; 309 | 310 | bool drawMovingPart = (currentMillis - lastMovementTime <= ledOffDelay * 1000); 311 | 312 | // --- Background Fill --- 313 | if (!lightOn) { 314 | fill_solid(leds, NUM_LEDS, CRGB::Black); 315 | } else if (backgroundModeActive) { 316 | // Use stationaryIntensity (0.0 to 0.1) 317 | uint8_t r = max((uint8_t)1, (uint8_t)(baseColor.r * stationaryIntensity)); 318 | uint8_t g = max((uint8_t)1, (uint8_t)(baseColor.g * stationaryIntensity)); 319 | uint8_t b = max((uint8_t)1, (uint8_t)(baseColor.b * stationaryIntensity)); 320 | fill_solid(leds, NUM_LEDS, CRGB(r, g, b)); 321 | } else { 322 | fill_solid(leds, NUM_LEDS, CRGB::Black); 323 | } 324 | 325 | // --- Moving Beam Drawing --- 326 | if (lightOn && drawMovingPart) { 327 | float prop = constrain((float)(currentDistance - MIN_DISTANCE) / (MAX_DISTANCE - MIN_DISTANCE), 0.0, 1.0); 328 | int ledPosition = round(prop * (NUM_LEDS - 1)); 329 | int centerLED = constrain(ledPosition + centerShift, 0, NUM_LEDS - 1); 330 | 331 | // Use movingIntensity (0.0 to 1.0) 332 | CRGB fullBrightColor = CRGB((uint8_t)(baseColor.r * movingIntensity), 333 | (uint8_t)(baseColor.g * movingIntensity), 334 | (uint8_t)(baseColor.b * movingIntensity)); 335 | 336 | int direction = lastMovementDirection; 337 | if (direction == 0) direction = 1; // Default direction if no movement detected yet 338 | 339 | int halfMainLength = movingLength / 2; 340 | int totalLightLength = movingLength + additionalLEDs; 341 | if (totalLightLength <= 0) totalLightLength = 1; 342 | 343 | int leftEdge, rightEdge; 344 | if (direction > 0) { // Moving away 345 | leftEdge = centerLED - halfMainLength; 346 | rightEdge = leftEdge + movingLength - 1 + additionalLEDs; 347 | } else { // Moving towards 348 | rightEdge = centerLED + halfMainLength; 349 | leftEdge = rightEdge - movingLength + 1 - additionalLEDs; 350 | } 351 | 352 | // Clamp edges to valid LED indices 353 | leftEdge = max(0, leftEdge); 354 | rightEdge = min(NUM_LEDS - 1, rightEdge); 355 | 356 | // Calculate effective gradient parameters using gradientSoftness 357 | int effectiveFadeWidth = map(gradientSoftness, 0, 10, 1, 10); 358 | float effectiveFadeExponent = 1.0 + (gradientSoftness / 10.0) * 2.0; 359 | 360 | int actualBeamPixelLength = rightEdge - leftEdge + 1; 361 | effectiveFadeWidth = constrain(effectiveFadeWidth, 1, max(1, actualBeamPixelLength / 2)); 362 | 363 | // Draw the beam with gradient 364 | for (int i = leftEdge; i <= rightEdge; i++) { 365 | int posInBeam; 366 | if (direction > 0) { 367 | posInBeam = i - leftEdge; 368 | } else { 369 | posInBeam = rightEdge - i; 370 | } 371 | 372 | float factor = 1.0f; 373 | 374 | // Apply fade if needed 375 | if (gradientSoftness > 0 && totalLightLength > 1) { 376 | if (posInBeam < effectiveFadeWidth) { 377 | float normalizedPos = (effectiveFadeWidth > 0) ? (float)(posInBeam + 1) / (effectiveFadeWidth + 1) : 1.0f; 378 | factor = pow(normalizedPos, effectiveFadeExponent); 379 | } 380 | else if (posInBeam >= (totalLightLength - effectiveFadeWidth)) { 381 | int posFromEnd = totalLightLength - 1 - posInBeam; 382 | float normalizedPos = (effectiveFadeWidth > 0) ? (float)(posFromEnd + 1) / (effectiveFadeWidth + 1) : 1.0f; 383 | factor = pow(normalizedPos, effectiveFadeExponent); 384 | } 385 | } 386 | 387 | factor = constrain(factor, 0.0f, 1.0f); 388 | 389 | // Set LED color if factor is significant 390 | if (factor > 0.01f) { 391 | CRGB beamColor; 392 | beamColor.r = (uint8_t)(fullBrightColor.r * factor); 393 | beamColor.g = (uint8_t)(fullBrightColor.g * factor); 394 | beamColor.b = (uint8_t)(fullBrightColor.b * factor); 395 | 396 | // Blend with background if background mode is active 397 | if (backgroundModeActive) { 398 | uint8_t bgR = max((uint8_t)1, (uint8_t)(baseColor.r * stationaryIntensity)); 399 | uint8_t bgG = max((uint8_t)1, (uint8_t)(baseColor.g * stationaryIntensity)); 400 | uint8_t bgB = max((uint8_t)1, (uint8_t)(baseColor.b * stationaryIntensity)); 401 | 402 | leds[i].r = max(beamColor.r, leds[i].r); 403 | leds[i].g = max(beamColor.g, leds[i].g); 404 | leds[i].b = max(beamColor.b, leds[i].b); 405 | leds[i].r = max(leds[i].r, bgR); 406 | leds[i].g = max(leds[i].g, bgG); 407 | leds[i].b = max(leds[i].b, bgB); 408 | } else { 409 | leds[i] = beamColor; // Just set the beam color 410 | } 411 | } 412 | } // End of pixel loop 413 | } // End of drawMovingPart 414 | 415 | FastLED.show(); 416 | vTaskDelay(pdMS_TO_TICKS(updateInterval)); 417 | } // End of infinite loop 418 | } 419 | 420 | void webServerTask(void * parameter) { 421 | Serial.println("Web Server Task started"); 422 | for (;;) { 423 | server.handleClient(); 424 | ArduinoOTA.handle(); // Handle OTA updates here 425 | vTaskDelay(pdMS_TO_TICKS(2)); // Small delay for web server handling 426 | } 427 | } 428 | 429 | // ------------------------- Time Functions (Local Time Logic) ------------------------- 430 | 431 | // Modified: Sets UTC time AND saves client TZ offset 432 | void handleSetTime() { 433 | bool timeUpdated = false; 434 | bool tzUpdated = false; 435 | 436 | // Handling Timezone (TZ) offset 437 | if (server.hasArg("tz")) { 438 | int tz_offset_minutes = server.arg("tz").toInt(); 439 | // Check validity and save if changed 440 | if (tz_offset_minutes >= -720 && tz_offset_minutes <= 840) { // -12:00 to +14:00 441 | if (!isTimeOffsetSet || clientTimezoneOffsetMinutes != tz_offset_minutes) { 442 | clientTimezoneOffsetMinutes = tz_offset_minutes; 443 | isTimeOffsetSet = true; 444 | tzUpdated = true; 445 | Serial.print("Client timezone offset received and set to (minutes): "); 446 | Serial.println(clientTimezoneOffsetMinutes); 447 | saveSettings(); // Save settings, including new TZ 448 | } 449 | } else { 450 | Serial.print("Received invalid timezone offset (minutes): "); 451 | Serial.println(tz_offset_minutes); 452 | } 453 | } 454 | 455 | // Handling time setting (Epoch UTC) 456 | if (server.hasArg("epoch")) { 457 | unsigned long epoch = strtoul(server.arg("epoch").c_str(), NULL, 10); 458 | Serial.print("Received Epoch: "); Serial.println(epoch); 459 | 460 | if (epoch > 946684800UL) { // Check Epoch validity (after 1 Jan 2000) 461 | struct timeval tv; 462 | tv.tv_sec = epoch; // Set UTC time 463 | tv.tv_usec = 0; 464 | settimeofday(&tv, NULL); 465 | Serial.print("System time set via settimeofday() to (UTC): "); Serial.println(epoch); 466 | timeUpdated = true; 467 | } else { 468 | Serial.print("Received invalid epoch value: "); Serial.println(epoch); 469 | } 470 | } 471 | 472 | // After setting time or TZ, immediately check schedule 473 | if (timeUpdated || tzUpdated) { 474 | updateTime(); // Call for immediate reaction to time/TZ change 475 | } 476 | 477 | // Form response 478 | String response = ""; 479 | if(timeUpdated) response += "Time OK"; 480 | if(tzUpdated) response += (response.length() > 0 ? ", TZ OK" : "TZ OK"); 481 | if(response.length() == 0) response = "No change"; 482 | 483 | server.send(200, "text/plain", response); 484 | } 485 | 486 | // Modified: Uses saved TZ offset to calculate local time 487 | void updateTime() { 488 | unsigned long currentMillis = millis(); 489 | if (currentMillis - lastTimeCheck >= 1000) { 490 | lastTimeCheck = currentMillis; 491 | time_t nowUtc = time(nullptr); // Get current UTC time 492 | 493 | // Periodically check if time has been set 494 | static bool lastTimeValid = false; 495 | if (nowUtc > 1000000000UL && !lastTimeValid) { 496 | lastTimeValid = true; 497 | Serial.println("NTP time received successfully!"); 498 | } 499 | 500 | // Check that time is synchronized and offset is set 501 | if (nowUtc < 1000000000UL || !isTimeOffsetSet) { 502 | // Default ON if time/TZ not set, but only if not overridden 503 | if (!smarthomeOverride) { 504 | lightOn = true; 505 | } 506 | return; // Exit if we cannot check schedule 507 | } 508 | 509 | // Calculate client's local time 510 | time_t clientLocalEpoch = nowUtc + (clientTimezoneOffsetMinutes * 60); 511 | struct tm timeinfo_local; 512 | gmtime_r(&clientLocalEpoch, &timeinfo_local); // Convert local epoch into tm structure 513 | 514 | // Compare with schedule (using local hours/minutes) 515 | int currentTotalMinutes = timeinfo_local.tm_hour * 60 + timeinfo_local.tm_min; 516 | int startTotalMinutes = startHour * 60 + startMinute; 517 | int endTotalMinutes = endHour * 60 + endMinute; 518 | bool shouldBeOn; 519 | 520 | if (startTotalMinutes <= endTotalMinutes) { // Schedule within the same day 521 | shouldBeOn = (currentTotalMinutes >= startTotalMinutes && currentTotalMinutes < endTotalMinutes); 522 | } else { // Schedule crosses midnight 523 | shouldBeOn = (currentTotalMinutes >= startTotalMinutes || currentTotalMinutes < endTotalMinutes); 524 | } 525 | 526 | // Update light state if schedule changes it AND no manual override 527 | if (!smarthomeOverride && (lightOn != shouldBeOn)) { 528 | lightOn = shouldBeOn; 529 | Serial.print("Schedule updated light state to: "); Serial.println(lightOn ? "ON" : "OFF"); 530 | Serial.printf(" (Based on Est. Local Time: %02d:%02d)\n", timeinfo_local.tm_hour, timeinfo_local.tm_min); 531 | } 532 | } 533 | } 534 | 535 | // NEW: Handler to return current estimated local time as JSON 536 | void handleGetCurrentTime() { 537 | char timeStr[20] = "N/A"; // Default if time/tz not set 538 | time_t nowUtc = time(nullptr); 539 | 540 | if (nowUtc > 1000000000UL && isTimeOffsetSet) { 541 | time_t clientLocalEpoch = nowUtc + (clientTimezoneOffsetMinutes * 60); 542 | struct tm timeinfo_client; 543 | gmtime_r(&clientLocalEpoch, &timeinfo_client); // Use gmtime_r for thread safety 544 | strftime(timeStr, sizeof(timeStr), "%H:%M:%S", &timeinfo_client); 545 | } else if (nowUtc > 1000000000UL && !isTimeOffsetSet) { 546 | // Time synced, but TZ offset missing 547 | strcpy(timeStr, "TZ Not Set"); 548 | } else { 549 | // Time not synced yet 550 | strcpy(timeStr, "Sync Pend"); 551 | } 552 | 553 | String json = "{\"time\":\""; 554 | json += timeStr; 555 | json += "\"}"; 556 | server.send(200, "application/json", json); 557 | } 558 | 559 | 560 | // (handleSetSchedule, handleNotFound - no changes needed) 561 | void handleSetSchedule() { 562 | if (server.hasArg("startHour") && server.hasArg("startMinute") && 563 | server.hasArg("endHour") && server.hasArg("endMinute")) { 564 | startHour = server.arg("startHour").toInt(); 565 | startMinute = server.arg("startMinute").toInt(); 566 | endHour = server.arg("endHour").toInt(); 567 | endMinute = server.arg("endMinute").toInt(); 568 | startHour = constrain(startHour, 0, 23); 569 | startMinute = constrain(startMinute, 0, 59); 570 | endHour = constrain(endHour, 0, 23); 571 | endMinute = constrain(endMinute, 0, 59); 572 | 573 | Serial.print("Schedule set via HTTP: "); 574 | Serial.printf("%02d:%02d - %02d:%02d (Local)\n", startHour, startMinute, endHour, endMinute); 575 | saveSettings(); // Save the new schedule to EEPROM 576 | updateTime(); // Immediately check if the new schedule changes the light state 577 | } 578 | server.sendHeader("Location", "/"); // Redirect back to the root page 579 | server.send(303); 580 | } 581 | void handleNotFound() { 582 | server.send(404, "text/plain", "Not Found"); 583 | } 584 | 585 | 586 | // ------------------------- WiFi Setup ------------------------- 587 | void setupWiFi() { 588 | WiFi.mode(WIFI_AP); 589 | IPAddress local_IP(192, 168, 4, 22); 590 | IPAddress gateway(192, 168, 4, 1); 591 | IPAddress subnet(255, 255, 255, 0); 592 | WiFi.softAPConfig(local_IP, gateway, subnet); 593 | 594 | // Generate Random Suffix 595 | String uniqueID = ""; 596 | for (int i = 0; i < 3; i++) { 597 | char randomLetter = 'A' + random(0, 26); // Generate A-Z 598 | uniqueID += randomLetter; 599 | } 600 | 601 | String deviceName = "LightTrack " + uniqueID; // Use random suffix 602 | WiFi.softAP(deviceName.c_str(), "12345678"); 603 | Serial.print("WiFi AP Name: "); Serial.println(deviceName); 604 | 605 | // Reduce WiFi power slightly (optional) 606 | esp_wifi_set_max_tx_power(21); // Corresponds to +8.5dBm on C3 607 | 608 | Serial.println("AP IP address: "); 609 | Serial.println(WiFi.softAPIP()); 610 | 611 | // Register HTTP handlers 612 | server.on("/", handleRoot); 613 | server.on("/setInterval", handleSetInterval); 614 | server.on("/setLedOffDelay", handleSetLedOffDelay); 615 | server.on("/setBaseColor", handleSetBaseColor); 616 | server.on("/setMovingIntensity", handleSetMovingIntensity); 617 | server.on("/setStationaryIntensity", handleSetStationaryIntensity); 618 | server.on("/setMovingLength", handleSetMovingLength); 619 | server.on("/setAdditionalLEDs", handleSetAdditionalLEDs); 620 | server.on("/setCenterShift", handleSetCenterShift); 621 | server.on("/setGradientSoftness", handleSetGradientSoftness); 622 | server.on("/setTime", handleSetTime); 623 | server.on("/setSchedule", handleSetSchedule); 624 | server.on("/smarthome/on", handleSmartHomeOn); 625 | server.on("/smarthome/off", handleSmartHomeOff); 626 | server.on("/smarthome/clear", handleSmartHomeClear); 627 | server.on("/toggleNightMode", handleToggleBackgroundMode); 628 | server.on("/getCurrentTime", handleGetCurrentTime); // NEW: Register time endpoint 629 | server.onNotFound(handleNotFound); 630 | 631 | server.begin(); 632 | Serial.println("Web server started."); 633 | } 634 | 635 | // ------------------------- Web Interface Handler ------------------------- 636 | void handleRoot() { 637 | char scheduleStartStr[6]; 638 | sprintf(scheduleStartStr, "%02d:%02d", startHour, startMinute); 639 | char scheduleEndStr[6]; 640 | sprintf(scheduleEndStr, "%02d:%02d", endHour, endMinute); 641 | 642 | int movingIntensityPercent = (int)(movingIntensity * 100.0 + 0.5); 643 | float stationaryIntensityPercent = stationaryIntensity * 100.0; 644 | 645 | // Use standard string concatenation for HTML 646 | String html = ""; 647 | html += ""; 648 | html += ""; 649 | html += "LED Control Panel"; 650 | html += ""; 651 | html += ""; 669 | // ----- MODIFIED JAVASCRIPT ----- 670 | html += ""; 711 | // ----- END OF MODIFIED JAVASCRIPT ----- 712 | html += ""; 713 | html += ""; // Call setDeviceTime on load, which will then call updateTimeDisplay 714 | html += "
"; 715 | html += "

LED Control Panel

"; 716 | 717 | html += ""; 722 | 723 | html += "

Moving Light Intensity: "; html += String(movingIntensityPercent); html += "%

"; 724 | html += ""; 725 | 726 | html += "

Moving Light Length: "; html += String(movingLength); html += "

"; 727 | html += ""; 728 | 729 | html += "

Additional LEDs (direction): "; html += String(additionalLEDs); html += "

"; 730 | html += ""; 731 | 732 | html += "

Gradient Softness (0=Hard, 10=Soft): "; html += String(gradientSoftness); html += "

"; 733 | html += ""; 734 | 735 | html += "

Center Shift (LEDs): "; html += String(centerShift); html += "

"; 736 | html += ""; 737 | 738 | html += "

LED Off Delay (seconds): "; html += String(ledOffDelay); html += "

"; 739 | html += ""; 740 | 741 | html += "
"; 742 | 743 | html += "

Background Light Mode:

"; 744 | html += ""; 745 | 746 | html += "

Background Light Intensity: "; html += String(stationaryIntensityPercent, 1); html += "%

"; 747 | html += ""; 748 | 749 | html += "
"; 750 | 751 | html += "

Schedule Window (Local Time):

"; 752 | html += "
"; 753 | html += ""; 754 | html += ""; 755 | html += "
"; 756 | // ----- NEW HTML FOR TIME DISPLAY ----- 757 | html += "
Est. Local Time: Loading...
"; 758 | // ------------------------------------ 759 | 760 | html += ""; 761 | html += "
"; // container 762 | html += ""; 763 | html += ""; 764 | 765 | server.send(200, "text/html", html); 766 | } 767 | 768 | 769 | // ------------------------- HTTP Handlers (Settings) ------------------------- 770 | // (No changes needed for these handlers) 771 | void handleSetInterval() { 772 | if (server.hasArg("value")) { 773 | updateInterval = server.arg("value").toInt(); 774 | if(updateInterval < 10) updateInterval = 10; 775 | Serial.print("Update interval set to: "); Serial.println(updateInterval); 776 | saveSettings(); 777 | } 778 | server.sendHeader("Location", "/"); 779 | server.send(303); 780 | } 781 | void handleSetLedOffDelay() { 782 | if (server.hasArg("value")) { 783 | ledOffDelay = server.arg("value").toInt(); 784 | ledOffDelay = constrain(ledOffDelay, 1, 60); 785 | Serial.print("LED off delay set to: "); Serial.println(ledOffDelay); 786 | saveSettings(); 787 | } 788 | server.sendHeader("Location", "/"); 789 | server.send(303); 790 | } 791 | void handleSetBaseColor() { 792 | if (server.hasArg("r") && server.hasArg("g") && server.hasArg("b")) { 793 | baseColor = CRGB(server.arg("r").toInt(), server.arg("g").toInt(), server.arg("b").toInt()); 794 | Serial.print("Base color set to RGB: "); Serial.print(baseColor.r); Serial.print(", "); Serial.print(baseColor.g); Serial.print(", "); Serial.println(baseColor.b); 795 | saveSettings(); 796 | } 797 | // No redirect needed for color change - async update 798 | server.send(200, "text/plain", "OK"); 799 | } 800 | void handleSetMovingLength() { 801 | if (server.hasArg("value")) { 802 | movingLength = server.arg("value").toInt(); 803 | movingLength = constrain(movingLength, 1, NUM_LEDS); 804 | Serial.print("Moving length set to: "); Serial.println(movingLength); 805 | saveSettings(); 806 | } 807 | // No redirect needed - async update 808 | server.send(200, "text/plain", "OK"); 809 | } 810 | void handleSetAdditionalLEDs() { 811 | if (server.hasArg("value")) { 812 | additionalLEDs = server.arg("value").toInt(); 813 | additionalLEDs = constrain(additionalLEDs, 0, NUM_LEDS / 2); 814 | Serial.print("Additional LEDs set to: "); Serial.println(additionalLEDs); 815 | saveSettings(); 816 | } 817 | // No redirect needed - async update 818 | server.send(200, "text/plain", "OK"); 819 | } 820 | void handleSetCenterShift() { 821 | if (server.hasArg("value")) { 822 | centerShift = server.arg("value").toInt(); 823 | centerShift = constrain(centerShift, -NUM_LEDS / 2, NUM_LEDS / 2); 824 | Serial.print("Center shift set to: "); Serial.println(centerShift); 825 | saveSettings(); 826 | } 827 | // No redirect needed - async update 828 | server.send(200, "text/plain", "OK"); 829 | } 830 | // Intensity Handlers Updated 831 | void handleSetMovingIntensity() { 832 | if (server.hasArg("value")) { 833 | float val_percent = server.arg("value").toFloat(); 834 | movingIntensity = constrain(val_percent / 100.0, 0.0, 1.0); 835 | Serial.print("Moving intensity set to: "); Serial.print(movingIntensity * 100.0, 0); Serial.println("%"); 836 | saveSettings(); 837 | } 838 | // No redirect needed - async update 839 | server.send(200, "text/plain", "OK"); 840 | } 841 | void handleSetStationaryIntensity() { 842 | if (server.hasArg("value")) { 843 | float val_percent = server.arg("value").toFloat(); 844 | stationaryIntensity = constrain(val_percent / 100.0, 0.0, 0.1); 845 | Serial.print("Stationary intensity set to: "); Serial.print(stationaryIntensity * 100.0, 1); Serial.println("%"); 846 | saveSettings(); 847 | } 848 | // No redirect needed - async update 849 | server.send(200, "text/plain", "OK"); 850 | } 851 | // Gradient Handler (Exists) 852 | void handleSetGradientSoftness() { 853 | if (server.hasArg("value")) { 854 | gradientSoftness = server.arg("value").toInt(); 855 | gradientSoftness = constrain(gradientSoftness, 0, 10); 856 | Serial.print("Gradient Softness set to: "); Serial.println(gradientSoftness); 857 | saveSettings(); 858 | } 859 | // No redirect needed - async update 860 | server.send(200, "text/plain", "OK"); 861 | } 862 | 863 | 864 | // ------------------------- OTA Setup Function ------------------------- 865 | void setupOTA() { 866 | ArduinoOTA.onStart([]() { 867 | String type = ArduinoOTA.getCommand() == U_FLASH ? "sketch" : "filesystem"; 868 | Serial.println("Start updating " + type); 869 | }); 870 | ArduinoOTA.onEnd([]() { 871 | Serial.println("\nEnd"); 872 | }); 873 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { 874 | Serial.printf("Progress: %u%%\r", (progress / (total / 100))); 875 | }); 876 | ArduinoOTA.onError([](ota_error_t error) { 877 | Serial.printf("Error[%u]: ", error); 878 | if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); 879 | else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); 880 | else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); 881 | else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); 882 | else if (error == OTA_END_ERROR) Serial.println("End Failed"); 883 | }); 884 | 885 | // Use the same device name for OTA hostname 886 | String uniqueID = ""; 887 | uint64_t chipid = ESP.getEfuseMac(); 888 | randomSeed((unsigned long)chipid ^ (unsigned long)(chipid >> 32)); 889 | for (int i = 0; i < 3; i++) { uniqueID += (char)('A' + random(0, 26)); } 890 | String hostname = "LightTrack-" + uniqueID; 891 | 892 | ArduinoOTA.setHostname(hostname.c_str()); 893 | ArduinoOTA.begin(); 894 | Serial.print("OTA Initialized. Hostname: "); Serial.println(hostname); 895 | } 896 | 897 | // ------------------------- Setup ------------------------- 898 | void setup() { 899 | Serial.begin(115200); 900 | delay(500); 901 | Serial.println("\n\n--------------------------------------"); 902 | Serial.println("ESP32-C3 LightTrack Starting (Local Time Schedule)"); 903 | Serial.println("--------------------------------------"); 904 | 905 | // Initialize random number generator 906 | uint64_t chipid = ESP.getEfuseMac(); 907 | randomSeed((unsigned long)chipid ^ (unsigned long)(chipid >> 32)); 908 | 909 | // Initialize LEDs immediately 910 | Serial.println("Initializing LED Strip (FastLED)..."); 911 | FastLED.addLeds(leds, NUM_LEDS).setCorrection(TypicalLEDStrip); 912 | FastLED.setBrightness(255); 913 | FastLED.clear(); 914 | leds[0] = CRGB::White; 915 | FastLED.show(); 916 | 917 | // Initialize SPIFFS and load settings 918 | Serial.println("Initializing SPIFFS & EEPROM..."); 919 | if (!SPIFFS.begin(true)) { 920 | Serial.println("!!! Failed to mount SPIFFS. Formatting..."); 921 | if (!SPIFFS.begin(true)) { 922 | Serial.println("!!! SPIFFS Format failed. Continuing anyway."); 923 | } 924 | } 925 | loadSettings(); 926 | 927 | // Initialize sensor 928 | Serial.println("Initializing Radar Sensor (Serial1)..."); 929 | Serial1.begin(256000, SERIAL_8N1, 20, 21); 930 | 931 | // Set up WiFi 932 | Serial.println("Setting up WiFi AP Mode..."); 933 | setupWiFi(); 934 | 935 | // Start NTP without blocking 936 | Serial.println("Setting up NTP time synchronization (async)..."); 937 | configTzTime("UTC0", "pool.ntp.org", "time.nist.gov"); 938 | 939 | // Set up OTA 940 | Serial.println("Setting up OTA Updates..."); 941 | setupOTA(); 942 | 943 | // Turn off indicator LED 944 | leds[0] = CRGB::Black; 945 | FastLED.show(); 946 | 947 | // Create tasks 948 | Serial.println("Creating RTOS Tasks..."); 949 | xTaskCreatePinnedToCore(sensorTask, "Sensor Task", 2048, NULL, 2, NULL, 1); 950 | xTaskCreatePinnedToCore(ledTask, "LED Task", 8192, NULL, 1, NULL, 1); 951 | xTaskCreatePinnedToCore(webServerTask, "WebServer Task", 4096, NULL, 1, NULL, 0); 952 | 953 | Serial.println("--------------------------------------"); 954 | Serial.println("Setup Complete. System Running."); 955 | Serial.print("Connect to WiFi AP '"); Serial.print(WiFi.softAPSSID()); 956 | Serial.print("' with password '12345678'"); 957 | Serial.print(" and browse to http://"); Serial.println(WiFi.softAPIP()); 958 | Serial.println("--------------------------------------"); 959 | } 960 | 961 | // ------------------------- Loop ------------------------- 962 | void loop() { 963 | updateTime(); // Check schedule using local time calculation 964 | 965 | // Optional status logging 966 | static unsigned long lastLoopLog = 0; 967 | if (millis() - lastLoopLog > 15000) { // Log status every 15 seconds 968 | lastLoopLog = millis(); 969 | Serial.println("--- Status Update ---"); 970 | Serial.print("Uptime: "); Serial.print(millis()/1000); Serial.println(" s"); 971 | 972 | time_t now = time(nullptr); 973 | if (now < 1000000000UL) { 974 | Serial.println("System Time (UTC): Not Synced"); 975 | Serial.println("Est. Local Time: N/A"); 976 | } else { 977 | struct tm timeinfo_utc; 978 | gmtime_r(&now, &timeinfo_utc); 979 | char utc_buf[25]; strftime(utc_buf, sizeof(utc_buf), "%Y-%m-%d %H:%M:%S", &timeinfo_utc); 980 | Serial.print("System Time (UTC): "); Serial.println(utc_buf); 981 | 982 | if (isTimeOffsetSet) { 983 | time_t clientLocalEpoch = now + (clientTimezoneOffsetMinutes * 60); 984 | struct tm timeinfo_client; 985 | gmtime_r(&clientLocalEpoch, &timeinfo_client); 986 | char local_buf[20]; strftime(local_buf, sizeof(local_buf), "%H:%M:%S", &timeinfo_client); 987 | Serial.print("Est. Local Time: "); Serial.print(local_buf); 988 | Serial.print(" (Offset: "); Serial.print(clientTimezoneOffsetMinutes); Serial.println(" min)"); 989 | } else { 990 | Serial.println("Est. Local Time: Timezone Not Set"); 991 | } 992 | } 993 | 994 | Serial.print("Schedule: "); Serial.printf("%02d:%02d - %02d:%02d (Local)\n", startHour, startMinute, endHour, endMinute); 995 | Serial.print("Light status (lightOn): "); Serial.println(lightOn ? "ON" : "OFF"); 996 | Serial.print("Background Mode: "); Serial.println(backgroundModeActive ? "ON" : "OFF"); 997 | Serial.print("SmartHome Override: "); Serial.println(smarthomeOverride ? "YES" : "NO"); 998 | Serial.print("Moving Intensity: "); Serial.print(movingIntensity * 100.0, 0); Serial.println("%"); 999 | Serial.print("Stationary Intensity: "); Serial.print(stationaryIntensity * 100.0, 1); Serial.println("%"); 1000 | Serial.print("Gradient Softness: "); Serial.println(gradientSoftness); 1001 | Serial.print("Free heap: "); Serial.println(ESP.getFreeHeap()); 1002 | Serial.println("---------------------"); 1003 | } 1004 | 1005 | vTaskDelay(pdMS_TO_TICKS(1000)); // Check schedule roughly every second 1006 | } 1007 | -------------------------------------------------------------------------------- /HomeAssistant/homeassistant.txt: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include // For isnan 8 | #include "esp_wifi.h" 9 | #include 10 | #include 11 | #include 12 | 13 | // --- MQTT INTEGRATION START --- 14 | #include // For MQTT 15 | #include // For JSON messages 16 | // --- MQTT INTEGRATION END --- 17 | 18 | // ------------------------- LED Configuration ------------------------- 19 | #define LED_PIN 2 20 | #define NUM_LEDS 300 21 | #define CHIPSET WS2812B 22 | #define COLOR_ORDER GRB 23 | CRGB leds[NUM_LEDS]; 24 | 25 | // ------------------------- Sensor Parameters ------------------------- 26 | #define SENSOR_HEADER 0xAA 27 | #define MIN_DISTANCE 20 28 | #define MAX_DISTANCE 1000 29 | #define DEFAULT_DISTANCE 1000 30 | #define NOISE_THRESHOLD 5 31 | 32 | // ------------------------- Display Parameters ------------------------- 33 | int updateInterval = 20; 34 | float movingIntensity = 0.3; // Stored as 0.0-1.0 35 | float stationaryIntensity = 0.03; // Stored as 0.0-0.1 (0-10%) 36 | int movingLength = 33; 37 | int centerShift = 0; 38 | int additionalLEDs = 0; 39 | CRGB baseColor = CRGB(255, 200, 50); // This is the color variable 40 | int ledOffDelay = 5; 41 | int gradientSoftness = 7; // Gradient configuration 42 | 43 | // Global sensor distance 44 | volatile unsigned int g_sensorDistance = DEFAULT_DISTANCE; 45 | 46 | // Background Light Mode (Used by Web UI toggle primarily now) 47 | volatile bool backgroundModeActive = false; 48 | 49 | // ------------------------- Time and Schedule Parameters (Local Time Logic) ------------------------- 50 | int startHour = 20; // Start time (local) 51 | int startMinute = 0; 52 | int endHour = 8; // End time (local) 53 | int endMinute = 30; 54 | bool lightOn = true; // Current light state (based on schedule or manual override) 55 | unsigned long lastTimeCheck = 0; // For periodic time checks 56 | volatile bool smarthomeOverride = false; // Flag for manual control 57 | 58 | // Variables for local time 59 | volatile int clientTimezoneOffsetMinutes = 0; // Client offset from UTC in minutes (East +, West -) 60 | volatile bool isTimeOffsetSet = false; // Flag indicating offset has been set 61 | 62 | // --- MQTT INTEGRATION START --- 63 | // ############################################################################ 64 | // ## MQTT & HOME ASSISTANT CONFIGURATION ## 65 | // ############################################################################ 66 | 67 | // --- Wi-Fi Credentials (ESP32 will connect to this network) --- 68 | const char* main_wifi_ssid = "YOUR_WIFI_SSID"; // <<<### CHANGE THIS: Your Home Wi-Fi SSID 69 | const char* main_wifi_password = "YOUR_WIFI_PASSWORD"; // <<<### CHANGE THIS: Your Home Wi-Fi Password 70 | 71 | // --- MQTT Broker Settings --- 72 | const char* mqtt_server = "IP_OF_YOUR_HA_MACHINE"; // <<<### CHANGE THIS: IP address of your Home Assistant server 73 | const int mqtt_port = 1883; // Default MQTT port 74 | 75 | // --- MQTT User Credentials --- 76 | // Create a dedicated user in Home Assistant for MQTT (Settings > People > Users) 77 | const char* mqtt_user = "HA_MQTT_USER"; // <<<### CHANGE THIS: Username for MQTT 78 | const char* mqtt_pass = "HA_MQTT_PASSWORD"; // <<<### CHANGE THIS: Password for MQTT User 79 | // --------------------------- 80 | 81 | // --- MQTT Client and Topics (Auto-generated based on MAC address) --- 82 | WiFiClient espClient; 83 | PubSubClient mqttClient(espClient); 84 | String mqtt_device_id = ""; // Will be: "lighttrack_XXXXXX" 85 | String mqtt_topic_base = ""; // Will be: "lighttrack/lighttrack_XXXXXX" 86 | 87 | // Main Light Topics 88 | String ha_state_topic = ""; // Will be: "lighttrack/lighttrack_XXXXXX/state" 89 | String ha_command_topic = ""; // Will be: "lighttrack/lighttrack_XXXXXX/set" 90 | String ha_availability_topic = ""; // Will be: "lighttrack/lighttrack_XXXXXX/availability" 91 | String ha_discovery_topic = ""; // Will be: "homeassistant/light/lighttrack_XXXXXX/config" 92 | 93 | // --- Topics for additional number entities --- 94 | String ha_number_mv_len_state_topic = ""; // Moving Length State 95 | String ha_number_mv_len_cmd_topic = ""; // Moving Length Command 96 | String ha_number_add_leds_state_topic = ""; // Additional LEDs State 97 | String ha_number_add_leds_cmd_topic = ""; // Additional LEDs Command 98 | String ha_number_gradient_state_topic = ""; // Gradient Softness State 99 | String ha_number_gradient_cmd_topic = ""; // Gradient Softness Command 100 | String ha_number_center_shift_state_topic = ""; // Center Shift State 101 | String ha_number_center_shift_cmd_topic = ""; // Center Shift Command 102 | String ha_number_off_delay_state_topic = ""; // LED Off Delay State 103 | String ha_number_off_delay_cmd_topic = ""; // LED Off Delay Command 104 | String ha_number_stat_intens_state_topic = ""; // Stationary Intensity State (0-100.0) 105 | String ha_number_stat_intens_cmd_topic = ""; // Stationary Intensity Command (0-100.0) 106 | 107 | const char* HA_PAYLOAD_ONLINE = "online"; 108 | const char* HA_PAYLOAD_OFFLINE = "offline"; 109 | 110 | // --- Home Assistant Effects --- 111 | const char* HA_EFFECT_SOLID = "Solid"; 112 | const char* HA_EFFECT_BACKGROUND = "Background"; 113 | const char* HA_EFFECT_SCHEDULE = "Schedule"; 114 | const char* HA_EFFECT_STATIONARY = "Stationary"; 115 | const char* effect_list[] = {HA_EFFECT_SOLID, HA_EFFECT_BACKGROUND, HA_EFFECT_SCHEDULE, HA_EFFECT_STATIONARY}; 116 | const int num_effects = sizeof(effect_list) / sizeof(effect_list[0]); 117 | 118 | String current_ha_effect = HA_EFFECT_SOLID; // Current HA effect active 119 | 120 | unsigned long lastMqttReconnectAttempt = 0; 121 | const long mqttReconnectInterval = 5000; // milliseconds 122 | 123 | // Discovery Flags 124 | bool mqtt_discovery_published = false; // For the main light entity 125 | bool mqtt_discovery_numbers_published = false; // For the number entities 126 | 127 | TaskHandle_t mqttTaskHandle = NULL; 128 | 129 | // Forward declaration for functions 130 | void saveSettings(); 131 | void handleSmartHomeClear(bool isHttpRequest); 132 | void updateTime(); 133 | void publishState(); // MQTT publish main light state 134 | void publishParameterStates(); // MQTT publish number parameter states 135 | bool setupWiFi(); // Modified to return bool 136 | void setupOTA(); 137 | void mqttCallback(char* topic, byte* payload, unsigned int length); // MQTT callback 138 | void generateMqttIdAndTopics(); // Generates ID and ALL topics 139 | void publishDiscovery(); // Publishes all discovery messages 140 | void publishNumberDiscovery(const String&, const String&, const String&, const String&, const char*, int, int, float, const char*); // Helper 141 | 142 | // ############################################################################ 143 | // ## END OF MQTT CONFIGURATION ## 144 | // ############################################################################ 145 | // --- MQTT INTEGRATION END --- 146 | 147 | // ------------------------- EEPROM ------------------------- 148 | #define EEPROM_SIZE 132 149 | void loadSettings() { 150 | Serial.println("Loading settings from EEPROM..."); 151 | EEPROM.begin(EEPROM_SIZE); 152 | int offset = 0; 153 | 154 | EEPROM.get(offset, reinterpret_cast(updateInterval)); offset += sizeof(updateInterval); 155 | EEPROM.get(offset, reinterpret_cast(ledOffDelay)); offset += sizeof(ledOffDelay); 156 | EEPROM.get(offset, reinterpret_cast(movingIntensity)); offset += sizeof(movingIntensity); 157 | EEPROM.get(offset, reinterpret_cast(stationaryIntensity)); offset += sizeof(stationaryIntensity); 158 | EEPROM.get(offset, reinterpret_cast(movingLength)); offset += sizeof(movingLength); 159 | EEPROM.get(offset, reinterpret_cast(centerShift)); offset += sizeof(centerShift); 160 | { int temp = additionalLEDs; EEPROM.get(offset, temp); additionalLEDs = temp; offset += sizeof(temp); } 161 | EEPROM.get(offset, baseColor); offset += sizeof(baseColor); // baseColor IS LOADED HERE 162 | offset += sizeof(float); 163 | EEPROM.get(offset, startHour); offset += sizeof(startHour); 164 | EEPROM.get(offset, startMinute); offset += sizeof(startMinute); 165 | EEPROM.get(offset, endHour); offset += sizeof(endHour); 166 | EEPROM.get(offset, endMinute); offset += sizeof(endMinute); 167 | EEPROM.get(offset, gradientSoftness); offset += sizeof(gradientSoftness); 168 | { 169 | int temp_tz = 0; 170 | EEPROM.get(offset, temp_tz); 171 | if (temp_tz >= -720 && temp_tz <= 840) { clientTimezoneOffsetMinutes = temp_tz; isTimeOffsetSet = true; } 172 | else { clientTimezoneOffsetMinutes = 0; isTimeOffsetSet = false; } 173 | offset += sizeof(temp_tz); 174 | } 175 | EEPROM.end(); 176 | 177 | if (isnan(movingIntensity)) { Serial.println("Warning: movingIntensity was NaN, resetting to default."); movingIntensity = 0.3; } 178 | if (isnan(stationaryIntensity)) { Serial.println("Warning: stationaryIntensity was NaN, resetting to default."); stationaryIntensity = 0.03; } 179 | 180 | if (updateInterval < 10 || updateInterval > 200) updateInterval = 20; 181 | if (ledOffDelay < 1 || ledOffDelay > 60) ledOffDelay = 5; 182 | if (movingIntensity < 0.0 || movingIntensity > 1.0) movingIntensity = 0.3; 183 | if (stationaryIntensity < 0.0 || stationaryIntensity > 0.1) stationaryIntensity = 0.03; 184 | if (movingLength < 1 || movingLength > NUM_LEDS) movingLength = 33; 185 | if (abs(centerShift) > NUM_LEDS/2) centerShift = 0; 186 | if (additionalLEDs < 0 || additionalLEDs > NUM_LEDS/2) additionalLEDs = 0; 187 | gradientSoftness = constrain(gradientSoftness, 0, 10); 188 | 189 | bool scheduleInvalid = false; 190 | if (startHour < 0 || startHour > 23 || startMinute < 0 || startMinute > 59 || endHour < 0 || endHour > 23 || endMinute < 0 || endMinute > 59) { 191 | scheduleInvalid = true; 192 | } 193 | if (scheduleInvalid || (startHour == 0 && startMinute == 0 && endHour == 0 && endMinute == 0)){ 194 | Serial.println("Warning: Schedule was invalid or all zeros, resetting to default (20:00-08:30)."); 195 | startHour = 20; startMinute = 0; endHour = 8; endMinute = 30; 196 | } 197 | 198 | Serial.println("Settings loaded and validated:"); 199 | Serial.print("- Update interval: "); Serial.println(updateInterval); 200 | Serial.print("- Moving intensity: "); Serial.print(movingIntensity * 100.0, 0); Serial.println("%"); 201 | Serial.print("- Stationary intensity: "); Serial.print(stationaryIntensity * 100.0, 1); Serial.println("%"); 202 | Serial.print("- Base Color (RGB): "); Serial.print(baseColor.r); Serial.print(", "); Serial.print(baseColor.g); Serial.print(", "); Serial.println(baseColor.b); 203 | Serial.print("- Moving Length: "); Serial.println(movingLength); 204 | // ... (other logs) 205 | } 206 | void saveSettings() { 207 | Serial.println("Saving settings to EEPROM..."); 208 | EEPROM.begin(EEPROM_SIZE); 209 | int offset = 0; 210 | EEPROM.put(offset, updateInterval); offset += sizeof(updateInterval); 211 | EEPROM.put(offset, ledOffDelay); offset += sizeof(ledOffDelay); 212 | EEPROM.put(offset, movingIntensity); offset += sizeof(movingIntensity); 213 | EEPROM.put(offset, stationaryIntensity); offset += sizeof(stationaryIntensity); 214 | EEPROM.put(offset, movingLength); offset += sizeof(movingLength); 215 | EEPROM.put(offset, centerShift); offset += sizeof(centerShift); 216 | { int temp = additionalLEDs; EEPROM.put(offset, temp); offset += sizeof(temp); } 217 | EEPROM.put(offset, baseColor); offset += sizeof(baseColor); // baseColor IS SAVED HERE 218 | offset += sizeof(float); 219 | EEPROM.put(offset, startHour); offset += sizeof(startHour); 220 | EEPROM.put(offset, startMinute); offset += sizeof(startMinute); 221 | EEPROM.put(offset, endHour); offset += sizeof(endHour); 222 | EEPROM.put(offset, endMinute); offset += sizeof(endMinute); 223 | EEPROM.put(offset, gradientSoftness); offset += sizeof(gradientSoftness); 224 | { int temp_tz = clientTimezoneOffsetMinutes; EEPROM.put(offset, temp_tz); offset += sizeof(temp_tz); } 225 | boolean result = EEPROM.commit(); 226 | EEPROM.end(); 227 | Serial.print("Settings saved to EEPROM: "); Serial.println(result ? "OK" : "FAILED"); 228 | } 229 | 230 | // --- MQTT INTEGRATION START --- 231 | void publishAvailability(bool available) { 232 | if (!mqttClient.connected() && available) return; 233 | if (mqttClient.connected() || !available) { 234 | const char* payload = available ? HA_PAYLOAD_ONLINE : HA_PAYLOAD_OFFLINE; 235 | if (ha_availability_topic != "") { 236 | mqttClient.publish(ha_availability_topic.c_str(), payload, true); 237 | } 238 | } 239 | } 240 | 241 | void publishNumberDiscovery( 242 | const String& param_id, const String& name_suffix, const String& state_topic, 243 | const String& cmd_topic, const char* unit, int min_val, int max_val, 244 | float step_val, const char* icon = "mdi:tune" 245 | ) 246 | { 247 | if (!mqttClient.connected() || state_topic == "" || cmd_topic == "") return; 248 | StaticJsonDocument<1024> doc; 249 | String unique_id = mqtt_device_id + "_" + param_id; 250 | String entity_name = "LightTrack " + name_suffix; 251 | String discovery_topic_num = "homeassistant/number/" + unique_id + "/config"; 252 | doc["name"] = entity_name; 253 | doc["unique_id"] = unique_id; 254 | doc["stat_t"] = state_topic; 255 | doc["cmd_t"] = cmd_topic; 256 | doc["avty_t"] = ha_availability_topic; 257 | doc["pl_avail"] = HA_PAYLOAD_ONLINE; 258 | doc["pl_not_avail"] = HA_PAYLOAD_OFFLINE; 259 | doc["min"] = min_val; 260 | doc["max"] = max_val; 261 | doc["step"] = step_val; 262 | if (unit && strlen(unit) > 0) { doc["unit_of_meas"] = unit; } 263 | doc["mode"] = "slider"; 264 | doc["icon"] = icon; 265 | JsonObject device = doc.createNestedObject("device"); 266 | device["identifiers"] = mqtt_device_id; 267 | String discoveryJson; 268 | serializeJson(doc, discoveryJson); 269 | Serial.print("MQTT Pub Number Discovery: "); Serial.println(discovery_topic_num); 270 | if (!mqttClient.publish(discovery_topic_num.c_str(), discoveryJson.c_str(), true)) { 271 | Serial.println(" -> FAILED."); 272 | } 273 | } 274 | 275 | void publishDiscovery() { 276 | if (!mqttClient.connected()) return; 277 | if (!mqtt_discovery_published && ha_discovery_topic != "") { 278 | StaticJsonDocument<1024> doc; 279 | String deviceNameDisplay = "LightTrack " + mqtt_device_id.substring(mqtt_device_id.lastIndexOf('_') + 1); 280 | doc["name"] = deviceNameDisplay; 281 | doc["unique_id"] = mqtt_device_id; 282 | doc["cmd_t"] = ha_command_topic; 283 | doc["stat_t"] = ha_state_topic; 284 | doc["avty_t"] = ha_availability_topic; 285 | doc["pl_avail"] = HA_PAYLOAD_ONLINE; 286 | doc["pl_not_avail"] = HA_PAYLOAD_OFFLINE; 287 | doc["schema"] = "json"; 288 | doc["brightness"] = true; 289 | doc["rgb"] = true; // THIS ENABLES COLOR CONTROL FOR THE LIGHT ENTITY 290 | JsonArray effectList = doc.createNestedArray("effect_list"); 291 | for(int i=0; i < num_effects; i++){ effectList.add(effect_list[i]); } 292 | doc["effect"] = true; 293 | JsonObject device = doc.createNestedObject("device"); 294 | device["identifiers"] = mqtt_device_id; 295 | device["name"] = deviceNameDisplay; 296 | device["manufacturer"] = "DIY Yari & AI"; 297 | device["model"] = "ESP32-C3 LightTrack"; 298 | device["sw_version"] = "2.2-MQTT"; 299 | String discoveryJson; 300 | serializeJson(doc, discoveryJson); 301 | Serial.print("MQTT Pub Light Discovery to "); Serial.println(ha_discovery_topic); 302 | if (mqttClient.publish(ha_discovery_topic.c_str(), discoveryJson.c_str(), true)) { 303 | Serial.println(" -> Light Discovery published."); 304 | mqtt_discovery_published = true; 305 | } else { 306 | Serial.println(" -> Light Discovery FAILED."); 307 | } 308 | } 309 | if (!mqtt_discovery_numbers_published) { 310 | Serial.println("Publishing Number discoveries..."); 311 | publishNumberDiscovery("moving_length", "Moving Length", ha_number_mv_len_state_topic, ha_number_mv_len_cmd_topic, "LEDs", 1, NUM_LEDS, 1.0, "mdi:arrow-split-vertical"); 312 | publishNumberDiscovery("additional_leds", "Trail LEDs", ha_number_add_leds_state_topic, ha_number_add_leds_cmd_topic, "LEDs", 0, NUM_LEDS / 2, 1.0, "mdi:ray-vertex-reverse"); 313 | publishNumberDiscovery("gradient", "Gradient", ha_number_gradient_state_topic, ha_number_gradient_cmd_topic, "", 0, 10, 1.0, "mdi:gradient-vertical"); 314 | publishNumberDiscovery("center_shift", "Center Shift", ha_number_center_shift_state_topic, ha_number_center_shift_cmd_topic, "LEDs", -NUM_LEDS / 2, NUM_LEDS / 2, 1.0, "mdi:arrow-expand-horizontal"); 315 | publishNumberDiscovery("off_delay", "Off Delay", ha_number_off_delay_state_topic, ha_number_off_delay_cmd_topic, "sec", 1, 60, 1.0, "mdi:timer-sand"); 316 | publishNumberDiscovery("stationary_intensity", "Background Intensity", ha_number_stat_intens_state_topic, ha_number_stat_intens_cmd_topic, "%", 0, 100, 0.1, "mdi:brightness-percent"); 317 | mqtt_discovery_numbers_published = true; 318 | Serial.println("Number discoveries published."); 319 | } 320 | } 321 | 322 | void publishParameterStates() { 323 | if (!mqttClient.connected()) return; 324 | char buffer[12]; 325 | if (ha_number_mv_len_state_topic != "") { snprintf(buffer, sizeof(buffer), "%d", movingLength); mqttClient.publish(ha_number_mv_len_state_topic.c_str(), buffer, true); } 326 | if (ha_number_add_leds_state_topic != "") { snprintf(buffer, sizeof(buffer), "%d", additionalLEDs); mqttClient.publish(ha_number_add_leds_state_topic.c_str(), buffer, true); } 327 | if (ha_number_gradient_state_topic != "") { snprintf(buffer, sizeof(buffer), "%d", gradientSoftness); mqttClient.publish(ha_number_gradient_state_topic.c_str(), buffer, true); } 328 | if (ha_number_center_shift_state_topic != "") { snprintf(buffer, sizeof(buffer), "%d", centerShift); mqttClient.publish(ha_number_center_shift_state_topic.c_str(), buffer, true); } 329 | if (ha_number_off_delay_state_topic != "") { snprintf(buffer, sizeof(buffer), "%d", ledOffDelay); mqttClient.publish(ha_number_off_delay_state_topic.c_str(), buffer, true); } 330 | if (ha_number_stat_intens_state_topic != "") { snprintf(buffer, sizeof(buffer), "%.1f", stationaryIntensity * 1000.0); mqttClient.publish(ha_number_stat_intens_state_topic.c_str(), buffer, true); } 331 | } 332 | 333 | void publishState() { 334 | if (!mqttClient.connected() || ha_state_topic == "") return; 335 | StaticJsonDocument<384> doc; 336 | doc["state"] = lightOn ? "ON" : "OFF"; 337 | doc["brightness"] = map(round(movingIntensity * 100), 0, 100, 0, 255); 338 | JsonObject colorObj = doc.createNestedObject("color"); // THIS OBJECT IS FOR COLOR 339 | colorObj["r"] = baseColor.r; 340 | colorObj["g"] = baseColor.g; 341 | colorObj["b"] = baseColor.b; 342 | doc["effect"] = current_ha_effect; 343 | String stateJson; 344 | serializeJson(doc, stateJson); 345 | if(!mqttClient.publish(ha_state_topic.c_str(), stateJson.c_str(), true)) {} 346 | } 347 | 348 | void mqttCallback(char* topic, byte* payload, unsigned int length) { 349 | Serial.print("MQTT Recv ["); Serial.print(topic); Serial.print("] "); 350 | char payloadStr[length + 1]; 351 | memcpy(payloadStr, payload, length); 352 | payloadStr[length] = '\0'; 353 | Serial.println(payloadStr); 354 | String topicStr = String(topic); 355 | bool paramsNeedPublishing = false; 356 | 357 | if (topicStr == ha_command_topic) { 358 | StaticJsonDocument<256> doc; 359 | DeserializationError error = deserializeJson(doc, payload, length); 360 | if (error) { Serial.print("MQTT JSON Err: "); Serial.println(error.f_str()); return; } 361 | bool stateChangedForSave = false; // To track if saveSettings is needed 362 | bool publishNeeded = false; 363 | 364 | if (doc.containsKey("state")) { 365 | String stateValue = doc["state"]; 366 | if (stateValue == "ON") { if (!lightOn) { lightOn = true; stateChangedForSave = true; } smarthomeOverride = true; if (current_ha_effect == HA_EFFECT_SCHEDULE || current_ha_effect == HA_EFFECT_STATIONARY) { current_ha_effect = HA_EFFECT_SOLID; backgroundModeActive = false; }} 367 | else if (stateValue == "OFF") { if (lightOn) { lightOn = false; stateChangedForSave = true; } smarthomeOverride = true; } 368 | if(stateChangedForSave) publishNeeded = true; 369 | } 370 | if (doc.containsKey("brightness")) { 371 | uint8_t brightness_val = doc["brightness"]; 372 | float newIntensity = constrain(map(brightness_val, 0, 255, 0, 100) / 100.0, 0.0, 1.0); 373 | if (abs(movingIntensity - newIntensity) > 0.001) { movingIntensity = newIntensity; /* stateChangedForSave = true; */ } // Brightness change usually doesn't warrant EEPROM save immediately 374 | smarthomeOverride = true; 375 | if (!lightOn && brightness_val > 0) { lightOn = true; if (current_ha_effect == HA_EFFECT_SCHEDULE || current_ha_effect == HA_EFFECT_STATIONARY) { current_ha_effect = HA_EFFECT_SOLID; backgroundModeActive = false; }} 376 | publishNeeded = true; 377 | } 378 | if (doc.containsKey("color")) { // COLOR IS HANDLED HERE 379 | JsonObject colorObj = doc["color"]; 380 | CRGB newColor = CRGB(colorObj["r"], colorObj["g"], colorObj["b"]); 381 | if (baseColor != newColor) { baseColor = newColor; stateChangedForSave = true; } 382 | smarthomeOverride = true; 383 | if (!lightOn) { lightOn = true; if (current_ha_effect == HA_EFFECT_SCHEDULE || current_ha_effect == HA_EFFECT_STATIONARY) { current_ha_effect = HA_EFFECT_SOLID; backgroundModeActive = false; }} 384 | publishNeeded = true; 385 | } 386 | if (doc.containsKey("effect")) { 387 | String effect_str = doc["effect"]; bool effectMatched = false; 388 | for(int i=0; i < num_effects; i++){ if(effect_str == effect_list[i]){ if (current_ha_effect != effect_list[i]) { current_ha_effect = effect_list[i]; stateChangedForSave = true; } effectMatched = true; break; }} 389 | if(effectMatched){ 390 | Serial.print("MQTT Effect set to: "); Serial.println(current_ha_effect); 391 | if (current_ha_effect == HA_EFFECT_SCHEDULE) { smarthomeOverride = false; backgroundModeActive = false; handleSmartHomeClear(false); publishNeeded = false; } 392 | else { smarthomeOverride = true; if (current_ha_effect == HA_EFFECT_SOLID) { backgroundModeActive = false; } else if (current_ha_effect == HA_EFFECT_BACKGROUND) { backgroundModeActive = true; } else if (current_ha_effect == HA_EFFECT_STATIONARY) { backgroundModeActive = true; } if (!lightOn) { lightOn = true; }} 393 | publishNeeded = true; 394 | } else { Serial.print("MQTT Unknown effect: "); Serial.println(effect_str); } 395 | } 396 | if(publishNeeded){ publishState(); } 397 | // if(stateChangedForSave){ saveSettings(); } // Decide if these changes should be saved 398 | } 399 | else if (topicStr == ha_number_mv_len_cmd_topic) { int val = atoi(payloadStr); val = constrain(val, 1, NUM_LEDS); if (movingLength != val) { movingLength = val; paramsNeedPublishing = true; saveSettings(); }} 400 | else if (topicStr == ha_number_add_leds_cmd_topic) { int val = atoi(payloadStr); val = constrain(val, 0, NUM_LEDS / 2); if (additionalLEDs != val) { additionalLEDs = val; paramsNeedPublishing = true; saveSettings(); }} 401 | else if (topicStr == ha_number_gradient_cmd_topic) { int val = atoi(payloadStr); val = constrain(val, 0, 10); if (gradientSoftness != val) { gradientSoftness = val; paramsNeedPublishing = true; saveSettings(); }} 402 | else if (topicStr == ha_number_center_shift_cmd_topic) { int val = atoi(payloadStr); val = constrain(val, -NUM_LEDS / 2, NUM_LEDS / 2); if (centerShift != val) { centerShift = val; paramsNeedPublishing = true; saveSettings(); }} 403 | else if (topicStr == ha_number_off_delay_cmd_topic) { int val = atoi(payloadStr); val = constrain(val, 1, 60); if (ledOffDelay != val) { ledOffDelay = val; paramsNeedPublishing = true; saveSettings(); }} 404 | else if (topicStr == ha_number_stat_intens_cmd_topic) { float val_percent = atof(payloadStr); float newIntensity = constrain(val_percent / 1000.0, 0.0, 0.1); if (abs(stationaryIntensity - newIntensity) > 0.0001) { stationaryIntensity = newIntensity; paramsNeedPublishing = true; saveSettings(); }} 405 | if (paramsNeedPublishing) { publishParameterStates(); } 406 | } 407 | 408 | void generateMqttIdAndTopics() { 409 | uint8_t mac[6]; WiFi.macAddress(mac); mqtt_device_id = "lighttrack_"; char mac_part[3]; 410 | for (int i = 3; i < 6; i++){ sprintf(mac_part, "%02X", mac[i]); mqtt_device_id += mac_part; } 411 | Serial.print("MQTT Device ID generated: "); Serial.println(mqtt_device_id); 412 | mqtt_topic_base = "lighttrack/" + mqtt_device_id; 413 | ha_availability_topic = mqtt_topic_base + "/availability"; 414 | ha_state_topic = mqtt_topic_base + "/state"; 415 | ha_command_topic = mqtt_topic_base + "/set"; 416 | ha_discovery_topic = "homeassistant/light/" + mqtt_device_id + "/config"; 417 | ha_number_mv_len_state_topic = mqtt_topic_base + "/moving_length/state"; ha_number_mv_len_cmd_topic = mqtt_topic_base + "/moving_length/set"; 418 | ha_number_add_leds_state_topic = mqtt_topic_base + "/additional_leds/state"; ha_number_add_leds_cmd_topic = mqtt_topic_base + "/additional_leds/set"; 419 | ha_number_gradient_state_topic = mqtt_topic_base + "/gradient/state"; ha_number_gradient_cmd_topic = mqtt_topic_base + "/gradient/set"; 420 | ha_number_center_shift_state_topic = mqtt_topic_base + "/center_shift/state"; ha_number_center_shift_cmd_topic = mqtt_topic_base + "/center_shift/set"; 421 | ha_number_off_delay_state_topic = mqtt_topic_base + "/off_delay/state"; ha_number_off_delay_cmd_topic = mqtt_topic_base + "/off_delay/set"; 422 | ha_number_stat_intens_state_topic = mqtt_topic_base + "/stationary_intensity/state"; ha_number_stat_intens_cmd_topic = mqtt_topic_base + "/stationary_intensity/set"; 423 | if (String(WiFi.getHostname()) != mqtt_device_id && !String(WiFi.getHostname()).startsWith("LightTrack-OTA-")) { if (WiFi.setHostname(mqtt_device_id.c_str())) { Serial.print("WiFi Hostname set to: "); Serial.println(mqtt_device_id); } else { Serial.println("Failed to set WiFi Hostname."); }} 424 | mqttClient.setServer(mqtt_server, mqtt_port); mqttClient.setCallback(mqttCallback); mqttClient.setBufferSize(1024); 425 | } 426 | 427 | void reconnectMqtt() { 428 | if (WiFi.status() != WL_CONNECTED) { lastMqttReconnectAttempt = millis(); return; } 429 | if (mqtt_device_id == "") { Serial.println("MQTT: mqtt_device_id is empty, generating..."); generateMqttIdAndTopics(); if (mqtt_device_id == "") { Serial.println("MQTT: CRITICAL - Failed to generate ID."); return; } } 430 | if (!mqttClient.connected()) { 431 | unsigned long now = millis(); 432 | if (now - lastMqttReconnectAttempt > mqttReconnectInterval) { 433 | lastMqttReconnectAttempt = now; 434 | Serial.print("MQTT Attempting connection to "); Serial.print(mqtt_server); Serial.print(" (Client ID: "); Serial.print(mqtt_device_id); Serial.println(")..."); 435 | if (mqttClient.connect(mqtt_device_id.c_str(), mqtt_user, mqtt_pass, ha_availability_topic.c_str(), 0, true, HA_PAYLOAD_OFFLINE)) { 436 | Serial.println("MQTT connected!"); publishAvailability(true); 437 | mqttClient.subscribe(ha_command_topic.c_str()); mqttClient.subscribe(ha_number_mv_len_cmd_topic.c_str()); mqttClient.subscribe(ha_number_add_leds_cmd_topic.c_str()); 438 | mqttClient.subscribe(ha_number_gradient_cmd_topic.c_str()); mqttClient.subscribe(ha_number_center_shift_cmd_topic.c_str()); mqttClient.subscribe(ha_number_off_delay_cmd_topic.c_str()); 439 | mqttClient.subscribe(ha_number_stat_intens_cmd_topic.c_str()); Serial.println("MQTT Subscribed to command topics."); 440 | mqtt_discovery_published = false; mqtt_discovery_numbers_published = false; publishDiscovery(); 441 | publishState(); publishParameterStates(); 442 | } else { Serial.print("MQTT connect failed, rc="); Serial.print(mqttClient.state()); Serial.println(" Retrying..."); } 443 | } 444 | } 445 | } 446 | 447 | void mqttTask(void * parameter) { 448 | Serial.println("MQTT Task started."); 449 | for (;;) { 450 | if (WiFi.status() == WL_CONNECTED) { 451 | if (mqtt_device_id == "" || ha_command_topic == "") { Serial.println("MQTT Task: WiFi connected, generating ID/topics..."); generateMqttIdAndTopics(); if(mqtt_device_id == "" || ha_command_topic == "") { Serial.println("MQTT Task: ID/topic generation failed. Will retry."); vTaskDelay(pdMS_TO_TICKS(5000)); continue; }} 452 | if (!mqttClient.connected()) { reconnectMqtt(); } mqttClient.loop(); 453 | } else { vTaskDelay(pdMS_TO_TICKS(1000)); } 454 | vTaskDelay(pdMS_TO_TICKS(50)); 455 | } 456 | } 457 | // --- MQTT INTEGRATION END --- 458 | 459 | // ------------------------- Web Server ------------------------- 460 | WebServer server(80); 461 | void handleRoot(); void handleSetInterval(); void handleSetLedOffDelay(); void handleSetBaseColor(); 462 | void handleSetMovingIntensity(); void handleSetStationaryIntensity(); void handleSetMovingLength(); 463 | void handleSetAdditionalLEDs(); void handleSetCenterShift(); void handleSetGradientSoftness(); 464 | void handleSetTime(); void handleSetSchedule(); void handleNotFound(); void handleSmartHomeOn(); 465 | void handleSmartHomeOff(); void handleToggleBackgroundMode(); void handleGetCurrentTime(); 466 | 467 | void handleSmartHomeOn() { lightOn = true; smarthomeOverride = true; current_ha_effect = HA_EFFECT_SOLID; backgroundModeActive = false; server.send(200, "text/plain", "Smart Home Override: ON"); publishState(); } 468 | void handleSmartHomeOff() { lightOn = false; smarthomeOverride = true; server.send(200, "text/plain", "Smart Home Override: OFF"); publishState(); } 469 | void handleSmartHomeClear(bool isHttpRequest = true) { smarthomeOverride = false; current_ha_effect = HA_EFFECT_SCHEDULE; backgroundModeActive = false; Serial.println("Smart Home Override Cleared. Switched to HA_EFFECT_SCHEDULE."); updateTime(); if (isHttpRequest) { server.send(200, "text/plain", "Smart Home Override: CLEARED. Schedule active."); }} 470 | 471 | void handleToggleBackgroundMode() { 472 | // This web button now primarily toggles between HA_EFFECT_SOLID and HA_EFFECT_BACKGROUND 473 | // It assumes 'smarthomeOverride = true' because it's a manual web action. 474 | smarthomeOverride = true; 475 | if (current_ha_effect == HA_EFFECT_BACKGROUND || current_ha_effect == HA_EFFECT_STATIONARY) { 476 | current_ha_effect = HA_EFFECT_SOLID; 477 | backgroundModeActive = false; // Explicitly turn off for Solid 478 | } else { 479 | current_ha_effect = HA_EFFECT_BACKGROUND; 480 | backgroundModeActive = true; // Explicitly turn on for Background 481 | if (!lightOn) lightOn = true; // Turn on if activating background 482 | } 483 | Serial.print("Web Toggle Background: New HA Effect: "); Serial.println(current_ha_effect); 484 | server.sendHeader("Location", "/"); server.send(303); 485 | publishState(); 486 | } 487 | 488 | unsigned int readSensorData() { 489 | #ifndef SIMULATE_SENSOR 490 | if (Serial1.available() < 7) return g_sensorDistance; if (Serial1.read() != SENSOR_HEADER) { while (Serial1.available()) Serial1.read(); return g_sensorDistance; } if (Serial1.read() != SENSOR_HEADER) { while (Serial1.available()) Serial1.read(); return g_sensorDistance; } byte buf[5]; size_t bytesRead = Serial1.readBytes(buf, 5); if (bytesRead < 5) { while (Serial1.available()) Serial1.read(); return g_sensorDistance; } unsigned int distance = (buf[2] << 8) | buf[1]; if (distance < MIN_DISTANCE || distance > MAX_DISTANCE) return g_sensorDistance; return distance; 491 | #else 492 | static unsigned int simulatedDistance = MIN_DISTANCE; static int simDir = 10; simulatedDistance += simDir; if (simulatedDistance >= MAX_DISTANCE - 50) simDir = -10; if (simulatedDistance <= MIN_DISTANCE + 50) simDir = 10; return simulatedDistance; 493 | #endif 494 | } 495 | 496 | void sensorTask(void * parameter) { Serial.println("Sensor Task started"); for (;;) { unsigned int newDistance = readSensorData(); g_sensorDistance = newDistance; vTaskDelay(pdMS_TO_TICKS(5));}} 497 | void ledTask(void * parameter) { 498 | static unsigned int lastSensor = g_sensorDistance; static int lastMovementDirection = 0; static unsigned long lastMovementTime = millis(); 499 | FastLED.clear(); FastLED.show(); vTaskDelay(pdMS_TO_TICKS(1000)); Serial.println("LED Task initialized and starting main loop"); 500 | for (;;) { 501 | unsigned long currentMillis = millis(); unsigned int currentDistance = g_sensorDistance; bool isLightActive = lightOn; 502 | bool actualBackgroundOn = (current_ha_effect == HA_EFFECT_BACKGROUND) || (current_ha_effect == HA_EFFECT_STATIONARY); 503 | if (!isLightActive) { fill_solid(leds, NUM_LEDS, CRGB::Black); } 504 | else if (actualBackgroundOn) { uint8_t r = max((uint8_t)1, (uint8_t)(baseColor.r * stationaryIntensity)); uint8_t g = max((uint8_t)1, (uint8_t)(baseColor.g * stationaryIntensity)); uint8_t b = max((uint8_t)1, (uint8_t)(baseColor.b * stationaryIntensity)); fill_solid(leds, NUM_LEDS, CRGB(r, g, b)); } 505 | else { fill_solid(leds, NUM_LEDS, CRGB::Black); } 506 | if (isLightActive && current_ha_effect != HA_EFFECT_STATIONARY) { 507 | int diff = (int)currentDistance - (int)lastSensor; int absDiff = abs(diff); 508 | if (absDiff >= NOISE_THRESHOLD) { if (currentMillis - lastMovementTime > 50 || (diff > 0 && lastMovementDirection < 0) || (diff < 0 && lastMovementDirection > 0)) { lastMovementTime = currentMillis; lastMovementDirection = (diff > 0) ? 1 : -1; }} 509 | lastSensor = currentDistance; bool drawMovingPart = (currentMillis - lastMovementTime <= (unsigned long)ledOffDelay * 1000); 510 | if (drawMovingPart) { 511 | float prop = constrain((float)(currentDistance - MIN_DISTANCE) / (MAX_DISTANCE - MIN_DISTANCE), 0.0, 1.0); int ledPosition = round(prop * (NUM_LEDS - 1)); int centerLED = constrain(ledPosition + centerShift, 0, NUM_LEDS - 1); 512 | CRGB fullBrightColor = CRGB((uint8_t)(baseColor.r * movingIntensity), (uint8_t)(baseColor.g * movingIntensity), (uint8_t)(baseColor.b * movingIntensity)); 513 | int direction = lastMovementDirection; if (direction == 0) direction = 1; int halfMainLength = movingLength / 2; int totalLightLength = movingLength + additionalLEDs; if (totalLightLength <= 0) totalLightLength = 1; 514 | int leftEdge, rightEdge; if (direction > 0) { leftEdge = centerLED - halfMainLength; rightEdge = leftEdge + movingLength - 1 + additionalLEDs; } else { rightEdge = centerLED + halfMainLength; leftEdge = rightEdge - movingLength + 1 - additionalLEDs; } 515 | leftEdge = max(0, leftEdge); rightEdge = min(NUM_LEDS - 1, rightEdge); int effectiveFadeWidth = map(gradientSoftness, 0, 10, 1, 10); float effectiveFadeExponent = 1.0 + (gradientSoftness / 10.0) * 2.0; 516 | int actualBeamPixelLength = rightEdge - leftEdge + 1; effectiveFadeWidth = constrain(effectiveFadeWidth, 1, max(1, actualBeamPixelLength / 2)); 517 | for (int i = leftEdge; i <= rightEdge; i++) { 518 | int posInBeam; if (direction > 0) { posInBeam = i - leftEdge; } else { posInBeam = rightEdge - i; } float factor = 1.0f; 519 | if (gradientSoftness > 0 && totalLightLength > 1) { if (posInBeam < effectiveFadeWidth) { float normalizedPos = (effectiveFadeWidth > 0) ? (float)(posInBeam + 1) / (effectiveFadeWidth + 1) : 1.0f; factor = pow(normalizedPos, effectiveFadeExponent); } else if (posInBeam >= (totalLightLength - effectiveFadeWidth)) { int posFromEnd = totalLightLength - 1 - posInBeam; float normalizedPos = (effectiveFadeWidth > 0) ? (float)(posFromEnd + 1) / (effectiveFadeWidth + 1) : 1.0f; factor = pow(normalizedPos, effectiveFadeExponent); }} 520 | factor = constrain(factor, 0.0f, 1.0f); 521 | if (factor > 0.01f) { CRGB beamColor; beamColor.r = (uint8_t)(fullBrightColor.r * factor); beamColor.g = (uint8_t)(fullBrightColor.g * factor); beamColor.b = (uint8_t)(fullBrightColor.b * factor); leds[i].r = max(beamColor.r, leds[i].r); leds[i].g = max(beamColor.g, leds[i].g); leds[i].b = max(beamColor.b, leds[i].b); } 522 | } 523 | } 524 | } 525 | FastLED.show(); vTaskDelay(pdMS_TO_TICKS(updateInterval)); 526 | } 527 | } 528 | void webServerTask(void * parameter) { Serial.println("Web Server Task started"); for (;;) { if (WiFi.status() == WL_CONNECTED || WiFi.softAPgetStationNum() > 0) { server.handleClient(); } ArduinoOTA.handle(); vTaskDelay(pdMS_TO_TICKS(2)); }} 529 | 530 | void handleSetTime() { bool tu = false; bool tzu = false; if (server.hasArg("tz")) { int tzo = server.arg("tz").toInt(); if (tzo >= -720 && tzo <= 840) { if (!isTimeOffsetSet || clientTimezoneOffsetMinutes != tzo) { clientTimezoneOffsetMinutes = tzo; isTimeOffsetSet = true; tzu = true; saveSettings(); }} else { Serial.print("Invalid TZ: "); Serial.println(tzo); }} if (server.hasArg("epoch")) { unsigned long e = strtoul(server.arg("epoch").c_str(), NULL, 10); if (e > 946684800UL) { struct timeval tv; tv.tv_sec = e; tv.tv_usec = 0; settimeofday(&tv, NULL); tu = true; } else { Serial.print("Invalid epoch: "); Serial.println(e); }} if (tu || tzu) { updateTime(); } String r = ""; if(tu) r += "Time OK"; if(tzu) r += (r.length() > 0 ? ", TZ OK" : "TZ OK"); if(r.length() == 0) r = "No change"; server.send(200, "text/plain", r); } 531 | void updateTime() { time_t nu = time(nullptr); static bool ltv = false; bool ctv = (nu > 1000000000UL); if (ctv && !ltv) { Serial.println("NTP OK!"); } ltv = ctv; if (!ctv || !isTimeOffsetSet) { return; } time_t cle = nu + (clientTimezoneOffsetMinutes * 60); struct tm til; gmtime_r(&cle, &til); int ctm = til.tm_hour * 60 + til.tm_min; int stm = startHour * 60 + startMinute; int etm = endHour * 60 + endMinute; bool sbo; if (stm <= etm) { sbo = (ctm >= stm && ctm < etm); } else { sbo = (ctm >= stm || ctm < etm); } bool sac = false; if (!smarthomeOverride && (lightOn != sbo)) { lightOn = sbo; sac = true; Serial.print("Schedule set light: "); Serial.println(lightOn ? "ON" : "OFF"); } static unsigned long lssp = 0; if (sac || (mqttClient.connected() && (millis() - lssp > 30000))) { if (current_ha_effect == HA_EFFECT_SCHEDULE) { publishState(); lssp = millis(); }}} 532 | void handleGetCurrentTime() { char ts[20] = "N/A"; time_t nu = time(nullptr); if (nu > 1000000000UL && isTimeOffsetSet) { time_t cle = nu + (clientTimezoneOffsetMinutes * 60); struct tm tic; gmtime_r(&cle, &tic); strftime(ts, sizeof(ts), "%H:%M:%S", &tic); } else if (nu > 1000000000UL && !isTimeOffsetSet) { strcpy(ts, "TZ Not Set"); } else { strcpy(ts, "Sync Pend"); } String j = "{\"time\":\""; j += ts; j += "\"}"; server.send(200, "application/json", j); } 533 | void handleSetSchedule() { if (server.hasArg("startHour") && server.hasArg("startMinute") && server.hasArg("endHour") && server.hasArg("endMinute")) { startHour = server.arg("startHour").toInt(); startMinute = server.arg("startMinute").toInt(); endHour = server.arg("endHour").toInt(); endMinute = server.arg("endMinute").toInt(); startHour = constrain(startHour,0,23); startMinute = constrain(startMinute,0,59); endHour = constrain(endHour,0,23); endMinute = constrain(endMinute,0,59); saveSettings(); updateTime(); } server.sendHeader("Location", "/"); server.send(303); } 534 | void handleNotFound() { server.send(404, "text/plain", "Not Found"); } 535 | 536 | bool setupWiFi() { WiFi.mode(WIFI_STA); Serial.print("Connecting to WiFi: '"); Serial.print(main_wifi_ssid); Serial.println("'..."); WiFi.begin(main_wifi_ssid, main_wifi_password); unsigned long wst = millis(); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); if (millis() - wst > 20000) { Serial.println("\nWiFi Connection FAILED!"); return false; }} Serial.println("\nWiFi connected!"); Serial.print("IP: "); Serial.println(WiFi.localIP()); generateMqttIdAndTopics(); server.on("/", HTTP_GET, handleRoot); server.on("/setInterval", HTTP_GET, handleSetInterval); server.on("/setLedOffDelay", HTTP_GET, handleSetLedOffDelay); server.on("/setBaseColor", HTTP_GET, handleSetBaseColor); server.on("/setMovingIntensity", HTTP_GET, handleSetMovingIntensity); server.on("/setStationaryIntensity", HTTP_GET, handleSetStationaryIntensity); server.on("/setMovingLength", HTTP_GET, handleSetMovingLength); server.on("/setAdditionalLEDs", HTTP_GET, handleSetAdditionalLEDs); server.on("/setCenterShift", HTTP_GET, handleSetCenterShift); server.on("/setGradientSoftness", HTTP_GET, handleSetGradientSoftness); server.on("/setTime", HTTP_GET, handleSetTime); server.on("/setSchedule", HTTP_GET, handleSetSchedule); server.on("/smarthome/on", HTTP_GET, handleSmartHomeOn); server.on("/smarthome/off", HTTP_GET, handleSmartHomeOff); server.on("/smarthome/clear", HTTP_GET, [](){ handleSmartHomeClear(true); }); server.on("/toggleNightMode", HTTP_GET, handleToggleBackgroundMode); server.on("/getCurrentTime", HTTP_GET, handleGetCurrentTime); server.onNotFound(handleNotFound); server.begin(); Serial.println("Web server started."); return true; } 537 | 538 | void handleRoot() { char sss[6]; sprintf(sss, "%02d:%02d", startHour, startMinute); char ses[6]; sprintf(ses, "%02d:%02d", endHour, endMinute); int mip = round(movingIntensity * 100.0); float sip = stationaryIntensity * 1000.0; bool wbbo = (current_ha_effect == HA_EFFECT_BACKGROUND) || (current_ha_effect == HA_EFFECT_STATIONARY); String h = ""; h += "LED Control

LightTrack Control

Moving Light

Intensity: "; h += String(mip); h += "%

Length: "; h += String(movingLength); h += "

Trail LEDs: "; h += String(additionalLEDs); h += "

Gradient: "; h += String(gradientSoftness); h += "

Center Shift: "; h += String(centerShift); h += "

Off Delay: "; h += String(ledOffDelay); h += "s


Background Light

Intensity: "; h += String(sip, 1); h += "%


Schedule (Local Time)

to
Est. Local: Loading...

Device Control (HA)

"; server.send(200, "text/html", h); } 539 | 540 | void handleSetInterval() { if (server.hasArg("value")) { updateInterval = server.arg("value").toInt(); if(updateInterval < 10) updateInterval = 10; saveSettings(); } server.send(200, "text/plain", "OK"); } 541 | void handleSetLedOffDelay() { if (server.hasArg("value")) { ledOffDelay = server.arg("value").toInt(); ledOffDelay = constrain(ledOffDelay, 1, 60); saveSettings(); publishParameterStates(); } server.send(200, "text/plain", "OK"); } 542 | void handleSetBaseColor() { if (server.hasArg("r") && server.hasArg("g") && server.hasArg("b")) { baseColor = CRGB(server.arg("r").toInt(), server.arg("g").toInt(), server.arg("b").toInt()); saveSettings(); publishState(); } server.send(200, "text/plain", "OK"); } // baseColor is handled by publishState 543 | void handleSetMovingLength() { if (server.hasArg("value")) { movingLength = server.arg("value").toInt(); movingLength = constrain(movingLength, 1, NUM_LEDS); saveSettings(); publishParameterStates(); } server.send(200, "text/plain", "OK"); } 544 | void handleSetAdditionalLEDs() { if (server.hasArg("value")) { additionalLEDs = server.arg("value").toInt(); additionalLEDs = constrain(additionalLEDs, 0, NUM_LEDS / 2); saveSettings(); publishParameterStates(); } server.send(200, "text/plain", "OK"); } 545 | void handleSetCenterShift() { if (server.hasArg("value")) { centerShift = server.arg("value").toInt(); centerShift = constrain(centerShift, -NUM_LEDS / 2, NUM_LEDS / 2); saveSettings(); publishParameterStates(); } server.send(200, "text/plain", "OK"); } 546 | void handleSetMovingIntensity() { if (server.hasArg("value")) { float vp = server.arg("value").toFloat(); movingIntensity = constrain(vp / 100.0, 0.0, 1.0); saveSettings(); publishState(); } server.send(200, "text/plain", "OK"); } // movingIntensity (brightness) is handled by publishState 547 | void handleSetStationaryIntensity() { if (server.hasArg("value")) { float vp = server.arg("value").toFloat(); stationaryIntensity = constrain(vp / 1000.0, 0.0, 0.1); saveSettings(); publishParameterStates(); } server.send(200, "text/plain", "OK"); } 548 | void handleSetGradientSoftness() { if (server.hasArg("value")) { gradientSoftness = server.arg("value").toInt(); gradientSoftness = constrain(gradientSoftness, 0, 10); saveSettings(); publishParameterStates(); } server.send(200, "text/plain", "OK"); } 549 | 550 | void setupOTA() { String ho = "LightTrack-OTA-Unknown"; if (mqtt_device_id != "") { ho = mqtt_device_id; } else { uint8_t mo[6]; if (esp_wifi_get_mac(WIFI_IF_STA, mo) == ESP_OK) { char ms[7]; sprintf(ms, "%02X%02X%02X", mo[3], mo[4], mo[5]); ho = "LightTrack-OTA-" + String(ms); } else { uint64_t cf = ESP.getEfuseMac(); uint32_t cp = (uint32_t)(cf >> 24); ho = "LightTrack-OTA-" + String(cp, HEX); } Serial.print("OTA fallback hostname: "); Serial.println(ho); } ArduinoOTA.setHostname(ho.c_str()); ArduinoOTA.onStart([]() { String t = ArduinoOTA.getCommand() == U_FLASH ? "sketch" : "filesystem"; Serial.println("OTA Start: " + t); if (mqttClient.connected()) { publishAvailability(false); } }); ArduinoOTA.onEnd([]() { Serial.println("\nOTA End"); }); ArduinoOTA.onProgress([](unsigned int p, unsigned int t) { Serial.printf("OTA Progress: %u%%\r", (p / (t / 100))); }); ArduinoOTA.onError([](ota_error_t e) { Serial.printf("OTA Error[%u]: ", e); if (e == OTA_AUTH_ERROR) Serial.println("Auth Failed"); else if (e == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); else if (e == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); else if (e == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); else if (e == OTA_END_ERROR) Serial.println("End Failed"); }); ArduinoOTA.begin(); Serial.print("OTA Initialized. Hostname: "); Serial.println(ArduinoOTA.getHostname()); } 551 | 552 | void setup() { 553 | Serial.begin(115200); delay(500); Serial.println("\n\n--- LightTrack MQTT v2.2 ---"); 554 | uint64_t cid = ESP.getEfuseMac(); randomSeed((unsigned long)cid ^ (unsigned long)(cid >> 32)); 555 | Serial.println("LEDs Init..."); FastLED.addLeds(leds, NUM_LEDS).setCorrection(TypicalLEDStrip); FastLED.setBrightness(255); FastLED.clear(); leds[0] = CRGB::Red; FastLED.show(); 556 | Serial.println("SPIFFS/EEPROM Init..."); if (!SPIFFS.begin(true)) { Serial.println("SPIFFS Mount Fail. Formatting..."); if (!SPIFFS.format()) { Serial.println("SPIFFS Format FAILED."); } else { Serial.println("SPIFFS Formatted. REBOOTING."); delay(3000); ESP.restart(); }} loadSettings(); 557 | Serial.println("Sensor Init..."); Serial1.begin(256000, SERIAL_8N1, 20, 21); 558 | leds[0] = CRGB::Yellow; FastLED.show(); bool wcis = setupWiFi(); 559 | Serial.println("NTP Init..."); configTzTime("UTC0", "pool.ntp.org", "time.nist.gov"); 560 | Serial.println("OTA Init..."); setupOTA(); 561 | if (wcis) { leds[0] = CRGB::Green; FastLED.show(); } else { leds[0] = CRGB::Blue; FastLED.show(); Serial.println("Initial WiFi FAILED.");} 562 | Serial.println("MQTT Task Create..."); xTaskCreatePinnedToCore(mqttTask, "MQTTTask", 4096, NULL, 1, &mqttTaskHandle, 0); 563 | delay(1000); leds[0] = CRGB::Black; FastLED.show(); 564 | Serial.println("RTOS Tasks Create (Sensor, LED, Web)..."); 565 | xTaskCreatePinnedToCore(sensorTask, "Sensor", 2048, NULL, 2, NULL, 1); 566 | xTaskCreatePinnedToCore(ledTask, "LED", 8192, NULL, 1, NULL, 1); 567 | xTaskCreatePinnedToCore(webServerTask, "Web", 4096, NULL, 1, NULL, 0); 568 | Serial.println("--- Setup Complete ---"); 569 | if (WiFi.status() == WL_CONNECTED) { Serial.print("Web UI: http://"); Serial.println(WiFi.localIP()); } else { Serial.println("WiFi not connected. Web UI unavailable."); } 570 | Serial.println("----------------------"); 571 | } 572 | 573 | void loop() { 574 | updateTime(); 575 | static unsigned long ll = 0; static IPAddress ldip = IPAddress(0,0,0,0); 576 | if (millis() - ll > 15000) { 577 | ll = millis(); Serial.println("--- Status ---"); Serial.print("Uptime: "); Serial.print(millis()/1000); Serial.println("s"); 578 | wl_status_t ws = WiFi.status(); Serial.print("WiFi: "); if(ws == WL_CONNECTED) { Serial.print("OK IP:"); Serial.println(WiFi.localIP()); if(WiFi.localIP()!=ldip && WiFi.localIP()!=IPAddress(0,0,0,0)){ ldip=WiFi.localIP(); Serial.print("Web: http://"); Serial.println(ldip);}} else { Serial.print("Fail("); Serial.print(ws); Serial.println(")");} 579 | Serial.print("MQTT: "); Serial.println(mqttClient.connected() ? "OK" : "Fail"); 580 | time_t n = time(nullptr); if (n < 1000000000UL) { Serial.println("Time: NTP Sync Pend"); } else { struct tm ti; gmtime_r(&n, &ti); char ub[25]; strftime(ub, sizeof(ub), "%F %T", &ti); Serial.print("UTC: "); Serial.println(ub); if (isTimeOffsetSet) { time_t cle = n+(clientTimezoneOffsetMinutes*60); struct tm tic; gmtime_r(&cle, &tic); char lb[20]; strftime(lb, sizeof(lb), "%T", &tic); Serial.print("Local: "); Serial.print(lb); Serial.print(" (Off:"); Serial.print(clientTimezoneOffsetMinutes); Serial.println("m)");} else {Serial.println("Local: TZ Not Set");}} 581 | Serial.printf("Sched: %02d:%02d-%02d:%02d (L) Light:%s HA-Eff:%s Ovrd:%s\n", startHour,startMinute,endHour,endMinute, lightOn?"ON":"OFF", current_ha_effect.c_str(), smarthomeOverride?"Y":"N"); 582 | Serial.printf("MovInt:%.0f%% StatInt:%.1f%% MovLen:%d Trail:%d Grad:%d Shift:%d OffDel:%ds\n", movingIntensity*100.0, stationaryIntensity*100.0, movingLength, additionalLEDs, gradientSoftness, centerShift, ledOffDelay); // Note: stationaryIntensity for log is 0-10%, MQTT is 0-100 for HA 583 | Serial.print("Heap: "); Serial.println(ESP.getFreeHeap()); Serial.println("--------------"); 584 | } 585 | vTaskDelay(pdMS_TO_TICKS(1000)); 586 | } --------------------------------------------------------------------------------