Compare commits

58 Commits

Author SHA1 Message Date
30b25784c1 fix: lifecycle 2026-05-05 22:01:22 +02:00
0da0905697 fix(bluetooth_controller): cache for dispose 2026-05-05 21:26:58 +02:00
4fceb0c690 feat: better pairing broken notification 2026-05-05 21:06:11 +02:00
230a6838e0 fix: fix dfu id mismatch because stale notification 2026-05-05 20:44:31 +02:00
512c31d356 feat: snackbar for flash err + disconnect on dfurecovery end 2026-05-05 20:19:43 +02:00
f1491749d5 fix: bootloader device card persistence fix 2026-05-05 20:02:04 +02:00
073d825a3e dev: add some logging for dfu 2026-05-04 14:51:53 +02:00
bcccd03ecc feat: smarter firmware confirm via reconnect 2026-05-04 14:40:13 +02:00
16690dc216 feat: fw-update recovery flow 2026-05-04 13:54:19 +02:00
9b672a7503 feat: update optimizations 2026-05-04 13:18:03 +02:00
f5e5c3904f fix: fix disconnect when selecting firmware for dfu 2026-05-03 19:24:57 +02:00
3310387ec4 fix: show firmware update only after pairing 2026-05-01 15:11:51 +02:00
aa2d150300 feat: fullscreen OTA with back-block and warning 2026-05-01 15:06:46 +02:00
dc1f53b6e1 feat: recover bootloader OTA transfers 2026-04-29 19:59:11 +02:00
16365e1d04 fix: align bootloader image validation limits 2026-04-29 19:55:10 +02:00
09c686d542 docs: document bootloader OTA flow 2026-04-29 18:04:28 +02:00
06834a0cc0 feat: switch firmware updates to bootloader OTA 2026-04-29 18:02:48 +02:00
b673c9100d feat: add bootloader DFU protocol validation 2026-04-29 17:56:32 +02:00
eb26c759e8 refactor: remove direct trainer address assignment 2026-04-28 21:32:43 +02:00
5285c44173 feat: use shifter trainer scan flow 2026-04-28 21:31:52 +02:00
be1c39d5d7 feat: add shifter trainer scan service 2026-04-28 21:29:26 +02:00
7628947623 feat: add trainer scan protocol models 2026-04-28 21:28:40 +02:00
76b7195e5e fix: smooth scan RSSI readings 2026-04-28 20:38:33 +02:00
96416a2f73 fix(ios): show FTMS trainers advertised as 16-bit UUID 2026-04-28 20:25:30 +02:00
ac93c01cea feat: pairing ui 2026-04-28 20:22:15 +02:00
e3eba0bfc1 chore: refine ios pairing recovery copy 2026-04-28 20:13:18 +02:00
9922b90f49 fix(ios): open settings from pairing recovery 2026-04-28 20:06:10 +02:00
2e7c10f87d fix(pairing): pairing flow preempt status read fix 2026-04-28 19:56:54 +02:00
1f5ec5ebb2 fix(ios): ios bluetooth permission 2026-04-28 17:25:06 +02:00
84e026de52 Merge branch 'main' into ui-rework 2026-04-28 17:15:42 +02:00
2fa0447593 chore: ios dev files 2026-04-28 17:15:26 +02:00
57a14134a6 feat: ui rework and gear generator 2026-04-28 17:13:30 +02:00
82ea8125e1 feat: redesign and lots of progress 2026-04-26 22:43:22 +02:00
16ac66471a feat(ui): add gear preset shortcuts 2026-04-23 22:37:52 +02:00
ddaed084dc feat(ui): overhaul device details layout 2026-04-23 22:33:20 +02:00
87193c3ae9 feat(ui): restyle device scanning flows 2026-04-23 22:24:03 +02:00
9016b9de77 bd: backup 2026-04-23 20:24 2026-04-23 22:24:03 +02:00
7bb540c503 feat(ui): redesign devices and settings tabs 2026-04-23 22:06:20 +02:00
8cf6e95474 feat(ui): add themed shell navigation 2026-04-23 21:57:24 +02:00
bf67e9c2ae bd: backup 2026-04-23 19:57 2026-04-23 21:57:24 +02:00
cdd587c6de bd: backup 2026-04-23 19:42 2026-04-23 21:42:23 +02:00
65e295f16d feat: check for abawo manu data 2026-04-23 21:33:34 +02:00
b76503b144 docs(dfu): add operator guide and troubleshooting 2026-03-04 18:12:04 +01:00
bdcd200a62 test(dfu): cover retry failures and sequence wrap cases 2026-03-04 18:09:48 +01:00
2ac68e09ab bd: backup 2026-03-04 17:07 2026-03-04 18:07:18 +01:00
1dbbf191e6 feat(dfu): add firmware update controls to device page 2026-03-04 18:07:12 +01:00
32f258a492 bd: backup 2026-03-03 16:11 2026-03-03 17:11:51 +01:00
c581b4d92c feat(dfu): verify reconnect before reporting update success 2026-03-03 17:11:47 +01:00
aafa9928ac feat(dfu): add firmware file selection and validation 2026-03-03 17:06:54 +01:00
8b24084f97 feat(dfu): add firmware transfer engine with ack retries 2026-03-03 17:00:37 +01:00
dd2afa34ef bd: backup 2026-03-03 15:55 2026-03-03 16:55:11 +01:00
fb85565854 feat(dfu): add connection and MTU preflight checks 2026-03-03 16:54:56 +01:00
7a33e71410 feat(dfu): add packet codec and crc32 utilities 2026-03-03 16:48:49 +01:00
e704f27a96 feat(dfu): add protocol constants and progress models 2026-03-03 16:45:16 +01:00
08405c879b fix(devices details page): style fixes 2026-03-03 16:41:43 +01:00
76c0fbe237 bd: backup 2026-03-03 15:37 2026-03-03 16:37:50 +01:00
d3a2fe6613 bd: backup 2026-03-03 15:20 2026-03-03 16:20:28 +01:00
a673aa14b7 bd init: initialize beads issue tracking 2026-03-03 16:15:18 +01:00
368 changed files with 12300 additions and 1571 deletions

49
.beads/.gitignore vendored Normal file
View File

@ -0,0 +1,49 @@
# Dolt database (managed by Dolt, not git)
dolt/
dolt-access.lock
# Runtime files
bd.sock
bd.sock.startlock
sync-state.json
last-touched
# Local version tracking (prevents upgrade notification spam after git ops)
.local_version
# Worktree redirect file (contains relative path to main repo's .beads/)
# Must not be committed as paths would be wrong in other clones
redirect
# Sync state (local-only, per-machine)
# These files are machine-specific and should not be shared across clones
.sync.lock
export-state/
# Ephemeral store (SQLite - wisps/molecules, intentionally not versioned)
ephemeral.sqlite3
ephemeral.sqlite3-journal
ephemeral.sqlite3-wal
ephemeral.sqlite3-shm
# Dolt server management (auto-started by bd)
dolt-server.pid
dolt-server.log
dolt-server.lock
# Legacy files (from pre-Dolt versions)
*.db
*.db?*
*.db-journal
*.db-wal
*.db-shm
db.sqlite
bd.db
daemon.lock
daemon.log
daemon-*.log.gz
daemon.pid
# NOTE: Do NOT add negation patterns here.
# They would override fork protection in .git/info/exclude.
# Config files (metadata.json, config.yaml) are tracked by git by default
# since no pattern above ignores them.

81
.beads/README.md Normal file
View File

@ -0,0 +1,81 @@
# Beads - AI-Native Issue Tracking
Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code.
## What is Beads?
Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git.
**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads)
## Quick Start
### Essential Commands
```bash
# Create new issues
bd create "Add user authentication"
# View all issues
bd list
# View issue details
bd show <issue-id>
# Update issue status
bd update <issue-id> --claim
bd update <issue-id> --status done
# Sync with Dolt remote
bd dolt push
```
### Working with Issues
Issues in Beads are:
- **Git-native**: Stored in `.beads/issues.jsonl` and synced like code
- **AI-friendly**: CLI-first design works perfectly with AI coding agents
- **Branch-aware**: Issues can follow your branch workflow
- **Always in sync**: Auto-syncs with your commits
## Why Beads?
**AI-Native Design**
- Built specifically for AI-assisted development workflows
- CLI-first interface works seamlessly with AI coding agents
- No context switching to web UIs
🚀 **Developer Focused**
- Issues live in your repo, right next to your code
- Works offline, syncs when you push
- Fast, lightweight, and stays out of your way
🔧 **Git Integration**
- Automatic sync with git commits
- Branch-aware issue tracking
- Intelligent JSONL merge resolution
## Get Started with Beads
Try Beads in your own projects:
```bash
# Install Beads
curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash
# Initialize in your repo
bd init
# Create your first issue
bd create "Try out Beads"
```
## Learn More
- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs)
- **Quick Start Guide**: Run `bd quickstart`
- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples)
---
*Beads: Issue tracking that moves at the speed of thought*

