Merge pull request #54 from Hoggormino/ci-build-workflow-and-makefile

Add CI build workflow and root Makefile
This commit is contained in:
I12BP8
2026-09-17 17:12:31 +02:00
committed by GitHub
7 changed files with 499 additions and 1 deletions
+289
View File
@@ -0,0 +1,289 @@
# TagTinker CI - builds every deliverable in this monorepo on each push/PR.
#
# Jobs (all independent, all run in parallel on ubuntu-latest):
# fap - Flipper Zero application (.fap) via ufbt, release + dev SDK channels
# worker - Cloudflare Worker (cloud-plugins/) typecheck + wrangler dry-run bundle
# esp32 - ESP32-S2 WiFi devboard firmware via the ESP-IDF v5.2.8 docker image
# web - syntax check of the inline JavaScript in web-image-prep/index.html
# lint - `ufbt lint` (clang-format), ADVISORY ONLY - never fails the run
#
# There are deliberately NO `paths:` filters: esp32-wifi-fw/shared/tt_wifi_proto.h
# is compiled into BOTH the FAP (via shared/tt_wifi_proto_fap.h) and the ESP
# firmware, so a change on either side must always rebuild both.
#
# Deployment of web-image-prep/ to GitHub Pages lives in pages.yml, not here.
name: Build
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
# One run per ref. A newer push to a PR branch cancels the still-running older
# one; pushes to main are never cancelled so every main commit keeps a result.
concurrency:
group: build-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
# ---------------------------------------------------------------------------
# Flipper Zero application (application.fam at repo root)
# ---------------------------------------------------------------------------
fap:
name: Flipper app (${{ matrix.channel }} SDK)
runs-on: ubuntu-latest
timeout-minutes: 30
# The `release` channel is the one users run (matches the local ufbt setup:
# firmware 1.4.3 / API 87.1 / f7) and is a hard gate. The `dev` channel
# tracks unreleased firmware and gives early warning of upcoming API
# breaks, so it is allowed to fail without failing the workflow.
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
include:
- channel: release
experimental: false
- channel: dev
experimental: true
steps:
- name: Checkout
uses: actions/checkout@v7
# Installs Python + ufbt, downloads/caches the SDK + toolchain, then runs
# `ufbt -s` inside app-dir. Output lands in <app-dir>/dist.
- name: Build FAP with ufbt
id: build-app
uses: flipperdevices/flipperzero-ufbt-action@v0.1
with:
app-dir: .
task: build
sdk-channel: ${{ matrix.channel }}
sdk-hw-target: f7
- name: Report SDK / toolchain used
env:
CHANNEL: ${{ matrix.channel }}
UFBT_STATUS: ${{ steps.build-app.outputs.ufbt-status }}
API_VERSION: ${{ steps.build-app.outputs.api-version }}
SUFFIX: ${{ steps.build-app.outputs.suffix }}
TOOLCHAIN: ${{ steps.build-app.outputs.toolchain-version }}
FAP_DIR: ${{ steps.build-app.outputs.fap-dir }}
run: |
sdk="$(printf '%s' "$UFBT_STATUS" | jq -r '"\(.version) (hw target \(.target))"' 2>/dev/null || echo unknown)"
{
echo "### Flipper app - ${CHANNEL} channel"
echo "- SDK: ${sdk}"
echo "- API version: ${API_VERSION}"
echo "- Toolchain: ${TOOLCHAIN}"
echo "- Artifact suffix: ${SUFFIX}"
} | tee -a "$GITHUB_STEP_SUMMARY"
ls -l "$FAP_DIR" "$FAP_DIR/debug"
- name: Upload FAP + debug ELF
uses: actions/upload-artifact@v7
with:
name: tagtinker-fap-${{ matrix.channel }}
path: |
dist/*.fap
dist/debug/*.elf
if-no-files-found: error
retention-days: 14
# ---------------------------------------------------------------------------
# Cloudflare Worker (cloud-plugins/)
# ---------------------------------------------------------------------------
worker:
name: Cloudflare Worker
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: cloud-plugins
steps:
- name: Checkout
uses: actions/checkout@v7
# Node 22 LTS; package.json declares no `engines`, so pin it here.
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
cache-dependency-path: cloud-plugins/package-lock.json
- name: Install dependencies
run: npm ci
- name: Typecheck
run: npx tsc --noEmit
# `npm run build` = `wrangler deploy --dry-run --outdir dist`: bundles the
# worker without touching Cloudflare, so no account/API token is needed.
- name: Bundle (wrangler dry-run)
run: npm run build
env:
CI: "true"
WRANGLER_SEND_METRICS: "false"
- name: Upload worker bundle
uses: actions/upload-artifact@v7
with:
name: tagtinker-worker-bundle
path: cloud-plugins/dist/index.js
if-no-files-found: error
retention-days: 14
# ---------------------------------------------------------------------------
# ESP32-S2 WiFi devboard firmware (esp32-wifi-fw/)
# ---------------------------------------------------------------------------
esp32:
name: ESP32-S2 WiFi firmware
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v7
# Runs `idf.py build` inside espressif/idf:v5.2.8 (same minor as the
# local ~/esp/esp-idf v5.2.8 install). The action only exports
# IDF_TARGET=esp32s2; sdkconfig.defaults additionally pins
# CONFIG_IDF_TARGET="esp32s2" and sdkconfig itself is gitignored, so a
# clean checkout resolves to the S2 target either way. If the two ever
# disagree idf.py refuses to build, which is what we want.
- name: Build with ESP-IDF v5.2.8
uses: espressif/esp-idf-ci-action@v1
with:
esp_idf_version: v5.2.8
target: esp32s2
path: esp32-wifi-fw
command: idf.py build
- name: Verify resolved target and list binaries
working-directory: esp32-wifi-fw
run: |
if ! grep -q '^CONFIG_IDF_TARGET="esp32s2"' sdkconfig; then
echo "::error file=esp32-wifi-fw/sdkconfig.defaults::sdkconfig did not resolve to esp32s2:"
grep '^CONFIG_IDF_TARGET' sdkconfig || echo "(CONFIG_IDF_TARGET not set at all)"
exit 1
fi
echo "Target OK: $(grep '^CONFIG_IDF_TARGET=' sdkconfig)"
ls -l \
build/tagtinker_wifi.bin \
build/bootloader/bootloader.bin \
build/partition_table/partition-table.bin \
build/ota_data_initial.bin
# Paths are kept relative to build/ so the layout inside the artifact
# matches the offsets/paths recorded in flasher_args.json.
- name: Upload firmware images
uses: actions/upload-artifact@v7
with:
name: tagtinker-esp32s2-firmware
path: |
esp32-wifi-fw/build/tagtinker_wifi.bin
esp32-wifi-fw/build/bootloader/bootloader.bin
esp32-wifi-fw/build/partition_table/partition-table.bin
esp32-wifi-fw/build/ota_data_initial.bin
esp32-wifi-fw/build/flasher_args.json
if-no-files-found: error
retention-days: 14
# ---------------------------------------------------------------------------
# Web image-prep tool (web-image-prep/index.html, single self-contained file)
# ---------------------------------------------------------------------------
web:
name: Web image-prep page
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 22
# The page has no build step, so the cheapest meaningful gate is to make
# sure every inline <script> still parses. Each block is written to a
# temp file (.mjs for type="module", .js otherwise) and run through
# `node --check`; <script src=...> and non-JS types (JSON, templates)
# are skipped.
- name: Syntax-check inline scripts
run: |
node - <<'JS'
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const file = 'web-image-prep/index.html';
const html = fs.readFileSync(file, 'utf8');
const tmp = fs.mkdtempSync(path.join(process.env.RUNNER_TEMP || os.tmpdir(), 'inline-js-'));
const re = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi;
const classicTypes = new Set(['', 'text/javascript', 'application/javascript']);
let index = 0, checked = 0, failed = 0, m;
while ((m = re.exec(html)) !== null) {
index++;
const [, attrs, body] = m;
if (/\bsrc\s*=/i.test(attrs)) continue;
const typeMatch = /\btype\s*=\s*["']?([^"'\s>]+)/i.exec(attrs);
const type = typeMatch ? typeMatch[1].toLowerCase() : '';
const isModule = type === 'module';
if (!isModule && !classicTypes.has(type)) continue;
const line = html.slice(0, m.index).split('\n').length;
const out = path.join(tmp, `script-${index}-line${line}${isModule ? '.mjs' : '.js'}`);
fs.writeFileSync(out, body);
const r = spawnSync(process.execPath, ['--check', out], { encoding: 'utf8' });
checked++;
if (r.status === 0) {
console.log(`ok <script> #${index} (line ${line}, ${isModule ? 'module' : 'classic'}, ${body.length} bytes)`);
} else {
failed++;
console.error(`::error file=${file},line=${line}::syntax error in inline <script> #${index}`);
console.error(r.stderr || r.stdout);
}
}
if (checked === 0) {
console.error(`::error file=${file}::no inline <script> blocks found - extractor is probably broken`);
process.exit(1);
}
console.log(`${checked} inline script(s) checked, ${failed} failed`);
process.exit(failed ? 1 : 0);
JS
# ---------------------------------------------------------------------------
# clang-format lint - ADVISORY ONLY
# ---------------------------------------------------------------------------
# `ufbt lint` is known to fail on main: the FAP sources intentionally use
# column-aligned macros/struct fields that clang-format rejects, and lint
# walks the whole repo (flagging the hyphenated cloud-plugins/, esp32-wifi-fw/
# and web-image-prep/ folders). The action exits non-zero on any violation,
# so the job is marked continue-on-error and only surfaces its findings in
# the job summary. Do NOT "fix" this with `ufbt format` - that would also
# rewrite the ESP32 C sources.
lint:
name: Lint (advisory, non-blocking)
runs-on: ubuntu-latest
timeout-minutes: 20
continue-on-error: true
steps:
- name: Checkout
uses: actions/checkout@v7
- name: ufbt lint (clang-format check)
uses: flipperdevices/flipperzero-ufbt-action@v0.1
with:
app-dir: .
task: lint
sdk-channel: release
sdk-hw-target: f7
+5
View File
@@ -40,3 +40,8 @@ __pycache__/
# OS garbage
.DS_Store
Desktop.ini
# Local dev tooling (see Makefile)
.venv/
# Generated by `ufbt lint` / `ufbt format`; not a project file
.clang-format
+1 -1
View File
@@ -7,7 +7,7 @@ Thanks for wanting to help out! Here's how to get involved.
1. Fork the repo
2. Create a feature branch: `git checkout -b my-feature`
3. Make your changes
4. Test on hardware if possible (or at minimum, ensure `ufbt build` passes)
4. Test on hardware if possible (or at minimum, make sure `make build-fap` (plain `ufbt`) passes). CI builds the Flipper FAP, the cloud worker and the ESP32 firmware on every PR, so keep all three compiling.
5. Commit with a clear message
6. Open a pull request
+171
View File
@@ -0,0 +1,171 @@
# TagTinker - developer convenience Makefile
#
# One entry point for the three build products in this repository:
# * Flipper Zero FAP (application.fam at the repo root, built with ufbt)
# * Cloudflare Worker (cloud-plugins/, TypeScript bundled by wrangler)
# * ESP32-S2 WiFi firmware (esp32-wifi-fw/, ESP-IDF v5.2)
# plus a local server for the static Image Prep web tool (web-image-prep/).
#
# Usage:
# make # list targets (same as `make help`)
# make setup # one-time: .venv + ufbt SDK + worker node_modules
# make build-all # build FAP, worker and ESP32 firmware
# make launch # build + install + run the FAP on a USB-connected Flipper
# make flash-esp ESP_PORT=/dev/cu.usbserial-XXXX
# make build-esp IDF_PYTHON_ENV_PATH=$HOME/.espressif/python_env/idf5.2_py3.11_env
# (ESP-IDF's export.sh picks the first python3 on PATH and expects a matching
# ~/.espressif/python_env/idf5.2_pyX.Y_env. When exactly one such env exists
# this Makefile exports IDF_PYTHON_ENV_PATH for you; set it explicitly if you
# keep several, or keep them somewhere other than ~/.espressif.)
#
# Every variable in the "Tools" and "Locations" blocks can be overridden from
# the command line or the environment, e.g. `make build-esp IDF_PATH=/opt/esp-idf`.
# Use absolute paths for IDF_PATH.
#
# Portability: written for GNU Make 3.81 (the stock /usr/bin/make on macOS) as
# well as GNU Make 4.x. Do not introduce 4.x-only features (ONESHELL, the
# shell-assignment operator, the file/let/intcmp functions, undefine, private).
# Every recipe line runs in its own shell, so steps that must share state -
# sourcing ESP-IDF's export.sh and then calling idf.py - are chained with &&
# on one line.
SHELL := /bin/bash
.DEFAULT_GOAL := help
# --- Tools ------------------------------------------------------------------
VENV ?= .venv
# Prefer the ufbt that `make setup` installs into the venv; else use PATH.
UFBT ?= $(if $(wildcard $(VENV)/bin/ufbt),$(VENV)/bin/ufbt,ufbt)
PYTHON ?= python3
NPM ?= npm
NPX ?= npx
IDF_PATH ?= $(HOME)/esp/esp-idf
export IDF_PATH
# Same chip the CI job builds for. Exporting it makes ESP-IDF refuse a stale
# sdkconfig that was generated for a different target instead of silently
# building it (sdkconfig.defaults pins the same value for clean builds).
IDF_TARGET ?= esp32s2
export IDF_TARGET
# ESP-IDF's export.sh detects `python3` from PATH and expects a virtualenv named
# after that interpreter (idf5.2_py3.14_env for Python 3.14). If install.sh ran
# with a different interpreter that env does not exist and export.sh fails even
# though a working idf5.2_py3.11_env sits right next to it. When exactly one 5.2
# env exists, point ESP-IDF at it; `?=` keeps any explicit override.
IDF_TOOLS_PATH ?= $(HOME)/.espressif
_idf_py_envs := $(wildcard $(IDF_TOOLS_PATH)/python_env/idf5.2_py*_env)
ifeq ($(words $(_idf_py_envs)),1)
IDF_PYTHON_ENV_PATH ?= $(_idf_py_envs)
endif
ifneq ($(IDF_PYTHON_ENV_PATH),)
export IDF_PYTHON_ENV_PATH
endif
# --- Locations / ports ------------------------------------------------------
ESP_PORT ?=
WEB_PORT ?= 8000
WEB_DIR ?= web-image-prep
ESP_DIR ?= esp32-wifi-fw
WORKER_DIR ?= cloud-plugins
# idf.py auto-detects the serial port when ESP_PORT is empty.
IDF_PORT_FLAG = $(if $(ESP_PORT),-p $(ESP_PORT),)
# Source ESP-IDF's export.sh in the *current* shell. Its chatty output goes to
# a temp log that is only printed if sourcing fails. Must be chained with &&
# on the same recipe line as the idf.py call that needs the environment.
IDF_ENV = idf_log=$$(mktemp) \
&& { . "$(IDF_PATH)/export.sh" >"$$idf_log" 2>&1 \
|| { cat "$$idf_log"; rm -f "$$idf_log"; \
echo "error: sourcing $(IDF_PATH)/export.sh failed." >&2; \
echo " Fix: run '$(IDF_PATH)/install.sh esp32s2', or - if 'python3' on PATH is not the interpreter" >&2; \
echo " install.sh used - pass IDF_PYTHON_ENV_PATH=$(IDF_TOOLS_PATH)/python_env/idf5.2_pyX.Y_env" >&2; \
exit 1; }; } \
&& rm -f "$$idf_log"
.PHONY: help setup build-fap build-worker build-esp build-all launch flash-esp \
serve-web lint check-worker clean distclean check-esp-env check-root worker-deps
# --- Help -------------------------------------------------------------------
help: ## Show this help
@echo "TagTinker developer targets (GNU Make $(MAKE_VERSION))"
@echo ""
@grep -E '^[a-zA-Z0-9_-]+:.*## ' $(firstword $(MAKEFILE_LIST)) \
| awk 'BEGIN { FS = ":.*## " } { printf " %-14s %s\n", $$1, $$2 }'
@echo ""
@echo "Variables (override on the command line, e.g. make flash-esp ESP_PORT=/dev/cu.usbserial-XXXX):"
@echo " UFBT=$(UFBT) PYTHON=$(PYTHON) NPM=$(NPM)"
@echo " IDF_PATH=$(IDF_PATH) ESP_PORT=$(if $(ESP_PORT),$(ESP_PORT),<auto>)"
@echo " IDF_PYTHON_ENV_PATH=$(if $(IDF_PYTHON_ENV_PATH),$(IDF_PYTHON_ENV_PATH),<ESP-IDF default>)"
@echo " ESP_DIR=$(ESP_DIR) WORKER_DIR=$(WORKER_DIR) WEB_DIR=$(WEB_DIR) WEB_PORT=$(WEB_PORT)"
# --- Setup ------------------------------------------------------------------
setup: ## One-time: create .venv with ufbt, fetch the Flipper SDK, npm ci the worker
@test -x $(VENV)/bin/python || { echo ">> creating virtualenv $(VENV)"; $(PYTHON) -m venv $(VENV); }
$(VENV)/bin/python -m pip install -q -U ufbt
$(VENV)/bin/ufbt update
cd $(WORKER_DIR) && $(NPM) ci
# Install worker dependencies only when node_modules is missing.
worker-deps:
@test -d $(WORKER_DIR)/node_modules || { echo ">> $(WORKER_DIR)/node_modules missing, running npm ci"; cd $(WORKER_DIR) && $(NPM) ci; }
# clean/distclean delete cwd-relative paths; refuse to run anywhere but the repo root.
check-root:
@test -f application.fam -a -d $(ESP_DIR) -a -d $(WORKER_DIR) || { \
echo "error: run make from the TagTinker repository root (or use make -C <repo>)" >&2; exit 1; }
# Fail early with a readable message when ESP-IDF is not where we expect it.
check-esp-env:
@test -f "$(IDF_PATH)/export.sh" || { \
echo "error: ESP-IDF not found at '$(IDF_PATH)' (no export.sh there)." >&2; \
echo " Install ESP-IDF v5.2 for the esp32s2 target, or point IDF_PATH at it:" >&2; \
echo " make build-esp IDF_PATH=/path/to/esp-idf" >&2; \
exit 1; }
# --- Build ------------------------------------------------------------------
build-fap: ## Build the Flipper app -> dist/tagtinker.fap (+ dist/debug/tagtinker_d.elf)
@# Bare `ufbt` (scons default targets) builds AND installs into dist/;
@# `ufbt build` only builds/validates inside ~/.ufbt/build and never copies.
$(UFBT)
build-worker: worker-deps ## Type-check and bundle the Cloudflare worker -> cloud-plugins/dist/index.js
cd $(WORKER_DIR) && $(NPX) tsc --noEmit
cd $(WORKER_DIR) && CI=true WRANGLER_SEND_METRICS=false $(NPM) run build
build-esp: check-esp-env ## Build the ESP32-S2 WiFi devboard firmware (ESP-IDF v5.2) -> esp32-wifi-fw/build/
@echo ">> idf.py -C $(ESP_DIR) build (IDF_PATH=$(IDF_PATH))"
@$(IDF_ENV) && idf.py -C $(ESP_DIR) build
build-all: build-fap build-worker build-esp ## Build the FAP, the worker and the ESP32 firmware
# --- Run / flash ------------------------------------------------------------
launch: build-fap ## Build, install and run the FAP on the USB-connected Flipper (port auto-detected)
$(UFBT) launch
flash-esp: check-esp-env build-esp ## Build and flash the ESP32-S2 firmware (ESP_PORT=/dev/... to pick a port)
@echo ">> idf.py -C $(ESP_DIR) $(IDF_PORT_FLAG) flash (IDF_PATH=$(IDF_PATH))"
@$(IDF_ENV) && idf.py -C $(ESP_DIR) $(IDF_PORT_FLAG) flash
serve-web: ## Serve the Image Prep web tool locally (WEB_PORT=8000)
@echo ">> serving $(WEB_DIR)/ at http://localhost:$(WEB_PORT)/ (Ctrl-C to stop)"
$(PYTHON) -m http.server $(WEB_PORT) --directory $(WEB_DIR)
# --- Checks -----------------------------------------------------------------
lint: ## ADVISORY: run ufbt lint (clang-format). Fails on main by design; not a CI gate
@echo ">> note: lint is advisory. The FAP sources use column-aligned macros/struct fields that"
@echo ">> clang-format rejects, and ufbt lint drops a generated .clang-format at the repo"
@echo ">> root (gitignored). Never run 'ufbt format' - it would rewrite the ESP32 sources too."
$(UFBT) lint
check-worker: worker-deps ## Type-check the worker only (npx tsc --noEmit)
cd $(WORKER_DIR) && $(NPX) tsc --noEmit
# --- Cleanup ----------------------------------------------------------------
clean: check-root ## Remove build outputs only (dist/, worker dist + .wrangler, ESP build/, generated .clang-format)
rm -rf dist $(WORKER_DIR)/dist $(WORKER_DIR)/.wrangler $(ESP_DIR)/build
rm -f .clang-format .vscode/compile_commands.json
distclean: check-root clean ## clean + remove .venv, worker node_modules and ESP sdkconfig (re-run 'make setup' afterwards)
@echo "!! distclean: removing $(VENV)/, $(WORKER_DIR)/node_modules/ and $(ESP_DIR)/sdkconfig"
rm -rf $(VENV) $(WORKER_DIR)/node_modules $(ESP_DIR)/sdkconfig
+28
View File
@@ -9,6 +9,7 @@
<img alt="License: GPL-3.0" src="https://img.shields.io/badge/License-GPL--3.0-blue.svg">
<img alt="Platform: Flipper Zero" src="https://img.shields.io/badge/Platform-Flipper%20Zero-black.svg">
<a href="https://i12bp8.github.io/TagTinker/"><img alt="Image Prep" src="https://img.shields.io/badge/Image%20Prep-Open%20in%20browser-a78bfa?logo=github"></a>
<a href="https://github.com/i12bp8/TagTinker/actions/workflows/build.yml"><img alt="Build" src="https://github.com/i12bp8/TagTinker/actions/workflows/build.yml/badge.svg"></a>
</p>
<p align="center">
@@ -48,6 +49,33 @@ This tool is built for IoT security curiosity, learning about obscure protocols,
3. Copy the BMP into `apps_data/tagtinker/dropped/` on the SD card (over `qFlipper`, USB MTP, or whatever you use).
4. On the Flipper open `Targeted Payloads → <your tag> → Set Image`, pick the BMP, choose a page, send.
## Development
The repo holds three build products plus the static web tool. A root `Makefile` wraps every toolchain and works with the stock `make` on macOS (GNU Make 3.81) as well as GNU Make 4.x. Run `make` with no arguments to list all targets.
**Prerequisites**
- **Python 3** — `make setup` creates `.venv/` and installs [ufbt](https://github.com/flipperdevices/flipperzero-ufbt) into it for the Flipper app.
- **Node.js 22+ (LTS)** — for the Cloudflare worker in `cloud-plugins/`.
- **ESP-IDF v5.2** (optional) — only needed to build or flash the WiFi devboard firmware in `esp32-wifi-fw/`. Install it at `~/esp/esp-idf` or pass `IDF_PATH=/path/to/esp-idf`. ESP-IDF's `export.sh` expects a Python virtualenv matching the `python3` on your `PATH`; if exactly one `~/.espressif/python_env/idf5.2_py*_env` exists the Makefile points ESP-IDF at it automatically, otherwise pass `IDF_PYTHON_ENV_PATH=...` yourself.
**Make targets**
| Target | What it does |
| --- | --- |
| `make setup` | One-time: create `.venv/` with ufbt, download the Flipper SDK, `npm ci` the worker |
| `make build-all` | Build the FAP, the worker and the ESP32 firmware |
| `make build-fap` | Build the Flipper app → `dist/tagtinker.fap` |
| `make launch` | Build, install and run the FAP on a USB-connected Flipper (port auto-detected) |
| `make build-worker` | Type-check and bundle the worker → `cloud-plugins/dist/index.js` |
| `make build-esp` | Build the ESP32-S2 firmware → `esp32-wifi-fw/build/tagtinker_wifi.bin` |
| `make flash-esp ESP_PORT=/dev/cu.usbserial-XXXX` | Build and flash the devboard (omit `ESP_PORT` to auto-detect) |
| `make serve-web` | Serve the Image Prep tool at `http://localhost:8000/` |
| `make lint` | Advisory `ufbt lint`; it currently fails on deliberately column-aligned code and is not a CI gate |
| `make clean` | Remove build outputs only (keeps `.venv/`, `node_modules/` and `sdkconfig`) |
CI ([`build.yml`](.github/workflows/build.yml)) builds all three components — FAP, worker and ESP32 firmware — on every pull request and every push to `main`; the web tool deploys to GitHub Pages from `main`.
## FAQ
**Does this require a Flipper Zero?**
+1
View File
@@ -4,6 +4,7 @@
"private": true,
"description": "Cloudflare Worker that renders TagTinker WiFi plugins server-side.",
"scripts": {
"build": "wrangler deploy --dry-run --outdir dist",
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"tail": "wrangler tail"
+4
View File
@@ -44,3 +44,7 @@ CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
# We don't include a phy_init partition (no room next to the ESP Flasher
# fixed offsets); use the embedded default calibration data.
CONFIG_ESP_PHY_INIT_DATA_IN_PARTITION=n
# Pin the chip target so clean builds (CI, fresh clones) do not silently fall
# back to plain esp32. The Flipper WiFi Devboard is an ESP32-S2.
CONFIG_IDF_TARGET="esp32s2"