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 Stack-chan Match Setup page showing language, commentary style, and upcoming matches
- English setup page for choosing language, commentary style, and a match. + Choose a match, perspective, position, language, and commentary style. -## System design - -![Stack-chan Matchday system design](docs/images/system-design.png) - -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. - - - - - - -
- A phone camera scanning the setup QR code displayed on Stack-chan
- Scan the on-device QR code to open the local setup page (example LAN address shown). -
- A hand holding Stack-chan's top touch bar while the device displays the Chinese mute confirmation
- 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 -``` +![Stack-chan Matchday system design](docs/images/system-design.png) -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 比分与文字直播,并通过 -## 系统设计 - -![Stack-chan Matchday 系统设计](docs/images/system-design.png) - -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)。
手机相机正在扫描 Stack-chan 屏幕上的设置二维码
- 用手机扫描设备二维码,打开局域网设置页(图中为示例地址)。 + 扫描设备二维码,打开局域网设置页。
手指长按 Stack-chan 头顶触摸条,设备显示静音和已静音提示
- 长按头顶触摸条进入静音;语音、动作和警报灯停止,视觉信息继续更新。 + 长按进入静音;视觉信息继续更新。
-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 -``` +## 系统设计 + +![Stack-chan Matchday 系统设计](docs/images/system-design.png) + +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. + + + + + + +
+ A phone camera scanning the setup QR code displayed on Stack-chan
+ Scan the on-device QR to open Match Setup on the local network. +
+ A hand holding Stack-chan's top touch bar while the device displays the Chinese mute confirmation
+ 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"), ]