View File

@ -0,0 +1,13 @@
{
"last_dolt_commit": "bqh2h35ln2mtcthhm8l4918e231lh9ld",
"last_event_id": 0,
"timestamp": "2026-04-23T20:24:03.015942859Z",
"counts": {
"issues": 16,
"events": 41,
"comments": 0,
"dependencies": 26,
"labels": 0,
"config": 11
}
}

View File

View File

@ -0,0 +1,11 @@
{"key":"auto_compact_enabled","value":"false"}
{"key":"compact_batch_size","value":"50"}
{"key":"compact_parallel_workers","value":"5"}
{"key":"compact_tier1_days","value":"30"}
{"key":"compact_tier1_dep_levels","value":"2"}
{"key":"compact_tier2_commits","value":"100"}
{"key":"compact_tier2_days","value":"90"}
{"key":"compact_tier2_dep_levels","value":"5"}
{"key":"compaction_enabled","value":"false"}
{"key":"issue_prefix","value":"abawo_bt_app"}
{"key":"schema_version","value":"6"}

View File

@ -0,0 +1,26 @@
{"created_at":"2026-03-03T16:38:33Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q","issue_id":"abawo_bt_app-20q.1","type":"parent-child"}
{"created_at":"2026-03-03T16:39:10Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q","issue_id":"abawo_bt_app-20q.2","type":"parent-child"}
{"created_at":"2026-03-03T16:39:47Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q.1","issue_id":"abawo_bt_app-20q.2","type":"blocks"}
{"created_at":"2026-03-03T16:39:10Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q","issue_id":"abawo_bt_app-20q.3","type":"parent-child"}
{"created_at":"2026-03-03T16:39:47Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q.4","issue_id":"abawo_bt_app-20q.3","type":"blocks"}
{"created_at":"2026-03-03T16:39:47Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q.7","issue_id":"abawo_bt_app-20q.3","type":"blocks"}
{"created_at":"2026-03-03T16:39:10Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q","issue_id":"abawo_bt_app-20q.4","type":"parent-child"}
{"created_at":"2026-03-03T16:39:10Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q","issue_id":"abawo_bt_app-20q.5","type":"parent-child"}
{"created_at":"2026-03-03T16:39:47Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q.7","issue_id":"abawo_bt_app-20q.5","type":"blocks"}
{"created_at":"2026-03-03T16:39:10Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q","issue_id":"abawo_bt_app-20q.6","type":"parent-child"}
{"created_at":"2026-03-03T16:39:47Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q.3","issue_id":"abawo_bt_app-20q.6","type":"blocks"}
{"created_at":"2026-03-03T16:39:47Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q.5","issue_id":"abawo_bt_app-20q.6","type":"blocks"}
{"created_at":"2026-03-03T16:39:47Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q.9","issue_id":"abawo_bt_app-20q.6","type":"blocks"}
{"created_at":"2026-03-03T16:39:17Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q","issue_id":"abawo_bt_app-20q.7","type":"parent-child"}
{"created_at":"2026-03-03T16:39:47Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q.1","issue_id":"abawo_bt_app-20q.7","type":"blocks"}
{"created_at":"2026-03-03T16:39:47Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q.2","issue_id":"abawo_bt_app-20q.7","type":"blocks"}
{"created_at":"2026-03-03T16:39:47Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q.8","issue_id":"abawo_bt_app-20q.7","type":"blocks"}
{"created_at":"2026-03-03T16:39:23Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q","issue_id":"abawo_bt_app-20q.8","type":"parent-child"}
{"created_at":"2026-03-03T16:39:47Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q.1","issue_id":"abawo_bt_app-20q.8","type":"blocks"}
{"created_at":"2026-03-03T16:39:28Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q","issue_id":"abawo_bt_app-20q.9","type":"parent-child"}
{"created_at":"2026-03-03T16:39:47Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-20q.7","issue_id":"abawo_bt_app-20q.9","type":"blocks"}
{"created_at":"2026-04-23T21:42:57Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-8eb","issue_id":"abawo_bt_app-55i","type":"discovered-from"}
{"created_at":"2026-04-23T21:42:57Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-8eb","issue_id":"abawo_bt_app-6c0","type":"discovered-from"}
{"created_at":"2026-04-23T21:42:57Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-8eb","issue_id":"abawo_bt_app-8b8","type":"discovered-from"}
{"created_at":"2026-04-23T21:42:57Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-8eb","issue_id":"abawo_bt_app-bhs","type":"discovered-from"}
{"created_at":"2026-04-23T21:42:57Z","created_by":"Yandrik","depends_on_id":"abawo_bt_app-8eb","issue_id":"abawo_bt_app-cy2","type":"discovered-from"}

View File

