Skip to content

Commit 756140f

Browse files
committed
add build script
1 parent 18f0d8a commit 756140f

2 files changed

Lines changed: 189 additions & 1 deletion

File tree

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,3 @@ scripts/api_migrate.py
1010
build.py
1111
/firmware
1212
/test/hardware
13-
support/build_firmware.py

support/build_firmware.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# MIT License
2+
#
3+
# Copyright (c) 2025 Felix Biego
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
#
23+
# ______________ _____
24+
# ___ __/___ /_ ___(_)_____ _______ _______
25+
# __ /_ __ __ \__ / _ _ \__ __ `/_ __ \
26+
# _ __/ _ /_/ /_ / / __/_ /_/ / / /_/ /
27+
# /_/ /_.___/ /_/ \___/ _\__, / \____/
28+
# /____/
29+
#
30+
31+
import re
32+
import os, shutil
33+
from datetime import datetime
34+
from pathlib import Path
35+
import json
36+
37+
Import("env")
38+
39+
sep = os.sep
40+
41+
FILE_PATH = Path(f"src{sep}ui{sep}ui.c")
42+
PIO_INI_PATH = Path("platformio.ini")
43+
FIRMWARE_JSON_PATH = Path(f"firmware{sep}firmware.json")
44+
45+
print("Extra Script")
46+
47+
def get_env_name(env_name):
48+
with open(PIO_INI_PATH) as f:
49+
content = f.read()
50+
51+
pattern = rf"default_envs\s*=\s*{re.escape(env_name)}\s*;\s*(.+)"
52+
match = re.search(pattern, content)
53+
54+
if match:
55+
description = match.group(1).strip()
56+
return description
57+
else:
58+
return env_name
59+
60+
def merge_bins(pairs, out_path, new_pairs, chip, env):
61+
"""
62+
Merge ESP32 .bin segments into one file starting from the lowest offset.
63+
64+
pairs: list of (offset, path) tuples (e.g. (0x1000, "bootloader.bin"))
65+
out_path: output file path
66+
"""
67+
68+
# Normalize and load
69+
segs = []
70+
for off, path in pairs:
71+
off = int(off, 0) if isinstance(off, str) else off
72+
with open(path, "rb") as f:
73+
data = f.read()
74+
segs.append((off, data, path))
75+
76+
# Find address bounds
77+
min_off = min(o for o, _, _ in segs)
78+
max_end = max(o + len(d) for o, d, _ in segs)
79+
total_size = max_end - min_off
80+
81+
out_path = out_path.replace('.bin', f"_0x{min_off:X}.bin")
82+
83+
print(f"Merging {len(segs)} segments into {out_path}")
84+
print(f"Start offset: 0x{min_off:X}, total size: {total_size} bytes")
85+
86+
buf = bytearray([0xFF]) * total_size
87+
88+
for off, data, path in segs:
89+
rel_off = off - min_off
90+
# print(f" {path}: offset=0x{off:X}, rel=0x{rel_off:X}, size={len(data)}")
91+
buf[rel_off:rel_off + len(data)] = data
92+
93+
os.makedirs(os.path.dirname(out_path), exist_ok=True)
94+
with open(out_path, "wb") as f:
95+
f.write(buf)
96+
97+
data = {}
98+
data[env] = {}
99+
data[env]["name"] = get_env_name(env)
100+
data[env]["file"] = f"{out_path.split(sep)[-1]}"
101+
data[env]["address"] = f"0x{min_off:X}"
102+
data[env]["size"] = total_size
103+
data[env]["chip"] = chip
104+
105+
106+
existing_data = {}
107+
if os.path.isfile(FIRMWARE_JSON_PATH):
108+
with open(FIRMWARE_JSON_PATH, 'r', encoding='utf-8') as f:
109+
existing_data = json.load(f)
110+
111+
existing_data.update(data)
112+
113+
with open(FIRMWARE_JSON_PATH, 'w', encoding='utf-8') as f:
114+
json.dump(existing_data, f, ensure_ascii=False, indent=4)
115+
116+
print(f"✅ Wrote merged file: {out_path} ({len(buf)} bytes)")
117+
118+
def read_version(content):
119+
version = re.search(r'#define\s+UI_VERSION\s+"([^"]+)"', content)
120+
if not all([version]):
121+
raise ValueError("Missing version fields in file")
122+
return {
123+
"version": version.group(1)
124+
}
125+
126+
def version_utils(file_path):
127+
content = file_path.read_text()
128+
version = read_version(content)
129+
return version
130+
131+
132+
def after_build(source, target, env):
133+
env_name = str(source[0]).split(sep)[-2]
134+
135+
dest_dir = f"firmware"
136+
os.makedirs(dest_dir, exist_ok=True)
137+
138+
skip_files = []
139+
skip_files.append("firmware.json") # always skip firmware.json
140+
141+
if os.path.isfile(FIRMWARE_JSON_PATH):
142+
with open(FIRMWARE_JSON_PATH, 'r', encoding='utf-8') as f:
143+
data = json.load(f)
144+
for env_key, env_data in data.items():
145+
if "file" in env_data:
146+
skip_files.append(env_data["file"])
147+
148+
# Remove all files and subfolders inside dest_dir
149+
for filename in os.listdir(dest_dir):
150+
if filename in skip_files:
151+
print(f"Skipping {filename} as it's in the skip list")
152+
continue
153+
file_path = os.path.join(dest_dir, filename)
154+
try:
155+
if os.path.isfile(file_path) or os.path.islink(file_path):
156+
os.remove(file_path) # remove file or symlink
157+
elif os.path.isdir(file_path):
158+
shutil.rmtree(file_path) # remove directory
159+
except Exception as e:
160+
print(f"Failed to delete {file_path}: {e}")
161+
162+
163+
info = version_utils(FILE_PATH)
164+
vers = f"v{info['version']}"
165+
166+
merged_path = f"{dest_dir}{sep}{env_name}_{vers}.bin"
167+
168+
# Get full upload command (PlatformIO’s real flash command)
169+
upload_cmd = env.subst("$UPLOADCMD") + f" {str(source[0])}"
170+
171+
# print(f"Upload cmd: {upload_cmd}")
172+
173+
# Parse the chip type
174+
chip_match = re.search(r"--chip\s+(\S+)", upload_cmd)
175+
chip = chip_match.group(1) if chip_match else "esp32"
176+
177+
# Parse offset + .bin pairs
178+
pairs = re.findall(r"(0x[0-9a-fA-F]+)\s+(\S+\.bin)", upload_cmd)
179+
if not pairs:
180+
print("❌ Could not detect any binary segments from upload command!")
181+
print(upload_cmd)
182+
return
183+
184+
merge_bins(pairs, merged_path, pairs, chip, env_name) # use custom merge function
185+
186+
187+
# Run after main program build
188+
env.AddPostAction("upload", after_build)
189+
env.AddPostAction("buildprog", after_build)

0 commit comments

Comments
 (0)