diff --git a/README.md b/README.md
index 63bfa8b..95e3b61 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[English](README.md) | [简体中文](README.zh-CN.md)
Stack-chan Matchday is a lightweight [Stack-chan](https://github.com/stack-chan/stack-chan)
-mod and Python LAN watcher that together turn a CoreS3 robot into a World Cup
+mod and Python LAN watcher that turn a CoreS3 robot into a World Cup
co-watching companion. It shows both teams' Kalshi advance-market
probabilities, follows ESPN scores and live commentary, reacts with speech,
balloons, lights, and safe head movements, and lets you choose the next match
@@ -11,26 +11,10 @@ from a phone.
> [!IMPORTANT]
> This is a read-only match companion. It does not trade, access a Kalshi
-> account, or provide betting advice. `position_team` is a manually entered
-> preference: the watcher describes its benefit or pressure around major ESPN
-> events, results, and Kalshi moves from a confirmed matching position market,
-> but never reads a real account. Kalshi data comes from its public REST API;
-> ESPN data comes from publicly reachable, undocumented endpoints that may
-> change or lag behind the broadcast.
-
-## Features
-
-- A persistent two-team probability bar with flags and a bottom market ticker.
-- Goal, card, substitution, close-miss, match-state, and final-result reactions.
-- Three switchable commentary styles: casual co-watching, balanced narration,
- and professional play-by-play with ESPN-supplied detail.
-- Separate support and position perspectives: routine play follows the team you
- support, while major events add position impact and acknowledge conflicts.
-- A phone setup page hosted by Stack-chan, with live Chinese/English switching.
-- Double-tap head-touch and Power-button shortcuts for the setup QR code.
-- Fixture discovery, adaptive polling, hot configuration reload, and quiet hours.
-- Optional LAN TTS; visual feedback and tone patterns still work without it.
-- Standalone mode for following up to four active markets without a fixture.
+> account, or provide betting advice. A position is a manually entered
+> preference, never a real-account holding. Kalshi data comes from its public
+> REST API; ESPN data comes from publicly reachable, undocumented endpoints
+> that may change or lag behind the broadcast.
## In action
@@ -48,463 +32,148 @@ from a phone.

- English setup page for choosing language, commentary style, and a match.
+ Choose a match, perspective, position, language, and commentary style.
|
-## System design
-
-
-
-Kalshi and ESPN are read-only inputs to the Python watcher. The watcher sends
-fixture options, display commands, and acknowledgements to the Matchday mod.
-It also parses match events into shared facts and renders the selected
-commentary style; the mod only forwards settings and plays the resulting
-display, speech, and reaction commands. The upstream host firmware and TTS
-module are not modified for commentary styling.
-The phone talks to Stack-chan's own `/setup` page; the device stores a pending
-selection, and the watcher validates it, atomically updates the local JSON
-configuration, hot-reloads, and acknowledges the device. For speech,
-Stack-chan requests `/say?text=...` from the optional LAN TTS server and
-receives a 24 kHz mono 16-bit PCM WAV response.
-
-The phone, watcher host, TTS server, and Stack-chan must be on the same trusted
-LAN. The optional watcher-hosted `:8788/setup` page is a local admin fallback,
-not the primary QR flow.
-
-## Repository layout
-
-- `mod/` — the device mod, split into small JS modules plus flag and QR assets.
-- `host/` — a required CoreS3 partition patch, an optional CJK-font patch, and
- the font preparation helper. These are build/resource changes; the upstream
- runtime JS/C source remains unchanged.
-- `tools/` — the watcher, local setup service, macOS TTS server, replay tool,
- serial helper, asset generator, and tests. The default HTTP workflow uses the
- Python standard library; serial transport additionally requires `pyserial`.
-- `config/` — the example watcher configuration, flag-pack definition, and
- global ESPN player catalog.
-- `docs/` — the [commentary-styles PRD](docs/commentary-styles-prd.md) and
- version-specific upgrade notes, including the bilingual
- [Matchday MOD 1.5.0 notes](docs/releases/1.5.0.md) and
- [1.4.0 notes](docs/releases/1.4.0.md).
-
-## Requirements
-
-- A CoreS3-based Stack-chan with 16 MB flash and a USB data cable.
-- Git, Python 3.10+, Node.js 20+ (Node.js 22 is the tested upstream version),
- npm, and `xz` on the build computer.
-- Moddable SDK and ESP-IDF. The upstream `xs-dev` setup command below installs
- and checks them.
-- A phone and watcher computer on the same trusted LAN as Stack-chan.
-- `qrencode` only when generating the device-specific setup QR.
-- macOS only for the included `say`-based TTS server. Other systems can run the
- watcher without speech or provide a compatible `/say` WAV service.
-
-The commands below are written for macOS/Linux shells. Set two absolute paths
-once and keep using them throughout the installation:
-
-```sh
-mkdir -p "$HOME/src"
-export MATCHDAY_DIR="$HOME/src/stackchan-matchday"
-export STACKCHAN_DIR="$HOME/src/stack-chan"
-```
-
-## Install
-
-Already running an earlier Matchday release? For 1.4.0 commentary styles,
-update this watcher checkout and reinstall the Matchday mod. You do not need
-to rebuild or reflash the official host firmware or replace the TTS module;
-see the [1.4.0 release notes](docs/releases/1.4.0.md).
-
-Support/position-aware wording, position-based market selection, and the global
-player catalog are watcher-only updates. An installed 1.5.0 mod can stay in
-place. Deploy both the watcher code and `config/espn_player_catalog.json`, then
-restart the Python watcher process; the mod and host do not need reflashing.
-
-### 1. Clone and prepare the upstream build environment
-
-```sh
-git clone https://github.com/xymeow/stackchan-matchday.git "$MATCHDAY_DIR"
-git clone https://github.com/stack-chan/stack-chan.git "$STACKCHAN_DIR"
-
-cd "$STACKCHAN_DIR"
-git switch --detach ded5ca94ef50411aec213b85a23d1afe72d4c29e
-
-cd "$STACKCHAN_DIR/firmware"
-npm ci
-npm run setup -- --device=esp32
-npm run doctor
-```
-
-The pinned commit is the tested base for the patches in this repository. Do
-not build until `npm run doctor` lists `esp32` as a supported target. See the
-upstream [getting-started guide](https://github.com/stack-chan/stack-chan/blob/dev/v1.0/firmware/docs/getting-started.md)
-if `xs-dev` reports a platform-specific prerequisite.
-
-### 2. Patch, build, and flash the host once
-
-Apply the partition patch on every new CoreS3 host checkout:
-
-```sh
-cd "$STACKCHAN_DIR"
-git am "$MATCHDAY_DIR/host/patches/0001-Add-xs-mod-partition-for-M5StackChan-CoreS3.patch"
-```
-
-For Chinese labels and balloons, also apply the font patch and prepare a
-CJK-capable TTF. Pure-English installations may skip these two commands and
-set the watcher language to `en`:
-
-```sh
-git am "$MATCHDAY_DIR/host/patches/0002-Add-optional-StackChanCN-24-GB2312-font-resource.patch"
-python3 "$MATCHDAY_DIR/host/prepare_cjk_font.py" "$STACKCHAN_DIR"
-```
-
-Build and flash the host:
-
-```sh
-cd "$STACKCHAN_DIR/firmware"
-export PATH="$PWD/node_modules/.bin:$PATH"
-mcconfig -d -m -p esp32:./platforms/m5stackchan_cores3 -t deploy \
- "$PWD/stackchan/manifest_m5stackchan_cores3.json"
-```
-
-See [host/README.md](host/README.md) for font selection and patch details.
-
-### 3. Generate the QR, then build and install the mod
-
-The QR image is a static asset compiled into the mod; it is not generated at
-runtime. Use a stable DHCP reservation, IP address, or resolvable mDNS name,
-and generate the QR before installing the mod. The UI reads the PNG's natural
-dimensions; keep both edges at or below 168 px so the title and URL still fit:
-
-```sh
-export STACKCHAN_HOST=stackchan.local
-qrencode -s 4 -m 1 -o "$MATCHDAY_DIR/mod/assets/setup/setup-qr.png" \
- "http://$STACKCHAN_HOST/setup"
-file "$MATCHDAY_DIR/mod/assets/setup/setup-qr.png"
-```
-
-If `file` reports an edge larger than 168 px, regenerate with `-s 3`. Then
-build and install the mod:
-
-```sh
-cd "$STACKCHAN_DIR/firmware"
-npm run mod --target=esp32:./platforms/m5stackchan_cores3 -- -f rgb565be \
- "$MATCHDAY_DIR/mod/manifest.json"
-```
-
-`-f rgb565be` is required on CoreS3; without it, flag colors are byte-swapped.
-
-`npm run mod` installs over the xsbug debug protocol, which needs an xsbug
-listener and can stall mid-write while the device is busy (a stalled install
-invalidates the mod until reinstalled). The debugger-free alternative is to
-build only, then write the archive straight into the `xs` partition:
-
-```sh
-mcrun -d -m -p esp32:./platforms/m5stackchan_cores3 -t build -f rgb565be \
- "$MATCHDAY_DIR/mod/manifest.json"
-python3 -m esptool --chip esp32s3 --before default-reset --after hard-reset \
- write-flash 0xDF0000 "$MODDABLE/build/bin/esp32/debug/mod/mod.xsa"
-```
-
-`0xDF0000` is the `xs` partition offset from the partition patch; esptool
-verifies the write and the host mounts the archive on the reset that follows.
-Verify the mod from the watcher computer:
-
-```sh
-curl "http://$STACKCHAN_HOST/health"
-curl "http://$STACKCHAN_HOST/api/status"
-```
-
-### 4. Configure and start the watcher
-
-```sh
-cp "$MATCHDAY_DIR/config/kalshi_watchlist.example.json" \
- "$MATCHDAY_DIR/config/kalshi_watchlist.json"
-```
-
-Edit the copied file and check these values:
+## How to use
-- `stackchan_host` matches `$STACKCHAN_HOST` or the device's LAN IP.
-- `stackchan_transport` is `http`; the phone setup relay does not work over
- serial transport.
-- `setup_server.enabled` is `true`.
-- Port `8788` is free. Its default `127.0.0.1` binding keeps the optional local
- admin page on the watcher computer only.
+1. **Start the watcher.** Keep the watcher computer awake and the `--watch`
+ process running. The phone, computer, and Stack-chan must share a trusted
+ LAN.
+2. **Open Match Setup.** Double-tap the touch bar on Stack-chan's head, or
+ briefly press Power, to show the setup QR. Scan it with your phone.
+3. **Choose what to watch.** Select a match, supported team (or Neutral),
+ optional pregame position (or No position), and commentary style, then tap
+ **Start watching**.
+4. **Wait for confirmation.** The watcher validates the ESPN/Kalshi pairing,
+ updates its configuration, and acknowledges the device. The new selection
+ takes effect without restarting the watcher or device. Changing only the
+ commentary style does not replay old events.
+
+During the match, hold the top touch bar for about one second to toggle the
+boss-key mute. Speech, tones, celebrations, and alert lights stop while the
+probability bar, balloons, and ticker continue updating. If no fixture is
+available, paste a Kalshi event URL or ticker into Match Setup to follow its
+four most-traded markets in standalone mode.
+
+See [Configuration and operation](docs/configuration.md) for daily prompts,
+language, support and position behavior, commentary styles, mute controls, and
+standalone-market details.
-Validate the JSON, then keep the watcher running:
+## Features
-```sh
-python3 -m json.tool "$MATCHDAY_DIR/config/kalshi_watchlist.json"
-python3 "$MATCHDAY_DIR/tools/stackchan_kalshi_watch.py" \
- --config "$MATCHDAY_DIR/config/kalshi_watchlist.json" --watch
-```
+- Persistent two-team probability bar, flags, and a bottom market ticker.
+- Reactions to goals, cards, substitutions, close misses, match phases, and
+ final results.
+- Casual, balanced, and professional commentary generated from shared event
+ facts, with compact on-device balloons.
+- Separate support and position perspectives, including explicit conflict and
+ uncertain-event wording.
+- Phone-based match setup with live Chinese/English switching and hot reload.
+- Optional LAN TTS; visual feedback and tone patterns still work without it.
-The example `KXEXAMPLE-...` tickers are intentional placeholders. Until you
-select a live match from the phone or replace them with real open tickers, the
-watcher may report them as missing. `--dry-run` only suppresses device writes;
-it still queries the public APIs and is not an offline installation check.
+## Quick start
-### 5. Optional: enable LAN speech
+### Already installed
-In a second terminal on macOS, re-export the repository path and start the
-included server in the foreground so errors remain visible:
+Update the repository and restart the watcher. Support/position wording,
+position-based market selection, and the global player catalog are watcher-only
+changes; an installed 1.5.0 mod and the official host can stay in place.
```sh
export MATCHDAY_DIR="$HOME/src/stackchan-matchday"
-python3 "$MATCHDAY_DIR/tools/stackchan_tts_server.py" --host 0.0.0.0 --port 8787
+cd "$MATCHDAY_DIR"
+git pull
+python3 tools/stackchan_kalshi_watch.py \
+ --config config/kalshi_watchlist.json --watch
```
-Leave that terminal running. In another terminal, verify it and point the
-device at the watcher's LAN address, not `127.0.0.1`:
+Deploy `config/espn_player_catalog.json` with the watcher checkout. For exact
+upgrade boundaries, see the bilingual
+[Matchday MOD 1.5.0 notes](docs/releases/1.5.0.md) and
+[1.4.0 commentary-style notes](docs/releases/1.4.0.md).
-```sh
-curl "http://127.0.0.1:8787/health"
-export STACKCHAN_HOST=stackchan.local
-export WATCHER_HOST=192.168.1.20
-curl --request POST --data-binary "tts host $WATCHER_HOST:8787" \
- "http://$STACKCHAN_HOST/api/command"
-curl --request POST --data-binary "say Matchday ready" \
- "http://$STACKCHAN_HOST/api/command"
-```
+### First installation
-Allow inbound TCP `8787` through the computer firewall. Use `say -v '?'` to
-list installed macOS voices; override defaults with
-`STACKCHAN_TTS_ZH_VOICE`, `STACKCHAN_TTS_EN_VOICE`, and
-`STACKCHAN_TTS_RATE`. Without a reachable TTS server, the mod automatically
-falls back to short tone patterns.
+You need a CoreS3 Stack-chan with 16 MB flash, Python 3.10+, Node.js 20+,
+Moddable SDK, ESP-IDF, a USB data cable, and a phone and watcher computer on
+the same trusted LAN. macOS is required only for the included `say`-based TTS
+server.
-## How to use
+Follow [Getting started](docs/getting-started.md) for the tested upstream
+revision, one-time host preparation, QR generation, mod installation, watcher
+configuration, TTS, and verification. Host partition and optional CJK-font
+patch details live in [host/README.md](host/README.md).
-1. **Start the watcher.** Keep the watcher computer awake and the `--watch`
- process running. The phone, computer, and Stack-chan must share a trusted
- LAN.
-2. **Wake the setup QR.** Double-tap the three-zone touch bar on top of
- Stack-chan's head. Tap once while the QR is visible to hide it; it also
- closes after 90 seconds. A short Power-button press toggles it on the pinned
- host firmware.
-3. **Scan and choose.** Open the QR on a phone, choose 中文 or English, select a
- match, your team (or Neutral), an optional pregame position (or No
- position), and a commentary style, then tap **Start watching**. The
- Match Setup enables the Kalshi market for the selected position team; with
- No position, it enables the first market in the match pair. The position is
- still only a manual preference and no account is read.
-4. **Wait for confirmation.** The page first says that it is waiting for the
- watcher. The watcher validates the ESPN/Kalshi pairing, atomically updates
- the local configuration, hot-reloads, and acknowledges the device. No
- watcher or device restart is needed.
-5. **Watch together.** During the match, Stack-chan updates flags and
- probabilities and reacts to score and commentary events with its screen,
- face, lights, head, tones, and optional speech.
-6. **Watch any market.** If no fixture is available, paste a Kalshi event URL
- or ticker into the same page. The event's four most-traded markets appear in
- the bottom ticker; fixture-only flags, probability bar, and ESPN commentary
- are temporarily disabled.
-7. **Mute for meetings (boss key).** Hold the top touch bar for about one
- second to toggle mute: speech, tones, celebrations, and alert lights stop,
- while the probability bar, balloons, and ticker keep updating silently. A
- corner `MUTE` / `静音` badge stays visible until unmuted. `mute on 60` over
- `/api/command` (or the control panel's *mute 60m* button) silences a timed
- meeting and announces when sound returns; an indefinite mute survives
- reboots.
-
-
-
-
- 
- Scan the on-device QR code to open the local setup page (example LAN address shown).
- |
-
- 
- Hold the top touch bar to mute speech, tones, motion, and alert lights while visual updates continue.
- |
-
-
-
-Once per local day, the watcher can proactively ask you to scan and choose a
-match. Configure this with `setup_server.daily_prompt_hour` (`-1` disables it),
-`prompt_minutes_before`, `quiet_hours`, and `lookahead_days`.
-
-## Configuration and language
-
-The setup page switches all labels immediately and persists its language on
-the device. Applying a match also switches watcher-generated speech and
-balloons. The top-level `language` value may be `zh` or `en`.
-
-User-facing text accepts either a legacy string or a localized object:
-
-```json
-{
- "language": "en",
- "mac_voice": {"zh": "Tingting", "en": "Samantha"},
- "espn": {
- "commentary_style": "balanced",
- "label": {"zh": "法国 vs 摩洛哥", "en": "France vs Morocco"},
- "team_names": {
- "France": {"zh": "法国", "en": "France"}
- }
- },
- "markets": [{
- "ticker": "KXEXAMPLE-FRA",
- "label": {"zh": "法国晋级", "en": "France to advance"}
- }]
-}
-```
+After installation, starting the watcher normally requires only:
-The watcher first consults the global `config/espn_player_catalog.json`, keyed
-by stable ESPN athlete IDs such as `espn:362150`. Entries contain formal
-Chinese/English names, manually verified casual nicknames, a featured-player
-flag, and an optional goal chant. `balanced`, `professional`, and every device
-balloon always use the formal name; only casual speech may use a friendly
-nickname. Legacy per-match `player_names` and `star_chants` remain supported
-and override the catalog, and custom goal-signal speech keeps the same
-localized-leaf format.
-
-An unmatched player falls back to ESPN's original name; the watcher neither
-transliterates nor invents a nickname. When an ESPN roster first appears or
-changes, the watcher logs named and featured coverage plus the raw-name
-fallback list so the catalog can be completed before a later match without
-blocking commentary.
-
-### Commentary styles
-
-`espn.commentary_style` is a persistent global preference and accepts exactly
-`casual`, `balanced`, or `professional`. Configurations that omit it continue
-to use `balanced`.
-
-| Value | Voice | Device balloon |
-| --- | --- | --- |
-| `casual` | Friendly co-watching language, while retaining every core fact | Compact event summary |
-| `balanced` | Clear, natural narration compatible with the previous behavior | Compact event summary |
-| `professional` | Core facts plus reliably parsed ESPN details and football terminology | Compact event summary; detail stays in speech |
-
-Every style keeps the match time, event type, team, required players, event
-result, and current score. Penalties, cards, and substitutions keep all
-necessary participants. Suspected goals and events awaiting commentary
-confirmation remain explicitly uncertain in every style. Professional speech
-may add an assist or cross, shot type and body part, field or goal location,
-goalkeeper save, set-piece position, or substitution/injury reason only when
-ESPN explicitly supplies it and the watcher can parse it reliably; it never
-reads the raw English commentary aloud or guesses missing detail.
-
-Across all three styles, balloons normally stay in the form “time +
-player/team + event + score”; only punctuation and a small amount of wording
-vary.
-
-The supported team sets the everyday co-watching point of view. Routine shots,
-saves, and corners do not force a position aside. Major ESPN events such as
-goals, penalties, red cards, and results add whether the declared position
-benefits or comes under pressure; aligned support and position are combined,
-while a conflict explicitly separates the emotional and position outcomes.
-Only a Kalshi market that Match Setup has matched to the selected position and
-marked `tracks_position` uses position wording for price moves or suspected
-goals. Suspected events still say “if confirmed” and await commentary.
-
-The selected style also applies to match phases and results, Kalshi market
-jumps, and suspected-goal alerts. It changes wording only: TTS voice and rate,
-sound effects, celebrations, expressions, lights, priorities, and alert
-switches are unchanged. You can switch it from either setup page during a
-match; newly generated alerts use the new style immediately without replaying
-old ESPN events or resetting market baselines, queues, or polling state.
-
-Serial transport is intended for direct command/control only. Install
-`pyserial` and configure `stackchan_serial_port` if you choose it; phone setup,
-device status detection, and the options/pending/ack relay require HTTP.
-
-## Troubleshooting
-
-- **The setup page has no matches:** confirm the watcher is running with setup
- enabled and can reach both public APIs. Only open Kalshi events in the
- configured `kalshi_series_ticker` that match an ESPN `pre` or `in` fixture
- by both team names are listed.
-- **The page stays on “waiting for watcher”:** the watcher is stopped, is using
- serial transport, failed to bind local port `8788`, or cannot reach the
- device on TCP `80`. Check its terminal output first.
-- **The QR opens the wrong address:** regenerate
- `mod/assets/setup/setup-qr.png` and reinstall the mod. The bitmap is static;
- changing the on-screen URL does not rewrite its modules.
-- **Chinese renders as boxes:** apply the optional font patch, prepare a CJK
- TTF, then rebuild and reflash the host before reinstalling the mod.
-- **There is no speech:** first look for the corner `MUTE` / `静音` badge or
- run `mute status` — a long-press on the top bar toggles the boss key. Then
- check the TTS `/health` response, computer firewall, and `tts status`
- through `/api/command`. Visual effects and tone fallback do not depend on
- TTS.
-- **`npm run mod` hangs at "Installing mod...":** the xsbug-protocol install
- stalled; a killed install leaves the mod missing until reinstalled. Quit
- xsbug, then use the build + `esptool write-flash 0xDF0000` path from the
- install section — it needs no debugger and verifies the write.
-- **Device freezes and drops off the network while xsbug is attached:** xsbug
- pauses the whole runtime at exception breakpoints, stopping Wi-Fi, touch,
- and timers. Detach `serial2xsbug`/xsbug for unattended running; to capture
- logs without freezing, use `$MODDABLE/tools/xsbug-log` instead.
-- **`stackchan.local` does not resolve:** use the device's LAN IP and regenerate
- the QR. Reserve that address in DHCP so the static QR stays valid.
-- **Markets show as missing:** the example values are placeholders. Select a
- live fixture from the setup page or replace them with open tickers returned
- by `python3 "$MATCHDAY_DIR/tools/stackchan_kalshi_watch.py" discover --query QUERY`.
-
-## Device API
-
-`GET /api/help` lists the plain-text command surface accepted by
-`POST /api/command`:
-
-```text
-pkbar es 62 AA151B be 38 EF3340
-balloon temp 8000 Spain scores!
-voice favorite-goal Number seven scores!
-celebrate goal 170 21 27
-celebrate say 170 21 27 Goal!
-celebrate result win 170 21 27 Full time
-setup show http://stackchan.local/setup
-say Hello
-mute on 60 · mute off · mute status
-face happy · look 8 -2 · idle look on · light flash 0 85 164
+```sh
+python3 tools/stackchan_kalshi_watch.py \
+ --config config/kalshi_watchlist.json --watch
```
-`GET /api/status` reports the mod version, probability bar, TTS, power,
-network, and setup-trigger counters. `POST /api/control` accepts JSON actions.
-The watcher-facing setup endpoints are `/api/match-setup`, `/options`,
-`/apply`, `/ack`, `/pending`, and `/language`.
-
-Commentary style has a dedicated relay path. The device accepts
-`POST /api/match-setup/style` with
-`{"commentary_style":"casual|balanced|professional"}` and forwards it through
-the existing pending/ack flow. The watcher-hosted admin service accepts the
-same body at `POST /api/setup/style`; `GET /api/setup/status` reports the
-effective value.
-
-## Development
-
-Run the complete local test suite from this repository:
+## System design
-```sh
-cd "$MATCHDAY_DIR"
-python3 -m unittest discover -s tools -p 'test_*.py'
-node tools/test_stackchan_mod_web_behavior.mjs
-```
+
-Build a mod archive without installing it from the upstream `firmware/`
-directory:
+Kalshi and ESPN are read-only inputs to the Python watcher. The watcher
+discovers fixtures, validates market pairing, parses events into shared facts,
+renders the selected commentary style, and sends options, acknowledgements,
+display, speech, and reaction commands to the Matchday mod.
-```sh
-cd "$STACKCHAN_DIR/firmware"
-mcrun -d -m -p esp32:./platforms/m5stackchan_cores3 -t build -f rgb565be \
- "$MATCHDAY_DIR/mod/manifest.json"
-```
+The mod hosts the phone setup page, forwards pending settings, and plays the
+resulting commands. Commentary styling does not modify the official host
+firmware or TTS module. Optional speech uses a LAN `/say?text=...` service that
+returns a 24 kHz mono 16-bit PCM WAV response.
-Replay the France–Morocco ESPN history through the same alert parser. Preview
-is the default; stop the continuous watcher before opting into execution:
+The device stores a pending phone selection until the watcher validates it,
+atomically updates the local JSON configuration, hot-reloads, and acknowledges
+the device. The phone, watcher, TTS server, and Stack-chan must remain on the
+same trusted LAN; the watcher-hosted `:8788/setup` page is a local fallback,
+not the primary QR flow.
-```sh
-python3 "$MATCHDAY_DIR/tools/stackchan_match_replay.py" \
- --config "$MATCHDAY_DIR/config/kalshi_watchlist.json"
-python3 "$MATCHDAY_DIR/tools/stackchan_match_replay.py" \
- --config "$MATCHDAY_DIR/config/kalshi_watchlist.json" --language en
-python3 "$MATCHDAY_DIR/tools/stackchan_match_replay.py" \
- --config "$MATCHDAY_DIR/config/kalshi_watchlist.json" --execute
-```
+## Documentation
+
+| Guide | Contents |
+| --- | --- |
+| [Getting started](docs/getting-started.md) | Requirements, host/mod installation, watcher, TTS, verification |
+| [Configuration and operation](docs/configuration.md) | Language, perspectives, player catalog, styles, mute, standalone mode |
+| [Device API](docs/device-api.md) | Commands, status, control, and Match Setup relay endpoints |
+| [Development](docs/development.md) | Repository map, tests, archive builds, and replay tooling |
+| [Host preparation](host/README.md) | Partition and optional CJK-font patches |
+| [Commentary styles PRD](docs/commentary-styles-prd.md) | Product rules and implementation contract |
+| [Release notes](docs/releases/1.5.0.md) | Version-specific upgrade boundaries |
+| [GitHub Wiki](https://github.com/xymeow/stackchan-matchday/wiki) | Human- and agent-friendly navigation, FAQ, and field debugging |
+
+English guides link to their Chinese counterparts at the top of each page.
+Repository documentation is the versioned source of truth; the Wiki is the
+short operational index and troubleshooting layer.
+
+### Compatibility notes
+
+- The watcher uses Python's standard library for the default HTTP workflow;
+ serial transport additionally requires `pyserial`.
+- Phone setup, device status detection, and the pending/ack relay require HTTP.
+- The example `KXEXAMPLE-...` tickers are placeholders. Select a live fixture
+ from Match Setup or replace them with real open markets.
+- ESPN endpoints are unofficial. Missing or ambiguous upstream detail is
+ ignored rather than translated into a guessed football fact.
+- Commentary style can change during a match without resetting ESPN history,
+ market baselines, queues, or polling state. API compatibility is documented
+ in the [Device API guide](docs/device-api.md).
+
+### For maintainers and AI agents
+
+- Repository docs are versioned with the code and are the source of truth for
+ build parameters, interfaces, and configuration behavior.
+- The Wiki is for environment-specific field knowledge; do not keep partition
+ offsets or version-bound commands only in the Wiki.
+- The watcher owns data parsing and copy; the mod relays configuration and
+ performs device feedback; the host provides partitions and fonts.
+- When behavior changes, update the relevant guide, example configuration, and
+ release notes instead of expanding the README with implementation details.
## Security
@@ -512,7 +181,7 @@ The device HTTP API is unauthenticated and CORS-open by design. Use it only on
a trusted LAN; do not port-forward TCP `80`, `8787`, or `8788`. The fallback AP
(`StackChan-Matchday` / `stackchan`) appears only when the device has no Wi-Fi
credentials. Configure Wi-Fi with the official
-[Stack-chan web console](https://stack-chan.github.io/web/) over BLE before
+[Stack-chan web console](https://stack-chan.github.io/stack-chan/web/preference/) over BLE before
starting the watcher.
## Credits and licenses
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 1e96a0a..beee47a 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -3,31 +3,15 @@
[English](README.md) | [简体中文](README.zh-CN.md)
Stack-chan Matchday 是一个轻量的
-[Stack-chan](https://github.com/stack-chan/stack-chan) Mod 与 Python
-局域网 watcher,可把 CoreS3 机器人变成世界杯陪看搭子:屏幕持续显示双方在
-Kalshi 晋级市场中的概率,跟随 ESPN 比分与文字直播,并通过语音、气泡、灯光和
-安全幅度的头部动作做出反应;下一场看什么,可以直接用手机选择。
+[Stack-chan](https://github.com/stack-chan/stack-chan) Mod 与 Python 局域网
+watcher,可把 CoreS3 机器人变成世界杯陪看搭子:屏幕持续显示双方在 Kalshi
+晋级市场中的概率,跟随 ESPN 比分与文字直播,并通过语音、气泡、灯光和安全幅度的
+头部动作做出反应。下一场看什么,直接用手机选择。
> [!IMPORTANT]
> 这是一个只读的比赛陪看工具。它不会交易、不会访问 Kalshi 账户,也不提供投注
-> 建议。`position_team` 只是手动填写的偏好:watcher 会在重大 ESPN 事件、赛果以及
-> 已确认对应持仓的 Kalshi 市场变化中说明利好或利空,但不会读取真实账户。Kalshi
-> 数据来自其公开 REST API;ESPN 数据来自可公开访问但未正式文档化的接口,可能
-> 变更,也可能落后于电视直播。
-
-## 功能
-
-- 常驻的双方概率条、球队旗帜与底部市场 ticker。
-- 对进球、红黄牌、换人、险情、比赛状态和终场结果做出反应。
-- 三档可即时切换的播报语气:朋友陪看的 `casual`、自然播报的 `balanced`,以及
- 补充 ESPN 明确信息的专业解说 `professional`。
-- 支持队与持仓队分开建模:普通攻防站在支持队视角,重大事件再自然补充持仓影响,
- 两者方向相反时也会明确表达冲突。
-- 由 Stack-chan 自己托管的手机设置页,可即时切换中英文。
-- 双击头顶触摸条或短按 Power 键唤出设置二维码。
-- 自动发现比赛、自适应轮询、配置热更新与免打扰时段。
-- 可选局域网 TTS;没有 TTS 时,视觉反馈和提示音仍然可用。
-- 没有比赛时,也可单独跟踪一个事件中最多四个活跃市场。
+> 建议。`position_team` 只是手动填写的偏好;Kalshi 数据来自公开 REST API,ESPN
+> 数据来自可公开访问但未正式文档化的接口,可能变更,也可能落后于电视直播。
## 实机效果
@@ -59,409 +43,143 @@ Kalshi 晋级市场中的概率,跟随 ESPN 比分与文字直播,并通过
-## 系统设计
-
-
-
-Kalshi 和 ESPN 只作为 Python watcher 的只读数据源。watcher 向 Matchday Mod
-发送可选比赛、显示命令和设置确认。比赛事件先在 watcher 中解析成三档共享的
-结构化事实,再按当前语气生成文案;Mod 只负责转发设置并执行显示、语音和动作命令,
-不为此修改官方 host firmware 或 TTS 模块。手机访问的是 Stack-chan 自己的 `/setup`
-页面:设备先保存待处理选择,watcher 再完成校验、原子更新本地 JSON 配置、热重载,
-最后确认设备。播报时,Stack-chan 主动向可选局域网 TTS 服务请求
-`/say?text=...`,并接收 24 kHz、单声道、16-bit PCM WAV。
-
-手机、watcher 电脑、TTS 服务和 Stack-chan 必须处于同一个可信局域网。
-watcher 电脑上的 `:8788/setup` 只是可选的本机管理后备页,不是扫码主流程。
-
-## 仓库结构
-
-- `mod/` — 设备端 Mod,由多个小型 JS 模块以及旗帜、二维码资源组成。
-- `host/` — CoreS3 必需的分区补丁、可选 CJK 字体补丁和字体准备脚本。这些只是
- 构建/资源层改动,不修改上游 runtime JS/C 源码。
-- `tools/` — watcher、本地设置服务、macOS TTS 服务、比赛回放、串口辅助工具、
- 素材生成器与测试。默认 HTTP 流程只用 Python 标准库;串口模式另需
- `pyserial`。
-- `config/` — watcher 示例配置、旗帜包定义与全局 ESPN 球员目录。
-- `docs/` — [三档播报语气 PRD](docs/commentary-styles-prd.md) 与各版本升级说明,
- 包括双语的 [Matchday MOD 1.5.0 说明](docs/releases/1.5.0.md)与
- [1.4.0 说明](docs/releases/1.4.0.md)。
-
-## 环境要求
-
-- 基于 CoreS3、配备 16 MB Flash 的 Stack-chan,以及一根可传数据的 USB 线。
-- 构建电脑上安装 Git、Python 3.10+、Node.js 20+(上游已测试 Node.js 22)、
- npm 和 `xz`。
-- Moddable SDK 与 ESP-IDF;下文的上游 `xs-dev` 命令会负责安装并检查。
-- 手机、watcher 电脑和 Stack-chan 位于同一个可信局域网。
-- 只有生成设备专属二维码时才需要 `qrencode`。
-- 仓库自带的 `say` TTS 服务仅支持 macOS;其他系统可以不启用语音,或自行提供
- 兼容的 `/say` WAV 服务。
-
-下列命令适用于 macOS/Linux shell。先设置两个绝对路径,后续安装始终沿用:
-
-```sh
-mkdir -p "$HOME/src"
-export MATCHDAY_DIR="$HOME/src/stackchan-matchday"
-export STACKCHAN_DIR="$HOME/src/stack-chan"
-```
-
-## 安装
-
-如果已经在使用旧版 Matchday,要升级 1.4.0 三档播报,只需更新本 watcher 仓库并
-重新安装 Matchday Mod;无需重新构建或烧录官方 host firmware,也无需更换 TTS
-模块。详见 [1.4.0 版本说明](docs/releases/1.4.0.md)。
-
-支持/持仓视角文案、按持仓选择市场和全局球员目录属于 watcher-only 更新:已有
-1.5.0 Mod 可直接沿用。请同步更新 watcher 代码与
-`config/espn_player_catalog.json`,然后重启 Python watcher 进程;无需刷写 Mod 或 host。
-
-### 1. 克隆仓库并准备上游构建环境
-
-```sh
-git clone https://github.com/xymeow/stackchan-matchday.git "$MATCHDAY_DIR"
-git clone https://github.com/stack-chan/stack-chan.git "$STACKCHAN_DIR"
-
-cd "$STACKCHAN_DIR"
-git switch --detach ded5ca94ef50411aec213b85a23d1afe72d4c29e
-
-cd "$STACKCHAN_DIR/firmware"
-npm ci
-npm run setup -- --device=esp32
-npm run doctor
-```
-
-这个固定 commit 是本仓库补丁测试过的基线。只有在 `npm run doctor` 把 `esp32`
-列为 supported target 后,才继续构建。若 `xs-dev` 提示平台相关依赖,请查看上游
-[环境配置说明](https://github.com/stack-chan/stack-chan/blob/dev/v1.0/firmware/docs/getting-started.md)。
-
-### 2. 应用补丁并烧录一次 host
-
-每个新的 CoreS3 host checkout 都必须应用分区补丁:
-
-```sh
-cd "$STACKCHAN_DIR"
-git am "$MATCHDAY_DIR/host/patches/0001-Add-xs-mod-partition-for-M5StackChan-CoreS3.patch"
-```
-
-要显示中文标签和气泡,还需应用字体补丁并准备一份支持 CJK 的 TTF。纯英文安装
-可以跳过下面两条命令,并把 watcher 的语言设为 `en`:
-
-```sh
-git am "$MATCHDAY_DIR/host/patches/0002-Add-optional-StackChanCN-24-GB2312-font-resource.patch"
-python3 "$MATCHDAY_DIR/host/prepare_cjk_font.py" "$STACKCHAN_DIR"
-```
-
-构建并烧录 host:
-
-```sh
-cd "$STACKCHAN_DIR/firmware"
-export PATH="$PWD/node_modules/.bin:$PATH"
-mcconfig -d -m -p esp32:./platforms/m5stackchan_cores3 -t deploy \
- "$PWD/stackchan/manifest_m5stackchan_cores3.json"
-```
-
-字体选择与补丁细节见 [host/README.zh-CN.md](host/README.zh-CN.md)。
-
-### 3. 先生成二维码,再构建并安装 Mod
-
-二维码是编译进 Mod 的静态图片,运行时不会自动重画。请先为设备设置稳定的 DHCP
-保留地址、IP 或可解析的 mDNS 名称,然后在安装 Mod 前生成二维码。UI 会读取 PNG
-的实际尺寸;请让宽和高都不超过 168 px,以便标题和 URL 仍能完整显示:
-
-```sh
-export STACKCHAN_HOST=stackchan.local
-qrencode -s 4 -m 1 -o "$MATCHDAY_DIR/mod/assets/setup/setup-qr.png" \
- "http://$STACKCHAN_HOST/setup"
-file "$MATCHDAY_DIR/mod/assets/setup/setup-qr.png"
-```
-
-如果 `file` 显示任一边超过 168 px,请改用 `-s 3` 重新生成。然后构建并安装
-Mod:
-
-```sh
-cd "$STACKCHAN_DIR/firmware"
-npm run mod --target=esp32:./platforms/m5stackchan_cores3 -- -f rgb565be \
- "$MATCHDAY_DIR/mod/manifest.json"
-```
-
-CoreS3 必须使用 `-f rgb565be`,否则旗帜颜色会发生字节序错位。
-
-`npm run mod` 走 xsbug 调试协议安装,需要 xsbug 监听端口,且设备忙时可能在写入
-中途卡死(卡死会让 Mod 失效直到重装)。更稳妥的免调试器方案是只构建、然后把归
-档直接写进 `xs` 分区:
-
-```sh
-mcrun -d -m -p esp32:./platforms/m5stackchan_cores3 -t build -f rgb565be \
- "$MATCHDAY_DIR/mod/manifest.json"
-python3 -m esptool --chip esp32s3 --before default-reset --after hard-reset \
- write-flash 0xDF0000 "$MODDABLE/build/bin/esp32/debug/mod/mod.xsa"
-```
-
-`0xDF0000` 是分区补丁中 `xs` 分区的偏移;esptool 自带写入校验,复位后 host 直接
-挂载新归档。随后从 watcher 电脑验收 Mod:
-
-```sh
-curl "http://$STACKCHAN_HOST/health"
-curl "http://$STACKCHAN_HOST/api/status"
-```
-
-### 4. 配置并启动 watcher
-
-```sh
-cp "$MATCHDAY_DIR/config/kalshi_watchlist.example.json" \
- "$MATCHDAY_DIR/config/kalshi_watchlist.json"
-```
-
-编辑复制出的文件,确认:
-
-- `stackchan_host` 与 `$STACKCHAN_HOST` 或设备局域网 IP 一致。
-- `stackchan_transport` 是 `http`;手机设置中继不支持串口模式。
-- `setup_server.enabled` 是 `true`。
-- 端口 `8788` 未被占用。默认绑定 `127.0.0.1`,因此可选的本机管理页不会暴露到
- 局域网。
-
-检查 JSON 后,让 watcher 持续运行:
-
-```sh
-python3 -m json.tool "$MATCHDAY_DIR/config/kalshi_watchlist.json"
-python3 "$MATCHDAY_DIR/tools/stackchan_kalshi_watch.py" \
- --config "$MATCHDAY_DIR/config/kalshi_watchlist.json" --watch
-```
-
-示例中的 `KXEXAMPLE-...` 是有意保留的占位 ticker。在手机选择真实比赛或手动
-换成开放 ticker 之前,watcher 可能报告它们 missing。`--dry-run` 只会阻止写入
-设备,仍会访问公开 API,不能当作离线安装验收。
-
-### 5. 可选:启用局域网语音
-
-在 macOS 的第二个终端里重新导出仓库路径,再以前台方式启动自带服务,便于直接
-看到错误:
-
-```sh
-export MATCHDAY_DIR="$HOME/src/stackchan-matchday"
-python3 "$MATCHDAY_DIR/tools/stackchan_tts_server.py" --host 0.0.0.0 --port 8787
-```
-
-保持这个终端运行。在另一个终端中先验收服务,再让设备连接 watcher 电脑的局域网
-地址,而不是 `127.0.0.1`:
-
-```sh
-curl "http://127.0.0.1:8787/health"
-export STACKCHAN_HOST=stackchan.local
-export WATCHER_HOST=192.168.1.20
-curl --request POST --data-binary "tts host $WATCHER_HOST:8787" \
- "http://$STACKCHAN_HOST/api/command"
-curl --request POST --data-binary "say 比赛日准备好了" \
- "http://$STACKCHAN_HOST/api/command"
-```
-
-请在电脑防火墙中允许 TCP `8787` 入站。用 `say -v '?'` 查看 macOS 已安装的
-声音;可通过 `STACKCHAN_TTS_ZH_VOICE`、`STACKCHAN_TTS_EN_VOICE` 和
-`STACKCHAN_TTS_RATE` 覆盖默认值。TTS 不可达时,Mod 会自动回退到短提示音。
-
## 如何使用
-1. **先启动 watcher。** 保持 watcher 电脑唤醒,并让 `--watch` 进程持续运行;
- 手机、电脑和 Stack-chan 必须在同一个可信局域网。
-2. **摸头唤出设置码。** 双击 Stack-chan 头顶的三段触摸条,屏幕会显示设置二维码;
- 显示时轻点一次可关闭,90 秒后也会自动关闭。在固定的 host firmware 上,短按
- Power 键同样可以开关二维码。
-3. **扫码选择比赛。** 用手机打开二维码,选择中文或 English、要看的比赛、支持
- 球队(也可中立)、可选的赛前持仓(也可“没买”)以及播报语气,再点“开始
- 看球”。有持仓时,Match Setup 会启用该队对应的 Kalshi 市场;选择“没买”时
- 默认启用比赛双方列表中的第一个市场。持仓仍只是手动偏好,系统不会读取账户。
-4. **等待确认。** 页面会先显示“已提交,等待 watcher”。watcher 会校验 ESPN 与
- Kalshi 的双方匹配,原子更新本地配置、热重载并确认设备;无需重启 watcher 或
- Stack-chan。
-5. **开始共看。** 比赛期间,Stack-chan 会更新旗帜和概率,并用屏幕、表情、灯光、
- 头部、提示音与可选语音响应比分和文字直播事件。
-6. **没有球赛也能看盘。** 在同一页面粘贴 Kalshi event URL 或 ticker;该事件中
- 成交最活跃的最多四个市场会显示在底部 ticker,比赛专属的旗帜概率条和 ESPN
- 播报会暂时关闭。
-7. **开会一键静音(老板键)。** 长按头顶触摸条约一秒即可切换静音:语音、音效、
- 庆祝动作和警报闪灯全部停止,概率条、气泡和 ticker 继续无声更新,屏幕角落
- 会常驻 `静音` / `MUTE` 标签直到恢复。也可用 `/api/command` 发送
- `mute on 60`(或控制面板的“mute 60m”按钮)定时静音一场会议,到点自动恢复
- 并气泡提示;不限时静音重启后依然保留。
+1. **启动 watcher。** 保持 watcher 电脑唤醒,并让 `--watch` 进程持续运行;手机、
+ 电脑和 Stack-chan 必须位于同一个可信局域网。
+2. **唤出设置码。** 双击头顶触摸条或短按 Power 键,屏幕会显示设置二维码;轻点
+ 一次可关闭,90 秒后也会自动关闭。
+3. **用手机选择。** 扫码后选择语言、比赛、支持队、可选持仓和播报语气,再点
+ “开始看球”。页面显示 watcher 已确认后,配置立即生效,无需重启。
+4. **一起看球。** Stack-chan 会持续更新旗帜、概率和 ticker,并响应比分、文字直播
+ 与盘口变化。
+
+长按头顶触摸条约一秒可切换静音:语音、音效、庆祝动作和警报灯停止,概率条、气泡
+和 ticker 继续更新。没有球赛时,也可在同一页面粘贴 Kalshi event URL 或 ticker,
+单独跟踪最多四个活跃市场。更多选项见[配置与播报](docs/configuration.zh-CN.md)。

- 用手机扫描设备二维码,打开局域网设置页(图中为示例地址)。
+ 扫描设备二维码,打开局域网设置页。
|

- 长按头顶触摸条进入静音;语音、动作和警报灯停止,视觉信息继续更新。
+ 长按进入静音;视觉信息继续更新。
|
-watcher 还可以每天主动提醒你扫码选比赛。相关配置为
-`setup_server.daily_prompt_hour`(`-1` 关闭)、`prompt_minutes_before`、
-`quiet_hours` 和 `lookahead_days`。
-
-## 配置与语言
-
-设置页会立即切换全部界面文案,并把语言持久化到设备。应用一场比赛后,watcher
-产生的语音和气泡也会切换。顶层 `language` 可以是 `zh` 或 `en`。
-
-面向用户的配置文本既可写成旧式字符串,也可写成中英对象:
-
-```json
-{
- "language": "en",
- "mac_voice": {"zh": "Tingting", "en": "Samantha"},
- "espn": {
- "commentary_style": "balanced",
- "label": {"zh": "法国 vs 摩洛哥", "en": "France vs Morocco"},
- "team_names": {
- "France": {"zh": "法国", "en": "France"}
- }
- },
- "markets": [{
- "ticker": "KXEXAMPLE-FRA",
- "label": {"zh": "法国晋级", "en": "France to advance"}
- }]
-}
-```
+## 功能
-watcher 会先查全局 `config/espn_player_catalog.json`。目录以稳定的 ESPN athlete
-ID(例如 `espn:362150`)为主键,保存正式中英文名、经人工核实的 casual 昵称、
-球星标记和可选进球口号。`balanced`、`professional` 与所有气泡始终使用正式名;
-只有 casual 语音可以使用目录中的亲切昵称。旧配置里的 `player_names` 和
-`star_chants` 继续有效,并优先于全局目录;自定义进球信号语音也仍支持上述本地化
-格式。
-
-无法匹配目录的球员会回退到 ESPN 原名,不音译、不猜昵称。ESPN roster 首次出现
-或发生变化后,watcher 会在日志输出目录命中数、球星数和原名回退名单,便于赛前
-补齐覆盖,而不会阻断播报。
-
-### 三档播报语气
-
-`espn.commentary_style` 是全局持久偏好,只接受 `casual`、`balanced` 或
-`professional`。旧配置缺少该字段时默认使用 `balanced`。
-
-| 配置值 | 语音 | 设备气泡 |
-| --- | --- | --- |
-| `casual` | 朋友陪看的口语表达,但完整保留核心事实 | 精炼事件摘要 |
-| `balanced` | 清楚、自然,兼容此前的播报方式 | 精炼事件摘要 |
-| `professional` | 先播核心事实,再用足球术语补充可靠解析出的 ESPN 细节 | 仍是精炼摘要,详细信息交给语音 |
-
-三档都必须保留比赛时间、事件类型、球队、必要球员、事件结果和当前比分;点球、
-红黄牌和换人也会保留必要参与者。“疑似进球”“等待文字直播确认”等不确定性在
-任何语气下都不会被省略。专业档只会补充 ESPN 明确提供且 watcher 能可靠解析的
-助攻或传中、射门方式和身体部位、场上或球门位置、门将扑救、定位球位置、换人或
-伤情原因;不会直接朗读英文原文,也不会猜测缺失细节。
-
-三档气泡原则上都保持“时间 + 球员/球队 + 事件 + 比分”,只允许标点和少量措辞
-差异。
-
-支持队决定日常陪看视角;普通射门、扑救、角球等攻防事件不会牵强讨论仓位。进球、
-点球、红牌和赛果等重大 ESPN 事件才会追加持仓利好/利空;支持与持仓同向时合并成
-一句自然反应,方向冲突时分别说明“心里支持谁”和“仓位受益或承压”。只有与所选
-持仓队对应且由 Match Setup 标记为 `tracks_position` 的 Kalshi 市场,盘口变化和
-疑似进球提醒才会使用持仓措辞;疑似事件仍保留“如果属实”和等待确认。
-
-语气也覆盖比赛阶段和赛果、Kalshi 盘口突变与疑似进球提醒,并且只改变文案:TTS
-音色和语速、音效、庆祝动作、表情、灯效、优先级与提醒开关均保持不变。比赛中可从
-任一设置页即时切换;新生成的提醒会立即使用新语气,但不会重播 ESPN 旧事件,也
-不会重置盘口基线、告警队列或轮询状态。
-
-串口模式只适合直接命令/控制。如果使用它,需要安装 `pyserial` 并配置
-`stackchan_serial_port`;手机设置、设备状态检测和 options/pending/ack 中继都
-依赖 HTTP。
-
-## 排障
-
-- **设置页没有比赛:** 确认 watcher 正以 setup enabled 状态运行,并能访问两个
- 公开 API。只有同时满足以下条件的比赛才会列出:ESPN 状态为 `pre`/`in`,并且
- 配置的 `kalshi_series_ticker` 中存在能按双方队名匹配的开放 Kalshi 事件。
-- **页面一直停在“等待 watcher”:** watcher 已停止、正在用串口模式、无法绑定
- 本机 `8788`,或无法访问设备 TCP `80`。请先看 watcher 终端日志。
-- **二维码打开了错误地址:** 重新生成 `mod/assets/setup/setup-qr.png` 并重装
- Mod。它是静态图片;仅修改屏幕上的 URL 不会改写二维码模块。
-- **中文显示成方框:** 应用可选字体补丁,准备 CJK TTF,重新构建并烧录 host,
- 然后重装 Mod。
-- **没有语音:** 先看屏幕角落有没有 `静音` / `MUTE` 标签,或运行 `mute status`
- ——长按头顶触摸条会切换老板键。再检查 TTS `/health`、电脑防火墙,以及通过
- `/api/command` 返回的 `tts status`。视觉效果和提示音回退不依赖 TTS。
-- **`npm run mod` 卡在 “Installing mod...”:** xsbug 协议安装中途卡死;被中断的
- 安装会让 Mod 失效直到重装。退出 xsbug,改用安装章节里的“构建 +
- `esptool write-flash 0xDF0000`”方案——无需调试器且自带写入校验。
-- **接着 xsbug 时设备冻结并掉网:** xsbug 在异常断点处会暂停整个运行时,WiFi、
- 触摸和定时器全部停摆。日常运行请断开 `serial2xsbug`/xsbug;要无冻结地抓日志,
- 用 `$MODDABLE/tools/xsbug-log` 无头模式。
-- **`stackchan.local` 无法解析:** 改用设备局域网 IP 并重新生成二维码;最好在
- DHCP 中为它保留地址。
-- **市场显示 missing:** 示例值只是占位符。请从设置页选择真实比赛,或用
- `python3 "$MATCHDAY_DIR/tools/stackchan_kalshi_watch.py" discover --query 关键词`
- 返回的开放 ticker 替换。
-
-## 设备 API
-
-`GET /api/help` 会列出 `POST /api/command` 接受的纯文本命令:
+- 常驻双方概率条、球队旗帜与底部市场 ticker。
+- 对进球、红黄牌、换人、险情、比赛阶段和赛果做出反应。
+- `casual`、`balanced`、`professional` 三档播报语气,可在比赛中即时切换。
+- 支持队和持仓队分开建模;重大事件会自然说明持仓利好或利空。
+- 由设备托管的中英文手机设置页,以及 watcher 的配置热更新与确认链路。
+- 自动发现比赛、自适应轮询、免打扰时段和无比赛市场跟踪。
+- 可选局域网 TTS;服务不可用时,视觉反馈和提示音仍然工作。
+- 以 ESPN athlete ID 为主键的全局球员目录;只使用人工核实的中文名和昵称。
-```text
-pkbar es 62 AA151B be 38 EF3340
-balloon temp 8000 西班牙进球了!
-voice favorite-goal 7号球员进球啦
-celebrate goal 170 21 27
-celebrate say 170 21 27 进球了!
-celebrate result win 170 21 27 比赛结束
-setup show http://stackchan.local/setup
-say 你好
-mute on 60 · mute off · mute status
-face happy · look 8 -2 · idle look on · light flash 0 85 164
-```
+## 快速开始
-`GET /api/status` 会返回 Mod 版本、概率条、TTS、电源、网络和设置触发计数器。
-`POST /api/control` 接受 JSON action。watcher 使用的设置接口为
-`/api/match-setup`、`/options`、`/apply`、`/ack`、`/pending` 和
-`/language`。
+### 环境要求
-播报语气使用独立转发链路。设备接收 `POST /api/match-setup/style`,请求体为
-`{"commentary_style":"casual|balanced|professional"}`,并通过现有 pending/ack
-流程转发;watcher 本地管理服务的对应接口为 `POST /api/setup/style`,相同请求体
-即可更新语气,`GET /api/setup/status` 会返回当前生效值。
+- 基于 CoreS3、配备 16 MB Flash 的 Stack-chan,以及一根可传数据的 USB 线。
+- Git、Python 3.10+、Node.js 20+、npm、`xz`、Moddable SDK 与 ESP-IDF。
+- 手机、watcher 电脑和 Stack-chan 位于同一个可信局域网。
+- 只有生成设备专属二维码时需要 `qrencode`;仓库自带的 TTS 服务仅支持 macOS。
-## 开发
+首次安装需要准备并烧录一次官方 host、生成设备二维码、安装 Matchday Mod,再配置
+watcher。完整、可复现的命令见[安装与升级](docs/getting-started.zh-CN.md);host 的分区
+和可选 CJK 字体步骤以 [host/README.zh-CN.md](host/README.zh-CN.md) 为准。
-在本仓库运行完整测试:
+### 首次安装路径
-```sh
-cd "$MATCHDAY_DIR"
-python3 -m unittest discover -s tools -p 'test_*.py'
-node tools/test_stackchan_mod_web_behavior.mjs
-```
+1. 在固定的上游 Stack-chan commit 上应用 `xs` 分区补丁;中文界面再加入 CJK 字体,
+ 然后烧录一次 host。
+2. 给设备保留稳定的局域网地址,生成不超过 168 px 的设置二维码,构建并安装 Mod。
+3. 复制示例 watcher 配置,填写设备地址并启动 `--watch`。
+4. 从设备扫码选择比赛;需要语音时,再启动可选的 macOS 局域网 TTS。
+
+这四步的版本绑定命令只在详细文档中维护,避免 README 与实际分区、构建参数漂移。
-在上游 `firmware/` 目录构建但不安装 Mod archive:
+已有可用的 host 与 Mod 时,启动 watcher 的最短路径是:
```sh
-cd "$STACKCHAN_DIR/firmware"
-mcrun -d -m -p esp32:./platforms/m5stackchan_cores3 -t build -f rgb565be \
- "$MATCHDAY_DIR/mod/manifest.json"
+git clone https://github.com/xymeow/stackchan-matchday.git
+cd stackchan-matchday
+cp config/kalshi_watchlist.example.json config/kalshi_watchlist.json
+# 编辑 stackchan_host,然后:
+python3 tools/stackchan_kalshi_watch.py \
+ --config config/kalshi_watchlist.json --watch
```
-可通过同一套 parser 回放法国—摩洛哥的 ESPN 历史。默认只预览命令;选择实际执行
-前,请先停止持续运行的 watcher:
+示例 ticker 是占位值;从手机设置页选择真实比赛后,watcher 会校验 ESPN 与 Kalshi
+的双方匹配并原子更新配置。旧版升级前请查看
+[Matchday Mod 1.5.0 说明](docs/releases/1.5.0.md)及[全部版本说明](docs/releases/);
+watcher-only 更新不应触发不必要的刷机。
-```sh
-python3 "$MATCHDAY_DIR/tools/stackchan_match_replay.py" \
- --config "$MATCHDAY_DIR/config/kalshi_watchlist.json"
-python3 "$MATCHDAY_DIR/tools/stackchan_match_replay.py" \
- --config "$MATCHDAY_DIR/config/kalshi_watchlist.json" --language en
-python3 "$MATCHDAY_DIR/tools/stackchan_match_replay.py" \
- --config "$MATCHDAY_DIR/config/kalshi_watchlist.json" --execute
-```
+## 系统设计
+
+
+
+Kalshi 和 ESPN 只作为 Python watcher 的只读数据源。watcher 负责解析统一的比赛
+事实、生成三档文案,并向设备发送显示、语音和动作命令;Mod 负责手机设置转发与设备
+反馈,不修改官方 host runtime 或 TTS 模块。
+
+手机访问 Stack-chan 自己的 `/setup` 页面。设备先保存待处理选择,watcher 再完成
+校验、原子更新本地 JSON、热重载并确认设备。可选的局域网 TTS 接收设备发出的
+`/say?text=...` 请求,返回 24 kHz、单声道、16-bit PCM WAV。
+
+watcher 电脑上的 `:8788/setup` 只是默认绑定回环地址的本机管理后备页,不是设备
+二维码的主流程。
+
+```text
+Kalshi + ESPN ──只读──> Python watcher ──HTTP──> Stack-chan Matchday Mod
+ │ │
+ └── 配置校验与确认 <── 手机 /setup
+ │
+ └── 可选局域网 TTS
+```
+
+## 文档
+
+| 想做什么 | 文档 |
+| --- | --- |
+| 首次安装、升级、Mod 构建、watcher 与 TTS 启动 | [安装与升级](docs/getting-started.zh-CN.md) |
+| 调整语言、语气、支持/持仓、球员目录和轮询 | [配置与播报](docs/configuration.zh-CN.md) |
+| 构建或重新烧录官方 host、加入中文字体 | [Host firmware](host/README.zh-CN.md) |
+| 调用设备命令、状态与 Match Setup 接口 | [设备 API](docs/device-api.zh-CN.md) |
+| 运行测试、构建归档和回放比赛 | [开发指南](docs/development.zh-CN.md) |
+| 理解三档语气的产品规则 | [播报语气 PRD](docs/commentary-styles-prd.md) |
+| 查看版本差异与升级边界 | [版本说明](docs/releases/) |
+| 处理联网、二维码、TTS、xsbug 等常见问题 | [排障与 FAQ(Wiki)](https://github.com/xymeow/stackchan-matchday/wiki) |
+
+### 兼容性说明
+
+- watcher 的默认 HTTP 工作流只使用 Python 标准库;串口传输还需要 `pyserial`。
+- 手机设置、设备状态检测和 pending/ack 转发链路依赖 HTTP。
+- 示例中的 `KXEXAMPLE-...` ticker 只是占位值;请从手机设置页选择实时比赛,或替换为
+ 仍在交易的真实市场。
+- ESPN 接口并非正式 API;缺失或含糊的上游细节会被忽略,不会猜测足球事实。
+- 比赛中切换语气不会重置 ESPN 历史、盘口基线、队列或轮询状态;API 兼容性见
+ [设备 API](docs/device-api.zh-CN.md)。
+
+### 给维护者和 AI agent
+
+- 仓库内文档与代码一起版本化,是构建参数、接口和配置行为的事实来源。
+- Wiki 用于环境相关的排障经验;不要只在 Wiki 保存分区偏移或版本绑定的命令。
+- watcher 拥有数据解析和文案;Mod 只转发配置并执行设备反馈;host 只提供分区和字体。
+- 修改行为时同步更新对应文档、示例配置和版本说明,避免 README 再次堆积实现细节。
## 安全
设备 HTTP API 按设计不设认证并开放 CORS。只应在可信局域网使用,不要转发 TCP
`80`、`8787` 或 `8788`。设备没有 Wi-Fi 凭据时才会出现后备 AP
(`StackChan-Matchday` / `stackchan`)。启动 watcher 前,请通过 BLE 使用官方
-[Stack-chan Web Console](https://stack-chan.github.io/web/) 配置 Wi-Fi。
+[Stack-chan Web Console](https://stack-chan.github.io/stack-chan/web/preference/) 配置 Wi-Fi。
## 致谢与许可证
diff --git a/docs/configuration.md b/docs/configuration.md
new file mode 100644
index 0000000..044d9d8
--- /dev/null
+++ b/docs/configuration.md
@@ -0,0 +1,188 @@
+# Configuration and operation
+
+[English](configuration.md) | [简体中文](configuration.zh-CN.md)
+
+The phone setup page is the normal control surface. It changes all labels
+immediately, persists its language on the device, and applies the selected
+match, supported team, optional position, and commentary style through the
+watcher's pending/ack flow. The watcher validates the ESPN/Kalshi pairing,
+atomically updates its local JSON, hot-reloads, and acknowledges Stack-chan.
+
+## Match Setup flow
+
+1. Keep the watcher computer awake and the `--watch` process running on the
+ same trusted LAN as the phone and Stack-chan.
+2. Double-tap the three-zone touch bar on top of Stack-chan's head. A short
+ Power-button press also toggles Match Setup on the pinned host firmware.
+3. Scan the QR, choose 中文 or English, a match, supported team or Neutral,
+ optional pregame position or No position, and a commentary style.
+4. Tap **Start watching** and wait for watcher confirmation. No device or
+ watcher restart is required.
+
+Tap the touch bar once while the QR is visible to hide it. It closes
+automatically after 90 seconds.
+
+
+
+
+ 
+ Scan the on-device QR to open Match Setup on the local network.
+ |
+
+ 
+ Hold the top touch bar to mute sound, motion, and alert lights while visual updates continue.
+ |
+
+
+
+For a matched fixture, Match Setup enables the Kalshi market associated with
+the selected position team. With No position, it enables the first market in
+the match pair. A position is only a manual perspective; Matchday never reads
+a Kalshi account.
+
+## Configuration format
+
+The top-level `language` is `zh` or `en`. User-facing values accept either a
+legacy string or a localized object:
+
+```json
+{
+ "language": "en",
+ "mac_voice": {"zh": "Tingting", "en": "Samantha"},
+ "espn": {
+ "commentary_style": "balanced",
+ "label": {"zh": "法国 vs 摩洛哥", "en": "France vs Morocco"},
+ "team_names": {
+ "France": {"zh": "法国", "en": "France"}
+ }
+ },
+ "markets": [{
+ "ticker": "KXEXAMPLE-FRA",
+ "label": {"zh": "法国晋级", "en": "France to advance"}
+ }]
+}
+```
+
+Start from `config/kalshi_watchlist.example.json`, keep the untracked working
+copy at `config/kalshi_watchlist.json`, and validate it with
+`python3 -m json.tool` before starting the watcher.
+
+The setup service normally binds `127.0.0.1:8788`; its `/setup` page is a local
+admin fallback rather than the primary on-device QR flow. Once per local day,
+the watcher can ask you to scan and choose a match. Configure that behavior
+with `setup_server.daily_prompt_hour` (`-1` disables it),
+`prompt_minutes_before`, `quiet_hours`, and `lookahead_days`.
+
+## Player catalog and naming
+
+The watcher first consults `config/espn_player_catalog.json`, keyed by stable
+ESPN athlete IDs such as `espn:362150`. Entries provide formal Chinese and
+English names, manually verified casual nicknames, a featured-player flag,
+and an optional goal chant.
+
+`balanced`, `professional`, and every device balloon use the formal name.
+Only casual speech may use a friendly nickname. Legacy per-match
+`player_names` and `star_chants` remain supported and override the catalog;
+custom goal-signal speech uses the same localized-leaf format.
+
+An unmatched player falls back to ESPN's original name. The watcher does not
+transliterate or invent a nickname. When an ESPN roster first appears or
+changes, it logs named and featured coverage and the raw-name fallback list so
+the catalog can be completed for a later match without blocking commentary.
+
+## Commentary styles
+
+`espn.commentary_style` is a persistent global preference with exactly three
+values. If omitted, it defaults to `balanced`.
+
+| Value | Voice | Device balloon |
+| --- | --- | --- |
+| `casual` | Friendly co-watching language with every core fact | Compact event summary |
+| `balanced` | Clear, natural narration compatible with previous behavior | Compact event summary |
+| `professional` | Core facts plus reliably parsed ESPN detail and football terminology | Compact summary; detail stays in speech |
+
+Every style keeps match time, event type, team, required players, result, and
+current score. Penalties, cards, and substitutions retain all necessary
+participants. Suspected goals and events awaiting commentary confirmation stay
+explicitly uncertain in every style.
+
+Professional speech may add an assist or cross, shot type and body part, field
+or goal location, goalkeeper save, set-piece position, or substitution/injury
+reason only when ESPN explicitly supplies it and the watcher parses it
+reliably. It never reads raw English commentary aloud or guesses missing
+detail. Balloons normally remain “time + player/team + event + score,” with
+only small punctuation or wording differences between styles.
+
+The selected style also applies to match phases and results, Kalshi moves, and
+suspected-goal alerts. It changes wording only: TTS voice and rate, sound
+effects, celebrations, expressions, lights, priority, and alert switches are
+unchanged. Switching during a match affects newly generated alerts without
+replaying old ESPN events or resetting market baselines, queues, or polling.
+
+## Support and position perspectives
+
+The supported team defines everyday co-watching language. Routine shots,
+saves, and corners do not force position commentary. Major events—goals,
+penalties, red cards, and results—add whether the manually declared position
+benefits or comes under pressure.
+
+Aligned support and position are combined. A conflict explicitly separates
+the emotional and position outcomes. Only a Kalshi market that Match Setup has
+matched to the selected position and marked `tracks_position` uses position
+wording for price changes or suspected goals. Unconfirmed events always retain
+“if confirmed” or equivalent wording and wait for commentary confirmation.
+
+## Polling, alerts, and quiet hours
+
+Top-level `poll_seconds` is the base interval. `adaptive_polling` can use
+different Kalshi and ESPN intervals when a match is distant, warming up,
+approaching kickoff, live, or finished. During a live ESPN fixture,
+`espn.poll_seconds` may be configured separately. Shorter intervals increase
+load on the device and upstream services; they do not guarantee that upstream
+data will appear sooner.
+
+Common alert settings include:
+
+- `max_alerts_per_cycle` — maximum alerts delivered in one polling cycle.
+- `startup_summary_on_watch` / `speak_startup_summary` — startup summary and
+ whether it is spoken.
+- `display_refresh_seconds` — persistent-display refresh interval.
+- `quiet_hours` — do-not-disturb period.
+- `alert_balloon_seconds` — duration of ordinary alert balloons.
+- Per-market `alert_move_cents`, `speak_move_cents`,
+ `min_seconds_between_alerts`, and `alerts_enabled`.
+
+The watcher can also prompt for a daily match selection:
+
+- `setup_server.daily_prompt_hour` — local hour; `-1` disables the prompt.
+- `setup_server.prompt_minutes_before` — pre-kickoff prompt window.
+- `setup_server.lookahead_days` — fixture-discovery horizon.
+- `quiet_hours` — suppresses proactive prompts at unsuitable times.
+
+## Mute and other controls
+
+Hold the top touch bar for about one second to toggle the boss-key mute.
+Speech, tones, celebrations, and alert lights stop while the probability bar,
+balloons, and ticker update silently. A corner `MUTE` / `静音` badge remains
+until unmuted.
+
+`mute on 60` sent to `/api/command`, or the control panel's **mute 60m** button,
+starts a timed meeting mute and announces when sound returns. An indefinite
+mute survives reboots. See the [Device API](device-api.md) for the complete
+command examples.
+
+## Standalone market mode
+
+If no fixture is available, paste a Kalshi event URL or ticker into Match
+Setup. The event's four most-traded markets appear in the bottom ticker.
+Fixture-only flags, probability bar, and ESPN commentary are disabled until a
+matched fixture is selected again.
+
+## HTTP and serial transport
+
+HTTP is the full workflow and uses Python's standard library. Phone setup,
+device status detection, and the options/pending/ack relay require it.
+
+Serial transport is intended for direct command and control only. Install
+`pyserial` and set `stackchan_serial_port` if you use it; Match Setup relay and
+status detection are unavailable over serial.
diff --git a/docs/configuration.zh-CN.md b/docs/configuration.zh-CN.md
new file mode 100644
index 0000000..8257b8a
--- /dev/null
+++ b/docs/configuration.zh-CN.md
@@ -0,0 +1,188 @@
+# 配置与播报
+
+[English](configuration.md) | [简体中文](configuration.zh-CN.md)
+
+本文说明 watcher 的持久配置、手机 Match Setup、语言与三档播报、支持/持仓视角、
+球员目录和独立市场模式。配置示例位于
+`config/kalshi_watchlist.example.json`;请把本机修改写入未提交的
+`config/kalshi_watchlist.json`。
+
+## 配置入口
+
+| 入口 | 用途 |
+| --- | --- |
+| 设备 `http:///setup` | 日常选择比赛、语言、支持队、持仓和语气;推荐入口 |
+| watcher `http://127.0.0.1:8788/setup` | watcher 电脑上的本机管理后备页 |
+| `config/kalshi_watchlist.json` | 轮询、提醒开关、市场、TTS 和高级 ESPN 配置 |
+
+手机设置流程使用 pending/ack:设备先保存待处理选择,watcher 校验 ESPN 与 Kalshi
+双方匹配,原子更新 JSON、热重载,再确认设备。只切换语气时不会重置 ESPN 已读事件、
+盘口基线、告警队列或轮询状态。
+
+手机设置、设备状态检测和 options/pending/ack 中继都依赖 HTTP。串口模式只适合直接
+命令和控制;使用时还需安装 `pyserial` 并设置 `stackchan_serial_port`。
+
+## 最小 watcher 配置
+
+```json
+{
+ "stackchan_host": "stackchan.local",
+ "stackchan_transport": "http",
+ "language": "zh",
+ "setup_server": {
+ "enabled": true,
+ "host": "127.0.0.1",
+ "port": 8788,
+ "kalshi_series_ticker": "KXWCADVANCE",
+ "lookahead_days": 10,
+ "refresh_seconds": 900
+ },
+ "espn": {
+ "enabled": false,
+ "commentary_style": "balanced"
+ },
+ "markets": [{
+ "ticker": "KXEXAMPLE-TEAM",
+ "label": {"zh": "示例市场", "en": "Example market"},
+ "side_i_care": "yes"
+ }]
+}
+```
+
+watcher 至少需要一个市场;这里的 `KXEXAMPLE-...` ticker 不是真实市场。通过手机
+选择比赛后,watcher 会写入
+匹配结果;也可以用 discover 命令查找开放市场:
+
+```sh
+python3 tools/stackchan_kalshi_watch.py discover --query 关键词
+```
+
+## 语言与本地化文本
+
+顶层 `language` 接受 `zh` 或 `en`。设置页会立即切换设备界面文案并持久化;应用
+一场比赛后,watcher 生成的语音和气泡也使用该语言。
+
+面向用户的配置文本既可以是旧式字符串,也可以是中英对象:
+
+```json
+{
+ "language": "en",
+ "mac_voice": {"zh": "Tingting", "en": "Samantha"},
+ "espn": {
+ "label": {"zh": "法国 vs 摩洛哥", "en": "France vs Morocco"},
+ "team_names": {
+ "France": {"zh": "法国", "en": "France"}
+ }
+ },
+ "markets": [{
+ "ticker": "KXEXAMPLE-FRA",
+ "label": {"zh": "法国晋级", "en": "France to advance"}
+ }]
+}
+```
+
+## 三档播报语气
+
+`espn.commentary_style` 是全局持久偏好,只接受 `casual`、`balanced` 或
+`professional`。旧配置缺少该字段时默认 `balanced`。
+
+| 配置值 | 语音 | 设备气泡 |
+| --- | --- | --- |
+| `casual` | 朋友陪看的口语表达,但完整保留核心事实 | 精炼事件摘要 |
+| `balanced` | 清楚、自然,兼容此前播报 | 精炼事件摘要 |
+| `professional` | 先播核心事实,再补充可靠解析出的 ESPN 细节 | 仍精炼,详细信息交给语音 |
+
+三档共用解析后的结构化事实,必须保留比赛时间、事件类型、球队、必要球员、事件结果
+和当前比分;点球、红黄牌和换人也保留必要参与者。“疑似进球”“等待文字直播确认”
+等不确定性在任何语气下都不会省略。
+
+专业档只使用 ESPN 明确提供且 watcher 能可靠解析的助攻或传中、射门方式和身体
+部位、场上或球门位置、门将扑救、定位球位置、换人或伤情原因。它不会朗读英文原文,
+也不会猜测缺失或含糊的细节。
+
+三档气泡原则上保持“时间 + 球员/球队 + 事件 + 比分”,只允许标点和少量措辞差异。
+语气只改变文案,不改变 TTS 音色和语速、音效、动作、表情、灯效、优先级或提醒
+开关。比赛中切换立即作用于新提醒,但不会重播旧事件。
+
+## 支持队与持仓队
+
+- `espn.favorite_team` 是支持队,决定普通射门、扑救、角球等日常陪看视角;可中立。
+- `espn.position_team` 是手动填写的持仓偏好;可选择“没买”。系统不读取真实账户。
+- 进球、点球、红牌和赛果等重大事件才补充持仓利好或利空。
+- 支持与持仓同向时合并成自然反应;方向冲突时分别说明支持方向和仓位影响。
+- Match Setup 有持仓时启用该队对应市场;没买时默认启用第一个市场,但不会把它
+ 描述成持仓。
+- 只有与持仓队匹配且标记为 `tracks_position` 的 Kalshi 市场,盘口变化和疑似进球
+ 才使用持仓措辞;疑似事件仍保留“如果属实”和等待确认。
+
+## 球员名字、昵称和进球口号
+
+watcher 先查全局 `config/espn_player_catalog.json`。目录以稳定的 ESPN athlete ID
+(如 `espn:362150`)为主键,保存正式中英文名、经人工核实的 casual 昵称、球星
+标记和可选进球口号。
+
+- `balanced`、`professional` 和所有气泡始终使用正式名。
+- 只有 `casual` 语音可以使用目录中的亲切昵称。
+- 旧配置内的 `player_names` 和 `star_chants` 继续有效,并优先于全局目录。
+- 自定义进球口号支持字符串或 `{ "zh": "...", "en": "..." }`。
+- 未命中目录的球员回退 ESPN 原名,不音译、不猜昵称。
+
+ESPN roster 首次出现或变化时,watcher 会记录目录命中数、球星数和原名回退名单。
+维护者应在赛前根据这些日志补齐目录,并为昵称保留可靠来源;正式名来源不等于可以
+推断昵称。
+
+局部覆盖示例:
+
+```json
+{
+ "espn": {
+ "player_names": {
+ "Kylian Mbappé": {"zh": "姆巴佩", "en": "Kylian Mbappe"}
+ },
+ "star_chants": {
+ "Kylian Mbappé": {
+ "zh": "{name}!{name}!打进去了!",
+ "en": "{name}! {name}! He scores!"
+ }
+ }
+ }
+}
+```
+
+## 轮询、提醒和静默时段
+
+顶层 `poll_seconds` 是基础间隔;`adaptive_polling` 可在远离开赛、赛前预热、临近
+开赛和赛后使用不同的 Kalshi/ESPN 间隔。ESPN 比赛中的 `espn.poll_seconds` 可单独
+设置。过短间隔会增加设备与上游压力,不保证上游数据更早出现。
+
+常用提醒字段包括:
+
+- `max_alerts_per_cycle`:单轮最多投递的提醒数。
+- `startup_summary_on_watch` / `speak_startup_summary`:启动摘要及是否朗读。
+- `display_refresh_seconds`:常驻显示刷新间隔。
+- `quiet_hours`:免打扰时段。
+- `alert_balloon_seconds`:普通提醒气泡时长。
+- 各市场的 `alert_move_cents`、`speak_move_cents`、`min_seconds_between_alerts` 和
+ `alerts_enabled`。
+
+watcher 还可每天提醒扫码选比赛:
+
+- `setup_server.daily_prompt_hour`:本地小时;`-1` 关闭。
+- `setup_server.prompt_minutes_before`:开赛前提醒窗口。
+- `setup_server.lookahead_days`:比赛发现范围。
+- `quiet_hours`:抑制不合时段的主动提醒。
+
+## 静音(老板键)
+
+长按头顶触摸条约一秒可切换不限时静音。静音停止远程 TTS、提示音、原始 `tone`、
+庆祝动作和警报闪灯;概率条、气泡、ticker 和手机设置页继续工作。不限时静音跨
+重启保留,定时静音不跨重启。
+
+可通过设备命令使用 `mute on [分钟]`、`mute off`、`mute status`,或在控制面板
+选择定时静音。完整接口见[设备 API](device-api.zh-CN.md)。
+
+## 独立市场模式
+
+没有比赛时,可在设备设置页粘贴 Kalshi event URL 或 ticker。watcher 会选择该事件
+中成交最活跃的最多四个市场,在底部 ticker 中显示;比赛专属旗帜概率条和 ESPN
+播报暂时关闭。重新选择比赛后恢复比赛模式。
diff --git a/docs/development.md b/docs/development.md
new file mode 100644
index 0000000..b7757da
--- /dev/null
+++ b/docs/development.md
@@ -0,0 +1,110 @@
+# Development
+
+[English](development.md) | [简体中文](development.zh-CN.md)
+
+## Repository map
+
+- `mod/` — device mod, split into small JavaScript modules plus flag and QR
+ assets.
+- `host/` — required CoreS3 partition patch, optional CJK-font patch, and font
+ preparation helper. These are build/resource changes; upstream runtime JS/C
+ source remains unchanged.
+- `tools/` — watcher, local setup service, macOS TTS server, replay tool,
+ serial helper, asset generator, and tests. Default HTTP needs only Python's
+ standard library; serial transport additionally needs `pyserial`.
+- `config/` — example watcher configuration, flag-pack definition, and global
+ ESPN player catalog.
+- `docs/` — versioned user, API, development, product, and release guides.
+
+## Test suites
+
+Run all local tests from this repository:
+
+```sh
+cd "$MATCHDAY_DIR"
+python3 -m unittest discover -s tools -p 'test_*.py'
+node tools/test_stackchan_mod_web_behavior.mjs
+```
+
+The Python suite covers watcher behavior, Match Setup, commentary contracts,
+and device-facing integration helpers. The Node suite exercises the mod web
+surface without installing it on hardware.
+
+## Build without installing
+
+Build a mod archive from the upstream Stack-chan `firmware/` directory:
+
+```sh
+cd "$STACKCHAN_DIR/firmware"
+mcrun -d -m -p esp32:./platforms/m5stackchan_cores3 -t build -f rgb565be \
+ "$MATCHDAY_DIR/mod/manifest.json"
+```
+
+CoreS3 requires `-f rgb565be`. The resulting debug archive is normally at
+`$MODDABLE/build/bin/esp32/debug/mod/mod.xsa`. See
+[Getting started](getting-started.md) before writing an archive to hardware;
+its `0xDF0000` offset assumes this repository's host partition patch.
+
+## Replay a recorded match
+
+The replay tool runs the France–Morocco ESPN history through the same alert
+parser. Preview is the default. Stop the continuous watcher before explicitly
+executing commands on a connected device.
+
+```sh
+python3 "$MATCHDAY_DIR/tools/stackchan_match_replay.py" \
+ --config "$MATCHDAY_DIR/config/kalshi_watchlist.json"
+python3 "$MATCHDAY_DIR/tools/stackchan_match_replay.py" \
+ --config "$MATCHDAY_DIR/config/kalshi_watchlist.json" --language en
+python3 "$MATCHDAY_DIR/tools/stackchan_match_replay.py" \
+ --config "$MATCHDAY_DIR/config/kalshi_watchlist.json" --execute
+```
+
+Use preview to inspect wording and event ordering without sending device
+commands. `--execute` is intentionally opt-in because it produces real screen,
+speech, light, and motion activity.
+
+## Debugging on hardware
+
+`npm run mod` uses the xsbug protocol. A stalled install can invalidate the mod
+until it is reinstalled. More importantly, xsbug exception breakpoints pause
+the entire device runtime, including Wi-Fi, touch, and timers; do not leave an
+interactive xsbug session attached during unattended match watching.
+
+Use `$MODDABLE/tools/xsbug-log` when you need logs without breakpoint freezes.
+For a deterministic reinstall, follow the debugger-free build and esptool path
+in [Getting started](getting-started.md). Field symptoms and short recovery
+recipes are indexed in the
+[GitHub Wiki](https://github.com/xymeow/stackchan-matchday/wiki).
+
+## Documentation synchronization checklist
+
+When behavior changes, update the applicable items together:
+
+1. Example configuration in `config/kalshi_watchlist.example.json`.
+2. The corresponding English and Chinese installation, configuration, API, or
+ development guides.
+3. User entry points in `README.md` and `README.zh-CN.md`, keeping only concise
+ summaries and links there.
+4. Release notes under `docs/releases/`.
+5. `docs/commentary-styles-prd.md` when product rules change.
+6. Environment-specific troubleshooting in the Wiki when useful, linked from
+ the README documentation index.
+
+English and Chinese headings, commands, and facts should remain aligned. The
+Chinese text may use natural phrasing rather than translating sentence by
+sentence.
+
+## Security and local state
+
+- Do not commit `config/kalshi_watchlist.json`, API keys, account information,
+ device Wi-Fi credentials, or private TTS configuration.
+- The Kalshi REST MVP does not require an API key. If future functionality
+ requires authentication, read it from environment variables or a local file,
+ never firmware.
+- Keep device endpoints on a trusted LAN; tests must not expose TCP `80`,
+ `8787`, or `8788` to the internet.
+- Serial transport additionally requires `pyserial`. Do not mistake a runtime
+ paused by a debugger for a network failure.
+- Before testing real hardware, verify that no other watcher or replay process
+ is sending commands at the same time.
diff --git a/docs/development.zh-CN.md b/docs/development.zh-CN.md
new file mode 100644
index 0000000..cce2b9f
--- /dev/null
+++ b/docs/development.zh-CN.md
@@ -0,0 +1,105 @@
+# 开发指南
+
+[English](development.md) | [简体中文](development.zh-CN.md)
+
+本文面向修改 watcher、Matchday Mod、host 构建补丁或文档的维护者和 AI agent。
+
+## 组件边界
+
+| 目录 | 责任 | 不应承担的责任 |
+| --- | --- | --- |
+| `tools/` | 数据获取、解析、状态、文案、设置服务、TTS、回放和测试 | 设备 UI 与 host runtime |
+| `mod/` | 手机设置中继、设备显示、语音、动作、灯效和资源 | 推断 ESPN 事实或生成专业文案 |
+| `host/` | CoreS3 分区补丁、可选 CJK 字体补丁与准备脚本 | 修改上游 runtime JS/C 源码 |
+| `config/` | 示例 watcher 配置、旗帜定义、全局球员目录 | 运行时秘密或真实账户信息 |
+| `docs/` | 版本化安装、配置、接口、PRD 和升级说明 | 环境相关且易变的排障记录 |
+
+构建参数、分区偏移、接口和配置行为必须保存在仓库文档并随代码评审。GitHub Wiki
+适合 xsbug、mDNS、防火墙和串口抓日志等环境经验,但不应成为版本绑定事实的唯一来源。
+
+## 本地测试
+
+从 Matchday 仓库根目录运行完整测试:
+
+```sh
+cd "$MATCHDAY_DIR"
+python3 -m unittest discover -s tools -p 'test_*.py'
+node tools/test_stackchan_mod_web_behavior.mjs
+```
+
+Python 测试覆盖 watcher、Match Setup、结构化事实、文案和投递行为;Node 测试检查
+设备 Web/API 行为。修改跨越 watcher 与 Mod 时,两组都要运行。
+
+建议按改动范围补充针对性测试:
+
+- 三档对同一事件必须包含相同的球队、球员、结果和比分。
+- 疑似进球与等待确认在所有语气中保持不确定性。
+- 专业细节只来自可可靠解析的 ESPN 字段;缺失值不能触发猜测。
+- 只切换语气不能重复旧事件或重置盘口、告警与轮询状态。
+- Match Setup 需要覆盖设备 pending/ack 与 watcher 原子更新两端。
+- 投递失败测试应验证退避和有限重试,避免设备重启风暴。
+
+## 构建 Mod 归档
+
+在上游 Stack-chan `firmware/` 目录构建但不安装:
+
+```sh
+cd "$STACKCHAN_DIR/firmware"
+mcrun -d -m -p esp32:./platforms/m5stackchan_cores3 -t build -f rgb565be \
+ "$MATCHDAY_DIR/mod/manifest.json"
+```
+
+CoreS3 的像素格式必须是 `rgb565be`。完整安装与分区写入流程见
+[安装与升级](getting-started.zh-CN.md);不要在开发文档复制另一份偏移说明。
+
+## 回放 ESPN 历史比赛
+
+`stackchan_match_replay.py` 使用和 watcher 相同的 parser 回放法国—摩洛哥 ESPN
+历史。默认只预览命令;执行前先停止持续运行的 watcher,避免两个进程同时控制设备。
+
+```sh
+python3 "$MATCHDAY_DIR/tools/stackchan_match_replay.py" \
+ --config "$MATCHDAY_DIR/config/kalshi_watchlist.json"
+python3 "$MATCHDAY_DIR/tools/stackchan_match_replay.py" \
+ --config "$MATCHDAY_DIR/config/kalshi_watchlist.json" --language en
+python3 "$MATCHDAY_DIR/tools/stackchan_match_replay.py" \
+ --config "$MATCHDAY_DIR/config/kalshi_watchlist.json" --execute
+```
+
+先用默认预览核对命令、语言、球员名、比分和不确定性,再使用 `--execute` 做设备端
+验收。涉及 TTS 与动作串行时,还要观察命令到达顺序和设备状态轮询频率。
+
+## 硬件调试
+
+`npm run mod` 使用 xsbug 协议。安装中断可能使 Mod 暂时不可用,必须重新安装;
+更重要的是,xsbug 的异常断点会暂停整个设备 runtime,包括 Wi-Fi、触摸和定时器。
+无人值守看球时不要保持交互式 xsbug 会话连接。
+
+需要不触发断点暂停的日志时,使用 `$MODDABLE/tools/xsbug-log`。需要确定性重装时,
+按照[安装与升级](getting-started.zh-CN.md)中的无调试器构建与 esptool 流程操作。
+现场症状与安全恢复顺序由
+[GitHub Wiki](https://github.com/xymeow/stackchan-matchday/wiki) 维护。
+
+## 文档同步清单
+
+行为改动完成时,根据影响面更新:
+
+1. 示例配置 `config/kalshi_watchlist.example.json`。
+2. 对应的中英文安装、配置、API 或开发文档。
+3. 用户入口 `README.md` 与 `README.zh-CN.md`,但只保留摘要和链接。
+4. 版本说明 `docs/releases/`。
+5. 如果产品规则变化,更新 `docs/commentary-styles-prd.md`。
+6. 环境型排障经验可补到 Wiki,并从 README 文档索引链接。
+
+中英文文档的标题、命令和事实应保持一致;中文可以自然表达,不需要逐句直译。
+
+## 安全与本地状态
+
+- 不提交 `config/kalshi_watchlist.json`、API 密钥、账户信息、设备 Wi-Fi 凭据或私有
+ TTS 配置。
+- Kalshi REST MVP 不需要 API key;未来需要认证时,应从环境变量或本地文件读取,
+ 不要写入 firmware。
+- 设备接口仅用于可信局域网,不要在测试中把 TCP `80`、`8787` 或 `8788` 暴露到
+ 公网。
+- 串口模式需要额外的 `pyserial`;不要让调试器暂停运行时后误判成网络故障。
+- 测试真实设备前,先确认没有另一个 watcher 或回放进程同时发送命令。
diff --git a/docs/device-api.md b/docs/device-api.md
new file mode 100644
index 0000000..de0e702
--- /dev/null
+++ b/docs/device-api.md
@@ -0,0 +1,109 @@
+# Device API
+
+[English](device-api.md) | [简体中文](device-api.zh-CN.md)
+
+Stack-chan Matchday exposes an unauthenticated, CORS-open HTTP API for trusted
+LAN use. Do not expose device TCP `80`, TTS TCP `8787`, or watcher setup TCP
+`8788` to the internet.
+
+## Health, help, and status
+
+- `GET /health` checks that the device web service is reachable.
+- `GET /api/help` returns the plain-text command surface accepted by
+ `POST /api/command`.
+- `GET /api/status` reports the mod version, probability bar, TTS, power,
+ network, mute state, and setup-trigger counters.
+
+## Plain-text commands
+
+Send one command as the raw body of `POST /api/command`:
+
+```text
+pkbar es 62 AA151B be 38 EF3340
+balloon temp 8000 Spain scores!
+voice favorite-goal Number seven scores!
+celebrate goal 170 21 27
+celebrate say 170 21 27 Goal!
+celebrate result win 170 21 27 Full time
+setup show http://stackchan.local/setup
+say Hello
+mute on 60
+mute off
+mute status
+face happy
+look 8 -2
+idle look on
+light flash 0 85 164
+```
+
+For example:
+
+```sh
+export STACKCHAN_HOST=stackchan.local
+curl --request POST --data-binary "say Matchday ready" \
+ "http://$STACKCHAN_HOST/api/command"
+curl --request POST --data-binary "mute status" \
+ "http://$STACKCHAN_HOST/api/command"
+```
+
+## JSON control
+
+`POST /api/control` accepts JSON actions for the browser-based control panel
+and other structured clients. Device capabilities may vary by mod version, so
+clients should check the HTTP response and, when needed, read `/api/status`
+instead of assuming that an action ran.
+
+```sh
+curl --request POST \
+ --header 'Content-Type: application/json' \
+ --data '{"action":"mute","enabled":true,"minutes":60}' \
+ "http://$STACKCHAN_HOST/api/control"
+```
+
+Use `GET /api/help` on the installed mod as the runtime source of truth for
+available plain-text commands.
+
+## Match Setup relay
+
+The device-facing setup surface uses these endpoints under `/api/match-setup`:
+
+- `/options` receives fixture and standalone-event choices from the watcher.
+- `/apply` stores a phone selection as pending.
+- `/pending` lets the watcher retrieve that selection.
+- `/ack` confirms success or reports validation failure back to the phone.
+- `/language` updates the persisted device language.
+
+These endpoints form a pending/ack handshake: the phone writes to Stack-chan,
+the watcher validates and atomically updates its local configuration, and the
+device displays the acknowledgement. They require HTTP transport.
+
+## Commentary-style endpoints
+
+The device accepts `POST /api/match-setup/style` with one of `casual`,
+`balanced`, or `professional`, for example:
+
+```json
+{"commentary_style":"professional"}
+```
+
+It forwards the setting through the same pending/ack flow. The watcher-hosted
+admin service accepts the same body at `POST /api/setup/style`.
+`GET /api/setup/status` reports the effective value.
+
+Changing only the style must not reset ESPN event history, market baselines,
+alert queues, or polling state, and must not replay old commentary. The new
+style applies to subsequently generated alerts.
+
+## Calling conventions
+
+- Use every endpoint only on a trusted LAN; the device API has no
+ authentication and allows CORS.
+- Send text commands as UTF-8. Preserve Chinese text rather than translating
+ it before sending it to the device.
+- Watcher HTTP delivery should detect failures and back off instead of retrying
+ continuously; device resources are limited.
+- Avoid high-frequency status polling while long TTS audio is playing.
+- Match Setup and its pending/ack flow require HTTP; serial transport does not
+ provide this workflow.
+- When changing an endpoint, update the mod, watcher client, tests, and this
+ guide together.
diff --git a/docs/device-api.zh-CN.md b/docs/device-api.zh-CN.md
new file mode 100644
index 0000000..2275a59
--- /dev/null
+++ b/docs/device-api.zh-CN.md
@@ -0,0 +1,121 @@
+# 设备 API
+
+[English](device-api.md) | [简体中文](device-api.zh-CN.md)
+
+Matchday Mod 在 Stack-chan 的 TCP `80` 上提供无认证 HTTP API。它用于同一可信
+局域网内的 watcher、手机设置页和手工诊断;不要将端口转发到互联网。
+
+以下示例假设:
+
+```sh
+export STACKCHAN_HOST=stackchan.local
+```
+
+## 健康与状态
+
+| 方法与路径 | 用途 |
+| --- | --- |
+| `GET /health` | 最小存活检查 |
+| `GET /api/status` | Mod 版本、概率条、TTS、电源、网络、静音和设置触发计数 |
+| `GET /api/help` | `POST /api/command` 支持的纯文本命令 |
+
+```sh
+curl "http://$STACKCHAN_HOST/health"
+curl "http://$STACKCHAN_HOST/api/status"
+curl "http://$STACKCHAN_HOST/api/help"
+```
+
+## 纯文本命令
+
+向 `POST /api/command` 发送单条纯文本命令:
+
+```sh
+curl --request POST --data-binary "say 比赛日准备好了" \
+ "http://$STACKCHAN_HOST/api/command"
+```
+
+常用命令:
+
+```text
+pkbar es 62 AA151B be 38 EF3340
+balloon temp 8000 西班牙进球了!
+voice favorite-goal 7号球员进球啦
+celebrate goal 170 21 27
+celebrate say 170 21 27 进球了!
+celebrate result win 170 21 27 比赛结束
+setup show http://stackchan.local/setup
+say 你好
+mute on 60
+mute off
+mute status
+face happy
+look 8 -2
+idle look on
+light flash 0 85 164
+```
+
+参数格式和当前可用命令以设备返回的 `GET /api/help` 为准。发送动态语音前,可先用
+`tts host :8787` 设置局域网 TTS,再用 `tts status` 检查连接。
+
+## JSON 控制
+
+`POST /api/control` 接受 JSON action,供控制面板和结构化客户端使用。设备能力可能
+随 Mod 版本变化;客户端应检查 HTTP 状态并在需要时读取 `/api/status`,不要假定
+命令已执行。
+
+```sh
+curl --request POST \
+ --header 'Content-Type: application/json' \
+ --data '{"action":"mute","enabled":true,"minutes":60}' \
+ "http://$STACKCHAN_HOST/api/control"
+```
+
+## Match Setup
+
+设备托管 `GET /setup` 手机页面。watcher 使用以下接口完成 options/pending/ack
+中继:
+
+| 路径 | 角色 |
+| --- | --- |
+| `/api/match-setup` | Match Setup 主资源 |
+| `/api/match-setup/options` | watcher 发布可选比赛和市场 |
+| `/api/match-setup/apply` | 手机提交选择 |
+| `/api/match-setup/pending` | watcher 读取待处理选择 |
+| `/api/match-setup/ack` | watcher 确认或拒绝应用结果 |
+| `/api/match-setup/language` | 更新界面语言 |
+| `/api/match-setup/style` | 更新播报语气 |
+
+这是一个异步确认流程:设备收到手机选择不代表 watcher 已采用。客户端应显示待处理
+状态,直到 watcher 返回 ack;不要在提交后自行假定配置成功。
+
+### 即时切换语气
+
+设备接收 `casual`、`balanced` 或 `professional` 之一,例如:
+
+```http
+POST /api/match-setup/style
+Content-Type: application/json
+
+{"commentary_style":"professional"}
+```
+
+它通过现有 pending/ack 流程转发。watcher 本机设置服务的对应接口为:
+
+```http
+POST /api/setup/style
+Content-Type: application/json
+
+{"commentary_style":"professional"}
+```
+
+`GET /api/setup/status` 返回 watcher 当前生效值。切换只影响新生成文案,不应重置
+ESPN 已读事件、盘口基线、告警队列或轮询状态。
+
+## 调用约定
+
+- 所有接口仅用于可信局域网;设备 API 无认证并开放 CORS。
+- 文本命令使用 UTF-8,请保留中文正文,不要先转成英文再交给设备。
+- watcher 的 HTTP 投递应检查失败并退避,不要无间隔重试;设备资源有限。
+- 长时间 TTS 播放期间不要用高频状态轮询压迫设备。
+- 需要设置页和 pending/ack 时必须使用 HTTP;串口 transport 不提供这条链路。
+- 修改接口时同步更新 Mod、watcher 客户端、测试和本文。
diff --git a/docs/getting-started.md b/docs/getting-started.md
new file mode 100644
index 0000000..6f415ef
--- /dev/null
+++ b/docs/getting-started.md
@@ -0,0 +1,188 @@
+# Getting started
+
+[English](getting-started.md) | [简体中文](getting-started.zh-CN.md)
+
+This guide covers a first Stack-chan Matchday installation: preparing the
+tested upstream Stack-chan checkout, installing the one-time host changes,
+building the Matchday mod, starting the watcher, and optionally enabling LAN
+speech.
+
+Already running Matchday? Read the version-specific
+[1.5.0](releases/1.5.0.md) and [1.4.0](releases/1.4.0.md) notes before rebuilding.
+Support/position-aware wording, position-based market selection, and the global
+player catalog are watcher-only updates. An installed 1.5.0 mod can stay in
+place: deploy the watcher code and `config/espn_player_catalog.json`, then
+restart the watcher process.
+
+## Requirements
+
+- A CoreS3-based Stack-chan with 16 MB flash and a USB data cable.
+- Git, Python 3.10+, Node.js 20+ (Node.js 22 is the tested upstream version),
+ npm, and `xz` on the build computer.
+- Moddable SDK and ESP-IDF. The upstream `xs-dev` setup below installs and
+ checks them.
+- A phone and watcher computer on the same trusted LAN as Stack-chan.
+- `qrencode` only when generating the device-specific setup QR.
+- macOS only for the included `say`-based TTS server. Other systems can run
+ without speech or provide a compatible `/say` WAV service.
+
+Commands below use macOS/Linux shell syntax. Set two absolute paths once and
+reuse them throughout installation:
+
+```sh
+mkdir -p "$HOME/src"
+export MATCHDAY_DIR="$HOME/src/stackchan-matchday"
+export STACKCHAN_DIR="$HOME/src/stack-chan"
+```
+
+## 1. Clone Matchday
+
+```sh
+git clone https://github.com/xymeow/stackchan-matchday.git "$MATCHDAY_DIR"
+```
+
+If it is already cloned, preserve your local configuration, update the code,
+and read the [release notes](releases/) before deciding which components need
+to be reinstalled.
+
+## 2. Prepare and flash the host once
+
+Every new CoreS3 host checkout needs the Matchday partition patch. Chinese
+labels and balloons additionally need the optional CJK-font patch and prepared
+font resource; an English-only installation may skip those two pieces.
+
+Follow [Host preparation](../host/README.md) for the tested upstream commit,
+dependency checks, partition and optional font patches, build, and flash
+commands. That guide is the canonical source for host changes and explains why
+the upstream runtime JS/C source remains unmodified.
+
+The Matchday mod cannot be installed until the host contains the `xs`
+partition. Host work is normally one-time; watcher-only updates do not require
+reflashing it. If `npm run doctor` reports a missing platform prerequisite,
+consult the upstream
+[getting-started guide](https://github.com/stack-chan/stack-chan/blob/dev/v1.0/firmware/docs/getting-started.md)
+and build only after `esp32` is listed as a supported target.
+
+## 3. Generate the setup QR
+
+The QR is a static image compiled into the mod, not generated at runtime. Give
+Stack-chan a stable DHCP reservation, IP address, or resolvable mDNS name, then
+generate the asset before installing the mod. Keep both PNG edges at or below
+168 px so the title and URL still fit.
+
+```sh
+export STACKCHAN_HOST=stackchan.local
+qrencode -s 4 -m 1 -o "$MATCHDAY_DIR/mod/assets/setup/setup-qr.png" \
+ "http://$STACKCHAN_HOST/setup"
+file "$MATCHDAY_DIR/mod/assets/setup/setup-qr.png"
+```
+
+If `file` reports an edge larger than 168 px, regenerate with `-s 3`.
+
+## 4. Build and install the mod
+
+From the upstream `firmware/` directory:
+
+```sh
+cd "$STACKCHAN_DIR/firmware"
+npm run mod --target=esp32:./platforms/m5stackchan_cores3 -- -f rgb565be \
+ "$MATCHDAY_DIR/mod/manifest.json"
+```
+
+`-f rgb565be` is required on CoreS3. Without it, flag colors are byte-swapped.
+
+`npm run mod` installs over the xsbug debug protocol. It needs an xsbug
+listener and may stall mid-write if the device is busy; a killed or stalled
+write leaves the mod unavailable until it is reinstalled. A debugger-free
+alternative is to build only and write the archive directly to `xs`:
+
+```sh
+mcrun -d -m -p esp32:./platforms/m5stackchan_cores3 -t build -f rgb565be \
+ "$MATCHDAY_DIR/mod/manifest.json"
+python3 -m esptool --chip esp32s3 --before default-reset --after hard-reset \
+ write-flash 0xDF0000 "$MODDABLE/build/bin/esp32/debug/mod/mod.xsa"
+```
+
+`0xDF0000` is the `xs` partition offset created by the host patch. esptool
+verifies the write; the host mounts the archive after the following reset.
+
+Verify the device from the watcher computer:
+
+```sh
+curl "http://$STACKCHAN_HOST/health"
+curl "http://$STACKCHAN_HOST/api/status"
+```
+
+## 5. Configure the watcher
+
+Create a local configuration from the tracked example:
+
+```sh
+cp "$MATCHDAY_DIR/config/kalshi_watchlist.example.json" \
+ "$MATCHDAY_DIR/config/kalshi_watchlist.json"
+```
+
+Check these values in the copy:
+
+- `stackchan_host` matches `$STACKCHAN_HOST` or the device LAN IP.
+- `stackchan_transport` is `http`; phone setup does not work over serial.
+- `setup_server.enabled` is `true`.
+- Port `8788` is free. The default `127.0.0.1` binding keeps the optional
+ watcher admin page local to that computer.
+
+Validate the JSON and start the continuous watcher:
+
+```sh
+python3 -m json.tool "$MATCHDAY_DIR/config/kalshi_watchlist.json"
+python3 "$MATCHDAY_DIR/tools/stackchan_kalshi_watch.py" \
+ --config "$MATCHDAY_DIR/config/kalshi_watchlist.json" --watch
+```
+
+The example `KXEXAMPLE-...` tickers are deliberate placeholders. Until you
+select a live match from the phone or enter real open tickers, the watcher may
+report them missing. `--dry-run` suppresses device writes but still calls the
+public APIs; it is not an offline installation test.
+
+See [Configuration and operation](configuration.md) for language, commentary,
+support and position behavior, player names, mute, and standalone mode.
+
+## 6. Optional: enable LAN speech
+
+On macOS, start the included TTS server in a second terminal and leave it in
+the foreground so errors remain visible:
+
+```sh
+export MATCHDAY_DIR="$HOME/src/stackchan-matchday"
+python3 "$MATCHDAY_DIR/tools/stackchan_tts_server.py" --host 0.0.0.0 --port 8787
+```
+
+Verify it, then point the device at the watcher's LAN address—not
+`127.0.0.1`:
+
+```sh
+curl "http://127.0.0.1:8787/health"
+export STACKCHAN_HOST=stackchan.local
+export WATCHER_HOST=192.168.1.20
+curl --request POST --data-binary "tts host $WATCHER_HOST:8787" \
+ "http://$STACKCHAN_HOST/api/command"
+curl --request POST --data-binary "say Matchday ready" \
+ "http://$STACKCHAN_HOST/api/command"
+```
+
+Allow inbound TCP `8787` through the computer firewall. `say -v '?'` lists
+installed macOS voices. Override the defaults with
+`STACKCHAN_TTS_ZH_VOICE`, `STACKCHAN_TTS_EN_VOICE`, and
+`STACKCHAN_TTS_RATE`. If TTS is unreachable, the mod falls back to short tone
+patterns.
+
+## Verification and field troubleshooting
+
+Verify the mod with `/health` and `/api/status`, then confirm that a phone
+selection moves from pending to acknowledged while the watcher is running.
+Preserve the watcher output and status response before restarting or
+reflashing anything.
+
+Use the [GitHub Wiki](https://github.com/xymeow/stackchan-matchday/wiki) for
+symptom-first checks covering networking, setup, QR, TTS, CJK text, markets,
+and debugger freezes. This versioned guide remains the canonical reference for
+commands and partition-dependent values.
diff --git a/docs/getting-started.zh-CN.md b/docs/getting-started.zh-CN.md
new file mode 100644
index 0000000..a50c111
--- /dev/null
+++ b/docs/getting-started.zh-CN.md
@@ -0,0 +1,194 @@
+# 安装与升级
+
+[English](getting-started.md) | [简体中文](getting-started.zh-CN.md)
+
+本文覆盖 Matchday Mod、设备二维码、Python watcher 和可选局域网 TTS。官方
+Stack-chan host 的分区补丁、CJK 字体和首次烧录由
+[Host firmware 构建改动](../host/README.zh-CN.md)维护,本文不重复那组命令。
+
+## 安装结果
+
+完成后应有四个彼此独立的部分:
+
+1. 官方 Stack-chan host,加入 Matchday 所需的 `xs` 分区和可选 CJK 字体。
+2. 安装在 `xs` 分区中的 Matchday Mod。
+3. 在同一可信局域网持续运行的 Python watcher。
+4. 可选的局域网 TTS 服务;它不可用时视觉反馈和短提示音仍然工作。
+
+watcher 负责数据解析和文案,Mod 负责设备显示、语音、动作和手机设置中继。更新
+watcher 文案通常不需要重新烧录 host。
+
+## 环境要求
+
+- 基于 CoreS3、配备 16 MB Flash 的 Stack-chan,以及一根可传数据的 USB 线。
+- Git、Python 3.10+、Node.js 20+(上游已测试 Node.js 22)、npm 和 `xz`。
+- Moddable SDK 与 ESP-IDF;上游 `xs-dev` 会安装并检查它们。
+- 手机、watcher 电脑和 Stack-chan 位于同一个可信局域网。
+- 生成设备专属二维码时需要 `qrencode`。
+- 仓库自带的 `say` TTS 服务仅支持 macOS;其他系统可不启用语音,或提供兼容的
+ `/say` WAV 服务。
+
+下列命令适用于 macOS/Linux shell。先统一设置绝对路径:
+
+```sh
+mkdir -p "$HOME/src"
+export MATCHDAY_DIR="$HOME/src/stackchan-matchday"
+export STACKCHAN_DIR="$HOME/src/stack-chan"
+```
+
+每次打开新终端后,都需要重新导出这些变量。
+
+## 1. 克隆 Matchday
+
+```sh
+git clone https://github.com/xymeow/stackchan-matchday.git "$MATCHDAY_DIR"
+```
+
+如果已经克隆,请保留本地配置,更新代码后先查看
+[版本说明](releases/)再决定哪些组件需要重装。
+
+## 2. 准备并烧录 host
+
+严格按照 [host/README.zh-CN.md](../host/README.zh-CN.md) 操作。该文档维护以下
+版本绑定事实:
+
+- 经验证的上游 commit;
+- CoreS3 `xs` 分区补丁和 `0xDF0000` 偏移;
+- 可选 `StackChanCN-24` 字体资源;
+- 上游依赖检查、host 构建与烧录命令。
+
+每个新的上游 checkout 都要应用分区补丁。中文界面还要准备支持 CJK 的 TTF 并应用
+字体补丁。成功烧录一次后,仅更新 Matchday Mod 或 watcher 不必重复烧录 host。
+如果 `npm run doctor` 报告平台依赖缺失,请查阅上游
+[环境配置说明](https://github.com/stack-chan/stack-chan/blob/dev/v1.0/firmware/docs/getting-started.md),
+并在 `esp32` 被列为 supported target 后再构建。
+
+## 3. 生成设备二维码
+
+二维码作为静态 PNG 编译进 Mod,运行时不会自动重画。先给设备设置稳定的 DHCP
+保留地址、局域网 IP 或可解析的 mDNS 名称,然后在安装 Mod 前生成二维码:
+
+```sh
+export STACKCHAN_HOST=stackchan.local
+qrencode -s 4 -m 1 -o "$MATCHDAY_DIR/mod/assets/setup/setup-qr.png" \
+ "http://$STACKCHAN_HOST/setup"
+file "$MATCHDAY_DIR/mod/assets/setup/setup-qr.png"
+```
+
+宽和高都不能超过 168 px,否则标题和 URL 可能无法完整显示。如果 `file` 显示任一
+边超过 168 px,请改用 `-s 3` 重新生成。
+
+修改 `stackchan_host` 或屏幕上的 URL 不会改写这个 PNG。设备地址发生变化后,需要
+重新生成二维码并重装 Mod。
+
+## 4. 构建并安装 Mod
+
+### 调试协议安装
+
+在上游 `firmware/` 目录运行:
+
+```sh
+cd "$STACKCHAN_DIR/firmware"
+npm run mod --target=esp32:./platforms/m5stackchan_cores3 -- -f rgb565be \
+ "$MATCHDAY_DIR/mod/manifest.json"
+```
+
+CoreS3 必须使用 `-f rgb565be`,否则旗帜颜色会发生字节序错位。`npm run mod` 通过
+xsbug 调试协议安装,需要 xsbug 正在监听;设备忙时可能在写入中途卡住。
+
+### 推荐:直接写入 `xs` 分区
+
+不需要调试器时,先构建归档,再由 esptool 直接写入分区:
+
+```sh
+cd "$STACKCHAN_DIR/firmware"
+mcrun -d -m -p esp32:./platforms/m5stackchan_cores3 -t build -f rgb565be \
+ "$MATCHDAY_DIR/mod/manifest.json"
+python3 -m esptool --chip esp32s3 --before default-reset --after hard-reset \
+ write-flash 0xDF0000 "$MODDABLE/build/bin/esp32/debug/mod/mod.xsa"
+```
+
+`0xDF0000` 来自仓库的 host 分区补丁。esptool 会校验写入,复位后 host 直接挂载新
+归档。不要在没有确认分区表一致时把该偏移用于其他硬件或 host 构建。
+
+### 验收 Mod
+
+从 watcher 电脑检查:
+
+```sh
+curl "http://$STACKCHAN_HOST/health"
+curl "http://$STACKCHAN_HOST/api/status"
+```
+
+两者都应返回成功响应。若失败,请先确认设备已连入同一局域网,再查
+[排障与 FAQ](https://github.com/xymeow/stackchan-matchday/wiki)。
+
+## 5. 配置并启动 watcher
+
+复制示例配置:
+
+```sh
+cp "$MATCHDAY_DIR/config/kalshi_watchlist.example.json" \
+ "$MATCHDAY_DIR/config/kalshi_watchlist.json"
+```
+
+至少确认以下字段:
+
+- `stackchan_host` 与 `$STACKCHAN_HOST` 或设备局域网 IP 一致。
+- `stackchan_transport` 是 `http`;手机设置中继不支持串口模式。
+- `setup_server.enabled` 是 `true`。
+- 本机端口 `8788` 未被占用。默认绑定 `127.0.0.1`,不会暴露到局域网。
+
+检查 JSON 并持续运行 watcher:
+
+```sh
+python3 -m json.tool "$MATCHDAY_DIR/config/kalshi_watchlist.json"
+python3 "$MATCHDAY_DIR/tools/stackchan_kalshi_watch.py" \
+ --config "$MATCHDAY_DIR/config/kalshi_watchlist.json" --watch
+```
+
+示例中的 `KXEXAMPLE-...` 是有意保留的占位 ticker。在手机选择真实比赛或手动换成
+开放 ticker 之前,watcher 可能报告它们 missing。`--dry-run` 只阻止写入设备,
+仍会访问公开 API,不能当作离线安装验收。
+
+watcher 启动后,双击 Stack-chan 头顶触摸条或短按 Power 键,扫码选择比赛。页面
+应先显示“已提交,等待 watcher”,随后变为已确认。完整行为见
+[配置与播报](configuration.zh-CN.md)。
+
+## 6. 可选:启用局域网 TTS
+
+在 macOS 的第二个终端启动自带服务,前台运行便于看到错误:
+
+```sh
+export MATCHDAY_DIR="$HOME/src/stackchan-matchday"
+python3 "$MATCHDAY_DIR/tools/stackchan_tts_server.py" --host 0.0.0.0 --port 8787
+```
+
+保持该终端运行。在另一个终端先验收服务,再让设备连接 watcher 电脑的局域网地址,
+而不是 `127.0.0.1`:
+
+```sh
+curl "http://127.0.0.1:8787/health"
+export STACKCHAN_HOST=stackchan.local
+export WATCHER_HOST=192.168.1.20
+curl --request POST --data-binary "tts host $WATCHER_HOST:8787" \
+ "http://$STACKCHAN_HOST/api/command"
+curl --request POST --data-binary "say 比赛日准备好了" \
+ "http://$STACKCHAN_HOST/api/command"
+```
+
+请在电脑防火墙中允许 TCP `8787` 入站。用 `say -v '?'` 查看 macOS 已安装声音;
+`STACKCHAN_TTS_ZH_VOICE`、`STACKCHAN_TTS_EN_VOICE` 和
+`STACKCHAN_TTS_RATE` 可覆盖默认值。TTS 不可达时,Mod 会自动回退到短提示音。
+
+## 升级边界
+
+| 变更 | 通常需要更新 |
+| --- | --- |
+| watcher 文案、球员目录、解析和轮询 | watcher 代码与配置;重启 Python 进程 |
+| Mod 页面、设备动作、命令或资源 | 重新构建并安装 Mod |
+| `xs` 分区或 CJK host 字体 | 重新应用补丁并烧录 host |
+| macOS TTS 服务实现 | 重启 TTS 服务;不需要刷设备 |
+
+具体版本可能跨越多个边界。以对应的[版本说明](releases/)为准,不要仅根据版本号猜测
+是否需要刷机。
diff --git a/host/README.md b/host/README.md
index 26bd73d..e77d10f 100644
--- a/host/README.md
+++ b/host/README.md
@@ -78,6 +78,6 @@ mcconfig -d -m -p esp32:./platforms/m5stackchan_cores3 -t deploy \
"$PWD/stackchan/manifest_m5stackchan_cores3.json"
```
-After this one-time host flash, return to the root README to generate the
-device-specific QR and install the Matchday mod. Rebuilding only the mod does
-not require reflashing the host.
+After this one-time host flash, continue with [Getting started](../docs/getting-started.md)
+to generate the device-specific QR and install the Matchday mod. Rebuilding
+only the mod does not require reflashing the host.
diff --git a/host/README.zh-CN.md b/host/README.zh-CN.md
index 216d2a7..34d5a55 100644
--- a/host/README.zh-CN.md
+++ b/host/README.zh-CN.md
@@ -75,5 +75,5 @@ mcconfig -d -m -p esp32:./platforms/m5stackchan_cores3 -t deploy \
"$PWD/stackchan/manifest_m5stackchan_cores3.json"
```
-完成这次 host 烧录后,回到根 README 生成设备专属二维码并安装 Matchday Mod。
-以后仅重新构建 Mod 时,不需要再次烧录 host。
+完成这次 host 烧录后,继续阅读[安装指南](../docs/getting-started.zh-CN.md),生成设备专属二维码并
+安装 Matchday Mod。以后仅重新构建 Mod 时,不需要再次烧录 host。
diff --git a/tools/test_stackchan_commentary_style_contract.py b/tools/test_stackchan_commentary_style_contract.py
index 902e9bb..22dccad 100644
--- a/tools/test_stackchan_commentary_style_contract.py
+++ b/tools/test_stackchan_commentary_style_contract.py
@@ -43,8 +43,8 @@ def test_documented_style_endpoints_exist_in_both_services(self):
)
mod_web = (ROOT / "mod" / "web.js").read_text(encoding="utf-8")
documents = [
- (ROOT / "README.md").read_text(encoding="utf-8"),
- (ROOT / "README.zh-CN.md").read_text(encoding="utf-8"),
+ (ROOT / "docs" / "device-api.md").read_text(encoding="utf-8"),
+ (ROOT / "docs" / "device-api.zh-CN.md").read_text(encoding="utf-8"),
(ROOT / "docs" / "commentary-styles-prd.md").read_text(encoding="utf-8"),
(ROOT / "docs" / "releases" / "1.4.0.md").read_text(encoding="utf-8"),
]