@ -0,0 +1,41 @@
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T16:37:50Z","event_type":"created","id":1,"issue_id":"abawo_bt_app-20q","new_value":"","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T16:38:33Z","event_type":"created","id":2,"issue_id":"abawo_bt_app-20q.1","new_value":"","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T16:39:10Z","event_type":"created","id":3,"issue_id":"abawo_bt_app-20q.2","new_value":"","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T16:39:10Z","event_type":"created","id":5,"issue_id":"abawo_bt_app-20q.3","new_value":"","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T16:39:10Z","event_type":"created","id":7,"issue_id":"abawo_bt_app-20q.4","new_value":"","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T16:39:10Z","event_type":"created","id":9,"issue_id":"abawo_bt_app-20q.5","new_value":"","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T16:39:10Z","event_type":"created","id":10,"issue_id":"abawo_bt_app-20q.6","new_value":"","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T16:39:17Z","event_type":"created","id":11,"issue_id":"abawo_bt_app-20q.7","new_value":"","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T16:39:23Z","event_type":"created","id":12,"issue_id":"abawo_bt_app-20q.8","new_value":"","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T16:39:28Z","event_type":"created","id":13,"issue_id":"abawo_bt_app-20q.9","new_value":"","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T16:43:26Z","event_type":"claimed","id":14,"issue_id":"abawo_bt_app-20q.1","new_value":"{\"assignee\":\"Yandrik\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"abawo_bt_app-20q.1\",\"title\":\"Add DFU protocol constants and domain models\",\"description\":\"Add protocol surface in app code for DFU support.\\n\\nWork:\\n- Add UUID constants for dfu_control (...40008), dfu_data (...40009), dfu_ack (...4000a)\\n- Add opcode/frame constants (START=0x01, FINISH=0x02, ABORT=0x03, frame size 64, payload size 63)\\n- Add flags constants and typed update-state/progress models used by service/UI\\n- Remove future magic numbers by centralizing constants\",\"acceptance_criteria\":\"- All protocol constants match spec exactly\\n- No duplicated literal DFU UUID/opcode values across services/UI\\n- Domain models compile and are ready for transfer engine integration\",\"status\":\"open\",\"priority\":1,\"issue_type\":\"task\",\"owner\":\"me@yandrik.dev\",\"created_at\":\"2026-03-03T15:38:34Z\",\"created_by\":\"Yandrik\",\"updated_at\":\"2026-03-03T15:38:34Z\"}"}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T16:45:19Z","event_type":"closed","id":15,"issue_id":"abawo_bt_app-20q.1","new_value":"Implemented DFU constants, flags, opcodes, and typed progress models in shifter_types.dart","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T16:45:29Z","event_type":"claimed","id":16,"issue_id":"abawo_bt_app-20q.2","new_value":"{\"assignee\":\"Yandrik\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"abawo_bt_app-20q.2\",\"title\":\"Implement DFU packet codec and CRC32 utilities with tests\",\"description\":\"Implement pure protocol helpers.\\n\\nWork:\\n- Build START payload (11 bytes, LE fields)\\n- Build FINISH/ABORT payloads\\n- Build/segment DATA frames (seq + 63-byte payload)\\n- Implement ACK/sequence helpers including wrapping behavior\\n- Implement CRC32 (ISO-HDLC reflected polynomial 0xEDB88320, init/final xor FFFFFFFF)\",\"acceptance_criteria\":\"- CRC test vector passes: \\\"123456789\\\" =\\u003e 0xCBF43926\\n- START/FINISH/ABORT encoders produce exact lengths/byte layout\\n- Frame segmentation handles final partial payload correctly\\n- Seq wrap and ack+1 rewind helpers covered by tests\",\"status\":\"open\",\"priority\":1,\"issue_type\":\"task\",\"owner\":\"me@yandrik.dev\",\"created_at\":\"2026-03-03T15:39:10Z\",\"created_by\":\"Yandrik\",\"updated_at\":\"2026-03-03T15:39:10Z\"}"}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T16:48:57Z","event_type":"closed","id":17,"issue_id":"abawo_bt_app-20q.2","new_value":"Added DFU protocol codec/CRC utilities with unit tests for payloads, frames, and sequence helpers","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T16:49:06Z","event_type":"claimed","id":18,"issue_id":"abawo_bt_app-20q.8","new_value":"{\"assignee\":\"Yandrik\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"abawo_bt_app-20q.8\",\"title\":\"Add BLE DFU preflight checks (MTU and connection readiness)\",\"description\":\"Add runtime guards required for protocol correctness.\\n\\nWork:\\n- Ensure active connection to target button before DFU start\\n- Request elevated MTU (e.g. 128/247) before upload\\n- Validate negotiated MTU supports 64-byte data writes (ATT payload requirement)\\n- Fail early with actionable message when transport preconditions are not met\",\"acceptance_criteria\":\"- Upload start is blocked when MTU/connection preconditions fail\\n- Error messages explain what failed and next step\\n- Preflight result is exposed for transfer start path\",\"status\":\"open\",\"priority\":1,\"issue_type\":\"task\",\"owner\":\"me@yandrik.dev\",\"created_at\":\"2026-03-03T15:39:24Z\",\"created_by\":\"Yandrik\",\"updated_at\":\"2026-03-03T15:39:24Z\"}"}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T16:55:10Z","event_type":"closed","id":19,"issue_id":"abawo_bt_app-20q.8","new_value":"Implemented DFU preflight checks for connection state and negotiated MTU with typed results and tests","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T16:55:19Z","event_type":"claimed","id":20,"issue_id":"abawo_bt_app-20q.7","new_value":"{\"assignee\":\"Yandrik\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"abawo_bt_app-20q.7\",\"title\":\"Implement BLE DFU transfer engine with cumulative ACK retransmit\",\"description\":\"Build the runtime transfer engine used by UI.\\n\\nWork:\\n- Subscribe to dfu_ack indications before START\\n- Send START and require initial ACK 0xFF\\n- Stream dfu_data using write without response in windows (configurable, default 8)\\n- Track cumulative ACK; on stall/timeout rewind to ack+1 (wrapping aware)\\n- Handle invalid/no-progress scenarios with bounded retries\\n- Send FINISH after full acked upload\\n- Support ABORT for cancellation and terminal error cleanup\\n- Emit state/progress stream for UI\",\"acceptance_criteria\":\"- Happy path reaches done with full ACKed transfer\\n- Loss/stall path retransmits and recovers correctly\\n- Cancel triggers ABORT and returns to idle cleanly\\n- Engine surfaces explicit error reasons for UI\",\"status\":\"open\",\"priority\":1,\"issue_type\":\"feature\",\"owner\":\"me@yandrik.dev\",\"created_at\":\"2026-03-03T15:39:18Z\",\"created_by\":\"Yandrik\",\"updated_at\":\"2026-03-03T15:39:18Z\"}"}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T17:00:44Z","event_type":"closed","id":21,"issue_id":"abawo_bt_app-20q.7","new_value":"Implemented firmware transfer engine with preflight, cumulative ACK handling, retries, cancel ABORT, and unit tests","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T17:01:01Z","event_type":"claimed","id":22,"issue_id":"abawo_bt_app-20q.4","new_value":"{\"assignee\":\"Yandrik\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"abawo_bt_app-20q.4\",\"title\":\"Add firmware file selection and binary validation flow\",\"description\":\"Implement local firmware artifact input for v1.\\n\\nWork:\\n- Integrate file picker for local firmware .bin\\n- Read bytes safely and validate non-empty payload\\n- Guard against malformed selections and unsupported files\\n- Compute total_len and crc32 from selected bytes\\n- Generate per-session session_id and set flags=0x00 for v1\",\"acceptance_criteria\":\"- User can select .bin and app obtains byte payload\\n- Validation errors are explicit and user-facing\\n- Metadata (size/crc/session) is available to transfer engine\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"owner\":\"me@yandrik.dev\",\"created_at\":\"2026-03-03T15:39:10Z\",\"created_by\":\"Yandrik\",\"updated_at\":\"2026-03-03T15:39:10Z\"}"}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T17:04:51Z","event_type":"closed","id":23,"issue_id":"abawo_bt_app-20q.4","new_value":"Completed","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T17:07:21Z","event_type":"claimed","id":24,"issue_id":"abawo_bt_app-20q.9","new_value":"{\"assignee\":\"Yandrik\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"abawo_bt_app-20q.9\",\"title\":\"Handle post-FINISH disconnect/reboot and reconnect verification\",\"description\":\"Implement robust completion handling around expected device reset.\\n\\nWork:\\n- Treat disconnect after successful FINISH as expected behavior\\n- Reconnect with update-specific timeout strategy\\n- Verify device is reachable/readable after reconnect\\n- Surface success only after reconnect verification path\\n- Document limitation: no strict firmware version compare until firmware exposes version characteristic\",\"acceptance_criteria\":\"- Expected reset does not appear as generic failure\\n- Reconnect path is attempted and result is surfaced\\n- Completion criteria are consistent with v1 definition\",\"status\":\"open\",\"priority\":1,\"issue_type\":\"task\",\"owner\":\"me@yandrik.dev\",\"created_at\":\"2026-03-03T15:39:28Z\",\"created_by\":\"Yandrik\",\"updated_at\":\"2026-03-03T15:39:28Z\"}"}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T17:12:02Z","event_type":"closed","id":25,"issue_id":"abawo_bt_app-20q.9","new_value":"Added post-FINISH reset disconnect, reconnect, and reachability verification before marking update complete","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-03T17:12:17Z","event_type":"claimed","id":26,"issue_id":"abawo_bt_app-20q.3","new_value":"{\"assignee\":\"Yandrik\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"abawo_bt_app-20q.3\",\"title\":\"Integrate firmware update UI into device details page\",\"description\":\"Add user-facing update controls and status presentation.\\n\\nWork:\\n- Add update card with Select Firmware, Start Update, Cancel\\n- Show phase text, progress %, bytes sent/acked, and retry status\\n- Disable conflicting actions (gear writes / connect button-to-bike) during DFU\\n- Show explicit reboot expectation after FINISH\\n- Persist/clear transient state correctly on page lifecycle changes\",\"acceptance_criteria\":\"- UI can run full update flow start-to-finish\\n- Progress/state transitions are visible and consistent\\n- Conflicting controls are disabled during active transfer\\n- Failures and cancellations are clearly shown\",\"status\":\"open\",\"priority\":1,\"issue_type\":\"feature\",\"owner\":\"me@yandrik.dev\",\"created_at\":\"2026-03-03T15:39:10Z\",\"created_by\":\"Yandrik\",\"updated_at\":\"2026-03-03T15:39:10Z\"}"}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-04T18:07:17Z","event_type":"closed","id":27,"issue_id":"abawo_bt_app-20q.3","new_value":"Integrated firmware file selection, update controls, progress display, and DFU state handling into device details page","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-04T18:07:26Z","event_type":"claimed","id":28,"issue_id":"abawo_bt_app-20q.5","new_value":"{\"assignee\":\"Yandrik\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"abawo_bt_app-20q.5\",\"title\":\"Add DFU test suite for happy path, loss, stalls, and cancel\",\"description\":\"Add targeted tests for protocol and engine behavior.\\n\\nWork:\\n- Unit tests for codec + CRC + sequence helpers\\n- Engine tests with mocked BLE ack stream for:\\n - happy path\\n - dropped frame / stalled ACK and rewind\\n - timeout and bounded retry fail\\n - cancel/abort cleanup\\n- Ensure deterministic tests for wrap-around sequence scenarios\",\"acceptance_criteria\":\"- Tests cover success and critical failure/retry paths\\n- Wrap-around and ack rewind behavior is validated\\n- Regressions in sequencing/CRC are caught automatically\",\"status\":\"open\",\"priority\":1,\"issue_type\":\"task\",\"owner\":\"me@yandrik.dev\",\"created_at\":\"2026-03-03T15:39:10Z\",\"created_by\":\"Yandrik\",\"updated_at\":\"2026-03-03T15:39:10Z\"}"}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-04T18:09:54Z","event_type":"closed","id":29,"issue_id":"abawo_bt_app-20q.5","new_value":"Expanded DFU tests to cover bounded retry timeout failure and deterministic sequence wrap-around behavior","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-04T18:10:13Z","event_type":"claimed","id":30,"issue_id":"abawo_bt_app-20q.6","new_value":"{\"assignee\":\"Yandrik\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"abawo_bt_app-20q.6\",\"title\":\"Document DFU v1 operator flow, troubleshooting, and constraints\",\"description\":\"Document how to use and support the new updater.\\n\\nWork:\\n- Add app-side DFU flow docs (select/start/progress/reboot/reconnect)\\n- Add troubleshooting matrix for common failures (MTU, stalled ACK, reconnect timeout, CRC mismatch)\\n- Record explicit v1 limitations and future security/version-verification roadmap\\n- Add manual QA checklist for release validation\",\"acceptance_criteria\":\"- Team can execute update and triage failures from docs\\n- v1 limitations are explicit and not ambiguous\\n- QA checklist is actionable and complete\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"chore\",\"owner\":\"me@yandrik.dev\",\"created_at\":\"2026-03-03T15:39:10Z\",\"created_by\":\"Yandrik\",\"updated_at\":\"2026-03-03T15:39:10Z\"}"}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-04T18:12:10Z","event_type":"closed","id":31,"issue_id":"abawo_bt_app-20q.6","new_value":"Added DFU v1 operator guide with troubleshooting matrix, limitations, and QA checklist; linked from README","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-03-04T18:12:11Z","event_type":"closed","id":32,"issue_id":"abawo_bt_app-20q","new_value":"all steps complete","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-04-23T21:42:23Z","event_type":"created","id":33,"issue_id":"abawo_bt_app-8eb","new_value":"","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-04-23T21:42:57Z","event_type":"claimed","id":34,"issue_id":"abawo_bt_app-8eb","new_value":"{\"assignee\":\"Yandrik\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"abawo_bt_app-8eb\",\"title\":\"UI overhaul from mockups\",\"description\":\"App-wide UI reskin based on the provided mockups. Scope: shared theme tokens, shell navigation with Devices and Settings, Devices tab redesign, Settings redesign with theme selection, Connect Device page restyle, Device Details reskin, gear ratio preset additions and restyle, trainer popup restyle, and per-phase verification/commits.\",\"status\":\"open\",\"priority\":1,\"issue_type\":\"epic\",\"owner\":\"me@yandrik.dev\",\"created_at\":\"2026-04-23T19:42:23Z\",\"created_by\":\"Yandrik\",\"updated_at\":\"2026-04-23T19:42:23Z\"}"}
{"actor":"Yandrik","comment":null,"created_at":"2026-04-23T21:42:57Z","event_type":"created","id":35,"issue_id":"abawo_bt_app-cy2","new_value":"","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-04-23T21:42:57Z","event_type":"created","id":36,"issue_id":"abawo_bt_app-8b8","new_value":"","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-04-23T21:42:57Z","event_type":"created","id":37,"issue_id":"abawo_bt_app-55i","new_value":"","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-04-23T21:42:57Z","event_type":"created","id":38,"issue_id":"abawo_bt_app-bhs","new_value":"","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-04-23T21:42:57Z","event_type":"created","id":39,"issue_id":"abawo_bt_app-6c0","new_value":"","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-04-23T21:57:23Z","event_type":"claimed","id":40,"issue_id":"abawo_bt_app-cy2","new_value":"{\"assignee\":\"Yandrik\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"abawo_bt_app-cy2\",\"title\":\"Phase 1: theme and shell navigation\",\"description\":\"Implement shared app theme tokens, support system/light/dark theme selection, replace top-level navigation with Devices and Settings shell, retire Home as a destination, and keep Connect Device / Device Details as pushed routes.\",\"status\":\"open\",\"priority\":1,\"issue_type\":\"task\",\"owner\":\"me@yandrik.dev\",\"created_at\":\"2026-04-23T19:42:57Z\",\"created_by\":\"Yandrik\",\"updated_at\":\"2026-04-23T19:42:57Z\"}"}
{"actor":"Yandrik","comment":null,"created_at":"2026-04-23T21:57:37Z","event_type":"closed","id":41,"issue_id":"abawo_bt_app-cy2","new_value":"Completed","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-04-23T22:06:20Z","event_type":"claimed","id":42,"issue_id":"abawo_bt_app-8b8","new_value":"{\"assignee\":\"Yandrik\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"abawo_bt_app-8b8\",\"title\":\"Phase 2: devices and settings reskin\",\"description\":\"Redesign Devices tab and Settings tab using the new design system. Devices becomes the default tab and shows saved devices and add-device entry; Settings includes theme mode controls.\",\"status\":\"open\",\"priority\":1,\"issue_type\":\"task\",\"owner\":\"me@yandrik.dev\",\"created_at\":\"2026-04-23T19:42:57Z\",\"created_by\":\"Yandrik\",\"updated_at\":\"2026-04-23T19:42:57Z\"}"}
{"actor":"Yandrik","comment":null,"created_at":"2026-04-23T22:06:32Z","event_type":"closed","id":43,"issue_id":"abawo_bt_app-8b8","new_value":"Completed","old_value":""}
{"actor":"Yandrik","comment":null,"created_at":"2026-04-23T22:24:02Z","event_type":"claimed","id":44,"issue_id":"abawo_bt_app-55i","new_value":"{\"assignee\":\"Yandrik\",\"status\":\"in_progress\"}","old_value":"{\"id\":\"abawo_bt_app-55i\",\"title\":\"Phase 3: connect flow and popup restyle\",\"description\":\"Restyle the Connect Device page and trainer assignment popup to match the new visual language while preserving the existing BLE behavior.\",\"status\":\"open\",\"priority\":1,\"issue_type\":\"task\",\"owner\":\"me@yandrik.dev\",\"created_at\":\"2026-04-23T19:42:57Z\",\"created_by\":\"Yandrik\",\"updated_at\":\"2026-04-23T19:42:57Z\"}"}

