Compare commits
17 Commits
f4022dd249
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ac68e09ab | |||
| 1dbbf191e6 | |||
| 32f258a492 | |||
| c581b4d92c | |||
| aafa9928ac | |||
| 8b24084f97 | |||
| dd2afa34ef | |||
| fb85565854 | |||
| 7a33e71410 | |||
| e704f27a96 | |||
| 08405c879b | |||
| 76c0fbe237 | |||
| d3a2fe6613 | |||
| a673aa14b7 | |||
| 575ccaae42 | |||
| dcb1e6596e | |||
| f92d6d04f5 |
49
.beads/.gitignore
vendored
Normal file
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
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
13
.beads/backup/backup_state.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"last_dolt_commit": "mf3is3p9ve79on2q1achdjp8v50rq6s3",
|
||||||
|
"last_event_id": 0,
|
||||||
|
"timestamp": "2026-03-04T17:07:18.317614374Z",
|
||||||
|
"counts": {
|
||||||
|
"issues": 10,
|
||||||
|
"events": 24,
|
||||||
|
"comments": 0,
|
||||||
|
"dependencies": 21,
|
||||||
|
"labels": 0,
|
||||||
|
"config": 11
|
||||||
|
}
|
||||||
|
}
|
||||||
0
.beads/backup/comments.jsonl
Normal file
0
.beads/backup/comments.jsonl
Normal file
11
.beads/backup/config.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"}
|
||||||
21
.beads/backup/dependencies.jsonl
Normal file
21
.beads/backup/dependencies.jsonl
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{"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"}
|
||||||
24
.beads/backup/events.jsonl
Normal file
24
.beads/backup/events.jsonl
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{"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":""}
|
||||||
10
.beads/backup/issues.jsonl
Normal file
10
.beads/backup/issues.jsonl
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{"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":"","closed_at":null,"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":"open","target":"","timeout_ns":0,"title":"Implement Universal Shifters BLE DFU v1 in app (manual .bin upload)","updated_at":"2026-03-03T15:37:50Z","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":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":"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":"open","target":"","timeout_ns":0,"title":"Add DFU test suite for happy path, loss, stalls, and cancel","updated_at":"2026-03-03T15:39:10Z","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":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":"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":"open","target":"","timeout_ns":0,"title":"Document DFU v1 operator flow, troubleshooting, and constraints","updated_at":"2026-03-03T15:39:10Z","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":""}
|
||||||
0
.beads/backup/labels.jsonl
Normal file
0
.beads/backup/labels.jsonl
Normal file
55
.beads/config.yaml
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
1
.beads/dolt-monitor.pid
Normal file
@ -0,0 +1 @@
|
|||||||
|
48179
|
||||||
1
.beads/dolt-server.activity
Normal file
1
.beads/dolt-server.activity
Normal file
@ -0,0 +1 @@
|
|||||||
|
1772550918
|
||||||
1
.beads/dolt-server.port
Normal file
1
.beads/dolt-server.port
Normal file
@ -0,0 +1 @@
|
|||||||
|
13365
|
||||||
9
.beads/hooks/post-checkout
Executable file
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
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
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
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
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
0
.beads/interactions.jsonl
Normal file
6
.beads/metadata.json
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
4
.gitignore
vendored
@ -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
|
||||||
|
|||||||
150
AGENTS.md
Normal file
150
AGENTS.md
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# Agent Instructions
|
||||||
|
|
||||||
|
This project uses **bd** (beads) for issue tracking. Run `bd onboard` to get started.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bd ready # Find available work
|
||||||
|
bd show <id> # View issue details
|
||||||
|
bd update <id> --claim # Claim work atomically
|
||||||
|
bd close <id> # Complete work
|
||||||
|
bd sync # Sync with git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Non-Interactive Shell Commands
|
||||||
|
|
||||||
|
**ALWAYS use non-interactive flags** with file operations to avoid hanging on confirmation prompts.
|
||||||
|
|
||||||
|
Shell commands like `cp`, `mv`, and `rm` may be aliased to include `-i` (interactive) mode on some systems, causing the agent to hang indefinitely waiting for y/n input.
|
||||||
|
|
||||||
|
**Use these forms instead:**
|
||||||
|
```bash
|
||||||
|
# Force overwrite without prompting
|
||||||
|
cp -f source dest # NOT: cp source dest
|
||||||
|
mv -f source dest # NOT: mv source dest
|
||||||
|
rm -f file # NOT: rm file
|
||||||
|
|
||||||
|
# For recursive operations
|
||||||
|
rm -rf directory # NOT: rm -r directory
|
||||||
|
cp -rf source dest # NOT: cp -r source dest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Other commands that may prompt:**
|
||||||
|
- `scp` - use `-o BatchMode=yes` for non-interactive
|
||||||
|
- `ssh` - use `-o BatchMode=yes` to fail instead of prompting
|
||||||
|
- `apt-get` - use `-y` flag
|
||||||
|
- `brew` - use `HOMEBREW_NO_AUTO_UPDATE=1` env var
|
||||||
|
|
||||||
|
<!-- BEGIN BEADS INTEGRATION -->
|
||||||
|
## Issue Tracking with bd (beads)
|
||||||
|
|
||||||
|
**IMPORTANT**: This project uses **bd (beads)** for ALL issue tracking. Do NOT use markdown TODOs, task lists, or other tracking methods.
|
||||||
|
|
||||||
|
### Why bd?
|
||||||
|
|
||||||
|
- Dependency-aware: Track blockers and relationships between issues
|
||||||
|
- Git-friendly: Auto-syncs to JSONL for version control
|
||||||
|
- Agent-optimized: JSON output, ready work detection, discovered-from links
|
||||||
|
- Prevents duplicate tracking systems and confusion
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
**Check for ready work:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bd ready --json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create new issues:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bd create "Issue title" --description="Detailed context" -t bug|feature|task -p 0-4 --json
|
||||||
|
bd create "Issue title" --description="What this issue is about" -p 1 --deps discovered-from:bd-123 --json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Claim and update:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bd update <id> --claim --json
|
||||||
|
bd update bd-42 --priority 1 --json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Complete work:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bd close bd-42 --reason "Completed" --json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue Types
|
||||||
|
|
||||||
|
- `bug` - Something broken
|
||||||
|
- `feature` - New functionality
|
||||||
|
- `task` - Work item (tests, docs, refactoring)
|
||||||
|
- `epic` - Large feature with subtasks
|
||||||
|
- `chore` - Maintenance (dependencies, tooling)
|
||||||
|
|
||||||
|
### Priorities
|
||||||
|
|
||||||
|
- `0` - Critical (security, data loss, broken builds)
|
||||||
|
- `1` - High (major features, important bugs)
|
||||||
|
- `2` - Medium (default, nice-to-have)
|
||||||
|
- `3` - Low (polish, optimization)
|
||||||
|
- `4` - Backlog (future ideas)
|
||||||
|
|
||||||
|
### Workflow for AI Agents
|
||||||
|
|
||||||
|
1. **Check ready work**: `bd ready` shows unblocked issues
|
||||||
|
2. **Claim your task atomically**: `bd update <id> --claim`
|
||||||
|
3. **Work on it**: Implement, test, document
|
||||||
|
4. **Discover new work?** Create linked issue:
|
||||||
|
- `bd create "Found bug" --description="Details about what was found" -p 1 --deps discovered-from:<parent-id>`
|
||||||
|
5. **Complete**: `bd close <id> --reason "Done"`
|
||||||
|
|
||||||
|
### Auto-Sync
|
||||||
|
|
||||||
|
bd automatically syncs with git:
|
||||||
|
|
||||||
|
- Exports to `.beads/issues.jsonl` after changes (5s debounce)
|
||||||
|
- Imports from JSONL when newer (e.g., after `git pull`)
|
||||||
|
- No manual export/import needed!
|
||||||
|
|
||||||
|
### Important Rules
|
||||||
|
|
||||||
|
- ✅ Use bd for ALL task tracking
|
||||||
|
- ✅ Always use `--json` flag for programmatic use
|
||||||
|
- ✅ Link discovered work with `discovered-from` dependencies
|
||||||
|
- ✅ Check `bd ready` before asking "what should I work on?"
|
||||||
|
- ❌ Do NOT create markdown TODO lists
|
||||||
|
- ❌ Do NOT use external issue trackers
|
||||||
|
- ❌ Do NOT duplicate tracking systems
|
||||||
|
|
||||||
|
For more details, see README.md and docs/QUICKSTART.md.
|
||||||
|
|
||||||
|
<!-- END BEADS INTEGRATION -->
|
||||||
|
|
||||||
|
## Landing the Plane (Session Completion)
|
||||||
|
|
||||||
|
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.
|
||||||
|
|
||||||
|
**MANDATORY WORKFLOW:**
|
||||||
|
|
||||||
|
1. **File issues for remaining work** - Create issues for anything that needs follow-up
|
||||||
|
2. **Run quality gates** (if code changed) - Tests, linters, builds
|
||||||
|
3. **Update issue status** - Close finished work, update in-progress items
|
||||||
|
4. **PUSH TO REMOTE** - This is MANDATORY:
|
||||||
|
```bash
|
||||||
|
git pull --rebase
|
||||||
|
bd sync
|
||||||
|
git push
|
||||||
|
git status # MUST show "up to date with origin"
|
||||||
|
```
|
||||||
|
5. **Clean up** - Clear stashes, prune remote branches
|
||||||
|
6. **Verify** - All changes committed AND pushed
|
||||||
|
7. **Hand off** - Provide context for next session
|
||||||
|
|
||||||
|
**CRITICAL RULES:**
|
||||||
|
- Work is NOT complete until `git push` succeeds
|
||||||
|
- NEVER stop before pushing - that leaves work stranded locally
|
||||||
|
- NEVER say "ready to push when you are" - YOU must push
|
||||||
|
- If push fails, resolve and retry until it succeeds
|
||||||
@ -11,12 +11,12 @@ android {
|
|||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_1_8
|
jvmTarget = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
|
|||||||
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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 {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "8.1.0" apply false
|
id "com.android.application" version "8.6.0" apply false
|
||||||
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include ":app"
|
include ":app"
|
||||||
|
|||||||
BIN
assets/images/shifter-wireframe.png
Normal file
BIN
assets/images/shifter-wireframe.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 283 KiB |
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
3
flutter_rust_bridge.yaml
Normal file
3
flutter_rust_bridge.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
rust_input: crate::api
|
||||||
|
rust_root: rust/
|
||||||
|
dart_output: lib/src/rust
|
||||||
13
integration_test/simple_test.dart
Normal file
13
integration_test/simple_test.dart
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:abawo_bt_app/main.dart';
|
||||||
|
import 'package:abawo_bt_app/src/rust/frb_generated.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
setUpAll(() async => await RustLib.init());
|
||||||
|
testWidgets('Can call rust function', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const MyApp());
|
||||||
|
expect(find.textContaining('Result: `Hello, Tom!`'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,131 +1,367 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:anyhow/anyhow.dart';
|
import 'package:anyhow/anyhow.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'
|
||||||
|
hide ConnectionStatus, Result, Logger;
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'bluetooth.g.dart';
|
part 'bluetooth.g.dart';
|
||||||
|
|
||||||
final log = Logger('BluetoothController');
|
final log = Logger('BluetoothController');
|
||||||
|
|
||||||
@riverpod
|
@Riverpod(keepAlive: true)
|
||||||
|
FlutterReactiveBle reactiveBle(Ref ref) {
|
||||||
|
ref.keepAlive();
|
||||||
|
return FlutterReactiveBle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
Future<BluetoothController> bluetooth(Ref ref) async {
|
Future<BluetoothController> bluetooth(Ref ref) async {
|
||||||
final controller = BluetoothController();
|
ref.keepAlive();
|
||||||
log.info(await controller.init());
|
final controller = BluetoothController(ref.read(reactiveBleProvider));
|
||||||
|
await controller.init();
|
||||||
return controller;
|
return controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
Stream<(ConnectionStatus, String?)> connectionStatus(Ref ref) {
|
||||||
|
final asyncController = ref.watch(bluetoothProvider);
|
||||||
|
return asyncController.when(
|
||||||
|
data: (controller) => controller.connectionStateStream,
|
||||||
|
loading: () => Stream.value((ConnectionStatus.disconnected, null)),
|
||||||
|
error: (_, __) => Stream.value((ConnectionStatus.disconnected, null)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the connection status of the Bluetooth device.
|
||||||
|
enum ConnectionStatus { disconnected, connecting, connected, disconnecting }
|
||||||
|
|
||||||
class BluetoothController {
|
class BluetoothController {
|
||||||
StreamSubscription<BluetoothAdapterState>? _btStateSubscription;
|
BluetoothController(this._ble);
|
||||||
StreamSubscription<List<ScanResult>>? _scanResultsSubscription;
|
|
||||||
List<ScanResult> _latestScanResults = [];
|
static const int defaultMtu = 64;
|
||||||
|
|
||||||
|
final FlutterReactiveBle _ble;
|
||||||
|
|
||||||
|
StreamSubscription<BleStatus>? _bleStatusSubscription;
|
||||||
|
StreamSubscription<DiscoveredDevice>? _scanResultsSubscription;
|
||||||
|
Timer? _scanTimeout;
|
||||||
|
final Map<String, DiscoveredDevice> _scanResultsById = {};
|
||||||
|
final _scanResultsSubject =
|
||||||
|
BehaviorSubject<List<DiscoveredDevice>>.seeded(const []);
|
||||||
|
final _isScanningSubject = BehaviorSubject<bool>.seeded(false);
|
||||||
|
|
||||||
|
String? _connectedDeviceId;
|
||||||
|
StreamSubscription<ConnectionStateUpdate>? _connectionStateSubscription;
|
||||||
|
final _connectionStateSubject =
|
||||||
|
BehaviorSubject<(ConnectionStatus, String?)>.seeded(
|
||||||
|
(ConnectionStatus.disconnected, null));
|
||||||
|
|
||||||
|
Stream<(ConnectionStatus, String?)> get connectionStateStream =>
|
||||||
|
_connectionStateSubject.stream;
|
||||||
|
|
||||||
|
(ConnectionStatus, String?) get currentConnectionState =>
|
||||||
|
_connectionStateSubject.value;
|
||||||
|
|
||||||
|
Stream<List<DiscoveredDevice>> get scanResultsStream =>
|
||||||
|
_scanResultsSubject.stream;
|
||||||
|
|
||||||
|
Stream<bool> get isScanningStream => _isScanningSubject.stream;
|
||||||
|
|
||||||
|
List<DiscoveredDevice> get scanResults => _scanResultsSubject.value;
|
||||||
|
|
||||||
Future<Result<void>> init() async {
|
Future<Result<void>> init() async {
|
||||||
if (await FlutterBluePlus.isSupported == false) {
|
_bleStatusSubscription ??= _ble.statusStream.listen((status) {
|
||||||
log.severe("Bluetooth is not supported on this device!");
|
log.info('BLE status: $status');
|
||||||
return bail("Bluetooth is not supported on this device!");
|
|
||||||
}
|
|
||||||
|
|
||||||
_btStateSubscription =
|
|
||||||
FlutterBluePlus.adapterState.listen((BluetoothAdapterState state) {
|
|
||||||
if (state == BluetoothAdapterState.on) {
|
|
||||||
log.info("Bluetooth is on!");
|
|
||||||
// usually start scanning, connecting, etc
|
|
||||||
} else {
|
|
||||||
log.info("Bluetooth is off!");
|
|
||||||
// show an error to the user, etc
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!kIsWeb && Platform.isAndroid) {
|
|
||||||
await FlutterBluePlus.turnOn();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(null);
|
return Ok(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start scanning for Bluetooth devices
|
|
||||||
///
|
|
||||||
/// [withServices] - Optional list of service UUIDs to filter devices by
|
|
||||||
/// [withNames] - Optional list of device names to filter by
|
|
||||||
/// [timeout] - Optional duration after which scanning will automatically stop
|
|
||||||
Future<Result<void>> startScan({
|
Future<Result<void>> startScan({
|
||||||
List<Guid>? withServices,
|
List<Uuid>? withServices,
|
||||||
List<String>? withNames,
|
|
||||||
Duration? timeout,
|
Duration? timeout,
|
||||||
|
ScanMode scanMode = ScanMode.lowLatency,
|
||||||
|
bool requireLocationServicesEnabled = true,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
if (_isScanningSubject.value) {
|
||||||
// Wait for Bluetooth to be enabled
|
return Ok(null);
|
||||||
await FlutterBluePlus.adapterState
|
|
||||||
.where((val) => val == BluetoothAdapterState.on)
|
|
||||||
.first;
|
|
||||||
|
|
||||||
// Set up scan results listener
|
|
||||||
_scanResultsSubscription = FlutterBluePlus.onScanResults.listen(
|
|
||||||
(results) {
|
|
||||||
if (results.isNotEmpty) {
|
|
||||||
_latestScanResults = results;
|
|
||||||
ScanResult latestResult = results.last;
|
|
||||||
log.info(
|
|
||||||
'${latestResult.device.remoteId}: "${latestResult.advertisementData.advName}" found!');
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onError: (e) {
|
|
||||||
log.severe('Scan error: $e');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clean up subscription when scanning completes
|
try {
|
||||||
FlutterBluePlus.cancelWhenScanComplete(_scanResultsSubscription!);
|
final status = _ble.status;
|
||||||
|
if (status != BleStatus.ready) {
|
||||||
|
await _ble.statusStream
|
||||||
|
.where((value) => value == BleStatus.ready)
|
||||||
|
.first;
|
||||||
|
}
|
||||||
|
|
||||||
// Start scanning with optional parameters
|
_scanTimeout?.cancel();
|
||||||
await FlutterBluePlus.startScan(
|
_scanResultsById.clear();
|
||||||
withServices: withServices ?? [],
|
_scanResultsSubject.add(const []);
|
||||||
withNames: withNames ?? [],
|
_isScanningSubject.add(true);
|
||||||
timeout: timeout,
|
|
||||||
);
|
_scanResultsSubscription = _ble
|
||||||
|
.scanForDevices(
|
||||||
|
withServices: withServices ?? const [],
|
||||||
|
scanMode: scanMode,
|
||||||
|
requireLocationServicesEnabled: requireLocationServicesEnabled,
|
||||||
|
)
|
||||||
|
.listen((device) {
|
||||||
|
_scanResultsById[device.id] = device;
|
||||||
|
_scanResultsSubject
|
||||||
|
.add(_scanResultsById.values.toList(growable: false));
|
||||||
|
}, onError: (Object error, StackTrace st) {
|
||||||
|
log.severe('Scan error: $error', error, st);
|
||||||
|
_isScanningSubject.add(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (timeout != null) {
|
||||||
|
_scanTimeout = Timer(timeout, () {
|
||||||
|
unawaited(stopScan());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(null);
|
return Ok(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
_isScanningSubject.add(false);
|
||||||
return bail('Failed to start Bluetooth scan: $e');
|
return bail('Failed to start Bluetooth scan: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop an ongoing Bluetooth scan
|
|
||||||
Future<Result<void>> stopScan() async {
|
Future<Result<void>> stopScan() async {
|
||||||
try {
|
try {
|
||||||
await FlutterBluePlus.stopScan();
|
_scanTimeout?.cancel();
|
||||||
|
_scanTimeout = null;
|
||||||
|
await _scanResultsSubscription?.cancel();
|
||||||
|
_scanResultsSubscription = null;
|
||||||
|
_isScanningSubject.add(false);
|
||||||
return Ok(null);
|
return Ok(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
_isScanningSubject.add(false);
|
||||||
return bail('Failed to stop Bluetooth scan: $e');
|
return bail('Failed to stop Bluetooth scan: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the latest scan results
|
|
||||||
List<ScanResult> get scanResults => _latestScanResults;
|
|
||||||
|
|
||||||
/// Wait for the current scan to complete
|
|
||||||
Future<Result<void>> waitForScanToComplete() async {
|
Future<Result<void>> waitForScanToComplete() async {
|
||||||
try {
|
try {
|
||||||
await FlutterBluePlus.isScanning.where((val) => val == false).first;
|
await isScanningStream.where((val) => val == false).first;
|
||||||
return Ok(null);
|
return Ok(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return bail('Error waiting for scan to complete: $e');
|
return bail('Error waiting for scan to complete: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if currently scanning
|
Future<bool> get isScanning async => isScanningStream.first;
|
||||||
Future<bool> get isScanning async {
|
|
||||||
return await FlutterBluePlus.isScanning.first;
|
Future<Result<void>> connect(DiscoveredDevice device,
|
||||||
|
{Duration? timeout}) async {
|
||||||
|
return connectById(device.id, timeout: timeout ?? Duration(seconds: 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> connectById(
|
||||||
|
String deviceId, {
|
||||||
|
Duration timeout = const Duration(seconds: 10),
|
||||||
|
Map<Uuid, List<Uuid>>? servicesWithCharacteristicsToDiscover,
|
||||||
|
}) async {
|
||||||
|
final currentState = currentConnectionState;
|
||||||
|
final currentDeviceId = currentState.$2;
|
||||||
|
|
||||||
|
if (deviceId == currentDeviceId &&
|
||||||
|
(currentState.$1 == ConnectionStatus.connected ||
|
||||||
|
currentState.$1 == ConnectionStatus.connecting)) {
|
||||||
|
log.info('Already connected or connecting to $deviceId.');
|
||||||
|
if (currentState.$1 == ConnectionStatus.connected) {
|
||||||
|
unawaited(_requestMtuOnConnect(deviceId));
|
||||||
|
}
|
||||||
|
return Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDeviceId != null && deviceId != currentDeviceId) {
|
||||||
|
final disconnectResult = await disconnect();
|
||||||
|
if (disconnectResult.isErr()) {
|
||||||
|
return disconnectResult
|
||||||
|
.context('Failed to disconnect from previous device');
|
||||||
|
}
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _connectionStateSubscription?.cancel();
|
||||||
|
_updateConnectionState(ConnectionStatus.connecting, deviceId);
|
||||||
|
|
||||||
|
_connectionStateSubscription = _ble
|
||||||
|
.connectToDevice(
|
||||||
|
id: deviceId,
|
||||||
|
connectionTimeout: timeout,
|
||||||
|
servicesWithCharacteristicsToDiscover:
|
||||||
|
servicesWithCharacteristicsToDiscover,
|
||||||
|
)
|
||||||
|
.listen((update) {
|
||||||
|
switch (update.connectionState) {
|
||||||
|
case DeviceConnectionState.connected:
|
||||||
|
_connectedDeviceId = deviceId;
|
||||||
|
_updateConnectionState(ConnectionStatus.connected, deviceId);
|
||||||
|
unawaited(_requestMtuOnConnect(deviceId));
|
||||||
|
break;
|
||||||
|
case DeviceConnectionState.connecting:
|
||||||
|
_updateConnectionState(ConnectionStatus.connecting, deviceId);
|
||||||
|
break;
|
||||||
|
case DeviceConnectionState.disconnecting:
|
||||||
|
_updateConnectionState(ConnectionStatus.disconnecting, deviceId);
|
||||||
|
break;
|
||||||
|
case DeviceConnectionState.disconnected:
|
||||||
|
_cleanUpConnection();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, onError: (Object error, StackTrace st) {
|
||||||
|
log.severe('Failed to connect to $deviceId: $error', error, st);
|
||||||
|
_cleanUpConnection();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
_cleanUpConnection();
|
||||||
|
return bail('Failed to connect to $deviceId: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> disconnect() async {
|
||||||
|
final deviceIdToDisconnect =
|
||||||
|
_connectedDeviceId ?? _connectionStateSubject.value.$2;
|
||||||
|
if (deviceIdToDisconnect == null) {
|
||||||
|
_cleanUpConnection();
|
||||||
|
return Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateConnectionState(
|
||||||
|
ConnectionStatus.disconnecting, deviceIdToDisconnect);
|
||||||
|
try {
|
||||||
|
await _connectionStateSubscription?.cancel();
|
||||||
|
_connectionStateSubscription = null;
|
||||||
|
_cleanUpConnection();
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
_cleanUpConnection();
|
||||||
|
return bail('Failed to disconnect from $deviceIdToDisconnect: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<List<int>>> readCharacteristic(
|
||||||
|
String deviceId,
|
||||||
|
String serviceUuid,
|
||||||
|
String characteristicUuid,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final characteristic = QualifiedCharacteristic(
|
||||||
|
serviceId: Uuid.parse(serviceUuid),
|
||||||
|
characteristicId: Uuid.parse(characteristicUuid),
|
||||||
|
deviceId: deviceId,
|
||||||
|
);
|
||||||
|
final value = await _ble.readCharacteristic(characteristic);
|
||||||
|
return Ok(value);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Error reading characteristic: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> writeCharacteristic(
|
||||||
|
String deviceId,
|
||||||
|
String serviceUuid,
|
||||||
|
String characteristicUuid,
|
||||||
|
List<int> value, {
|
||||||
|
bool withResponse = true,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final characteristic = QualifiedCharacteristic(
|
||||||
|
serviceId: Uuid.parse(serviceUuid),
|
||||||
|
characteristicId: Uuid.parse(characteristicUuid),
|
||||||
|
deviceId: deviceId,
|
||||||
|
);
|
||||||
|
if (withResponse) {
|
||||||
|
await _ble.writeCharacteristicWithResponse(
|
||||||
|
characteristic,
|
||||||
|
value: value,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await _ble.writeCharacteristicWithoutResponse(
|
||||||
|
characteristic,
|
||||||
|
value: value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Error writing characteristic: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> requestMtu(String deviceId,
|
||||||
|
{int mtu = defaultMtu}) async {
|
||||||
|
final result = await requestMtuAndGetValue(deviceId, mtu: mtu);
|
||||||
|
if (result.isErr()) {
|
||||||
|
return bail(result.unwrapErr());
|
||||||
|
}
|
||||||
|
return Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<int>> requestMtuAndGetValue(String deviceId,
|
||||||
|
{int mtu = defaultMtu}) async {
|
||||||
|
try {
|
||||||
|
final negotiatedMtu = await _ble.requestMtu(deviceId: deviceId, mtu: mtu);
|
||||||
|
log.info(
|
||||||
|
'MTU negotiated for $deviceId: requested $mtu, got $negotiatedMtu');
|
||||||
|
return Ok(negotiatedMtu);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Error requesting MTU $mtu for $deviceId: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _requestMtuOnConnect(String deviceId) async {
|
||||||
|
final mtuResult = await requestMtu(deviceId, mtu: defaultMtu);
|
||||||
|
if (mtuResult.isErr()) {
|
||||||
|
log.warning(
|
||||||
|
'MTU request after connect failed for $deviceId: ${mtuResult.unwrapErr()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<int>> subscribeToCharacteristic(
|
||||||
|
String deviceId,
|
||||||
|
String serviceUuid,
|
||||||
|
String characteristicUuid,
|
||||||
|
) {
|
||||||
|
final characteristic = QualifiedCharacteristic(
|
||||||
|
serviceId: Uuid.parse(serviceUuid),
|
||||||
|
characteristicId: Uuid.parse(characteristicUuid),
|
||||||
|
deviceId: deviceId,
|
||||||
|
);
|
||||||
|
return _ble.subscribeToCharacteristic(characteristic);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateConnectionState(ConnectionStatus status, String? deviceId) {
|
||||||
|
if (_connectionStateSubject.value.$1 == status &&
|
||||||
|
_connectionStateSubject.value.$2 == deviceId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_connectionStateSubject.add((status, deviceId));
|
||||||
|
log.fine(
|
||||||
|
'Connection state updated: $status, device: ${deviceId ?? 'none'}');
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cleanUpConnection() {
|
||||||
|
_connectedDeviceId = null;
|
||||||
|
_updateConnectionState(ConnectionStatus.disconnected, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Result<void>> dispose() async {
|
Future<Result<void>> dispose() async {
|
||||||
|
_scanTimeout?.cancel();
|
||||||
await _scanResultsSubscription?.cancel();
|
await _scanResultsSubscription?.cancel();
|
||||||
await _btStateSubscription?.cancel();
|
await _bleStatusSubscription?.cancel();
|
||||||
|
await disconnect();
|
||||||
|
await _scanResultsSubject.close();
|
||||||
|
await _isScanningSubject.close();
|
||||||
|
await _connectionStateSubject.close();
|
||||||
return Ok(null);
|
return Ok(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,24 @@ part of 'bluetooth.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$bluetoothHash() => r'5e9c37c57e723b84dd08fd8763e7c445b3a4dbf3';
|
String _$bluetoothHash() => r'f1fb75c72a7a473fc545baea6bedfdf4a21ab26b';
|
||||||
|
|
||||||
|
String _$reactiveBleHash() => r'9c4d4f37f7a0da1741b42d6a4c3f0f00c2c07f3c';
|
||||||
|
|
||||||
|
/// See also [reactiveBle].
|
||||||
|
@ProviderFor(reactiveBle)
|
||||||
|
final reactiveBleProvider = AutoDisposeProvider<FlutterReactiveBle>.internal(
|
||||||
|
reactiveBle,
|
||||||
|
name: r'reactiveBleProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product') ? null : _$reactiveBleHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef ReactiveBleRef = AutoDisposeProviderRef<FlutterReactiveBle>;
|
||||||
|
|
||||||
/// See also [bluetooth].
|
/// See also [bluetooth].
|
||||||
@ProviderFor(bluetooth)
|
@ProviderFor(bluetooth)
|
||||||
@ -23,5 +40,24 @@ final bluetoothProvider =
|
|||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
typedef BluetoothRef = AutoDisposeFutureProviderRef<BluetoothController>;
|
typedef BluetoothRef = AutoDisposeFutureProviderRef<BluetoothController>;
|
||||||
|
String _$connectionStatusHash() => r'4dba587ef7703dabca4b2b9800e0798decfe2977';
|
||||||
|
|
||||||
|
/// See also [connectionStatus].
|
||||||
|
@ProviderFor(connectionStatus)
|
||||||
|
final connectionStatusProvider =
|
||||||
|
AutoDisposeStreamProvider<(ConnectionStatus, String?)>.internal(
|
||||||
|
connectionStatus,
|
||||||
|
name: r'connectionStatusProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$connectionStatusHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef ConnectionStatusRef
|
||||||
|
= AutoDisposeStreamProviderRef<(ConnectionStatus, String?)>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|||||||
627
lib/controller/bluetooth.old.dart
Normal file
627
lib/controller/bluetooth.old.dart
Normal file
@ -0,0 +1,627 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:anyhow/anyhow.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'bluetooth.g.dart';
|
||||||
|
|
||||||
|
final log = Logger('BluetoothController');
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
Future<BluetoothController> bluetooth(Ref ref) async {
|
||||||
|
ref.keepAlive();
|
||||||
|
final controller = BluetoothController();
|
||||||
|
log.info(await controller.init());
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
Stream<(ConnectionStatus, BluetoothDevice?)> connectionStatus(Ref ref) {
|
||||||
|
// Get the (potentially still loading) BluetoothController
|
||||||
|
final asyncController = ref.watch(bluetoothProvider);
|
||||||
|
|
||||||
|
// If the controller is ready, return its stream. Otherwise, return an empty stream.
|
||||||
|
// The provider will automatically update when the controller becomes ready.
|
||||||
|
return asyncController.when(
|
||||||
|
data: (controller) => controller.connectionStateStream,
|
||||||
|
loading: () => Stream.value((ConnectionStatus.disconnected, null)),
|
||||||
|
error: (_, __) => Stream.value((ConnectionStatus.disconnected, null)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the connection status of the Bluetooth device.
|
||||||
|
enum ConnectionStatus { disconnected, connecting, connected, disconnecting }
|
||||||
|
|
||||||
|
class BluetoothController {
|
||||||
|
StreamSubscription<BluetoothAdapterState>? _btStateSubscription;
|
||||||
|
StreamSubscription<List<ScanResult>>? _scanResultsSubscription;
|
||||||
|
List<ScanResult> _latestScanResults = [];
|
||||||
|
StreamSubscription<void>? _servicesResetSubscription;
|
||||||
|
final Map<String, Map<Guid, BluetoothService>> _servicesByDevice = {};
|
||||||
|
final Map<String, Map<String, BluetoothCharacteristic>>
|
||||||
|
_characteristicsByDevice = {};
|
||||||
|
// Connection State
|
||||||
|
BluetoothDevice? _connectedDevice;
|
||||||
|
StreamSubscription<BluetoothConnectionState>? _connectionStateSubscription;
|
||||||
|
final _connectionStateSubject =
|
||||||
|
BehaviorSubject<(ConnectionStatus, BluetoothDevice?)>.seeded(
|
||||||
|
(ConnectionStatus.disconnected, null));
|
||||||
|
|
||||||
|
/// Stream providing the current connection status and the connected device (if any).
|
||||||
|
Stream<(ConnectionStatus, BluetoothDevice?)> get connectionStateStream =>
|
||||||
|
_connectionStateSubject.stream;
|
||||||
|
|
||||||
|
/// Gets the latest connection status and device.
|
||||||
|
(ConnectionStatus, BluetoothDevice?) get currentConnectionState =>
|
||||||
|
_connectionStateSubject.value;
|
||||||
|
|
||||||
|
Future<Result<void>> init() async {
|
||||||
|
log.severe("CALLED FBPON!");
|
||||||
|
if (await FlutterBluePlus.isSupported == false) {
|
||||||
|
log.severe("Bluetooth is not supported on this device!");
|
||||||
|
return bail("Bluetooth is not supported on this device!");
|
||||||
|
}
|
||||||
|
|
||||||
|
_btStateSubscription =
|
||||||
|
FlutterBluePlus.adapterState.listen((BluetoothAdapterState state) {
|
||||||
|
if (state == BluetoothAdapterState.on) {
|
||||||
|
log.info("Bluetooth is on!");
|
||||||
|
// usually start scanning, connecting, etc
|
||||||
|
} else {
|
||||||
|
log.info("Bluetooth is off!");
|
||||||
|
// show an error to the user, etc
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!kIsWeb && Platform.isAndroid) {
|
||||||
|
await FlutterBluePlus.turnOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionStateStream.listen((state) {
|
||||||
|
log.info('Connection state changed: $state');
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start scanning for Bluetooth devices
|
||||||
|
///
|
||||||
|
/// [withServices] - Optional list of service UUIDs to filter devices by
|
||||||
|
/// [withNames] - Optional list of device names to filter by
|
||||||
|
/// [timeout] - Optional duration after which scanning will automatically stop
|
||||||
|
Future<Result<void>> startScan({
|
||||||
|
List<Guid>? withServices,
|
||||||
|
List<String>? withNames,
|
||||||
|
Duration? timeout,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// Wait for Bluetooth to be enabled
|
||||||
|
await FlutterBluePlus.adapterState
|
||||||
|
.where((val) => val == BluetoothAdapterState.on)
|
||||||
|
.first;
|
||||||
|
|
||||||
|
// Set up scan results listener
|
||||||
|
_scanResultsSubscription = FlutterBluePlus.onScanResults.listen(
|
||||||
|
(results) {
|
||||||
|
if (results.isNotEmpty) {
|
||||||
|
_latestScanResults = results;
|
||||||
|
ScanResult latestResult = results.last;
|
||||||
|
log.info(
|
||||||
|
'${latestResult.device.remoteId}: "${latestResult.advertisementData.advName}" found!');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (e) {
|
||||||
|
log.severe('Scan error: $e');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clean up subscription when scanning completes
|
||||||
|
FlutterBluePlus.cancelWhenScanComplete(_scanResultsSubscription!);
|
||||||
|
|
||||||
|
// Start scanning with optional parameters
|
||||||
|
await FlutterBluePlus.startScan(
|
||||||
|
withServices: withServices ?? [],
|
||||||
|
withNames: withNames ?? [],
|
||||||
|
timeout: timeout,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Failed to start Bluetooth scan: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop an ongoing Bluetooth scan
|
||||||
|
Future<Result<void>> stopScan() async {
|
||||||
|
try {
|
||||||
|
await FlutterBluePlus.stopScan();
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Failed to stop Bluetooth scan: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the latest scan results
|
||||||
|
List<ScanResult> get scanResults => _latestScanResults;
|
||||||
|
|
||||||
|
/// Wait for the current scan to complete
|
||||||
|
Future<Result<void>> waitForScanToComplete() async {
|
||||||
|
try {
|
||||||
|
await FlutterBluePlus.isScanning.where((val) => val == false).first;
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Error waiting for scan to complete: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if currently scanning
|
||||||
|
Future<bool> get isScanning async {
|
||||||
|
return await FlutterBluePlus.isScanning.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connects to a specific Bluetooth device.
|
||||||
|
///
|
||||||
|
/// Ensures that only one device is connected at a time. If another device
|
||||||
|
/// is already connected or connecting, it will be disconnected first.
|
||||||
|
Future<Result<void>> connect(BluetoothDevice device,
|
||||||
|
{Duration? timeout}) async {
|
||||||
|
final currentState = currentConnectionState;
|
||||||
|
final currentDevice = currentState.$2;
|
||||||
|
|
||||||
|
// Prevent connecting if already connected/connecting to the *same* device
|
||||||
|
if (device.remoteId == currentDevice?.remoteId &&
|
||||||
|
(currentState.$1 == ConnectionStatus.connected ||
|
||||||
|
currentState.$1 == ConnectionStatus.connecting)) {
|
||||||
|
log.info('Currently connected device: ${currentState.$2}');
|
||||||
|
log.info('Already connected or connecting to ${device.remoteId}.');
|
||||||
|
return Ok(null); // Or potentially an error/different status?
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('Attempting to connect to ${device.remoteId}...');
|
||||||
|
|
||||||
|
// If connecting or connected to a *different* device, disconnect it first.
|
||||||
|
if (currentDevice != null && device.remoteId != currentDevice.remoteId) {
|
||||||
|
log.info(
|
||||||
|
'Disconnecting from previous device ${currentDevice.remoteId} first.');
|
||||||
|
final disconnectResult = await disconnect();
|
||||||
|
if (disconnectResult.isErr()) {
|
||||||
|
return disconnectResult
|
||||||
|
.context('Failed to disconnect from previous device');
|
||||||
|
}
|
||||||
|
// Wait a moment for the disconnection to fully process
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Cancel any previous connection state listener before starting a new one
|
||||||
|
await _connectionStateSubscription?.cancel();
|
||||||
|
_connectionStateSubscription =
|
||||||
|
device.connectionState.listen((BluetoothConnectionState state) async {
|
||||||
|
log.info('[${device.remoteId}] Connection state changed: $state');
|
||||||
|
switch (state) {
|
||||||
|
case BluetoothConnectionState.connected:
|
||||||
|
_connectedDevice = device;
|
||||||
|
_updateConnectionState(ConnectionStatus.connected, device);
|
||||||
|
// IMPORTANT: Discover services after connecting
|
||||||
|
try {
|
||||||
|
_attachServicesResetListener(device);
|
||||||
|
final servicesResult =
|
||||||
|
await _discoverAndCacheServices(device, force: true);
|
||||||
|
if (servicesResult.isErr()) {
|
||||||
|
throw servicesResult.unwrapErr();
|
||||||
|
}
|
||||||
|
log.info(
|
||||||
|
'[${device.remoteId}] Services discovered: \n${servicesResult.unwrap().map((e) => e.uuid.toString()).join('\n')}');
|
||||||
|
} catch (e) {
|
||||||
|
log.severe(
|
||||||
|
'[${device.remoteId}] Error discovering services: $e. Disconnecting.');
|
||||||
|
// Disconnect if service discovery fails, as the connection might be unusable
|
||||||
|
await disconnect();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BluetoothConnectionState.disconnected:
|
||||||
|
if (_connectionStateSubject.value.$1 !=
|
||||||
|
ConnectionStatus.connected) {
|
||||||
|
log.warning(
|
||||||
|
'[${device.remoteId}] Disconnected WITHOUT being connected! Reason: ${device.disconnectReason?.code} ${device.disconnectReason?.description}\nDoing nothing');
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
log.warning(
|
||||||
|
'[${device.remoteId}] Disconnected. Reason: ${device.disconnectReason?.code} ${device.disconnectReason?.description}');
|
||||||
|
// Only clean up if this is the device we were connected/connecting to
|
||||||
|
if (_connectionStateSubject.value.$2?.remoteId ==
|
||||||
|
device.remoteId) {
|
||||||
|
// Clean up connection state, handling disconnection.
|
||||||
|
// In general, reconnection is better, but this is how it's handled here.
|
||||||
|
// App behavior would be to go back to the homepage on disconnection
|
||||||
|
_cleanUpConnection();
|
||||||
|
} else {
|
||||||
|
log.info(
|
||||||
|
'[${device.remoteId}] Received disconnect for a device we were not tracking.');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BluetoothConnectionState.connecting:
|
||||||
|
case BluetoothConnectionState.disconnecting:
|
||||||
|
// deprecated states
|
||||||
|
log.warning(
|
||||||
|
'Received unexpected connection state: ${device.connectionState}. This should not happen.');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await device.connect(
|
||||||
|
license: License.free,
|
||||||
|
timeout: timeout ?? const Duration(seconds: 15),
|
||||||
|
mtu: 512,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Note: Success is primarily handled by the connectionState listener
|
||||||
|
log.info(
|
||||||
|
'Connection initiated for ${device.remoteId}. Waiting for state change.');
|
||||||
|
_connectionStateSubject.add((ConnectionStatus.connected, device));
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
log.severe('Failed to connect to ${device.remoteId}: $e');
|
||||||
|
_cleanUpConnection(); // Clean up state on connection failure
|
||||||
|
return bail('Failed to connect to ${device.remoteId}: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connects to a device using its remote ID string with a specific timeout.
|
||||||
|
Future<Result<void>> connectById(String remoteId,
|
||||||
|
{Duration timeout = const Duration(seconds: 10)}) async {
|
||||||
|
log.info('Attempting to connect by ID: $remoteId with timeout: $timeout');
|
||||||
|
try {
|
||||||
|
// Get the BluetoothDevice object from the ID
|
||||||
|
final device = BluetoothDevice.fromId(remoteId);
|
||||||
|
|
||||||
|
// Call the existing connect method, passing the device and timeout
|
||||||
|
// Assumes the 'connect' method below is modified to accept the timeout.
|
||||||
|
return await connect(device, timeout: timeout); // Pass timeout here
|
||||||
|
} catch (e, st) {
|
||||||
|
// Catch potential errors from fromId or during connection setup before connect() is called
|
||||||
|
log.severe('Error connecting by ID $remoteId: $e');
|
||||||
|
_cleanUpConnection(); // Ensure state is cleaned up
|
||||||
|
return bail('Failed to initiate connection for ID $remoteId: $e', st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disconnects from the currently connected device.
|
||||||
|
Future<Result<void>> disconnect() async {
|
||||||
|
final deviceToDisconnect =
|
||||||
|
_connectedDevice ?? _connectionStateSubject.value.$2;
|
||||||
|
if (deviceToDisconnect == null) {
|
||||||
|
log.info('No device is currently connected or connecting.');
|
||||||
|
// Ensure state is definitely disconnected if called unnecessarily
|
||||||
|
_cleanUpConnection();
|
||||||
|
return Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('Disconnecting from ${deviceToDisconnect.remoteId}...');
|
||||||
|
_updateConnectionState(ConnectionStatus.disconnecting, deviceToDisconnect);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deviceToDisconnect.disconnect();
|
||||||
|
log.info('Disconnect command sent to ${deviceToDisconnect.remoteId}.');
|
||||||
|
// State update to disconnected is handled by the connectionState listener
|
||||||
|
// but we call cleanup here as a safety measure in case the listener fails
|
||||||
|
_cleanUpConnection();
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
log.severe(
|
||||||
|
'Failed to disconnect from ${deviceToDisconnect.remoteId}: $e');
|
||||||
|
// Even on error, try to clean up the state
|
||||||
|
_cleanUpConnection();
|
||||||
|
return bail(
|
||||||
|
'Failed to disconnect from ${deviceToDisconnect.remoteId}: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateConnectionState(
|
||||||
|
ConnectionStatus status, BluetoothDevice? device) {
|
||||||
|
// Avoid emitting redundant states
|
||||||
|
if (_connectionStateSubject.value.$1 == status &&
|
||||||
|
_connectionStateSubject.value.$2?.remoteId == device?.remoteId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_connectionStateSubject.add((status, device));
|
||||||
|
log.fine(
|
||||||
|
'Connection state updated: $status, Device: ${device?.remoteId ?? 'none'}');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<List<BluetoothService>>> discoverServices(
|
||||||
|
BluetoothDevice device, {
|
||||||
|
bool force = false,
|
||||||
|
}) async {
|
||||||
|
return _discoverAndCacheServices(device, force: force);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> writeCharacteristic(
|
||||||
|
BluetoothDevice device,
|
||||||
|
String serviceUuid,
|
||||||
|
String characteristicUuid,
|
||||||
|
List<int> value, {
|
||||||
|
bool withoutResponse = false,
|
||||||
|
bool allowLongWrite = false,
|
||||||
|
int timeout = 15,
|
||||||
|
}) async {
|
||||||
|
final serviceGuid = Guid(serviceUuid);
|
||||||
|
final characteristicGuid = Guid(characteristicUuid);
|
||||||
|
final chrResult =
|
||||||
|
await _getCharacteristic(device, serviceGuid, characteristicGuid);
|
||||||
|
if (chrResult.isErr()) {
|
||||||
|
return chrResult.context('Failed to resolve characteristic for write');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await chrResult.unwrap().write(
|
||||||
|
value,
|
||||||
|
withoutResponse: withoutResponse,
|
||||||
|
allowLongWrite: allowLongWrite,
|
||||||
|
timeout: timeout,
|
||||||
|
);
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Error writing characteristic $characteristicUuid: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<StreamSubscription<List<int>>>> subscribeToNotifications(
|
||||||
|
BluetoothDevice device,
|
||||||
|
String serviceUuid,
|
||||||
|
String characteristicUuid, {
|
||||||
|
void Function(List<int>)? onValue,
|
||||||
|
bool useLastValueStream = false,
|
||||||
|
int timeout = 15,
|
||||||
|
}) async {
|
||||||
|
return _subscribeToCharacteristic(
|
||||||
|
device,
|
||||||
|
serviceUuid,
|
||||||
|
characteristicUuid,
|
||||||
|
useLastValueStream: useLastValueStream,
|
||||||
|
timeout: timeout,
|
||||||
|
forceIndications: false,
|
||||||
|
onValue: onValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<StreamSubscription<List<int>>>> subscribeToIndications(
|
||||||
|
BluetoothDevice device,
|
||||||
|
String serviceUuid,
|
||||||
|
String characteristicUuid, {
|
||||||
|
void Function(List<int>)? onValue,
|
||||||
|
bool useLastValueStream = false,
|
||||||
|
int timeout = 15,
|
||||||
|
}) async {
|
||||||
|
return _subscribeToCharacteristic(
|
||||||
|
device,
|
||||||
|
serviceUuid,
|
||||||
|
characteristicUuid,
|
||||||
|
useLastValueStream: useLastValueStream,
|
||||||
|
timeout: timeout,
|
||||||
|
forceIndications: true,
|
||||||
|
onValue: onValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> unsubscribeFromCharacteristic(
|
||||||
|
BluetoothDevice device,
|
||||||
|
String serviceUuid,
|
||||||
|
String characteristicUuid, {
|
||||||
|
int timeout = 15,
|
||||||
|
}) async {
|
||||||
|
final serviceGuid = Guid(serviceUuid);
|
||||||
|
final characteristicGuid = Guid(characteristicUuid);
|
||||||
|
final chrResult =
|
||||||
|
await _getCharacteristic(device, serviceGuid, characteristicGuid);
|
||||||
|
if (chrResult.isErr()) {
|
||||||
|
return chrResult
|
||||||
|
.context('Failed to resolve characteristic to unsubscribe');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await chrResult.unwrap().setNotifyValue(false, timeout: timeout);
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Error disabling notifications for $characteristicUuid: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to clean up connection resources and state.
|
||||||
|
Future<void> _cleanUpConnection() async {
|
||||||
|
log.fine('Cleaning up connection state and subscriptions.');
|
||||||
|
_connectedDevice = null;
|
||||||
|
await _servicesResetSubscription?.cancel();
|
||||||
|
_servicesResetSubscription = null;
|
||||||
|
_servicesByDevice.clear();
|
||||||
|
_characteristicsByDevice.clear();
|
||||||
|
await _connectionStateSubscription?.cancel();
|
||||||
|
_connectionStateSubscription = null;
|
||||||
|
_updateConnectionState(ConnectionStatus.disconnected, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> dispose() async {
|
||||||
|
await _scanResultsSubscription?.cancel();
|
||||||
|
await _btStateSubscription?.cancel();
|
||||||
|
await disconnect(); // Ensure disconnection on dispose
|
||||||
|
await _connectionStateSubject.close();
|
||||||
|
return Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<List<int>>> readCharacteristic(
|
||||||
|
BluetoothDevice device, String svcUuid, String characteristic) async {
|
||||||
|
// Implement reading characteristic logic here
|
||||||
|
// This is a placeholder implementation
|
||||||
|
log.info(
|
||||||
|
'Reading characteristic from device: $device, characteristic: $characteristic');
|
||||||
|
final serviceUUID = Guid(svcUuid);
|
||||||
|
final characteristicUUID = Guid(characteristic);
|
||||||
|
|
||||||
|
if (!device.servicesList.map((e) => e.uuid).contains(serviceUUID)) {
|
||||||
|
return bail('Service $svcUuid not found on device ${device.remoteId}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final BluetoothService service =
|
||||||
|
(device.servicesList).firstWhere((s) => s.uuid == serviceUUID);
|
||||||
|
|
||||||
|
if (service.characteristics.isEmpty ||
|
||||||
|
!service.characteristics
|
||||||
|
.map((c) => c.uuid)
|
||||||
|
.contains(characteristicUUID)) {
|
||||||
|
return bail(
|
||||||
|
'Characteristic $characteristic not found on device ${device.remoteId}');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final val = await service.characteristics
|
||||||
|
.firstWhere((c) => c.uuid == characteristicUUID)
|
||||||
|
.read();
|
||||||
|
return Ok(val);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Error reading characteristic: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _deviceKey(BluetoothDevice device) => device.remoteId.str;
|
||||||
|
|
||||||
|
String _characteristicKey(Guid serviceUuid, Guid characteristicUuid) =>
|
||||||
|
'${serviceUuid.toString()}|${characteristicUuid.toString()}';
|
||||||
|
|
||||||
|
void _cacheServices(BluetoothDevice device, List<BluetoothService> services) {
|
||||||
|
final serviceMap = <Guid, BluetoothService>{};
|
||||||
|
final characteristicMap = <String, BluetoothCharacteristic>{};
|
||||||
|
|
||||||
|
for (final service in services) {
|
||||||
|
serviceMap[service.uuid] = service;
|
||||||
|
for (final chr in service.characteristics) {
|
||||||
|
characteristicMap[_characteristicKey(service.uuid, chr.uuid)] = chr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_servicesByDevice[_deviceKey(device)] = serviceMap;
|
||||||
|
_characteristicsByDevice[_deviceKey(device)] = characteristicMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _attachServicesResetListener(BluetoothDevice device) {
|
||||||
|
_servicesResetSubscription?.cancel();
|
||||||
|
_servicesResetSubscription = device.onServicesReset.listen((_) async {
|
||||||
|
log.info('[${device.remoteId}] Services reset. Re-discovering.');
|
||||||
|
final res = await _discoverAndCacheServices(device, force: true);
|
||||||
|
if (res.isErr()) {
|
||||||
|
log.severe(
|
||||||
|
'[${device.remoteId}] Failed to re-discover services: ${res.unwrapErr()}');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
device.cancelWhenDisconnected(_servicesResetSubscription!);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<List<BluetoothService>>> _discoverAndCacheServices(
|
||||||
|
BluetoothDevice device, {
|
||||||
|
bool force = false,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
if (!force) {
|
||||||
|
final cached = _servicesByDevice[_deviceKey(device)];
|
||||||
|
if (cached != null && cached.isNotEmpty) {
|
||||||
|
return Ok(cached.values.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!force && device.servicesList.isNotEmpty) {
|
||||||
|
_cacheServices(device, device.servicesList);
|
||||||
|
return Ok(device.servicesList);
|
||||||
|
}
|
||||||
|
|
||||||
|
final services = await device.discoverServices();
|
||||||
|
_cacheServices(device, services);
|
||||||
|
return Ok(services);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Failed to discover services for ${device.remoteId}: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<BluetoothCharacteristic>> _getCharacteristic(
|
||||||
|
BluetoothDevice device,
|
||||||
|
Guid serviceUuid,
|
||||||
|
Guid characteristicUuid,
|
||||||
|
) async {
|
||||||
|
final deviceKey = _deviceKey(device);
|
||||||
|
final cached = _characteristicsByDevice[deviceKey]
|
||||||
|
?[_characteristicKey(serviceUuid, characteristicUuid)];
|
||||||
|
if (cached != null) {
|
||||||
|
return Ok(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
final discoverResult = await _discoverAndCacheServices(device);
|
||||||
|
if (discoverResult.isErr()) {
|
||||||
|
return bail(discoverResult.unwrapErr().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
final refreshed = _characteristicsByDevice[deviceKey]
|
||||||
|
?[_characteristicKey(serviceUuid, characteristicUuid)];
|
||||||
|
if (refreshed == null) {
|
||||||
|
return bail(
|
||||||
|
'Characteristic $characteristicUuid not found on service $serviceUuid for device ${device.remoteId}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(refreshed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<StreamSubscription<List<int>>>> _subscribeToCharacteristic(
|
||||||
|
BluetoothDevice device,
|
||||||
|
String serviceUuid,
|
||||||
|
String characteristicUuid, {
|
||||||
|
required bool forceIndications,
|
||||||
|
required bool useLastValueStream,
|
||||||
|
required int timeout,
|
||||||
|
void Function(List<int>)? onValue,
|
||||||
|
}) async {
|
||||||
|
final serviceGuid = Guid(serviceUuid);
|
||||||
|
final characteristicGuid = Guid(characteristicUuid);
|
||||||
|
final chrResult =
|
||||||
|
await _getCharacteristic(device, serviceGuid, characteristicGuid);
|
||||||
|
if (chrResult.isErr()) {
|
||||||
|
return bail('Failed to resolve characteristic subscription: '
|
||||||
|
'${chrResult.unwrapErr()}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final characteristic = chrResult.unwrap();
|
||||||
|
final properties = characteristic.properties;
|
||||||
|
if (forceIndications && !properties.indicate) {
|
||||||
|
return bail(
|
||||||
|
'Characteristic $characteristicUuid does not support indications');
|
||||||
|
}
|
||||||
|
if (!forceIndications && !properties.notify && !properties.indicate) {
|
||||||
|
return bail(
|
||||||
|
'Characteristic $characteristicUuid does not support notifications');
|
||||||
|
}
|
||||||
|
if (forceIndications && !kIsWeb && !Platform.isAndroid) {
|
||||||
|
return bail('Indications can only be forced on Android.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final stream = useLastValueStream
|
||||||
|
? characteristic.lastValueStream
|
||||||
|
: characteristic.onValueReceived;
|
||||||
|
final subscription = stream.listen(onValue ?? (_) {});
|
||||||
|
device.cancelWhenDisconnected(subscription);
|
||||||
|
|
||||||
|
await characteristic.setNotifyValue(
|
||||||
|
true,
|
||||||
|
timeout: timeout,
|
||||||
|
forceIndications: forceIndications,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(subscription);
|
||||||
|
} catch (e) {
|
||||||
|
return bail(
|
||||||
|
'Error subscribing to characteristic $characteristicUuid: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
lib/controller/connected_device.dart
Normal file
1
lib/controller/connected_device.dart
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
143
lib/database/database.dart
Normal file
143
lib/database/database.dart
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import 'package:anyhow/anyhow.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift_flutter/drift_flutter.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'database.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class NConnectedDevices extends _$NConnectedDevices {
|
||||||
|
@override
|
||||||
|
Future<List<ConnectedDevice>> build() async {
|
||||||
|
final db = await ref.watch(databaseProvider);
|
||||||
|
return await db.getAllConnectedDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<int>> addConnectedDevice(
|
||||||
|
ConnectedDevicesCompanion device) async {
|
||||||
|
final db = await ref.watch(databaseProvider);
|
||||||
|
final res = await db.addConnectedDevice(device);
|
||||||
|
if (res.isOk()) {
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> deleteConnectedDevice(int id) async {
|
||||||
|
final db = await ref.watch(databaseProvider);
|
||||||
|
final res = await db.deleteConnectedDevice(id);
|
||||||
|
if (res.isOk()) {
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provider for the [AppDatabase] instance
|
||||||
|
final databaseProvider = Provider<AppDatabase>((ref) {
|
||||||
|
final database = AppDatabase();
|
||||||
|
ref.onDispose(() => database.close());
|
||||||
|
return database;
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Provider for all connected devices as a stream
|
||||||
|
final connectedDevicesStreamProvider =
|
||||||
|
StreamProvider<List<ConnectedDevice>>((ref) {
|
||||||
|
final database = ref.watch(databaseProvider);
|
||||||
|
return database.getAllConnectedDevicesStream();
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Provider for all connected devices as a future
|
||||||
|
final connectedDevicesProvider = FutureProvider<List<ConnectedDevice>>((ref) {
|
||||||
|
final database = ref.watch(databaseProvider);
|
||||||
|
return database.getAllConnectedDevices();
|
||||||
|
});
|
||||||
|
|
||||||
|
class ConnectedDevices extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
TextColumn get deviceName => text()();
|
||||||
|
TextColumn get deviceAddress => text()();
|
||||||
|
TextColumn get deviceType => text()();
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
DateTimeColumn get lastConnectedAt => dateTime().nullable()();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DriftDatabase(tables: [ConnectedDevices])
|
||||||
|
class AppDatabase extends _$AppDatabase {
|
||||||
|
// After generating code, this class needs to define a `schemaVersion` getter
|
||||||
|
// and a constructor telling drift where the database should be stored.
|
||||||
|
// These are described in the getting started guide: https://drift.simonbinder.eu/setup/
|
||||||
|
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get schemaVersion => 1;
|
||||||
|
|
||||||
|
static QueryExecutor _openConnection() {
|
||||||
|
return driftDatabase(
|
||||||
|
name: 'my_database',
|
||||||
|
native: const DriftNativeOptions(
|
||||||
|
// By default, `driftDatabase` from `package:drift_flutter` stores the
|
||||||
|
// database files in `getApplicationDocumentsDirectory()`.
|
||||||
|
databaseDirectory: getApplicationSupportDirectory,
|
||||||
|
),
|
||||||
|
// If you need web support, see https://drift.simonbinder.eu/platforms/web/
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ConnectedDevice>> getAllConnectedDevices() {
|
||||||
|
return select(connectedDevices).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new connected device to the database.
|
||||||
|
///
|
||||||
|
/// [device] is a [ConnectedDevicesCompanion] representing the device to be inserted.
|
||||||
|
///
|
||||||
|
/// Returns a [Result] indicating success or failure of the device insertion.
|
||||||
|
/// On successful insertion, returns [Ok]\(rowid\). On failure, returns an error with a descriptive message.
|
||||||
|
Future<Result<int>> addConnectedDevice(
|
||||||
|
ConnectedDevicesCompanion device) async {
|
||||||
|
try {
|
||||||
|
if (!device.deviceAddress.present) {
|
||||||
|
return bail('Device address is required to save a connected device.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final exists = await (select(connectedDevices)
|
||||||
|
..where(
|
||||||
|
(tbl) => tbl.deviceAddress.equals(device.deviceAddress.value)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (exists != null) {
|
||||||
|
return bail('Device ${device.deviceAddress.value} is already added.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final rowid = await into(connectedDevices).insert(device);
|
||||||
|
return Ok(rowid);
|
||||||
|
} catch (e, st) {
|
||||||
|
return bail('Failed to add device: $e', st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes a connected device from the database by its ID.
|
||||||
|
///
|
||||||
|
/// [id] is the ID of the device to be deleted.
|
||||||
|
///
|
||||||
|
/// Returns a [Result] indicating success or failure of the deletion.
|
||||||
|
/// On successful deletion, returns [Ok](number of deleted rows). On failure, returns an error with a descriptive message.
|
||||||
|
Future<Result<void>> deleteConnectedDevice(int id) async {
|
||||||
|
try {
|
||||||
|
final count = await (delete(connectedDevices)
|
||||||
|
..where((tbl) => tbl.id.equals(id)))
|
||||||
|
.go();
|
||||||
|
if (count == 0) {
|
||||||
|
return bail('Device with id $id not found.');
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
} catch (e, st) {
|
||||||
|
return bail('Failed to delete device with id $id: $e', st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<ConnectedDevice>> getAllConnectedDevicesStream() {
|
||||||
|
return select(connectedDevices).watch();
|
||||||
|
}
|
||||||
|
}
|
||||||
586
lib/database/database.g.dart
Normal file
586
lib/database/database.g.dart
Normal file
@ -0,0 +1,586 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'database.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
class $ConnectedDevicesTable extends ConnectedDevices
|
||||||
|
with TableInfo<$ConnectedDevicesTable, ConnectedDevice> {
|
||||||
|
@override
|
||||||
|
final GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
$ConnectedDevicesTable(this.attachedDatabase, [this._alias]);
|
||||||
|
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||||
|
'id', aliasedName, false,
|
||||||
|
hasAutoIncrement: true,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints:
|
||||||
|
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||||
|
static const VerificationMeta _deviceNameMeta =
|
||||||
|
const VerificationMeta('deviceName');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String> deviceName = GeneratedColumn<String>(
|
||||||
|
'device_name', aliasedName, false,
|
||||||
|
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
static const VerificationMeta _deviceAddressMeta =
|
||||||
|
const VerificationMeta('deviceAddress');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String> deviceAddress = GeneratedColumn<String>(
|
||||||
|
'device_address', aliasedName, false,
|
||||||
|
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
static const VerificationMeta _deviceTypeMeta =
|
||||||
|
const VerificationMeta('deviceType');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String> deviceType = GeneratedColumn<String>(
|
||||||
|
'device_type', aliasedName, false,
|
||||||
|
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
static const VerificationMeta _createdAtMeta =
|
||||||
|
const VerificationMeta('createdAt');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
|
||||||
|
'created_at', aliasedName, false,
|
||||||
|
type: DriftSqlType.dateTime,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultValue: currentDateAndTime);
|
||||||
|
static const VerificationMeta _lastConnectedAtMeta =
|
||||||
|
const VerificationMeta('lastConnectedAt');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<DateTime> lastConnectedAt =
|
||||||
|
GeneratedColumn<DateTime>('last_connected_at', aliasedName, true,
|
||||||
|
type: DriftSqlType.dateTime, requiredDuringInsert: false);
|
||||||
|
@override
|
||||||
|
List<GeneratedColumn> get $columns =>
|
||||||
|
[id, deviceName, deviceAddress, deviceType, createdAt, lastConnectedAt];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
@override
|
||||||
|
String get actualTableName => $name;
|
||||||
|
static const String $name = 'connected_devices';
|
||||||
|
@override
|
||||||
|
VerificationContext validateIntegrity(Insertable<ConnectedDevice> instance,
|
||||||
|
{bool isInserting = false}) {
|
||||||
|
final context = VerificationContext();
|
||||||
|
final data = instance.toColumns(true);
|
||||||
|
if (data.containsKey('id')) {
|
||||||
|
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('device_name')) {
|
||||||
|
context.handle(
|
||||||
|
_deviceNameMeta,
|
||||||
|
deviceName.isAcceptableOrUnknown(
|
||||||
|
data['device_name']!, _deviceNameMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_deviceNameMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('device_address')) {
|
||||||
|
context.handle(
|
||||||
|
_deviceAddressMeta,
|
||||||
|
deviceAddress.isAcceptableOrUnknown(
|
||||||
|
data['device_address']!, _deviceAddressMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_deviceAddressMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('device_type')) {
|
||||||
|
context.handle(
|
||||||
|
_deviceTypeMeta,
|
||||||
|
deviceType.isAcceptableOrUnknown(
|
||||||
|
data['device_type']!, _deviceTypeMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_deviceTypeMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('created_at')) {
|
||||||
|
context.handle(_createdAtMeta,
|
||||||
|
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('last_connected_at')) {
|
||||||
|
context.handle(
|
||||||
|
_lastConnectedAtMeta,
|
||||||
|
lastConnectedAt.isAcceptableOrUnknown(
|
||||||
|
data['last_connected_at']!, _lastConnectedAtMeta));
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<GeneratedColumn> get $primaryKey => {id};
|
||||||
|
@override
|
||||||
|
ConnectedDevice map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
return ConnectedDevice(
|
||||||
|
id: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||||
|
deviceName: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}device_name'])!,
|
||||||
|
deviceAddress: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}device_address'])!,
|
||||||
|
deviceType: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}device_type'])!,
|
||||||
|
createdAt: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
|
||||||
|
lastConnectedAt: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.dateTime, data['${effectivePrefix}last_connected_at']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
$ConnectedDevicesTable createAlias(String alias) {
|
||||||
|
return $ConnectedDevicesTable(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectedDevice extends DataClass implements Insertable<ConnectedDevice> {
|
||||||
|
final int id;
|
||||||
|
final String deviceName;
|
||||||
|
final String deviceAddress;
|
||||||
|
final String deviceType;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime? lastConnectedAt;
|
||||||
|
const ConnectedDevice(
|
||||||
|
{required this.id,
|
||||||
|
required this.deviceName,
|
||||||
|
required this.deviceAddress,
|
||||||
|
required this.deviceType,
|
||||||
|
required this.createdAt,
|
||||||
|
this.lastConnectedAt});
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
map['id'] = Variable<int>(id);
|
||||||
|
map['device_name'] = Variable<String>(deviceName);
|
||||||
|
map['device_address'] = Variable<String>(deviceAddress);
|
||||||
|
map['device_type'] = Variable<String>(deviceType);
|
||||||
|
map['created_at'] = Variable<DateTime>(createdAt);
|
||||||
|
if (!nullToAbsent || lastConnectedAt != null) {
|
||||||
|
map['last_connected_at'] = Variable<DateTime>(lastConnectedAt);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectedDevicesCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return ConnectedDevicesCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
deviceName: Value(deviceName),
|
||||||
|
deviceAddress: Value(deviceAddress),
|
||||||
|
deviceType: Value(deviceType),
|
||||||
|
createdAt: Value(createdAt),
|
||||||
|
lastConnectedAt: lastConnectedAt == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(lastConnectedAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ConnectedDevice.fromJson(Map<String, dynamic> json,
|
||||||
|
{ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return ConnectedDevice(
|
||||||
|
id: serializer.fromJson<int>(json['id']),
|
||||||
|
deviceName: serializer.fromJson<String>(json['deviceName']),
|
||||||
|
deviceAddress: serializer.fromJson<String>(json['deviceAddress']),
|
||||||
|
deviceType: serializer.fromJson<String>(json['deviceType']),
|
||||||
|
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||||
|
lastConnectedAt: serializer.fromJson<DateTime?>(json['lastConnectedAt']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': serializer.toJson<int>(id),
|
||||||
|
'deviceName': serializer.toJson<String>(deviceName),
|
||||||
|
'deviceAddress': serializer.toJson<String>(deviceAddress),
|
||||||
|
'deviceType': serializer.toJson<String>(deviceType),
|
||||||
|
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||||
|
'lastConnectedAt': serializer.toJson<DateTime?>(lastConnectedAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectedDevice copyWith(
|
||||||
|
{int? id,
|
||||||
|
String? deviceName,
|
||||||
|
String? deviceAddress,
|
||||||
|
String? deviceType,
|
||||||
|
DateTime? createdAt,
|
||||||
|
Value<DateTime?> lastConnectedAt = const Value.absent()}) =>
|
||||||
|
ConnectedDevice(
|
||||||
|
id: id ?? this.id,
|
||||||
|
deviceName: deviceName ?? this.deviceName,
|
||||||
|
deviceAddress: deviceAddress ?? this.deviceAddress,
|
||||||
|
deviceType: deviceType ?? this.deviceType,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
lastConnectedAt: lastConnectedAt.present
|
||||||
|
? lastConnectedAt.value
|
||||||
|
: this.lastConnectedAt,
|
||||||
|
);
|
||||||
|
ConnectedDevice copyWithCompanion(ConnectedDevicesCompanion data) {
|
||||||
|
return ConnectedDevice(
|
||||||
|
id: data.id.present ? data.id.value : this.id,
|
||||||
|
deviceName:
|
||||||
|
data.deviceName.present ? data.deviceName.value : this.deviceName,
|
||||||
|
deviceAddress: data.deviceAddress.present
|
||||||
|
? data.deviceAddress.value
|
||||||
|
: this.deviceAddress,
|
||||||
|
deviceType:
|
||||||
|
data.deviceType.present ? data.deviceType.value : this.deviceType,
|
||||||
|
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||||
|
lastConnectedAt: data.lastConnectedAt.present
|
||||||
|
? data.lastConnectedAt.value
|
||||||
|
: this.lastConnectedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('ConnectedDevice(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('deviceName: $deviceName, ')
|
||||||
|
..write('deviceAddress: $deviceAddress, ')
|
||||||
|
..write('deviceType: $deviceType, ')
|
||||||
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('lastConnectedAt: $lastConnectedAt')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
id, deviceName, deviceAddress, deviceType, createdAt, lastConnectedAt);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is ConnectedDevice &&
|
||||||
|
other.id == this.id &&
|
||||||
|
other.deviceName == this.deviceName &&
|
||||||
|
other.deviceAddress == this.deviceAddress &&
|
||||||
|
other.deviceType == this.deviceType &&
|
||||||
|
other.createdAt == this.createdAt &&
|
||||||
|
other.lastConnectedAt == this.lastConnectedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectedDevicesCompanion extends UpdateCompanion<ConnectedDevice> {
|
||||||
|
final Value<int> id;
|
||||||
|
final Value<String> deviceName;
|
||||||
|
final Value<String> deviceAddress;
|
||||||
|
final Value<String> deviceType;
|
||||||
|
final Value<DateTime> createdAt;
|
||||||
|
final Value<DateTime?> lastConnectedAt;
|
||||||
|
const ConnectedDevicesCompanion({
|
||||||
|
this.id = const Value.absent(),
|
||||||
|
this.deviceName = const Value.absent(),
|
||||||
|
this.deviceAddress = const Value.absent(),
|
||||||
|
this.deviceType = const Value.absent(),
|
||||||
|
this.createdAt = const Value.absent(),
|
||||||
|
this.lastConnectedAt = const Value.absent(),
|
||||||
|
});
|
||||||
|
ConnectedDevicesCompanion.insert({
|
||||||
|
this.id = const Value.absent(),
|
||||||
|
required String deviceName,
|
||||||
|
required String deviceAddress,
|
||||||
|
required String deviceType,
|
||||||
|
this.createdAt = const Value.absent(),
|
||||||
|
this.lastConnectedAt = const Value.absent(),
|
||||||
|
}) : deviceName = Value(deviceName),
|
||||||
|
deviceAddress = Value(deviceAddress),
|
||||||
|
deviceType = Value(deviceType);
|
||||||
|
static Insertable<ConnectedDevice> custom({
|
||||||
|
Expression<int>? id,
|
||||||
|
Expression<String>? deviceName,
|
||||||
|
Expression<String>? deviceAddress,
|
||||||
|
Expression<String>? deviceType,
|
||||||
|
Expression<DateTime>? createdAt,
|
||||||
|
Expression<DateTime>? lastConnectedAt,
|
||||||
|
}) {
|
||||||
|
return RawValuesInsertable({
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
if (deviceName != null) 'device_name': deviceName,
|
||||||
|
if (deviceAddress != null) 'device_address': deviceAddress,
|
||||||
|
if (deviceType != null) 'device_type': deviceType,
|
||||||
|
if (createdAt != null) 'created_at': createdAt,
|
||||||
|
if (lastConnectedAt != null) 'last_connected_at': lastConnectedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectedDevicesCompanion copyWith(
|
||||||
|
{Value<int>? id,
|
||||||
|
Value<String>? deviceName,
|
||||||
|
Value<String>? deviceAddress,
|
||||||
|
Value<String>? deviceType,
|
||||||
|
Value<DateTime>? createdAt,
|
||||||
|
Value<DateTime?>? lastConnectedAt}) {
|
||||||
|
return ConnectedDevicesCompanion(
|
||||||
|
id: id ?? this.id,
|
||||||
|
deviceName: deviceName ?? this.deviceName,
|
||||||
|
deviceAddress: deviceAddress ?? this.deviceAddress,
|
||||||
|
deviceType: deviceType ?? this.deviceType,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
lastConnectedAt: lastConnectedAt ?? this.lastConnectedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
if (id.present) {
|
||||||
|
map['id'] = Variable<int>(id.value);
|
||||||
|
}
|
||||||
|
if (deviceName.present) {
|
||||||
|
map['device_name'] = Variable<String>(deviceName.value);
|
||||||
|
}
|
||||||
|
if (deviceAddress.present) {
|
||||||
|
map['device_address'] = Variable<String>(deviceAddress.value);
|
||||||
|
}
|
||||||
|
if (deviceType.present) {
|
||||||
|
map['device_type'] = Variable<String>(deviceType.value);
|
||||||
|
}
|
||||||
|
if (createdAt.present) {
|
||||||
|
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||||
|
}
|
||||||
|
if (lastConnectedAt.present) {
|
||||||
|
map['last_connected_at'] = Variable<DateTime>(lastConnectedAt.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('ConnectedDevicesCompanion(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('deviceName: $deviceName, ')
|
||||||
|
..write('deviceAddress: $deviceAddress, ')
|
||||||
|
..write('deviceType: $deviceType, ')
|
||||||
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('lastConnectedAt: $lastConnectedAt')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _$AppDatabase extends GeneratedDatabase {
|
||||||
|
_$AppDatabase(QueryExecutor e) : super(e);
|
||||||
|
$AppDatabaseManager get managers => $AppDatabaseManager(this);
|
||||||
|
late final $ConnectedDevicesTable connectedDevices =
|
||||||
|
$ConnectedDevicesTable(this);
|
||||||
|
@override
|
||||||
|
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||||
|
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||||
|
@override
|
||||||
|
List<DatabaseSchemaEntity> get allSchemaEntities => [connectedDevices];
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $$ConnectedDevicesTableCreateCompanionBuilder
|
||||||
|
= ConnectedDevicesCompanion Function({
|
||||||
|
Value<int> id,
|
||||||
|
required String deviceName,
|
||||||
|
required String deviceAddress,
|
||||||
|
required String deviceType,
|
||||||
|
Value<DateTime> createdAt,
|
||||||
|
Value<DateTime?> lastConnectedAt,
|
||||||
|
});
|
||||||
|
typedef $$ConnectedDevicesTableUpdateCompanionBuilder
|
||||||
|
= ConnectedDevicesCompanion Function({
|
||||||
|
Value<int> id,
|
||||||
|
Value<String> deviceName,
|
||||||
|
Value<String> deviceAddress,
|
||||||
|
Value<String> deviceType,
|
||||||
|
Value<DateTime> createdAt,
|
||||||
|
Value<DateTime?> lastConnectedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
class $$ConnectedDevicesTableFilterComposer
|
||||||
|
extends Composer<_$AppDatabase, $ConnectedDevicesTable> {
|
||||||
|
$$ConnectedDevicesTableFilterComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnFilters<int> get id => $composableBuilder(
|
||||||
|
column: $table.id, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<String> get deviceName => $composableBuilder(
|
||||||
|
column: $table.deviceName, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<String> get deviceAddress => $composableBuilder(
|
||||||
|
column: $table.deviceAddress, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<String> get deviceType => $composableBuilder(
|
||||||
|
column: $table.deviceType, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||||
|
column: $table.createdAt, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<DateTime> get lastConnectedAt => $composableBuilder(
|
||||||
|
column: $table.lastConnectedAt,
|
||||||
|
builder: (column) => ColumnFilters(column));
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$ConnectedDevicesTableOrderingComposer
|
||||||
|
extends Composer<_$AppDatabase, $ConnectedDevicesTable> {
|
||||||
|
$$ConnectedDevicesTableOrderingComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnOrderings<int> get id => $composableBuilder(
|
||||||
|
column: $table.id, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get deviceName => $composableBuilder(
|
||||||
|
column: $table.deviceName, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get deviceAddress => $composableBuilder(
|
||||||
|
column: $table.deviceAddress,
|
||||||
|
builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get deviceType => $composableBuilder(
|
||||||
|
column: $table.deviceType, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||||
|
column: $table.createdAt, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<DateTime> get lastConnectedAt => $composableBuilder(
|
||||||
|
column: $table.lastConnectedAt,
|
||||||
|
builder: (column) => ColumnOrderings(column));
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$ConnectedDevicesTableAnnotationComposer
|
||||||
|
extends Composer<_$AppDatabase, $ConnectedDevicesTable> {
|
||||||
|
$$ConnectedDevicesTableAnnotationComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
GeneratedColumn<int> get id =>
|
||||||
|
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get deviceName => $composableBuilder(
|
||||||
|
column: $table.deviceName, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get deviceAddress => $composableBuilder(
|
||||||
|
column: $table.deviceAddress, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get deviceType => $composableBuilder(
|
||||||
|
column: $table.deviceType, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<DateTime> get lastConnectedAt => $composableBuilder(
|
||||||
|
column: $table.lastConnectedAt, builder: (column) => column);
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$ConnectedDevicesTableTableManager extends RootTableManager<
|
||||||
|
_$AppDatabase,
|
||||||
|
$ConnectedDevicesTable,
|
||||||
|
ConnectedDevice,
|
||||||
|
$$ConnectedDevicesTableFilterComposer,
|
||||||
|
$$ConnectedDevicesTableOrderingComposer,
|
||||||
|
$$ConnectedDevicesTableAnnotationComposer,
|
||||||
|
$$ConnectedDevicesTableCreateCompanionBuilder,
|
||||||
|
$$ConnectedDevicesTableUpdateCompanionBuilder,
|
||||||
|
(
|
||||||
|
ConnectedDevice,
|
||||||
|
BaseReferences<_$AppDatabase, $ConnectedDevicesTable, ConnectedDevice>
|
||||||
|
),
|
||||||
|
ConnectedDevice,
|
||||||
|
PrefetchHooks Function()> {
|
||||||
|
$$ConnectedDevicesTableTableManager(
|
||||||
|
_$AppDatabase db, $ConnectedDevicesTable table)
|
||||||
|
: super(TableManagerState(
|
||||||
|
db: db,
|
||||||
|
table: table,
|
||||||
|
createFilteringComposer: () =>
|
||||||
|
$$ConnectedDevicesTableFilterComposer($db: db, $table: table),
|
||||||
|
createOrderingComposer: () =>
|
||||||
|
$$ConnectedDevicesTableOrderingComposer($db: db, $table: table),
|
||||||
|
createComputedFieldComposer: () =>
|
||||||
|
$$ConnectedDevicesTableAnnotationComposer($db: db, $table: table),
|
||||||
|
updateCompanionCallback: ({
|
||||||
|
Value<int> id = const Value.absent(),
|
||||||
|
Value<String> deviceName = const Value.absent(),
|
||||||
|
Value<String> deviceAddress = const Value.absent(),
|
||||||
|
Value<String> deviceType = const Value.absent(),
|
||||||
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
|
Value<DateTime?> lastConnectedAt = const Value.absent(),
|
||||||
|
}) =>
|
||||||
|
ConnectedDevicesCompanion(
|
||||||
|
id: id,
|
||||||
|
deviceName: deviceName,
|
||||||
|
deviceAddress: deviceAddress,
|
||||||
|
deviceType: deviceType,
|
||||||
|
createdAt: createdAt,
|
||||||
|
lastConnectedAt: lastConnectedAt,
|
||||||
|
),
|
||||||
|
createCompanionCallback: ({
|
||||||
|
Value<int> id = const Value.absent(),
|
||||||
|
required String deviceName,
|
||||||
|
required String deviceAddress,
|
||||||
|
required String deviceType,
|
||||||
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
|
Value<DateTime?> lastConnectedAt = const Value.absent(),
|
||||||
|
}) =>
|
||||||
|
ConnectedDevicesCompanion.insert(
|
||||||
|
id: id,
|
||||||
|
deviceName: deviceName,
|
||||||
|
deviceAddress: deviceAddress,
|
||||||
|
deviceType: deviceType,
|
||||||
|
createdAt: createdAt,
|
||||||
|
lastConnectedAt: lastConnectedAt,
|
||||||
|
),
|
||||||
|
withReferenceMapper: (p0) => p0
|
||||||
|
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||||
|
.toList(),
|
||||||
|
prefetchHooksCallback: null,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $$ConnectedDevicesTableProcessedTableManager = ProcessedTableManager<
|
||||||
|
_$AppDatabase,
|
||||||
|
$ConnectedDevicesTable,
|
||||||
|
ConnectedDevice,
|
||||||
|
$$ConnectedDevicesTableFilterComposer,
|
||||||
|
$$ConnectedDevicesTableOrderingComposer,
|
||||||
|
$$ConnectedDevicesTableAnnotationComposer,
|
||||||
|
$$ConnectedDevicesTableCreateCompanionBuilder,
|
||||||
|
$$ConnectedDevicesTableUpdateCompanionBuilder,
|
||||||
|
(
|
||||||
|
ConnectedDevice,
|
||||||
|
BaseReferences<_$AppDatabase, $ConnectedDevicesTable, ConnectedDevice>
|
||||||
|
),
|
||||||
|
ConnectedDevice,
|
||||||
|
PrefetchHooks Function()>;
|
||||||
|
|
||||||
|
class $AppDatabaseManager {
|
||||||
|
final _$AppDatabase _db;
|
||||||
|
$AppDatabaseManager(this._db);
|
||||||
|
$$ConnectedDevicesTableTableManager get connectedDevices =>
|
||||||
|
$$ConnectedDevicesTableTableManager(_db, _db.connectedDevices);
|
||||||
|
}
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$nConnectedDevicesHash() => r'022e744d950bb37c1016266064639e51ce6031b5';
|
||||||
|
|
||||||
|
/// See also [NConnectedDevices].
|
||||||
|
@ProviderFor(NConnectedDevices)
|
||||||
|
final nConnectedDevicesProvider = AutoDisposeAsyncNotifierProvider<
|
||||||
|
NConnectedDevices, List<ConnectedDevice>>.internal(
|
||||||
|
NConnectedDevices.new,
|
||||||
|
name: r'nConnectedDevicesProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$nConnectedDevicesHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$NConnectedDevices = AutoDisposeAsyncNotifier<List<ConnectedDevice>>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
@ -1,19 +1,23 @@
|
|||||||
import 'package:abawo_bt_app/pages/devices_page.dart';
|
import 'package:abawo_bt_app/pages/devices_page.dart';
|
||||||
|
import 'package:abawo_bt_app/src/rust/frb_generated.dart';
|
||||||
import 'package:abawo_bt_app/util/sharedPrefs.dart';
|
import 'package:abawo_bt_app/util/sharedPrefs.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:nb_utils/nb_utils.dart';
|
||||||
import 'pages/home_page.dart';
|
import 'pages/home_page.dart';
|
||||||
import 'pages/settings_page.dart';
|
import 'pages/settings_page.dart';
|
||||||
|
import 'package:abawo_bt_app/pages/device_details_page.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
Logger.root.level = Level.ALL; // defaults to Level.INFO
|
Logger.root.level = Level.ALL; // defaults to Level.INFO
|
||||||
Logger.root.onRecord.listen((record) {
|
Logger.root.onRecord.listen((record) {
|
||||||
print('${record.level.name}: ${record.time}: ${record.message}');
|
print('${record.level.name}: ${record.time}: ${record.message}');
|
||||||
});
|
});
|
||||||
|
await RustLib.init();
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await initialize();
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
@ -49,6 +53,7 @@ class AbawoBtApp extends StatelessWidget {
|
|||||||
|
|
||||||
// Configure GoRouter
|
// Configure GoRouter
|
||||||
final _router = GoRouter(
|
final _router = GoRouter(
|
||||||
|
navigatorKey: navigatorKey,
|
||||||
initialLocation: '/',
|
initialLocation: '/',
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
@ -65,5 +70,42 @@ final _router = GoRouter(
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/device/:deviceAddress',
|
||||||
|
builder: (context, state) {
|
||||||
|
final deviceAddress = state.pathParameters['deviceAddress']!;
|
||||||
|
return DeviceDetailsPage(deviceAddress: deviceAddress);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:abawo_bt_app/src/rust/api/simple.dart';
|
||||||
|
import 'package:abawo_bt_app/src/rust/frb_generated.dart';
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
await RustLib.init();
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('flutter_rust_bridge quickstart')),
|
||||||
|
body: Center(
|
||||||
|
child: Text(
|
||||||
|
'Action: Call Rust `greet("Tom")`\nResult: `${greet(name: "Tom")}`'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
import 'package:abawo_bt_app/util/constants.dart';
|
||||||
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart' show DeviceIdentifier;
|
||||||
|
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
part 'bluetooth_device_model.freezed.dart';
|
part 'bluetooth_device_model.freezed.dart';
|
||||||
@ -12,6 +15,24 @@ enum DeviceType {
|
|||||||
other,
|
other,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DeviceType deviceTypeFromUuids(List<Uuid> uuids) {
|
||||||
|
if (uuids.any((uuid) => isAbawoUniversalShiftersDeviceGuid(uuid))) {
|
||||||
|
return DeviceType.universalShifters;
|
||||||
|
}
|
||||||
|
return DeviceType.other;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceType deviceTypeFromString(String type) {
|
||||||
|
return DeviceType.values.firstWhere(
|
||||||
|
(e) => e.toString().split('.').last == type,
|
||||||
|
orElse: () => DeviceType.other,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String deviceTypeToString(DeviceType type) {
|
||||||
|
return type.toString().split('.').last;
|
||||||
|
}
|
||||||
|
|
||||||
/// Model representing a Bluetooth device
|
/// Model representing a Bluetooth device
|
||||||
@freezed
|
@freezed
|
||||||
abstract class BluetoothDeviceModel with _$BluetoothDeviceModel {
|
abstract class BluetoothDeviceModel with _$BluetoothDeviceModel {
|
||||||
@ -25,23 +46,28 @@ abstract class BluetoothDeviceModel with _$BluetoothDeviceModel {
|
|||||||
/// MAC address of the device
|
/// MAC address of the device
|
||||||
required String address,
|
required String address,
|
||||||
|
|
||||||
/// Signal strength indicator (RSSI)
|
|
||||||
int? rssi,
|
|
||||||
|
|
||||||
/// Type of the device
|
/// Type of the device
|
||||||
@Default(DeviceType.other) DeviceType type,
|
@Default(DeviceType.other) DeviceType type,
|
||||||
|
|
||||||
/// Whether the device is currently connected
|
|
||||||
@Default(false) bool isConnected,
|
|
||||||
|
|
||||||
/// Additional device information
|
/// Additional device information
|
||||||
Map<String, dynamic>? manufacturerData,
|
Map<String, dynamic>? manufacturerData,
|
||||||
|
|
||||||
/// Service UUIDs advertised by the device
|
/// Identifier of the device
|
||||||
List<String>? serviceUuids,
|
@DeviceIdentJsonConverter() required DeviceIdentifier deviceIdent,
|
||||||
}) = _BluetoothDeviceModel;
|
}) = _BluetoothDeviceModel;
|
||||||
|
|
||||||
/// Create a BluetoothDeviceModel from JSON
|
/// Create a BluetoothDeviceModel from JSON
|
||||||
factory BluetoothDeviceModel.fromJson(Map<String, dynamic> json) =>
|
factory BluetoothDeviceModel.fromJson(Map<String, dynamic> json) =>
|
||||||
_$BluetoothDeviceModelFromJson(json);
|
_$BluetoothDeviceModelFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DeviceIdentJsonConverter
|
||||||
|
implements JsonConverter<DeviceIdentifier, String> {
|
||||||
|
const DeviceIdentJsonConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
DeviceIdentifier fromJson(String json) => DeviceIdentifier(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toJson(DeviceIdentifier object) => object.str;
|
||||||
|
}
|
||||||
|
|||||||
@ -24,20 +24,15 @@ mixin _$BluetoothDeviceModel {
|
|||||||
/// MAC address of the device
|
/// MAC address of the device
|
||||||
String get address;
|
String get address;
|
||||||
|
|
||||||
/// Signal strength indicator (RSSI)
|
|
||||||
int? get rssi;
|
|
||||||
|
|
||||||
/// Type of the device
|
/// Type of the device
|
||||||
DeviceType get type;
|
DeviceType get type;
|
||||||
|
|
||||||
/// Whether the device is currently connected
|
|
||||||
bool get isConnected;
|
|
||||||
|
|
||||||
/// Additional device information
|
/// Additional device information
|
||||||
Map<String, dynamic>? get manufacturerData;
|
Map<String, dynamic>? get manufacturerData;
|
||||||
|
|
||||||
/// Service UUIDs advertised by the device
|
/// Identifier of the device
|
||||||
List<String>? get serviceUuids;
|
@DeviceIdentJsonConverter()
|
||||||
|
DeviceIdentifier get deviceIdent;
|
||||||
|
|
||||||
/// Create a copy of BluetoothDeviceModel
|
/// Create a copy of BluetoothDeviceModel
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@ -58,32 +53,21 @@ mixin _$BluetoothDeviceModel {
|
|||||||
(identical(other.id, id) || other.id == id) &&
|
(identical(other.id, id) || other.id == id) &&
|
||||||
(identical(other.name, name) || other.name == name) &&
|
(identical(other.name, name) || other.name == name) &&
|
||||||
(identical(other.address, address) || other.address == address) &&
|
(identical(other.address, address) || other.address == address) &&
|
||||||
(identical(other.rssi, rssi) || other.rssi == rssi) &&
|
|
||||||
(identical(other.type, type) || other.type == type) &&
|
(identical(other.type, type) || other.type == type) &&
|
||||||
(identical(other.isConnected, isConnected) ||
|
|
||||||
other.isConnected == isConnected) &&
|
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other.manufacturerData, manufacturerData) &&
|
.equals(other.manufacturerData, manufacturerData) &&
|
||||||
const DeepCollectionEquality()
|
(identical(other.deviceIdent, deviceIdent) ||
|
||||||
.equals(other.serviceUuids, serviceUuids));
|
other.deviceIdent == deviceIdent));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(
|
int get hashCode => Object.hash(runtimeType, id, name, address, type,
|
||||||
runtimeType,
|
const DeepCollectionEquality().hash(manufacturerData), deviceIdent);
|
||||||
id,
|
|
||||||
name,
|
|
||||||
address,
|
|
||||||
rssi,
|
|
||||||
type,
|
|
||||||
isConnected,
|
|
||||||
const DeepCollectionEquality().hash(manufacturerData),
|
|
||||||
const DeepCollectionEquality().hash(serviceUuids));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'BluetoothDeviceModel(id: $id, name: $name, address: $address, rssi: $rssi, type: $type, isConnected: $isConnected, manufacturerData: $manufacturerData, serviceUuids: $serviceUuids)';
|
return 'BluetoothDeviceModel(id: $id, name: $name, address: $address, type: $type, manufacturerData: $manufacturerData, deviceIdent: $deviceIdent)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,11 +81,9 @@ abstract mixin class $BluetoothDeviceModelCopyWith<$Res> {
|
|||||||
{String id,
|
{String id,
|
||||||
String? name,
|
String? name,
|
||||||
String address,
|
String address,
|
||||||
int? rssi,
|
|
||||||
DeviceType type,
|
DeviceType type,
|
||||||
bool isConnected,
|
|
||||||
Map<String, dynamic>? manufacturerData,
|
Map<String, dynamic>? manufacturerData,
|
||||||
List<String>? serviceUuids});
|
@DeviceIdentJsonConverter() DeviceIdentifier deviceIdent});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -120,11 +102,9 @@ class _$BluetoothDeviceModelCopyWithImpl<$Res>
|
|||||||
Object? id = null,
|
Object? id = null,
|
||||||
Object? name = freezed,
|
Object? name = freezed,
|
||||||
Object? address = null,
|
Object? address = null,
|
||||||
Object? rssi = freezed,
|
|
||||||
Object? type = null,
|
Object? type = null,
|
||||||
Object? isConnected = null,
|
|
||||||
Object? manufacturerData = freezed,
|
Object? manufacturerData = freezed,
|
||||||
Object? serviceUuids = freezed,
|
Object? deviceIdent = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
id: null == id
|
id: null == id
|
||||||
@ -139,26 +119,18 @@ class _$BluetoothDeviceModelCopyWithImpl<$Res>
|
|||||||
? _self.address
|
? _self.address
|
||||||
: address // ignore: cast_nullable_to_non_nullable
|
: address // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
rssi: freezed == rssi
|
|
||||||
? _self.rssi
|
|
||||||
: rssi // ignore: cast_nullable_to_non_nullable
|
|
||||||
as int?,
|
|
||||||
type: null == type
|
type: null == type
|
||||||
? _self.type
|
? _self.type
|
||||||
: type // ignore: cast_nullable_to_non_nullable
|
: type // ignore: cast_nullable_to_non_nullable
|
||||||
as DeviceType,
|
as DeviceType,
|
||||||
isConnected: null == isConnected
|
|
||||||
? _self.isConnected
|
|
||||||
: isConnected // ignore: cast_nullable_to_non_nullable
|
|
||||||
as bool,
|
|
||||||
manufacturerData: freezed == manufacturerData
|
manufacturerData: freezed == manufacturerData
|
||||||
? _self.manufacturerData
|
? _self.manufacturerData
|
||||||
: manufacturerData // ignore: cast_nullable_to_non_nullable
|
: manufacturerData // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>?,
|
as Map<String, dynamic>?,
|
||||||
serviceUuids: freezed == serviceUuids
|
deviceIdent: null == deviceIdent
|
||||||
? _self.serviceUuids
|
? _self.deviceIdent
|
||||||
: serviceUuids // ignore: cast_nullable_to_non_nullable
|
: deviceIdent // ignore: cast_nullable_to_non_nullable
|
||||||
as List<String>?,
|
as DeviceIdentifier,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,13 +142,10 @@ class _BluetoothDeviceModel implements BluetoothDeviceModel {
|
|||||||
{required this.id,
|
{required this.id,
|
||||||
this.name,
|
this.name,
|
||||||
required this.address,
|
required this.address,
|
||||||
this.rssi,
|
|
||||||
this.type = DeviceType.other,
|
this.type = DeviceType.other,
|
||||||
this.isConnected = false,
|
|
||||||
final Map<String, dynamic>? manufacturerData,
|
final Map<String, dynamic>? manufacturerData,
|
||||||
final List<String>? serviceUuids})
|
@DeviceIdentJsonConverter() required this.deviceIdent})
|
||||||
: _manufacturerData = manufacturerData,
|
: _manufacturerData = manufacturerData;
|
||||||
_serviceUuids = serviceUuids;
|
|
||||||
factory _BluetoothDeviceModel.fromJson(Map<String, dynamic> json) =>
|
factory _BluetoothDeviceModel.fromJson(Map<String, dynamic> json) =>
|
||||||
_$BluetoothDeviceModelFromJson(json);
|
_$BluetoothDeviceModelFromJson(json);
|
||||||
|
|
||||||
@ -192,20 +161,11 @@ class _BluetoothDeviceModel implements BluetoothDeviceModel {
|
|||||||
@override
|
@override
|
||||||
final String address;
|
final String address;
|
||||||
|
|
||||||
/// Signal strength indicator (RSSI)
|
|
||||||
@override
|
|
||||||
final int? rssi;
|
|
||||||
|
|
||||||
/// Type of the device
|
/// Type of the device
|
||||||
@override
|
@override
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
final DeviceType type;
|
final DeviceType type;
|
||||||
|
|
||||||
/// Whether the device is currently connected
|
|
||||||
@override
|
|
||||||
@JsonKey()
|
|
||||||
final bool isConnected;
|
|
||||||
|
|
||||||
/// Additional device information
|
/// Additional device information
|
||||||
final Map<String, dynamic>? _manufacturerData;
|
final Map<String, dynamic>? _manufacturerData;
|
||||||
|
|
||||||
@ -219,18 +179,10 @@ class _BluetoothDeviceModel implements BluetoothDeviceModel {
|
|||||||
return EqualUnmodifiableMapView(value);
|
return EqualUnmodifiableMapView(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Service UUIDs advertised by the device
|
/// Identifier of the device
|
||||||
final List<String>? _serviceUuids;
|
|
||||||
|
|
||||||
/// Service UUIDs advertised by the device
|
|
||||||
@override
|
@override
|
||||||
List<String>? get serviceUuids {
|
@DeviceIdentJsonConverter()
|
||||||
final value = _serviceUuids;
|
final DeviceIdentifier deviceIdent;
|
||||||
if (value == null) return null;
|
|
||||||
if (_serviceUuids is EqualUnmodifiableListView) return _serviceUuids;
|
|
||||||
// ignore: implicit_dynamic_type
|
|
||||||
return EqualUnmodifiableListView(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a copy of BluetoothDeviceModel
|
/// Create a copy of BluetoothDeviceModel
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@ -256,32 +208,21 @@ class _BluetoothDeviceModel implements BluetoothDeviceModel {
|
|||||||
(identical(other.id, id) || other.id == id) &&
|
(identical(other.id, id) || other.id == id) &&
|
||||||
(identical(other.name, name) || other.name == name) &&
|
(identical(other.name, name) || other.name == name) &&
|
||||||
(identical(other.address, address) || other.address == address) &&
|
(identical(other.address, address) || other.address == address) &&
|
||||||
(identical(other.rssi, rssi) || other.rssi == rssi) &&
|
|
||||||
(identical(other.type, type) || other.type == type) &&
|
(identical(other.type, type) || other.type == type) &&
|
||||||
(identical(other.isConnected, isConnected) ||
|
|
||||||
other.isConnected == isConnected) &&
|
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other._manufacturerData, _manufacturerData) &&
|
.equals(other._manufacturerData, _manufacturerData) &&
|
||||||
const DeepCollectionEquality()
|
(identical(other.deviceIdent, deviceIdent) ||
|
||||||
.equals(other._serviceUuids, _serviceUuids));
|
other.deviceIdent == deviceIdent));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(
|
int get hashCode => Object.hash(runtimeType, id, name, address, type,
|
||||||
runtimeType,
|
const DeepCollectionEquality().hash(_manufacturerData), deviceIdent);
|
||||||
id,
|
|
||||||
name,
|
|
||||||
address,
|
|
||||||
rssi,
|
|
||||||
type,
|
|
||||||
isConnected,
|
|
||||||
const DeepCollectionEquality().hash(_manufacturerData),
|
|
||||||
const DeepCollectionEquality().hash(_serviceUuids));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'BluetoothDeviceModel(id: $id, name: $name, address: $address, rssi: $rssi, type: $type, isConnected: $isConnected, manufacturerData: $manufacturerData, serviceUuids: $serviceUuids)';
|
return 'BluetoothDeviceModel(id: $id, name: $name, address: $address, type: $type, manufacturerData: $manufacturerData, deviceIdent: $deviceIdent)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,11 +238,9 @@ abstract mixin class _$BluetoothDeviceModelCopyWith<$Res>
|
|||||||
{String id,
|
{String id,
|
||||||
String? name,
|
String? name,
|
||||||
String address,
|
String address,
|
||||||
int? rssi,
|
|
||||||
DeviceType type,
|
DeviceType type,
|
||||||
bool isConnected,
|
|
||||||
Map<String, dynamic>? manufacturerData,
|
Map<String, dynamic>? manufacturerData,
|
||||||
List<String>? serviceUuids});
|
@DeviceIdentJsonConverter() DeviceIdentifier deviceIdent});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -320,11 +259,9 @@ class __$BluetoothDeviceModelCopyWithImpl<$Res>
|
|||||||
Object? id = null,
|
Object? id = null,
|
||||||
Object? name = freezed,
|
Object? name = freezed,
|
||||||
Object? address = null,
|
Object? address = null,
|
||||||
Object? rssi = freezed,
|
|
||||||
Object? type = null,
|
Object? type = null,
|
||||||
Object? isConnected = null,
|
|
||||||
Object? manufacturerData = freezed,
|
Object? manufacturerData = freezed,
|
||||||
Object? serviceUuids = freezed,
|
Object? deviceIdent = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_BluetoothDeviceModel(
|
return _then(_BluetoothDeviceModel(
|
||||||
id: null == id
|
id: null == id
|
||||||
@ -339,26 +276,18 @@ class __$BluetoothDeviceModelCopyWithImpl<$Res>
|
|||||||
? _self.address
|
? _self.address
|
||||||
: address // ignore: cast_nullable_to_non_nullable
|
: address // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
rssi: freezed == rssi
|
|
||||||
? _self.rssi
|
|
||||||
: rssi // ignore: cast_nullable_to_non_nullable
|
|
||||||
as int?,
|
|
||||||
type: null == type
|
type: null == type
|
||||||
? _self.type
|
? _self.type
|
||||||
: type // ignore: cast_nullable_to_non_nullable
|
: type // ignore: cast_nullable_to_non_nullable
|
||||||
as DeviceType,
|
as DeviceType,
|
||||||
isConnected: null == isConnected
|
|
||||||
? _self.isConnected
|
|
||||||
: isConnected // ignore: cast_nullable_to_non_nullable
|
|
||||||
as bool,
|
|
||||||
manufacturerData: freezed == manufacturerData
|
manufacturerData: freezed == manufacturerData
|
||||||
? _self._manufacturerData
|
? _self._manufacturerData
|
||||||
: manufacturerData // ignore: cast_nullable_to_non_nullable
|
: manufacturerData // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>?,
|
as Map<String, dynamic>?,
|
||||||
serviceUuids: freezed == serviceUuids
|
deviceIdent: null == deviceIdent
|
||||||
? _self._serviceUuids
|
? _self.deviceIdent
|
||||||
: serviceUuids // ignore: cast_nullable_to_non_nullable
|
: deviceIdent // ignore: cast_nullable_to_non_nullable
|
||||||
as List<String>?,
|
as DeviceIdentifier,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,14 +12,11 @@ _BluetoothDeviceModel _$BluetoothDeviceModelFromJson(
|
|||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
name: json['name'] as String?,
|
name: json['name'] as String?,
|
||||||
address: json['address'] as String,
|
address: json['address'] as String,
|
||||||
rssi: (json['rssi'] as num?)?.toInt(),
|
|
||||||
type: $enumDecodeNullable(_$DeviceTypeEnumMap, json['type']) ??
|
type: $enumDecodeNullable(_$DeviceTypeEnumMap, json['type']) ??
|
||||||
DeviceType.other,
|
DeviceType.other,
|
||||||
isConnected: json['isConnected'] as bool? ?? false,
|
|
||||||
manufacturerData: json['manufacturerData'] as Map<String, dynamic>?,
|
manufacturerData: json['manufacturerData'] as Map<String, dynamic>?,
|
||||||
serviceUuids: (json['serviceUuids'] as List<dynamic>?)
|
deviceIdent: const DeviceIdentJsonConverter()
|
||||||
?.map((e) => e as String)
|
.fromJson(json['deviceIdent'] as String),
|
||||||
.toList(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$BluetoothDeviceModelToJson(
|
Map<String, dynamic> _$BluetoothDeviceModelToJson(
|
||||||
@ -28,11 +25,10 @@ Map<String, dynamic> _$BluetoothDeviceModelToJson(
|
|||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
'address': instance.address,
|
'address': instance.address,
|
||||||
'rssi': instance.rssi,
|
|
||||||
'type': _$DeviceTypeEnumMap[instance.type]!,
|
'type': _$DeviceTypeEnumMap[instance.type]!,
|
||||||
'isConnected': instance.isConnected,
|
|
||||||
'manufacturerData': instance.manufacturerData,
|
'manufacturerData': instance.manufacturerData,
|
||||||
'serviceUuids': instance.serviceUuids,
|
'deviceIdent':
|
||||||
|
const DeviceIdentJsonConverter().toJson(instance.deviceIdent),
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$DeviceTypeEnumMap = {
|
const _$DeviceTypeEnumMap = {
|
||||||
|
|||||||
70
lib/model/firmware_file_selection.dart
Normal file
70
lib/model/firmware_file_selection.dart
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
class DfuV1FirmwareMetadata {
|
||||||
|
const DfuV1FirmwareMetadata({
|
||||||
|
required this.totalLength,
|
||||||
|
required this.crc32,
|
||||||
|
required this.sessionId,
|
||||||
|
required this.flags,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int totalLength;
|
||||||
|
final int crc32;
|
||||||
|
final int sessionId;
|
||||||
|
final int flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DfuV1PreparedFirmware {
|
||||||
|
const DfuV1PreparedFirmware({
|
||||||
|
required this.fileName,
|
||||||
|
required this.fileBytes,
|
||||||
|
required this.metadata,
|
||||||
|
this.filePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String fileName;
|
||||||
|
final String? filePath;
|
||||||
|
final Uint8List fileBytes;
|
||||||
|
final DfuV1FirmwareMetadata metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FirmwareSelectionFailureReason {
|
||||||
|
canceled,
|
||||||
|
malformedSelection,
|
||||||
|
unsupportedExtension,
|
||||||
|
emptyFile,
|
||||||
|
readFailed,
|
||||||
|
}
|
||||||
|
|
||||||
|
class FirmwareSelectionFailure {
|
||||||
|
const FirmwareSelectionFailure({
|
||||||
|
required this.reason,
|
||||||
|
required this.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
final FirmwareSelectionFailureReason reason;
|
||||||
|
final String message;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FirmwareFileSelectionResult {
|
||||||
|
const FirmwareFileSelectionResult._({
|
||||||
|
this.firmware,
|
||||||
|
this.failure,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DfuV1PreparedFirmware? firmware;
|
||||||
|
final FirmwareSelectionFailure? failure;
|
||||||
|
|
||||||
|
bool get isSuccess => firmware != null;
|
||||||
|
|
||||||
|
bool get isCanceled =>
|
||||||
|
failure?.reason == FirmwareSelectionFailureReason.canceled;
|
||||||
|
|
||||||
|
static FirmwareFileSelectionResult success(DfuV1PreparedFirmware firmware) {
|
||||||
|
return FirmwareFileSelectionResult._(firmware: firmware);
|
||||||
|
}
|
||||||
|
|
||||||
|
static FirmwareFileSelectionResult failed(FirmwareSelectionFailure failure) {
|
||||||
|
return FirmwareFileSelectionResult._(failure: failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
456
lib/model/shifter_types.dart
Normal file
456
lib/model/shifter_types.dart
Normal file
@ -0,0 +1,456 @@
|
|||||||
|
import 'package:cbor/simple.dart';
|
||||||
|
|
||||||
|
const String universalShifterControlServiceUuid =
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba4ffc2';
|
||||||
|
const String universalShifterStatusCharacteristicUuid =
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba40000';
|
||||||
|
const String universalShifterConnectToAddrCharacteristicUuid =
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba40001';
|
||||||
|
const String universalShifterCommandCharacteristicUuid =
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba40005';
|
||||||
|
const String universalShifterGearRatiosCharacteristicUuid =
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba40006';
|
||||||
|
const String universalShifterDfuControlCharacteristicUuid =
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba40008';
|
||||||
|
const String universalShifterDfuDataCharacteristicUuid =
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba40009';
|
||||||
|
const String universalShifterDfuAckCharacteristicUuid =
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba4000a';
|
||||||
|
const String ftmsServiceUuid = '00001826-0000-1000-8000-00805f9b34fb';
|
||||||
|
|
||||||
|
const int universalShifterDfuOpcodeStart = 0x01;
|
||||||
|
const int universalShifterDfuOpcodeFinish = 0x02;
|
||||||
|
const int universalShifterDfuOpcodeAbort = 0x03;
|
||||||
|
|
||||||
|
const int universalShifterDfuFrameSizeBytes = 64;
|
||||||
|
const int universalShifterDfuFramePayloadSizeBytes = 63;
|
||||||
|
const int universalShifterAttWriteOverheadBytes = 3;
|
||||||
|
const int universalShifterDfuMinimumMtu =
|
||||||
|
universalShifterDfuFrameSizeBytes + universalShifterAttWriteOverheadBytes;
|
||||||
|
const int universalShifterDfuPreferredMtu = 128;
|
||||||
|
|
||||||
|
const int universalShifterDfuFlagEncrypted = 0x01;
|
||||||
|
const int universalShifterDfuFlagSigned = 0x02;
|
||||||
|
const int universalShifterDfuFlagNone = 0x00;
|
||||||
|
|
||||||
|
const int errorSequence = 1;
|
||||||
|
const int errorFtmsMissing = 2;
|
||||||
|
const int errorPairingAuth = 3;
|
||||||
|
const int errorPairingEncrypt = 4;
|
||||||
|
const int errorFtmsRequiredCharMissing = 5;
|
||||||
|
|
||||||
|
enum DfuUpdateState {
|
||||||
|
idle,
|
||||||
|
starting,
|
||||||
|
waitingForAck,
|
||||||
|
transferring,
|
||||||
|
finishing,
|
||||||
|
completed,
|
||||||
|
aborted,
|
||||||
|
failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
class DfuUpdateFlags {
|
||||||
|
const DfuUpdateFlags({
|
||||||
|
this.encrypted = false,
|
||||||
|
this.signed = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool encrypted;
|
||||||
|
final bool signed;
|
||||||
|
|
||||||
|
int get rawValue {
|
||||||
|
var value = 0;
|
||||||
|
if (encrypted) {
|
||||||
|
value |= universalShifterDfuFlagEncrypted;
|
||||||
|
}
|
||||||
|
if (signed) {
|
||||||
|
value |= universalShifterDfuFlagSigned;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static DfuUpdateFlags fromRaw(int rawValue) {
|
||||||
|
return DfuUpdateFlags(
|
||||||
|
encrypted: (rawValue & universalShifterDfuFlagEncrypted) != 0,
|
||||||
|
signed: (rawValue & universalShifterDfuFlagSigned) != 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DfuUpdateProgress {
|
||||||
|
const DfuUpdateProgress({
|
||||||
|
required this.state,
|
||||||
|
required this.totalBytes,
|
||||||
|
required this.sentBytes,
|
||||||
|
required this.lastAckedSequence,
|
||||||
|
required this.sessionId,
|
||||||
|
required this.flags,
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DfuUpdateState state;
|
||||||
|
final int totalBytes;
|
||||||
|
final int sentBytes;
|
||||||
|
final int lastAckedSequence;
|
||||||
|
final int sessionId;
|
||||||
|
final DfuUpdateFlags flags;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
double get fractionComplete {
|
||||||
|
if (totalBytes <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return sentBytes.clamp(0, totalBytes) / totalBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get percentComplete => (fractionComplete * 100).round();
|
||||||
|
|
||||||
|
bool get isTerminal =>
|
||||||
|
state == DfuUpdateState.completed ||
|
||||||
|
state == DfuUpdateState.aborted ||
|
||||||
|
state == DfuUpdateState.failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DfuPreflightFailureReason {
|
||||||
|
deviceNotConnected,
|
||||||
|
wrongConnectedDevice,
|
||||||
|
mtuRequestFailed,
|
||||||
|
mtuTooLow,
|
||||||
|
}
|
||||||
|
|
||||||
|
class DfuPreflightResult {
|
||||||
|
const DfuPreflightResult._({
|
||||||
|
required this.requestedMtu,
|
||||||
|
required this.requiredMtu,
|
||||||
|
required this.negotiatedMtu,
|
||||||
|
required this.failureReason,
|
||||||
|
required this.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int requestedMtu;
|
||||||
|
final int requiredMtu;
|
||||||
|
final int? negotiatedMtu;
|
||||||
|
final DfuPreflightFailureReason? failureReason;
|
||||||
|
final String? message;
|
||||||
|
|
||||||
|
bool get canStart => failureReason == null;
|
||||||
|
|
||||||
|
static DfuPreflightResult ready({
|
||||||
|
required int requestedMtu,
|
||||||
|
required int negotiatedMtu,
|
||||||
|
int requiredMtu = universalShifterDfuMinimumMtu,
|
||||||
|
}) {
|
||||||
|
return DfuPreflightResult._(
|
||||||
|
requestedMtu: requestedMtu,
|
||||||
|
requiredMtu: requiredMtu,
|
||||||
|
negotiatedMtu: negotiatedMtu,
|
||||||
|
failureReason: null,
|
||||||
|
message: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static DfuPreflightResult failed({
|
||||||
|
required int requestedMtu,
|
||||||
|
required DfuPreflightFailureReason failureReason,
|
||||||
|
required String message,
|
||||||
|
int requiredMtu = universalShifterDfuMinimumMtu,
|
||||||
|
int? negotiatedMtu,
|
||||||
|
}) {
|
||||||
|
return DfuPreflightResult._(
|
||||||
|
requestedMtu: requestedMtu,
|
||||||
|
requiredMtu: requiredMtu,
|
||||||
|
negotiatedMtu: negotiatedMtu,
|
||||||
|
failureReason: failureReason,
|
||||||
|
message: message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShifterErrorInfo {
|
||||||
|
const ShifterErrorInfo({
|
||||||
|
required this.code,
|
||||||
|
required this.title,
|
||||||
|
required this.details,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int code;
|
||||||
|
final String title;
|
||||||
|
final String details;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShifterErrorInfo shifterErrorInfo(int code) {
|
||||||
|
switch (code) {
|
||||||
|
case errorSequence:
|
||||||
|
return const ShifterErrorInfo(
|
||||||
|
code: errorSequence,
|
||||||
|
title: 'Invalid command sequence',
|
||||||
|
details:
|
||||||
|
'The button received Connect without a fresh target address. The app must write connect_to_addr first, then the connect command.',
|
||||||
|
);
|
||||||
|
case errorFtmsMissing:
|
||||||
|
return const ShifterErrorInfo(
|
||||||
|
code: errorFtmsMissing,
|
||||||
|
title: 'FTMS service missing',
|
||||||
|
details:
|
||||||
|
'The selected bike does not expose the FTMS service (UUID 0x1826), so pairing cannot continue.',
|
||||||
|
);
|
||||||
|
case errorPairingAuth:
|
||||||
|
return const ShifterErrorInfo(
|
||||||
|
code: errorPairingAuth,
|
||||||
|
title: 'Pairing authentication failed',
|
||||||
|
details:
|
||||||
|
'Bonding authentication with the bike failed. Remove old bonds on both devices and try pairing again nearby.',
|
||||||
|
);
|
||||||
|
case errorPairingEncrypt:
|
||||||
|
return const ShifterErrorInfo(
|
||||||
|
code: errorPairingEncrypt,
|
||||||
|
title: 'Pairing/encryption failed',
|
||||||
|
details:
|
||||||
|
'The secure link to the bike could not be established. Retry close to the bike and ensure it is pairable.',
|
||||||
|
);
|
||||||
|
case errorFtmsRequiredCharMissing:
|
||||||
|
return const ShifterErrorInfo(
|
||||||
|
code: errorFtmsRequiredCharMissing,
|
||||||
|
title: 'Required FTMS characteristic missing',
|
||||||
|
details:
|
||||||
|
'The bike has FTMS but is missing required characteristics (for example Indoor Bike Data), so control cannot start.',
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return ShifterErrorInfo(
|
||||||
|
code: code,
|
||||||
|
title: 'Unknown error',
|
||||||
|
details: 'The button reported an unknown error code ($code).',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UniversalShifterCommand {
|
||||||
|
reset(0x00),
|
||||||
|
startScan(0x01),
|
||||||
|
stopScan(0x02),
|
||||||
|
connectToDevice(0x03),
|
||||||
|
disconnect(0x04),
|
||||||
|
turnOff(0x05);
|
||||||
|
|
||||||
|
const UniversalShifterCommand(this.value);
|
||||||
|
final int value;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ControlConnectionState {
|
||||||
|
disconnected,
|
||||||
|
connected;
|
||||||
|
|
||||||
|
static ControlConnectionState fromRaw(dynamic raw) {
|
||||||
|
if (raw is int) {
|
||||||
|
return raw == 1
|
||||||
|
? ControlConnectionState.connected
|
||||||
|
: ControlConnectionState.disconnected;
|
||||||
|
}
|
||||||
|
if (raw is String) {
|
||||||
|
final normalized = raw.toLowerCase();
|
||||||
|
if (normalized.contains('connected')) {
|
||||||
|
return ControlConnectionState.connected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ControlConnectionState.disconnected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TrainerConnectionState {
|
||||||
|
idle,
|
||||||
|
connecting,
|
||||||
|
pairing,
|
||||||
|
connected,
|
||||||
|
discoveringFtms,
|
||||||
|
ftmsReady,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrainerStatus {
|
||||||
|
const TrainerStatus({required this.state, this.errorCode});
|
||||||
|
|
||||||
|
final TrainerConnectionState state;
|
||||||
|
final int? errorCode;
|
||||||
|
|
||||||
|
String get label {
|
||||||
|
switch (state) {
|
||||||
|
case TrainerConnectionState.idle:
|
||||||
|
return 'Idle';
|
||||||
|
case TrainerConnectionState.connecting:
|
||||||
|
return 'Connecting';
|
||||||
|
case TrainerConnectionState.pairing:
|
||||||
|
return 'Pairing';
|
||||||
|
case TrainerConnectionState.connected:
|
||||||
|
return 'Connected';
|
||||||
|
case TrainerConnectionState.discoveringFtms:
|
||||||
|
return 'Discovering FTMS';
|
||||||
|
case TrainerConnectionState.ftmsReady:
|
||||||
|
return 'FTMS Ready';
|
||||||
|
case TrainerConnectionState.error:
|
||||||
|
return 'Error${errorCode != null ? ' ($errorCode)' : ''}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static TrainerStatus fromRaw(dynamic raw) {
|
||||||
|
if (raw is int) {
|
||||||
|
switch (raw) {
|
||||||
|
case 1:
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.connecting);
|
||||||
|
case 2:
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.pairing);
|
||||||
|
case 3:
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.connected);
|
||||||
|
case 4:
|
||||||
|
return const TrainerStatus(
|
||||||
|
state: TrainerConnectionState.discoveringFtms);
|
||||||
|
case 5:
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.ftmsReady);
|
||||||
|
default:
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.idle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw is List && raw.isNotEmpty) {
|
||||||
|
final variant = raw.first;
|
||||||
|
final value = raw.length > 1 ? raw[1] : null;
|
||||||
|
if (variant is int && (variant == 5 || variant == 6)) {
|
||||||
|
return TrainerStatus(
|
||||||
|
state: TrainerConnectionState.error,
|
||||||
|
errorCode: value is int ? value : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw is Map) {
|
||||||
|
final entry = raw.entries.isNotEmpty ? raw.entries.first : null;
|
||||||
|
if (entry != null) {
|
||||||
|
final key = entry.key;
|
||||||
|
final value = entry.value;
|
||||||
|
if ((key is int && (key == 5 || key == 6)) ||
|
||||||
|
(key is String && key.toLowerCase().contains('error'))) {
|
||||||
|
return TrainerStatus(
|
||||||
|
state: TrainerConnectionState.error,
|
||||||
|
errorCode: value is int ? value : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw is String) {
|
||||||
|
final normalized = raw.toLowerCase();
|
||||||
|
if (normalized.contains('connecting')) {
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.connecting);
|
||||||
|
}
|
||||||
|
if (normalized.contains('pairing')) {
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.pairing);
|
||||||
|
}
|
||||||
|
if (normalized.contains('connected')) {
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.connected);
|
||||||
|
}
|
||||||
|
if (normalized.contains('discover')) {
|
||||||
|
return const TrainerStatus(
|
||||||
|
state: TrainerConnectionState.discoveringFtms);
|
||||||
|
}
|
||||||
|
if (normalized.contains('ready')) {
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.ftmsReady);
|
||||||
|
}
|
||||||
|
if (normalized.contains('error')) {
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.idle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CentralStatus {
|
||||||
|
const CentralStatus({
|
||||||
|
required this.control,
|
||||||
|
required this.trainer,
|
||||||
|
required this.hasSavedBond,
|
||||||
|
required this.connectedTrainerAddr,
|
||||||
|
required this.lastFailure,
|
||||||
|
required this.raw,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ControlConnectionState control;
|
||||||
|
final TrainerStatus trainer;
|
||||||
|
final bool hasSavedBond;
|
||||||
|
final List<int>? connectedTrainerAddr;
|
||||||
|
final int? lastFailure;
|
||||||
|
final dynamic raw;
|
||||||
|
|
||||||
|
String get statusLine =>
|
||||||
|
'Control: ${control.name}, Trainer: ${trainer.label}${lastFailure != null ? ', Last failure: $lastFailure' : ''}';
|
||||||
|
|
||||||
|
static CentralStatus fromCborBytes(List<int> bytes) {
|
||||||
|
final decoded = cbor.decode(bytes);
|
||||||
|
if (decoded is! Map) {
|
||||||
|
return CentralStatus(
|
||||||
|
control: ControlConnectionState.disconnected,
|
||||||
|
trainer: const TrainerStatus(state: TrainerConnectionState.idle),
|
||||||
|
hasSavedBond: false,
|
||||||
|
connectedTrainerAddr: null,
|
||||||
|
lastFailure: null,
|
||||||
|
raw: decoded,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final controlRaw = _readMapValue(decoded, [0, 'control']);
|
||||||
|
final trainerRaw = _readMapValue(decoded, [1, 'trainer']);
|
||||||
|
final hasSavedBondRaw = _readMapValue(decoded, [2, 'has_saved_bond']);
|
||||||
|
final connectedTrainerAddrRaw =
|
||||||
|
_readMapValue(decoded, [3, 'connected_trainer_addr']);
|
||||||
|
final lastFailureRaw = _readMapValue(decoded, [4, 'last_failure']);
|
||||||
|
|
||||||
|
return CentralStatus(
|
||||||
|
control: ControlConnectionState.fromRaw(controlRaw),
|
||||||
|
trainer: TrainerStatus.fromRaw(trainerRaw),
|
||||||
|
hasSavedBond: hasSavedBondRaw is bool ? hasSavedBondRaw : false,
|
||||||
|
connectedTrainerAddr: _toByteList(connectedTrainerAddrRaw),
|
||||||
|
lastFailure: lastFailureRaw is int ? lastFailureRaw : null,
|
||||||
|
raw: decoded,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic _readMapValue(Map<dynamic, dynamic> map, List<dynamic> keys) {
|
||||||
|
for (final key in keys) {
|
||||||
|
if (map.containsKey(key)) {
|
||||||
|
return map[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int>? _toByteList(dynamic value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value is List) {
|
||||||
|
return value.whereType<int>().toList(growable: false);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> parseMacToLittleEndianBytes(String macAddress) {
|
||||||
|
final compact = macAddress.replaceAll(':', '').replaceAll('-', '');
|
||||||
|
if (compact.length != 12) {
|
||||||
|
throw FormatException('Invalid MAC address format: $macAddress');
|
||||||
|
}
|
||||||
|
final bytes = <int>[];
|
||||||
|
for (int i = 0; i < compact.length; i += 2) {
|
||||||
|
bytes.add(int.parse(compact.substring(i, i + 2), radix: 16));
|
||||||
|
}
|
||||||
|
return bytes.reversed.toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
String formatMacAddressFromLittleEndian(List<int> bytes) {
|
||||||
|
if (bytes.length != 6) {
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
return bytes.reversed
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||||
|
.join(':');
|
||||||
|
}
|
||||||
1190
lib/pages/device_details_page.dart
Normal file
1190
lib/pages/device_details_page.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,17 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:abawo_bt_app/controller/bluetooth.dart';
|
import 'package:abawo_bt_app/controller/bluetooth.dart';
|
||||||
|
import 'package:abawo_bt_app/model/bluetooth_device_model.dart';
|
||||||
import 'package:abawo_bt_app/util/constants.dart';
|
import 'package:abawo_bt_app/util/constants.dart';
|
||||||
|
import 'package:anyhow/anyhow.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
|
||||||
import 'package:abawo_bt_app/widgets/device_listitem.dart';
|
import 'package:abawo_bt_app/widgets/device_listitem.dart';
|
||||||
import 'package:abawo_bt_app/widgets/scanning_animation.dart'; // Import the new animation widget
|
import 'package:abawo_bt_app/widgets/horizontal_scanning_animation.dart'; // Import the new horizontal animation
|
||||||
|
import 'package:abawo_bt_app/database/database.dart';
|
||||||
|
import 'package:drift/drift.dart' show Value;
|
||||||
|
|
||||||
const Duration _scanDuration = Duration(seconds: 10);
|
const Duration _scanDuration = Duration(seconds: 10);
|
||||||
|
|
||||||
@ -18,25 +24,20 @@ class ConnectDevicePage extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _ConnectDevicePageState extends ConsumerState<ConnectDevicePage>
|
class _ConnectDevicePageState extends ConsumerState<ConnectDevicePage>
|
||||||
with TickerProviderStateMixin {
|
with TickerProviderStateMixin {
|
||||||
// Use TickerProviderStateMixin for multiple controllers if needed later, good practice
|
// TickerProviderStateMixin is no longer needed as animations are self-contained or handled by StreamBuilder
|
||||||
bool _initialScanStarted = false;
|
int _retryScanCounter = 0; // Used to force animation reset
|
||||||
|
bool _initialScanTriggered = false; // Track if the first scan was requested
|
||||||
bool _showOnlyAbawoDevices = true; // State for filtering devices
|
bool _showOnlyAbawoDevices = true; // State for filtering devices
|
||||||
late AnimationController
|
|
||||||
_progressController; // Controller for scan duration progress
|
|
||||||
late AnimationController
|
|
||||||
_waveAnimationController; // Controller for wave animation
|
|
||||||
|
|
||||||
// Function to start scan safely after controller is ready
|
// Function to start scan safely after controller is ready
|
||||||
void _startScanIfNeeded(BluetoothController controller) {
|
void _startScanIfNeeded(BluetoothController controller) {
|
||||||
// Use WidgetsBinding to schedule the scan start after the build phase
|
// Use WidgetsBinding to schedule the scan start after the build phase
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!_initialScanStarted && mounted) {
|
// Start scan only if it hasn't been triggered yet and the widget is mounted
|
||||||
|
if (!_initialScanTriggered && mounted) {
|
||||||
controller.startScan(timeout: _scanDuration);
|
controller.startScan(timeout: _scanDuration);
|
||||||
_startScanProgressAnimation(); // Start scan duration progress animation
|
|
||||||
_startWaveAnimation(); // Start the wave animation
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_initialScanStarted = true;
|
_initialScanTriggered = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,64 +47,17 @@ class _ConnectDevicePageState extends ConsumerState<ConnectDevicePage>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Initialize scan progress controller
|
super.initState();
|
||||||
_progressController = AnimationController(
|
// No animation controllers needed here anymore
|
||||||
vsync: this,
|
|
||||||
duration: _scanDuration,
|
|
||||||
)..addListener(() {
|
|
||||||
// Trigger rebuild when animation value changes for the progress indicator
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize wave animation controller
|
|
||||||
_waveAnimationController = AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: const Duration(milliseconds: 1500), // New duration: 1.5 seconds
|
|
||||||
)..repeat(); // Make the wave animation repeat
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// Stop animations before disposing
|
// Dispose controllers if they existed (they don't anymore)
|
||||||
_progressController.stop();
|
|
||||||
_waveAnimationController.stop();
|
|
||||||
_progressController.dispose();
|
|
||||||
_waveAnimationController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper method to start/reset scan progress animation
|
// Removed _startScanProgressAnimation, _startWaveAnimation, _stopAnimations
|
||||||
void _startScanProgressAnimation() {
|
|
||||||
if (mounted) {
|
|
||||||
_progressController.reset();
|
|
||||||
_progressController.forward().whenCompleteOrCancel(() {
|
|
||||||
// Optional: Add logic if needed when scan progress completes or cancels
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper method to start the wave animation
|
|
||||||
void _startWaveAnimation() {
|
|
||||||
if (mounted && !_waveAnimationController.isAnimating) {
|
|
||||||
_waveAnimationController.reset();
|
|
||||||
_waveAnimationController.repeat();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper method to stop animations when scan finishes/cancels
|
|
||||||
void _stopAnimations() {
|
|
||||||
if (mounted) {
|
|
||||||
if (_progressController.isAnimating) {
|
|
||||||
_progressController.stop();
|
|
||||||
}
|
|
||||||
if (_waveAnimationController.isAnimating) {
|
|
||||||
_waveAnimationController.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -134,155 +88,264 @@ class _ConnectDevicePageState extends ConsumerState<ConnectDevicePage>
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Column(
|
||||||
child: Column(
|
// Use Column instead of Center(Column(...))
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(16.0), // Add padding around the title
|
||||||
|
child: Text(
|
||||||
'Available Devices',
|
'Available Devices',
|
||||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
),
|
||||||
// Use Consumer to react to bluetoothProvider changes
|
// Use Consumer to get the BluetoothController
|
||||||
Expanded(
|
Expanded(
|
||||||
// Allow the Consumer content to expand
|
// Allow the device list to take available space
|
||||||
child: Consumer(builder: (context, ref, child) {
|
child: Consumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
final btAsyncValue = ref.watch(bluetoothProvider);
|
final btAsyncValue = ref.watch(bluetoothProvider);
|
||||||
|
final connectedDevices =
|
||||||
|
ref.watch(nConnectedDevicesProvider).valueOrNull ??
|
||||||
|
const <ConnectedDevice>[];
|
||||||
|
final connectedDeviceAddresses = connectedDevices
|
||||||
|
.map((device) => device.deviceAddress)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
return btAsyncValue.when(
|
return btAsyncValue.when(
|
||||||
loading: () => const Center(
|
loading: () =>
|
||||||
child:
|
const Center(child: CircularProgressIndicator()),
|
||||||
CircularProgressIndicator()), // Center loading indicator
|
error: (err, stack) => Center(child: Text('Error: $err')),
|
||||||
error: (err, stack) => Center(
|
|
||||||
child: Text(
|
|
||||||
'Error loading Bluetooth: $err')), // Center error
|
|
||||||
data: (controller) {
|
data: (controller) {
|
||||||
// Start the initial scan once the controller is ready
|
// Trigger the initial scan if needed
|
||||||
// Start initial scan and animation
|
|
||||||
_startScanIfNeeded(controller);
|
_startScanIfNeeded(controller);
|
||||||
|
|
||||||
// Use StreamBuilder to watch the scanning state
|
// StreamBuilder for Scan Results (Device List)
|
||||||
return StreamBuilder<bool>(
|
return StreamBuilder<List<DiscoveredDevice>>(
|
||||||
stream: FlutterBluePlus.isScanning,
|
stream: controller.scanResultsStream,
|
||||||
initialData:
|
initialData: const [],
|
||||||
false, // Default to not scanning before check
|
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final isScanning = snapshot.data ?? false;
|
final results = snapshot.data ?? [];
|
||||||
|
|
||||||
if (isScanning && _initialScanStarted) {
|
|
||||||
_startWaveAnimation(); // Ensure wave animation is running
|
|
||||||
// Show the new scanning wave animation with the progress indicator value
|
|
||||||
return Center(
|
|
||||||
// Pass the wave animation controller. The progress value will be added
|
|
||||||
// to the ScanningWaveAnimation widget itself later.
|
|
||||||
child: ScanningWaveAnimation(
|
|
||||||
animation: _waveAnimationController,
|
|
||||||
progressValue: _progressController
|
|
||||||
.value, // Pass the scan progress
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (!_initialScanStarted) {
|
|
||||||
// Show placeholder or button to start initial scan if needed, or just empty space
|
|
||||||
return const SizedBox(
|
|
||||||
height: 50); // Placeholder before scan starts
|
|
||||||
} else {
|
|
||||||
// Scan finished, stop animations and show results
|
|
||||||
_stopAnimations();
|
|
||||||
final results = controller.scanResults;
|
|
||||||
// Filter results based on the toggle state
|
// Filter results based on the toggle state
|
||||||
final filteredResults = _showOnlyAbawoDevices
|
final filteredResults = _showOnlyAbawoDevices
|
||||||
? results
|
? results
|
||||||
.where((device) => device
|
.where((device) =>
|
||||||
.advertisementData.serviceUuids
|
device.serviceUuids.any(isAbawoDeviceGuid))
|
||||||
.contains(Guid(abawoServiceBtUUID)))
|
|
||||||
.toList()
|
.toList()
|
||||||
: results;
|
: results;
|
||||||
|
|
||||||
// Use Column + Expanded for ListView + Button layout
|
if (!_initialScanTriggered && filteredResults.isEmpty) {
|
||||||
return Column(
|
// Show a message or placeholder before the first scan starts or if no devices found initially
|
||||||
children: [
|
return const Center(
|
||||||
Expanded(
|
|
||||||
// Allow ListView to take available space
|
|
||||||
child: filteredResults
|
|
||||||
.isEmpty // Use filtered list check
|
|
||||||
? const Center(
|
|
||||||
child: Text(
|
child: Text(
|
||||||
'No devices found.')) // Center empty text
|
'Scanning for devices...')); // Or CircularProgressIndicator()
|
||||||
: ListView.builder(
|
}
|
||||||
itemCount: filteredResults
|
|
||||||
.length, // Use filtered list length
|
if (filteredResults.isEmpty && _initialScanTriggered) {
|
||||||
|
// Show 'No devices found' only after the initial scan was triggered
|
||||||
|
return const Center(child: Text('No devices found.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the list
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: filteredResults.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final device = filteredResults[
|
final device = filteredResults[index];
|
||||||
index]; // Use filtered list
|
final isAlreadyConnected =
|
||||||
final isAbawoDevice = device
|
connectedDeviceAddresses.contains(device.id);
|
||||||
.advertisementData.serviceUuids
|
final abawoDevice =
|
||||||
.contains(
|
device.serviceUuids.any(isAbawoDeviceGuid);
|
||||||
Guid(abawoServiceBtUUID));
|
final connectable = device.serviceUuids
|
||||||
final deviceName =
|
.any(isConnectableAbawoDeviceGuid);
|
||||||
device.device.advName.isEmpty
|
final deviceName = device.name.isEmpty
|
||||||
? 'Unknown Device'
|
? 'Unknown Device'
|
||||||
: device.device.advName;
|
: device.name;
|
||||||
// Use the custom DeviceListItem widget
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
// Wrap with InkWell for tap feedback
|
onTap: () async {
|
||||||
onTap: () {
|
if (isAlreadyConnected) {
|
||||||
if (!isAbawoDevice) {
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
// Show a snackbar for non-Abawo devices
|
const SnackBar(
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
.showSnackBar(const SnackBar(
|
|
||||||
content: Text(
|
content: Text(
|
||||||
'This app can only connect to abawo devices.')));
|
'This device is already connected in the app.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO: Implement connect logic
|
if (!abawoDevice) {
|
||||||
// controller.connectToDevice(device.device); // Pass the BluetoothDevice
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
// context.go('/control/${device.device.remoteId.str}');
|
const SnackBar(
|
||||||
print(
|
content: Text(
|
||||||
'Tapped on ${device.device.remoteId.str}');
|
'This app can only connect to abawo devices.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else if (!connectable) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'This device is not connectable with the app.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
final res = await controller.connect(device);
|
||||||
|
print('res: $res');
|
||||||
|
switch (res) {
|
||||||
|
case Ok():
|
||||||
|
// trigger pairing/permission prompt if needed
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
controller.readCharacteristic(
|
||||||
|
device.id,
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba4ffc2',
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba40000');
|
||||||
|
}
|
||||||
|
// Save to DB and navigate
|
||||||
|
final notifier = ref.read(
|
||||||
|
nConnectedDevicesProvider.notifier);
|
||||||
|
final name = device.name.isNotEmpty
|
||||||
|
? device.name
|
||||||
|
: 'Unknown Device';
|
||||||
|
final deviceCompanion =
|
||||||
|
ConnectedDevicesCompanion(
|
||||||
|
deviceName: Value(name),
|
||||||
|
deviceAddress: Value(device.id),
|
||||||
|
deviceType: Value(deviceTypeToString(
|
||||||
|
deviceTypeFromUuids(
|
||||||
|
device.serviceUuids))),
|
||||||
|
lastConnectedAt: Value(DateTime.now()),
|
||||||
|
);
|
||||||
|
final addResult = await notifier
|
||||||
|
.addConnectedDevice(deviceCompanion);
|
||||||
|
|
||||||
|
// Check if mounted before using context
|
||||||
|
if (!context.mounted) break;
|
||||||
|
|
||||||
|
if (addResult.isErr()) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Failed to save device: ${addResult.unwrapErr()}')),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
context.go('/device/${device.id}');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Err(:final v):
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Connection unsuccessful:\n${v.toString()}'),
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print('Tapped on ${device.id}');
|
||||||
},
|
},
|
||||||
child: DeviceListItem(
|
child: DeviceListItem(
|
||||||
deviceName: deviceName,
|
deviceName: deviceName,
|
||||||
deviceId:
|
deviceId: device.id,
|
||||||
device.device.remoteId.str,
|
type: deviceTypeFromUuids(device.serviceUuids),
|
||||||
isUnknownDevice:
|
|
||||||
device.device.advName.isEmpty,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} // End of itemBuilder
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
// Bottom section: Scanning Animation and Retry Button (visible only when scanning)
|
||||||
// Add padding around the button
|
Consumer(
|
||||||
|
// Use Consumer to get the controller for the retry button action
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final btController = ref
|
||||||
|
.watch(bluetoothProvider)
|
||||||
|
.asData
|
||||||
|
?.value; // Get controller safely
|
||||||
|
|
||||||
|
return StreamBuilder<bool>(
|
||||||
|
stream: btController?.isScanningStream ?? Stream<bool>.empty(),
|
||||||
|
initialData: false,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final isScanning = snapshot.data ?? false;
|
||||||
|
|
||||||
|
// Show bottom section only if scanning
|
||||||
|
if (!isScanning) {
|
||||||
|
// Show only the retry button when not scanning (optional, could be hidden)
|
||||||
|
// For now, let's keep the button always visible but disabled when not scannable.
|
||||||
|
// A better approach might be to hide the button when not scanning.
|
||||||
|
// Let's show the button but potentially disabled later if controller is null.
|
||||||
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: btController != null
|
||||||
// Retry scan by calling startScan on the controller
|
? () {
|
||||||
// Ensure _initialScanStarted is true so indicator shows
|
// Retry scan ONLY when NOT currently scanning
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_initialScanStarted = true;
|
_initialScanTriggered =
|
||||||
|
true; // Ensure state reflects scan attempt
|
||||||
|
_retryScanCounter++; // Increment key counter
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
controller.startScan(
|
btController.startScan(timeout: _scanDuration);
|
||||||
timeout: _scanDuration);
|
}
|
||||||
_startScanProgressAnimation(); // Restart scan progress animation
|
: null, // Disable if controller not ready
|
||||||
_startWaveAnimation(); // Ensure wave animation runs on retry
|
|
||||||
},
|
|
||||||
child: const Text('Retry Scan'),
|
child: const Text('Retry Scan'),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
// If scanning, show animation and button
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8.0, horizontal: 16.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.scaffoldBackgroundColor
|
||||||
|
.withValues(alpha: 0.9), // Slight overlay effect
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.1),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, -4), // Shadow upwards
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min, // Keep column compact
|
||||||
|
children: [
|
||||||
|
// Pass isScanning and the ValueKey
|
||||||
|
HorizontalScanningAnimation(
|
||||||
|
key: ValueKey(
|
||||||
|
_retryScanCounter), // Force state rebuild on counter change
|
||||||
|
isScanning: isScanning,
|
||||||
|
height: 40,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ElevatedButton(
|
||||||
|
// Button does nothing if pressed *while* scanning.
|
||||||
|
// It just indicates the status.
|
||||||
|
onPressed: null, // Disable button while scanning
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
disabledBackgroundColor: Theme.of(context)
|
||||||
|
.primaryColor
|
||||||
|
.withValues(alpha: 0.5), // Custom disabled color
|
||||||
|
disabledForegroundColor: Colors.white70,
|
||||||
|
),
|
||||||
|
child:
|
||||||
|
const Text('Scanning...'), // Just indicate status
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8), // Add some bottom padding
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
], // End of outer Column children
|
||||||
],
|
), // End of Scaffold
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
|
import 'package:abawo_bt_app/controller/bluetooth.dart';
|
||||||
|
import 'package:abawo_bt_app/database/database.dart';
|
||||||
|
import 'package:abawo_bt_app/model/bluetooth_device_model.dart';
|
||||||
|
import 'package:abawo_bt_app/widgets/device_listitem.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class HomePage extends StatelessWidget {
|
class HomePage extends StatelessWidget {
|
||||||
@ -53,10 +58,7 @@ class HomePage extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: const Center(
|
||||||
child: Text(
|
child: DevicesList(),
|
||||||
'No devices connected yet',
|
|
||||||
style: TextStyle(color: Colors.grey),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -73,3 +75,148 @@ class HomePage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DevicesList extends ConsumerStatefulWidget {
|
||||||
|
const DevicesList({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<DevicesList> createState() => _DevicesListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DevicesListState extends ConsumerState<DevicesList> {
|
||||||
|
String? _connectingDeviceId; // ID of device currently being connected
|
||||||
|
|
||||||
|
Future<void> _removeDevice(ConnectedDevice device) async {
|
||||||
|
final shouldRemove = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: const Text('Remove device?'),
|
||||||
|
content:
|
||||||
|
Text('Do you want to remove ${device.deviceName} from the app?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||||
|
child: const Text('Remove'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldRemove != true || !mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await ref
|
||||||
|
.read(nConnectedDevicesProvider.notifier)
|
||||||
|
.deleteConnectedDevice(device.id);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isErr()) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Failed to remove device: ${result.unwrapErr()}')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_connectingDeviceId == device.deviceAddress) {
|
||||||
|
setState(() {
|
||||||
|
_connectingDeviceId = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('${device.deviceName} removed from the app.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final asyncDevices = ref.watch(nConnectedDevicesProvider);
|
||||||
|
|
||||||
|
return asyncDevices.when(
|
||||||
|
loading: () => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
error: (error, stackTrace) => Center(
|
||||||
|
child: Text(
|
||||||
|
'Error loading devices: ${error.toString()}',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
data: (devices) {
|
||||||
|
if (devices.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Text(
|
||||||
|
'No devices connected yet',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: devices.length,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final device = devices[index];
|
||||||
|
return InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
if (_connectingDeviceId != null) return;
|
||||||
|
setState(() {
|
||||||
|
_connectingDeviceId = device.deviceAddress;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final controller = await ref.read(bluetoothProvider.future);
|
||||||
|
final result = await controller.connectById(
|
||||||
|
device.deviceAddress,
|
||||||
|
timeout: const Duration(seconds: 10),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.isOk()) {
|
||||||
|
context.go('/device/${device.deviceAddress}');
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Connection failed. Is the device turned on and in range?'),
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Error: ${e.toString()}')),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setState(() {
|
||||||
|
_connectingDeviceId = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: DeviceListItem(
|
||||||
|
deviceName: device.deviceName,
|
||||||
|
deviceId: device.deviceAddress,
|
||||||
|
type: deviceTypeFromString(device.deviceType),
|
||||||
|
isConnecting: device.deviceAddress == _connectingDeviceId,
|
||||||
|
trailing: IconButton(
|
||||||
|
tooltip: 'Remove device',
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
onPressed: () => _removeDevice(device),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
137
lib/service/dfu_protocol.dart
Normal file
137
lib/service/dfu_protocol.dart
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:abawo_bt_app/model/shifter_types.dart';
|
||||||
|
|
||||||
|
const int _startPayloadLength = 11;
|
||||||
|
|
||||||
|
class DfuStartPayload {
|
||||||
|
const DfuStartPayload({
|
||||||
|
required this.totalLength,
|
||||||
|
required this.imageCrc32,
|
||||||
|
required this.sessionId,
|
||||||
|
required this.flags,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int totalLength;
|
||||||
|
final int imageCrc32;
|
||||||
|
final int sessionId;
|
||||||
|
final int flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DfuDataFrame {
|
||||||
|
const DfuDataFrame({
|
||||||
|
required this.sequence,
|
||||||
|
required this.offset,
|
||||||
|
required this.payloadLength,
|
||||||
|
required this.bytes,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int sequence;
|
||||||
|
final int offset;
|
||||||
|
final int payloadLength;
|
||||||
|
final Uint8List bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DfuProtocol {
|
||||||
|
const DfuProtocol._();
|
||||||
|
|
||||||
|
static Uint8List encodeStartPayload(DfuStartPayload payload) {
|
||||||
|
final data = ByteData(_startPayloadLength);
|
||||||
|
data.setUint8(0, universalShifterDfuOpcodeStart);
|
||||||
|
data.setUint32(1, payload.totalLength, Endian.little);
|
||||||
|
data.setUint32(5, payload.imageCrc32, Endian.little);
|
||||||
|
data.setUint8(9, payload.sessionId);
|
||||||
|
data.setUint8(10, payload.flags);
|
||||||
|
return data.buffer.asUint8List();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Uint8List encodeFinishPayload() {
|
||||||
|
return Uint8List.fromList([universalShifterDfuOpcodeFinish]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Uint8List encodeAbortPayload() {
|
||||||
|
return Uint8List.fromList([universalShifterDfuOpcodeAbort]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<DfuDataFrame> buildDataFrames(
|
||||||
|
List<int> imageBytes, {
|
||||||
|
int startSequence = 0,
|
||||||
|
}) {
|
||||||
|
final frames = <DfuDataFrame>[];
|
||||||
|
var seq = _asU8(startSequence);
|
||||||
|
var offset = 0;
|
||||||
|
while (offset < imageBytes.length) {
|
||||||
|
final remaining = imageBytes.length - offset;
|
||||||
|
final chunkLength = remaining < universalShifterDfuFramePayloadSizeBytes
|
||||||
|
? remaining
|
||||||
|
: universalShifterDfuFramePayloadSizeBytes;
|
||||||
|
|
||||||
|
final frame = Uint8List(universalShifterDfuFrameSizeBytes);
|
||||||
|
frame[0] = seq;
|
||||||
|
frame.setRange(1, 1 + chunkLength, imageBytes, offset);
|
||||||
|
|
||||||
|
frames.add(
|
||||||
|
DfuDataFrame(
|
||||||
|
sequence: seq,
|
||||||
|
offset: offset,
|
||||||
|
payloadLength: chunkLength,
|
||||||
|
bytes: frame,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
offset += chunkLength;
|
||||||
|
seq = nextSequence(seq);
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int nextSequence(int sequence) {
|
||||||
|
return _asU8(sequence + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int rewindSequenceFromAck(int acknowledgedSequence) {
|
||||||
|
return nextSequence(acknowledgedSequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sequenceDistance(int from, int to) {
|
||||||
|
return _asU8(to - from);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int parseAckPayload(List<int> payload) {
|
||||||
|
if (payload.length != 1) {
|
||||||
|
throw const FormatException('ACK payload must be exactly 1 byte.');
|
||||||
|
}
|
||||||
|
return _asU8(payload.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const int crc32Initial = 0xFFFFFFFF;
|
||||||
|
static const int _crc32PolynomialReflected = 0xEDB88320;
|
||||||
|
|
||||||
|
static int crc32Update(int crc, List<int> bytes) {
|
||||||
|
var next = crc & 0xFFFFFFFF;
|
||||||
|
for (final byte in bytes) {
|
||||||
|
next ^= byte;
|
||||||
|
for (var bit = 0; bit < 8; bit++) {
|
||||||
|
if ((next & 0x1) != 0) {
|
||||||
|
next = (next >> 1) ^ _crc32PolynomialReflected;
|
||||||
|
} else {
|
||||||
|
next >>= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next & 0xFFFFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int crc32Finalize(int crc) {
|
||||||
|
return (crc ^ 0xFFFFFFFF) & 0xFFFFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int crc32(List<int> bytes) {
|
||||||
|
return crc32Finalize(crc32Update(crc32Initial, bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _asU8(int value) {
|
||||||
|
return value & 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
154
lib/service/firmware_file_selection_service.dart
Normal file
154
lib/service/firmware_file_selection_service.dart
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:abawo_bt_app/model/firmware_file_selection.dart';
|
||||||
|
import 'package:abawo_bt_app/model/shifter_types.dart';
|
||||||
|
import 'package:abawo_bt_app/service/dfu_protocol.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
|
||||||
|
typedef SessionIdGenerator = int Function();
|
||||||
|
|
||||||
|
class FirmwarePickerSelection {
|
||||||
|
const FirmwarePickerSelection({
|
||||||
|
required this.fileName,
|
||||||
|
required this.fileBytes,
|
||||||
|
this.filePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String fileName;
|
||||||
|
final Uint8List fileBytes;
|
||||||
|
final String? filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface class FirmwareFilePicker {
|
||||||
|
Future<FirmwarePickerSelection?> pickFirmwareFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocalFirmwareFilePicker implements FirmwareFilePicker {
|
||||||
|
@override
|
||||||
|
Future<FirmwarePickerSelection?> pickFirmwareFile() async {
|
||||||
|
final pickResult = await FilePicker.platform.pickFiles(
|
||||||
|
allowMultiple: false,
|
||||||
|
withData: true,
|
||||||
|
type: FileType.custom,
|
||||||
|
allowedExtensions: const ['bin'],
|
||||||
|
);
|
||||||
|
if (pickResult == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (pickResult.files.isEmpty) {
|
||||||
|
return FirmwarePickerSelection(
|
||||||
|
fileName: '',
|
||||||
|
fileBytes: Uint8List(0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final selected = pickResult.files.first;
|
||||||
|
final bytes = selected.bytes ?? await _readFromPath(selected.path);
|
||||||
|
|
||||||
|
return FirmwarePickerSelection(
|
||||||
|
fileName: selected.name,
|
||||||
|
filePath: selected.path,
|
||||||
|
fileBytes: bytes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Uint8List> _readFromPath(String? path) async {
|
||||||
|
if (path == null || path.trim().isEmpty) {
|
||||||
|
throw const FileSystemException(
|
||||||
|
'Selected file did not contain readable bytes or a valid path.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final file = File(path);
|
||||||
|
return file.readAsBytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FirmwareFileSelectionService {
|
||||||
|
FirmwareFileSelectionService({
|
||||||
|
required FirmwareFilePicker filePicker,
|
||||||
|
SessionIdGenerator? sessionIdGenerator,
|
||||||
|
}) : _filePicker = filePicker,
|
||||||
|
_sessionIdGenerator = sessionIdGenerator ?? _randomSessionId;
|
||||||
|
|
||||||
|
final FirmwareFilePicker _filePicker;
|
||||||
|
final SessionIdGenerator _sessionIdGenerator;
|
||||||
|
|
||||||
|
Future<FirmwareFileSelectionResult> selectAndPrepareDfuV1() async {
|
||||||
|
final FirmwarePickerSelection? selection;
|
||||||
|
try {
|
||||||
|
selection = await _filePicker.pickFirmwareFile();
|
||||||
|
} catch (error) {
|
||||||
|
return FirmwareFileSelectionResult.failed(
|
||||||
|
FirmwareSelectionFailure(
|
||||||
|
reason: FirmwareSelectionFailureReason.readFailed,
|
||||||
|
message: 'Could not read selected firmware file: $error',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selection == null) {
|
||||||
|
return FirmwareFileSelectionResult.failed(
|
||||||
|
const FirmwareSelectionFailure(
|
||||||
|
reason: FirmwareSelectionFailureReason.canceled,
|
||||||
|
message: 'Firmware selection canceled.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final fileName = selection.fileName.trim();
|
||||||
|
if (fileName.isEmpty) {
|
||||||
|
return FirmwareFileSelectionResult.failed(
|
||||||
|
const FirmwareSelectionFailure(
|
||||||
|
reason: FirmwareSelectionFailureReason.malformedSelection,
|
||||||
|
message: 'Selected firmware file is missing a valid name.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_hasBinExtension(fileName)) {
|
||||||
|
return FirmwareFileSelectionResult.failed(
|
||||||
|
FirmwareSelectionFailure(
|
||||||
|
reason: FirmwareSelectionFailureReason.unsupportedExtension,
|
||||||
|
message:
|
||||||
|
'Unsupported firmware file "$fileName". Please select a .bin file.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selection.fileBytes.isEmpty) {
|
||||||
|
return FirmwareFileSelectionResult.failed(
|
||||||
|
FirmwareSelectionFailure(
|
||||||
|
reason: FirmwareSelectionFailureReason.emptyFile,
|
||||||
|
message:
|
||||||
|
'Selected firmware file "$fileName" is empty. Choose a non-empty .bin file.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final metadata = DfuV1FirmwareMetadata(
|
||||||
|
totalLength: selection.fileBytes.length,
|
||||||
|
crc32: DfuProtocol.crc32(selection.fileBytes),
|
||||||
|
sessionId: _sessionIdGenerator() & 0xFF,
|
||||||
|
flags: universalShifterDfuFlagNone,
|
||||||
|
);
|
||||||
|
|
||||||
|
return FirmwareFileSelectionResult.success(
|
||||||
|
DfuV1PreparedFirmware(
|
||||||
|
fileName: fileName,
|
||||||
|
filePath: selection.filePath,
|
||||||
|
fileBytes: selection.fileBytes,
|
||||||
|
metadata: metadata,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasBinExtension(String fileName) {
|
||||||
|
return fileName.toLowerCase().endsWith('.bin');
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _randomSessionId() {
|
||||||
|
return Random.secure().nextInt(256);
|
||||||
|
}
|
||||||
|
}
|
||||||
690
lib/service/firmware_update_service.dart
Normal file
690
lib/service/firmware_update_service.dart
Normal file
@ -0,0 +1,690 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:abawo_bt_app/controller/bluetooth.dart';
|
||||||
|
import 'package:abawo_bt_app/model/shifter_types.dart';
|
||||||
|
import 'package:abawo_bt_app/service/dfu_protocol.dart';
|
||||||
|
import 'package:abawo_bt_app/service/shifter_service.dart';
|
||||||
|
import 'package:anyhow/anyhow.dart';
|
||||||
|
|
||||||
|
const int _initialAckSequence = 0xFF;
|
||||||
|
|
||||||
|
class FirmwareUpdateService {
|
||||||
|
FirmwareUpdateService({
|
||||||
|
required FirmwareUpdateTransport transport,
|
||||||
|
this.defaultWindowSize = 8,
|
||||||
|
this.maxNoProgressRetries = 5,
|
||||||
|
this.defaultAckTimeout = const Duration(milliseconds: 800),
|
||||||
|
this.defaultPostFinishResetTimeout = const Duration(seconds: 8),
|
||||||
|
this.defaultReconnectTimeout = const Duration(seconds: 12),
|
||||||
|
this.defaultVerificationTimeout = const Duration(seconds: 5),
|
||||||
|
}) : _transport = transport;
|
||||||
|
|
||||||
|
final FirmwareUpdateTransport _transport;
|
||||||
|
final int defaultWindowSize;
|
||||||
|
final int maxNoProgressRetries;
|
||||||
|
final Duration defaultAckTimeout;
|
||||||
|
final Duration defaultPostFinishResetTimeout;
|
||||||
|
final Duration defaultReconnectTimeout;
|
||||||
|
final Duration defaultVerificationTimeout;
|
||||||
|
|
||||||
|
final StreamController<DfuUpdateProgress> _progressController =
|
||||||
|
StreamController<DfuUpdateProgress>.broadcast();
|
||||||
|
|
||||||
|
DfuUpdateProgress _currentProgress = const DfuUpdateProgress(
|
||||||
|
state: DfuUpdateState.idle,
|
||||||
|
totalBytes: 0,
|
||||||
|
sentBytes: 0,
|
||||||
|
lastAckedSequence: _initialAckSequence,
|
||||||
|
sessionId: 0,
|
||||||
|
flags: DfuUpdateFlags(),
|
||||||
|
);
|
||||||
|
|
||||||
|
StreamSubscription<List<int>>? _ackSubscription;
|
||||||
|
Completer<void>? _ackSignal;
|
||||||
|
Completer<void>? _cancelSignal;
|
||||||
|
int _ackEventCount = 0;
|
||||||
|
String? _ackStreamError;
|
||||||
|
bool _isRunning = false;
|
||||||
|
bool _cancelRequested = false;
|
||||||
|
int _latestAckSequence = _initialAckSequence;
|
||||||
|
int _ackedFrames = 0;
|
||||||
|
int _totalFrames = 0;
|
||||||
|
int _totalBytes = 0;
|
||||||
|
|
||||||
|
Stream<DfuUpdateProgress> get progressStream => _progressController.stream;
|
||||||
|
|
||||||
|
DfuUpdateProgress get currentProgress => _currentProgress;
|
||||||
|
|
||||||
|
bool get isUpdating => _isRunning;
|
||||||
|
|
||||||
|
Future<Result<void>> startUpdate({
|
||||||
|
required List<int> imageBytes,
|
||||||
|
required int sessionId,
|
||||||
|
DfuUpdateFlags flags = const DfuUpdateFlags(),
|
||||||
|
int requestedMtu = universalShifterDfuPreferredMtu,
|
||||||
|
int? windowSize,
|
||||||
|
Duration? ackTimeout,
|
||||||
|
int? noProgressRetries,
|
||||||
|
Duration? postFinishResetTimeout,
|
||||||
|
Duration? reconnectTimeout,
|
||||||
|
Duration? verificationTimeout,
|
||||||
|
}) async {
|
||||||
|
if (_isRunning) {
|
||||||
|
return bail(
|
||||||
|
'Firmware update is already running. Cancel or wait for completion before starting a new upload.');
|
||||||
|
}
|
||||||
|
if (imageBytes.isEmpty) {
|
||||||
|
return bail(
|
||||||
|
'Firmware image is empty. Select a valid .bin file and retry.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final effectiveWindowSize = windowSize ?? defaultWindowSize;
|
||||||
|
final effectiveAckTimeout = ackTimeout ?? defaultAckTimeout;
|
||||||
|
final effectiveNoProgressRetries =
|
||||||
|
noProgressRetries ?? maxNoProgressRetries;
|
||||||
|
final effectivePostFinishResetTimeout =
|
||||||
|
postFinishResetTimeout ?? defaultPostFinishResetTimeout;
|
||||||
|
final effectiveReconnectTimeout =
|
||||||
|
reconnectTimeout ?? defaultReconnectTimeout;
|
||||||
|
final effectiveVerificationTimeout =
|
||||||
|
verificationTimeout ?? defaultVerificationTimeout;
|
||||||
|
|
||||||
|
if (effectiveWindowSize <= 0) {
|
||||||
|
return bail(
|
||||||
|
'DFU window size must be at least 1 frame. Got $effectiveWindowSize.');
|
||||||
|
}
|
||||||
|
if (effectiveNoProgressRetries < 0) {
|
||||||
|
return bail(
|
||||||
|
'No-progress retry limit cannot be negative. Got $effectiveNoProgressRetries.');
|
||||||
|
}
|
||||||
|
|
||||||
|
_isRunning = true;
|
||||||
|
_cancelRequested = false;
|
||||||
|
_cancelSignal = Completer<void>();
|
||||||
|
_ackSignal = null;
|
||||||
|
_ackEventCount = 0;
|
||||||
|
_ackStreamError = null;
|
||||||
|
_latestAckSequence = _initialAckSequence;
|
||||||
|
_ackedFrames = 0;
|
||||||
|
_totalFrames =
|
||||||
|
(imageBytes.length + universalShifterDfuFramePayloadSizeBytes - 1) ~/
|
||||||
|
universalShifterDfuFramePayloadSizeBytes;
|
||||||
|
_totalBytes = imageBytes.length;
|
||||||
|
|
||||||
|
final normalizedSessionId = sessionId & 0xFF;
|
||||||
|
final crc32 = DfuProtocol.crc32(imageBytes);
|
||||||
|
final frames = DfuProtocol.buildDataFrames(imageBytes);
|
||||||
|
var shouldAbortForCleanup = false;
|
||||||
|
|
||||||
|
_emitProgress(
|
||||||
|
state: DfuUpdateState.starting,
|
||||||
|
totalBytes: imageBytes.length,
|
||||||
|
sentBytes: 0,
|
||||||
|
lastAckedSequence: _initialAckSequence,
|
||||||
|
sessionId: normalizedSessionId,
|
||||||
|
flags: flags,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final preflightResult = await _transport.runPreflight(
|
||||||
|
requestedMtu: requestedMtu,
|
||||||
|
);
|
||||||
|
if (preflightResult.isErr()) {
|
||||||
|
throw _DfuFailure(
|
||||||
|
'DFU preflight check failed due to transport error: ${preflightResult.unwrapErr()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final preflight = preflightResult.unwrap();
|
||||||
|
if (!preflight.canStart) {
|
||||||
|
throw _DfuFailure(
|
||||||
|
preflight.message ??
|
||||||
|
'DFU preflight failed. Ensure button connection and MTU are ready, then retry.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _ackSubscription?.cancel();
|
||||||
|
_ackSubscription = _transport.subscribeToAck().listen(
|
||||||
|
_handleAckPayload,
|
||||||
|
onError: (Object error) {
|
||||||
|
_ackStreamError =
|
||||||
|
'ACK indication stream failed: $error. Reconnect and retry the update.';
|
||||||
|
_signalAckWaiters();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
_emitProgress(state: DfuUpdateState.waitingForAck);
|
||||||
|
final startEventCount = _ackEventCount;
|
||||||
|
final startWriteResult = await _transport.writeControl(
|
||||||
|
DfuProtocol.encodeStartPayload(
|
||||||
|
DfuStartPayload(
|
||||||
|
totalLength: imageBytes.length,
|
||||||
|
imageCrc32: crc32,
|
||||||
|
sessionId: normalizedSessionId,
|
||||||
|
flags: flags.rawValue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (startWriteResult.isErr()) {
|
||||||
|
throw _DfuFailure(
|
||||||
|
'Failed to send DFU START command: ${startWriteResult.unwrapErr()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
shouldAbortForCleanup = true;
|
||||||
|
|
||||||
|
final initialAck = await _waitForInitialAck(
|
||||||
|
afterEventCount: startEventCount,
|
||||||
|
timeout: effectiveAckTimeout,
|
||||||
|
);
|
||||||
|
if (initialAck != _initialAckSequence) {
|
||||||
|
throw _DfuFailure(
|
||||||
|
'Device did not acknowledge START correctly (expected ACK 0xFF, got 0x${initialAck.toRadixString(16).padLeft(2, '0').toUpperCase()}). Send ABORT, reconnect if needed, and retry.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_emitProgress(state: DfuUpdateState.transferring);
|
||||||
|
|
||||||
|
var nextFrameIndex = 0;
|
||||||
|
var retriesWithoutProgress = 0;
|
||||||
|
|
||||||
|
while (_ackedFrames < _totalFrames) {
|
||||||
|
_throwIfCancelled();
|
||||||
|
_throwIfAckStreamErrored();
|
||||||
|
|
||||||
|
final ackedBeforeWindow = _ackedFrames;
|
||||||
|
final endExclusive =
|
||||||
|
(nextFrameIndex + effectiveWindowSize).clamp(0, frames.length);
|
||||||
|
|
||||||
|
for (var frameIndex = nextFrameIndex;
|
||||||
|
frameIndex < endExclusive;
|
||||||
|
frameIndex++) {
|
||||||
|
_throwIfCancelled();
|
||||||
|
final writeResult =
|
||||||
|
await _transport.writeDataFrame(frames[frameIndex].bytes);
|
||||||
|
if (writeResult.isErr()) {
|
||||||
|
throw _DfuFailure(
|
||||||
|
'Failed sending DFU data frame #$frameIndex (seq 0x${frames[frameIndex].sequence.toRadixString(16).padLeft(2, '0').toUpperCase()}): ${writeResult.unwrapErr()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextFrameIndex = endExclusive;
|
||||||
|
|
||||||
|
if (_ackedFrames > ackedBeforeWindow) {
|
||||||
|
retriesWithoutProgress = 0;
|
||||||
|
nextFrameIndex = _ackedFrames;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final gotProgress = await _waitForAckProgress(
|
||||||
|
ackedFramesBeforeWait: ackedBeforeWindow,
|
||||||
|
timeout: effectiveAckTimeout,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (gotProgress) {
|
||||||
|
retriesWithoutProgress = 0;
|
||||||
|
nextFrameIndex = _ackedFrames;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
retriesWithoutProgress += 1;
|
||||||
|
if (retriesWithoutProgress > effectiveNoProgressRetries) {
|
||||||
|
throw _DfuFailure(
|
||||||
|
'Upload stalled: no ACK progress after $retriesWithoutProgress retries (last ACK 0x${_latestAckSequence.toRadixString(16).padLeft(2, '0').toUpperCase()}). Check BLE signal quality and retry.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextFrameIndex = _ackedFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
_emitProgress(
|
||||||
|
state: DfuUpdateState.finishing, sentBytes: imageBytes.length);
|
||||||
|
final finishResult =
|
||||||
|
await _transport.writeControl(DfuProtocol.encodeFinishPayload());
|
||||||
|
if (finishResult.isErr()) {
|
||||||
|
throw _DfuFailure(
|
||||||
|
'Failed to send DFU FINISH command: ${finishResult.unwrapErr()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _ackSubscription?.cancel();
|
||||||
|
_ackSubscription = null;
|
||||||
|
|
||||||
|
final resetDisconnectResult =
|
||||||
|
await _transport.waitForExpectedResetDisconnect(
|
||||||
|
timeout: effectivePostFinishResetTimeout,
|
||||||
|
);
|
||||||
|
if (resetDisconnectResult.isErr()) {
|
||||||
|
throw _DfuFailure(
|
||||||
|
'Device did not perform the expected post-FINISH reset disconnect: ${resetDisconnectResult.unwrapErr()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final reconnectResult = await _transport.reconnectForVerification(
|
||||||
|
timeout: effectiveReconnectTimeout,
|
||||||
|
);
|
||||||
|
if (reconnectResult.isErr()) {
|
||||||
|
throw _DfuFailure(
|
||||||
|
'Device did not reconnect after DFU reset: ${reconnectResult.unwrapErr()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final verificationResult = await _transport.verifyDeviceReachable(
|
||||||
|
timeout: effectiveVerificationTimeout,
|
||||||
|
);
|
||||||
|
if (verificationResult.isErr()) {
|
||||||
|
throw _DfuFailure(
|
||||||
|
'Device reconnected but post-update verification failed: ${verificationResult.unwrapErr()} '
|
||||||
|
'Firmware version cannot be compared yet because the device does not expose a version characteristic.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldAbortForCleanup = false;
|
||||||
|
_emitProgress(
|
||||||
|
state: DfuUpdateState.completed, sentBytes: imageBytes.length);
|
||||||
|
return Ok(null);
|
||||||
|
} on _DfuCancelled {
|
||||||
|
if (shouldAbortForCleanup) {
|
||||||
|
await _sendAbortForCleanup();
|
||||||
|
}
|
||||||
|
_emitProgress(state: DfuUpdateState.aborted);
|
||||||
|
return bail('Firmware update canceled by user.');
|
||||||
|
} on _DfuFailure catch (failure) {
|
||||||
|
if (shouldAbortForCleanup) {
|
||||||
|
await _sendAbortForCleanup();
|
||||||
|
}
|
||||||
|
_emitProgress(
|
||||||
|
state: DfuUpdateState.failed, errorMessage: failure.message);
|
||||||
|
return bail(failure.message);
|
||||||
|
} catch (error) {
|
||||||
|
if (shouldAbortForCleanup) {
|
||||||
|
await _sendAbortForCleanup();
|
||||||
|
}
|
||||||
|
final message =
|
||||||
|
'Firmware update failed unexpectedly: $error. Reconnect to the button and retry.';
|
||||||
|
_emitProgress(state: DfuUpdateState.failed, errorMessage: message);
|
||||||
|
return bail(message);
|
||||||
|
} finally {
|
||||||
|
await _ackSubscription?.cancel();
|
||||||
|
_ackSubscription = null;
|
||||||
|
_isRunning = false;
|
||||||
|
_cancelRequested = false;
|
||||||
|
_cancelSignal = null;
|
||||||
|
_ackSignal = null;
|
||||||
|
_ackEventCount = 0;
|
||||||
|
_ackStreamError = null;
|
||||||
|
_latestAckSequence = _currentProgress.lastAckedSequence;
|
||||||
|
_ackedFrames = 0;
|
||||||
|
_totalFrames = 0;
|
||||||
|
_totalBytes = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cancelUpdate() async {
|
||||||
|
if (!_isRunning || _cancelRequested) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_cancelRequested = true;
|
||||||
|
_cancelSignal?.complete();
|
||||||
|
_signalAckWaiters();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await cancelUpdate();
|
||||||
|
await _ackSubscription?.cancel();
|
||||||
|
_ackSubscription = null;
|
||||||
|
await _progressController.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleAckPayload(List<int> payload) {
|
||||||
|
try {
|
||||||
|
final sequence = DfuProtocol.parseAckPayload(payload);
|
||||||
|
final previousAck = _latestAckSequence;
|
||||||
|
_latestAckSequence = sequence;
|
||||||
|
|
||||||
|
if (_totalFrames > 0 &&
|
||||||
|
_currentProgress.state == DfuUpdateState.transferring) {
|
||||||
|
final delta = DfuProtocol.sequenceDistance(previousAck, sequence);
|
||||||
|
if (delta > 0) {
|
||||||
|
_ackedFrames = (_ackedFrames + delta).clamp(0, _totalFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
_emitProgress(
|
||||||
|
lastAckedSequence: sequence,
|
||||||
|
sentBytes:
|
||||||
|
_ackedBytesFromFrames(_ackedFrames, _totalFrames, _totalBytes),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_emitProgress(lastAckedSequence: sequence);
|
||||||
|
}
|
||||||
|
} on FormatException catch (error) {
|
||||||
|
_ackStreamError =
|
||||||
|
'Received malformed ACK indication: $error. Reconnect and retry.';
|
||||||
|
} finally {
|
||||||
|
_ackEventCount += 1;
|
||||||
|
_signalAckWaiters();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _emitProgress({
|
||||||
|
DfuUpdateState? state,
|
||||||
|
int? totalBytes,
|
||||||
|
int? sentBytes,
|
||||||
|
int? lastAckedSequence,
|
||||||
|
int? sessionId,
|
||||||
|
DfuUpdateFlags? flags,
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
final next = DfuUpdateProgress(
|
||||||
|
state: state ?? _currentProgress.state,
|
||||||
|
totalBytes: totalBytes ?? _currentProgress.totalBytes,
|
||||||
|
sentBytes: sentBytes ?? _currentProgress.sentBytes,
|
||||||
|
lastAckedSequence:
|
||||||
|
lastAckedSequence ?? _currentProgress.lastAckedSequence,
|
||||||
|
sessionId: sessionId ?? _currentProgress.sessionId,
|
||||||
|
flags: flags ?? _currentProgress.flags,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
);
|
||||||
|
_currentProgress = next;
|
||||||
|
_progressController.add(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> _waitForInitialAck({
|
||||||
|
required int afterEventCount,
|
||||||
|
required Duration timeout,
|
||||||
|
}) async {
|
||||||
|
final deadline = DateTime.now().add(timeout);
|
||||||
|
var observedEvents = afterEventCount;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
_throwIfCancelled();
|
||||||
|
_throwIfAckStreamErrored();
|
||||||
|
final remaining = deadline.difference(DateTime.now());
|
||||||
|
if (remaining <= Duration.zero) {
|
||||||
|
throw _DfuFailure(
|
||||||
|
'Timed out waiting for initial DFU ACK after START. Ensure indications are enabled and retry.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final gotEvent = await _waitForNextAckEvent(
|
||||||
|
afterEventCount: observedEvents,
|
||||||
|
timeout: remaining,
|
||||||
|
);
|
||||||
|
if (!gotEvent) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
observedEvents = _ackEventCount;
|
||||||
|
return _latestAckSequence;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _waitForAckProgress({
|
||||||
|
required int ackedFramesBeforeWait,
|
||||||
|
required Duration timeout,
|
||||||
|
}) async {
|
||||||
|
final deadline = DateTime.now().add(timeout);
|
||||||
|
var observedEvents = _ackEventCount;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
_throwIfCancelled();
|
||||||
|
_throwIfAckStreamErrored();
|
||||||
|
|
||||||
|
if (_ackedFrames > ackedFramesBeforeWait) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final remaining = deadline.difference(DateTime.now());
|
||||||
|
if (remaining <= Duration.zero) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final gotEvent = await _waitForNextAckEvent(
|
||||||
|
afterEventCount: observedEvents,
|
||||||
|
timeout: remaining,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!gotEvent) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
observedEvents = _ackEventCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _waitForNextAckEvent({
|
||||||
|
required int afterEventCount,
|
||||||
|
required Duration timeout,
|
||||||
|
}) async {
|
||||||
|
if (_ackEventCount > afterEventCount) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ackSignal ??= Completer<void>();
|
||||||
|
final signal = _ackSignal!;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Future.any<void>([
|
||||||
|
signal.future,
|
||||||
|
_cancelSignal?.future ?? Future<void>.value(),
|
||||||
|
]).timeout(timeout);
|
||||||
|
} on TimeoutException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identical(_ackSignal, signal)) {
|
||||||
|
_ackSignal = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_throwIfCancelled();
|
||||||
|
_throwIfAckStreamErrored();
|
||||||
|
return _ackEventCount > afterEventCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _throwIfCancelled() {
|
||||||
|
if (_cancelRequested) {
|
||||||
|
throw const _DfuCancelled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _throwIfAckStreamErrored() {
|
||||||
|
final error = _ackStreamError;
|
||||||
|
if (error != null) {
|
||||||
|
throw _DfuFailure(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _sendAbortForCleanup() async {
|
||||||
|
final result =
|
||||||
|
await _transport.writeControl(DfuProtocol.encodeAbortPayload());
|
||||||
|
if (result.isErr()) {
|
||||||
|
final cleanupMessage =
|
||||||
|
'Could not send DFU ABORT during cleanup: ${result.unwrapErr()}';
|
||||||
|
if (_currentProgress.state == DfuUpdateState.failed &&
|
||||||
|
_currentProgress.errorMessage != null) {
|
||||||
|
_emitProgress(
|
||||||
|
errorMessage: '${_currentProgress.errorMessage} $cleanupMessage',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _signalAckWaiters() {
|
||||||
|
final signal = _ackSignal;
|
||||||
|
if (signal != null && !signal.isCompleted) {
|
||||||
|
signal.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int _ackedBytesFromFrames(int ackedFrames, int totalFrames, int totalBytes) {
|
||||||
|
if (totalFrames == 0 || ackedFrames <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (ackedFrames >= totalFrames) {
|
||||||
|
return totalBytes;
|
||||||
|
}
|
||||||
|
return ackedFrames * universalShifterDfuFramePayloadSizeBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface class FirmwareUpdateTransport {
|
||||||
|
Future<Result<DfuPreflightResult>> runPreflight({required int requestedMtu});
|
||||||
|
|
||||||
|
Stream<List<int>> subscribeToAck();
|
||||||
|
|
||||||
|
Future<Result<void>> writeControl(List<int> payload);
|
||||||
|
|
||||||
|
Future<Result<void>> writeDataFrame(List<int> frame);
|
||||||
|
|
||||||
|
Future<Result<void>> waitForExpectedResetDisconnect({
|
||||||
|
required Duration timeout,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Result<void>> reconnectForVerification({
|
||||||
|
required Duration timeout,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Verifies that the device is reachable after reconnect.
|
||||||
|
///
|
||||||
|
/// Current limitation: strict firmware version comparison is not possible
|
||||||
|
/// yet because no firmware version characteristic is exposed by the device.
|
||||||
|
Future<Result<void>> verifyDeviceReachable({
|
||||||
|
required Duration timeout,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShifterFirmwareUpdateTransport implements FirmwareUpdateTransport {
|
||||||
|
ShifterFirmwareUpdateTransport({
|
||||||
|
required this.shifterService,
|
||||||
|
required this.bluetoothController,
|
||||||
|
required this.buttonDeviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ShifterService shifterService;
|
||||||
|
final BluetoothController bluetoothController;
|
||||||
|
final String buttonDeviceId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<DfuPreflightResult>> runPreflight({
|
||||||
|
required int requestedMtu,
|
||||||
|
}) {
|
||||||
|
return shifterService.runDfuPreflight(requestedMtu: requestedMtu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<int>> subscribeToAck() {
|
||||||
|
return bluetoothController.subscribeToCharacteristic(
|
||||||
|
buttonDeviceId,
|
||||||
|
universalShifterControlServiceUuid,
|
||||||
|
universalShifterDfuAckCharacteristicUuid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<void>> writeControl(List<int> payload) {
|
||||||
|
return bluetoothController.writeCharacteristic(
|
||||||
|
buttonDeviceId,
|
||||||
|
universalShifterControlServiceUuid,
|
||||||
|
universalShifterDfuControlCharacteristicUuid,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<void>> writeDataFrame(List<int> frame) {
|
||||||
|
return bluetoothController.writeCharacteristic(
|
||||||
|
buttonDeviceId,
|
||||||
|
universalShifterControlServiceUuid,
|
||||||
|
universalShifterDfuDataCharacteristicUuid,
|
||||||
|
frame,
|
||||||
|
withResponse: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<void>> waitForExpectedResetDisconnect({
|
||||||
|
required Duration timeout,
|
||||||
|
}) async {
|
||||||
|
final currentState = bluetoothController.currentConnectionState;
|
||||||
|
if (currentState.$1 == ConnectionStatus.disconnected) {
|
||||||
|
return Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bluetoothController.connectionStateStream
|
||||||
|
.firstWhere((state) => state.$1 == ConnectionStatus.disconnected)
|
||||||
|
.timeout(timeout);
|
||||||
|
return Ok(null);
|
||||||
|
} on TimeoutException {
|
||||||
|
return bail(
|
||||||
|
'Timed out after ${timeout.inMilliseconds}ms waiting for the expected reset disconnect.',
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return bail('Failed while waiting for expected reset disconnect: $error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<void>> reconnectForVerification({
|
||||||
|
required Duration timeout,
|
||||||
|
}) async {
|
||||||
|
final connectResult =
|
||||||
|
await bluetoothController.connectById(buttonDeviceId, timeout: timeout);
|
||||||
|
if (connectResult.isErr()) {
|
||||||
|
return bail(connectResult.unwrapErr());
|
||||||
|
}
|
||||||
|
|
||||||
|
final currentState = bluetoothController.currentConnectionState;
|
||||||
|
if (currentState.$1 == ConnectionStatus.connected &&
|
||||||
|
currentState.$2 == buttonDeviceId) {
|
||||||
|
return Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bluetoothController.connectionStateStream
|
||||||
|
.firstWhere(
|
||||||
|
(state) =>
|
||||||
|
state.$1 == ConnectionStatus.connected &&
|
||||||
|
state.$2 == buttonDeviceId,
|
||||||
|
)
|
||||||
|
.timeout(timeout);
|
||||||
|
return Ok(null);
|
||||||
|
} on TimeoutException {
|
||||||
|
return bail(
|
||||||
|
'Timed out after ${timeout.inMilliseconds}ms waiting for reconnect.',
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return bail('Reconnect wait failed: $error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<void>> verifyDeviceReachable({
|
||||||
|
required Duration timeout,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final statusResult = await shifterService.readStatus().timeout(timeout);
|
||||||
|
if (statusResult.isErr()) {
|
||||||
|
return bail(statusResult.unwrapErr());
|
||||||
|
}
|
||||||
|
return Ok(null);
|
||||||
|
} on TimeoutException {
|
||||||
|
return bail(
|
||||||
|
'Timed out after ${timeout.inMilliseconds}ms while reading status for post-update verification.',
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return bail('Post-update verification failed: $error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DfuFailure implements Exception {
|
||||||
|
const _DfuFailure(this.message);
|
||||||
|
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => message;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DfuCancelled implements Exception {
|
||||||
|
const _DfuCancelled();
|
||||||
|
}
|
||||||
325
lib/service/shifter_service.dart
Normal file
325
lib/service/shifter_service.dart
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:abawo_bt_app/controller/bluetooth.dart';
|
||||||
|
import 'package:abawo_bt_app/model/shifter_types.dart';
|
||||||
|
import 'package:anyhow/anyhow.dart';
|
||||||
|
|
||||||
|
class ShifterService {
|
||||||
|
ShifterService({
|
||||||
|
BluetoothController? bluetooth,
|
||||||
|
required this.buttonDeviceId,
|
||||||
|
DfuPreflightBluetoothAdapter? dfuPreflightBluetooth,
|
||||||
|
}) : _bluetooth = bluetooth,
|
||||||
|
_dfuPreflightBluetooth =
|
||||||
|
dfuPreflightBluetooth ?? _BluetoothDfuPreflightAdapter(bluetooth!) {
|
||||||
|
if (bluetooth == null && dfuPreflightBluetooth == null) {
|
||||||
|
throw ArgumentError(
|
||||||
|
'Either bluetooth or dfuPreflightBluetooth must be provided.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final BluetoothController? _bluetooth;
|
||||||
|
final String buttonDeviceId;
|
||||||
|
final DfuPreflightBluetoothAdapter _dfuPreflightBluetooth;
|
||||||
|
|
||||||
|
BluetoothController get _requireBluetooth {
|
||||||
|
final bluetooth = _bluetooth;
|
||||||
|
if (bluetooth == null) {
|
||||||
|
throw StateError('Bluetooth controller is not available.');
|
||||||
|
}
|
||||||
|
return bluetooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
final StreamController<CentralStatus> _statusController =
|
||||||
|
StreamController<CentralStatus>.broadcast();
|
||||||
|
StreamSubscription<List<int>>? _statusSubscription;
|
||||||
|
|
||||||
|
Stream<CentralStatus> get statusStream => _statusController.stream;
|
||||||
|
|
||||||
|
static const int _gearRatioSlots = 32;
|
||||||
|
static const int _defaultGearIndexOffset = _gearRatioSlots;
|
||||||
|
static const int _gearRatioPayloadBytes = _gearRatioSlots + 1;
|
||||||
|
static const double _maxGearRatio = 255 / 64;
|
||||||
|
static const int _gearRatioWriteMtu = 64;
|
||||||
|
|
||||||
|
Future<Result<void>> writeConnectToAddress(String bikeDeviceId) async {
|
||||||
|
try {
|
||||||
|
final payload = parseMacToLittleEndianBytes(bikeDeviceId);
|
||||||
|
return _requireBluetooth.writeCharacteristic(
|
||||||
|
buttonDeviceId,
|
||||||
|
universalShifterControlServiceUuid,
|
||||||
|
universalShifterConnectToAddrCharacteristicUuid,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
} on FormatException catch (e) {
|
||||||
|
return bail('Could not parse bike address "$bikeDeviceId": $e');
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Failed writing connect address: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> writeCommand(UniversalShifterCommand command) {
|
||||||
|
return _requireBluetooth.writeCharacteristic(
|
||||||
|
buttonDeviceId,
|
||||||
|
universalShifterControlServiceUuid,
|
||||||
|
universalShifterCommandCharacteristicUuid,
|
||||||
|
[command.value],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> connectButtonToBike(String bikeDeviceId) async {
|
||||||
|
final addrRes = await writeConnectToAddress(bikeDeviceId);
|
||||||
|
if (addrRes.isErr()) {
|
||||||
|
return addrRes;
|
||||||
|
}
|
||||||
|
return writeCommand(UniversalShifterCommand.connectToDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<GearRatiosData>> readGearRatios() async {
|
||||||
|
final readRes = await _requireBluetooth.readCharacteristic(
|
||||||
|
buttonDeviceId,
|
||||||
|
universalShifterControlServiceUuid,
|
||||||
|
universalShifterGearRatiosCharacteristicUuid,
|
||||||
|
);
|
||||||
|
if (readRes.isErr()) {
|
||||||
|
return bail(readRes.unwrapErr());
|
||||||
|
}
|
||||||
|
|
||||||
|
final raw = readRes.unwrap();
|
||||||
|
if (raw.length > _gearRatioPayloadBytes) {
|
||||||
|
return bail(
|
||||||
|
'Invalid gear ratio payload length: expected at most $_gearRatioPayloadBytes, got ${raw.length}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalizedRaw = List<int>.filled(
|
||||||
|
_gearRatioPayloadBytes,
|
||||||
|
0,
|
||||||
|
growable: false,
|
||||||
|
);
|
||||||
|
for (var i = 0; i < raw.length; i++) {
|
||||||
|
normalizedRaw[i] = raw[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
final ratios = <double>[];
|
||||||
|
for (var i = 0; i < _gearRatioSlots; i++) {
|
||||||
|
final value = normalizedRaw[i];
|
||||||
|
if (value == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ratios.add(_decodeGearRatio(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
final defaultIndexRaw = normalizedRaw[_defaultGearIndexOffset];
|
||||||
|
final defaultGearIndex = ratios.isEmpty
|
||||||
|
? 0
|
||||||
|
: defaultIndexRaw.clamp(0, ratios.length - 1).toInt();
|
||||||
|
return Ok(
|
||||||
|
GearRatiosData(
|
||||||
|
ratios: ratios,
|
||||||
|
defaultGearIndex: defaultGearIndex,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> writeGearRatios(GearRatiosData data) async {
|
||||||
|
final mtuResult = await _requireBluetooth.requestMtu(
|
||||||
|
buttonDeviceId,
|
||||||
|
mtu: _gearRatioWriteMtu,
|
||||||
|
);
|
||||||
|
if (mtuResult.isErr()) {
|
||||||
|
return bail(
|
||||||
|
'Could not request MTU before writing gear ratios: ${mtuResult.unwrapErr()}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final payload =
|
||||||
|
List<int>.filled(_gearRatioPayloadBytes, 0, growable: false);
|
||||||
|
final ratios = data.ratios;
|
||||||
|
final limit =
|
||||||
|
ratios.length < _gearRatioSlots ? ratios.length : _gearRatioSlots;
|
||||||
|
for (var i = 0; i < limit; i++) {
|
||||||
|
payload[i] = _encodeGearRatio(ratios[i]);
|
||||||
|
}
|
||||||
|
payload[_defaultGearIndexOffset] =
|
||||||
|
limit == 0 ? 0 : data.defaultGearIndex.clamp(0, limit - 1).toInt();
|
||||||
|
|
||||||
|
return _requireBluetooth.writeCharacteristic(
|
||||||
|
buttonDeviceId,
|
||||||
|
universalShifterControlServiceUuid,
|
||||||
|
universalShifterGearRatiosCharacteristicUuid,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<CentralStatus>> readStatus() async {
|
||||||
|
final readRes = await _requireBluetooth.readCharacteristic(
|
||||||
|
buttonDeviceId,
|
||||||
|
universalShifterControlServiceUuid,
|
||||||
|
universalShifterStatusCharacteristicUuid,
|
||||||
|
);
|
||||||
|
if (readRes.isErr()) {
|
||||||
|
return bail(readRes.unwrapErr());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Ok(CentralStatus.fromCborBytes(readRes.unwrap()));
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Failed to decode status payload: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<DfuPreflightResult>> runDfuPreflight({
|
||||||
|
int requestedMtu = universalShifterDfuPreferredMtu,
|
||||||
|
}) async {
|
||||||
|
final currentConnection = _dfuPreflightBluetooth.currentConnectionState;
|
||||||
|
final connectionStatus = currentConnection.$1;
|
||||||
|
final connectedDeviceId = currentConnection.$2;
|
||||||
|
|
||||||
|
if (connectionStatus != ConnectionStatus.connected ||
|
||||||
|
connectedDeviceId == null) {
|
||||||
|
return Ok(
|
||||||
|
DfuPreflightResult.failed(
|
||||||
|
requestedMtu: requestedMtu,
|
||||||
|
failureReason: DfuPreflightFailureReason.deviceNotConnected,
|
||||||
|
message:
|
||||||
|
'No button connection is active. Connect the target button, then retry the firmware update.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectedDeviceId != buttonDeviceId) {
|
||||||
|
return Ok(
|
||||||
|
DfuPreflightResult.failed(
|
||||||
|
requestedMtu: requestedMtu,
|
||||||
|
failureReason: DfuPreflightFailureReason.wrongConnectedDevice,
|
||||||
|
message:
|
||||||
|
'Connected to a different button ($connectedDeviceId). Reconnect to $buttonDeviceId before starting the firmware update.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final mtuResult = await _dfuPreflightBluetooth.requestMtuAndGetValue(
|
||||||
|
buttonDeviceId,
|
||||||
|
mtu: requestedMtu,
|
||||||
|
);
|
||||||
|
if (mtuResult.isErr()) {
|
||||||
|
return Ok(
|
||||||
|
DfuPreflightResult.failed(
|
||||||
|
requestedMtu: requestedMtu,
|
||||||
|
failureReason: DfuPreflightFailureReason.mtuRequestFailed,
|
||||||
|
message:
|
||||||
|
'Could not negotiate BLE MTU for DFU. Move closer to the button and retry. Details: ${mtuResult.unwrapErr()}',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final negotiatedMtu = mtuResult.unwrap();
|
||||||
|
if (negotiatedMtu < universalShifterDfuMinimumMtu) {
|
||||||
|
return Ok(
|
||||||
|
DfuPreflightResult.failed(
|
||||||
|
requestedMtu: requestedMtu,
|
||||||
|
negotiatedMtu: negotiatedMtu,
|
||||||
|
failureReason: DfuPreflightFailureReason.mtuTooLow,
|
||||||
|
message:
|
||||||
|
'Negotiated MTU $negotiatedMtu is too small for 64-byte DFU frames. Need at least $universalShifterDfuMinimumMtu bytes MTU (64-byte frame + $universalShifterAttWriteOverheadBytes-byte ATT overhead). Reconnect and retry.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(
|
||||||
|
DfuPreflightResult.ready(
|
||||||
|
requestedMtu: requestedMtu,
|
||||||
|
negotiatedMtu: negotiatedMtu,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void startStatusNotifications() {
|
||||||
|
if (_statusSubscription != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_statusSubscription = _requireBluetooth
|
||||||
|
.subscribeToCharacteristic(
|
||||||
|
buttonDeviceId,
|
||||||
|
universalShifterControlServiceUuid,
|
||||||
|
universalShifterStatusCharacteristicUuid,
|
||||||
|
)
|
||||||
|
.listen(
|
||||||
|
(data) {
|
||||||
|
try {
|
||||||
|
final status = CentralStatus.fromCborBytes(data);
|
||||||
|
_statusController.add(status);
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore malformed payloads but keep stream alive.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (_) {
|
||||||
|
// Keep UI running; reconnection logic is handled elsewhere.
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stopStatusNotifications() async {
|
||||||
|
await _statusSubscription?.cancel();
|
||||||
|
_statusSubscription = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await stopStatusNotifications();
|
||||||
|
await _statusController.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
int _encodeGearRatio(double value) {
|
||||||
|
if (value <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
final clamped = value.clamp(0, _maxGearRatio);
|
||||||
|
final scaled = (clamped * 64).round();
|
||||||
|
if (scaled <= 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return scaled.clamp(1, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _decodeGearRatio(int raw) {
|
||||||
|
return raw / 64.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface class DfuPreflightBluetoothAdapter {
|
||||||
|
(ConnectionStatus, String?) get currentConnectionState;
|
||||||
|
Future<Result<int>> requestMtuAndGetValue(
|
||||||
|
String deviceId, {
|
||||||
|
required int mtu,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BluetoothDfuPreflightAdapter implements DfuPreflightBluetoothAdapter {
|
||||||
|
const _BluetoothDfuPreflightAdapter(this._bluetooth);
|
||||||
|
|
||||||
|
final BluetoothController _bluetooth;
|
||||||
|
|
||||||
|
@override
|
||||||
|
(ConnectionStatus, String?) get currentConnectionState =>
|
||||||
|
_bluetooth.currentConnectionState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<int>> requestMtuAndGetValue(
|
||||||
|
String deviceId, {
|
||||||
|
required int mtu,
|
||||||
|
}) {
|
||||||
|
return _bluetooth.requestMtuAndGetValue(deviceId, mtu: mtu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GearRatiosData {
|
||||||
|
const GearRatiosData({
|
||||||
|
required this.ratios,
|
||||||
|
required this.defaultGearIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<double> ratios;
|
||||||
|
final int defaultGearIndex;
|
||||||
|
}
|
||||||
10
lib/src/rust/api/simple.dart
Normal file
10
lib/src/rust/api/simple.dart
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||||
|
|
||||||
|
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
||||||
|
|
||||||
|
import '../frb_generated.dart';
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
|
||||||
|
String greet({required String name}) =>
|
||||||
|
RustLib.instance.api.crateApiSimpleGreet(name: name);
|
||||||
240
lib/src/rust/frb_generated.dart
Normal file
240
lib/src/rust/frb_generated.dart
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||||
|
|
||||||
|
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
|
||||||
|
|
||||||
|
import 'api/simple.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'frb_generated.dart';
|
||||||
|
import 'frb_generated.io.dart'
|
||||||
|
if (dart.library.js_interop) 'frb_generated.web.dart';
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
|
||||||
|
/// Main entrypoint of the Rust API
|
||||||
|
class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||||
|
@internal
|
||||||
|
static final instance = RustLib._();
|
||||||
|
|
||||||
|
RustLib._();
|
||||||
|
|
||||||
|
/// Initialize flutter_rust_bridge
|
||||||
|
static Future<void> init({
|
||||||
|
RustLibApi? api,
|
||||||
|
BaseHandler? handler,
|
||||||
|
ExternalLibrary? externalLibrary,
|
||||||
|
bool forceSameCodegenVersion = true,
|
||||||
|
}) async {
|
||||||
|
await instance.initImpl(
|
||||||
|
api: api,
|
||||||
|
handler: handler,
|
||||||
|
externalLibrary: externalLibrary,
|
||||||
|
forceSameCodegenVersion: forceSameCodegenVersion,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize flutter_rust_bridge in mock mode.
|
||||||
|
/// No libraries for FFI are loaded.
|
||||||
|
static void initMock({
|
||||||
|
required RustLibApi api,
|
||||||
|
}) {
|
||||||
|
instance.initMockImpl(
|
||||||
|
api: api,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispose flutter_rust_bridge
|
||||||
|
///
|
||||||
|
/// The call to this function is optional, since flutter_rust_bridge (and everything else)
|
||||||
|
/// is automatically disposed when the app stops.
|
||||||
|
static void dispose() => instance.disposeImpl();
|
||||||
|
|
||||||
|
@override
|
||||||
|
ApiImplConstructor<RustLibApiImpl, RustLibWire> get apiImplConstructor =>
|
||||||
|
RustLibApiImpl.new;
|
||||||
|
|
||||||
|
@override
|
||||||
|
WireConstructor<RustLibWire> get wireConstructor =>
|
||||||
|
RustLibWire.fromExternalLibrary;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> executeRustInitializers() async {
|
||||||
|
await api.crateApiSimpleInitApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ExternalLibraryLoaderConfig get defaultExternalLibraryLoaderConfig =>
|
||||||
|
kDefaultExternalLibraryLoaderConfig;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get codegenVersion => '2.11.1';
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get rustContentHash => -1918914929;
|
||||||
|
|
||||||
|
static const kDefaultExternalLibraryLoaderConfig =
|
||||||
|
ExternalLibraryLoaderConfig(
|
||||||
|
stem: 'rust_lib_abawo_bt_app',
|
||||||
|
ioDirectory: 'rust/target/release/',
|
||||||
|
webPrefix: 'pkg/',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class RustLibApi extends BaseApi {
|
||||||
|
String crateApiSimpleGreet({required String name});
|
||||||
|
|
||||||
|
Future<void> crateApiSimpleInitApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
|
RustLibApiImpl({
|
||||||
|
required super.handler,
|
||||||
|
required super.wire,
|
||||||
|
required super.generalizedFrbRustBinding,
|
||||||
|
required super.portManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String crateApiSimpleGreet({required String name}) {
|
||||||
|
return handler.executeSync(SyncTask(
|
||||||
|
callFfi: () {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(name, serializer);
|
||||||
|
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 1)!;
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_String,
|
||||||
|
decodeErrorData: null,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiSimpleGreetConstMeta,
|
||||||
|
argValues: [name],
|
||||||
|
apiImpl: this,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiSimpleGreetConstMeta => const TaskConstMeta(
|
||||||
|
debugName: "greet",
|
||||||
|
argNames: ["name"],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> crateApiSimpleInitApp() {
|
||||||
|
return handler.executeNormal(NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
pdeCallFfi(generalizedFrbRustBinding, serializer,
|
||||||
|
funcId: 2, port: port_);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_unit,
|
||||||
|
decodeErrorData: null,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiSimpleInitAppConstMeta,
|
||||||
|
argValues: [],
|
||||||
|
apiImpl: this,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiSimpleInitAppConstMeta => const TaskConstMeta(
|
||||||
|
debugName: "init_app",
|
||||||
|
argNames: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String dco_decode_String(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return raw as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return raw as Uint8List;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int dco_decode_u_8(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return raw as int;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void dco_decode_unit(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String sse_decode_String(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
var inner = sse_decode_list_prim_u_8_strict(deserializer);
|
||||||
|
return utf8.decoder.convert(inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
var len_ = sse_decode_i_32(deserializer);
|
||||||
|
return deserializer.buffer.getUint8List(len_);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_u_8(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
return deserializer.buffer.getUint8();
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_decode_unit(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_i_32(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
return deserializer.buffer.getInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
bool sse_decode_bool(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
return deserializer.buffer.getUint8() != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_String(String self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_prim_u_8_strict(
|
||||||
|
Uint8List self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
sse_encode_i_32(self.length, serializer);
|
||||||
|
serializer.buffer.putUint8List(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_u_8(int self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
serializer.buffer.putUint8(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_unit(void self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_i_32(int self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
serializer.buffer.putInt32(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_bool(bool self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
serializer.buffer.putUint8(self ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
84
lib/src/rust/frb_generated.io.dart
Normal file
84
lib/src/rust/frb_generated.io.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||||
|
|
||||||
|
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
|
||||||
|
|
||||||
|
import 'api/simple.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:ffi' as ffi;
|
||||||
|
import 'frb_generated.dart';
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
|
||||||
|
|
||||||
|
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
|
RustLibApiImplPlatform({
|
||||||
|
required super.handler,
|
||||||
|
required super.wire,
|
||||||
|
required super.generalizedFrbRustBinding,
|
||||||
|
required super.portManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String dco_decode_String(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int dco_decode_u_8(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void dco_decode_unit(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String sse_decode_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_u_8(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_decode_unit(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_i_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
bool sse_decode_bool(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_String(String self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_prim_u_8_strict(
|
||||||
|
Uint8List self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_u_8(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_unit(void self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_i_32(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section: wire_class
|
||||||
|
|
||||||
|
class RustLibWire implements BaseWire {
|
||||||
|
factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) =>
|
||||||
|
RustLibWire(lib.ffiDynamicLibrary);
|
||||||
|
|
||||||
|
/// Holds the symbol lookup function.
|
||||||
|
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
|
||||||
|
_lookup;
|
||||||
|
|
||||||
|
/// The symbols are looked up in [dynamicLibrary].
|
||||||
|
RustLibWire(ffi.DynamicLibrary dynamicLibrary)
|
||||||
|
: _lookup = dynamicLibrary.lookup;
|
||||||
|
}
|
||||||
84
lib/src/rust/frb_generated.web.dart
Normal file
84
lib/src/rust/frb_generated.web.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||||
|
|
||||||
|
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
|
||||||
|
|
||||||
|
// Static analysis wrongly picks the IO variant, thus ignore this
|
||||||
|
// ignore_for_file: argument_type_not_assignable
|
||||||
|
|
||||||
|
import 'api/simple.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'frb_generated.dart';
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
|
||||||
|
|
||||||
|
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
|
RustLibApiImplPlatform({
|
||||||
|
required super.handler,
|
||||||
|
required super.wire,
|
||||||
|
required super.generalizedFrbRustBinding,
|
||||||
|
required super.portManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String dco_decode_String(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int dco_decode_u_8(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void dco_decode_unit(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String sse_decode_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_u_8(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_decode_unit(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_i_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
bool sse_decode_bool(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_String(String self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_prim_u_8_strict(
|
||||||
|
Uint8List self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_u_8(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_unit(void self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_i_32(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section: wire_class
|
||||||
|
|
||||||
|
class RustLibWire implements BaseWire {
|
||||||
|
RustLibWire.fromExternalLibrary(ExternalLibrary lib);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JS('wasm_bindgen')
|
||||||
|
external RustLibWasmModule get wasmModule;
|
||||||
|
|
||||||
|
@JS()
|
||||||
|
@anonymous
|
||||||
|
extension type RustLibWasmModule._(JSObject _) implements JSObject {}
|
||||||
@ -1 +1,21 @@
|
|||||||
const abawoServiceBtUUID = '0993826f-0ee4-4b37-9614-d13ecba4ffc2';
|
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
|
||||||
|
|
||||||
|
const abawoServiceBtUUIDPrefix = '0993826f-0ee4-4b37-9614';
|
||||||
|
const abawoUniversalShiftersServiceBtUUID =
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba4ffc2';
|
||||||
|
|
||||||
|
bool isAbawoDeviceGuid(Uuid guid) {
|
||||||
|
return guid
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll('-', '')
|
||||||
|
.startsWith(abawoServiceBtUUIDPrefix.toLowerCase().replaceAll('-', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAbawoUniversalShiftersDeviceGuid(Uuid guid) {
|
||||||
|
return guid == Uuid.parse(abawoUniversalShiftersServiceBtUUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isConnectableAbawoDeviceGuid(Uuid guid) {
|
||||||
|
return isAbawoUniversalShiftersDeviceGuid(guid);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
part 'sharedPrefs.g.dart';
|
// part 'sharedPrefs.g.dart';
|
||||||
|
|
||||||
final sharedPreferencesProvider =
|
final sharedPreferencesProvider =
|
||||||
Provider<SharedPreferences>((ref) => throw UnimplementedError());
|
Provider<SharedPreferences>((ref) => throw UnimplementedError());
|
||||||
|
|||||||
@ -1,197 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'sharedPrefs.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// RiverpodGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
String _$sharedPrefValueHash() => r'6c78fac8d11d0df162d4d53f465c1c8535fcd150';
|
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
|
||||||
class _SystemHash {
|
|
||||||
_SystemHash._();
|
|
||||||
|
|
||||||
static int combine(int hash, int value) {
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
hash = 0x1fffffff & (hash + value);
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
|
||||||
return hash ^ (hash >> 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int finish(int hash) {
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
hash = hash ^ (hash >> 11);
|
|
||||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class _$SharedPrefValue extends BuildlessAutoDisposeNotifier<T> {
|
|
||||||
late final String key;
|
|
||||||
late final T defaultValue;
|
|
||||||
|
|
||||||
T build(
|
|
||||||
String key,
|
|
||||||
T defaultValue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// See also [SharedPrefValue].
|
|
||||||
@ProviderFor(SharedPrefValue)
|
|
||||||
const sharedPrefValueProvider = SharedPrefValueFamily();
|
|
||||||
|
|
||||||
/// See also [SharedPrefValue].
|
|
||||||
class SharedPrefValueFamily extends Family<T> {
|
|
||||||
/// See also [SharedPrefValue].
|
|
||||||
const SharedPrefValueFamily();
|
|
||||||
|
|
||||||
/// See also [SharedPrefValue].
|
|
||||||
SharedPrefValueProvider call(
|
|
||||||
String key,
|
|
||||||
T defaultValue,
|
|
||||||
) {
|
|
||||||
return SharedPrefValueProvider(
|
|
||||||
key,
|
|
||||||
defaultValue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
SharedPrefValueProvider getProviderOverride(
|
|
||||||
covariant SharedPrefValueProvider provider,
|
|
||||||
) {
|
|
||||||
return call(
|
|
||||||
provider.key,
|
|
||||||
provider.defaultValue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
|
||||||
|
|
||||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
|
||||||
_allTransitiveDependencies;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String? get name => r'sharedPrefValueProvider';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// See also [SharedPrefValue].
|
|
||||||
class SharedPrefValueProvider
|
|
||||||
extends AutoDisposeNotifierProviderImpl<SharedPrefValue, T> {
|
|
||||||
/// See also [SharedPrefValue].
|
|
||||||
SharedPrefValueProvider(
|
|
||||||
String key,
|
|
||||||
T defaultValue,
|
|
||||||
) : this._internal(
|
|
||||||
() => SharedPrefValue()
|
|
||||||
..key = key
|
|
||||||
..defaultValue = defaultValue,
|
|
||||||
from: sharedPrefValueProvider,
|
|
||||||
name: r'sharedPrefValueProvider',
|
|
||||||
debugGetCreateSourceHash:
|
|
||||||
const bool.fromEnvironment('dart.vm.product')
|
|
||||||
? null
|
|
||||||
: _$sharedPrefValueHash,
|
|
||||||
dependencies: SharedPrefValueFamily._dependencies,
|
|
||||||
allTransitiveDependencies:
|
|
||||||
SharedPrefValueFamily._allTransitiveDependencies,
|
|
||||||
key: key,
|
|
||||||
defaultValue: defaultValue,
|
|
||||||
);
|
|
||||||
|
|
||||||
SharedPrefValueProvider._internal(
|
|
||||||
super._createNotifier, {
|
|
||||||
required super.name,
|
|
||||||
required super.dependencies,
|
|
||||||
required super.allTransitiveDependencies,
|
|
||||||
required super.debugGetCreateSourceHash,
|
|
||||||
required super.from,
|
|
||||||
required this.key,
|
|
||||||
required this.defaultValue,
|
|
||||||
}) : super.internal();
|
|
||||||
|
|
||||||
final String key;
|
|
||||||
final T defaultValue;
|
|
||||||
|
|
||||||
@override
|
|
||||||
T runNotifierBuild(
|
|
||||||
covariant SharedPrefValue notifier,
|
|
||||||
) {
|
|
||||||
return notifier.build(
|
|
||||||
key,
|
|
||||||
defaultValue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Override overrideWith(SharedPrefValue Function() create) {
|
|
||||||
return ProviderOverride(
|
|
||||||
origin: this,
|
|
||||||
override: SharedPrefValueProvider._internal(
|
|
||||||
() => create()
|
|
||||||
..key = key
|
|
||||||
..defaultValue = defaultValue,
|
|
||||||
from: from,
|
|
||||||
name: null,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
debugGetCreateSourceHash: null,
|
|
||||||
key: key,
|
|
||||||
defaultValue: defaultValue,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
AutoDisposeNotifierProviderElement<SharedPrefValue, T> createElement() {
|
|
||||||
return _SharedPrefValueProviderElement(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return other is SharedPrefValueProvider &&
|
|
||||||
other.key == key &&
|
|
||||||
other.defaultValue == defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
|
||||||
hash = _SystemHash.combine(hash, key.hashCode);
|
|
||||||
hash = _SystemHash.combine(hash, defaultValue.hashCode);
|
|
||||||
|
|
||||||
return _SystemHash.finish(hash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
|
||||||
// ignore: unused_element
|
|
||||||
mixin SharedPrefValueRef on AutoDisposeNotifierProviderRef<T> {
|
|
||||||
/// The parameter `key` of this provider.
|
|
||||||
String get key;
|
|
||||||
|
|
||||||
/// The parameter `defaultValue` of this provider.
|
|
||||||
T get defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SharedPrefValueProviderElement
|
|
||||||
extends AutoDisposeNotifierProviderElement<SharedPrefValue, T>
|
|
||||||
with SharedPrefValueRef {
|
|
||||||
_SharedPrefValueProviderElement(super.provider);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get key => (origin as SharedPrefValueProvider).key;
|
|
||||||
@override
|
|
||||||
T get defaultValue => (origin as SharedPrefValueProvider).defaultValue;
|
|
||||||
}
|
|
||||||
// ignore_for_file: type=lint
|
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
|
||||||
214
lib/widgets/bike_scan_dialog.dart
Normal file
214
lib/widgets/bike_scan_dialog.dart
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import 'package:abawo_bt_app/controller/bluetooth.dart';
|
||||||
|
import 'package:abawo_bt_app/model/shifter_types.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class BikeScanDialog extends ConsumerStatefulWidget {
|
||||||
|
const BikeScanDialog({
|
||||||
|
required this.excludedDeviceId,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String excludedDeviceId;
|
||||||
|
|
||||||
|
static Future<DiscoveredDevice?> show(
|
||||||
|
BuildContext context, {
|
||||||
|
required String excludedDeviceId,
|
||||||
|
}) {
|
||||||
|
return showDialog<DiscoveredDevice>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: true,
|
||||||
|
builder: (_) => BikeScanDialog(excludedDeviceId: excludedDeviceId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<BikeScanDialog> createState() => _BikeScanDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BikeScanDialogState extends ConsumerState<BikeScanDialog> {
|
||||||
|
bool _showAll = false;
|
||||||
|
BluetoothController? _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_startScan();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startScan() async {
|
||||||
|
final controller = await ref.read(bluetoothProvider.future);
|
||||||
|
_controller = controller;
|
||||||
|
await controller.stopScan();
|
||||||
|
await controller.startScan();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller?.stopScan();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final btAsync = ref.watch(bluetoothProvider);
|
||||||
|
|
||||||
|
return Dialog(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 520,
|
||||||
|
height: 520,
|
||||||
|
child: btAsync.when(
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (err, _) => Center(child: Text('Bluetooth error: $err')),
|
||||||
|
data: (controller) {
|
||||||
|
_controller ??= controller;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildHeader(context),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Expanded(
|
||||||
|
child: StreamBuilder<List<DiscoveredDevice>>(
|
||||||
|
stream: controller.scanResultsStream,
|
||||||
|
initialData: controller.scanResults,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final devices =
|
||||||
|
_filteredDevices(snapshot.data ?? const []);
|
||||||
|
if (devices.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Text('No matching devices nearby.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
itemCount: devices.length,
|
||||||
|
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final device = devices[index];
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primaryContainer,
|
||||||
|
child: const Icon(Icons.pedal_bike),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
device.name.isEmpty
|
||||||
|
? 'Unknown Device'
|
||||||
|
: device.name,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
device.id,
|
||||||
|
style: const TextStyle(fontFamily: 'monospace'),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
trailing: _RssiBadge(rssi: device.rssi),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop(device);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 12, 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Select Bike',
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text('Show All'),
|
||||||
|
Switch(
|
||||||
|
value: _showAll,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_showAll = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Rescan',
|
||||||
|
onPressed: _startScan,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Close',
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DiscoveredDevice> _filteredDevices(List<DiscoveredDevice> devices) {
|
||||||
|
final ftmsUuid = Uuid.parse(ftmsServiceUuid);
|
||||||
|
return devices.where((device) {
|
||||||
|
if (device.id == widget.excludedDeviceId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (_showAll) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return device.serviceUuids.contains(ftmsUuid);
|
||||||
|
}).toList(growable: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RssiBadge extends StatelessWidget {
|
||||||
|
const _RssiBadge({required this.rssi});
|
||||||
|
|
||||||
|
final int rssi;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final color = rssi > -65
|
||||||
|
? Colors.green
|
||||||
|
: rssi > -80
|
||||||
|
? Colors.orange
|
||||||
|
: Colors.red;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.15),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'$rssi dBm',
|
||||||
|
style: TextStyle(
|
||||||
|
color: color,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,18 +1,23 @@
|
|||||||
|
import 'package:abawo_bt_app/model/bluetooth_device_model.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:ui'; // Required for ImageFilter
|
import 'dart:ui'; // Required for ImageFilter
|
||||||
|
|
||||||
class DeviceListItem extends StatelessWidget {
|
class DeviceListItem extends StatelessWidget {
|
||||||
final String deviceName;
|
final String deviceName;
|
||||||
final String deviceId; // Added for potential future use or subtitle
|
final String deviceId; // Added for potential future use or subtitle
|
||||||
final bool isUnknownDevice;
|
final DeviceType type;
|
||||||
// final String? imageUrl; // Optional image URL - commented out for now
|
// final String? imageUrl; // Optional image URL - commented out for now
|
||||||
|
final bool isConnecting; // Add this line
|
||||||
|
final Widget? trailing;
|
||||||
|
|
||||||
const DeviceListItem({
|
const DeviceListItem({
|
||||||
super.key,
|
super.key,
|
||||||
required this.deviceName,
|
required this.deviceName,
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
this.isUnknownDevice = false,
|
required this.type,
|
||||||
// this.imageUrl,
|
// this.imageUrl,
|
||||||
|
this.isConnecting = false, // Add this line
|
||||||
|
this.trailing,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -22,11 +27,11 @@ class DeviceListItem extends StatelessWidget {
|
|||||||
|
|
||||||
// Glassy effect colors - adjust transparency and base color as needed
|
// Glassy effect colors - adjust transparency and base color as needed
|
||||||
final glassColor = isDarkMode
|
final glassColor = isDarkMode
|
||||||
? Colors.white.withOpacity(0.1)
|
? Colors.white.withValues(alpha: 0.1)
|
||||||
: Colors.black.withOpacity(0.05);
|
: Colors.black.withValues(alpha: 0.05);
|
||||||
final shadowColor = isDarkMode
|
final shadowColor = isDarkMode
|
||||||
? Colors.black.withOpacity(0.4)
|
? Colors.black.withValues(alpha: 0.4)
|
||||||
: Colors.grey.withOpacity(0.5);
|
: Colors.grey.withValues(alpha: 0.5);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
@ -57,18 +62,30 @@ class DeviceListItem extends StatelessWidget {
|
|||||||
glassColor, // Semi-transparent color for glass effect
|
glassColor, // Semi-transparent color for glass effect
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Colors.white.withOpacity(0.2), // Subtle border
|
color:
|
||||||
|
Colors.white.withValues(alpha: 0.2), // Subtle border
|
||||||
width: 0.5,
|
width: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: type == DeviceType.universalShifters
|
||||||
// Placeholder '?' - replace with Image widget when imageUrl is available
|
// For Universal Shifters: Image fills the container, constrained by rounded borders
|
||||||
|
? ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/shifter-wireframe.png',
|
||||||
|
fit: BoxFit.cover, // Cover the entire container
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// For other devices: Question mark with padding
|
||||||
|
: const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'?',
|
'?',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 30,
|
fontSize: 30,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white70, // Adjust color as needed
|
color: Colors.white70,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -84,14 +101,16 @@ class DeviceListItem extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
isUnknownDevice ? 'Unknown Device' : deviceName,
|
deviceName.isEmpty ? 'Unknown Device' : deviceName,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight:
|
fontWeight: deviceName.isEmpty
|
||||||
isUnknownDevice ? FontWeight.normal : FontWeight.w500,
|
? FontWeight.normal
|
||||||
fontStyle:
|
: FontWeight.w500,
|
||||||
isUnknownDevice ? FontStyle.italic : FontStyle.normal,
|
fontStyle: deviceName.isEmpty
|
||||||
color: isUnknownDevice
|
? FontStyle.italic
|
||||||
|
: FontStyle.normal,
|
||||||
|
color: deviceName.isEmpty
|
||||||
? theme.hintColor
|
? theme.hintColor
|
||||||
: theme.textTheme.bodyLarge?.color,
|
: theme.textTheme.bodyLarge?.color,
|
||||||
),
|
),
|
||||||
@ -108,6 +127,19 @@ class DeviceListItem extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Add spinner if connecting (Add this block)
|
||||||
|
if (isConnecting)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 12.0), // Add some spacing
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20, // Define spinner size
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.0), // Use a small spinner
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (trailing != null)
|
||||||
|
trailing!,
|
||||||
// Optional: Add an icon or button on the far right if needed later
|
// Optional: Add an icon or button on the far right if needed later
|
||||||
// Icon(Icons.chevron_right, color: theme.hintColor),
|
// Icon(Icons.chevron_right, color: theme.hintColor),
|
||||||
],
|
],
|
||||||
|
|||||||
1278
lib/widgets/gear_ratio_editor_card.dart
Normal file
1278
lib/widgets/gear_ratio_editor_card.dart
Normal file
File diff suppressed because it is too large
Load Diff
207
lib/widgets/horizontal_scanning_animation.dart
Normal file
207
lib/widgets/horizontal_scanning_animation.dart
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class HorizontalScanningAnimation extends StatefulWidget {
|
||||||
|
final bool isScanning; // Add this to control the animation
|
||||||
|
final Color waveColor;
|
||||||
|
final double height;
|
||||||
|
const HorizontalScanningAnimation({
|
||||||
|
super.key,
|
||||||
|
required this.isScanning, // Make it required
|
||||||
|
this.waveColor = Colors.lightBlueAccent,
|
||||||
|
this.height = 50.0,
|
||||||
|
});
|
||||||
|
@override
|
||||||
|
_HorizontalScanningAnimationState createState() =>
|
||||||
|
_HorizontalScanningAnimationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HorizontalScanningAnimationState
|
||||||
|
extends State<HorizontalScanningAnimation>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
// Start repeating only if initially scanning
|
||||||
|
if (widget.isScanning) {
|
||||||
|
_controller.repeat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant HorizontalScanningAnimation oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.isScanning != oldWidget.isScanning) {
|
||||||
|
if (widget.isScanning) {
|
||||||
|
// Start or resume repeating
|
||||||
|
if (!_controller.isAnimating) {
|
||||||
|
// If stopped previously, reset before repeating for a clean start
|
||||||
|
// Though, repeat() should handle restarting if stopped. Testing needed.
|
||||||
|
// _controller.reset(); // Optional: uncomment if repeat doesn't restart smoothly
|
||||||
|
_controller.repeat();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Stop repeating, but let the current animation cycle finish visually
|
||||||
|
if (_controller.isAnimating) {
|
||||||
|
_controller.stop(
|
||||||
|
canceled:
|
||||||
|
false); // Use canceled: false to let it finish the current tick
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Only build the painter if the controller is active or was recently stopped
|
||||||
|
// This prevents drawing when completely idle. Check if value is changing or non-zero.
|
||||||
|
// Or simply rely on the AnimatedBuilder which won't rebuild if controller is idle at 0.0
|
||||||
|
return SizedBox(
|
||||||
|
height: widget.height,
|
||||||
|
width: double.infinity,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
return CustomPaint(
|
||||||
|
painter: _HorizontalWavePainter(
|
||||||
|
progress: _controller.value,
|
||||||
|
waveColor: widget.waveColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HorizontalWavePainter extends CustomPainter {
|
||||||
|
final double progress; // Animation value from 0.0 to 1.0
|
||||||
|
final Color waveColor;
|
||||||
|
final int waveCount = 2; // Number of waves visible at once
|
||||||
|
final double waveAmplitude = 10.0; // Max height deviation of the wave
|
||||||
|
|
||||||
|
_HorizontalWavePainter({required this.progress, required this.waveColor});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()
|
||||||
|
..color = waveColor.withValues(alpha: 0.6) // Semi-transparent waves
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
final centerY = size.height / 2;
|
||||||
|
final width = size.width;
|
||||||
|
|
||||||
|
// Draw multiple waves propagating outwards
|
||||||
|
for (int i = 0; i < waveCount; i++) {
|
||||||
|
// Calculate the phase offset for each wave based on progress and index
|
||||||
|
// This creates the effect of waves moving outwards
|
||||||
|
double waveProgress = (progress + i / waveCount) % 1.0;
|
||||||
|
|
||||||
|
// Use an easing curve for smoother expansion
|
||||||
|
double easedProgress = Curves.easeInOutSine.transform(waveProgress);
|
||||||
|
|
||||||
|
// Calculate the current horizontal position (expanding from center)
|
||||||
|
// The wave starts narrow and expands outwards
|
||||||
|
double currentWidth =
|
||||||
|
width * easedProgress * 0.8; // Max 80% width expansion
|
||||||
|
double startX = (width / 2) - (currentWidth / 2);
|
||||||
|
double endX = (width / 2) + (currentWidth / 2);
|
||||||
|
|
||||||
|
// Calculate opacity based on progress (fade in and out)
|
||||||
|
double opacity;
|
||||||
|
if (waveProgress < 0.1) {
|
||||||
|
opacity = waveProgress / 0.1; // Fade in
|
||||||
|
} else if (waveProgress > 0.8) {
|
||||||
|
opacity = (1.0 - waveProgress) / 0.2; // Fade out
|
||||||
|
} else {
|
||||||
|
opacity = 1.0;
|
||||||
|
}
|
||||||
|
opacity = max(0.0, opacity); // Clamp opacity
|
||||||
|
|
||||||
|
if (opacity <= 0.0 || currentWidth < 5)
|
||||||
|
continue; // Skip drawing if invisible or too small
|
||||||
|
|
||||||
|
// Create the wave path
|
||||||
|
final path = Path();
|
||||||
|
path.moveTo(startX, centerY);
|
||||||
|
|
||||||
|
// Calculate points for the sine wave shape within the current width
|
||||||
|
const int segments = 50; // Number of segments for the curve
|
||||||
|
for (int j = 0; j <= segments; j++) {
|
||||||
|
double segmentProgress = j / segments;
|
||||||
|
double x = startX + currentWidth * segmentProgress;
|
||||||
|
// Apply sine wave based on segment progress and overall animation progress
|
||||||
|
// Multiply by (1 - easedProgress) to reduce amplitude as it expands
|
||||||
|
double yOffset = waveAmplitude *
|
||||||
|
sin(segmentProgress * 2 * pi + progress * 4 * pi) *
|
||||||
|
(1 - easedProgress * 0.8) * // Reduce amplitude as it expands
|
||||||
|
opacity; // Apply opacity effect to amplitude too
|
||||||
|
path.lineTo(x, centerY + yOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a filled shape (like a lens flare or horizontal bar)
|
||||||
|
// Adjust thickness based on easedProgress (thicker in the middle, thinner at ends)
|
||||||
|
double thickness =
|
||||||
|
waveAmplitude * (1 - easedProgress * 0.9) * opacity * 0.5;
|
||||||
|
paint.color = waveColor.withValues(
|
||||||
|
alpha: opacity * 0.5); // Update paint color with opacity
|
||||||
|
|
||||||
|
// Simplified: Draw a rectangle that pulses
|
||||||
|
// More complex shapes could be drawn here using path.arcTo or path.quadraticBezierTo
|
||||||
|
// For simplicity, let's use a slightly blurred rectangle effect
|
||||||
|
|
||||||
|
final rectPath = Path()
|
||||||
|
..addRRect(RRect.fromRectAndRadius(
|
||||||
|
Rect.fromCenter(
|
||||||
|
center: Offset(width / 2, centerY),
|
||||||
|
width: currentWidth,
|
||||||
|
height: thickness * 2),
|
||||||
|
Radius.circular(thickness)));
|
||||||
|
|
||||||
|
// Apply a blur effect
|
||||||
|
final blurPaint = Paint()
|
||||||
|
..color = waveColor.withValues(alpha: opacity * 0.4)
|
||||||
|
..maskFilter = MaskFilter.blur(
|
||||||
|
BlurStyle.normal, thickness * 1.5); // Blur based on thickness
|
||||||
|
|
||||||
|
// Draw the blurred shape
|
||||||
|
canvas.drawPath(rectPath, blurPaint);
|
||||||
|
|
||||||
|
// Draw a slightly smaller, less opaque shape on top for highlight
|
||||||
|
final highlightPaint = Paint()
|
||||||
|
..color = waveColor.withValues(alpha: opacity * 0.7)
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
final highlightRectPath = Path()
|
||||||
|
..addRRect(RRect.fromRectAndRadius(
|
||||||
|
Rect.fromCenter(
|
||||||
|
center: Offset(width / 2, centerY),
|
||||||
|
width: currentWidth * 0.95,
|
||||||
|
height: thickness * 1.5),
|
||||||
|
Radius.circular(thickness * 0.8)));
|
||||||
|
canvas.drawPath(highlightRectPath, highlightPaint);
|
||||||
|
|
||||||
|
// Old Path drawing - keep if rectangle isn't desired
|
||||||
|
// paint.color = waveColor.withValues(alpha: opacity * 0.5); // Apply opacity
|
||||||
|
// canvas.drawPath(path, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _HorizontalWavePainter oldDelegate) {
|
||||||
|
// Repaint whenever the animation progress or color changes
|
||||||
|
return oldDelegate.progress != progress ||
|
||||||
|
oldDelegate.waveColor != waveColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,14 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <nb_utils/nb_utils_plugin.h>
|
||||||
|
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) nb_utils_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "NbUtilsPlugin");
|
||||||
|
nb_utils_plugin_register_with_registrar(nb_utils_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
|
||||||
|
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,12 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
nb_utils
|
||||||
|
sqlite3_flutter_libs
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
rust_lib_abawo_bt_app
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
@ -5,10 +5,22 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import connectivity_plus
|
||||||
|
import file_picker
|
||||||
import flutter_blue_plus_darwin
|
import flutter_blue_plus_darwin
|
||||||
|
import nb_utils
|
||||||
|
import path_provider_foundation
|
||||||
|
import reactive_ble_mobile
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
import sqlite3_flutter_libs
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||||
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
|
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
|
||||||
|
NbUtilsPlugin.register(with: registry.registrar(forPlugin: "NbUtilsPlugin"))
|
||||||
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
|
ReactiveBlePlugin.register(with: registry.registrar(forPlugin: "ReactiveBlePlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
384
pubspec.lock
384
pubspec.lock
@ -73,6 +73,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2"
|
version: "2.4.2"
|
||||||
|
build_cli_annotations:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build_cli_annotations
|
||||||
|
sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
build_config:
|
build_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -129,14 +137,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.9.5"
|
version: "8.9.5"
|
||||||
|
cbor:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cbor
|
||||||
|
sha256: "2c5c37650f0a2d25149f03e748ab7b2857787bde338f95fe947738b80d713da2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.1"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.4.1"
|
||||||
|
charcode:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: charcode
|
||||||
|
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -165,10 +189,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: clock
|
name: clock
|
||||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.2"
|
||||||
code_builder:
|
code_builder:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -181,10 +205,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.0"
|
version: "1.19.1"
|
||||||
|
connectivity_plus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: connectivity_plus
|
||||||
|
sha256: "33bae12a398f841c6cda09d1064212957265869104c478e5ad51e2fb26c3973c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
|
connectivity_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: connectivity_plus_platform_interface
|
||||||
|
sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.1"
|
||||||
convert:
|
convert:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -193,6 +233,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5+2"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -257,14 +305,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.11"
|
version: "0.7.11"
|
||||||
|
drift:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: drift
|
||||||
|
sha256: "14a61af39d4584faf1d73b5b35e4b758a43008cf4c0fdb0576ec8e7032c0d9a5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.26.0"
|
||||||
|
drift_dev:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: drift_dev
|
||||||
|
sha256: "0d3f8b33b76cf1c6a82ee34d9511c40957549c4674b8f1688609e6d6c7306588"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.26.0"
|
||||||
|
drift_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: drift_flutter
|
||||||
|
sha256: "0cadbf3b8733409a6cf61d18ba2e94e149df81df7de26f48ae0695b48fd71922"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.4"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: fake_async
|
name: fake_async
|
||||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.3"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -281,6 +353,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
file_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_picker
|
||||||
|
sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.3.7"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -298,50 +378,63 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_blue_plus
|
name: flutter_blue_plus
|
||||||
sha256: "2d926dbef0fd6c58d4be8fca9eaaf1ba747c0ccb8373ddd5386665317e26eb61"
|
sha256: "399b3dbc15562ef59749f04e43a99ccbb91540022380d5f269aff3c2787534e4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.35.3"
|
version: "2.1.0"
|
||||||
flutter_blue_plus_android:
|
flutter_blue_plus_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_blue_plus_android
|
name: flutter_blue_plus_android
|
||||||
sha256: c1d83f84b514e46345a8a58599c428f20b11e78379521e0d3b0611c7b7cbf2c1
|
sha256: "5010b0960cce533a8fa71401573f044362c3e2e111dc6eb4898c92e85f85f50c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "8.1.0"
|
||||||
flutter_blue_plus_darwin:
|
flutter_blue_plus_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_blue_plus_darwin
|
name: flutter_blue_plus_darwin
|
||||||
sha256: "8d0a0f11f83b13dda173396b7e4028b4e8656bc8dbbc82c26a7e49aafc62644b"
|
sha256: d160a8128e3a016fa58dd65ab6dac05cbc73e0fa799a1f24211d041641ed63ba
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "8.1.0"
|
||||||
flutter_blue_plus_linux:
|
flutter_blue_plus_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_blue_plus_linux
|
name: flutter_blue_plus_linux
|
||||||
sha256: "1d367ed378b2bd6c3b9685fda7044e1d2f169884802b7dec7badb31a99a72660"
|
sha256: f5b02244d89465ba82c8c512686c66362fbb01f52fa03d645ed353ebf3883242
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "8.1.0"
|
||||||
flutter_blue_plus_platform_interface:
|
flutter_blue_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_blue_plus_platform_interface
|
name: flutter_blue_plus_platform_interface
|
||||||
sha256: "114f8e85a03a28a48d707a4df6cc9218e1f2005cf260c5e815e5585a00da5778"
|
sha256: "6e0fc04b77491dbfdbcd46c1a021b12f2f5fc5d6e01777f93a38a8431989b7f0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "8.1.0"
|
||||||
flutter_blue_plus_web:
|
flutter_blue_plus_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_blue_plus_web
|
name: flutter_blue_plus_web
|
||||||
sha256: db70cdc41bc743763dc0d47e8c7c10f3923cbbe71b33d9dc21deea482affeb4d
|
sha256: "376aad9595ee389c7cd56e0c373e78abcaa790c821ece9cb81f0969ec94c5bca"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "8.1.0"
|
||||||
|
flutter_blue_plus_winrt:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_blue_plus_winrt
|
||||||
|
sha256: "34be2d8e23d5881b46accebb0e71025f7d52869d72ea98b5082c20764e06aa80"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.16"
|
||||||
|
flutter_driver:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -350,6 +443,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "5.0.0"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.33"
|
||||||
|
flutter_reactive_ble:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_reactive_ble
|
||||||
|
sha256: "3ca8430bc7a6cabe5529aab4afaa2e3cb285941f7f7ab7472604074e347c1302"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.4.0"
|
||||||
flutter_riverpod:
|
flutter_riverpod:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -358,6 +467,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.1"
|
version: "2.6.1"
|
||||||
|
flutter_rust_bridge:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_rust_bridge
|
||||||
|
sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.1"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -368,6 +485,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
fluttertoast:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fluttertoast
|
||||||
|
sha256: "144ddd74d49c865eba47abe31cbc746c7b311c82d6c32e571fd73c4264b740e2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.0.0"
|
||||||
freezed:
|
freezed:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -392,6 +517,19 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
|
fuchsia_remote_debug_protocol:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
functional_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: functional_data
|
||||||
|
sha256: "76d17dc707c40e552014f5a49c0afcc3f1e3f05e800cd6b7872940bfe41a5039"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
glob:
|
glob:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -416,6 +554,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "2.3.2"
|
||||||
|
hex:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hex
|
||||||
|
sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
hotreloader:
|
hotreloader:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -428,10 +574,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.6.0"
|
||||||
http_multi_server:
|
http_multi_server:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -448,6 +594,11 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
integration_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -484,26 +635,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.7"
|
version: "11.0.2"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.8"
|
version: "3.0.10"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_testing
|
name: leak_tracker_testing
|
||||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.2"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -524,26 +675,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.16+1"
|
version: "0.12.18"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.0"
|
version: "1.17.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -552,6 +703,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
nb_utils:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: nb_utils
|
||||||
|
sha256: "2cfecbe67bc09ab31069cfe68e4eb232bc71f0670c98a91d0e73e7741a4798ed"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.2.0"
|
||||||
|
nm:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: nm
|
||||||
|
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.0"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -564,10 +731,34 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.1"
|
||||||
|
path_provider:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.5"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.16"
|
||||||
|
path_provider_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_foundation
|
||||||
|
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -624,6 +815,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.1"
|
version: "1.5.1"
|
||||||
|
process:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: process
|
||||||
|
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.5"
|
||||||
|
protobuf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: protobuf
|
||||||
|
sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -640,6 +847,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "1.5.0"
|
||||||
|
reactive_ble_mobile:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: reactive_ble_mobile
|
||||||
|
sha256: "78af92cfb184770a277a8bdaa76b396a28dbada8931f3d8d0ff552414681f5db"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.4.0"
|
||||||
|
reactive_ble_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: reactive_ble_platform_interface
|
||||||
|
sha256: d0e6f86c7ee3865b74d8a7b6deb1fb8320b24176dfd9a3e475f84b7117eb42c7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.4.0"
|
||||||
|
recase:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: recase
|
||||||
|
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.0"
|
||||||
riverpod:
|
riverpod:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -688,8 +919,15 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.0"
|
||||||
|
rust_lib_abawo_bt_app:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: rust_builder
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "0.0.1"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: rxdart
|
name: rxdart
|
||||||
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||||
@ -700,10 +938,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a"
|
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.2"
|
version: "2.5.4"
|
||||||
shared_preferences_android:
|
shared_preferences_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -805,14 +1043,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.0"
|
version: "7.0.0"
|
||||||
|
sqlite3:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqlite3
|
||||||
|
sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.5"
|
||||||
|
sqlite3_flutter_libs:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqlite3_flutter_libs
|
||||||
|
sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.32"
|
||||||
|
sqlparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqlparser
|
||||||
|
sha256: "27dd0a9f0c02e22ac0eb42a23df9ea079ce69b52bb4a3b478d64e0ef34a263ee"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.41.0"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.0"
|
version: "1.12.1"
|
||||||
state_notifier:
|
state_notifier:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -825,10 +1087,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stream_channel
|
name: stream_channel
|
||||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.4"
|
||||||
stream_transform:
|
stream_transform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -845,6 +1107,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
sync_http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sync_http
|
||||||
|
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.1"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -857,10 +1127,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.3"
|
version: "0.7.9"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -889,10 +1159,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.2.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -933,6 +1203,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.2"
|
||||||
|
webdriver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webdriver
|
||||||
|
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.13.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -958,5 +1244,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.6.1 <4.0.0"
|
dart: ">=3.9.0 <4.0.0"
|
||||||
flutter: ">=3.27.0"
|
flutter: ">=3.35.0"
|
||||||
|
|||||||
18
pubspec.yaml
18
pubspec.yaml
@ -39,11 +39,22 @@ dependencies:
|
|||||||
go_router: ^14.8.1
|
go_router: ^14.8.1
|
||||||
freezed_annotation: ^3.0.0
|
freezed_annotation: ^3.0.0
|
||||||
json_annotation: ^4.9.0
|
json_annotation: ^4.9.0
|
||||||
flutter_blue_plus: ^1.35.3
|
flutter_blue_plus: ^2.1.0
|
||||||
rust: ^3.1.0
|
rust: ^3.1.0
|
||||||
anyhow: ^3.0.1
|
anyhow: ^3.0.1
|
||||||
logging: ^1.3.0
|
logging: ^1.3.0
|
||||||
shared_preferences: ^2.5.2
|
shared_preferences: ^2.5.2
|
||||||
|
drift: ^2.26.0
|
||||||
|
drift_flutter: ^0.2.4
|
||||||
|
path_provider: ^2.1.5
|
||||||
|
rxdart: ^0.28.0
|
||||||
|
rust_lib_abawo_bt_app:
|
||||||
|
path: rust_builder
|
||||||
|
flutter_rust_bridge: 2.11.1
|
||||||
|
flutter_reactive_ble: ^5.4.0
|
||||||
|
nb_utils: ^7.2.0
|
||||||
|
cbor: ^6.3.3
|
||||||
|
file_picker: ^8.1.7
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -61,6 +72,9 @@ dev_dependencies:
|
|||||||
riverpod_lint: ^2.6.5
|
riverpod_lint: ^2.6.5
|
||||||
freezed: ^3.0.4
|
freezed: ^3.0.4
|
||||||
json_serializable: ^6.9.4
|
json_serializable: ^6.9.4
|
||||||
|
drift_dev: ^2.26.0
|
||||||
|
integration_test:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
@ -77,6 +91,8 @@ flutter:
|
|||||||
# assets:
|
# assets:
|
||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
|
assets:
|
||||||
|
- assets/images/shifter-wireframe.png
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
# https://flutter.dev/to/resolution-aware-images
|
||||||
|
|||||||
1
rust/.gitignore
vendored
Normal file
1
rust/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
729
rust/Cargo.lock
generated
Normal file
729
rust/Cargo.lock
generated
Normal file
@ -0,0 +1,729 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "addr2line"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
|
||||||
|
dependencies = [
|
||||||
|
"gimli",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allo-isolate"
|
||||||
|
version = "0.1.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "449e356a4864c017286dbbec0e12767ea07efba29e3b7d984194c2a7ff3c4550"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"atomic",
|
||||||
|
"backtrace",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_log-sys"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_logger"
|
||||||
|
version = "0.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3"
|
||||||
|
dependencies = [
|
||||||
|
"android_log-sys",
|
||||||
|
"env_filter",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.75"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "backtrace"
|
||||||
|
version = "0.3.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
|
||||||
|
dependencies = [
|
||||||
|
"addr2line",
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"miniz_oxide",
|
||||||
|
"object",
|
||||||
|
"rustc-demangle",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "build-target"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.0.83"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console_error_panic_hook"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dart-sys"
|
||||||
|
version = "4.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57967e4b200d767d091b961d6ab42cc7d0cc14fe9e052e75d0d3cf9eb732d895"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dashmap"
|
||||||
|
version = "5.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"hashbrown",
|
||||||
|
"lock_api",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "delegate-attr"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51aac4c99b2e6775164b412ea33ae8441b2fde2dbf05a20bc0052a63d08c475b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_filter"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flutter_rust_bridge"
|
||||||
|
version = "2.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dde126295b2acc5f0a712e265e91b6fdc0ed38767496483e592ae7134db83725"
|
||||||
|
dependencies = [
|
||||||
|
"allo-isolate",
|
||||||
|
"android_logger",
|
||||||
|
"anyhow",
|
||||||
|
"build-target",
|
||||||
|
"bytemuck",
|
||||||
|
"byteorder",
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"dart-sys",
|
||||||
|
"delegate-attr",
|
||||||
|
"flutter_rust_bridge_macros",
|
||||||
|
"futures",
|
||||||
|
"js-sys",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"oslog",
|
||||||
|
"portable-atomic",
|
||||||
|
"threadpool",
|
||||||
|
"tokio",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flutter_rust_bridge_macros"
|
||||||
|
version = "2.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d5f0420326b13675321b194928bb7830043b68cf8b810e1c651285c747abb080"
|
||||||
|
dependencies = [
|
||||||
|
"hex",
|
||||||
|
"md-5",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-channel"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-executor"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-io"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-macro"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-sink"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"futures-macro",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"memchr",
|
||||||
|
"pin-project-lite",
|
||||||
|
"pin-utils",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gimli"
|
||||||
|
version = "0.28.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
|
||||||
|
dependencies = [
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.150"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "md-5"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
|
||||||
|
dependencies = [
|
||||||
|
"adler",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_cpus"
|
||||||
|
version = "1.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "object"
|
||||||
|
version = "0.32.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oslog"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"dashmap",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.70"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust_lib_abawo_bt_app"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"flutter_rust_bridge",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-demangle"
|
||||||
|
version = "0.1.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.39"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "threadpool"
|
||||||
|
version = "1.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
|
||||||
|
dependencies = [
|
||||||
|
"num_cpus",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.34.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
|
||||||
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
|
"num_cpus",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-backend"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-futures"
|
||||||
|
version = "0.4.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-backend",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-sys"
|
||||||
|
version = "0.3.66"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
13
rust/Cargo.toml
Normal file
13
rust/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "rust_lib_abawo_bt_app"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "staticlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
flutter_rust_bridge = "=2.11.1"
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] }
|
||||||
1
rust/src/api/mod.rs
Normal file
1
rust/src/api/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod simple;
|
||||||
12
rust/src/api/simple.rs
Normal file
12
rust/src/api/simple.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#[flutter_rust_bridge::frb(sync)] // Synchronous mode for simplicity of the demo
|
||||||
|
pub fn greet(name: String) -> String {
|
||||||
|
format!("Hello, {name}!")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[flutter_rust_bridge::frb(init)]
|
||||||
|
pub fn init_app() {
|
||||||
|
// Default utilities - feel free to customize
|
||||||
|
flutter_rust_bridge::setup_default_user_utils();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
276
rust/src/frb_generated.rs
Normal file
276
rust/src/frb_generated.rs
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||||
|
|
||||||
|
#![allow(
|
||||||
|
non_camel_case_types,
|
||||||
|
unused,
|
||||||
|
non_snake_case,
|
||||||
|
clippy::needless_return,
|
||||||
|
clippy::redundant_closure_call,
|
||||||
|
clippy::redundant_closure,
|
||||||
|
clippy::useless_conversion,
|
||||||
|
clippy::unit_arg,
|
||||||
|
clippy::unused_unit,
|
||||||
|
clippy::double_parens,
|
||||||
|
clippy::let_and_return,
|
||||||
|
clippy::too_many_arguments,
|
||||||
|
clippy::match_single_binding,
|
||||||
|
clippy::clone_on_copy,
|
||||||
|
clippy::let_unit_value,
|
||||||
|
clippy::deref_addrof,
|
||||||
|
clippy::explicit_auto_deref,
|
||||||
|
clippy::borrow_deref_ref,
|
||||||
|
clippy::needless_borrow
|
||||||
|
)]
|
||||||
|
|
||||||
|
// Section: imports
|
||||||
|
|
||||||
|
use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
|
||||||
|
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
||||||
|
|
||||||
|
// Section: boilerplate
|
||||||
|
|
||||||
|
flutter_rust_bridge::frb_generated_boilerplate!(
|
||||||
|
default_stream_sink_codec = SseCodec,
|
||||||
|
default_rust_opaque = RustOpaqueMoi,
|
||||||
|
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||||
|
);
|
||||||
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1";
|
||||||
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1918914929;
|
||||||
|
|
||||||
|
// Section: executor
|
||||||
|
|
||||||
|
flutter_rust_bridge::frb_generated_default_handler!();
|
||||||
|
|
||||||
|
// Section: wire_funcs
|
||||||
|
|
||||||
|
fn wire__crate__api__simple__greet_impl(
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "greet",
|
||||||
|
port: None,
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_name = <String>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
transform_result_sse::<_, ()>((move || {
|
||||||
|
let output_ok = Result::<_, ()>::Ok(crate::api::simple::greet(api_name))?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn wire__crate__api__simple__init_app_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "init_app",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, ()>((move || {
|
||||||
|
let output_ok = Result::<_, ()>::Ok({
|
||||||
|
crate::api::simple::init_app();
|
||||||
|
})?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section: dart2rust
|
||||||
|
|
||||||
|
impl SseDecode for String {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
let mut inner = <Vec<u8>>::sse_decode(deserializer);
|
||||||
|
return String::from_utf8(inner).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseDecode for Vec<u8> {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
let mut len_ = <i32>::sse_decode(deserializer);
|
||||||
|
let mut ans_ = vec![];
|
||||||
|
for idx_ in 0..len_ {
|
||||||
|
ans_.push(<u8>::sse_decode(deserializer));
|
||||||
|
}
|
||||||
|
return ans_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseDecode for u8 {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
deserializer.cursor.read_u8().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseDecode for () {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseDecode for i32 {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
deserializer.cursor.read_i32::<NativeEndian>().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseDecode for bool {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
deserializer.cursor.read_u8().unwrap() != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pde_ffi_dispatcher_primary_impl(
|
||||||
|
func_id: i32,
|
||||||
|
port: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len: i32,
|
||||||
|
data_len: i32,
|
||||||
|
) {
|
||||||
|
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||||
|
match func_id {
|
||||||
|
2 => wire__crate__api__simple__init_app_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pde_ffi_dispatcher_sync_impl(
|
||||||
|
func_id: i32,
|
||||||
|
ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len: i32,
|
||||||
|
data_len: i32,
|
||||||
|
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
||||||
|
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||||
|
match func_id {
|
||||||
|
1 => wire__crate__api__simple__greet_impl(ptr, rust_vec_len, data_len),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section: rust2dart
|
||||||
|
|
||||||
|
impl SseEncode for String {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
<Vec<u8>>::sse_encode(self.into_bytes(), serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseEncode for Vec<u8> {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
<i32>::sse_encode(self.len() as _, serializer);
|
||||||
|
for item in self {
|
||||||
|
<u8>::sse_encode(item, serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseEncode for u8 {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
serializer.cursor.write_u8(self).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseEncode for () {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseEncode for i32 {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
serializer.cursor.write_i32::<NativeEndian>(self).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseEncode for bool {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
serializer.cursor.write_u8(self as _).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
mod io {
|
||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||||
|
|
||||||
|
// Section: imports
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use flutter_rust_bridge::for_generated::byteorder::{
|
||||||
|
NativeEndian, ReadBytesExt, WriteBytesExt,
|
||||||
|
};
|
||||||
|
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
|
||||||
|
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
||||||
|
|
||||||
|
// Section: boilerplate
|
||||||
|
|
||||||
|
flutter_rust_bridge::frb_generated_boilerplate_io!();
|
||||||
|
}
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
pub use io::*;
|
||||||
|
|
||||||
|
/// cbindgen:ignore
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
mod web {
|
||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||||
|
|
||||||
|
// Section: imports
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use flutter_rust_bridge::for_generated::byteorder::{
|
||||||
|
NativeEndian, ReadBytesExt, WriteBytesExt,
|
||||||
|
};
|
||||||
|
use flutter_rust_bridge::for_generated::wasm_bindgen;
|
||||||
|
use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*;
|
||||||
|
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
|
||||||
|
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
||||||
|
|
||||||
|
// Section: boilerplate
|
||||||
|
|
||||||
|
flutter_rust_bridge::frb_generated_boilerplate_web!();
|
||||||
|
}
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
pub use web::*;
|
||||||
2
rust/src/lib.rs
Normal file
2
rust/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod api;
|
||||||
|
mod frb_generated;
|
||||||
29
rust_builder/.gitignore
vendored
Normal file
29
rust_builder/.gitignore
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||||
|
/pubspec.lock
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
build/
|
||||||
1
rust_builder/README.md
Normal file
1
rust_builder/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
Please ignore this folder, which is just glue to build Rust with Flutter.
|
||||||
9
rust_builder/android/.gitignore
vendored
Normal file
9
rust_builder/android/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/libraries
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.cxx
|
||||||
56
rust_builder/android/build.gradle
Normal file
56
rust_builder/android/build.gradle
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// The Android Gradle Plugin builds the native code with the Android NDK.
|
||||||
|
|
||||||
|
group 'com.flutter_rust_bridge.rust_lib_abawo_bt_app'
|
||||||
|
version '1.0'
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// The Android Gradle Plugin knows how to build native code with the NDK.
|
||||||
|
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
android {
|
||||||
|
if (project.android.hasProperty("namespace")) {
|
||||||
|
namespace 'com.flutter_rust_bridge.rust_lib_abawo_bt_app'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bumping the plugin compileSdkVersion requires all clients of this plugin
|
||||||
|
// to bump the version in their app.
|
||||||
|
compileSdkVersion 33
|
||||||
|
|
||||||
|
// Use the NDK version
|
||||||
|
// declared in /android/app/build.gradle file of the Flutter project.
|
||||||
|
// Replace it with a version number if this plugin requires a specfic NDK version.
|
||||||
|
// (e.g. ndkVersion "23.1.7779620")
|
||||||
|
ndkVersion android.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 19
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "../cargokit/gradle/plugin.gradle"
|
||||||
|
cargokit {
|
||||||
|
manifestDir = "../../rust"
|
||||||
|
libname = "rust_lib_abawo_bt_app"
|
||||||
|
}
|
||||||
1
rust_builder/android/settings.gradle
Normal file
1
rust_builder/android/settings.gradle
Normal file
@ -0,0 +1 @@
|
|||||||
|
rootProject.name = 'rust_lib_abawo_bt_app'
|
||||||
3
rust_builder/android/src/main/AndroidManifest.xml
Normal file
3
rust_builder/android/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.flutter_rust_bridge.rust_lib_abawo_bt_app">
|
||||||
|
</manifest>
|
||||||
4
rust_builder/cargokit/.gitignore
vendored
Normal file
4
rust_builder/cargokit/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
target
|
||||||
|
.dart_tool
|
||||||
|
*.iml
|
||||||
|
!pubspec.lock
|
||||||
42
rust_builder/cargokit/LICENSE
Normal file
42
rust_builder/cargokit/LICENSE
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
Copyright 2022 Matej Knopp
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
MIT LICENSE
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||||
|
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||||
|
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
APACHE LICENSE, VERSION 2.0
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
11
rust_builder/cargokit/README
Normal file
11
rust_builder/cargokit/README
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
Experimental repository to provide glue for seamlessly integrating cargo build
|
||||||
|
with flutter plugins and packages.
|
||||||
|
|
||||||
|
See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/
|
||||||
|
for a tutorial on how to use Cargokit.
|
||||||
|
|
||||||
|
Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin.
|
||||||
|
|
||||||
58
rust_builder/cargokit/build_pod.sh
Executable file
58
rust_builder/cargokit/build_pod.sh
Executable file
@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
BASEDIR=$(dirname "$0")
|
||||||
|
|
||||||
|
# Workaround for https://github.com/dart-lang/pub/issues/4010
|
||||||
|
BASEDIR=$(cd "$BASEDIR" ; pwd -P)
|
||||||
|
|
||||||
|
# Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project
|
||||||
|
NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"`
|
||||||
|
|
||||||
|
export PATH=${NEW_PATH%?} # remove trailing :
|
||||||
|
|
||||||
|
env
|
||||||
|
|
||||||
|
# Platform name (macosx, iphoneos, iphonesimulator)
|
||||||
|
export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME
|
||||||
|
|
||||||
|
# Arctive architectures (arm64, armv7, x86_64), space separated.
|
||||||
|
export CARGOKIT_DARWIN_ARCHS=$ARCHS
|
||||||
|
|
||||||
|
# Current build configuration (Debug, Release)
|
||||||
|
export CARGOKIT_CONFIGURATION=$CONFIGURATION
|
||||||
|
|
||||||
|
# Path to directory containing Cargo.toml.
|
||||||
|
export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1
|
||||||
|
|
||||||
|
# Temporary directory for build artifacts.
|
||||||
|
export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR
|
||||||
|
|
||||||
|
# Output directory for final artifacts.
|
||||||
|
export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME
|
||||||
|
|
||||||
|
# Directory to store built tool artifacts.
|
||||||
|
export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool
|
||||||
|
|
||||||
|
# Directory inside root project. Not necessarily the top level directory of root project.
|
||||||
|
export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT
|
||||||
|
|
||||||
|
FLUTTER_EXPORT_BUILD_ENVIRONMENT=(
|
||||||
|
"$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS
|
||||||
|
"$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS
|
||||||
|
)
|
||||||
|
|
||||||
|
for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}"
|
||||||
|
do
|
||||||
|
if [[ -f "$path" ]]; then
|
||||||
|
source "$path"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
sh "$BASEDIR/run_build_tool.sh" build-pod "$@"
|
||||||
|
|
||||||
|
# Make a symlink from built framework to phony file, which will be used as input to
|
||||||
|
# build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate
|
||||||
|
# attribute on custom build phase)
|
||||||
|
ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony"
|
||||||
|
ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out"
|
||||||
5
rust_builder/cargokit/build_tool/README.md
Normal file
5
rust_builder/cargokit/build_tool/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
A sample command-line application with an entrypoint in `bin/`, library code
|
||||||
|
in `lib/`, and example unit test in `test/`.
|
||||||
34
rust_builder/cargokit/build_tool/analysis_options.yaml
Normal file
34
rust_builder/cargokit/build_tool/analysis_options.yaml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
# This file configures the static analysis results for your project (errors,
|
||||||
|
# warnings, and lints).
|
||||||
|
#
|
||||||
|
# This enables the 'recommended' set of lints from `package:lints`.
|
||||||
|
# This set helps identify many issues that may lead to problems when running
|
||||||
|
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||||
|
# style and format.
|
||||||
|
#
|
||||||
|
# If you want a smaller set of lints you can change this to specify
|
||||||
|
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||||
|
# (the recommended set includes the core lints).
|
||||||
|
# The core lints are also what is used by pub.dev for scoring packages.
|
||||||
|
|
||||||
|
include: package:lints/recommended.yaml
|
||||||
|
|
||||||
|
# Uncomment the following section to specify additional rules.
|
||||||
|
|
||||||
|
linter:
|
||||||
|
rules:
|
||||||
|
- prefer_relative_imports
|
||||||
|
- directives_ordering
|
||||||
|
|
||||||
|
# analyzer:
|
||||||
|
# exclude:
|
||||||
|
# - path/to/excluded/files/**
|
||||||
|
|
||||||
|
# For more information about the core and recommended set of lints, see
|
||||||
|
# https://dart.dev/go/core-lints
|
||||||
|
|
||||||
|
# For additional information about configuring this file, see
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
8
rust_builder/cargokit/build_tool/bin/build_tool.dart
Normal file
8
rust_builder/cargokit/build_tool/bin/build_tool.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'package:build_tool/build_tool.dart' as build_tool;
|
||||||
|
|
||||||
|
void main(List<String> arguments) {
|
||||||
|
build_tool.runMain(arguments);
|
||||||
|
}
|
||||||
8
rust_builder/cargokit/build_tool/lib/build_tool.dart
Normal file
8
rust_builder/cargokit/build_tool/lib/build_tool.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'src/build_tool.dart' as build_tool;
|
||||||
|
|
||||||
|
Future<void> runMain(List<String> args) async {
|
||||||
|
return build_tool.runMain(args);
|
||||||
|
}
|
||||||
@ -0,0 +1,195 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:version/version.dart';
|
||||||
|
|
||||||
|
import 'target.dart';
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
class AndroidEnvironment {
|
||||||
|
AndroidEnvironment({
|
||||||
|
required this.sdkPath,
|
||||||
|
required this.ndkVersion,
|
||||||
|
required this.minSdkVersion,
|
||||||
|
required this.targetTempDir,
|
||||||
|
required this.target,
|
||||||
|
});
|
||||||
|
|
||||||
|
static void clangLinkerWrapper(List<String> args) {
|
||||||
|
final clang = Platform.environment['_CARGOKIT_NDK_LINK_CLANG'];
|
||||||
|
if (clang == null) {
|
||||||
|
throw Exception(
|
||||||
|
"cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_CLANG env var");
|
||||||
|
}
|
||||||
|
final target = Platform.environment['_CARGOKIT_NDK_LINK_TARGET'];
|
||||||
|
if (target == null) {
|
||||||
|
throw Exception(
|
||||||
|
"cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_TARGET env var");
|
||||||
|
}
|
||||||
|
|
||||||
|
runCommand(clang, [
|
||||||
|
target,
|
||||||
|
...args,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full path to Android SDK.
|
||||||
|
final String sdkPath;
|
||||||
|
|
||||||
|
/// Full version of Android NDK.
|
||||||
|
final String ndkVersion;
|
||||||
|
|
||||||
|
/// Minimum supported SDK version.
|
||||||
|
final int minSdkVersion;
|
||||||
|
|
||||||
|
/// Target directory for build artifacts.
|
||||||
|
final String targetTempDir;
|
||||||
|
|
||||||
|
/// Target being built.
|
||||||
|
final Target target;
|
||||||
|
|
||||||
|
bool ndkIsInstalled() {
|
||||||
|
final ndkPath = path.join(sdkPath, 'ndk', ndkVersion);
|
||||||
|
final ndkPackageXml = File(path.join(ndkPath, 'package.xml'));
|
||||||
|
return ndkPackageXml.existsSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
void installNdk({
|
||||||
|
required String javaHome,
|
||||||
|
}) {
|
||||||
|
final sdkManagerExtension = Platform.isWindows ? '.bat' : '';
|
||||||
|
final sdkManager = path.join(
|
||||||
|
sdkPath,
|
||||||
|
'cmdline-tools',
|
||||||
|
'latest',
|
||||||
|
'bin',
|
||||||
|
'sdkmanager$sdkManagerExtension',
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info('Installing NDK $ndkVersion');
|
||||||
|
runCommand(sdkManager, [
|
||||||
|
'--install',
|
||||||
|
'ndk;$ndkVersion',
|
||||||
|
], environment: {
|
||||||
|
'JAVA_HOME': javaHome,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, String>> buildEnvironment() async {
|
||||||
|
final hostArch = Platform.isMacOS
|
||||||
|
? "darwin-x86_64"
|
||||||
|
: (Platform.isLinux ? "linux-x86_64" : "windows-x86_64");
|
||||||
|
|
||||||
|
final ndkPath = path.join(sdkPath, 'ndk', ndkVersion);
|
||||||
|
final toolchainPath = path.join(
|
||||||
|
ndkPath,
|
||||||
|
'toolchains',
|
||||||
|
'llvm',
|
||||||
|
'prebuilt',
|
||||||
|
hostArch,
|
||||||
|
'bin',
|
||||||
|
);
|
||||||
|
|
||||||
|
final minSdkVersion =
|
||||||
|
math.max(target.androidMinSdkVersion!, this.minSdkVersion);
|
||||||
|
|
||||||
|
final exe = Platform.isWindows ? '.exe' : '';
|
||||||
|
|
||||||
|
final arKey = 'AR_${target.rust}';
|
||||||
|
final arValue = ['${target.rust}-ar', 'llvm-ar', 'llvm-ar.exe']
|
||||||
|
.map((e) => path.join(toolchainPath, e))
|
||||||
|
.firstWhereOrNull((element) => File(element).existsSync());
|
||||||
|
if (arValue == null) {
|
||||||
|
throw Exception('Failed to find ar for $target in $toolchainPath');
|
||||||
|
}
|
||||||
|
|
||||||
|
final targetArg = '--target=${target.rust}$minSdkVersion';
|
||||||
|
|
||||||
|
final ccKey = 'CC_${target.rust}';
|
||||||
|
final ccValue = path.join(toolchainPath, 'clang$exe');
|
||||||
|
final cfFlagsKey = 'CFLAGS_${target.rust}';
|
||||||
|
final cFlagsValue = targetArg;
|
||||||
|
|
||||||
|
final cxxKey = 'CXX_${target.rust}';
|
||||||
|
final cxxValue = path.join(toolchainPath, 'clang++$exe');
|
||||||
|
final cxxFlagsKey = 'CXXFLAGS_${target.rust}';
|
||||||
|
final cxxFlagsValue = targetArg;
|
||||||
|
|
||||||
|
final linkerKey =
|
||||||
|
'cargo_target_${target.rust.replaceAll('-', '_')}_linker'.toUpperCase();
|
||||||
|
|
||||||
|
final ranlibKey = 'RANLIB_${target.rust}';
|
||||||
|
final ranlibValue = path.join(toolchainPath, 'llvm-ranlib$exe');
|
||||||
|
|
||||||
|
final ndkVersionParsed = Version.parse(ndkVersion);
|
||||||
|
final rustFlagsKey = 'CARGO_ENCODED_RUSTFLAGS';
|
||||||
|
final rustFlagsValue = _libGccWorkaround(targetTempDir, ndkVersionParsed);
|
||||||
|
|
||||||
|
final runRustTool =
|
||||||
|
Platform.isWindows ? 'run_build_tool.cmd' : 'run_build_tool.sh';
|
||||||
|
|
||||||
|
final packagePath = (await Isolate.resolvePackageUri(
|
||||||
|
Uri.parse('package:build_tool/buildtool.dart')))!
|
||||||
|
.toFilePath();
|
||||||
|
final selfPath = path.canonicalize(path.join(
|
||||||
|
packagePath,
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
runRustTool,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Make sure that run_build_tool is working properly even initially launched directly
|
||||||
|
// through dart run.
|
||||||
|
final toolTempDir =
|
||||||
|
Platform.environment['CARGOKIT_TOOL_TEMP_DIR'] ?? targetTempDir;
|
||||||
|
|
||||||
|
return {
|
||||||
|
arKey: arValue,
|
||||||
|
ccKey: ccValue,
|
||||||
|
cfFlagsKey: cFlagsValue,
|
||||||
|
cxxKey: cxxValue,
|
||||||
|
cxxFlagsKey: cxxFlagsValue,
|
||||||
|
ranlibKey: ranlibValue,
|
||||||
|
rustFlagsKey: rustFlagsValue,
|
||||||
|
linkerKey: selfPath,
|
||||||
|
// Recognized by main() so we know when we're acting as a wrapper
|
||||||
|
'_CARGOKIT_NDK_LINK_TARGET': targetArg,
|
||||||
|
'_CARGOKIT_NDK_LINK_CLANG': ccValue,
|
||||||
|
'CARGOKIT_TOOL_TEMP_DIR': toolTempDir,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workaround for libgcc missing in NDK23, inspired by cargo-ndk
|
||||||
|
String _libGccWorkaround(String buildDir, Version ndkVersion) {
|
||||||
|
final workaroundDir = path.join(
|
||||||
|
buildDir,
|
||||||
|
'cargokit',
|
||||||
|
'libgcc_workaround',
|
||||||
|
'${ndkVersion.major}',
|
||||||
|
);
|
||||||
|
Directory(workaroundDir).createSync(recursive: true);
|
||||||
|
if (ndkVersion.major >= 23) {
|
||||||
|
File(path.join(workaroundDir, 'libgcc.a'))
|
||||||
|
.writeAsStringSync('INPUT(-lunwind)');
|
||||||
|
} else {
|
||||||
|
// Other way around, untested, forward libgcc.a from libunwind once Rust
|
||||||
|
// gets updated for NDK23+.
|
||||||
|
File(path.join(workaroundDir, 'libunwind.a'))
|
||||||
|
.writeAsStringSync('INPUT(-lgcc)');
|
||||||
|
}
|
||||||
|
|
||||||
|
var rustFlags = Platform.environment['CARGO_ENCODED_RUSTFLAGS'] ?? '';
|
||||||
|
if (rustFlags.isNotEmpty) {
|
||||||
|
rustFlags = '$rustFlags\x1f';
|
||||||
|
}
|
||||||
|
rustFlags = '$rustFlags-L\x1f$workaroundDir';
|
||||||
|
return rustFlags;
|
||||||
|
}
|
||||||
|
}
|
||||||
266
rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart
Normal file
266
rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'crate_hash.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'precompile_binaries.dart';
|
||||||
|
import 'rustup.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
|
||||||
|
class Artifact {
|
||||||
|
/// File system location of the artifact.
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
/// Actual file name that the artifact should have in destination folder.
|
||||||
|
final String finalFileName;
|
||||||
|
|
||||||
|
AritifactType get type {
|
||||||
|
if (finalFileName.endsWith('.dll') ||
|
||||||
|
finalFileName.endsWith('.dll.lib') ||
|
||||||
|
finalFileName.endsWith('.pdb') ||
|
||||||
|
finalFileName.endsWith('.so') ||
|
||||||
|
finalFileName.endsWith('.dylib')) {
|
||||||
|
return AritifactType.dylib;
|
||||||
|
} else if (finalFileName.endsWith('.lib') || finalFileName.endsWith('.a')) {
|
||||||
|
return AritifactType.staticlib;
|
||||||
|
} else {
|
||||||
|
throw Exception('Unknown artifact type for $finalFileName');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Artifact({
|
||||||
|
required this.path,
|
||||||
|
required this.finalFileName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final _log = Logger('artifacts_provider');
|
||||||
|
|
||||||
|
class ArtifactProvider {
|
||||||
|
ArtifactProvider({
|
||||||
|
required this.environment,
|
||||||
|
required this.userOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BuildEnvironment environment;
|
||||||
|
final CargokitUserOptions userOptions;
|
||||||
|
|
||||||
|
Future<Map<Target, List<Artifact>>> getArtifacts(List<Target> targets) async {
|
||||||
|
final result = await _getPrecompiledArtifacts(targets);
|
||||||
|
|
||||||
|
final pendingTargets = List.of(targets);
|
||||||
|
pendingTargets.removeWhere((element) => result.containsKey(element));
|
||||||
|
|
||||||
|
if (pendingTargets.isEmpty) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
final rustup = Rustup();
|
||||||
|
for (final target in targets) {
|
||||||
|
final builder = RustBuilder(target: target, environment: environment);
|
||||||
|
builder.prepare(rustup);
|
||||||
|
_log.info('Building ${environment.crateInfo.packageName} for $target');
|
||||||
|
final targetDir = await builder.build();
|
||||||
|
// For local build accept both static and dynamic libraries.
|
||||||
|
final artifactNames = <String>{
|
||||||
|
...getArtifactNames(
|
||||||
|
target: target,
|
||||||
|
libraryName: environment.crateInfo.packageName,
|
||||||
|
aritifactType: AritifactType.dylib,
|
||||||
|
remote: false,
|
||||||
|
),
|
||||||
|
...getArtifactNames(
|
||||||
|
target: target,
|
||||||
|
libraryName: environment.crateInfo.packageName,
|
||||||
|
aritifactType: AritifactType.staticlib,
|
||||||
|
remote: false,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
final artifacts = artifactNames
|
||||||
|
.map((artifactName) => Artifact(
|
||||||
|
path: path.join(targetDir, artifactName),
|
||||||
|
finalFileName: artifactName,
|
||||||
|
))
|
||||||
|
.where((element) => File(element.path).existsSync())
|
||||||
|
.toList();
|
||||||
|
result[target] = artifacts;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<Target, List<Artifact>>> _getPrecompiledArtifacts(
|
||||||
|
List<Target> targets) async {
|
||||||
|
if (userOptions.usePrecompiledBinaries == false) {
|
||||||
|
_log.info('Precompiled binaries are disabled');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (environment.crateOptions.precompiledBinaries == null) {
|
||||||
|
_log.fine('Precompiled binaries not enabled for this crate');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
final start = Stopwatch()..start();
|
||||||
|
final crateHash = CrateHash.compute(environment.manifestDir,
|
||||||
|
tempStorage: environment.targetTempDir);
|
||||||
|
_log.fine(
|
||||||
|
'Computed crate hash $crateHash in ${start.elapsedMilliseconds}ms');
|
||||||
|
|
||||||
|
final downloadedArtifactsDir =
|
||||||
|
path.join(environment.targetTempDir, 'precompiled', crateHash);
|
||||||
|
Directory(downloadedArtifactsDir).createSync(recursive: true);
|
||||||
|
|
||||||
|
final res = <Target, List<Artifact>>{};
|
||||||
|
|
||||||
|
for (final target in targets) {
|
||||||
|
final requiredArtifacts = getArtifactNames(
|
||||||
|
target: target,
|
||||||
|
libraryName: environment.crateInfo.packageName,
|
||||||
|
remote: true,
|
||||||
|
);
|
||||||
|
final artifactsForTarget = <Artifact>[];
|
||||||
|
|
||||||
|
for (final artifact in requiredArtifacts) {
|
||||||
|
final fileName = PrecompileBinaries.fileName(target, artifact);
|
||||||
|
final downloadedPath = path.join(downloadedArtifactsDir, fileName);
|
||||||
|
if (!File(downloadedPath).existsSync()) {
|
||||||
|
final signatureFileName =
|
||||||
|
PrecompileBinaries.signatureFileName(target, artifact);
|
||||||
|
await _tryDownloadArtifacts(
|
||||||
|
crateHash: crateHash,
|
||||||
|
fileName: fileName,
|
||||||
|
signatureFileName: signatureFileName,
|
||||||
|
finalPath: downloadedPath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (File(downloadedPath).existsSync()) {
|
||||||
|
artifactsForTarget.add(Artifact(
|
||||||
|
path: downloadedPath,
|
||||||
|
finalFileName: artifact,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only provide complete set of artifacts.
|
||||||
|
if (artifactsForTarget.length == requiredArtifacts.length) {
|
||||||
|
_log.fine('Found precompiled artifacts for $target');
|
||||||
|
res[target] = artifactsForTarget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Response> _get(Uri url, {Map<String, String>? headers}) async {
|
||||||
|
int attempt = 0;
|
||||||
|
const maxAttempts = 10;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
return await get(url, headers: headers);
|
||||||
|
} on SocketException catch (e) {
|
||||||
|
// Try to detect reset by peer error and retry.
|
||||||
|
if (attempt++ < maxAttempts &&
|
||||||
|
(e.osError?.errorCode == 54 || e.osError?.errorCode == 10054)) {
|
||||||
|
_log.severe(
|
||||||
|
'Failed to download $url: $e, attempt $attempt of $maxAttempts, will retry...');
|
||||||
|
await Future.delayed(Duration(seconds: 1));
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _tryDownloadArtifacts({
|
||||||
|
required String crateHash,
|
||||||
|
required String fileName,
|
||||||
|
required String signatureFileName,
|
||||||
|
required String finalPath,
|
||||||
|
}) async {
|
||||||
|
final precompiledBinaries = environment.crateOptions.precompiledBinaries!;
|
||||||
|
final prefix = precompiledBinaries.uriPrefix;
|
||||||
|
final url = Uri.parse('$prefix$crateHash/$fileName');
|
||||||
|
final signatureUrl = Uri.parse('$prefix$crateHash/$signatureFileName');
|
||||||
|
_log.fine('Downloading signature from $signatureUrl');
|
||||||
|
final signature = await _get(signatureUrl);
|
||||||
|
if (signature.statusCode == 404) {
|
||||||
|
_log.warning(
|
||||||
|
'Precompiled binaries not available for crate hash $crateHash ($fileName)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (signature.statusCode != 200) {
|
||||||
|
_log.severe(
|
||||||
|
'Failed to download signature $signatureUrl: status ${signature.statusCode}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_log.fine('Downloading binary from $url');
|
||||||
|
final res = await _get(url);
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
_log.severe('Failed to download binary $url: status ${res.statusCode}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (verify(
|
||||||
|
precompiledBinaries.publicKey, res.bodyBytes, signature.bodyBytes)) {
|
||||||
|
File(finalPath).writeAsBytesSync(res.bodyBytes);
|
||||||
|
} else {
|
||||||
|
_log.shout('Signature verification failed! Ignoring binary.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AritifactType {
|
||||||
|
staticlib,
|
||||||
|
dylib,
|
||||||
|
}
|
||||||
|
|
||||||
|
AritifactType artifactTypeForTarget(Target target) {
|
||||||
|
if (target.darwinPlatform != null) {
|
||||||
|
return AritifactType.staticlib;
|
||||||
|
} else {
|
||||||
|
return AritifactType.dylib;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> getArtifactNames({
|
||||||
|
required Target target,
|
||||||
|
required String libraryName,
|
||||||
|
required bool remote,
|
||||||
|
AritifactType? aritifactType,
|
||||||
|
}) {
|
||||||
|
aritifactType ??= artifactTypeForTarget(target);
|
||||||
|
if (target.darwinArch != null) {
|
||||||
|
if (aritifactType == AritifactType.staticlib) {
|
||||||
|
return ['lib$libraryName.a'];
|
||||||
|
} else {
|
||||||
|
return ['lib$libraryName.dylib'];
|
||||||
|
}
|
||||||
|
} else if (target.rust.contains('-windows-')) {
|
||||||
|
if (aritifactType == AritifactType.staticlib) {
|
||||||
|
return ['$libraryName.lib'];
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
'$libraryName.dll',
|
||||||
|
'$libraryName.dll.lib',
|
||||||
|
if (!remote) '$libraryName.pdb'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else if (target.rust.contains('-linux-')) {
|
||||||
|
if (aritifactType == AritifactType.staticlib) {
|
||||||
|
return ['lib$libraryName.a'];
|
||||||
|
} else {
|
||||||
|
return ['lib$libraryName.so'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception("Unsupported target: ${target.rust}");
|
||||||
|
}
|
||||||
|
}
|
||||||
40
rust_builder/cargokit/build_tool/lib/src/build_cmake.dart
Normal file
40
rust_builder/cargokit/build_tool/lib/src/build_cmake.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'artifacts_provider.dart';
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'environment.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
|
||||||
|
class BuildCMake {
|
||||||
|
final CargokitUserOptions userOptions;
|
||||||
|
|
||||||
|
BuildCMake({required this.userOptions});
|
||||||
|
|
||||||
|
Future<void> build() async {
|
||||||
|
final targetPlatform = Environment.targetPlatform;
|
||||||
|
final target = Target.forFlutterName(Environment.targetPlatform);
|
||||||
|
if (target == null) {
|
||||||
|
throw Exception("Unknown target platform: $targetPlatform");
|
||||||
|
}
|
||||||
|
|
||||||
|
final environment = BuildEnvironment.fromEnvironment(isAndroid: false);
|
||||||
|
final provider =
|
||||||
|
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||||
|
final artifacts = await provider.getArtifacts([target]);
|
||||||
|
|
||||||
|
final libs = artifacts[target]!;
|
||||||
|
|
||||||
|
for (final lib in libs) {
|
||||||
|
if (lib.type == AritifactType.dylib) {
|
||||||
|
File(lib.path)
|
||||||
|
.copySync(path.join(Environment.outputDir, lib.finalFileName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
rust_builder/cargokit/build_tool/lib/src/build_gradle.dart
Normal file
49
rust_builder/cargokit/build_tool/lib/src/build_gradle.dart
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'artifacts_provider.dart';
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'environment.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
|
||||||
|
final log = Logger('build_gradle');
|
||||||
|
|
||||||
|
class BuildGradle {
|
||||||
|
BuildGradle({required this.userOptions});
|
||||||
|
|
||||||
|
final CargokitUserOptions userOptions;
|
||||||
|
|
||||||
|
Future<void> build() async {
|
||||||
|
final targets = Environment.targetPlatforms.map((arch) {
|
||||||
|
final target = Target.forFlutterName(arch);
|
||||||
|
if (target == null) {
|
||||||
|
throw Exception(
|
||||||
|
"Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}");
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final environment = BuildEnvironment.fromEnvironment(isAndroid: true);
|
||||||
|
final provider =
|
||||||
|
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||||
|
final artifacts = await provider.getArtifacts(targets);
|
||||||
|
|
||||||
|
for (final target in targets) {
|
||||||
|
final libs = artifacts[target]!;
|
||||||
|
final outputDir = path.join(Environment.outputDir, target.android!);
|
||||||
|
Directory(outputDir).createSync(recursive: true);
|
||||||
|
|
||||||
|
for (final lib in libs) {
|
||||||
|
if (lib.type == AritifactType.dylib) {
|
||||||
|
File(lib.path).copySync(path.join(outputDir, lib.finalFileName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
rust_builder/cargokit/build_tool/lib/src/build_pod.dart
Normal file
89
rust_builder/cargokit/build_tool/lib/src/build_pod.dart
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'artifacts_provider.dart';
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'environment.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
class BuildPod {
|
||||||
|
BuildPod({required this.userOptions});
|
||||||
|
|
||||||
|
final CargokitUserOptions userOptions;
|
||||||
|
|
||||||
|
Future<void> build() async {
|
||||||
|
final targets = Environment.darwinArchs.map((arch) {
|
||||||
|
final target = Target.forDarwin(
|
||||||
|
platformName: Environment.darwinPlatformName, darwinAarch: arch);
|
||||||
|
if (target == null) {
|
||||||
|
throw Exception(
|
||||||
|
"Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}");
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final environment = BuildEnvironment.fromEnvironment(isAndroid: false);
|
||||||
|
final provider =
|
||||||
|
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||||
|
final artifacts = await provider.getArtifacts(targets);
|
||||||
|
|
||||||
|
void performLipo(String targetFile, Iterable<String> sourceFiles) {
|
||||||
|
runCommand("lipo", [
|
||||||
|
'-create',
|
||||||
|
...sourceFiles,
|
||||||
|
'-output',
|
||||||
|
targetFile,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
final outputDir = Environment.outputDir;
|
||||||
|
|
||||||
|
Directory(outputDir).createSync(recursive: true);
|
||||||
|
|
||||||
|
final staticLibs = artifacts.values
|
||||||
|
.expand((element) => element)
|
||||||
|
.where((element) => element.type == AritifactType.staticlib)
|
||||||
|
.toList();
|
||||||
|
final dynamicLibs = artifacts.values
|
||||||
|
.expand((element) => element)
|
||||||
|
.where((element) => element.type == AritifactType.dylib)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final libName = environment.crateInfo.packageName;
|
||||||
|
|
||||||
|
// If there is static lib, use it and link it with pod
|
||||||
|
if (staticLibs.isNotEmpty) {
|
||||||
|
final finalTargetFile = path.join(outputDir, "lib$libName.a");
|
||||||
|
performLipo(finalTargetFile, staticLibs.map((e) => e.path));
|
||||||
|
} else {
|
||||||
|
// Otherwise try to replace bundle dylib with our dylib
|
||||||
|
final bundlePaths = [
|
||||||
|
'$libName.framework/Versions/A/$libName',
|
||||||
|
'$libName.framework/$libName',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final bundlePath in bundlePaths) {
|
||||||
|
final targetFile = path.join(outputDir, bundlePath);
|
||||||
|
if (File(targetFile).existsSync()) {
|
||||||
|
performLipo(targetFile, dynamicLibs.map((e) => e.path));
|
||||||
|
|
||||||
|
// Replace absolute id with @rpath one so that it works properly
|
||||||
|
// when moved to Frameworks.
|
||||||
|
runCommand("install_name_tool", [
|
||||||
|
'-id',
|
||||||
|
'@rpath/$bundlePath',
|
||||||
|
targetFile,
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw Exception('Unable to find bundle for dynamic library');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
271
rust_builder/cargokit/build_tool/lib/src/build_tool.dart
Normal file
271
rust_builder/cargokit/build_tool/lib/src/build_tool.dart
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:args/command_runner.dart';
|
||||||
|
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||||
|
import 'package:github/github.dart';
|
||||||
|
import 'package:hex/hex.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
import 'android_environment.dart';
|
||||||
|
import 'build_cmake.dart';
|
||||||
|
import 'build_gradle.dart';
|
||||||
|
import 'build_pod.dart';
|
||||||
|
import 'logging.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'precompile_binaries.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
import 'util.dart';
|
||||||
|
import 'verify_binaries.dart';
|
||||||
|
|
||||||
|
final log = Logger('build_tool');
|
||||||
|
|
||||||
|
abstract class BuildCommand extends Command {
|
||||||
|
Future<void> runBuildCommand(CargokitUserOptions options);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> run() async {
|
||||||
|
final options = CargokitUserOptions.load();
|
||||||
|
|
||||||
|
if (options.verboseLogging ||
|
||||||
|
Platform.environment['CARGOKIT_VERBOSE'] == '1') {
|
||||||
|
enableVerboseLogging();
|
||||||
|
}
|
||||||
|
|
||||||
|
await runBuildCommand(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BuildPodCommand extends BuildCommand {
|
||||||
|
@override
|
||||||
|
final name = 'build-pod';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Build cocoa pod library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||||
|
final build = BuildPod(userOptions: options);
|
||||||
|
await build.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BuildGradleCommand extends BuildCommand {
|
||||||
|
@override
|
||||||
|
final name = 'build-gradle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Build android library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||||
|
final build = BuildGradle(userOptions: options);
|
||||||
|
await build.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BuildCMakeCommand extends BuildCommand {
|
||||||
|
@override
|
||||||
|
final name = 'build-cmake';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Build CMake library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||||
|
final build = BuildCMake(userOptions: options);
|
||||||
|
await build.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GenKeyCommand extends Command {
|
||||||
|
@override
|
||||||
|
final name = 'gen-key';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Generate key pair for signing precompiled binaries';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void run() {
|
||||||
|
final kp = generateKey();
|
||||||
|
final private = HEX.encode(kp.privateKey.bytes);
|
||||||
|
final public = HEX.encode(kp.publicKey.bytes);
|
||||||
|
print("Private Key: $private");
|
||||||
|
print("Public Key: $public");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrecompileBinariesCommand extends Command {
|
||||||
|
PrecompileBinariesCommand() {
|
||||||
|
argParser
|
||||||
|
..addOption(
|
||||||
|
'repository',
|
||||||
|
mandatory: true,
|
||||||
|
help: 'Github repository slug in format owner/name',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'manifest-dir',
|
||||||
|
mandatory: true,
|
||||||
|
help: 'Directory containing Cargo.toml',
|
||||||
|
)
|
||||||
|
..addMultiOption('target',
|
||||||
|
help: 'Rust target triple of artifact to build.\n'
|
||||||
|
'Can be specified multiple times or omitted in which case\n'
|
||||||
|
'all targets for current platform will be built.')
|
||||||
|
..addOption(
|
||||||
|
'android-sdk-location',
|
||||||
|
help: 'Location of Android SDK (if available)',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'android-ndk-version',
|
||||||
|
help: 'Android NDK version (if available)',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'android-min-sdk-version',
|
||||||
|
help: 'Android minimum rquired version (if available)',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'temp-dir',
|
||||||
|
help: 'Directory to store temporary build artifacts',
|
||||||
|
)
|
||||||
|
..addFlag(
|
||||||
|
"verbose",
|
||||||
|
abbr: "v",
|
||||||
|
defaultsTo: false,
|
||||||
|
help: "Enable verbose logging",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final name = 'precompile-binaries';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Prebuild and upload binaries\n'
|
||||||
|
'Private key must be passed through PRIVATE_KEY environment variable. '
|
||||||
|
'Use gen_key through generate priave key.\n'
|
||||||
|
'Github token must be passed as GITHUB_TOKEN environment variable.\n';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> run() async {
|
||||||
|
final verbose = argResults!['verbose'] as bool;
|
||||||
|
if (verbose) {
|
||||||
|
enableVerboseLogging();
|
||||||
|
}
|
||||||
|
|
||||||
|
final privateKeyString = Platform.environment['PRIVATE_KEY'];
|
||||||
|
if (privateKeyString == null) {
|
||||||
|
throw ArgumentError('Missing PRIVATE_KEY environment variable');
|
||||||
|
}
|
||||||
|
final githubToken = Platform.environment['GITHUB_TOKEN'];
|
||||||
|
if (githubToken == null) {
|
||||||
|
throw ArgumentError('Missing GITHUB_TOKEN environment variable');
|
||||||
|
}
|
||||||
|
final privateKey = HEX.decode(privateKeyString);
|
||||||
|
if (privateKey.length != 64) {
|
||||||
|
throw ArgumentError('Private key must be 64 bytes long');
|
||||||
|
}
|
||||||
|
final manifestDir = argResults!['manifest-dir'] as String;
|
||||||
|
if (!Directory(manifestDir).existsSync()) {
|
||||||
|
throw ArgumentError('Manifest directory does not exist: $manifestDir');
|
||||||
|
}
|
||||||
|
String? androidMinSdkVersionString =
|
||||||
|
argResults!['android-min-sdk-version'] as String?;
|
||||||
|
int? androidMinSdkVersion;
|
||||||
|
if (androidMinSdkVersionString != null) {
|
||||||
|
androidMinSdkVersion = int.tryParse(androidMinSdkVersionString);
|
||||||
|
if (androidMinSdkVersion == null) {
|
||||||
|
throw ArgumentError(
|
||||||
|
'Invalid android-min-sdk-version: $androidMinSdkVersionString');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final targetStrigns = argResults!['target'] as List<String>;
|
||||||
|
final targets = targetStrigns.map((target) {
|
||||||
|
final res = Target.forRustTriple(target);
|
||||||
|
if (res == null) {
|
||||||
|
throw ArgumentError('Invalid target: $target');
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}).toList(growable: false);
|
||||||
|
final precompileBinaries = PrecompileBinaries(
|
||||||
|
privateKey: PrivateKey(privateKey),
|
||||||
|
githubToken: githubToken,
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
repositorySlug: RepositorySlug.full(argResults!['repository'] as String),
|
||||||
|
targets: targets,
|
||||||
|
androidSdkLocation: argResults!['android-sdk-location'] as String?,
|
||||||
|
androidNdkVersion: argResults!['android-ndk-version'] as String?,
|
||||||
|
androidMinSdkVersion: androidMinSdkVersion,
|
||||||
|
tempDir: argResults!['temp-dir'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
await precompileBinaries.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VerifyBinariesCommand extends Command {
|
||||||
|
VerifyBinariesCommand() {
|
||||||
|
argParser.addOption(
|
||||||
|
'manifest-dir',
|
||||||
|
mandatory: true,
|
||||||
|
help: 'Directory containing Cargo.toml',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final name = "verify-binaries";
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Verifies published binaries\n'
|
||||||
|
'Checks whether there is a binary published for each targets\n'
|
||||||
|
'and checks the signature.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> run() async {
|
||||||
|
final manifestDir = argResults!['manifest-dir'] as String;
|
||||||
|
final verifyBinaries = VerifyBinaries(
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
);
|
||||||
|
await verifyBinaries.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> runMain(List<String> args) async {
|
||||||
|
try {
|
||||||
|
// Init logging before options are loaded
|
||||||
|
initLogging();
|
||||||
|
|
||||||
|
if (Platform.environment['_CARGOKIT_NDK_LINK_TARGET'] != null) {
|
||||||
|
return AndroidEnvironment.clangLinkerWrapper(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
final runner = CommandRunner('build_tool', 'Cargokit built_tool')
|
||||||
|
..addCommand(BuildPodCommand())
|
||||||
|
..addCommand(BuildGradleCommand())
|
||||||
|
..addCommand(BuildCMakeCommand())
|
||||||
|
..addCommand(GenKeyCommand())
|
||||||
|
..addCommand(PrecompileBinariesCommand())
|
||||||
|
..addCommand(VerifyBinariesCommand());
|
||||||
|
|
||||||
|
await runner.run(args);
|
||||||
|
} on ArgumentError catch (e) {
|
||||||
|
stderr.writeln(e.toString());
|
||||||
|
exit(1);
|
||||||
|
} catch (e, s) {
|
||||||
|
log.severe(kDoubleSeparator);
|
||||||
|
log.severe('Cargokit BuildTool failed with error:');
|
||||||
|
log.severe(kSeparator);
|
||||||
|
log.severe(e);
|
||||||
|
// This tells user to install Rust, there's no need to pollute the log with
|
||||||
|
// stack trace.
|
||||||
|
if (e is! RustupNotFoundException) {
|
||||||
|
log.severe(kSeparator);
|
||||||
|
log.severe(s);
|
||||||
|
log.severe(kSeparator);
|
||||||
|
log.severe('BuildTool arguments: $args');
|
||||||
|
}
|
||||||
|
log.severe(kDoubleSeparator);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
198
rust_builder/cargokit/build_tool/lib/src/builder.dart
Normal file
198
rust_builder/cargokit/build_tool/lib/src/builder.dart
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'android_environment.dart';
|
||||||
|
import 'cargo.dart';
|
||||||
|
import 'environment.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'rustup.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
final _log = Logger('builder');
|
||||||
|
|
||||||
|
enum BuildConfiguration {
|
||||||
|
debug,
|
||||||
|
release,
|
||||||
|
profile,
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on BuildConfiguration {
|
||||||
|
bool get isDebug => this == BuildConfiguration.debug;
|
||||||
|
String get rustName => switch (this) {
|
||||||
|
BuildConfiguration.debug => 'debug',
|
||||||
|
BuildConfiguration.release => 'release',
|
||||||
|
BuildConfiguration.profile => 'release',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class BuildException implements Exception {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
BuildException(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'BuildException: $message';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BuildEnvironment {
|
||||||
|
final BuildConfiguration configuration;
|
||||||
|
final CargokitCrateOptions crateOptions;
|
||||||
|
final String targetTempDir;
|
||||||
|
final String manifestDir;
|
||||||
|
final CrateInfo crateInfo;
|
||||||
|
|
||||||
|
final bool isAndroid;
|
||||||
|
final String? androidSdkPath;
|
||||||
|
final String? androidNdkVersion;
|
||||||
|
final int? androidMinSdkVersion;
|
||||||
|
final String? javaHome;
|
||||||
|
|
||||||
|
BuildEnvironment({
|
||||||
|
required this.configuration,
|
||||||
|
required this.crateOptions,
|
||||||
|
required this.targetTempDir,
|
||||||
|
required this.manifestDir,
|
||||||
|
required this.crateInfo,
|
||||||
|
required this.isAndroid,
|
||||||
|
this.androidSdkPath,
|
||||||
|
this.androidNdkVersion,
|
||||||
|
this.androidMinSdkVersion,
|
||||||
|
this.javaHome,
|
||||||
|
});
|
||||||
|
|
||||||
|
static BuildConfiguration parseBuildConfiguration(String value) {
|
||||||
|
// XCode configuration adds the flavor to configuration name.
|
||||||
|
final firstSegment = value.split('-').first;
|
||||||
|
final buildConfiguration = BuildConfiguration.values.firstWhereOrNull(
|
||||||
|
(e) => e.name == firstSegment,
|
||||||
|
);
|
||||||
|
if (buildConfiguration == null) {
|
||||||
|
_log.warning('Unknown build configuraiton $value, will assume release');
|
||||||
|
return BuildConfiguration.release;
|
||||||
|
}
|
||||||
|
return buildConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BuildEnvironment fromEnvironment({
|
||||||
|
required bool isAndroid,
|
||||||
|
}) {
|
||||||
|
final buildConfiguration =
|
||||||
|
parseBuildConfiguration(Environment.configuration);
|
||||||
|
final manifestDir = Environment.manifestDir;
|
||||||
|
final crateOptions = CargokitCrateOptions.load(
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
);
|
||||||
|
final crateInfo = CrateInfo.load(manifestDir);
|
||||||
|
return BuildEnvironment(
|
||||||
|
configuration: buildConfiguration,
|
||||||
|
crateOptions: crateOptions,
|
||||||
|
targetTempDir: Environment.targetTempDir,
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
crateInfo: crateInfo,
|
||||||
|
isAndroid: isAndroid,
|
||||||
|
androidSdkPath: isAndroid ? Environment.sdkPath : null,
|
||||||
|
androidNdkVersion: isAndroid ? Environment.ndkVersion : null,
|
||||||
|
androidMinSdkVersion:
|
||||||
|
isAndroid ? int.parse(Environment.minSdkVersion) : null,
|
||||||
|
javaHome: isAndroid ? Environment.javaHome : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RustBuilder {
|
||||||
|
final Target target;
|
||||||
|
final BuildEnvironment environment;
|
||||||
|
|
||||||
|
RustBuilder({
|
||||||
|
required this.target,
|
||||||
|
required this.environment,
|
||||||
|
});
|
||||||
|
|
||||||
|
void prepare(
|
||||||
|
Rustup rustup,
|
||||||
|
) {
|
||||||
|
final toolchain = _toolchain;
|
||||||
|
if (rustup.installedTargets(toolchain) == null) {
|
||||||
|
rustup.installToolchain(toolchain);
|
||||||
|
}
|
||||||
|
if (toolchain == 'nightly') {
|
||||||
|
rustup.installRustSrcForNightly();
|
||||||
|
}
|
||||||
|
if (!rustup.installedTargets(toolchain)!.contains(target.rust)) {
|
||||||
|
rustup.installTarget(target.rust, toolchain: toolchain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CargoBuildOptions? get _buildOptions =>
|
||||||
|
environment.crateOptions.cargo[environment.configuration];
|
||||||
|
|
||||||
|
String get _toolchain => _buildOptions?.toolchain.name ?? 'stable';
|
||||||
|
|
||||||
|
/// Returns the path of directory containing build artifacts.
|
||||||
|
Future<String> build() async {
|
||||||
|
final extraArgs = _buildOptions?.flags ?? [];
|
||||||
|
final manifestPath = path.join(environment.manifestDir, 'Cargo.toml');
|
||||||
|
runCommand(
|
||||||
|
'rustup',
|
||||||
|
[
|
||||||
|
'run',
|
||||||
|
_toolchain,
|
||||||
|
'cargo',
|
||||||
|
'build',
|
||||||
|
...extraArgs,
|
||||||
|
'--manifest-path',
|
||||||
|
manifestPath,
|
||||||
|
'-p',
|
||||||
|
environment.crateInfo.packageName,
|
||||||
|
if (!environment.configuration.isDebug) '--release',
|
||||||
|
'--target',
|
||||||
|
target.rust,
|
||||||
|
'--target-dir',
|
||||||
|
environment.targetTempDir,
|
||||||
|
],
|
||||||
|
environment: await _buildEnvironment(),
|
||||||
|
);
|
||||||
|
return path.join(
|
||||||
|
environment.targetTempDir,
|
||||||
|
target.rust,
|
||||||
|
environment.configuration.rustName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, String>> _buildEnvironment() async {
|
||||||
|
if (target.android == null) {
|
||||||
|
return {};
|
||||||
|
} else {
|
||||||
|
final sdkPath = environment.androidSdkPath;
|
||||||
|
final ndkVersion = environment.androidNdkVersion;
|
||||||
|
final minSdkVersion = environment.androidMinSdkVersion;
|
||||||
|
if (sdkPath == null) {
|
||||||
|
throw BuildException('androidSdkPath is not set');
|
||||||
|
}
|
||||||
|
if (ndkVersion == null) {
|
||||||
|
throw BuildException('androidNdkVersion is not set');
|
||||||
|
}
|
||||||
|
if (minSdkVersion == null) {
|
||||||
|
throw BuildException('androidMinSdkVersion is not set');
|
||||||
|
}
|
||||||
|
final env = AndroidEnvironment(
|
||||||
|
sdkPath: sdkPath,
|
||||||
|
ndkVersion: ndkVersion,
|
||||||
|
minSdkVersion: minSdkVersion,
|
||||||
|
targetTempDir: environment.targetTempDir,
|
||||||
|
target: target,
|
||||||
|
);
|
||||||
|
if (!env.ndkIsInstalled() && environment.javaHome != null) {
|
||||||
|
env.installNdk(javaHome: environment.javaHome!);
|
||||||
|
}
|
||||||
|
return env.buildEnvironment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
rust_builder/cargokit/build_tool/lib/src/cargo.dart
Normal file
48
rust_builder/cargokit/build_tool/lib/src/cargo.dart
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:toml/toml.dart';
|
||||||
|
|
||||||
|
class ManifestException {
|
||||||
|
ManifestException(this.message, {required this.fileName});
|
||||||
|
|
||||||
|
final String? fileName;
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
if (fileName != null) {
|
||||||
|
return 'Failed to parse package manifest at $fileName: $message';
|
||||||
|
} else {
|
||||||
|
return 'Failed to parse package manifest: $message';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CrateInfo {
|
||||||
|
CrateInfo({required this.packageName});
|
||||||
|
|
||||||
|
final String packageName;
|
||||||
|
|
||||||
|
static CrateInfo parseManifest(String manifest, {final String? fileName}) {
|
||||||
|
final toml = TomlDocument.parse(manifest);
|
||||||
|
final package = toml.toMap()['package'];
|
||||||
|
if (package == null) {
|
||||||
|
throw ManifestException('Missing package section', fileName: fileName);
|
||||||
|
}
|
||||||
|
final name = package['name'];
|
||||||
|
if (name == null) {
|
||||||
|
throw ManifestException('Missing package name', fileName: fileName);
|
||||||
|
}
|
||||||
|
return CrateInfo(packageName: name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CrateInfo load(String manifestDir) {
|
||||||
|
final manifestFile = File(path.join(manifestDir, 'Cargo.toml'));
|
||||||
|
final manifest = manifestFile.readAsStringSync();
|
||||||
|
return parseManifest(manifest, fileName: manifestFile.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
124
rust_builder/cargokit/build_tool/lib/src/crate_hash.dart
Normal file
124
rust_builder/cargokit/build_tool/lib/src/crate_hash.dart
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:convert/convert.dart';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
class CrateHash {
|
||||||
|
/// Computes a hash uniquely identifying crate content. This takes into account
|
||||||
|
/// content all all .rs files inside the src directory, as well as Cargo.toml,
|
||||||
|
/// Cargo.lock, build.rs and cargokit.yaml.
|
||||||
|
///
|
||||||
|
/// If [tempStorage] is provided, computed hash is stored in a file in that directory
|
||||||
|
/// and reused on subsequent calls if the crate content hasn't changed.
|
||||||
|
static String compute(String manifestDir, {String? tempStorage}) {
|
||||||
|
return CrateHash._(
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
tempStorage: tempStorage,
|
||||||
|
)._compute();
|
||||||
|
}
|
||||||
|
|
||||||
|
CrateHash._({
|
||||||
|
required this.manifestDir,
|
||||||
|
required this.tempStorage,
|
||||||
|
});
|
||||||
|
|
||||||
|
String _compute() {
|
||||||
|
final files = getFiles();
|
||||||
|
final tempStorage = this.tempStorage;
|
||||||
|
if (tempStorage != null) {
|
||||||
|
final quickHash = _computeQuickHash(files);
|
||||||
|
final quickHashFolder = Directory(path.join(tempStorage, 'crate_hash'));
|
||||||
|
quickHashFolder.createSync(recursive: true);
|
||||||
|
final quickHashFile = File(path.join(quickHashFolder.path, quickHash));
|
||||||
|
if (quickHashFile.existsSync()) {
|
||||||
|
return quickHashFile.readAsStringSync();
|
||||||
|
}
|
||||||
|
final hash = _computeHash(files);
|
||||||
|
quickHashFile.writeAsStringSync(hash);
|
||||||
|
return hash;
|
||||||
|
} else {
|
||||||
|
return _computeHash(files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes a quick hash based on files stat (without reading contents). This
|
||||||
|
/// is used to cache the real hash, which is slower to compute since it involves
|
||||||
|
/// reading every single file.
|
||||||
|
String _computeQuickHash(List<File> files) {
|
||||||
|
final output = AccumulatorSink<Digest>();
|
||||||
|
final input = sha256.startChunkedConversion(output);
|
||||||
|
|
||||||
|
final data = ByteData(8);
|
||||||
|
for (final file in files) {
|
||||||
|
input.add(utf8.encode(file.path));
|
||||||
|
final stat = file.statSync();
|
||||||
|
data.setUint64(0, stat.size);
|
||||||
|
input.add(data.buffer.asUint8List());
|
||||||
|
data.setUint64(0, stat.modified.millisecondsSinceEpoch);
|
||||||
|
input.add(data.buffer.asUint8List());
|
||||||
|
}
|
||||||
|
|
||||||
|
input.close();
|
||||||
|
return base64Url.encode(output.events.single.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _computeHash(List<File> files) {
|
||||||
|
final output = AccumulatorSink<Digest>();
|
||||||
|
final input = sha256.startChunkedConversion(output);
|
||||||
|
|
||||||
|
void addTextFile(File file) {
|
||||||
|
// text Files are hashed by lines in case we're dealing with github checkout
|
||||||
|
// that auto-converts line endings.
|
||||||
|
final splitter = LineSplitter();
|
||||||
|
if (file.existsSync()) {
|
||||||
|
final data = file.readAsStringSync();
|
||||||
|
final lines = splitter.convert(data);
|
||||||
|
for (final line in lines) {
|
||||||
|
input.add(utf8.encode(line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final file in files) {
|
||||||
|
addTextFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.close();
|
||||||
|
final res = output.events.single;
|
||||||
|
|
||||||
|
// Truncate to 128bits.
|
||||||
|
final hash = res.bytes.sublist(0, 16);
|
||||||
|
return hex.encode(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<File> getFiles() {
|
||||||
|
final src = Directory(path.join(manifestDir, 'src'));
|
||||||
|
final files = src
|
||||||
|
.listSync(recursive: true, followLinks: false)
|
||||||
|
.whereType<File>()
|
||||||
|
.toList();
|
||||||
|
files.sortBy((element) => element.path);
|
||||||
|
void addFile(String relative) {
|
||||||
|
final file = File(path.join(manifestDir, relative));
|
||||||
|
if (file.existsSync()) {
|
||||||
|
files.add(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addFile('Cargo.toml');
|
||||||
|
addFile('Cargo.lock');
|
||||||
|
addFile('build.rs');
|
||||||
|
addFile('cargokit.yaml');
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String manifestDir;
|
||||||
|
final String? tempStorage;
|
||||||
|
}
|
||||||
68
rust_builder/cargokit/build_tool/lib/src/environment.dart
Normal file
68
rust_builder/cargokit/build_tool/lib/src/environment.dart
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
extension on String {
|
||||||
|
String resolveSymlink() => File(this).resolveSymbolicLinksSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Environment {
|
||||||
|
/// Current build configuration (debug or release).
|
||||||
|
static String get configuration =>
|
||||||
|
_getEnv("CARGOKIT_CONFIGURATION").toLowerCase();
|
||||||
|
|
||||||
|
static bool get isDebug => configuration == 'debug';
|
||||||
|
static bool get isRelease => configuration == 'release';
|
||||||
|
|
||||||
|
/// Temporary directory where Rust build artifacts are placed.
|
||||||
|
static String get targetTempDir => _getEnv("CARGOKIT_TARGET_TEMP_DIR");
|
||||||
|
|
||||||
|
/// Final output directory where the build artifacts are placed.
|
||||||
|
static String get outputDir => _getEnvPath('CARGOKIT_OUTPUT_DIR');
|
||||||
|
|
||||||
|
/// Path to the crate manifest (containing Cargo.toml).
|
||||||
|
static String get manifestDir => _getEnvPath('CARGOKIT_MANIFEST_DIR');
|
||||||
|
|
||||||
|
/// Directory inside root project. Not necessarily root folder. Symlinks are
|
||||||
|
/// not resolved on purpose.
|
||||||
|
static String get rootProjectDir => _getEnv('CARGOKIT_ROOT_PROJECT_DIR');
|
||||||
|
|
||||||
|
// Pod
|
||||||
|
|
||||||
|
/// Platform name (macosx, iphoneos, iphonesimulator).
|
||||||
|
static String get darwinPlatformName =>
|
||||||
|
_getEnv("CARGOKIT_DARWIN_PLATFORM_NAME");
|
||||||
|
|
||||||
|
/// List of architectures to build for (arm64, armv7, x86_64).
|
||||||
|
static List<String> get darwinArchs =>
|
||||||
|
_getEnv("CARGOKIT_DARWIN_ARCHS").split(' ');
|
||||||
|
|
||||||
|
// Gradle
|
||||||
|
static String get minSdkVersion => _getEnv("CARGOKIT_MIN_SDK_VERSION");
|
||||||
|
static String get ndkVersion => _getEnv("CARGOKIT_NDK_VERSION");
|
||||||
|
static String get sdkPath => _getEnvPath("CARGOKIT_SDK_DIR");
|
||||||
|
static String get javaHome => _getEnvPath("CARGOKIT_JAVA_HOME");
|
||||||
|
static List<String> get targetPlatforms =>
|
||||||
|
_getEnv("CARGOKIT_TARGET_PLATFORMS").split(',');
|
||||||
|
|
||||||
|
// CMAKE
|
||||||
|
static String get targetPlatform => _getEnv("CARGOKIT_TARGET_PLATFORM");
|
||||||
|
|
||||||
|
static String _getEnv(String key) {
|
||||||
|
final res = Platform.environment[key];
|
||||||
|
if (res == null) {
|
||||||
|
throw Exception("Missing environment variable $key");
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _getEnvPath(String key) {
|
||||||
|
final res = _getEnv(key);
|
||||||
|
if (Directory(res).existsSync()) {
|
||||||
|
return res.resolveSymlink();
|
||||||
|
} else {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
rust_builder/cargokit/build_tool/lib/src/logging.dart
Normal file
52
rust_builder/cargokit/build_tool/lib/src/logging.dart
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
const String kSeparator = "--";
|
||||||
|
const String kDoubleSeparator = "==";
|
||||||
|
|
||||||
|
bool _lastMessageWasSeparator = false;
|
||||||
|
|
||||||
|
void _log(LogRecord rec) {
|
||||||
|
final prefix = '${rec.level.name}: ';
|
||||||
|
final out = rec.level == Level.SEVERE ? stderr : stdout;
|
||||||
|
if (rec.message == kSeparator) {
|
||||||
|
if (!_lastMessageWasSeparator) {
|
||||||
|
out.write(prefix);
|
||||||
|
out.writeln('-' * 80);
|
||||||
|
_lastMessageWasSeparator = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else if (rec.message == kDoubleSeparator) {
|
||||||
|
out.write(prefix);
|
||||||
|
out.writeln('=' * 80);
|
||||||
|
_lastMessageWasSeparator = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out.write(prefix);
|
||||||
|
out.writeln(rec.message);
|
||||||
|
_lastMessageWasSeparator = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void initLogging() {
|
||||||
|
Logger.root.level = Level.INFO;
|
||||||
|
Logger.root.onRecord.listen((LogRecord rec) {
|
||||||
|
final lines = rec.message.split('\n');
|
||||||
|
for (final line in lines) {
|
||||||
|
if (line.isNotEmpty || lines.length == 1 || line != lines.last) {
|
||||||
|
_log(LogRecord(
|
||||||
|
rec.level,
|
||||||
|
line,
|
||||||
|
rec.loggerName,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void enableVerboseLogging() {
|
||||||
|
Logger.root.level = Level.ALL;
|
||||||
|
}
|
||||||
309
rust_builder/cargokit/build_tool/lib/src/options.dart
Normal file
309
rust_builder/cargokit/build_tool/lib/src/options.dart
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||||
|
import 'package:hex/hex.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:source_span/source_span.dart';
|
||||||
|
import 'package:yaml/yaml.dart';
|
||||||
|
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'environment.dart';
|
||||||
|
import 'rustup.dart';
|
||||||
|
|
||||||
|
final _log = Logger('options');
|
||||||
|
|
||||||
|
/// A class for exceptions that have source span information attached.
|
||||||
|
class SourceSpanException implements Exception {
|
||||||
|
// This is a getter so that subclasses can override it.
|
||||||
|
/// A message describing the exception.
|
||||||
|
String get message => _message;
|
||||||
|
final String _message;
|
||||||
|
|
||||||
|
// This is a getter so that subclasses can override it.
|
||||||
|
/// The span associated with this exception.
|
||||||
|
///
|
||||||
|
/// This may be `null` if the source location can't be determined.
|
||||||
|
SourceSpan? get span => _span;
|
||||||
|
final SourceSpan? _span;
|
||||||
|
|
||||||
|
SourceSpanException(this._message, this._span);
|
||||||
|
|
||||||
|
/// Returns a string representation of `this`.
|
||||||
|
///
|
||||||
|
/// [color] may either be a [String], a [bool], or `null`. If it's a string,
|
||||||
|
/// it indicates an ANSI terminal color escape that should be used to
|
||||||
|
/// highlight the span's text. If it's `true`, it indicates that the text
|
||||||
|
/// should be highlighted using the default color. If it's `false` or `null`,
|
||||||
|
/// it indicates that the text shouldn't be highlighted.
|
||||||
|
@override
|
||||||
|
String toString({Object? color}) {
|
||||||
|
if (span == null) return message;
|
||||||
|
return 'Error on ${span!.message(message, color: color)}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Toolchain {
|
||||||
|
stable,
|
||||||
|
beta,
|
||||||
|
nightly,
|
||||||
|
}
|
||||||
|
|
||||||
|
class CargoBuildOptions {
|
||||||
|
final Toolchain toolchain;
|
||||||
|
final List<String> flags;
|
||||||
|
|
||||||
|
CargoBuildOptions({
|
||||||
|
required this.toolchain,
|
||||||
|
required this.flags,
|
||||||
|
});
|
||||||
|
|
||||||
|
static Toolchain _toolchainFromNode(YamlNode node) {
|
||||||
|
if (node case YamlScalar(value: String name)) {
|
||||||
|
final toolchain =
|
||||||
|
Toolchain.values.firstWhereOrNull((element) => element.name == name);
|
||||||
|
if (toolchain != null) {
|
||||||
|
return toolchain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Unknown toolchain. Must be one of ${Toolchain.values.map((e) => e.name)}.',
|
||||||
|
node.span);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CargoBuildOptions parse(YamlNode node) {
|
||||||
|
if (node is! YamlMap) {
|
||||||
|
throw SourceSpanException('Cargo options must be a map', node.span);
|
||||||
|
}
|
||||||
|
Toolchain toolchain = Toolchain.stable;
|
||||||
|
List<String> flags = [];
|
||||||
|
for (final MapEntry(:key, :value) in node.nodes.entries) {
|
||||||
|
if (key case YamlScalar(value: 'toolchain')) {
|
||||||
|
toolchain = _toolchainFromNode(value);
|
||||||
|
} else if (key case YamlScalar(value: 'extra_flags')) {
|
||||||
|
if (value case YamlList(nodes: List<YamlNode> list)) {
|
||||||
|
if (list.every((element) {
|
||||||
|
if (element case YamlScalar(value: String _)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})) {
|
||||||
|
flags = list.map((e) => e.value as String).toList();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Extra flags must be a list of strings', value.span);
|
||||||
|
} else {
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Unknown cargo option type. Must be "toolchain" or "extra_flags".',
|
||||||
|
key.span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CargoBuildOptions(toolchain: toolchain, flags: flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on YamlMap {
|
||||||
|
/// Map that extracts keys so that we can do map case check on them.
|
||||||
|
Map<dynamic, YamlNode> get valueMap =>
|
||||||
|
nodes.map((key, value) => MapEntry(key.value, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrecompiledBinaries {
|
||||||
|
final String uriPrefix;
|
||||||
|
final PublicKey publicKey;
|
||||||
|
|
||||||
|
PrecompiledBinaries({
|
||||||
|
required this.uriPrefix,
|
||||||
|
required this.publicKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
static PublicKey _publicKeyFromHex(String key, SourceSpan? span) {
|
||||||
|
final bytes = HEX.decode(key);
|
||||||
|
if (bytes.length != 32) {
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Invalid public key. Must be 32 bytes long.', span);
|
||||||
|
}
|
||||||
|
return PublicKey(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PrecompiledBinaries parse(YamlNode node) {
|
||||||
|
if (node case YamlMap(valueMap: Map<dynamic, YamlNode> map)) {
|
||||||
|
if (map
|
||||||
|
case {
|
||||||
|
'url_prefix': YamlNode urlPrefixNode,
|
||||||
|
'public_key': YamlNode publicKeyNode,
|
||||||
|
}) {
|
||||||
|
final urlPrefix = switch (urlPrefixNode) {
|
||||||
|
YamlScalar(value: String urlPrefix) => urlPrefix,
|
||||||
|
_ => throw SourceSpanException(
|
||||||
|
'Invalid URL prefix value.', urlPrefixNode.span),
|
||||||
|
};
|
||||||
|
final publicKey = switch (publicKeyNode) {
|
||||||
|
YamlScalar(value: String publicKey) =>
|
||||||
|
_publicKeyFromHex(publicKey, publicKeyNode.span),
|
||||||
|
_ => throw SourceSpanException(
|
||||||
|
'Invalid public key value.', publicKeyNode.span),
|
||||||
|
};
|
||||||
|
return PrecompiledBinaries(
|
||||||
|
uriPrefix: urlPrefix,
|
||||||
|
publicKey: publicKey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Invalid precompiled binaries value. '
|
||||||
|
'Expected Map with "url_prefix" and "public_key".',
|
||||||
|
node.span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cargokit options specified for Rust crate.
|
||||||
|
class CargokitCrateOptions {
|
||||||
|
CargokitCrateOptions({
|
||||||
|
this.cargo = const {},
|
||||||
|
this.precompiledBinaries,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Map<BuildConfiguration, CargoBuildOptions> cargo;
|
||||||
|
final PrecompiledBinaries? precompiledBinaries;
|
||||||
|
|
||||||
|
static CargokitCrateOptions parse(YamlNode node) {
|
||||||
|
if (node is! YamlMap) {
|
||||||
|
throw SourceSpanException('Cargokit options must be a map', node.span);
|
||||||
|
}
|
||||||
|
final options = <BuildConfiguration, CargoBuildOptions>{};
|
||||||
|
PrecompiledBinaries? precompiledBinaries;
|
||||||
|
|
||||||
|
for (final entry in node.nodes.entries) {
|
||||||
|
if (entry
|
||||||
|
case MapEntry(
|
||||||
|
key: YamlScalar(value: 'cargo'),
|
||||||
|
value: YamlNode node,
|
||||||
|
)) {
|
||||||
|
if (node is! YamlMap) {
|
||||||
|
throw SourceSpanException('Cargo options must be a map', node.span);
|
||||||
|
}
|
||||||
|
for (final MapEntry(:YamlNode key, :value) in node.nodes.entries) {
|
||||||
|
if (key case YamlScalar(value: String name)) {
|
||||||
|
final configuration = BuildConfiguration.values
|
||||||
|
.firstWhereOrNull((element) => element.name == name);
|
||||||
|
if (configuration != null) {
|
||||||
|
options[configuration] = CargoBuildOptions.parse(value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Unknown build configuration. Must be one of ${BuildConfiguration.values.map((e) => e.name)}.',
|
||||||
|
key.span);
|
||||||
|
}
|
||||||
|
} else if (entry.key case YamlScalar(value: 'precompiled_binaries')) {
|
||||||
|
precompiledBinaries = PrecompiledBinaries.parse(entry.value);
|
||||||
|
} else {
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Unknown cargokit option type. Must be "cargo" or "precompiled_binaries".',
|
||||||
|
entry.key.span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CargokitCrateOptions(
|
||||||
|
cargo: options,
|
||||||
|
precompiledBinaries: precompiledBinaries,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CargokitCrateOptions load({
|
||||||
|
required String manifestDir,
|
||||||
|
}) {
|
||||||
|
final uri = Uri.file(path.join(manifestDir, "cargokit.yaml"));
|
||||||
|
final file = File.fromUri(uri);
|
||||||
|
if (file.existsSync()) {
|
||||||
|
final contents = loadYamlNode(file.readAsStringSync(), sourceUrl: uri);
|
||||||
|
return parse(contents);
|
||||||
|
} else {
|
||||||
|
return CargokitCrateOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CargokitUserOptions {
|
||||||
|
// When Rustup is installed always build locally unless user opts into
|
||||||
|
// using precompiled binaries.
|
||||||
|
static bool defaultUsePrecompiledBinaries() {
|
||||||
|
return Rustup.executablePath() == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
CargokitUserOptions({
|
||||||
|
required this.usePrecompiledBinaries,
|
||||||
|
required this.verboseLogging,
|
||||||
|
});
|
||||||
|
|
||||||
|
CargokitUserOptions._()
|
||||||
|
: usePrecompiledBinaries = defaultUsePrecompiledBinaries(),
|
||||||
|
verboseLogging = false;
|
||||||
|
|
||||||
|
static CargokitUserOptions parse(YamlNode node) {
|
||||||
|
if (node is! YamlMap) {
|
||||||
|
throw SourceSpanException('Cargokit options must be a map', node.span);
|
||||||
|
}
|
||||||
|
bool usePrecompiledBinaries = defaultUsePrecompiledBinaries();
|
||||||
|
bool verboseLogging = false;
|
||||||
|
|
||||||
|
for (final entry in node.nodes.entries) {
|
||||||
|
if (entry.key case YamlScalar(value: 'use_precompiled_binaries')) {
|
||||||
|
if (entry.value case YamlScalar(value: bool value)) {
|
||||||
|
usePrecompiledBinaries = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Invalid value for "use_precompiled_binaries". Must be a boolean.',
|
||||||
|
entry.value.span);
|
||||||
|
} else if (entry.key case YamlScalar(value: 'verbose_logging')) {
|
||||||
|
if (entry.value case YamlScalar(value: bool value)) {
|
||||||
|
verboseLogging = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Invalid value for "verbose_logging". Must be a boolean.',
|
||||||
|
entry.value.span);
|
||||||
|
} else {
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Unknown cargokit option type. Must be "use_precompiled_binaries" or "verbose_logging".',
|
||||||
|
entry.key.span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CargokitUserOptions(
|
||||||
|
usePrecompiledBinaries: usePrecompiledBinaries,
|
||||||
|
verboseLogging: verboseLogging,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CargokitUserOptions load() {
|
||||||
|
String fileName = "cargokit_options.yaml";
|
||||||
|
var userProjectDir = Directory(Environment.rootProjectDir);
|
||||||
|
|
||||||
|
while (userProjectDir.parent.path != userProjectDir.path) {
|
||||||
|
final configFile = File(path.join(userProjectDir.path, fileName));
|
||||||
|
if (configFile.existsSync()) {
|
||||||
|
final contents = loadYamlNode(
|
||||||
|
configFile.readAsStringSync(),
|
||||||
|
sourceUrl: configFile.uri,
|
||||||
|
);
|
||||||
|
final res = parse(contents);
|
||||||
|
if (res.verboseLogging) {
|
||||||
|
_log.info('Found user options file at ${configFile.path}');
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
userProjectDir = userProjectDir.parent;
|
||||||
|
}
|
||||||
|
return CargokitUserOptions._();
|
||||||
|
}
|
||||||
|
|
||||||
|
final bool usePrecompiledBinaries;
|
||||||
|
final bool verboseLogging;
|
||||||
|
}
|
||||||
@ -0,0 +1,202 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||||
|
import 'package:github/github.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'artifacts_provider.dart';
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'cargo.dart';
|
||||||
|
import 'crate_hash.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'rustup.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
|
||||||
|
final _log = Logger('precompile_binaries');
|
||||||
|
|
||||||
|
class PrecompileBinaries {
|
||||||
|
PrecompileBinaries({
|
||||||
|
required this.privateKey,
|
||||||
|
required this.githubToken,
|
||||||
|
required this.repositorySlug,
|
||||||
|
required this.manifestDir,
|
||||||
|
required this.targets,
|
||||||
|
this.androidSdkLocation,
|
||||||
|
this.androidNdkVersion,
|
||||||
|
this.androidMinSdkVersion,
|
||||||
|
this.tempDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PrivateKey privateKey;
|
||||||
|
final String githubToken;
|
||||||
|
final RepositorySlug repositorySlug;
|
||||||
|
final String manifestDir;
|
||||||
|
final List<Target> targets;
|
||||||
|
final String? androidSdkLocation;
|
||||||
|
final String? androidNdkVersion;
|
||||||
|
final int? androidMinSdkVersion;
|
||||||
|
final String? tempDir;
|
||||||
|
|
||||||
|
static String fileName(Target target, String name) {
|
||||||
|
return '${target.rust}_$name';
|
||||||
|
}
|
||||||
|
|
||||||
|
static String signatureFileName(Target target, String name) {
|
||||||
|
return '${target.rust}_$name.sig';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> run() async {
|
||||||
|
final crateInfo = CrateInfo.load(manifestDir);
|
||||||
|
|
||||||
|
final targets = List.of(this.targets);
|
||||||
|
if (targets.isEmpty) {
|
||||||
|
targets.addAll([
|
||||||
|
...Target.buildableTargets(),
|
||||||
|
if (androidSdkLocation != null) ...Target.androidTargets(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.info('Precompiling binaries for $targets');
|
||||||
|
|
||||||
|
final hash = CrateHash.compute(manifestDir);
|
||||||
|
_log.info('Computed crate hash: $hash');
|
||||||
|
|
||||||
|
final String tagName = 'precompiled_$hash';
|
||||||
|
|
||||||
|
final github = GitHub(auth: Authentication.withToken(githubToken));
|
||||||
|
final repo = github.repositories;
|
||||||
|
final release = await _getOrCreateRelease(
|
||||||
|
repo: repo,
|
||||||
|
tagName: tagName,
|
||||||
|
packageName: crateInfo.packageName,
|
||||||
|
hash: hash,
|
||||||
|
);
|
||||||
|
|
||||||
|
final tempDir = this.tempDir != null
|
||||||
|
? Directory(this.tempDir!)
|
||||||
|
: Directory.systemTemp.createTempSync('precompiled_');
|
||||||
|
|
||||||
|
tempDir.createSync(recursive: true);
|
||||||
|
|
||||||
|
final crateOptions = CargokitCrateOptions.load(
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
);
|
||||||
|
|
||||||
|
final buildEnvironment = BuildEnvironment(
|
||||||
|
configuration: BuildConfiguration.release,
|
||||||
|
crateOptions: crateOptions,
|
||||||
|
targetTempDir: tempDir.path,
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
crateInfo: crateInfo,
|
||||||
|
isAndroid: androidSdkLocation != null,
|
||||||
|
androidSdkPath: androidSdkLocation,
|
||||||
|
androidNdkVersion: androidNdkVersion,
|
||||||
|
androidMinSdkVersion: androidMinSdkVersion,
|
||||||
|
);
|
||||||
|
|
||||||
|
final rustup = Rustup();
|
||||||
|
|
||||||
|
for (final target in targets) {
|
||||||
|
final artifactNames = getArtifactNames(
|
||||||
|
target: target,
|
||||||
|
libraryName: crateInfo.packageName,
|
||||||
|
remote: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (artifactNames.every((name) {
|
||||||
|
final fileName = PrecompileBinaries.fileName(target, name);
|
||||||
|
return (release.assets ?? []).any((e) => e.name == fileName);
|
||||||
|
})) {
|
||||||
|
_log.info("All artifacts for $target already exist - skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.info('Building for $target');
|
||||||
|
|
||||||
|
final builder =
|
||||||
|
RustBuilder(target: target, environment: buildEnvironment);
|
||||||
|
builder.prepare(rustup);
|
||||||
|
final res = await builder.build();
|
||||||
|
|
||||||
|
final assets = <CreateReleaseAsset>[];
|
||||||
|
for (final name in artifactNames) {
|
||||||
|
final file = File(path.join(res, name));
|
||||||
|
if (!file.existsSync()) {
|
||||||
|
throw Exception('Missing artifact: ${file.path}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = file.readAsBytesSync();
|
||||||
|
final create = CreateReleaseAsset(
|
||||||
|
name: PrecompileBinaries.fileName(target, name),
|
||||||
|
contentType: "application/octet-stream",
|
||||||
|
assetData: data,
|
||||||
|
);
|
||||||
|
final signature = sign(privateKey, data);
|
||||||
|
final signatureCreate = CreateReleaseAsset(
|
||||||
|
name: signatureFileName(target, name),
|
||||||
|
contentType: "application/octet-stream",
|
||||||
|
assetData: signature,
|
||||||
|
);
|
||||||
|
bool verified = verify(public(privateKey), data, signature);
|
||||||
|
if (!verified) {
|
||||||
|
throw Exception('Signature verification failed');
|
||||||
|
}
|
||||||
|
assets.add(create);
|
||||||
|
assets.add(signatureCreate);
|
||||||
|
}
|
||||||
|
_log.info('Uploading assets: ${assets.map((e) => e.name)}');
|
||||||
|
for (final asset in assets) {
|
||||||
|
// This seems to be failing on CI so do it one by one
|
||||||
|
int retryCount = 0;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
await repo.uploadReleaseAssets(release, [asset]);
|
||||||
|
break;
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (retryCount == 10) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
++retryCount;
|
||||||
|
_log.shout(
|
||||||
|
'Upload failed (attempt $retryCount, will retry): ${e.toString()}');
|
||||||
|
await Future.delayed(Duration(seconds: 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.info('Cleaning up');
|
||||||
|
tempDir.deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Release> _getOrCreateRelease({
|
||||||
|
required RepositoriesService repo,
|
||||||
|
required String tagName,
|
||||||
|
required String packageName,
|
||||||
|
required String hash,
|
||||||
|
}) async {
|
||||||
|
Release release;
|
||||||
|
try {
|
||||||
|
_log.info('Fetching release $tagName');
|
||||||
|
release = await repo.getReleaseByTagName(repositorySlug, tagName);
|
||||||
|
} on ReleaseNotFound {
|
||||||
|
_log.info('Release not found - creating release $tagName');
|
||||||
|
release = await repo.createRelease(
|
||||||
|
repositorySlug,
|
||||||
|
CreateRelease.from(
|
||||||
|
tagName: tagName,
|
||||||
|
name: 'Precompiled binaries ${hash.substring(0, 8)}',
|
||||||
|
targetCommitish: null,
|
||||||
|
isDraft: false,
|
||||||
|
isPrerelease: false,
|
||||||
|
body: 'Precompiled binaries for crate $packageName, '
|
||||||
|
'crate hash $hash.',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return release;
|
||||||
|
}
|
||||||
|
}
|
||||||
136
rust_builder/cargokit/build_tool/lib/src/rustup.dart
Normal file
136
rust_builder/cargokit/build_tool/lib/src/rustup.dart
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
class _Toolchain {
|
||||||
|
_Toolchain(
|
||||||
|
this.name,
|
||||||
|
this.targets,
|
||||||
|
);
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final List<String> targets;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Rustup {
|
||||||
|
List<String>? installedTargets(String toolchain) {
|
||||||
|
final targets = _installedTargets(toolchain);
|
||||||
|
return targets != null ? List.unmodifiable(targets) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void installToolchain(String toolchain) {
|
||||||
|
log.info("Installing Rust toolchain: $toolchain");
|
||||||
|
runCommand("rustup", ['toolchain', 'install', toolchain]);
|
||||||
|
_installedToolchains
|
||||||
|
.add(_Toolchain(toolchain, _getInstalledTargets(toolchain)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void installTarget(
|
||||||
|
String target, {
|
||||||
|
required String toolchain,
|
||||||
|
}) {
|
||||||
|
log.info("Installing Rust target: $target");
|
||||||
|
runCommand("rustup", [
|
||||||
|
'target',
|
||||||
|
'add',
|
||||||
|
'--toolchain',
|
||||||
|
toolchain,
|
||||||
|
target,
|
||||||
|
]);
|
||||||
|
_installedTargets(toolchain)?.add(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<_Toolchain> _installedToolchains;
|
||||||
|
|
||||||
|
Rustup() : _installedToolchains = _getInstalledToolchains();
|
||||||
|
|
||||||
|
List<String>? _installedTargets(String toolchain) => _installedToolchains
|
||||||
|
.firstWhereOrNull(
|
||||||
|
(e) => e.name == toolchain || e.name.startsWith('$toolchain-'))
|
||||||
|
?.targets;
|
||||||
|
|
||||||
|
static List<_Toolchain> _getInstalledToolchains() {
|
||||||
|
String extractToolchainName(String line) {
|
||||||
|
// ignore (default) after toolchain name
|
||||||
|
final parts = line.split(' ');
|
||||||
|
return parts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
final res = runCommand("rustup", ['toolchain', 'list']);
|
||||||
|
|
||||||
|
// To list all non-custom toolchains, we need to filter out lines that
|
||||||
|
// don't start with "stable", "beta", or "nightly".
|
||||||
|
Pattern nonCustom = RegExp(r"^(stable|beta|nightly)");
|
||||||
|
final lines = res.stdout
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.where((e) => e.isNotEmpty && e.startsWith(nonCustom))
|
||||||
|
.map(extractToolchainName)
|
||||||
|
.toList(growable: true);
|
||||||
|
|
||||||
|
return lines
|
||||||
|
.map(
|
||||||
|
(name) => _Toolchain(
|
||||||
|
name,
|
||||||
|
_getInstalledTargets(name),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(growable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> _getInstalledTargets(String toolchain) {
|
||||||
|
final res = runCommand("rustup", [
|
||||||
|
'target',
|
||||||
|
'list',
|
||||||
|
'--toolchain',
|
||||||
|
toolchain,
|
||||||
|
'--installed',
|
||||||
|
]);
|
||||||
|
final lines = res.stdout
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.where((e) => e.isNotEmpty)
|
||||||
|
.toList(growable: true);
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _didInstallRustSrcForNightly = false;
|
||||||
|
|
||||||
|
void installRustSrcForNightly() {
|
||||||
|
if (_didInstallRustSrcForNightly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Useful for -Z build-std
|
||||||
|
runCommand(
|
||||||
|
"rustup",
|
||||||
|
['component', 'add', 'rust-src', '--toolchain', 'nightly'],
|
||||||
|
);
|
||||||
|
_didInstallRustSrcForNightly = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? executablePath() {
|
||||||
|
final envPath = Platform.environment['PATH'];
|
||||||
|
final envPathSeparator = Platform.isWindows ? ';' : ':';
|
||||||
|
final home = Platform.isWindows
|
||||||
|
? Platform.environment['USERPROFILE']
|
||||||
|
: Platform.environment['HOME'];
|
||||||
|
final paths = [
|
||||||
|
if (home != null) path.join(home, '.cargo', 'bin'),
|
||||||
|
if (envPath != null) ...envPath.split(envPathSeparator),
|
||||||
|
];
|
||||||
|
for (final p in paths) {
|
||||||
|
final rustup = Platform.isWindows ? 'rustup.exe' : 'rustup';
|
||||||
|
final rustupPath = path.join(p, rustup);
|
||||||
|
if (File(rustupPath).existsSync()) {
|
||||||
|
return rustupPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
140
rust_builder/cargokit/build_tool/lib/src/target.dart
Normal file
140
rust_builder/cargokit/build_tool/lib/src/target.dart
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
class Target {
|
||||||
|
Target({
|
||||||
|
required this.rust,
|
||||||
|
this.flutter,
|
||||||
|
this.android,
|
||||||
|
this.androidMinSdkVersion,
|
||||||
|
this.darwinPlatform,
|
||||||
|
this.darwinArch,
|
||||||
|
});
|
||||||
|
|
||||||
|
static final all = [
|
||||||
|
Target(
|
||||||
|
rust: 'armv7-linux-androideabi',
|
||||||
|
flutter: 'android-arm',
|
||||||
|
android: 'armeabi-v7a',
|
||||||
|
androidMinSdkVersion: 16,
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'aarch64-linux-android',
|
||||||
|
flutter: 'android-arm64',
|
||||||
|
android: 'arm64-v8a',
|
||||||
|
androidMinSdkVersion: 21,
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'i686-linux-android',
|
||||||
|
flutter: 'android-x86',
|
||||||
|
android: 'x86',
|
||||||
|
androidMinSdkVersion: 16,
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'x86_64-linux-android',
|
||||||
|
flutter: 'android-x64',
|
||||||
|
android: 'x86_64',
|
||||||
|
androidMinSdkVersion: 21,
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'x86_64-pc-windows-msvc',
|
||||||
|
flutter: 'windows-x64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'x86_64-unknown-linux-gnu',
|
||||||
|
flutter: 'linux-x64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'aarch64-unknown-linux-gnu',
|
||||||
|
flutter: 'linux-arm64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'x86_64-apple-darwin',
|
||||||
|
darwinPlatform: 'macosx',
|
||||||
|
darwinArch: 'x86_64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'aarch64-apple-darwin',
|
||||||
|
darwinPlatform: 'macosx',
|
||||||
|
darwinArch: 'arm64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'aarch64-apple-ios',
|
||||||
|
darwinPlatform: 'iphoneos',
|
||||||
|
darwinArch: 'arm64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'aarch64-apple-ios-sim',
|
||||||
|
darwinPlatform: 'iphonesimulator',
|
||||||
|
darwinArch: 'arm64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'x86_64-apple-ios',
|
||||||
|
darwinPlatform: 'iphonesimulator',
|
||||||
|
darwinArch: 'x86_64',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
static Target? forFlutterName(String flutterName) {
|
||||||
|
return all.firstWhereOrNull((element) => element.flutter == flutterName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Target? forDarwin({
|
||||||
|
required String platformName,
|
||||||
|
required String darwinAarch,
|
||||||
|
}) {
|
||||||
|
return all.firstWhereOrNull((element) => //
|
||||||
|
element.darwinPlatform == platformName &&
|
||||||
|
element.darwinArch == darwinAarch);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Target? forRustTriple(String triple) {
|
||||||
|
return all.firstWhereOrNull((element) => element.rust == triple);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Target> androidTargets() {
|
||||||
|
return all
|
||||||
|
.where((element) => element.android != null)
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns buildable targets on current host platform ignoring Android targets.
|
||||||
|
static List<Target> buildableTargets() {
|
||||||
|
if (Platform.isLinux) {
|
||||||
|
// Right now we don't support cross-compiling on Linux. So we just return
|
||||||
|
// the host target.
|
||||||
|
final arch = runCommand('arch', []).stdout as String;
|
||||||
|
if (arch.trim() == 'aarch64') {
|
||||||
|
return [Target.forRustTriple('aarch64-unknown-linux-gnu')!];
|
||||||
|
} else {
|
||||||
|
return [Target.forRustTriple('x86_64-unknown-linux-gnu')!];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return all.where((target) {
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
return target.rust.contains('-windows-');
|
||||||
|
} else if (Platform.isMacOS) {
|
||||||
|
return target.darwinPlatform != null;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}).toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return rust;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String? flutter;
|
||||||
|
final String rust;
|
||||||
|
final String? android;
|
||||||
|
final int? androidMinSdkVersion;
|
||||||
|
final String? darwinPlatform;
|
||||||
|
final String? darwinArch;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user