Web-based firmware flasher, build system, and font builder for CrossPoint Reader devices. Hosted at crosspointreader.com.
- Stable firmware flashing — Flash the latest CrossPoint release or stock Xteink firmware to X3 and X4 devices directly from the browser using WebSerial
- Insider builds — Nightly firmware compiled automatically from the upstream
masterbranch - Beta builds — Curated test builds exposed alongside stable, sourced from either an admin-uploaded
.binor a tagged GitHub release on the firmware repo - Custom font firmware — Replace built-in fonts in the firmware with user-supplied TTF/OTF files via a CI build
- SD-card font builder — Convert TTF/OTF files into
.cpfontfiles for SD-card font loading on the device, using the firmware repo's own conversion script (no client-side reimplementation) - Stock firmware — Restore original Xteink firmware (English or Chinese) for both X3 and X4
- Admin dashboard — Manually trigger builds, manage beta entries, and monitor build status
The project runs on Cloudflare Workers with GitHub Actions handling everything that needs a real toolchain (firmware compilation, font conversion):
- Worker (
src/index.ts) — API routes, firmware proxying, beta/font orchestration, static asset serving - R2 (
crosspoint-firmware) — Compiled firmware binaries and font build artifacts - KV (
BUILD_META) — Build metadata, sha caches, font build state - Static assets (
public/) — HTML pages, the WebSerial flasher module, the admin dashboard, and bundled X3 firmware - GitHub Actions workflows in
.github/workflows/— long-running build jobs the Worker can dispatch
A daily cron job (or manual trigger from /admin) dispatches the workflow, which:
- Checks the upstream crosspoint-reader/crosspoint-reader
masterfor new commits - If there's a new commit (or the previous run failed), clones with submodules, installs the pioarduino PlatformIO fork, and runs
pio run -e gh_release - Uploads the resulting
firmware.binto R2 via the Worker API - Stores build metadata (version, commit, changelog since last tag) in KV
Insider builds are exposed at /insider and surfaced in the catalog under the insider channel.
Lets a user replace one or more built-in fonts in the firmware:
- The user uploads TTFs via
/fonts(the "Custom Font Firmware" form) - Worker stores them in R2 under
builds/custom/{buildId}/fonts/...and dispatches the workflow - Workflow checks out the firmware repo, drops the user's TTFs over the built-in font sources, applies any label/size overrides, runs the firmware-side font conversion, then builds the firmware
- The completed
firmware.binis uploaded back to the Worker; the user is offered a download
Converts arbitrary TTF/OTF files into .cpfont files for SD-card loading without recompiling firmware:
- The user uploads up to four primary styles (regular, bold, italic, bold-italic), up to two fallback family regular styles, plus a family name, point sizes, and either a Unicode interval preset or custom converter ranges at
/fonts - Worker stores the TTFs in R2 under
font-builds/{buildId}/in/and dispatches the workflow - Workflow checks out this repo, installs
freetype-py fonttools brotli, and runs the vendored SD-card generator atscripts/font-builder/fontconvert_sdcard.py - Each
.cpfontis uploaded back to the Worker underfont-builds/{buildId}/out/ - The frontend polls
/api/font-build/status, streams the script's stderr (glyph counts, kerning pair counts) into the UI, and offers individual or zipped downloads
The current generator started as a snapshot of crosspoint-reader's lib/EpdFont/scripts/fontconvert_sdcard.py, but it now lives in this repo so the website can evolve SD-card-specific features like multi-family fallback handling independently.
Built .cpfont files install on the device by copying to the SD card under /fonts/YourFont/ (or /.fonts/YourFont/ for a hidden folder).
Beta builds appear in the catalog as a separate channel and are managed from the admin dashboard. Each entry has one of two sources:
- Upload — Admin attaches a local
.bin. Stored verbatim in R2. - GitHub release — Admin enters a release tag (default repo
crosspoint-reader/crosspoint-reader). The Worker resolves the release via the GitHub API, fetches itsfirmware.binasset (or the first.binasset iffirmware.binisn't present), and caches the bytes into R2 under the same key as an upload. The original release tag is recorded so the entry can be re-fetched later.
Editing an entry lets you re-link it to a different release tag, which transparently replaces the stored binary. Existing upload-backed betas keep working — the source field is optional and absent legacy entries are treated as uploads.
The browser-based flasher (public/js/flasher.js) uses esptool-js via WebSerial:
- Connects to the ESP32-C3 over USB serial
- Reads and validates the partition table (X3 or X4)
- Writes firmware to the backup OTA partition
- Updates the OTA boot selector to swap partitions on next boot
/debug is a diagnostic tool for inspecting a device over WebSerial without flashing anything. It connects to the ESP32-C3, reads the partition table, and reports whether the layout matches a known profile:
- CrossPoint — X4 CrossPoint layout, ready to flash
- CrossPoint KO fork — KO community fork layout, ready to flash
- Stock X3 — Needs repartition before CrossPoint can be flashed
- Unknown — No match; the raw table is shown for inspection
Useful when a user reports a flash failure or unexpected behavior — ask them to load /debug, connect the device, and share the layout badge and dumped partition entries.
- Node.js 20+
- A Cloudflare Workers Paid plan
- A GitHub classic personal access token with
workflowscope (for dispatching workflows in this repo)
npm install
# Create Cloudflare resources (first time only)
npx wrangler r2 bucket create crosspoint-firmware
npx wrangler kv namespace create BUILD_META
# Update the KV namespace ID in wrangler.jsonc
# Set secrets
npx wrangler secret put GITHUB_WEBHOOK_SECRET # Generate with: openssl rand -hex 32
npx wrangler secret put GITHUB_TOKEN # Classic PAT with workflow scope
# Deploy
npm run deploySet these on the SoFriendly/crosspoint-tools repo (Settings → Secrets → Actions):
WEBHOOK_SECRET— Same value asGITHUB_WEBHOOK_SECRETin the Worker. Used by every workflow to authenticate callbacks (status updates, asset downloads, result uploads).GH_PAT— GitHub classic PAT (no scopes needed if everything you read is public). Used by the workflows for higher API rate limits when reading the upstream firmware repo.
npm run dev # Starts wrangler dev server on localhost:8787
npm run tunnel # Exposes localhost:8787 through a temporary public tunnelIf you want local builds to dispatch GitHub Actions, set GITHUB_TOKEN in .dev.vars to a token that can trigger workflows on the repo you are targeting. By default that is crosspoint-reader/crosspoint-tools, but you can override it with:
GITHUB_ACTIONS_REPO=owner/repoGITHUB_ACTIONS_REF=branch-or-tag
Without GITHUB_TOKEN, font/custom/manual build triggers will fail before the GitHub Actions step starts.
If Wrangler fails while downloading cloudflared, install it yourself and rerun the tunnel command:
brew install cloudflared
npm run tunnelThe tunnel script prefers an existing cloudflared on your PATH via CLOUDFLARED_PATH, so Wrangler can skip its own download step.
On Windows, run npm run tunnel from Git Bash/WSL if you want Unix-like tooling, or set CLOUDFLARED_PATH to your cloudflared.exe path before launching it from PowerShell or Command Prompt.
For end-to-end build testing, the worker now passes the current request origin to GitHub Actions as the webhook callback base URL. In practice that means:
- Use
npm run devfor local handler testing only. - Run
npm run devin one terminal andnpm run tunnelin a second terminal when you need GitHub Actions to call your dev worker back. - Open the site through the tunnel URL, not
localhost, so the worker dispatches GitHub Actions with the tunnel origin. - If you want to keep browsing on
localhostwhile callbacks go elsewhere, setWEBHOOK_BASE_URL=https://your-tunnel-or-dev-hostin.dev.vars. - If you do not know the shared GitHub
WEBHOOK_SECRET, setALLOW_INSECURE_DEV_WEBHOOKS=truein.dev.varsfor local-only testing so your dev worker accepts GitHub callbacks without matching the production secret. - If you want to use your own fork or sandbox repo for Actions, set
GITHUB_ACTIONS_REPOand optionallyGITHUB_ACTIONS_REFin.dev.vars, then add the same workflow files andWEBHOOK_SECRETsecret to that repo.
The WebSerial flasher is based on xteink-flasher, licensed under the MIT License. The partition table handling, OTA flashing logic, and device model support were adapted from that project.
The SD-card font builder uses the vendored generator in scripts/font-builder/, originally sourced from the crosspoint-reader firmware repo. Kerning, ligature, and interval behavior now live here, so changes to website-only font features can be made locally without waiting on firmware-repo updates.
MIT