STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
ota_upload.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3ESP32 Ethernet OTA Upload Script
4=================================
5Recompiles the OTA_Test_Firmware with a custom serial message,
6then pushes the resulting binary to the ESP32 over TCP (W5500 Ethernet).
7
8Usage:
9 python ota_upload.py # Uses timestamped default message
10 python ota_upload.py --message "Hello world" # Custom message
11 python ota_upload.py --ip 192.168.2.10 # Override target IP
12
13Protocol:
14 1. Send 4-byte firmware size (big-endian)
15 2. Send raw firmware binary in 4 KB chunks
16"""
17
18import argparse
19import datetime
20import os
21import socket
22import struct
23import subprocess
24import sys
25import time
26import shutil
27
28# ── Defaults ──────────────────────────────────────────────────
29DEFAULT_IP = "192.168.2.5"
30DEFAULT_PORT = 3232
31CHUNK_SIZE = 4096
32CONNECT_TIMEOUT = 5 # seconds
33TRANSFER_TIMEOUT = 30 # seconds
34
35# Path to the PlatformIO project (relative to this script)
36SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
37PROJECT_DIR = os.path.join(SCRIPT_DIR, "OTA_Test_Firmware")
38PIO_BUILD_DIR = os.path.join(PROJECT_DIR, ".pio", "build", "adafruit_feather_esp32s3")
39FIRMWARE_BIN = os.path.join(PIO_BUILD_DIR, "firmware.bin")
40
41
43 print("=" * 56)
44 print(" ESP32-S3 Ethernet OTA Upload Tool")
45 print("=" * 56)
46 print()
47
48
49def find_pio_command() -> str:
50 """Try to find the 'pio' command in PATH or common installation locations."""
51 # 1. Check if it's already in the environment PATH
52 pio_path = shutil.which("pio")
53 if pio_path:
54 return pio_path
55
56 # 2. Check common PlatformIO locations on Windows
57 if sys.platform == "win32":
58 user_profile = os.environ.get("USERPROFILE", "")
59 paths_to_check = [
60 os.path.join(user_profile, ".platformio", "penv", "Scripts", "pio.exe"),
61 os.path.join(
62 user_profile,
63 "AppData",
64 "Local",
65 "Programs",
66 "Python",
67 "Python313",
68 "Scripts",
69 "pio.exe",
70 ), # common alternative
71 "C:\\Python313\\Scripts\\pio.exe",
72 ]
73 for path in paths_to_check:
74 if os.path.isfile(path):
75 return path
76
77 # Fallback to just "pio" and hope for the best
78 return "pio"
79
80
81def compile_firmware(message: str) -> str:
82 """Compile the firmware with the given OTA_MESSAGE baked in.
83
84 Returns the path to the compiled .bin file.
85 """
86 print(f"[COMPILE] Message to bake into firmware:")
87 print(f' "{message}"')
88 print()
89
90 # Inject the message as an extra build flag via environment variable.
91 # This appends to whatever is in platformio.ini's build_flags.
92 env = os.environ.copy()
93 escaped_msg = message.replace('"', '\\"')
94 extra_flags = f"-DOTA_MESSAGE='\"{escaped_msg}\"'"
95 env["PLATFORMIO_BUILD_FLAGS"] = extra_flags
96
97 pio_cmd = find_pio_command()
98 print(f"[COMPILE] Running: {pio_cmd} run (in {PROJECT_DIR})")
99 print(f"[COMPILE] Extra build flags: {extra_flags}")
100 print()
101
102 try:
103 result = subprocess.run(
104 [pio_cmd, "run"], cwd=PROJECT_DIR, env=env, capture_output=True, text=True
105 )
106 except FileNotFoundError:
107 print("[COMPILE] ERROR: 'pio' command not found.")
108 print(" Make sure PlatformIO CLI is installed and in your PATH.")
109 print(" Install: pip install platformio")
110 sys.exit(1)
111
112 # Print PlatformIO output
113 if result.stdout:
114 for line in result.stdout.strip().split("\n"):
115 print(f" [PIO] {line}")
116 if result.stderr:
117 for line in result.stderr.strip().split("\n"):
118 # PlatformIO prints progress to stderr; only flag actual errors
119 if "error" in line.lower():
120 print(f" [PIO ERROR] {line}")
121 else:
122 print(f" [PIO] {line}")
123
124 if result.returncode != 0:
125 print()
126 print(f"[COMPILE] FAILED (exit code {result.returncode})")
127 sys.exit(1)
128
129 if not os.path.isfile(FIRMWARE_BIN):
130 print(f"[COMPILE] ERROR: Expected binary not found at:")
131 print(f" {FIRMWARE_BIN}")
132 sys.exit(1)
133
134 size = os.path.getsize(FIRMWARE_BIN)
135 print()
136 print(f"[COMPILE] SUCCESS — firmware.bin = {size:,} bytes")
137 print(f" Path: {FIRMWARE_BIN}")
138 print()
139 return FIRMWARE_BIN
140
141
142def upload_firmware(bin_path: str, ip: str, port: int):
143 """Send the firmware binary to the ESP32 over TCP."""
144 file_size = os.path.getsize(bin_path)
145
146 print(f"[UPLOAD] Target: {ip}:{port}")
147 print(f"[UPLOAD] Firmware size: {file_size:,} bytes")
148 print()
149
150 # ── Connect ──────────────────────────────────────────────
151 print(f"[UPLOAD] Connecting to ESP32...")
152 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
153 sock.settimeout(CONNECT_TIMEOUT)
154
155 try:
156 sock.connect((ip, port))
157 except socket.timeout:
158 print(f"[UPLOAD] ERROR: Connection timed out after {CONNECT_TIMEOUT}s.")
159 print(f" Is the ESP32 running and reachable at {ip}?")
160 sys.exit(1)
161 except ConnectionRefusedError:
162 print(f"[UPLOAD] ERROR: Connection refused by {ip}:{port}.")
163 print(f" Is the OTA firmware running on the ESP32?")
164 sys.exit(1)
165 except OSError as e:
166 print(f"[UPLOAD] ERROR: Could not connect: {e}")
167 sys.exit(1)
168
169 print(f"[UPLOAD] Connected!")
170 sock.settimeout(TRANSFER_TIMEOUT)
171
172 try:
173 # ── Send size header (4 bytes, big-endian) ───────────
174 header = struct.pack(">I", file_size)
175 sock.sendall(header)
176 print(f"[UPLOAD] Sent size header: {file_size} bytes")
177
178 # ── Stream firmware binary ───────────────────────────
179 sent = 0
180 last_percent = -1
181 start_time = time.time()
182
183 with open(bin_path, "rb") as f:
184 while sent < file_size:
185 chunk = f.read(CHUNK_SIZE)
186 if not chunk:
187 break
188 sock.sendall(chunk)
189 sent += len(chunk)
190
191 percent = int(sent * 100 / file_size)
192 # Print progress every 5%
193 if percent // 5 != last_percent // 5:
194 last_percent = percent
195 elapsed = time.time() - start_time
196 rate = sent / elapsed / 1024 if elapsed > 0 else 0
197 print(
198 f"[UPLOAD] Progress: {percent:3d}% "
199 f"({sent:,} / {file_size:,} bytes) "
200 f"[{rate:.1f} KB/s]"
201 )
202
203 elapsed = time.time() - start_time
204 print()
205 print(f"[UPLOAD] Transfer complete! {sent:,} bytes in {elapsed:.1f}s")
206 print(f"[UPLOAD] Waiting for ESP32 confirmation...")
207
208 try:
209 # Wait for "OK" response from ESP32
210 response = sock.recv(1024).decode(errors="ignore").strip()
211 if "OK" in response:
212 print(f"[UPLOAD] SUCCESS: ESP32 received firmware and is rebooting.")
213 else:
214 print(
215 f"[UPLOAD] WARNING: Connection closed without 'OK' response. Response was: '{response}'"
216 )
217 except socket.timeout:
218 print(f"[UPLOAD] WARNING: Timed out waiting for 'OK' confirmation.")
219
220 except socket.timeout:
221 print(f"[UPLOAD] ERROR: Transfer timed out.")
222 sys.exit(1)
223 except BrokenPipeError:
224 print(f"[UPLOAD] ERROR: Connection lost during transfer.")
225 sys.exit(1)
226 finally:
227 sock.close()
228
229
230def main():
232
233 parser = argparse.ArgumentParser(
234 description="Compile and OTA-upload firmware to ESP32-S3 over Ethernet"
235 )
236 parser.add_argument(
237 "--message",
238 "-m",
239 default=None,
240 help="Message to bake into firmware (default: timestamped string)",
241 )
242 parser.add_argument(
243 "--ip", default=DEFAULT_IP, help=f"ESP32 IP address (default: {DEFAULT_IP})"
244 )
245 parser.add_argument(
246 "--port",
247 "-p",
248 type=int,
249 default=DEFAULT_PORT,
250 help=f"ESP32 OTA TCP port (default: {DEFAULT_PORT})",
251 )
252 parser.add_argument(
253 "--skip-compile",
254 action="store_true",
255 help="Skip compilation, just upload the existing binary",
256 )
257 args = parser.parse_args()
258
259 # Default message includes a timestamp so you can tell updates apart
260 if args.message is None:
261 ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
262 args.message = f"OTA update at {ts}"
263
264 # ── Compile ──────────────────────────────────────────────
265 if args.skip_compile:
266 if not os.path.isfile(FIRMWARE_BIN):
267 print(f"[ERROR] --skip-compile set but no binary found at:")
268 print(f" {FIRMWARE_BIN}")
269 sys.exit(1)
270 print(f"[COMPILE] Skipped (--skip-compile). Using existing binary.")
271 print(f" {FIRMWARE_BIN} ({os.path.getsize(FIRMWARE_BIN):,} bytes)")
272 print()
273 bin_path = FIRMWARE_BIN
274 else:
275 bin_path = compile_firmware(args.message)
276
277 # ── Upload ───────────────────────────────────────────────
278 upload_firmware(bin_path, args.ip, args.port)
279
280 print()
281 print("=" * 56)
282 print(" Done! Watch the ESP32 serial monitor for the")
283 print(f' new message: "{args.message}"')
284 print("=" * 56)
285
286
287if __name__ == "__main__":
288 main()
static std::vector< string_t > split(const string_t &reference_string)
split the string input to reference tokens
Definition json.hpp:14471
str find_pio_command()
Definition ota_upload.py:49
str compile_firmware(str message)
Definition ota_upload.py:81
upload_firmware(str bin_path, str ip, int port)
TOML_NODISCARD_CTOR time() noexcept=default
int main()
static void decode(const disk_t *d)