Skip to content

Commit edb8dd8

Browse files
committed
refactor: update and fix OTA updates
Add documentation
1 parent 7f870f3 commit edb8dd8

5 files changed

Lines changed: 202 additions & 33 deletions

File tree

README.md

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ Turn your OBEGRÄNSAD LED Wall Lamp into a live drawing canvas
1919
- [Software Setup](#software-setup)
2020
- [ESP32 Setup with VS Code and PlatformIO](#esp32-setup-with-vs-code-and-platformio)
2121
- [WiFi Configuration](#wifi-configuration)
22+
- [OTA Updates](#ota-updates)
23+
- [Configuration](#configuration)
24+
- [Upload Methods](#upload-methods)
25+
- [Visual Feedback](#visual-feedback)
2226
- [HTTP API Reference](#http-api-reference)
2327
- [Device Information](#device-information)
2428
- [Plugin Control](#plugin-control)
@@ -180,6 +184,80 @@ For ESP8266, WiFi Manager is not available. Set `WIFI_SSID` and `WIFI_PASSWORD`
180184

181185
---
182186

187+
## OTA Updates
188+
189+
Over-The-Air (OTA) updates allow you to upload new firmware wirelessly without a USB connection. This is powered by [ElegantOTA](https://github.com/ayushsharma82/ElegantOTA).
190+
191+
### Configuration
192+
193+
Before using OTA, configure the following:
194+
195+
1. **Set OTA Credentials** in `include/secrets.h`:
196+
```cpp
197+
#define OTA_USERNAME "admin"
198+
#define OTA_PASSWORD "your-password"
199+
```
200+
201+
2. **Configure Upload Settings** in `platformio.ini` (for the `esp32dev` environment):
202+
```ini
203+
extra_scripts = upload.py
204+
upload_protocol = custom
205+
custom_upload_url = http://192.168.178.50 # Your device IP
206+
custom_username = admin
207+
custom_password = your-password
208+
```
209+
210+
**Note:** Replace `192.168.178.50` with your device's actual IP address.
211+
212+
### Upload Methods
213+
214+
#### Method 1: Web Interface (Manual Upload)
215+
216+
1. Navigate to `http://your-device-ip/update` in your browser
217+
2. Login with your configured credentials (default: `admin` / `ikea-led-wall`)
218+
3. Select your firmware file (`.pio/build/esp32dev/firmware.bin`)
219+
4. Click "Update" and wait for completion
220+
5. Device will automatically reboot with new firmware
221+
222+
#### Method 2: PlatformIO (Automated Upload)
223+
224+
Upload directly from PlatformIO via the command line:
225+
226+
```bash
227+
pio run -e esp32dev -t upload
228+
```
229+
230+
Or use the PlatformIO Upload button in VS Code (bottom toolbar).
231+
232+
**Requirements:**
233+
- Python packages: `requests_toolbelt` and `tqdm`
234+
- Install if needed: `pip install requests_toolbelt tqdm`
235+
236+
### Visual Feedback
237+
238+
During OTA updates, the LED matrix provides visual feedback:
239+
240+
- **"U" letter displayed**: Update has started
241+
- **Serial output**: Progress updates every second
242+
- **"R" letter displayed**: Update completed (device will reboot)
243+
244+
Monitor the serial output for detailed progress:
245+
```
246+
OTA update started!
247+
OTA Progress Current: 262144 bytes, Final: 1440655 bytes
248+
OTA Progress Current: 524288 bytes, Final: 1440655 bytes
249+
...
250+
OTA update finished successfully!
251+
```
252+
253+
**Troubleshooting:**
254+
- Ensure device is connected to the same network
255+
- Verify IP address in `platformio.ini` matches device IP
256+
- Check credentials match in both `secrets.h` and `platformio.ini`
257+
- For upload failures, try the web interface method first
258+
259+
---
260+
183261
## HTTP API Reference
184262
185263
Base URL: `http://your-server/api`
@@ -524,7 +602,7 @@ Create a second automation or condition to call `rest_command.obegraensad_bright
524602
**Structure:**
525603
- `src/` - Arduino code
526604
- `platformio.ini` - Build configuration
527-
- Uncomment OTA lines for over-the-air updates (replace IP with your device)
605+
- See [OTA Updates](#ota-updates) section for wireless firmware upload configuration
528606

529607
### Frontend Development
530608

include/secrets.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@
1010
#endif
1111

1212
// If you would like to perform OTA updates, you need to define the credentials here
13-
#define OTA_USERNAME ""
14-
#define OTA_PASSWORD ""
13+
#define OTA_USERNAME "admin"
14+
#define OTA_PASSWORD "ikea-led-wall"

platformio.ini

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
[platformio]
1212
default_envs = esp32dev
13+
lib_compat_mode = strict
1314

1415
[env]
1516
framework = arduino
@@ -44,8 +45,10 @@ monitor_filters = esp32_exception_decoder
4445
; -DARDUINO_USB_MODE=1
4546
; extra_scripts = upload.py
4647
; upload_protocol = custom
47-
; upload_url = http://192.168.178.50/update
48+
; custom_upload_url = http://192.168.178.50
4849
; upload_port = 192.168.178.50
50+
; custom_username = admin
51+
; custom_password = ikea-led-wall
4952

5053
[env:ESP32-wemos]
5154
lib_deps =
@@ -62,8 +65,9 @@ board_build.partitions = partitions-4MB.csv
6265
monitor_filters = esp32_exception_decoder
6366
; extra_scripts = upload.py
6467
; upload_protocol = custom
65-
; upload_url = http://192.168.178.50/update
66-
; upload_port = 192.168.178.50
68+
; custom_upload_url = http://192.168.178.50
69+
; custom_username = admin
70+
; custom_password = ikea-led-wall
6771

6872
[env:esp32dev]
6973
lib_deps =
@@ -80,8 +84,9 @@ board_build.partitions = partitions-4MB.csv
8084
monitor_filters = esp32_exception_decoder
8185
; extra_scripts = upload.py
8286
; upload_protocol = custom
83-
; upload_url = http://192.168.178.50/update
84-
; upload_port = 192.168.178.50
87+
; custom_upload_url = http://192.168.178.50
88+
; custom_username = admin
89+
; custom_password = ikea-led-wall
8590

8691
[env:nodemcuv2]
8792
board = nodemcuv2

src/ota.cpp

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ unsigned long ota_progress_millis = 0;
99

1010
void onOTAStart()
1111
{
12-
// Log when OTA has started
1312
Serial.println("OTA update started!");
1413
currentStatus = UPDATE;
1514

@@ -56,12 +55,15 @@ void onOTAEnd(bool success)
5655

5756
void initOTA(AsyncWebServer &server)
5857
{
59-
ElegantOTA.begin(&server); // Start ElegantOTA
58+
ElegantOTA.begin(&server);
59+
ElegantOTA.setAutoReboot(true);
6060
ElegantOTA.setAuth(otaUser, otaPassword);
61-
// ElegantOTA callbacks
62-
ElegantOTA.onStart(onOTAStart);
63-
ElegantOTA.onProgress(onOTAProgress);
64-
ElegantOTA.onEnd(onOTAEnd);
61+
62+
ElegantOTA.onStart([]() { onOTAStart(); });
63+
64+
ElegantOTA.onProgress([](size_t current, size_t final) { onOTAProgress(current, final); });
65+
66+
ElegantOTA.onEnd([](bool success) { onOTAEnd(success); });
6567
}
6668

6769
#endif

upload.py

Lines changed: 103 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Allows PlatformIO to upload directly to AsyncElegantOTA
1+
# Allows PlatformIO to upload directly to ElegantOTA
22
#
33
# To use:
44
# - copy this script into the same folder as your platformio.ini
@@ -9,14 +9,16 @@
99
# custom_upload_url = <your upload URL>
1010
#
1111
# An example of an upload URL:
12-
# custom_upload_URL = http://192.168.1.123/update
12+
# custom_upload_url = http://192.168.1.123/update
13+
# also possible: custom_upload_url = http://domainname/update
1314

15+
import sys
1416
import requests
1517
import hashlib
16-
Import('env')
17-
18-
username = "admin"
19-
password = "ikea-led-wall"
18+
from urllib.parse import urlparse
19+
import time
20+
from requests.auth import HTTPDigestAuth
21+
Import("env")
2022

2123
try:
2224
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
@@ -29,31 +31,113 @@
2931

3032
def on_upload(source, target, env):
3133
firmware_path = str(source[0])
32-
upload_url = env.GetProjectOption('upload_url')
34+
35+
auth = None
36+
upload_url_compatibility = env.GetProjectOption('custom_upload_url')
37+
upload_url = upload_url_compatibility.replace("/update", "")
3338

3439
with open(firmware_path, 'rb') as firmware:
3540
md5 = hashlib.md5(firmware.read()).hexdigest()
41+
42+
parsed_url = urlparse(upload_url)
43+
host_ip = parsed_url.netloc
44+
45+
is_spiffs = source[0].name == "spiffs.bin"
46+
file_type = "fs" if is_spiffs else "fr"
47+
48+
# execute GET request
49+
start_url = f"{upload_url}/ota/start?mode={file_type}&hash={md5}"
50+
51+
start_headers = {
52+
'Host': host_ip,
53+
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/118.0',
54+
'Accept': '*/*',
55+
'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
56+
'Accept-Encoding': 'gzip, deflate',
57+
'Referer': f'{upload_url}/update',
58+
'Connection': 'keep-alive'
59+
}
60+
61+
try:
62+
checkAuthResponse = requests.get(f"{upload_url_compatibility}/update")
63+
except Exception as e:
64+
return 'Error checking auth: ' + repr(e)
65+
66+
if checkAuthResponse.status_code == 401:
67+
try:
68+
username = env.GetProjectOption('custom_username')
69+
password = env.GetProjectOption('custom_password')
70+
except:
71+
username = None
72+
password = None
73+
print("No authentication values specified.")
74+
print('Please, add some Options in your .ini file like: \n\ncustom_username=username\ncustom_password=password\n')
75+
if username is None or password is None:
76+
return "Authentication required, but no credentials provided."
77+
print("Serverconfiguration: authentication needed.")
78+
auth = HTTPDigestAuth(username, password)
79+
try:
80+
doUpdateAuth = requests.get(start_url, headers=start_headers, auth=auth)
81+
except Exception as e:
82+
return 'Error while authenticating: ' + repr(e)
83+
84+
if doUpdateAuth.status_code != 200:
85+
return "Authentication failed " + str(doUpdateAuth.status_code)
86+
print("Authentication successful")
87+
else:
88+
auth = None
89+
print("Serverconfiguration: authentication not needed.")
90+
try:
91+
doUpdate = requests.get(start_url, headers=start_headers)
92+
except Exception as e:
93+
return 'Error while starting upload: ' + repr(e)
94+
95+
if doUpdate.status_code != 200:
96+
return "Start request failed " + str(doUpdate.status_code)
97+
3698
firmware.seek(0)
3799
encoder = MultipartEncoder(fields={
38100
'MD5': md5,
39101
'firmware': ('firmware', firmware, 'application/octet-stream')}
40102
)
41103

42104
bar = tqdm(desc='Upload Progress',
43-
total=encoder.len,
44-
dynamic_ncols=True,
45-
unit='B',
46-
unit_scale=True,
47-
unit_divisor=4096
48-
)
105+
total=encoder.len,
106+
dynamic_ncols=True,
107+
unit='B',
108+
unit_scale=True,
109+
unit_divisor=1024
110+
)
49111

50112
monitor = MultipartEncoderMonitor(encoder, lambda monitor: bar.update(monitor.bytes_read - bar.n))
51113

52-
response = requests.post(upload_url,
53-
auth=(username, password),
54-
data=monitor,
55-
headers={'Content-Type': monitor.content_type})
114+
post_headers = {
115+
'Host': host_ip,
116+
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/118.0',
117+
'Accept': '*/*',
118+
'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
119+
'Accept-Encoding': 'gzip, deflate',
120+
'Referer': f'{upload_url}/update',
121+
'Connection': 'keep-alive',
122+
'Content-Type': monitor.content_type,
123+
'Content-Length': str(monitor.len),
124+
'Origin': f'{upload_url}'
125+
}
126+
127+
try:
128+
response = requests.post(f"{upload_url}/ota/upload", data=monitor, headers=post_headers, auth=auth)
129+
except Exception as e:
130+
return 'Error while uploading: ' + repr(e)
131+
56132
bar.close()
57-
print(response,response.text)
133+
time.sleep(0.1)
134+
135+
if response.status_code != 200:
136+
message = "\nUpload failed.\nServer response: " + response.text
137+
tqdm.write(message)
138+
else:
139+
message = "\nUpload successful.\nServer response: " + response.text
140+
tqdm.write(message)
141+
58142

59-
env.Replace(UPLOADCMD=on_upload)
143+
env.Replace(UPLOADCMD=on_upload)

0 commit comments

Comments
 (0)