View File

@ -0,0 +1,16 @@
{"acceptance_criteria":"- User can select a firmware .bin and complete upload end-to-end\n- Upload follows protocol and handles packet loss via retransmit\n- App shows clear progress and failure states\n- Device reconnects after reboot and is reachable","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"all steps complete","closed_at":"2026-03-04T17:12:11Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"c3bd8fa9b5b9d51b04cde5e11c2d8cdedaca29e049a64e8521172a69093b4ba3","created_at":"2026-03-03T15:37:50Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Implement the firmware update flow defined in universal-shifters/update-process.md for the Flutter app.\n\nScope:\n- Manual local .bin selection and upload over BLE GATT\n- START/DATA/FINISH/ABORT protocol support\n- Cumulative ACK handling with retransmit\n- Expected reboot/disconnect handling and reconnect check\n\nOut of scope for v1:\n- Hosted firmware distribution/backend\n- Cryptographic signature verification in app\n- Encrypted payload transport mode","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q","is_template":0,"issue_type":"epic","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement Universal Shifters BLE DFU v1 in app (manual .bin upload)","updated_at":"2026-03-04T17:12:11Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- All protocol constants match spec exactly\n- No duplicated literal DFU UUID/opcode values across services/UI\n- Domain models compile and are ready for transfer engine integration","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Implemented DFU constants, flags, opcodes, and typed progress models in shifter_types.dart","closed_at":"2026-03-03T15:45:20Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"d471157a1af6199f0ac0a1565b05b28d1a477e66d9166a3865edb42ea1e8c2ae","created_at":"2026-03-03T15:38:34Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Add protocol surface in app code for DFU support.\n\nWork:\n- Add UUID constants for dfu_control (...40008), dfu_data (...40009), dfu_ack (...4000a)\n- Add opcode/frame constants (START=0x01, FINISH=0x02, ABORT=0x03, frame size 64, payload size 63)\n- Add flags constants and typed update-state/progress models used by service/UI\n- Remove future magic numbers by centralizing constants","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.1","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add DFU protocol constants and domain models","updated_at":"2026-03-03T15:45:20Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- CRC test vector passes: \"123456789\" =\u003e 0xCBF43926\n- START/FINISH/ABORT encoders produce exact lengths/byte layout\n- Frame segmentation handles final partial payload correctly\n- Seq wrap and ack+1 rewind helpers covered by tests","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Added DFU protocol codec/CRC utilities with unit tests for payloads, frames, and sequence helpers","closed_at":"2026-03-03T15:48:57Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"9ba609e63c9e2f8a4e95de6795dace11c610aa9c13908b5dab7aa6cb83ab58d4","created_at":"2026-03-03T15:39:10Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Implement pure protocol helpers.\n\nWork:\n- Build START payload (11 bytes, LE fields)\n- Build FINISH/ABORT payloads\n- Build/segment DATA frames (seq + 63-byte payload)\n- Implement ACK/sequence helpers including wrapping behavior\n- Implement CRC32 (ISO-HDLC reflected polynomial 0xEDB88320, init/final xor FFFFFFFF)","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.2","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement DFU packet codec and CRC32 utilities with tests","updated_at":"2026-03-03T15:48:57Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- UI can run full update flow start-to-finish\n- Progress/state transitions are visible and consistent\n- Conflicting controls are disabled during active transfer\n- Failures and cancellations are clearly shown","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Integrated firmware file selection, update controls, progress display, and DFU state handling into device details page","closed_at":"2026-03-04T17:07:18Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"e8d2f905536263a05de5e7e86bf8f02fa0491d7129e30f36f2c4dd09acab1882","created_at":"2026-03-03T15:39:10Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Add user-facing update controls and status presentation.\n\nWork:\n- Add update card with Select Firmware, Start Update, Cancel\n- Show phase text, progress %, bytes sent/acked, and retry status\n- Disable conflicting actions (gear writes / connect button-to-bike) during DFU\n- Show explicit reboot expectation after FINISH\n- Persist/clear transient state correctly on page lifecycle changes","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.3","is_template":0,"issue_type":"feature","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Integrate firmware update UI into device details page","updated_at":"2026-03-04T17:07:18Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- User can select .bin and app obtains byte payload\n- Validation errors are explicit and user-facing\n- Metadata (size/crc/session) is available to transfer engine","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Completed","closed_at":"2026-03-03T16:04:51Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"be8d8646593e4fc6c269383efb41808c40ffdfb3103e58a90acc0142e32f4711","created_at":"2026-03-03T15:39:10Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Implement local firmware artifact input for v1.\n\nWork:\n- Integrate file picker for local firmware .bin\n- Read bytes safely and validate non-empty payload\n- Guard against malformed selections and unsupported files\n- Compute total_len and crc32 from selected bytes\n- Generate per-session session_id and set flags=0x00 for v1","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.4","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add firmware file selection and binary validation flow","updated_at":"2026-03-03T16:04:51Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- Tests cover success and critical failure/retry paths\n- Wrap-around and ack rewind behavior is validated\n- Regressions in sequencing/CRC are caught automatically","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Expanded DFU tests to cover bounded retry timeout failure and deterministic sequence wrap-around behavior","closed_at":"2026-03-04T17:09:54Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"f784095261bfc41eed5544a567f667bb2ba4816433f1d5d6ae21ea4616c7109a","created_at":"2026-03-03T15:39:10Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Add targeted tests for protocol and engine behavior.\n\nWork:\n- Unit tests for codec + CRC + sequence helpers\n- Engine tests with mocked BLE ack stream for:\n - happy path\n - dropped frame / stalled ACK and rewind\n - timeout and bounded retry fail\n - cancel/abort cleanup\n- Ensure deterministic tests for wrap-around sequence scenarios","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.5","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add DFU test suite for happy path, loss, stalls, and cancel","updated_at":"2026-03-04T17:09:54Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- Team can execute update and triage failures from docs\n- v1 limitations are explicit and not ambiguous\n- QA checklist is actionable and complete","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Added DFU v1 operator guide with troubleshooting matrix, limitations, and QA checklist; linked from README","closed_at":"2026-03-04T17:12:11Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"3a5dd3fd4d901ae53a80b3c7b488b41db5e09f7cc50da0e6c185c075b8ad7e51","created_at":"2026-03-03T15:39:10Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Document how to use and support the new updater.\n\nWork:\n- Add app-side DFU flow docs (select/start/progress/reboot/reconnect)\n- Add troubleshooting matrix for common failures (MTU, stalled ACK, reconnect timeout, CRC mismatch)\n- Record explicit v1 limitations and future security/version-verification roadmap\n- Add manual QA checklist for release validation","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.6","is_template":0,"issue_type":"chore","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":2,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Document DFU v1 operator flow, troubleshooting, and constraints","updated_at":"2026-03-04T17:12:11Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- Happy path reaches done with full ACKed transfer\n- Loss/stall path retransmits and recovers correctly\n- Cancel triggers ABORT and returns to idle cleanly\n- Engine surfaces explicit error reasons for UI","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Implemented firmware transfer engine with preflight, cumulative ACK handling, retries, cancel ABORT, and unit tests","closed_at":"2026-03-03T16:00:45Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"042d39f373b05c758d2d2724c757a7e790de522c2def68f8f12e9f2fbbb70dc6","created_at":"2026-03-03T15:39:18Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Build the runtime transfer engine used by UI.\n\nWork:\n- Subscribe to dfu_ack indications before START\n- Send START and require initial ACK 0xFF\n- Stream dfu_data using write without response in windows (configurable, default 8)\n- Track cumulative ACK; on stall/timeout rewind to ack+1 (wrapping aware)\n- Handle invalid/no-progress scenarios with bounded retries\n- Send FINISH after full acked upload\n- Support ABORT for cancellation and terminal error cleanup\n- Emit state/progress stream for UI","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.7","is_template":0,"issue_type":"feature","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Implement BLE DFU transfer engine with cumulative ACK retransmit","updated_at":"2026-03-03T16:00:45Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- Upload start is blocked when MTU/connection preconditions fail\n- Error messages explain what failed and next step\n- Preflight result is exposed for transfer start path","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Implemented DFU preflight checks for connection state and negotiated MTU with typed results and tests","closed_at":"2026-03-03T15:55:11Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"558fa59b42c42b122eefc43d3483d1a61bdd995464dbc36d166f763f42366e87","created_at":"2026-03-03T15:39:24Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Add runtime guards required for protocol correctness.\n\nWork:\n- Ensure active connection to target button before DFU start\n- Request elevated MTU (e.g. 128/247) before upload\n- Validate negotiated MTU supports 64-byte data writes (ATT payload requirement)\n- Fail early with actionable message when transport preconditions are not met","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.8","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Add BLE DFU preflight checks (MTU and connection readiness)","updated_at":"2026-03-03T15:55:11Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"- Expected reset does not appear as generic failure\n- Reconnect path is attempted and result is surfaced\n- Completion criteria are consistent with v1 definition","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Added post-FINISH reset disconnect, reconnect, and reachability verification before marking update complete","closed_at":"2026-03-03T16:12:02Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"024e8aa245b93098013fbd66102a621c9ecd58a1dda9fc276c74cd278bdd3512","created_at":"2026-03-03T15:39:28Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Implement robust completion handling around expected device reset.\n\nWork:\n- Treat disconnect after successful FINISH as expected behavior\n- Reconnect with update-specific timeout strategy\n- Verify device is reachable/readable after reconnect\n- Surface success only after reconnect verification path\n- Document limitation: no strict firmware version compare until firmware exposes version characteristic","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-20q.9","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Handle post-FINISH disconnect/reboot and reconnect verification","updated_at":"2026-03-03T16:12:02Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"239a18c9fae52747eae5d68a88cce6c1df568115003ba8b79dfe72abdd73a00d","created_at":"2026-04-23T19:42:57Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Restyle the Connect Device page and trainer assignment popup to match the new visual language while preserving the existing BLE behavior.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-55i","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"in_progress","target":"","timeout_ns":0,"title":"Phase 3: connect flow and popup restyle","updated_at":"2026-04-23T20:24:03Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"6bd716ab35225df8c9aadf4fb110a580ea3d8ac9d4987f0d52b99f661914292b","created_at":"2026-04-23T19:42:57Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Add Road, Gravel, and MTB presets, keep the current load behavior for presets, and finish shared component polish plus final analyze pass.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-6c0","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Phase 5: gear presets and polish","updated_at":"2026-04-23T19:42:57Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Completed","closed_at":"2026-04-23T20:06:32Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"ebc1040b3fc86b322ce87b736e76fc90beb04aa4cc6d81bec54d195fc9d471ce","created_at":"2026-04-23T19:42:57Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Redesign Devices tab and Settings tab using the new design system. Devices becomes the default tab and shows saved devices and add-device entry; Settings includes theme mode controls.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-8b8","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Phase 2: devices and settings reskin","updated_at":"2026-04-23T20:06:32Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"c636ac111ccf498970d1002590f65a5dd910551317916545dee637887fd7803b","created_at":"2026-04-23T19:42:23Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"App-wide UI reskin based on the provided mockups. Scope: shared theme tokens, shell navigation with Devices and Settings, Devices tab redesign, Settings redesign with theme selection, Connect Device page restyle, Device Details reskin, gear ratio preset additions and restyle, trainer popup restyle, and per-phase verification/commits.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-8eb","is_template":0,"issue_type":"epic","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"in_progress","target":"","timeout_ns":0,"title":"UI overhaul from mockups","updated_at":"2026-04-23T19:42:57Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"","actor":"","agent_state":"","assignee":null,"await_id":"","await_type":"","close_reason":"","closed_at":null,"closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"7ab3e9926f20344dfe55e09ec252d5e1cfac023357b09abb096e84d524cce310","created_at":"2026-04-23T19:42:57Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Reskin Device Details, preserving existing behavior for status, reconnect, firmware, and gear ratio editing while adapting the layout to the new mockup direction.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-bhs","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"open","target":"","timeout_ns":0,"title":"Phase 4: device details overhaul","updated_at":"2026-04-23T19:42:57Z","waiters":"","wisp_type":"","work_type":""}
{"acceptance_criteria":"","actor":"","agent_state":"","assignee":"Yandrik","await_id":"","await_type":"","close_reason":"Completed","closed_at":"2026-04-23T19:57:37Z","closed_by_session":"","compacted_at":null,"compacted_at_commit":null,"compaction_level":0,"content_hash":"014e2491f64f1273f482c9eb4259dad0eed5ea81030ebb21e91222dee3fd854a","created_at":"2026-04-23T19:42:57Z","created_by":"Yandrik","crystallizes":0,"defer_until":null,"description":"Implement shared app theme tokens, support system/light/dark theme selection, replace top-level navigation with Devices and Settings shell, retire Home as a destination, and keep Connect Device / Device Details as pushed routes.","design":"","due_at":null,"ephemeral":0,"estimated_minutes":null,"event_kind":"","external_ref":null,"hook_bead":"","id":"abawo_bt_app-cy2","is_template":0,"issue_type":"task","last_activity":null,"metadata":"{}","mol_type":"","notes":"","original_size":null,"owner":"me@yandrik.dev","payload":"","pinned":0,"priority":1,"quality_score":null,"rig":"","role_bead":"","role_type":"","sender":"","source_repo":"","source_system":"","spec_id":"","status":"closed","target":"","timeout_ns":0,"title":"Phase 1: theme and shell navigation","updated_at":"2026-04-23T19:57:37Z","waiters":"","wisp_type":"","work_type":""}

