Server: Frame.panel_type (new column + migration) is auto-derived from the device's reported board (X-Frame-Board), never user-set -- the panel is a property of the hardware, not a picker in the UI. image_pipeline's packing/render pipeline is parameterized by panel geometry instead of hardcoded 800x480 globals, with the real confirmed 13.3in geometry (1600x1200) registered alongside the original 7.3in panel. Existing 7.3in frames are unaffected (column default + board mapping both resolve to the original panel). Board identifiers are also renamed (devkit/xiao -> devkit_esp32c6/ xiao_esp32c6, plus new "ee02") since the EE02 board also carries a XIAO module -- "xiao" alone stopped disambiguating hardware. The server keeps accepting the legacy bare names indefinitely for already-flashed devices. Firmware: scaffolds a third build target (ee02, ESP32-S3 -- a real chip-target change, not just a same-chip Kconfig variant like xiao) and a new epd13in3e driver component skeleton. The actual panel init/LUT/ refresh register sequence isn't ported from vendor demo code yet (none was available), so that component deliberately fails to compile (#error) rather than risk sending unverified register values to real hardware -- devkit/xiao are unaffected and build identically to before. CI's ee02 build step is continue-on-error for the same reason.
236 lines
12 KiB
YAML
236 lines
12 KiB
YAML
name: Build and release firmware
|
|
|
|
# Fires when firmware/version.txt is bumped on main -- that's the deliberate
|
|
# "cut a release" signal (mirrors ESP-IDF's own convention of reading the
|
|
# embedded app version from this file), not every firmware/** push. Also
|
|
# runnable by hand for a one-off rebuild of the current version.
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- "firmware/version.txt"
|
|
workflow_dispatch:
|
|
|
|
jobs:
|
|
build-and-release:
|
|
# Deliberately NOT a job-level `container: espressif/idf:...` -- that
|
|
# image has no Node.js in it, and actions/checkout (like most marketplace
|
|
# actions) is a Node action that gets exec'd *inside* whatever container
|
|
# the job specifies, so checkout fails immediately with "node: not
|
|
# found" (hit this on the first real run). Checkout instead runs on the
|
|
# plain runner (which has Node), and only the three build steps below
|
|
# spin up the ESP-IDF image themselves (docker create/cp/start, see the
|
|
# comment on those steps for why not a plain `docker run -v`) -- the
|
|
# runner already bind-mounts the host's docker socket, so docker-in-
|
|
# docker works fine from an ordinary run: step (same mechanism the
|
|
# existing server-docker-build.yml relies on for docker buildx).
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Read firmware version
|
|
id: version
|
|
run: echo "version=$(tr -d '[:space:]' < firmware/version.txt)" >> "$GITHUB_OUTPUT"
|
|
|
|
# Three board variants: devkit/xiao (ESP32-C6, different partition
|
|
# tables/flash sizes -- see firmware/README.md's "Building for the
|
|
# Seeed XIAO ESP32-C6" section) and ee02 (ESP32-S3 + 13.3" panel,
|
|
# a genuinely different chip target, not just a Kconfig variant).
|
|
# build_for_board.sh gives each its own build dir/generated
|
|
# sdkconfig so this never fights over shared state. set-target
|
|
# first since a fresh checkout has no cached sdkconfig
|
|
# (firmware/sdkconfig* is gitignored, see firmware/.gitignore).
|
|
# safe.directory guards against git's "dubious ownership" check,
|
|
# since the container runs as root over content owned by a
|
|
# different uid.
|
|
#
|
|
# Deliberately `docker create`/`docker cp`/`docker start`, NOT
|
|
# `docker run -v "$PWD:/workspace"` -- the runner's own job
|
|
# workspace lives in a named Docker volume, not a real host path
|
|
# (confirmed from a failed run's container-inspect output), so a
|
|
# nested `docker run` bind-mounting "$PWD" talks to the *host*
|
|
# daemon about a path that only means something inside this job's
|
|
# own container -- it silently bind-mounted an empty directory,
|
|
# hence "build_for_board.sh: No such file or directory". `docker
|
|
# cp` streams files through the Docker API instead, so it works
|
|
# regardless of what backs either side's workspace.
|
|
- name: Build (devkit -- ESP32-C6-DevKitC-1)
|
|
run: |
|
|
mkdir -p /tmp/release-assets
|
|
cid=$(docker create -w /workspace/firmware espressif/idf:release-v6.0 bash -c '
|
|
git config --global --add safe.directory /workspace &&
|
|
. "$IDF_PATH/export.sh" &&
|
|
./build_for_board.sh devkit set-target esp32c6 &&
|
|
./build_for_board.sh devkit build
|
|
')
|
|
docker cp "$PWD/." "$cid:/workspace"
|
|
docker start -a "$cid"
|
|
docker cp "$cid:/workspace/firmware/build/espresso_frame.bin" /tmp/release-assets/firmware-devkit_esp32c6.bin
|
|
docker rm "$cid"
|
|
# Rename bridge: fielded devices flashed before this rename still
|
|
# report the bare "devkit" board name and look up "firmware-
|
|
# devkit.bin" for their OTA check -- publish a duplicate under
|
|
# the old name too so they can update at all. Safe to drop this
|
|
# duplicate in a later release once no fielded device reports
|
|
# the bare name anymore.
|
|
cp /tmp/release-assets/firmware-devkit_esp32c6.bin /tmp/release-assets/firmware-devkit.bin
|
|
|
|
- name: Build (xiao -- Seeed XIAO ESP32-C6)
|
|
run: |
|
|
cid=$(docker create -w /workspace/firmware espressif/idf:release-v6.0 bash -c '
|
|
git config --global --add safe.directory /workspace &&
|
|
. "$IDF_PATH/export.sh" &&
|
|
./build_for_board.sh xiao set-target esp32c6 &&
|
|
./build_for_board.sh xiao build
|
|
')
|
|
docker cp "$PWD/." "$cid:/workspace"
|
|
docker start -a "$cid"
|
|
docker cp "$cid:/workspace/firmware/build_xiao/espresso_frame.bin" /tmp/release-assets/firmware-xiao_esp32c6.bin
|
|
docker rm "$cid"
|
|
# Same rename-bridge reasoning as the devkit step above.
|
|
cp /tmp/release-assets/firmware-xiao_esp32c6.bin /tmp/release-assets/firmware-xiao.bin
|
|
|
|
# NOTE: this build is expected to FAIL until
|
|
# firmware/components/epd13in3e's panel driver is ported from
|
|
# vendor demo code (see that component's own top-of-file comment
|
|
# -- a deliberate #error, not a bug here). `continue-on-error` so
|
|
# this known, tracked gap doesn't block publishing the devkit/xiao
|
|
# release (those boards work today and shouldn't wait on ee02) --
|
|
# this step's own status still shows failed/red individually in
|
|
# the run's step list, it just doesn't fail the overall job. Once
|
|
# epd13in3e's driver is real, a build failure here becomes a
|
|
# genuine regression again -- remove `continue-on-error` at that
|
|
# point so it goes back to failing the job like the other two
|
|
# builds do.
|
|
- name: Build (ee02 -- Seeed EE02, XIAO ESP32-S3 Plus + 13.3in panel)
|
|
continue-on-error: true
|
|
run: |
|
|
cid=$(docker create -w /workspace/firmware espressif/idf:release-v6.0 bash -c '
|
|
git config --global --add safe.directory /workspace &&
|
|
. "$IDF_PATH/export.sh" &&
|
|
./build_for_board.sh ee02 set-target esp32s3 &&
|
|
./build_for_board.sh ee02 build
|
|
')
|
|
docker cp "$PWD/." "$cid:/workspace"
|
|
docker start -a "$cid"
|
|
docker cp "$cid:/workspace/firmware/build_ee02/espresso_frame.bin" /tmp/release-assets/firmware-ee02.bin
|
|
docker rm "$cid"
|
|
|
|
# Plain stdlib urllib rather than `requests` -- not guaranteed to be
|
|
# pip-installed in the IDF image, and this is simple enough not to
|
|
# need it. Re-running this workflow for the same version.txt (e.g. a
|
|
# manual re-dispatch) reuses the existing tag's release and replaces
|
|
# its assets rather than failing on "release already exists".
|
|
#
|
|
# Requires a Gitea PAT with repository read/write (release) scope in
|
|
# the `RELEASE_TOKEN` secret -- Repo Settings -> Actions -> Secrets.
|
|
# A token that already has REGISTRY_TOKEN's scope may work too if it
|
|
# covers repo contents, but keeping it separate keeps each secret's
|
|
# blast radius obvious.
|
|
- name: Publish Gitea release
|
|
env:
|
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
API_BASE: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
|
TAG: v${{ steps.version.outputs.version }}
|
|
COMMIT: ${{ gitea.sha }}
|
|
run: |
|
|
python3 - <<'PYEOF'
|
|
import json
|
|
import os
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
api = os.environ["API_BASE"]
|
|
token = os.environ["GITEA_TOKEN"]
|
|
tag = os.environ["TAG"]
|
|
commit = os.environ["COMMIT"]
|
|
|
|
|
|
def req(method, path, body=None):
|
|
data = json.dumps(body).encode() if body is not None else None
|
|
headers = {"Authorization": f"token {token}"}
|
|
if body is not None:
|
|
headers["Content-Type"] = "application/json"
|
|
r = urllib.request.Request(f"{api}{path}", data=data, method=method, headers=headers)
|
|
try:
|
|
with urllib.request.urlopen(r) as resp:
|
|
return resp.status, json.loads(resp.read() or b"{}")
|
|
except urllib.error.HTTPError as e:
|
|
return e.code, json.loads(e.read() or b"{}")
|
|
|
|
|
|
status, release = req("GET", f"/releases/tags/{tag}")
|
|
if status == 404:
|
|
status, release = req(
|
|
"POST",
|
|
"/releases",
|
|
{
|
|
"tag_name": tag,
|
|
"target_commitish": commit,
|
|
"name": tag,
|
|
"body": f"Automated build from commit {commit}.",
|
|
"draft": False,
|
|
"prerelease": False,
|
|
},
|
|
)
|
|
if status not in (200, 201):
|
|
raise SystemExit(f"Failed to create release {tag}: {status} {release}")
|
|
print(f"Created release {tag} (id={release['id']})")
|
|
elif status == 200:
|
|
print(f"Release {tag} already exists (id={release['id']}), reusing it")
|
|
else:
|
|
raise SystemExit(f"Failed to look up release {tag}: {status} {release}")
|
|
|
|
release_id = release["id"]
|
|
existing_assets = {a["name"]: a["id"] for a in release.get("assets", [])}
|
|
|
|
assets = [
|
|
("firmware-devkit_esp32c6.bin", "/tmp/release-assets/firmware-devkit_esp32c6.bin"),
|
|
("firmware-xiao_esp32c6.bin", "/tmp/release-assets/firmware-xiao_esp32c6.bin"),
|
|
("firmware-ee02.bin", "/tmp/release-assets/firmware-ee02.bin"),
|
|
# Rename-bridge duplicates for devices still on old firmware
|
|
# reporting the bare "devkit"/"xiao" board names -- see the
|
|
# build steps above. Safe to remove once no fielded device
|
|
# reports the bare name anymore.
|
|
("firmware-devkit.bin", "/tmp/release-assets/firmware-devkit.bin"),
|
|
("firmware-xiao.bin", "/tmp/release-assets/firmware-xiao.bin"),
|
|
]
|
|
for name, path in assets:
|
|
if not os.path.exists(path):
|
|
# Expected for firmware-ee02.bin while that build is
|
|
# still allowed to fail (continue-on-error, see the
|
|
# build step's own comment) -- publish whatever boards
|
|
# did build rather than crashing the whole release over
|
|
# a known, tracked gap.
|
|
print(f"Skipping {name}: build did not produce {path}")
|
|
continue
|
|
if name in existing_assets:
|
|
del_status, _ = req("DELETE", f"/releases/{release_id}/assets/{existing_assets[name]}")
|
|
print(f"Removed existing asset {name} (status {del_status})")
|
|
|
|
boundary = "geafirmwareboundary"
|
|
with open(path, "rb") as f:
|
|
file_bytes = f.read()
|
|
body = (
|
|
f"--{boundary}\r\n"
|
|
f'Content-Disposition: form-data; name="attachment"; filename="{name}"\r\n'
|
|
"Content-Type: application/octet-stream\r\n\r\n"
|
|
).encode() + file_bytes + f"\r\n--{boundary}--\r\n".encode()
|
|
|
|
r = urllib.request.Request(
|
|
f"{api}/releases/{release_id}/assets?name={name}",
|
|
data=body,
|
|
method="POST",
|
|
headers={
|
|
"Authorization": f"token {token}",
|
|
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
|
},
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(r) as resp:
|
|
print(f"Uploaded {name}: status {resp.status}")
|
|
except urllib.error.HTTPError as e:
|
|
raise SystemExit(f"Failed to upload {name}: {e.code} {e.read()}")
|
|
PYEOF
|