Compare commits
60 Commits
f4022dd249
...
faster-upd
| Author | SHA1 | Date | |
|---|---|---|---|
| 0da0905697 | |||
| 4fceb0c690 | |||
| 230a6838e0 | |||
| 512c31d356 | |||
| f1491749d5 | |||
| 073d825a3e | |||
| bcccd03ecc | |||
| 16690dc216 | |||
| 9b672a7503 | |||
| f5e5c3904f | |||
| 3310387ec4 | |||
| aa2d150300 | |||
| dc1f53b6e1 | |||
| 16365e1d04 | |||
| 09c686d542 | |||
| 06834a0cc0 | |||
| b673c9100d | |||
| eb26c759e8 | |||
| 5285c44173 | |||
| be1c39d5d7 | |||
| 7628947623 | |||
| 76b7195e5e | |||
| 96416a2f73 | |||
| ac93c01cea | |||
| e3eba0bfc1 | |||
| 9922b90f49 | |||
| 2e7c10f87d | |||
| 1f5ec5ebb2 | |||
| 84e026de52 | |||
| 2fa0447593 | |||
| 57a14134a6 | |||
| 82ea8125e1 | |||
| 16ac66471a | |||
| ddaed084dc | |||
| 87193c3ae9 | |||
| 9016b9de77 | |||
| 7bb540c503 | |||
| 8cf6e95474 | |||
| bf67e9c2ae | |||
| cdd587c6de | |||
| 65e295f16d | |||
| b76503b144 | |||
| bdcd200a62 | |||
| 2ac68e09ab | |||
| 1dbbf191e6 | |||
| 32f258a492 | |||
| c581b4d92c | |||
| aafa9928ac | |||
| 8b24084f97 | |||
| dd2afa34ef | |||
| fb85565854 | |||
| 7a33e71410 | |||
| e704f27a96 | |||
| 08405c879b | |||
| 76c0fbe237 | |||
| d3a2fe6613 | |||
| a673aa14b7 | |||
| 575ccaae42 | |||
| dcb1e6596e | |||
| f92d6d04f5 |
49
.beads/.gitignore
vendored
Normal 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
@ -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* ⚡
|
||||
13
.beads/backup/backup_state.json
Normal 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
|
||||
}
|
||||
}
|
||||
0
.beads/backup/comments.jsonl
Normal file
11
.beads/backup/config.jsonl
Normal 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"}
|
||||
26
.beads/backup/dependencies.jsonl
Normal 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"}
|
||||
41
.beads/backup/events.jsonl
Normal 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\"}"}
|
||||
16
.beads/backup/issues.jsonl
Normal 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":""}
|
||||
0
.beads/backup/labels.jsonl
Normal file
55
.beads/config.yaml
Normal 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
@ -0,0 +1 @@
|
||||
1720108
|
||||
9
.beads/hooks/post-checkout
Executable 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
@ -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
@ -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
@ -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 ---
|
||||
9
.beads/hooks/prepare-commit-msg
Executable 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 ---
|
||||
0
.beads/interactions.jsonl
Normal file
6
.beads/metadata.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"database": "dolt",
|
||||
"backend": "dolt",
|
||||
"dolt_mode": "server",
|
||||
"dolt_database": "abawo_bt_app"
|
||||
}
|
||||
4
.gitignore
vendored
@ -44,3 +44,7 @@ app.*.map.json
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
.aider*
|
||||
|
||||
# Dolt database files (added by bd init)
|
||||
.dolt/
|
||||
*.db
|
||||
|
||||
@ -41,3 +41,6 @@ Still mostly material design.
|
||||
### Company Color Theme
|
||||
todo
|
||||
|
||||
## Code
|
||||
|
||||
Always use `<color>.withValues(alpha: <alpha>)` instead of `<color>.withOpacity(<alpha>)` for colors.
|
||||
@ -2,6 +2,10 @@
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Operational Docs
|
||||
|
||||
- [Bootloader OTA Operator Guide](docs/bootloader-ota-operator-guide.md)
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
@ -9,6 +9,12 @@
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
analyzer:
|
||||
errors:
|
||||
invalid_annotation_target: ignore
|
||||
plugins:
|
||||
- custom_lint
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
@ -23,12 +29,5 @@ linter:
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` 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
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
||||
@ -11,12 +11,12 @@ android {
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
jvmTarget = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
@ -42,3 +42,7 @@ android {
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "io.reactivex.rxjava2:rxjava:2.2.21"
|
||||
}
|
||||
|
||||
@ -1,5 +1,26 @@
|
||||
package com.example.abawo_bt_app
|
||||
|
||||
import android.os.Bundle
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
|
||||
|
||||
@ -18,8 +18,8 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.1.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
||||
id "com.android.application" version "8.6.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
BIN
assets/images/gears/png/filled/chainring_05_filled.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
assets/images/gears/png/filled/chainring_06_filled.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
assets/images/gears/png/filled/chainring_07_filled.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
assets/images/gears/png/filled/chainring_08_filled.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
assets/images/gears/png/filled/chainring_09_filled.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
assets/images/gears/png/filled/chainring_10_filled.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
assets/images/gears/png/filled/chainring_11_filled.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
assets/images/gears/png/filled/chainring_12_filled.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
assets/images/gears/png/filled/chainring_13_filled.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
assets/images/gears/png/filled/chainring_14_filled.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
assets/images/gears/png/filled/chainring_15_filled.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
assets/images/gears/png/filled/chainring_16_filled.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
assets/images/gears/png/filled/chainring_17_filled.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
assets/images/gears/png/filled/chainring_18_filled.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
assets/images/gears/png/filled/chainring_19_filled.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
assets/images/gears/png/filled/chainring_20_filled.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
assets/images/gears/png/filled/chainring_21_filled.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
assets/images/gears/png/filled/chainring_22_filled.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
assets/images/gears/png/filled/chainring_23_filled.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
assets/images/gears/png/filled/chainring_24_filled.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
assets/images/gears/png/filled/chainring_25_filled.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/images/gears/png/filled/chainring_26_filled.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/images/gears/png/filled/chainring_27_filled.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/images/gears/png/filled/chainring_28_filled.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/images/gears/png/filled/chainring_29_filled.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/images/gears/png/filled/chainring_30_filled.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/images/gears/png/filled/chainring_31_filled.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/images/gears/png/filled/chainring_32_filled.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/images/gears/png/filled/chainring_33_filled.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/images/gears/png/filled/chainring_34_filled.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/images/gears/png/filled/chainring_35_filled.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
assets/images/gears/png/filled/chainring_36_filled.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
assets/images/gears/png/filled/chainring_37_filled.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/images/gears/png/filled/chainring_38_filled.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
assets/images/gears/png/filled/chainring_39_filled.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
assets/images/gears/png/filled/chainring_40_filled.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
assets/images/gears/png/filled/chainring_41_filled.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
assets/images/gears/png/filled/chainring_42_filled.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
assets/images/gears/png/filled/chainring_43_filled.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
assets/images/gears/png/filled/chainring_44_filled.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
assets/images/gears/png/filled/chainring_45_filled.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
assets/images/gears/png/filled/chainring_46_filled.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
assets/images/gears/png/filled/chainring_47_filled.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
assets/images/gears/png/filled/chainring_48_filled.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
assets/images/gears/png/filled/chainring_49_filled.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/images/gears/png/filled/chainring_50_filled.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/images/gears/png/filled/chainring_51_filled.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/images/gears/png/filled/chainring_52_filled.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/images/gears/png/filled/chainring_53_filled.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/images/gears/png/filled/chainring_54_filled.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/images/gears/png/filled/chainring_55_filled.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/images/gears/png/filled/chainring_56_filled.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/images/gears/png/filled/chainring_57_filled.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/images/gears/png/filled/chainring_58_filled.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/images/gears/png/filled/chainring_59_filled.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/images/gears/png/filled/chainring_60_filled.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/images/shifter-wireframe.png
Normal file
|
After Width: | Height: | Size: 283 KiB |
492
chainring_generator/generate_chainrings.py
Normal 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()
|
||||
BIN
chainring_generator/out/contact_sheet_filled.png
Normal file
|
After Width: | Height: | Size: 455 KiB |
BIN
chainring_generator/out/contact_sheet_outline.png
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
chainring_generator/out/png/filled/chainring_05_filled.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
chainring_generator/out/png/filled/chainring_06_filled.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
chainring_generator/out/png/filled/chainring_07_filled.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
chainring_generator/out/png/filled/chainring_08_filled.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
chainring_generator/out/png/filled/chainring_09_filled.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
chainring_generator/out/png/filled/chainring_10_filled.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
chainring_generator/out/png/filled/chainring_11_filled.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
chainring_generator/out/png/filled/chainring_12_filled.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
chainring_generator/out/png/filled/chainring_13_filled.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
chainring_generator/out/png/filled/chainring_14_filled.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
chainring_generator/out/png/filled/chainring_15_filled.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
chainring_generator/out/png/filled/chainring_16_filled.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
chainring_generator/out/png/filled/chainring_17_filled.png
Normal file
|
After Width: | Height: | Size: 17 KiB |