View File

55
.beads/config.yaml Normal file
View File

@ -0,0 +1,55 @@
# Beads Configuration File
# This file configures default behavior for all bd commands in this repository
# All settings can also be set via environment variables (BD_* prefix)
# or overridden with command-line flags
# Issue prefix for this repository (used by bd init)
# If not set, bd init will auto-detect from directory name
# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc.
# issue-prefix: ""
# Use no-db mode: load from JSONL, write back after each command
# When true, bd will use .beads/issues.jsonl as the source of truth
# instead of the Dolt database
# no-db: false
# Enable JSON output by default
# json: false
# Feedback title formatting for mutating commands (create/update/close/dep/edit)
# 0 = hide titles, N > 0 = truncate to N characters
# output:
# title-length: 255
# Default actor for audit trails (overridden by BD_ACTOR or --actor)
# actor: ""
# Export events (audit trail) to .beads/events.jsonl on each flush/sync
# When enabled, new events are appended incrementally using a high-water mark.
# Use 'bd export --events' to trigger manually regardless of this setting.
# events-export: false
# Multi-repo configuration (experimental - bd-307)
# Allows hydrating from multiple repositories and routing writes to the correct JSONL
# repos:
# primary: "." # Primary repo (where this database lives)
# additional: # Additional repos to hydrate from (read-only)
# - ~/beads-planning # Personal planning repo
# - ~/work-planning # Work planning repo
# JSONL backup (periodic export for off-machine recovery)
# Auto-enabled when a git remote exists. Override explicitly:
# backup:
# enabled: false # Disable auto-backup entirely
# interval: 15m # Minimum time between auto-exports
# git-push: false # Disable git push (export locally only)
# git-repo: "" # Separate git repo for backups (default: project repo)
# Integration settings (access with 'bd config get/set')
# These are stored in the database, not in this file:
# - jira.url
# - jira.project
# - linear.url
# - linear.api-key
# - github.org
# - github.repo

