-
-
Notifications
You must be signed in to change notification settings - Fork 27
Expand file tree
/
Copy pathrelease.py
More file actions
239 lines (197 loc) · 8.79 KB
/
Copy pathrelease.py
File metadata and controls
239 lines (197 loc) · 8.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
#!/usr/bin/env python3
"""
End-to-end release builder for the SmallOLED-PCMonitor web flasher.
Runs the whole release pipeline for the browser flasher at docs/:
1. Reads FIRMWARE_VERSION from src/config/config.h -> v<ver>
2. Locates the PlatformIO CLI (PATH, then the standard penv install)
3. Builds both OLED variants in a single PlatformIO invocation
(oled-096 = SSD1306/SSD1309, oled-13 = SH1106)
4. Merges bootloader + partitions + app into a single "Full" image per
variant (flashed at 0x0, what ESP Web Tools writes)
5. Copies the Full.bin images into docs/firmware/latest/ as
SmallOLED-<id>-v<ver>-Full.bin and writes the VERSION file the page reads
6. Writes the GitHub Release images into release/v<ver>/ with screen-size
names non-technical users recognise:
firmware-v<ver>-OLED_<size>.bin (new device, full 0x0 image)
OTA_ONLY_firmware-v<ver>-OLED_<size>.bin (existing device, web UI update)
The web flasher reads firmware id from the BOARDS map in docs/flasher.js:
SSD1306 (0.96") and SSD1309 (2.42") deliberately share the `ssd1306` image;
SH1106 (1.3") has its own `sh1106` image.
Usage:
python release.py # build + package both variants
python release.py --skip-build # package whatever .pio/build already has
python release.py v1.6.0 # override the version string
"""
import argparse
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
# Variants published by the web flasher.
# (PlatformIO env, firmware id, screen-size token, label).
# The firmware id must match the `firmware` field in docs/flasher.js (used for
# the docs/ flasher images). The size token drives the user-facing release/
# filenames (firmware-v<ver>-OLED_<size>.bin) that go on the GitHub Release.
VARIANTS = [
("oled-096", "ssd1306", "0.96inch", '0.96" SSD1306 (also 2.42" SSD1309)'),
("oled-13", "sh1106", "1.3inch", '1.3" SH1106'),
("oled-154", "ch1116", "1.54inch", '1.54" CH1116'),
]
# Flash offsets for the ESP32-C3 (bootloader starts at 0x0).
BOOTLOADER_OFFSET = 0x0
PARTITIONS_OFFSET = 0x8000
FIRMWARE_OFFSET = 0x10000
REPO_ROOT = Path(__file__).resolve().parent
CONFIG_H = REPO_ROOT / "src" / "config" / "config.h"
DOCS_LATEST = REPO_ROOT / "docs" / "firmware" / "latest"
def read_version() -> str:
"""Extract FIRMWARE_VERSION from src/config/config.h, normalised to v<ver>."""
if not CONFIG_H.exists():
sys.exit(f"error: {CONFIG_H} not found")
pat = re.compile(r'#define\s+FIRMWARE_VERSION\s+"([^"]+)"')
for line in CONFIG_H.read_text(encoding="utf-8").splitlines():
m = pat.search(line)
if m:
ver = m.group(1).strip()
return ver if ver.startswith("v") else "v" + ver
sys.exit("error: FIRMWARE_VERSION not found in src/config/config.h")
def locate_pio() -> str:
"""Locate the PlatformIO CLI executable."""
for name in ("pio", "pio.exe", "platformio", "platformio.exe"):
found = shutil.which(name)
if found:
return found
candidates = [
Path.home() / ".platformio" / "penv" / "Scripts" / "pio.exe",
Path.home() / ".platformio" / "penv" / "Scripts" / "platformio.exe",
Path.home() / ".platformio" / "penv" / "bin" / "pio",
Path.home() / ".platformio" / "penv" / "bin" / "platformio",
]
for c in candidates:
if c.exists():
return str(c)
sys.exit(
"error: pio executable not found.\n"
" Tried PATH and the standard ~/.platformio/penv locations.\n"
" Install PlatformIO Core or add it to PATH."
)
def run(cmd, cwd=REPO_ROOT):
"""Run a subprocess, exit on failure."""
print(f"\n$ {' '.join(str(c) for c in cmd)}")
result = subprocess.run(cmd, cwd=cwd)
if result.returncode != 0:
sys.exit(f"error: command failed with exit code {result.returncode}")
def build_envs(pio_path: str):
"""Build every variant env in a single PlatformIO invocation."""
cmd = [pio_path, "run"]
for env, *_ in VARIANTS:
cmd.extend(["-e", env])
run(cmd)
def build_dir(env: str) -> Path:
return REPO_ROOT / ".pio" / "build" / env
def merge_full_bin(env: str, out_path: Path):
"""Merge bootloader + partitions + firmware into a single 0x0 image."""
bd = build_dir(env)
bootloader = bd / "bootloader.bin"
partitions = bd / "partitions.bin"
firmware = bd / "firmware.bin"
for p in (bootloader, partitions, firmware):
if not p.exists():
sys.exit(f"error: {p} not found - run a build first (omit --skip-build).")
out_path.parent.mkdir(parents=True, exist_ok=True)
with open(out_path, "wb") as out:
bl = bootloader.read_bytes()
out.write(bl)
out.write(b"\xFF" * (PARTITIONS_OFFSET - len(bl)))
pt = partitions.read_bytes()
out.write(pt)
out.write(b"\xFF" * (FIRMWARE_OFFSET - (PARTITIONS_OFFSET + len(pt))))
out.write(firmware.read_bytes())
size = out_path.stat().st_size
print(f" Full: {out_path.relative_to(REPO_ROOT)} ({size / 1024:.1f} KB)")
def copy_ota_bin(env: str, out_path: Path):
"""Copy firmware.bin verbatim as the OTA update image."""
firmware = build_dir(env) / "firmware.bin"
if not firmware.exists():
sys.exit(f"error: {firmware} not found - run a build first (omit --skip-build).")
out_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(firmware, out_path)
size = out_path.stat().st_size
print(f" OTA: {out_path.relative_to(REPO_ROOT)} ({size / 1024:.1f} KB)")
def write_version_file(version: str):
DOCS_LATEST.mkdir(parents=True, exist_ok=True)
(DOCS_LATEST / "VERSION").write_text(version + "\n", encoding="utf-8")
def find_old_full_bins(version: str):
"""List Full.bin files in docs/firmware/latest/ not for this version."""
if not DOCS_LATEST.exists():
return []
pat = re.compile(r"^SmallOLED-(.+)-(v[^-]+)-Full\.bin$")
old = []
for f in DOCS_LATEST.iterdir():
m = pat.match(f.name)
if m and m.group(2) != version:
old.append(f.name)
return sorted(old)
def main():
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("version", nargs="?", default=None,
help="Version override (default: read from config.h)")
parser.add_argument("--skip-build", action="store_true",
help="Skip the PlatformIO build (assume .pio/build is current)")
args = parser.parse_args()
version = args.version or read_version()
if not version.startswith("v"):
version = "v" + version
ota_dir = REPO_ROOT / "release" / version
print(f"SmallOLED web-flasher release: {version}")
print("Variants: " + ", ".join(f"{env} -> {fid}" for env, fid, *_ in VARIANTS))
if not args.skip_build:
pio = locate_pio()
print(f"PlatformIO: {pio}")
build_envs(pio)
else:
print("Skipping build (--skip-build)")
print("\n--- Web flasher images (docs/firmware/latest/) ---")
full_names = []
for env, fid, _size, _label in VARIANTS:
out = DOCS_LATEST / f"SmallOLED-{fid}-{version}-Full.bin"
merge_full_bin(env, out)
full_names.append(out.name)
write_version_file(version)
print(f" VERSION ({version})")
print(f"\n--- GitHub Release images (release/{version}/) ---")
rel_names = []
for env, fid, size, _label in VARIANTS:
# Full 0x0 image for new devices - same bytes as the docs flasher image,
# named by screen size so non-technical users pick the right one.
full_src = DOCS_LATEST / f"SmallOLED-{fid}-{version}-Full.bin"
full_out = ota_dir / f"firmware-{version}-OLED_{size}.bin"
full_out.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(full_src, full_out)
print(f" Full: {full_out.relative_to(REPO_ROOT)} ({full_out.stat().st_size / 1024:.1f} KB)")
rel_names.append(full_out.name)
# OTA-only image for existing devices (web UI update).
ota_out = ota_dir / f"OTA_ONLY_firmware-{version}-OLED_{size}.bin"
copy_ota_bin(env, ota_out)
rel_names.append(ota_out.name)
print("\n" + "=" * 60)
print(f"Release {version} ready.")
print("=" * 60)
old = find_old_full_bins(version)
if old:
print("\nOlder Full.bin files still in docs/firmware/latest/ "
"(remove with `git rm` when no longer needed):")
for name in old:
print(f" {name}")
print("\nNext steps:")
print(" git add docs/firmware/latest/ src/config/config.h")
print(f' git commit -m "release {version}"')
print(f" gh release create {version} release/{version}/*.bin --notes \"...\"")
print(" git push")
if __name__ == "__main__":
main()