1
.beads/dolt-monitor.pid Normal file
View File

@ -0,0 +1 @@
1720108

9
.beads/hooks/post-checkout Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v0.57.0 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
bd hooks run post-checkout "$@"
_bd_exit=$?; if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION ---

9
.beads/hooks/post-merge Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v0.57.0 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
bd hooks run post-merge "$@"
_bd_exit=$?; if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION ---

9
.beads/hooks/pre-commit Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v0.57.0 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
bd hooks run pre-commit "$@"
_bd_exit=$?; if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION ---

9
.beads/hooks/pre-push Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v0.57.0 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
bd hooks run pre-push "$@"
_bd_exit=$?; if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION ---

View File

@ -0,0 +1,9 @@
#!/usr/bin/env sh
# --- BEGIN BEADS INTEGRATION v0.57.0 ---
# This section is managed by beads. Do not remove these markers.
if command -v bd >/dev/null 2>&1; then
export BD_GIT_HOOK=1
bd hooks run prepare-commit-msg "$@"
_bd_exit=$?; if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi
fi
# --- END BEADS INTEGRATION ---

View File

6
.beads/metadata.json Normal file
View File

@ -0,0 +1,6 @@
{
"database": "dolt",
"backend": "dolt",
"dolt_mode": "server",
"dolt_database": "abawo_bt_app"
}

4
.gitignore vendored
View File

@ -44,3 +44,7 @@ app.*.map.json
/android/app/profile /android/app/profile
/android/app/release /android/app/release
.aider* .aider*
# Dolt database files (added by bd init)
.dolt/
*.db

0
AGENTS.md Normal file
View File

View File

@ -41,3 +41,6 @@ Still mostly material design.
### Company Color Theme ### Company Color Theme
todo todo
## Code
Always use `<color>.withValues(alpha: <alpha>)` instead of `<color>.withOpacity(<alpha>)` for colors.

View File

@ -2,6 +2,10 @@
A new Flutter project. A new Flutter project.
## Operational Docs
- [Bootloader OTA Operator Guide](docs/bootloader-ota-operator-guide.md)
## Getting Started ## Getting Started
This project is a starting point for a Flutter application. This project is a starting point for a Flutter application.

View File

@ -9,6 +9,12 @@
# packages, and plugins designed to encourage good coding practices. # packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml
analyzer:
errors:
invalid_annotation_target: ignore
plugins:
- custom_lint
linter: linter:
# The lint rules applied to this project can be customized in the # The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml` # section below to disable rules from the `package:flutter_lints/flutter.yaml`
@ -23,12 +29,5 @@ linter:
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule # avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
errors:
invalid_annotation_target: ignore
plugins:
- custom_lint
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options

View File

@ -42,3 +42,7 @@ android {
flutter { flutter {
source = "../.." source = "../.."
} }
dependencies {
implementation "io.reactivex.rxjava2:rxjava:2.2.21"
}

View File

@ -1,5 +1,26 @@
package com.example.abawo_bt_app package com.example.abawo_bt_app
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.reactivex.exceptions.UndeliverableException
import io.reactivex.plugins.RxJavaPlugins
class MainActivity: FlutterActivity() class MainActivity: FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
RxJavaPlugins.setErrorHandler { throwable ->
val error = if (throwable is UndeliverableException && throwable.cause != null) {
throwable.cause!!
} else {
throwable
}
val className = error.javaClass.name
val message = error.message.orEmpty()
if (className.contains("BleGatt") || message.contains("GATT exception")) {
return@setErrorHandler
}
Thread.currentThread().uncaughtExceptionHandler
?.uncaughtException(Thread.currentThread(), error)
}
super.onCreate(savedInstanceState)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,492 @@
from __future__ import annotations
import argparse
import csv
import math
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path
from tempfile import TemporaryDirectory
import numpy as np
from PIL import Image, ImageOps
@dataclass(frozen=True)
class Canvas:
width: int = 588
height: int = 495
cx: float = 304.0
cy: float = 256.0
@dataclass(frozen=True)
class Style:
background: str = "#3d3d3d"
material: str = "#eeeeee"
outline: str = "#f4f4f4"
outline_width: float = 3.0
@dataclass(frozen=True)
class Geometry:
root_radius: float = 213.0
tooth_depth: float = 17.5
reference_teeth: int = 60
pitch_radius_offset: float = 8.0
tooth_sharpness: float = 1.32
low_tooth_threshold: int = 25
low_tooth_half_width: float = 0.30
low_tooth_cusp: float = 0.55
central_hole_radius: float = 46.0
bolt_hole_radius: float = 16.5
material_margin: float = 8.0
bolt_hole_min_teeth: int = 25
decorative_cutout_min_scale: float = 0.62
samples_per_tooth: int = 28
@dataclass(frozen=True)
class SprocketSizing:
pitch_radius: float
calculated_root_radius: float
root_radius: float
tooth_depth: float
outer_radius: float
decorative_scale: float
bolt_scale: float
include_bolt_holes: bool
include_decorative_cutouts: bool
@dataclass(frozen=True)
class Cutout:
kind: str
cx: float
cy: float
rx: float
ry: float | None = None
rotation: float = 0.0
CANVAS = Canvas()
STYLE = Style()
GEOMETRY = Geometry()
def fmt(value: float) -> str:
return f"{value:.3f}".rstrip("0").rstrip(".")
def polygon_path(points: list[tuple[float, float]]) -> str:
start = points[0]
rest = points[1:]
commands = [f"M {fmt(start[0])} {fmt(start[1])}"]
commands.extend(f"L {fmt(x)} {fmt(y)}" for x, y in rest)
commands.append("Z")
return " ".join(commands)
def circle_points(cx: float, cy: float, radius: float, samples: int = 144) -> list[tuple[float, float]]:
return [
(cx + math.cos(t) * radius, cy + math.sin(t) * radius)
for t in np.linspace(0.0, 2.0 * math.pi, samples, endpoint=False)
]
def ellipse_points(
cx: float,
cy: float,
rx: float,
ry: float,
rotation_deg: float = 0.0,
samples: int = 192,
) -> list[tuple[float, float]]:
rotation = math.radians(rotation_deg)
cos_r = math.cos(rotation)
sin_r = math.sin(rotation)
points = []
for t in np.linspace(0.0, 2.0 * math.pi, samples, endpoint=False):
x = math.cos(t) * rx
y = math.sin(t) * ry
points.append((cx + x * cos_r - y * sin_r, cy + x * sin_r + y * cos_r))
return points
DECORATIVE_CUTOUT_SPECS = (
("ellipse", 0.0, -141.0, 121.0, 41.0, 0.0),
("ellipse", -109.0, 58.0, 106.0, 41.0, 63.0),
("ellipse", 109.0, 58.0, 106.0, 41.0, -63.0),
)
BOLT_HOLE_OFFSETS = (
(-134.0, -76.0),
(134.0, -76.0),
(0.0, 157.0),
)
REFERENCE_BOLT_OFFSET_RADIUS = max(math.hypot(dx, dy) for dx, dy in BOLT_HOLE_OFFSETS)
def decorative_reference_extent() -> float:
extent = 0.0
for _, dx, dy, rx, ry, rotation in DECORATIVE_CUTOUT_SPECS:
points = ellipse_points(dx, dy, rx, ry, rotation, 240)
extent = max(extent, *(math.hypot(x, y) for x, y in points))
return extent
DECORATIVE_REFERENCE_EXTENT = decorative_reference_extent()
def clamp(value: float, minimum: float, maximum: float) -> float:
return max(minimum, min(maximum, value))
def calculated_pitch_radius(teeth: int, geometry: Geometry = GEOMETRY) -> float:
reference_pitch_radius = geometry.root_radius + geometry.pitch_radius_offset
return reference_pitch_radius * math.sin(math.pi / geometry.reference_teeth) / math.sin(math.pi / teeth)
def sprocket_sizing(teeth: int, geometry: Geometry = GEOMETRY) -> SprocketSizing:
pitch_radius = calculated_pitch_radius(teeth, geometry)
calculated_root_radius = pitch_radius - geometry.pitch_radius_offset
central_min_root_radius = geometry.central_hole_radius + geometry.material_margin
include_bolt_holes = teeth >= geometry.bolt_hole_min_teeth
if include_bolt_holes:
bolt_min_radius = geometry.central_hole_radius + geometry.bolt_hole_radius + geometry.material_margin
bolt_min_root_radius = bolt_min_radius + geometry.bolt_hole_radius + geometry.material_margin
min_root_radius = max(central_min_root_radius, bolt_min_root_radius)
else:
min_root_radius = central_min_root_radius
root_radius = max(calculated_root_radius, min_root_radius)
outer_radius = root_radius + geometry.tooth_depth
max_bolt_offset = max(0.0, root_radius - geometry.bolt_hole_radius - geometry.material_margin)
if include_bolt_holes:
min_bolt_offset = geometry.central_hole_radius + geometry.bolt_hole_radius + geometry.material_margin
bolt_offset = clamp(max_bolt_offset, min_bolt_offset, REFERENCE_BOLT_OFFSET_RADIUS)
bolt_scale = bolt_offset / REFERENCE_BOLT_OFFSET_RADIUS
else:
bolt_scale = 0.0
decorative_scale = clamp((root_radius - geometry.material_margin) / DECORATIVE_REFERENCE_EXTENT, 0.0, 1.0)
include_decorative_cutouts = decorative_scale >= geometry.decorative_cutout_min_scale
return SprocketSizing(
pitch_radius=pitch_radius,
calculated_root_radius=calculated_root_radius,
root_radius=root_radius,
tooth_depth=geometry.tooth_depth,
outer_radius=outer_radius,
decorative_scale=decorative_scale,
bolt_scale=bolt_scale,
include_bolt_holes=include_bolt_holes,
include_decorative_cutouts=include_decorative_cutouts,
)
def outer_points(
teeth: int,
geometry: Geometry = GEOMETRY,
canvas: Canvas = CANVAS,
radius_delta: float = 0.0,
) -> list[tuple[float, float]]:
samples = teeth * geometry.samples_per_tooth
sizing = sprocket_sizing(teeth, geometry)
points = []
phase_offset = -math.pi / 2.0
for index in range(samples):
theta = phase_offset + 2.0 * math.pi * index / samples
tooth_phase = (index % geometry.samples_per_tooth) / geometry.samples_per_tooth
radius = sizing.root_radius + sizing.tooth_depth * tooth_profile(teeth, tooth_phase, geometry) + radius_delta
points.append((canvas.cx + math.cos(theta) * radius, canvas.cy + math.sin(theta) * radius))
return points
def tooth_profile(teeth: int, phase: float, geometry: Geometry = GEOMETRY) -> float:
if teeth < geometry.low_tooth_threshold:
distance_from_peak = abs(phase - 0.5)
if distance_from_peak >= geometry.low_tooth_half_width:
return 0.0
return 1.0 - (distance_from_peak / geometry.low_tooth_half_width) ** geometry.low_tooth_cusp
smooth_profile = 0.5 - 0.5 * math.cos(2.0 * math.pi * phase)
return smooth_profile**geometry.tooth_sharpness
def offset_cutout(
kind: str,
dx: float,
dy: float,
rx: float,
ry: float | None,
rotation: float,
center_scale: float,
radius_scale: float,
canvas: Canvas = CANVAS,
) -> Cutout:
scaled_ry = None if ry is None else ry * radius_scale
return Cutout(
kind,
canvas.cx + dx * center_scale,
canvas.cy + dy * center_scale,
rx * radius_scale,
scaled_ry,
rotation,
)
def cutouts_for_teeth(teeth: int, canvas: Canvas = CANVAS, geometry: Geometry = GEOMETRY) -> list[Cutout]:
sizing = sprocket_sizing(teeth, geometry)
return [
*(
offset_cutout(kind, dx, dy, rx, ry, rotation, sizing.decorative_scale, sizing.decorative_scale, canvas)
for kind, dx, dy, rx, ry, rotation in DECORATIVE_CUTOUT_SPECS
if sizing.include_decorative_cutouts
),
offset_cutout("circle", 0.0, 0.0, geometry.central_hole_radius, None, 0.0, 1.0, 1.0, canvas),
*(
offset_cutout("circle", dx, dy, geometry.bolt_hole_radius, None, 0.0, sizing.bolt_scale, 1.0, canvas)
for dx, dy in BOLT_HOLE_OFFSETS
if sizing.include_bolt_holes
),
]
def cutout_path(cutout: Cutout, delta: float = 0.0, reverse: bool = False) -> str:
if cutout.kind == "circle":
points = circle_points(cutout.cx, cutout.cy, cutout.rx + delta, 160)
elif cutout.kind == "ellipse":
assert cutout.ry is not None
points = ellipse_points(
cutout.cx,
cutout.cy,
cutout.rx + delta,
cutout.ry + delta,
cutout.rotation,
240,
)
else:
raise ValueError(f"Unsupported cutout kind: {cutout.kind}")
if reverse:
points = list(reversed(points))
return polygon_path(points)
def cutout_paths_for_teeth(teeth: int, canvas: Canvas = CANVAS) -> list[str]:
return [cutout_path(cutout) for cutout in cutouts_for_teeth(teeth, canvas)]
def filled_svg(teeth: int, canvas: Canvas = CANVAS, style: Style = STYLE) -> str:
outer = polygon_path(outer_points(teeth))
cutouts = " ".join(cutout_paths_for_teeth(teeth, canvas))
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="{canvas.width}" height="{canvas.height}" viewBox="0 0 {canvas.width} {canvas.height}">
<path d="{outer} {cutouts}" fill="{style.material}" fill-rule="evenodd"/>
</svg>
"""
def outline_svg(teeth: int, canvas: Canvas = CANVAS, style: Style = STYLE) -> str:
half_width = style.outline_width / 2.0
outer = polygon_path(outer_points(teeth, radius_delta=half_width))
inner = polygon_path(list(reversed(outer_points(teeth, radius_delta=-half_width))))
paths = [f"{outer} {inner}"]
for cutout in cutouts_for_teeth(teeth, canvas):
paths.append(f"{cutout_path(cutout, half_width)} {cutout_path(cutout, -half_width, reverse=True)}")
path_elements = "\n ".join(
f'<path d="{path}" fill="{style.outline}" fill-rule="evenodd"/>'
for path in paths
)
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="{canvas.width}" height="{canvas.height}" viewBox="0 0 {canvas.width} {canvas.height}">
{path_elements}
</svg>
"""
def render_png(svg_path: Path, png_path: Path) -> None:
magick = shutil.which("magick") or shutil.which("convert")
if magick is None:
raise RuntimeError("ImageMagick is required for SVG-to-PNG conversion, but neither 'magick' nor 'convert' is on PATH.")
png_path.parent.mkdir(parents=True, exist_ok=True)
command = [
magick,
"-background",
"none",
"-density",
"192",
str(svg_path),
"-resize",
f"{CANVAS.width}x{CANVAS.height}!",
str(png_path),
]
subprocess.run(command, check=True)
def write_asset(teeth: int, variant: str, svg_root: Path, png_root: Path, png: bool = True) -> tuple[Path, Path | None]:
if variant == "filled":
svg_text = filled_svg(teeth)
elif variant == "outline":
svg_text = outline_svg(teeth)
else:
raise ValueError(f"Unsupported variant: {variant}")
stem = f"chainring_{teeth:02d}_{variant}"
svg_path = svg_root / variant / f"{stem}.svg"
png_path = png_root / variant / f"{stem}.png"
svg_path.parent.mkdir(parents=True, exist_ok=True)
svg_path.write_text(svg_text, encoding="utf-8")
if png:
render_png(svg_path, png_path)
return svg_path, png_path
return svg_path, None
def generate_all(min_teeth: int, max_teeth: int, output_dir: Path, png: bool = True) -> list[Path]:
svg_root = output_dir / "svg"
png_root = output_dir / "png"
png_paths: list[Path] = []
for teeth in range(min_teeth, max_teeth + 1):
for variant in ("filled", "outline"):
_, png_path = write_asset(teeth, variant, svg_root, png_root, png=png)
if png_path is not None:
png_paths.append(png_path)
return png_paths
def write_radii_manifest(min_teeth: int, max_teeth: int, output_path: Path) -> None:
output_path.parent.mkdir(parents=True, exist_ok=True)
fields = [
"teeth",
"pitch_radius_px",
"calculated_root_radius_px",
"root_radius_px",
"tooth_depth_px",
"outer_radius_px",
"bolt_scale",
"decorative_scale",
"bolt_holes",
"decorative_cutouts",
]
with output_path.open("w", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=fields)
writer.writeheader()
for teeth in range(min_teeth, max_teeth + 1):
sizing = sprocket_sizing(teeth)
writer.writerow(
{
"teeth": teeth,
"pitch_radius_px": f"{sizing.pitch_radius:.3f}",
"calculated_root_radius_px": f"{sizing.calculated_root_radius:.3f}",
"root_radius_px": f"{sizing.root_radius:.3f}",
"tooth_depth_px": f"{sizing.tooth_depth:.3f}",
"outer_radius_px": f"{sizing.outer_radius:.3f}",
"bolt_scale": f"{sizing.bolt_scale:.3f}",
"decorative_scale": f"{sizing.decorative_scale:.3f}",
"bolt_holes": int(sizing.include_bolt_holes),
"decorative_cutouts": int(sizing.include_decorative_cutouts),
}
)
def make_contact_sheet(png_paths: list[Path], output_path: Path, columns: int = 6) -> None:
if not png_paths:
return
thumbs = []
thumb_size = (196, 165)
for path in png_paths:
image = Image.open(path).convert("RGBA")
image.thumbnail(thumb_size, Image.Resampling.LANCZOS)
tile = Image.new("RGBA", thumb_size, (61, 61, 61, 255))
x = (thumb_size[0] - image.width) // 2
y = (thumb_size[1] - image.height) // 2
tile.alpha_composite(image, (x, y))
thumbs.append(tile)
rows = math.ceil(len(thumbs) / columns)
sheet = Image.new("RGBA", (columns * thumb_size[0], rows * thumb_size[1]), (36, 36, 36, 255))
for index, thumb in enumerate(thumbs):
x = (index % columns) * thumb_size[0]
y = (index // columns) * thumb_size[1]
sheet.alpha_composite(thumb, (x, y))
output_path.parent.mkdir(parents=True, exist_ok=True)
sheet.save(output_path)
def compare_to_reference(candidate_path: Path, reference_path: Path) -> dict[str, float]:
candidate = Image.open(candidate_path).convert("L")
reference = Image.open(reference_path).convert("L").resize(candidate.size, Image.Resampling.LANCZOS)
candidate_arr = np.asarray(ImageOps.autocontrast(candidate), dtype=np.float32) / 255.0
reference_arr = np.asarray(ImageOps.autocontrast(reference), dtype=np.float32) / 255.0
diff = candidate_arr - reference_arr
candidate_edges = np.abs(np.gradient(candidate_arr)[0]) + np.abs(np.gradient(candidate_arr)[1])
reference_edges = np.abs(np.gradient(reference_arr)[0]) + np.abs(np.gradient(reference_arr)[1])
edge_diff = candidate_edges - reference_edges
return {
"mse": float(np.mean(diff**2)),
"mae": float(np.mean(np.abs(diff))),
"edge_mae": float(np.mean(np.abs(edge_diff))),
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate chainring SVG and PNG assets with fixed reference-like inner cutouts.")
parser.add_argument("--min-teeth", type=int, default=5)
parser.add_argument("--max-teeth", type=int, default=60)
parser.add_argument("--output", type=Path, default=Path("out"))
parser.add_argument("--no-png", action="store_true")
parser.add_argument("--contact-sheet", action="store_true")
parser.add_argument("--reference", type=Path, help="Optional local reference image for reporting similarity metrics.")
parser.add_argument("--reference-teeth", type=int, default=44)
return parser.parse_args()
def main() -> None:
args = parse_args()
if args.min_teeth < 3:
raise ValueError("--min-teeth must be at least 3.")
if args.max_teeth < args.min_teeth:
raise ValueError("--max-teeth must be greater than or equal to --min-teeth.")
png_paths = generate_all(args.min_teeth, args.max_teeth, args.output, png=not args.no_png)
write_radii_manifest(args.min_teeth, args.max_teeth, args.output / "radii.csv")
if args.contact_sheet and png_paths:
filled_samples = [path for path in png_paths if path.parent.name == "filled"]
outline_samples = [path for path in png_paths if path.parent.name == "outline"]
make_contact_sheet(filled_samples, args.output / "contact_sheet_filled.png")
make_contact_sheet(outline_samples, args.output / "contact_sheet_outline.png")
if args.reference and not args.no_png:
with TemporaryDirectory() as tmp:
tmp_root = Path(tmp)
_, candidate = write_asset(args.reference_teeth, "filled", tmp_root / "svg", tmp_root / "png", png=True)
assert candidate is not None
metrics = compare_to_reference(candidate, args.reference)
print(
"reference_metrics "
+ " ".join(f"{name}={value:.6f}" for name, value in metrics.items())
)
print(f"generated teeth={args.min_teeth}..{args.max_teeth} output={args.output}")
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Some files were not shown because too many files have changed in this diff Show More