diff --git a/CHANGELOG.md b/CHANGELOG.md index b558ea8..383d55f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,7 +85,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Specs Designed in the Phase 7 detailed-design update (00-common-specs.md 0.4.6/0.4.7, 01-setup-settings.md) but not implemented -on either platform yet — tracked for a later release: +on either platform yet — this is the rest of Phase 7's scope (SP3 the bulk lane, SP4 Wi-Fi Direct) still to be done, open for a later topic: - **Wi-Fi Direct** (A7.5 / M7.5): randomly-generated SSID and passphrase per group, the Mac socket bound to the P2P interface only, and the Android 10–12 Location prompt it needs. No Android or Apple code exists for this today. diff --git a/CHANGELOG.vi.md b/CHANGELOG.vi.md index a60ad29..3889a1c 100644 --- a/CHANGELOG.vi.md +++ b/CHANGELOG.vi.md @@ -47,7 +47,7 @@ Các thay đổi đáng chú ý của kho hub HandLive (tài liệu và kế ho ### Specs Đã thiết kế trong bản cập nhật spec Phase 7 (00-common-specs.md 0.4.6/0.4.7, 01-setup-settings.md) nhưng chưa code trên -nền tảng nào — để lại cho bản phát hành sau: +nền tảng nào — đây là phần còn lại của Phase 7 (SP3 làn bulk, SP4 Wi-Fi Direct) vẫn phải làm, để lại cho topic sau: - **Wi-Fi Direct** (A7.5 / M7.5): SSID và passphrase sinh ngẫu nhiên mỗi nhóm, socket Mac ràng buộc chỉ với interface P2P, và prompt Location Android 10–12 mà nó cần. Chưa có code Android hay Apple nào cho phần này. diff --git a/CLAUDE.md b/CLAUDE.md index 2cb5308..e72c8fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,12 +3,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## Project status: Phase 0–3 on `main`; Phase 5–6 spikes on `main`; Phase 7 (Bluetooth offline connectivity) started 2026-10-28, BLE L2CAP pairing/control channel on `main`, release `v0.2.0` pending the owner's tag (Wi-Fi Direct and relay bulk dropped from this phase's scope); latest tagged release `v0.1.1` (2026-10-22); G1/G2/G5/G6 open; G4 closed (Phase 4 stopped, owner decision 2026-10-12) +## Project status: Phase 0–3 on `main`; Phase 5–6 spikes on `main`; Phase 7 (Bluetooth offline connectivity) in progress — SP1 (BLE L2CAP control channel) and SP2 (BLE pairing) released in `v0.2.0` (2026-10-30) on `main`; SP3 (per-connection bulk lane, `K_bulk`) and SP4 (Wi-Fi Direct) are specced but not yet implemented, open for a later topic; only bulk-over-relay is out of scope (dropped 2026-10-28); latest tagged release `v0.2.0` (2026-10-30); G1/G2/G5/G6 open; G4 closed (Phase 4 stopped, owner decision 2026-10-12) Phase 0–3 product code is on `main` (clipboard, SMS, iOS shell, Rust relay, call metadata/control). Phase 4–6 **spike probes** are also on `main` (merged 2026-09-30): `HFPSpike`, `CameraSpike`, Android `tools/web-spike`, Apple `Tools/WebSpike`. Coordinated public betas: **`v0.1.0-beta.1`**, **`v0.1.0-beta.2`** (2026-10-04, the first with installable files) and **`v0.1.0-beta.3`** (2026-10-07) on hub/android/apple/shared/relay -(not a store release), then **`v0.1.1`** (2026-10-22), the first tag without a pre-release suffix (still not a store release; Google Play readiness on Android, paid-team identifiers on iOS). Live progress board: hub `README.md` / `README.vi.md` (Roadmap and progress). Still +(not a store release), then **`v0.1.1`** (2026-10-22), the first tag without a pre-release suffix (still not a store release; Google Play readiness on Android, paid-team identifiers on iOS), then **`v0.2.0`** (2026-10-30), Phase 7 Bluetooth offline pairing (still not a store release). Live progress board: hub `README.md` / `README.vi.md` (Roadmap and progress). Still required before 1.0: gates **G1** (device matrix, TalkBack/VoiceOver) and **G2** (Play Console), Phase 2/3 checks on a real relay / APNs / FCM, and spike gates **G5** (paid Apple Developer team), **G6** (browser go/no-go). Owner decision 2026-09-28: Phases 5 and 6 proceed before Phase 4. Owner decision 2026-10-12 stops Phase 4 itself and @@ -61,7 +61,7 @@ project owner in Vietnamese with diacritics. **Commit identity:** always `Hồ Xuân Dũng ` (GitHub `xuandung38`). Never `dunghx1@viettel.com.vn` or any Viettel email in author/committer/`Signed-off-by`. Use `env -u CURSOR_AGENT git commit` so Cursor does not inject Co-authored-by trailers. -**Workspace tips to treat as current `main`** (ahead of tag `v0.1.1`; release `v0.2.0` pending the owner's tag, Phase 7 Bluetooth offline connectivity): shared `5cbe4aa`, relay `17f367b` (unchanged since `v0.1.1`), android `ed8e41f`, apple `7d3ce0b`, hub `main` tip. Confirm with `tools/workspace.sh status` before coding. **Local JDK:** Gradle needs `JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home` in a non-login shell, else `gradlew` prints "Unable to locate a Java Runtime" and a piped command still exits 0. +**Workspace tips to treat as current `main`:** tagged `v0.2.0` (2026-10-30) at hub `a9c7193`, shared `6e44aa1`, android `cd4527a`, apple `36f8cc8`, relay `17f367b` (unchanged since `v0.1.1`). Confirm with `tools/workspace.sh status` before coding. **Local JDK:** Gradle needs `JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home` in a non-login shell, else `gradlew` prints "Unable to locate a Java Runtime" and a piped command still exits 0. ### In progress / blocked (owner or gate) @@ -95,10 +95,12 @@ project owner in Vietnamese with diacritics. logs nothing for the bench) and keep HandLive's Notification access on. Then the Mac must show "Telegram Call"; swiping Telegram's in-call notification must keep the Mac panel in-call with End hidden, and hanging up must close it. Then run `call_latency.py` on a real app call for T3.3. -9. **Next release:** follow `docs/deployment-guide.md` (Cutting a release) and the 0.1.1 scan report: security scan of - everything since the last tag first, then versions, changelogs, website link and badge, tags (hub and shared first). - Open from the 0.1.1 scan: wrap the iOS UI-test seams (`-HLUITest*` launch arguments) and their `/tmp` debug writes - in `#if DEBUG`, fix the `project.yml` comment that calls `HandLiveiOSUITests` uncommitted. +9. **Next release:** follow `docs/deployment-guide.md` (Cutting a release) and the 0.2.0 scan report + (`reports/security-scan-0.2.0-2026-10-30.md`): security scan of everything since the last tag first, then versions, + changelogs, website link and badge, tags (hub and shared first). The 0.2.0 scan found nothing: 0 critical, 0 high, + 0 medium, 0 low. Still open, carried over from the 0.1.1 scan (untouched by Phase 7): wrap the iOS UI-test seams + (`-HLUITest*` launch arguments) and their `/tmp` debug writes in `#if DEBUG`, fix the `project.yml` comment that + calls `HandLiveiOSUITests` uncommitted (it is tracked in git). 10. **Open follow-ups** (no gate): `ManualClock` review debt, apple #5 being merged (`waitForSleepers` ignores cancellation; test helpers' `clock:`/`timer:` swapped); keep `-parallel-testing-enabled NO` (parallel saves ~0 s); measure the app-call label on Android 11–14 to know whether the launcher query matters there. The e2e `setup` check of diff --git a/README.md b/README.md index 4755f53..1b6ade8 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,13 @@ English | [Tiếng Việt](README.vi.md) [![ci-docs](https://github.com/HandLive/handlive/actions/workflows/ci-docs.yml/badge.svg)](https://github.com/HandLive/handlive/actions/workflows/ci-docs.yml) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) -[![Release](https://img.shields.io/badge/release-v0.1.1-orange.svg)](https://github.com/HandLive/handlive/releases/tag/v0.1.1) +[![Release](https://img.shields.io/badge/release-v0.2.0-orange.svg)](https://github.com/HandLive/handlive/releases/tag/v0.2.0) HandLive connects Android devices with macOS, iOS, and iPadOS. It syncs your clipboard, bridges SMS conversations, and delivers call notifications with action controls—without cloud lock-in. End-to-end encryption is always on, and the entire project is open source under Apache-2.0. ## Get the beta -Download the latest release [`v0.1.1`](https://github.com/HandLive/handlive/releases/tag/v0.1.1) (2026-10-22): +Download the latest release [`v0.2.0`](https://github.com/HandLive/handlive/releases/tag/v0.2.0) (2026-10-30): - **macOS:** Ad-hoc signed DMG (choose *Open Anyway* on first run). - **Android:** Signed `foss` APK. Its application id is new (`com.handlive.android`): remove a beta install first, then pair again. - **iOS / iPadOS:** Unsigned IPA for sideloading. A beta install updated to this build must pair again (new App Group). @@ -67,7 +67,7 @@ Phases ship in order, each as a usable slice. One exception (owner, 2026-09-28): | **4** Call audio | Bluetooth HFP/SCO and Opus fallback | **Stopped** (owner decision, 2026-10-12) · G4 closed: only macOS 27 is available, so calls stay notification + answer/decline/end, no audio | | **5** Virtual camera / mic | CMIOExtension and AudioServerPlugin | **Spike on `main`** (`CameraSpike`) · G5 needs a paid Apple Developer team | | **6** Continue Browsing | Foreground URL handover across devices | **Spike on `main`** · Chrome and Samsung Internet measured; browser go/no-go pending | -| **7** Connect anywhere | Auto link without a shared Wi-Fi network | **Started** (owner decision, 2026-10-28), no wait on G4/G5/G6 · spike G7 done; Bluetooth L2CAP pairing and control channel on `main`, Wi-Fi Direct and relay bulk dropped from this phase's scope | +| **7** Connect anywhere | Auto link without a shared Wi-Fi network | **In progress** · SP1+SP2 (Bluetooth control channel; QR flow B, PIN flow C via CPace-X25519-SHA512) **released in `v0.2.0`** (2026-10-30) on `main`; SP3 (per-connection bulk lane, `K_bulk`) and SP4 (Wi-Fi Direct) are specced but not yet implemented; only bulk-over-relay is dropped from scope | | **G0** Vectors | Shared cross-platform crypto test vectors | **Done** | | **G1** Real devices | Real-device Phase 1 matrix validation | **Open** (before 1.0) · App Store Connect screenshots recaptured at native 1320×2868 (2026-10-12) | | **G2** Play Console | SMS and call log permissions declaration | **Open** (before 1.0; Plan B: F-Droid/APK) · Play Console screenshots recaptured at 1080×1920 (2026-10-12) | diff --git a/README.vi.md b/README.vi.md index 0b74981..e359472 100644 --- a/README.vi.md +++ b/README.vi.md @@ -8,13 +8,13 @@ [![ci-docs](https://github.com/HandLive/handlive/actions/workflows/ci-docs.yml/badge.svg)](https://github.com/HandLive/handlive/actions/workflows/ci-docs.yml) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) -[![Release](https://img.shields.io/badge/release-v0.1.1-orange.svg)](https://github.com/HandLive/handlive/releases/tag/v0.1.1) +[![Release](https://img.shields.io/badge/release-v0.2.0-orange.svg)](https://github.com/HandLive/handlive/releases/tag/v0.2.0) HandLive kết nối điện thoại Android với các thiết bị macOS, iOS và iPadOS. Ứng dụng đồng bộ bảng nhớ tạm, chuyển tiếp tin nhắn SMS và hiển thị thông báo cuộc gọi kèm nút thao tác—hoàn toàn không phụ thuộc vào đám mây. Mã hóa đầu cuối luôn bật, và toàn bộ dự án là mã nguồn mở theo giấy phép Apache-2.0. ## Tải bản beta -Tải bản phát hành mới nhất [`v0.1.1`](https://github.com/HandLive/handlive/releases/tag/v0.1.1) (22/10/2026): +Tải bản phát hành mới nhất [`v0.2.0`](https://github.com/HandLive/handlive/releases/tag/v0.2.0) (30/10/2026): - **macOS:** Tệp DMG ký ad-hoc (chọn *Open Anyway* trong lần mở đầu tiên). - **Android:** Tệp APK `foss` đã ký. Mã ứng dụng đã đổi (`com.handlive.android`): gỡ bản beta cũ trước, rồi ghép đôi lại. - **iOS / iPadOS:** Tệp IPA chưa ký để sideload. Bản beta cập nhật lên bản này phải ghép đôi lại (App Group mới). @@ -67,7 +67,7 @@ HandLive xây lần lượt. Mỗi phase là một phần dùng được. Ngoạ | **4** Âm thanh cuộc gọi | Bluetooth HFP/SCO và dự phòng Opus | **Dừng** (quyết định của chủ dự án, 12/10/2026) · G4 đóng: chỉ có macOS 27, cuộc gọi chỉ còn thông báo và nút nghe/từ chối/kết thúc, không âm thanh | | **5** Camera / mic ảo | CMIOExtension và AudioServerPlugin | **Spike trên `main`** (`CameraSpike`) · G5 cần Apple Developer trả phí | | **6** Duyệt web tiếp | Chuyển tiếp URL đang mở giữa các thiết bị | **Spike trên `main`** · đã đo Chrome và Samsung Internet; còn go/no-go trình duyệt | -| **7** Kết nối mọi nơi | Tự kết nối khi không chung mạng Wi-Fi | **Đã bắt đầu** (quyết định chủ dự án, 28/10/2026), không chờ G4/G5/G6 · spike G7 xong; ghép đôi và kênh điều khiển qua Bluetooth L2CAP đã lên `main`, Wi-Fi Direct và bulk qua relay bị cắt khỏi phạm vi phase này | +| **7** Kết nối mọi nơi | Tự kết nối khi không chung mạng Wi-Fi | **Đang làm** · SP1+SP2 (kênh điều khiển Bluetooth; luồng B QR, luồng C PIN qua CPace-X25519-SHA512) **đã phát hành ở `v0.2.0`** (30/10/2026) trên `main`; SP3 (làn bulk theo từng kết nối, `K_bulk`) và SP4 (Wi-Fi Direct) đã có đặc tả nhưng chưa code; chỉ bulk qua relay bị bỏ khỏi phạm vi | | **G0** Vector mã hóa | Vector kiểm thử mã hóa dùng chung giữa các nền tảng | **Xong** | | **G1** Máy thật | Kiểm thực tế ma trận máy thật Phase 1 | **Mở** (trước 1.0) · ảnh App Store Connect đã chụp lại ở kích thước gốc 1320×2868 (12/10/2026) | | **G2** Play Console | Tờ khai quyền SMS và nhật ký cuộc gọi | **Mở** (trước 1.0; phương án B: F-Droid/APK) · ảnh Play Console đã chụp lại ở 1080×1920 (12/10/2026) | diff --git a/docs/detailed-design/00-common-specs.md b/docs/detailed-design/00-common-specs.md index aa3b8c7..8e33d45 100644 --- a/docs/detailed-design/00-common-specs.md +++ b/docs/detailed-design/00-common-specs.md @@ -280,7 +280,7 @@ message). Plaintext: | Offset | Length | Field | Description | |---|---|---|---| | 0 | 1 | `chan` | `0x01` control, `0x02` bulk (content: 0.4.7); any other value → close the link | -| 1 | 1 | `flags` | bit0 `more`: further fragments of the same message follow; bit1 `long`: the fragment belongs to the long lane of the control channel (channel `0x01` only, below); other bits 0 | +| 1 | 1 | `flags` | bit0 `more`: further fragments of the same message follow; bit1 `long`: the fragment belongs to the long lane of the control channel (channel `0x01` only, below; `long` = 1 on channel `0x02` → close the link, 0.4.7 bulk rule 5); other bits 0 | | 2 | 2 | `dlen` | uint16 BE, length of `data`, ≤ `BT_FRAGMENT_MAX` | | 4 | `dlen` | `data` | A fragment of one message | | 4 + `dlen` | rest | padding | Zero bytes up to the smallest of 256, 1,024 or 4,096 bytes that holds the frame | @@ -488,6 +488,114 @@ carries them depends on the route. Every other envelope, including inline clipbo RTT of 3 s still arrives well within it). Over Bluetooth, progress is shown for transfers of at least `BULK_DIRECT_MIN` (CLIP-03 field 2) with the line of CLIP-03 field 10. +#### Bulk lane on the Bluetooth link (channel `0x02`) + +The rules below are numbered so that tests can cite them ("0.4.7 bulk rule N"). They apply to a Bluetooth session. +On a LAN or relay session there is a single lane (`/v1/ctl`) and the lane argument is ignored. `/v1/stream/bulk` on +Wi-Fi Direct is outside these rules. + +1. **What goes on the bulk lane.** Exactly the envelopes of one CLIP-03 transfer, in both directions: + - `clipboard/push` with `transfer`; + - every `clipboard/chunk`; + - `clipboard/cancel`, any `reason`, from either side; + - the `ack` of a `push` with `transfer`: `applied`, `ignored` (`cancelled`, `conflict`…) or an error + (`CLIP_CHECKSUM_MISMATCH`, `BAD_REQUEST`, `INTERNAL`…); + - the `clipboard/conflict` that follows such an `ack` (CLIP-01 API 6), so it cannot overtake that `ack`. + + Every other envelope stays on control channel `0x01`: an inline `clipboard/push` and its `ack`, the + `clipboard/conflict` after an inline push, `session/*`, `ping/*` and every other `type`. +2. **The sender picks the lane; the transport never guesses.** An inline push and a transfer push share `type` = + `clipboard`, and every response has `type` = `ack`, so the lane cannot be read from the envelope. The module that + builds the envelope (the clipboard engine) passes the lane on every send path; the default is control. + Android (`core/transport`, `feature/connection`): + + ```kotlin + // core/transport + enum class EnvelopeLane { CONTROL, BULK } + + /** A session socket with a separate bulk lane (the Bluetooth link); other sockets carry both lanes as text. */ + interface BulkLaneSocket { + /** Queues [envelope] on channel 0x02; suspends while bulk rule 4 holds the queue full. */ + suspend fun sendBulk(envelope: String) + } + + // EncryptedEnvelopeChannel + suspend fun send(type: String, plaintext: ByteArray, id: String = ids.next(), + lane: EnvelopeLane = EnvelopeLane.CONTROL): String + // ControlSession + suspend fun send(type: MessageType, plaintext: ByteArray, id: String? = null, + lane: EnvelopeLane = EnvelopeLane.CONTROL): String + + // feature/connection + fun interface EnvelopeSender { + suspend fun send(type: MessageType, plaintext: ByteArray, id: String, lane: EnvelopeLane) + } + // PeerSession + suspend fun send(type: MessageType, plaintext: ByteArray, lane: EnvelopeLane = EnvelopeLane.CONTROL): String + suspend fun request(type: MessageType, plaintext: ByteArray, + timeout: Duration = TransportConstants.REQUEST_TIMEOUT, + lane: EnvelopeLane = EnvelopeLane.CONTROL, + afterSend: suspend (String) -> Unit = {}): Ack + suspend fun sendAck(ack: Ack, lane: EnvelopeLane = EnvelopeLane.CONTROL) + suspend fun sendError(re: String, code: ErrorCode, message: String, + lane: EnvelopeLane = EnvelopeLane.CONTROL) + ``` + + `EncryptedEnvelopeChannel` writes a `BULK` envelope with `sendBulk` when its socket is a `BulkLaneSocket` + (`BluetoothPeerSocket`), otherwise as `Frame.Text` like a control envelope. macOS (`HLTransport`): + + ```swift + public enum EnvelopeLane: Sendable, Equatable { case control, bulk } + + public enum ChannelMessage: Sendable, Equatable { + case text(String) + case binary(Data) + /// An envelope (0.5.1) on the bulk lane (0.4.7). Send only: a channel without a separate bulk lane sends it + /// as a text frame; `receive()` never returns it. + case bulkText(String) + } + + // ControlSession + public func send(_ type: MessageType, op: String, data: Body, + lane: EnvelopeLane = .control) async throws + public func sendRequest(_ type: MessageType, op: String, data: Body, + id: String = HLUUID.v7(), + lane: EnvelopeLane = .control) async throws -> PendingAck + public func sendBinary(_ type: MessageType, plaintext: Data, lane: EnvelopeLane = .control) async throws + public func reply(to requestId: String, with ack: Ack, lane: EnvelopeLane = .control) async throws + func transmit(_ envelope: Envelope, lane: EnvelopeLane = .control) async throws + ``` + + `transmit` sends `.bulkText` for `.bulk` and `.text` for `.control`. An `ack` kept to answer duplicates (0.5.1 + rule 2) is resent on the lane it was first sent on. +3. **Writing on the link.** The Bluetooth socket puts a bulk envelope (UTF-8, the same bytes as the WS text frame on + `/v1/ctl`) in the bulk queue of the 0.4.5 scheduler, as fragments on channel `0x02` with `long` = 0, `more` = 1 + on every fragment except the last, and `dlen` ≤ `BT_BULK_FRAGMENT_MAX`. One bulk message at a time per direction: + its fragments are never interleaved with those of another bulk message; control fragments may go between them + (0.4.5 "Priority"). +4. **Backpressure.** The bulk send call (`sendBulk` on Android; `send(.bulkText)` on macOS) does not return while + `BT_BULK_QUEUED` bulk envelopes are already queued and not fully written. On Bluetooth this replaces the CLIP-03 + "at most 4 chunks in the send buffer". An envelope is sealed with the session keys when it is queued, and the old + keys are kept only 30 s after a rekey (0.6.3 step 6): 4 chunk envelopes (about 350 KiB, 27 s at 12.9 KB/s) could + outlive them, 2 take about 14 s. +5. **Reading on the link.** The receiver reassembles channel `0x02` in a buffer of its own (not the long control + lane's) and closes the link without an answer (CONN-05 E8) when a `0x02` fragment has `long` = 1, a `0x02` + fragment has `dlen` over `BT_BULK_FRAGMENT_MAX`, or a bulk message passes `BT_MESSAGE_MAX` before its last + fragment. A `chan` other than `0x01` and `0x02` still closes the link (0.4.5). +6. **Handing up.** A complete bulk message goes to the session as envelope text, like a complete control message: + on Android as `Frame.Text` into the same `/v1/ctl` pipeline, on macOS as `ChannelMessage.text`. It goes through the + same decryption, the same `DEDUP_WINDOW` and the same dispatcher as the control channel. Invalid UTF-8 or an + invalid envelope is handled exactly like the same bytes in a WS text frame on `/v1/ctl`. The receiver does not + check which lane an envelope came on; rule 1 binds the sender only. On macOS `ChannelMessage.binary` stays the HL + frame of 0.5.2 and never travels on the Bluetooth link: `BluetoothChannel.send(.binary)` throws. +7. **Order.** Each channel keeps the sender's order; there is no order between channels. Every envelope of one + transfer travels on `0x02`, so they arrive in order (CLIP-03 API 4 rule 2 still holds). A control envelope may + overtake a bulk one: an inline `push` of a newer clip can arrive before the `cancel` `superseded` of the older + transfer, and CLIP-03 API 3 rule 2 already covers it (the new `push` replaces the old transfer). +8. **End of the link.** A bulk message still being reassembled when the link closes is dropped, and so are the + sender's queued bulk envelopes. The transfer then follows CLIP-03 E8, or E11 when a LAN session replaces the + Bluetooth one. + ## 0.5 Message frames ### 0.5.1 JSON envelope (WS text frame) @@ -1389,6 +1497,7 @@ SET-02 function manages these keys. | `BULK_DIRECT_MIN` | 128 KiB | Over Bluetooth, transfers this large or larger wait for Wi-Fi Direct first (0.4.7); about 10 s at the G7 rate of 12.9 KB/s | | `BT_BULK_FRAGMENT_MAX` | 1,020 bytes | `dlen` of a fragment on the long control lane (channel `0x01`, `long` = 1) and of a bulk fragment on channel `0x02` (fits the 1,040-byte record, about 80 ms of air time) | | `BT_BULK_INFLIGHT` | 2 records | Long-control and bulk records together written to the L2CAP stream and not yet drained; bounds how long a short control envelope waits (AC1) | +| `BT_BULK_QUEUED` | 2 envelopes | Bulk envelopes queued on the Bluetooth link and not fully written; the bulk send waits beyond that (0.4.7 bulk rule 4). Keeps a queued chunk within the 30 s the old keys live after a rekey | | `BULK_MAX_RESTARTS` | 2 | Lane-change restarts per clip (0.4.7); after that CLIP-03 E8 | ## 0.11 Client connection states (Mac/iOS) diff --git a/docs/detailed-design/00-common-specs.vi.md b/docs/detailed-design/00-common-specs.vi.md index be5171a..5fead9f 100644 --- a/docs/detailed-design/00-common-specs.vi.md +++ b/docs/detailed-design/00-common-specs.vi.md @@ -273,7 +273,7 @@ Sau bước 4, mọi bản ghi là một Noise transport message: ciphertext c | Offset | Độ dài | Trường | Mô tả | |---|---|---|---| | 0 | 1 | `chan` | `0x01` điều khiển, `0x02` bulk (nội dung: 0.4.7); giá trị khác → đóng link | -| 1 | 1 | `flags` | bit0 `more`: còn mảnh tiếp theo của cùng message; bit1 `long`: mảnh thuộc làn dài của kênh điều khiển (chỉ kênh `0x01`, xem dưới); các bit khác bằng 0 | +| 1 | 1 | `flags` | bit0 `more`: còn mảnh tiếp theo của cùng message; bit1 `long`: mảnh thuộc làn dài của kênh điều khiển (chỉ kênh `0x01`, xem dưới; `long` = 1 trên kênh `0x02` → đóng link, luật bulk 5 của 0.4.7); các bit khác bằng 0 | | 2 | 2 | `dlen` | uint16 BE, độ dài `data`, ≤ `BT_FRAGMENT_MAX` | | 4 | `dlen` | `data` | Một mảnh của một message | | 4 + `dlen` | phần còn lại | đệm | Byte 0, đệm tới cỡ nhỏ nhất trong 256, 1 024 hoặc 4 096 byte đủ chứa frame | @@ -473,6 +473,110 @@ Các lần truyền lớn (ảnh của CLIP-03, văn bản quá `CLIP_INLINE_MAX tới trong hạn). Qua Bluetooth, tiến độ hiện cho các lần truyền từ `BULK_DIRECT_MIN` trở lên (CLIP-03 trường 2) kèm dòng chữ ở CLIP-03 trường 10. +#### Làn bulk trên link Bluetooth (kênh `0x02`) + +Các luật dưới đây được đánh số để test trích dẫn được ("luật bulk N của 0.4.7"). Chúng áp dụng cho phiên Bluetooth. +Phiên LAN hoặc relay chỉ có một làn (`/v1/ctl`) và tham số làn bị bỏ qua. `/v1/stream/bulk` trên Wi-Fi Direct nằm +ngoài các luật này. + +1. **Thứ gì đi làn bulk.** Đúng các envelope của một lần truyền CLIP-03, cả hai chiều: + - `clipboard/push` có `transfer`; + - mọi `clipboard/chunk`; + - `clipboard/cancel`, `reason` nào cũng vậy, từ bên nào cũng vậy; + - `ack` của một `push` có `transfer`: `applied`, `ignored` (`cancelled`, `conflict`…) hoặc lỗi + (`CLIP_CHECKSUM_MISMATCH`, `BAD_REQUEST`, `INTERNAL`…); + - `clipboard/conflict` đi sau một `ack` như thế (CLIP-01 API 6), để nó không vượt được `ack` đó. + + Mọi envelope khác ở lại kênh điều khiển `0x01`: `clipboard/push` gửi thẳng và `ack` của nó, `clipboard/conflict` + sau một push gửi thẳng, `session/*`, `ping/*` và mọi `type` khác. +2. **Bên gửi chọn làn; transport không bao giờ đoán.** Push gửi thẳng và push có `transfer` cùng `type` = + `clipboard`, mọi phản hồi đều có `type` = `ack`, nên không đọc được làn từ envelope. Module dựng envelope (bộ máy + clipboard) truyền làn vào mọi đường gửi; mặc định là điều khiển. Android (`core/transport`, `feature/connection`): + + ```kotlin + // core/transport + enum class EnvelopeLane { CONTROL, BULK } + + /** A session socket with a separate bulk lane (the Bluetooth link); other sockets carry both lanes as text. */ + interface BulkLaneSocket { + /** Queues [envelope] on channel 0x02; suspends while bulk rule 4 holds the queue full. */ + suspend fun sendBulk(envelope: String) + } + + // EncryptedEnvelopeChannel + suspend fun send(type: String, plaintext: ByteArray, id: String = ids.next(), + lane: EnvelopeLane = EnvelopeLane.CONTROL): String + // ControlSession + suspend fun send(type: MessageType, plaintext: ByteArray, id: String? = null, + lane: EnvelopeLane = EnvelopeLane.CONTROL): String + + // feature/connection + fun interface EnvelopeSender { + suspend fun send(type: MessageType, plaintext: ByteArray, id: String, lane: EnvelopeLane) + } + // PeerSession + suspend fun send(type: MessageType, plaintext: ByteArray, lane: EnvelopeLane = EnvelopeLane.CONTROL): String + suspend fun request(type: MessageType, plaintext: ByteArray, + timeout: Duration = TransportConstants.REQUEST_TIMEOUT, + lane: EnvelopeLane = EnvelopeLane.CONTROL, + afterSend: suspend (String) -> Unit = {}): Ack + suspend fun sendAck(ack: Ack, lane: EnvelopeLane = EnvelopeLane.CONTROL) + suspend fun sendError(re: String, code: ErrorCode, message: String, + lane: EnvelopeLane = EnvelopeLane.CONTROL) + ``` + + `EncryptedEnvelopeChannel` ghi envelope `BULK` bằng `sendBulk` khi socket của nó là `BulkLaneSocket` + (`BluetoothPeerSocket`), còn lại ghi thành `Frame.Text` như envelope điều khiển. macOS (`HLTransport`): + + ```swift + public enum EnvelopeLane: Sendable, Equatable { case control, bulk } + + public enum ChannelMessage: Sendable, Equatable { + case text(String) + case binary(Data) + /// An envelope (0.5.1) on the bulk lane (0.4.7). Send only: a channel without a separate bulk lane sends it + /// as a text frame; `receive()` never returns it. + case bulkText(String) + } + + // ControlSession + public func send(_ type: MessageType, op: String, data: Body, + lane: EnvelopeLane = .control) async throws + public func sendRequest(_ type: MessageType, op: String, data: Body, + id: String = HLUUID.v7(), + lane: EnvelopeLane = .control) async throws -> PendingAck + public func sendBinary(_ type: MessageType, plaintext: Data, lane: EnvelopeLane = .control) async throws + public func reply(to requestId: String, with ack: Ack, lane: EnvelopeLane = .control) async throws + func transmit(_ envelope: Envelope, lane: EnvelopeLane = .control) async throws + ``` + + `transmit` gửi `.bulkText` cho `.bulk` và `.text` cho `.control`. `ack` được giữ lại để trả lời bản trùng (0.5.1 + luật 2) được gửi lại trên đúng làn của lần gửi đầu. +3. **Ghi lên link.** Socket Bluetooth đưa envelope bulk (UTF-8, đúng các byte của WS text frame trên `/v1/ctl`) vào + hàng đợi bulk của bộ lập lịch 0.4.5, thành các mảnh trên kênh `0x02` với `long` = 0, `more` = 1 ở mọi mảnh trừ mảnh + cuối, và `dlen` ≤ `BT_BULK_FRAGMENT_MAX`. Mỗi chiều chỉ một message bulk tại một thời điểm: mảnh của nó không bao + giờ xen với mảnh của message bulk khác; mảnh điều khiển được chen vào giữa (0.4.5 "Ưu tiên"). +4. **Chặn ngược.** Lệnh gửi bulk (`sendBulk` trên Android; `send(.bulkText)` trên macOS) không trả về khi đã có + `BT_BULK_QUEUED` envelope bulk trong hàng đợi mà chưa ghi hết. Trên Bluetooth, luật này thay cho "tối đa 4 chunk + trong bộ đệm gửi" của CLIP-03. Envelope được mã hóa bằng khóa phiên lúc vào hàng đợi, mà khóa cũ chỉ được giữ 30 s + sau rekey (0.6.3 bước 6): 4 envelope chunk (khoảng 350 KiB, 27 s ở 12.9 KB/s) có thể sống lâu hơn khóa cũ, 2 envelope + chỉ mất khoảng 14 s. +5. **Đọc từ link.** Bên nhận ráp kênh `0x02` trong bộ đệm riêng (không dùng chung với làn điều khiển dài) và đóng link + không trả lời (CONN-05 E8) khi một mảnh `0x02` có `long` = 1, một mảnh `0x02` có `dlen` vượt `BT_BULK_FRAGMENT_MAX`, + hoặc một message bulk vượt `BT_MESSAGE_MAX` trước mảnh cuối. `chan` khác `0x01` và `0x02` vẫn làm đóng link (0.4.5). +6. **Đưa lên trên.** Message bulk đủ mảnh được đưa lên phiên dưới dạng envelope text, giống message điều khiển đủ + mảnh: trên Android là `Frame.Text` vào cùng đường xử lý `/v1/ctl`, trên macOS là `ChannelMessage.text`. Nó đi qua + cùng bước giải mã, cùng `DEDUP_WINDOW` và cùng bộ điều phối như kênh điều khiển. UTF-8 hỏng hoặc envelope không hợp + lệ được xử lý y như cùng các byte đó trong một WS text frame trên `/v1/ctl`. Bên nhận không kiểm tra envelope đến từ + làn nào; luật 1 chỉ ràng buộc bên gửi. Trên macOS, `ChannelMessage.binary` vẫn là HL frame của 0.5.2 và không bao giờ + đi trên link Bluetooth: `BluetoothChannel.send(.binary)` ném lỗi. +7. **Thứ tự.** Mỗi kênh giữ đúng thứ tự bên gửi; giữa hai kênh không có thứ tự. Mọi envelope của một lần truyền đi + trên `0x02`, nên chúng đến đúng thứ tự (CLIP-03 API 4 luật 2 vẫn đúng). Envelope điều khiển có thể vượt envelope + bulk: `push` gửi thẳng của một clip mới hơn có thể đến trước `cancel` `superseded` của lần truyền cũ, và CLIP-03 + API 3 luật 2 đã lo trường hợp này (`push` mới thay lần truyền cũ). +8. **Link kết thúc.** Message bulk đang ráp dở khi link đóng bị bỏ, các envelope bulk còn trong hàng đợi của bên gửi + cũng vậy. Lần truyền khi đó theo CLIP-03 E8, hoặc E11 khi phiên LAN thay phiên Bluetooth. + ## 0.5 Khung tin ### 0.5.1 Envelope JSON (WS text frame) @@ -1366,6 +1470,7 @@ SET-02 quản lý các khóa này. | `BULK_DIRECT_MIN` | 128 KiB | Qua Bluetooth, lần truyền từ cỡ này trở lên chờ Wi-Fi Direct trước (0.4.7); khoảng 10 s ở tốc độ 12.9 KB/s của G7 | | `BT_BULK_FRAGMENT_MAX` | 1 020 byte | `dlen` của một mảnh trên làn dài kênh điều khiển (kênh `0x01`, `long` = 1) và của một mảnh bulk trên kênh `0x02` (vừa bản ghi 1 040 byte, chiếm sóng khoảng 80 ms) | | `BT_BULK_INFLIGHT` | 2 bản ghi | Tổng số bản ghi điều khiển dài và bulk đã ghi vào stream L2CAP mà chưa đi hết; giới hạn thời gian một envelope điều khiển ngắn phải chờ (AC1) | +| `BT_BULK_QUEUED` | 2 envelope | Số envelope bulk trong hàng đợi của link Bluetooth mà chưa ghi hết; quá số đó thì lệnh gửi bulk chờ (luật bulk 4 của 0.4.7). Giữ một chunk đang chờ nằm trong 30 s khóa cũ còn sống sau rekey | | `BULK_MAX_RESTARTS` | 2 | Số lần làm lại vì đổi làn cho mỗi clip (0.4.7); sau đó áp dụng CLIP-03 E8 | ## 0.11 Trạng thái kết nối của client (Mac/iOS) diff --git a/docs/detailed-design/04-clipboard.md b/docs/detailed-design/04-clipboard.md index 2d477f6..cee8f4e 100644 --- a/docs/detailed-design/04-clipboard.md +++ b/docs/detailed-design/04-clipboard.md @@ -1064,7 +1064,8 @@ Envelope = {"v":1,"type":"clipboard","id":"0192f3f1-2c40-7c21-9d32-e43f54a65b76" 3. Each chunk is a complete envelope (about 87 KiB after base64), subject to `DEDUP_WINDOW` and counted toward `REKEY_AFTER`; through the relay it is subject to `RELAY_RATE_LIMIT`. 4. The sender reads chunks from the file or from the normalized buffer and keeps at most 4 chunks - waiting in the send buffer; other envelopes are sent in between the chunks. + waiting in the send buffer (on the Bluetooth link, `BT_BULK_QUEUED` instead, 0.4.7 bulk rule 4); other + envelopes are sent in between the chunks. 5. Chunk data is never logged. #### API 5 — `WS clipboard/cancel` diff --git a/docs/detailed-design/04-clipboard.vi.md b/docs/detailed-design/04-clipboard.vi.md index d9e789e..4d4c933 100644 --- a/docs/detailed-design/04-clipboard.vi.md +++ b/docs/detailed-design/04-clipboard.vi.md @@ -1061,8 +1061,8 @@ Envelope = {"v":1,"type":"clipboard","id":"0192f3f1-2c40-7c21-9d32-e43f54a65b76" (đã hủy, đã xong) → bỏ im lặng; sai thứ tự → `ack` lỗi `BAD_REQUEST` cho `push`, xóa tệp tạm. 3. Mỗi khối là một envelope đầy đủ (khoảng 87 KiB sau base64), chịu `DEDUP_WINDOW`, được đếm vào `REKEY_AFTER`; qua relay chịu `RELAY_RATE_LIMIT`. - 4. Bên gửi đọc khối từ tệp hoặc bộ đệm đã chuẩn hóa, giữ tối đa 4 khối chờ trong bộ đệm gửi; - envelope khác được gửi xen giữa các khối. + 4. Bên gửi đọc khối từ tệp hoặc bộ đệm đã chuẩn hóa, giữ tối đa 4 khối chờ trong bộ đệm gửi + (trên link Bluetooth thay bằng `BT_BULK_QUEUED`, luật bulk 4 của 0.4.7); envelope khác được gửi xen giữa các khối. 5. Không log dữ liệu khối. #### API 5 — `WS clipboard/cancel` diff --git a/docs/project-roadmap.md b/docs/project-roadmap.md index 604be26..b6856a6 100644 --- a/docs/project-roadmap.md +++ b/docs/project-roadmap.md @@ -6,11 +6,11 @@ English | [Tiếng Việt](project-roadmap.vi.md) HandLive brings ecosystem-native features, such as Apple Handoff, to Android. The roadmap below is built **in order**. Each phase is a usable piece. Each later phase stands on the infrastructure of the phase before it. The WebSocket, pairing and encryption from Phase 1 are reused by every later phase. One exception to the order (project owner's decision, 2026-09-28): Phases 5 and 6 are done before Phase 4. Phase 4 itself is stopped (project owner's decision, 2026-10-12): only macOS 27 is available, so calls on the computer stay notification + answer/decline/end only, no audio; gate G4 is closed, reopened only on a later owner decision. Phase 7 started on 2026-10-28 (project owner's decision) without waiting for Phases 4–6 or their gates. -## Progress snapshot (2026-10-12) +## Progress snapshot (2026-10-30) -**Latest release:** [`v0.1.1`](https://github.com/HandLive/handlive/releases/tag/v0.1.1) (2026-10-22): the first tag without a pre-release suffix; Google Play readiness on Android (application id `com.handlive.android`, target API 36, R8 on release, signed Play bundle on CI) and the paid-team identifiers on iPhone and iPad (App Group `group.app.hxd.handlive`), the clipboard URI policy (android #13 fixed), the Mac welcome window returns to the menu bar on close; same three files on one page: Mac DMG signed ad hoc (Open Anyway once), iOS IPA (unsigned, for sideloading), Android APK (`foss`, signed). A beta install on Android must be removed first (new application id) and an iPhone beta install must pair again. Every change since beta.3 passed a security scan ([deployment guide](deployment-guide.md)). Live table: hub [`README.md`](../README.md#status). +**Latest release:** [`v0.2.0`](https://github.com/HandLive/handlive/releases/tag/v0.2.0) (2026-10-30): Phase 7 offline connectivity — when both devices are near each other without a network, they connect over Bluetooth L2CAP (control channel only: clipboard text, SMS and call notifications); first pairing over Bluetooth by QR (flow B) or a PIN through CPace-X25519-SHA512 (flow C); Bluetooth admission policy and pre-auth bans; "Connected via Bluetooth" label on Android and Mac. No relay deployment in this release; bulk-over-relay is dropped from scope, while SP3 (per-connection bulk lane, `K_bulk`) and SP4 (Wi-Fi Direct) are specced but not yet implemented (open for a later topic). Every change since v0.1.1 passed a security scan: 0 critical, 0 high, 0 medium, 0 low ([deployment guide](deployment-guide.md)). Live table: hub [`README.md`](../README.md#status). -**Previous release:** [`v0.1.0-beta.3`](https://github.com/HandLive/handlive/releases/tag/v0.1.0-beta.3) (2026-10-07): calls from other apps keep the app's name and survive a swiped in-call notification, text clips keep their HTML, the Liquid Glass app icon on iPhone, iPad and Mac, the iPhone keeps its session 25 s in the background; same files on that one page as beta.2: Mac DMG signed ad hoc (Open Anyway once), iOS IPA (unsigned, for sideloading), Android APK (`foss`, signed); every change since beta.2 passed a security scan ([deployment guide](deployment-guide.md)). Live table: hub [`README.md`](../README.md#status). +**Previous release:** [`v0.1.1`](https://github.com/HandLive/handlive/releases/tag/v0.1.1) (2026-10-22): the first tag without a pre-release suffix; Google Play readiness on Android (application id `com.handlive.android`, target API 36, R8 on release, signed Play bundle on CI) and the paid-team identifiers on iPhone and iPad (App Group `group.app.hxd.handlive`), the clipboard URI policy (android #13 fixed), the Mac welcome window returns to the menu bar on close; same three files on one page: Mac DMG signed ad hoc (Open Anyway once), iOS IPA (unsigned, for sideloading), Android APK (`foss`, signed). A beta install on Android must be removed first (new application id) and an iPhone beta install must pair again. Every change since beta.3 passed a security scan ([deployment guide](deployment-guide.md)). Live table: hub [`README.md`](../README.md#status). | Phase | Code on `main` | Gate / blocker | |-------|----------------|----------------| @@ -21,7 +21,7 @@ HandLive brings ecosystem-native features, such as Apple Handoff, to Android. Th | 4 | Stopped (owner decision, 2026-10-12) | G4 closed: only macOS 27 available, calls stay notification + answer/decline/end, no audio | | 5 | Spike on `main` (`CameraSpike`, merged 2026-09-30) | G5: need paid Apple Developer team | | 6 | Spike probes on `main` (Apple+Android merged 2026-09-30); product cards not started | G6: owner browser decisions; Firefox/Edge/Brave rows | -| 7 | Started (owner decision, 2026-10-28) | Spike G7 first, no wait on G4/G5/G6; both-offline case, no relay, no HFP bonding | +| 7 | In progress (SP1+SP2 released in `v0.2.0`, 2026-10-30) | Spike G7 done; Bluetooth offline pairing (QR flow B, PIN flow C via CPace-X25519-SHA512) shipped; SP3 (bulk lane, `K_bulk`) and SP4 (Wi-Fi Direct) specced, not implemented; only bulk-over-relay is out of scope | | G0 | Done | Test vectors | | G1 | Open (before 1.0) | Real-device Phase 1 matrix | | G2 | Open (before 1.0) | Play Console SMS / call-log (Plan B: F-Droid/APK) | @@ -62,7 +62,7 @@ decision. ## Phase 7. Connect anywhere (started) -**Started on 2026-10-28 by the project owner's decision, with the G7 spike (about one week), without waiting for gates G4, G5 and G6.** After the first QR pairing the phone and the Mac near each other connect by themselves when neither has a network (the owner's case), and neither device ever leaves or changes its current Wi-Fi; the same Bluetooth path also covers phone on mobile data and Mac on another Wi-Fi while they are near each other. A Bluetooth link (BLE or classic, chosen by G7) carries the control channel; a new bulk lane carries images over the LAN, Wi-Fi Direct (only while the Mac's Wi-Fi is idle) or Bluetooth; first pairing also works offline over BLE. Dropped on 2026-10-28: bulk over the relay (the relay is not deployed) and automatic HFP bonding (Phase 4 is stopped). If G7 fails there is no relay fallback and the product stays LAN only. Every Bluetooth path is secured beyond the session handshake (link-security layer, admission policy). Plan: `plans/20260925-implementation/phase-07-ket-noi-moi-noi.md`. +**Started on 2026-10-28 by the project owner's decision, with the G7 spike (about one week), without waiting for gates G4, G5 and G6.** After the first QR pairing the phone and the Mac near each other connect by themselves when neither has a network (the owner's case), and neither device ever leaves or changes its current Wi-Fi; the same Bluetooth path also covers phone on mobile data and Mac on another Wi-Fi while they are near each other. A Bluetooth link (BLE or classic, chosen by G7) carries the control channel; a new bulk lane (SP3) carries images over the LAN, Wi-Fi Direct (SP4, only while the Mac's Wi-Fi is idle) or Bluetooth; first pairing also works offline over BLE. Dropped on 2026-10-28: bulk over the relay (the relay is not deployed) and automatic HFP bonding (Phase 4 is stopped) — SP3 and SP4 themselves are not dropped, just not implemented yet. If G7 fails there is no relay fallback and the product stays LAN only. Every Bluetooth path is secured beyond the session handshake (link-security layer, admission policy). Plan: `plans/20260925-implementation/phase-07-ket-noi-moi-noi.md`. **Measure:** text under 50 ms on the LAN and within 200 ms over Bluetooth; a 5 MB image within 2 s on the LAN, the first one offline within 10 s; no device changes its Wi-Fi. **Effort:** estimated after G7. ## Total effort diff --git a/docs/project-roadmap.vi.md b/docs/project-roadmap.vi.md index 275a2a0..6c58a9d 100644 --- a/docs/project-roadmap.vi.md +++ b/docs/project-roadmap.vi.md @@ -6,11 +6,11 @@ HandLive đưa các tính năng native riêng trong từng hệ sinh thái, như Handoff trên Apple, lên Android. Lộ trình dưới đây xây **theo thứ tự**. Mỗi phase là một phần dùng được. Phase sau đứng trên hạ tầng phase trước. WebSocket, ghép cặp và mã hóa từ Phase 1 dùng lại cho mọi phase sau. Một ngoại lệ về thứ tự (quyết định của chủ dự án, 28/09/2026): Phase 5 và 6 làm trước Phase 4. Phase 4 đã dừng (quyết định của chủ dự án, 12/10/2026): chỉ có Mac macOS 27, nên cuộc gọi trên máy tính chỉ còn thông báo và nút nghe/từ chối/kết thúc, không có âm thanh; cổng G4 đóng, chỉ mở lại khi chủ dự án quyết định sau. Phase 7 bắt đầu ngày 28/10/2026 (quyết định của chủ dự án) mà không chờ Phase 4–6 hay các cổng của chúng. -## Tiến độ tóm tắt (12/10/2026) +## Tiến độ tóm tắt (30/10/2026) -**Bản phát hành mới nhất:** [`v0.1.1`](https://github.com/HandLive/handlive/releases/tag/v0.1.1) (22/10/2026): thẻ đầu tiên không có hậu tố tiền phát hành; Android sẵn sàng cho Google Play (mã ứng dụng `com.handlive.android`, target API 36, R8 cho bản phát hành, gói Play ký trên CI) và định danh của nhóm trả phí trên iPhone, iPad (App Group `group.app.hxd.handlive`), chính sách URI bảng nhớ tạm (đã sửa android #13), cửa sổ chào trên Mac trở về thanh menu khi đóng; vẫn 3 tệp trên một trang: DMG Mac ký ad hoc (Open Anyway một lần), IPA iOS (chưa ký, để sideload), APK Android (`foss`, đã ký). Bản beta trên Android phải gỡ trước (mã ứng dụng mới) và bản beta trên iPhone phải ghép đôi lại. Mọi thay đổi từ beta.3 đã qua rà soát bảo mật ([hướng dẫn phân phối](deployment-guide.vi.md)). Bảng trực tiếp: hub [`README.vi.md`](../README.vi.md#trạng-thái). +**Bản phát hành mới nhất:** [`v0.2.0`](https://github.com/HandLive/handlive/releases/tag/v0.2.0) (30/10/2026): Phase 7 kết nối offline — khi cả hai thiết bị ở gần nhau và không có mạng, chúng tự kết nối qua Bluetooth L2CAP (chỉ kênh điều khiển: chữ trong bảng nhớ tạm, SMS và thông báo cuộc gọi); ghép đôi đầu qua Bluetooth bằng QR (luồng B) hoặc PIN qua CPace-X25519-SHA512 (luồng C); có chính sách admission và chặn brute-force trên Bluetooth; nhãn "Đã kết nối qua Bluetooth" trên Android và Mac. Không triển khai relay trong bản này; bulk qua relay bị bỏ khỏi phạm vi, còn SP3 (làn bulk theo từng kết nối, `K_bulk`) và SP4 (Wi-Fi Direct) đã có đặc tả nhưng chưa code (để lại cho topic sau). Mọi thay đổi từ v0.1.1 đã qua quét bảo mật: 0 critical, 0 high, 0 medium, 0 low ([hướng dẫn phân phối](deployment-guide.vi.md)). Bảng trực tiếp: hub [`README.vi.md`](../README.vi.md#trạng-thái). -**Bản phát hành trước:** [`v0.1.0-beta.3`](https://github.com/HandLive/handlive/releases/tag/v0.1.0-beta.3) (07/10/2026): cuộc gọi từ ứng dụng khác giữ tên ứng dụng và không kết thúc khi thông báo đang gọi bị vuốt bỏ, clip chữ giữ HTML, biểu tượng ứng dụng Liquid Glass trên iPhone, iPad và Mac, iPhone giữ phiên 25 giây khi chạy nền; vẫn các tệp như beta.2 ở cùng trang đó: DMG Mac ký ad-hoc (bấm Open Anyway một lần), IPA iOS (chưa ký, để sideload), APK Android (`foss`, đã ký); mọi thay đổi từ beta.2 đã qua quét bảo mật ([hướng dẫn phân phối](deployment-guide.vi.md)). Bảng sống: [`README.vi.md`](../README.vi.md#trạng-thái). +**Bản phát hành trước:** [`v0.1.1`](https://github.com/HandLive/handlive/releases/tag/v0.1.1) (22/10/2026): thẻ đầu tiên không có hậu tố tiền phát hành; Android sẵn sàng cho Google Play (mã ứng dụng `com.handlive.android`, target API 36, R8 cho bản phát hành, gói Play ký trên CI) và định danh của nhóm trả phí trên iPhone, iPad (App Group `group.app.hxd.handlive`), chính sách URI bảng nhớ tạm (đã sửa android #13), cửa sổ chào trên Mac trở về thanh menu khi đóng; vẫn 3 tệp trên một trang: DMG Mac ký ad hoc (Open Anyway một lần), IPA iOS (chưa ký, để sideload), APK Android (`foss`, đã ký). Bản beta trên Android phải gỡ trước (mã ứng dụng mới) và bản beta trên iPhone phải ghép đôi lại. Mọi thay đổi từ beta.3 đã qua rà soát bảo mật ([hướng dẫn phân phối](deployment-guide.vi.md)). Bảng sống: [`README.vi.md`](../README.vi.md#trạng-thái). | Phase | Mã trên `main` | Cổng / điểm nghẽn | |-------|----------------|-------------------| @@ -21,7 +21,7 @@ HandLive đưa các tính năng native riêng trong từng hệ sinh thái, như | 4 | Đã dừng (quyết định của chủ dự án, 12/10/2026) | G4 đóng: chỉ có macOS 27, cuộc gọi chỉ còn thông báo và nút nghe/từ chối/kết thúc, không âm thanh | | 5 | Spike trên `main` (`CameraSpike`, merge 30/09/2026) | G5: cần Apple Developer trả phí | | 6 | Probe spike trên `main` (Apple+Android merge 30/09/2026); chưa mở thẻ sản phẩm | G6: quyết định trình duyệt; hàng Firefox/Edge/Brave | -| 7 | Đã bắt đầu (quyết định của chủ dự án, 28/10/2026) | Spike G7 trước, không chờ G4/G5/G6; trường hợp cả hai offline, không relay, không tự ghép đôi HFP | +| 7 | Đang làm (SP1+SP2 đã phát hành ở `v0.2.0`, 30/10/2026) | Spike G7 xong; ghép đôi Bluetooth offline (luồng B QR, luồng C PIN qua CPace-X25519-SHA512) đã phát hành; SP3 (làn bulk, `K_bulk`) và SP4 (Wi-Fi Direct) đã có đặc tả, chưa code; chỉ bulk qua relay nằm ngoài phạm vi | | G0 | Xong | Vector kiểm thử | | G1 | Mở (trước 1.0) | Ma trận Phase 1 máy thật | | G2 | Mở (trước 1.0) | Tờ khai SMS / nhật ký Play Console (phương án B: F-Droid/APK) | @@ -62,7 +62,7 @@ dự án quyết định sau. ## Phase 7. Kết nối mọi nơi (đã bắt đầu) -**Bắt đầu 28/10/2026, spike G7 (khoảng một tuần), không chờ G4, G5, G6.** Sau ghép đôi QR, điện thoại và Mac ở gần tự kết nối khi cả hai offline (trường hợp chủ dự án), không rời hay đổi Wi-Fi; cùng đường Bluetooth phủ trường hợp điện thoại dùng 4G và Mac ở Wi-Fi khác khi gần nhau. Liên kết Bluetooth (BLE hoặc cổ điển, G7 chọn) chở kênh điều khiển; làn dữ liệu lớn mới chở ảnh qua LAN, Wi-Fi Direct (nhu cầu, Wi-Fi Mac rảnh), Bluetooth; ghép đôi đầu offline qua BLE. Bỏ: bulk relay (không deploy), ghép đôi HFP (P4 dừng). Nếu G7 hỏng thì không relay dự phòng, chỉ còn LAN. Bluetooth được bảo mật ngoài bắt tay phiên (lớp bảo mật link, chính sách admission). Plan: `plans/20260925-implementation/phase-07-ket-noi-moi-noi.vi.md`. +**Bắt đầu 28/10/2026, spike G7 (khoảng một tuần), không chờ G4, G5, G6.** Sau ghép đôi QR, điện thoại và Mac ở gần tự kết nối khi cả hai offline (trường hợp chủ dự án), không rời hay đổi Wi-Fi; cùng đường Bluetooth phủ trường hợp điện thoại dùng 4G và Mac ở Wi-Fi khác khi gần nhau. Liên kết Bluetooth (BLE hoặc cổ điển, G7 chọn) chở kênh điều khiển; làn dữ liệu lớn mới (SP3) chở ảnh qua LAN, Wi-Fi Direct (SP4, nhu cầu, Wi-Fi Mac rảnh), Bluetooth; ghép đôi đầu offline qua BLE. Bỏ: bulk relay (không deploy), ghép đôi HFP (P4 dừng) — SP3 và SP4 không bị bỏ, chỉ là chưa code. Nếu G7 hỏng thì không relay dự phòng, chỉ còn LAN. Bluetooth được bảo mật ngoài bắt tay phiên (lớp bảo mật link, chính sách admission). Plan: `plans/20260925-implementation/phase-07-ket-noi-moi-noi.vi.md`. **Đo:** văn bản dưới 50 ms trên LAN và trong 200 ms qua Bluetooth; ảnh 5 MB trong 2 s trên LAN, ảnh đầu khi offline trong 10 s; không máy nào bị đổi Wi-Fi. **Công:** ước lượng sau G7. ## Công tổng diff --git a/plans/20260925-implementation/phase-07-ket-noi-moi-noi.md b/plans/20260925-implementation/phase-07-ket-noi-moi-noi.md index 1311d10..5dbc090 100644 --- a/plans/20260925-implementation/phase-07-ket-noi-moi-noi.md +++ b/plans/20260925-implementation/phase-07-ket-noi-moi-noi.md @@ -108,10 +108,11 @@ before the code of the card. |------|------|-------------|---------|---------------------| | T7.0 [test] | Gate G7 spike (above): throwaway probes on Android and macOS | — (measurement) | `reports/phase-07-spike-g7.md`; probes on the spike branch only, never merged | Bearer, join path and link-security prototype chosen; AC numbers measured; AC6 confirmed by the owner (bonding flow and AC5 dropped 2026-10-28) | | ~~T7.0a~~ [test] | *(Added 2026-10-28, lead decision.)* Scan-response probe: the throwaway probes of T7.0 with the 0.4.5 layout (128-bit `BT_SERVICE_UUID` in the advertisement, service data in the scan response) on the S25 + Mac, Mac scanning in the foreground and in the background | — (measurement) | A section in `reports/phase-07-spike-g7.md`; probes on the spike branch only, never merged | **Dropped 2026-10-28 (owner decision): scan response checked in A7.1b/M7.1c on real devices.** Was: service data received on the Mac in both cases; otherwise the advertisement layout of 0.4.5 changes (D) before S7.1a freezes its vectors | +| T7.0b [test] | *(Added 2026-10-30, lead decision.)* Wi-Fi Direct probe, before any SP4 code: throwaway probes on the S25 (group owner, `LEGACY_OR_R2`, SSID and passphrase drawn at random) and on the Mac (CoreWLAN join). Four points: (1) time from `direct/start` to the pinned TLS connection on the group, against `DIRECT_WIFI_JOIN_TIMEOUT` (8 s); (2) whether the saved network can be removed after leaving without an administrator prompt; (3) Location authorization on the Mac in a signed, notarized build, and whether a CoreWLAN scan returns the SSID without it; (4) a socket bound to the P2P interface reaches the phone while the default route stays untouched | — (measurement) | A section in a new `reports/phase-07-spike-wifi-direct.md`; probes on a spike branch only, never merged | The four points measured or answered on the S25 + Mac, run confirmed by the owner; if (1) or (2) fails, 0.4.6 and AC4 are revised (D) before S7.1d, or SP4 is dropped by owner decision | | D7.1 [docs] | Spec first, both languages. **0.11 and CONN:** start condition "a network or Bluetooth on and authorized"; network loss ends LAN/relay sessions only; pre-auth reactions only on routes with pinned TLS to the phone (`phoneAuthenticatedByTLS`), CONN-01 E4 and CONN-03 E9 rewritten as that rule. **0.4.5 Bluetooth link:** advertisement (service UUID, rotating hint; address rotated with the hint); link-security layer before any identifier (e.g. a Noise-style handshake keyed by `K_auth`, or a blinded identifier over a phone-issued nonce) and one generic pre-auth error; admission (global cap, one pre-auth link, phone nonce challenge, key-confirmation failures counted as `AUTH_FAILED`); frame = channel id ‖ length ‖ bytes, control before bulk, bulk fragments of a few KB, no close codes (end with encrypted `session/bye`). **Pairing over BLE:** `pr` = HMAC(`ps`, …), first frame MAC'd with a `ps` key before a slot is granted, one global slot with a short timeout; ~~PIN refused per transport~~ *changed 2026-10-28 (owner decision): the PIN pairs over Bluetooth through CPace (`CPACE-X25519-SHA512`), one online guess per channel, 3 per PIN, lockout (D7.1e, PAIR-01 flow C)*. **Bulk:** stream handshake for `bulk` with per-connection keys (`K_stream` + both nonces), whole transfer (push, chunks, ack) on the bulk lane over LAN, Wi-Fi Direct or Bluetooth (*dropped 2026-10-28:* bulk inside relay HL binary frames, bulk-demand op, relay hold rule). **Wi-Fi Direct (0.4.6):** SSID/passphrase (random per group, ≥ 128-bit passphrase), entry/exit rules of AC4, remove the saved network after leaving, HandLive socket bound to the P2P interface only. ~~**Bonding (AUDIO-01)**~~ *dropped 2026-10-28 with SP5; AUDIO-01 unchanged.* Plus constants, settings keys, error codes, SET-01/02/03, CONN-05/CONN-06 leaves, PAIR-01, CLIP-03 | — (contract; the vectors of S7.1 are its tests) | `docs/detailed-design/*.md` + `.vi.md` | `validate_design_docs.py` prints `problems=0`; `check_bilingual_docs.py` green; every new message type, error code, constant and string defined before code; AC8–AC9 traceable to spec rules | | S7.1a [shared] | *(Split 2026-10-28, lead decision; was S7.1.)* For SP1, link contract: link-security handshake (`challenge`, `noise1`, `noise2`, prologue bound to `n_s`, `K_bt_disc`/`K_bt_link`), record codec (`rlen` bounds; 272, 1,040, 4,112) and frame codec (channel ids, `more`/`long`, short and long control lanes, padding) with negative samples (`rlen` over its bound, unknown `chan`, `more` without `long`, a long-lane fragment over `BT_BULK_FRAGMENT_MAX`, a message over `BT_MESSAGE_MAX`), advertisement service data (frozen now from the 0.4.5 table; checked on real devices in A7.1b/M7.1c); UI strings for the Bluetooth prompts and the Bluetooth-off line; bench events `bt_seen`/`bt_lost` and a `reconnect_time.py` trigger for them | Vectors and negative samples are the tests: generators produce them, `check_*` validate them | `shared/test-vectors/`, `shared/strings/`, `shared/tools/` (incl. `bench/`) | Generators reproducible; `check_schemas.py`, `check_strings.py` green; other platforms told to re-run | | S7.1b [shared] | For SP2, made while SP1 runs: `pr_bt` HMAC, `K_bt_pdisc`/`K_bt_plink`, `BT_PIN_HINT`; CPace for the Bluetooth PIN (`cpace.json`, Kotlin and Swift: the draft's own `CPACE-X25519-SHA512` vectors, then the HandLive cases and negatives of 0.6.1), `K_bt_pinlink`/`K_bt_pin`; schemas for the pairing envelopes on a Bluetooth link; strings for the PIN over Bluetooth and `BT_PIN_LOCKOUT` | Vectors and negative samples are the tests: generators produce them, `check_*` validate them | `shared/test-vectors/`, `shared/schemas/`, `shared/strings/` | Generators reproducible; `check_schemas.py`, `check_strings.py` green; other platforms told to re-run; the pentester has reviewed `cpace.json` before A7.2a/M7.2a start | -| S7.1c [shared] | For SP3: per-connection `bulk` keys (`K_bulk` from `K_stream` and both nonces, 0.4.7); schemas for `bulk/stream_hello` and `bulk/stream_welcome` | Vectors and negative samples are the tests: generators produce them, `check_*` validate them | `shared/test-vectors/`, `shared/schemas/` | Generators reproducible; `check_schemas.py`, `check_strings.py` green; other platforms told to re-run | +| S7.1c [shared] | For SP4 (*moved from SP3 2026-10-30, lead decision: only `/v1/stream/bulk` on Wi-Fi Direct uses these keys*): per-connection `bulk` keys (`K_bulk` from `K_stream` and both nonces, 0.4.7); schemas for `bulk/stream_hello` and `bulk/stream_welcome` | Vectors and negative samples are the tests: generators produce them, `check_*` validate them | `shared/test-vectors/`, `shared/schemas/` | Generators reproducible; `check_schemas.py`, `check_strings.py` green; other platforms told to re-run | | S7.1d [shared] | For SP4: Wi-Fi Direct `ssid` and `passphrase` are random for every group (0.4.6), so no derivation vectors: format and charset samples, positive and negative (prefix `DIRECT-`, length, alphabet, passphrase entropy); schemas for `direct/start`, `direct/stop`, `direct/want` and the `direct/start` ack `{ssid, passphrase, host, port}` (00-common-specs 0.7.1); bench event `wifi_assoc`; strings for the Location prompts | Vectors and negative samples are the tests: generators produce them, `check_*` validate them | `shared/test-vectors/`, `shared/schemas/`, `shared/strings/`, `shared/tools/bench/` | Generators reproducible; `check_schemas.py`, `check_strings.py` green; other platforms told to re-run | | A7.1a [android] | *(Split 2026-10-28, lead decision; was A7.1.)* Link core, no radio: record and frame codecs with every length checked before allocating, short and long control lanes with reassembly, the scheduler of 0.4.5 "Priority" (short control > long control > bulk, `BT_BULK_INFLIGHT`; moved here from A7.4 because G7 condition 1 makes it part of the control lane), link-security layer (Noise responder, trial of each pair psk, link rekey), Bluetooth admission for the whole listener (`BT_LINK_LIMIT`, `BT_PREAUTH_LIMIT`, `BT_PREAUTH_TIMEOUT`, `BT_AUTH_FAIL_BLOCK`; never keyed by address) | Codec and link-security vectors of S7.1a; fake-pipe tests: handshake, a replayed `noise1` on a new channel, link rekey, pre-auth cap, block after `BT_AUTH_FAIL_BLOCK`, oversized `rlen`, a short envelope sent after a 256 KiB one arrives first, two envelopes of the same `type` keep their order, inflight bound | `android/core/transport` | All tests green | | A7.1b [android] | Radio and wiring: advertiser and L2CAP server in A-SVC (rotation every `BT_HINT_PERIOD`, rebuilt on `STATE_ON`, independent of the LAN server and of the network), `SessionTransport.BLUETOOTH` and every `when` on it (`PeerSession.Channel` and its users in the feature modules), `BluetoothPeerSocket` (virtual `WebSocketSession` like `RelayPeerSocket`) served by `ControlServer.serveBluetoothPeer`, `session/bye` before every close on Bluetooth; a **debug-build-only loopback shim** for the e2e harness (not needed for the SP1 measurement, may land last) | Regression tests for `serveRelayPeer`; session tests over the fake pipe; rotation and `STATE_ON` rebuild with a fake adapter; a release-build test asserting the shim is absent | `android/core/transport`, `android/feature/connection`, `android/feature/*` (route mapping only), `android/app` | All tests green; CONN-05 exceptions covered; AC1 (Bluetooth) measured on the S25, idle and under a debug-only long-control load through the scheduler; scan response contains service data read on Mac, even when app is in background | @@ -122,11 +123,11 @@ before the code of the card. | A7.2 [android] | Offline first pairing: advertise `pr` (HMAC form) during QR mode; grant the single pairing slot only after a valid `ps`-MAC'd first frame; PAIR-01 over the secured Bluetooth link; PIN over Bluetooth through CPace on the primitive of A7.2a (PAIR-01 flow C: `BT_PIN_HINT` advertising, `cpace1`/`cpace2`, attempt counting, `BT_PIN_LOCKOUT`) — *changed 2026-10-28 by owner decision, was "PIN refused on Bluetooth explicitly"* | PAIR-01 vectors over the fake pipe; CPace vectors of S7.1 (`cpace.json`); negative tests: wrong PIN (3 attempts then lockout), low-order `Ya`, `cpace1` outside a PIN window, `mode` not matching the link, missing/invalid first-frame MAC, slot hogging, second claimant | `android/feature/pairing`, `android/core/transport` | PAIR-01 exceptions over Bluetooth covered; AC6 measured | | M7.2a [macOS] | *(Added 2026-10-28, lead decision.)* CPace primitive, same scope as A7.2a (CryptoKit has no field arithmetic) | Same as A7.2a | `apple/Packages/HLCrypto` | Same as A7.2a | | M7.2 [macOS] | Offline first pairing: scan for `pr`, try every candidate with the same `pr`, run PAIR-01 over the secured `BluetoothChannel`, then pin and store as today; PIN: scan for `BT_PIN_HINT`, CPace initiator on the primitive of M7.2a, count wrong PINs, new PIN at 3 (*added 2026-10-28, owner decision*) | Same vectors plus `cpace.json`; timeout, wrong `pr`, spoofed advertiser with the right `pr`, a phone in PIN mode with another PIN, an attacker answering `cpace1` | `apple/Packages/HLTransport` (`Pairing*`) | AC6 measured; no QR format change | -| A7.3 [android] | Stream channel infrastructure: `/v1/stream/*` server on A-SVC (`stream_hello`/`stream_welcome`, 0.6.3 step 7) — **reuse it if a Phase 4 or 5 product card built it first** (plan decision I9) | Stream handshake vector tests (written first) | `android/core/transport`, `android/core/crypto` | Stream handshake works on the LAN; `camera`/`call-audio` unaffected | -| M7.3 [macOS] | Stream channel client, same scope and reuse rule as A7.3 | Same | `apple/Packages/HLTransport` | Same | +| A7.3 [android] | *(Moved to SP4 2026-10-30, lead decision: over Bluetooth bulk uses channel `0x02` with the session keys; only `/v1/stream/bulk` on Wi-Fi Direct needs a stream server.)* Stream channel infrastructure: `/v1/stream/*` server on A-SVC (`stream_hello`/`stream_welcome`, 0.6.3 step 7) — **reuse it if a Phase 4 or 5 product card built it first** (plan decision I9) | Stream handshake vector tests (written first) | `android/core/transport`, `android/core/crypto` | Stream handshake works on the LAN; `camera`/`call-audio` unaffected | +| M7.3 [macOS] | *(Moved to SP4 2026-10-30, as A7.3.)* Stream channel client, same scope and reuse rule as A7.3 | Same | `apple/Packages/HLTransport` | Same | | R7.1 [relay] | **Dropped 2026-10-28 (no relay deployment).** Was: confirm the relay carries bulk unchanged: `HR` binary frames of bulk size at the existing cap; no wire or cap change | Forwarding tests for bulk-sized `HR` frames (red if the relay misbehaves) | `relay/` tests only | `cargo test`, `cargo clippy` green; no change to `wire.rs` or the cap | -| A7.4 [android] | Bulk lane: `bulk` stream with per-connection keys; the whole CLIP-03 transfer on the bulk lane; path selection per the routing table (LAN > Wi-Fi Direct > Bluetooth); bulk channel `0x02` plugged into the scheduler of A7.1a (the scheduler moved to A7.1a on 2026-10-28). *Dropped 2026-10-28:* bulk over the relay split in `RelayPeerMux`, bulk-demand op, relay hold rule | CLIP-03 regression over `/v1/ctl` first; then red: chunks before push, AC1 under a 5 MB bulk transfer, a 10 MiB image over Bluetooth only completes in the background (*dropped:* the two relay tests) | `android/core/transport`, `android/feature/clipboard` | CLIP-03 exceptions pass on every path; AC1 (LAN) unchanged; AC3 (LAN) unchanged | -| M7.4 [macOS] | Bulk lane on the Mac side, same scope as A7.4, on the scheduler of M7.1b (`ClipboardPeer` seam; the `RelayLink`/`RelayPeerChannel` split is dropped 2026-10-28) | Same order as A7.4 | `apple/Packages/HLTransport`, `apple/Packages/HLAppCore` (clipboard) | Same as A7.4 | +| A7.4 [android] | *(Rescoped 2026-10-30, lead decision: SP3 is the Bluetooth bulk lane only; the `/v1/stream/bulk` lane joins SP4 with A7.5.)* Bulk lane over Bluetooth per 00-common-specs 0.4.7 "Bulk lane on the Bluetooth link": `EnvelopeLane` on every send path (bulk rule 2); the whole CLIP-03 transfer on channel `0x02` (rule 1); `BluetoothPeerSocket` as `BulkLaneSocket` with the `BT_BULK_QUEUED` backpressure (rules 3–4); reassembly and hand-up of channel `0x02` (rules 5–6); lane choice LAN > Bluetooth when a transfer starts, with an empty slot for the Wi-Fi Direct lane (filled in SP4; until then every size goes over Bluetooth in the background); restart when a LAN session replaces the Bluetooth one (0.4.7 "Changing lane", CLIP-03 E11). *Dropped 2026-10-28:* bulk over the relay split in `RelayPeerMux`, bulk-demand op, relay hold rule | CLIP-03 regression over `/v1/ctl` first; then red, one per rule: rule 1 lane of each envelope (push with `transfer`, chunk, cancel, the transfer `ack` and the `conflict` after it on `0x02`; inline push and its `ack` on `0x01`); rule 2 a duplicate's kept `ack` resent on its first lane; rule 4 a third chunk waits while 2 are queued; rule 5 link closed on `long` = 1, on `dlen` over `BT_BULK_FRAGMENT_MAX`, on a message over `BT_MESSAGE_MAX`; rule 6 a bulk envelope reaches the dispatcher and the `DEDUP_WINDOW`; rule 7 an inline push overtaking a bulk `cancel` `superseded` replaces the old transfer; rule 8 a half-reassembled bulk message dropped at link close; AC1 under a 5 MB bulk transfer; a 10 MiB image over Bluetooth only completes in the background (*dropped:* the two relay tests) | `android/core/transport`, `android/feature/connection` (`session/`), `android/feature/clipboard` | CLIP-03 exceptions pass on the LAN and over Bluetooth; AC1 (Bluetooth, during bulk) measured on the S25 + Mac; AC1 (LAN) and AC3 (LAN) unchanged | +| M7.4 [macOS] | *(Rescoped 2026-10-30, as A7.4.)* Bulk lane on the Mac side, same scope as A7.4, on the scheduler of M7.1b: `EnvelopeLane` and `ChannelMessage.bulkText` (bulk rule 2); `BluetoothChannel` sends `.bulkText` on `0x02` with the backpressure, hands `0x02` up as `.text`, caps reassembly at `BT_MESSAGE_MAX`, throws on `.binary` (rules 3–6); the `ClipboardPeer` seam passes the lane (the `RelayLink`/`RelayPeerChannel` split is dropped 2026-10-28) | Same order and rule list as A7.4 | `apple/Packages/HLTransport`, `apple/Packages/HLAppCore` (clipboard) | Same as A7.4; iOS builds and behaves as before | | A7.5 [android] | Wi-Fi Direct host (X only, on demand): group with an SSID and passphrase drawn at random for every group (0.4.6), `LEGACY_OR_R2` on API 36, started/stopped on request over the control lane, STA kept; `NEARBY_WIFI_DEVICES` (13+), `ACCESS_FINE_LOCATION` + location services (10–12); Wi-Fi off → report, never toggle | Format and charset vectors of S7.1d (*changed 2026-10-28: there is no derivation*); lifecycle tests with a fake `WifiP2pManager` (start, stop, crash, STA unchanged, stop after `DIRECT_WIFI_MAX`) | `android/feature/connection` (or a new `feature/directlink`) | AC4 verified on the S25; AC3 in X measured | | M7.5 [macOS] | Wi-Fi Direct join (X only): entry/exit rules of AC4 (on demand, idle ≥ `DIRECT_WIFI_IDLE_MIN`, no known network visible, periodic scan, `DIRECT_WIFI_MAX`), CoreWLAN join, gateway connect with the pinned certificate, socket bound to the P2P interface, saved network removed after leaving | Red first: never join while associated, never join when a known network is visible (deauth case), leave on known network, leave at max duration, saved network removed | `apple/Packages/HLTransport`, `apple/macOS/HandLive` | AC4 verified incl. the deauth scenario; AC3 in X ≤ 2 s | | A7.6 [android] | **Dropped 2026-10-28 (SP5; Phase 4 stopped, no HFP).** Was: automatic classic bonding, **started right after QR pairing** (and once for existing pairs when the feature ships, or whenever the bond is missing) , flow from G7 ((b) or (c)): bonding request over E2E, `createBond` to the Mac address received over E2E, pairing value sent over E2E when readable | Fake-adapter tests per state; never bond without an authenticated session; no discoverable request | `android/feature/call` or `android/feature/connection` | AC5, AC10 measured; refusal and timeout covered | @@ -149,11 +150,14 @@ before the code of the card. A7.1b ∥ M7.1c, with A7.7a ∥ M7.7a. On Android, A7.1b and A7.7a both touch `android/app` and `feature/connection`: one after the other. S7.1b runs during SP1, so `cpace.json` is reviewed before SP2. 2. SP2: A7.2a ∥ M7.2a (may start as soon as S7.1b is reviewed), then A7.2 ∥ M7.2. - 3. SP3: S7.1c; A7.3 ∥ M7.3 unless already built, then A7.4 ∥ M7.4. - 4. SP4: S7.1d; A7.5 ∥ M7.5, with A7.7b ∥ M7.7b. + 3. SP3 (*rescoped 2026-10-30, lead decision*): A7.4 ∥ M7.4 on the contract of 0.4.7 "Bulk lane on the Bluetooth link"; + no shared card (no new vectors, schemas or strings). SP3 ships on its own, before SP4. + 4. SP4: T7.0b → S7.1c ∥ S7.1d → A7.3 ∥ M7.3 → A7.5 ∥ M7.5 → A7.7b ∥ M7.7b. Within one platform the cards touch the + same files, so they run one after the other: Android A7.5 → A7.7b (`feature/connection`, `AndroidManifest.xml`); + Mac M7.4 → M7.5 → M7.7b (`ControlSession` and `ConnectionManager` in `HLTransport`, then `apple/macOS/HandLive`). 5. T7.1. - Each sub-project ends with a measurement on the S25 + Mac before the next one opens: SP1 AC1 (idle and under load), - AC2, scan response; SP2 AC6. + AC2, scan response; SP2 AC6; SP3 AC1 during a bulk transfer over Bluetooth and a 10 MiB image in the background. - *Dropped 2026-10-28:* SP5 (A7.6 ∥ M7.6) and the rule "if RFCOMM needs a bond, SP5 moves before SP1" — an RFCOMM bearer that needs a bond now fails G7 (see Bearer above). - One commit per logical step, `shared/` before the platform, never across repositories. @@ -311,3 +315,12 @@ failed 0, unverified 0; tier Light. (scan-response probe) added before the advertisement vectors are frozen. - Wi-Fi Direct `ssid`/`passphrase` are random per group (0.4.6): S7.1d and A7.5 check format and charset, no derivation vectors. + +### Card split — 2026-10-30 (lead decision, team chat) +- Over Bluetooth, bulk travels on link channel `0x02` with the session keys (0.4.7); `/v1/stream/bulk` and `K_bulk` + serve Wi-Fi Direct only. S7.1c, A7.3 and M7.3 therefore move from SP3 to SP4; SP3 is A7.4 ∥ M7.4 alone and ships + before SP4. Until SP4, an image of any size goes over Bluetooth in the background. +- The SP3 contract (lane chosen by the sender, hand-up of channel `0x02`, `BT_BULK_QUEUED`) is written in 0.4.7 + "Bulk lane on the Bluetooth link" before A7.4/M7.4 start. +- T7.0b added: Wi-Fi Direct on the Mac was never probed (G7 left it to SP4), so its join time, saved-network removal, + Location authorization and P2P binding are measured before any SP4 code. diff --git a/plans/20260925-implementation/phase-07-ket-noi-moi-noi.vi.md b/plans/20260925-implementation/phase-07-ket-noi-moi-noi.vi.md index 1ea30bc..80a9d77 100644 --- a/plans/20260925-implementation/phase-07-ket-noi-moi-noi.vi.md +++ b/plans/20260925-implementation/phase-07-ket-noi-moi-noi.vi.md @@ -105,10 +105,11 @@ và commit (ở trạng thái đỏ) trước code của thẻ đó. |------|------|-------------|---------|---------------------| | T7.0 [test] | Spike gate G7 (ở trên): probe dùng một lần trên Android và macOS | — (đo đạc) | `reports/phase-07-spike-g7.md`; probe chỉ ở nhánh spike, không bao giờ merge | Đã chọn lớp mang, cách join và bản thử lớp bảo mật link; đã đo các con số AC; chủ dự án xác nhận AC6 (bỏ luồng ghép đôi và AC5 ngày 2026-10-28) | | ~~T7.0a~~ [test] | *(Thêm 2026-10-28 theo quyết định của Lead.)* Probe scan response: các probe dùng một lần của T7.0, với bố cục của 0.4.5 (`BT_SERVICE_UUID` 128-bit trong gói quảng bá, service data trong scan response) trên S25 + Mac, Mac quét cả khi ở tiền cảnh lẫn chạy nền | — (đo đạc) | Một mục trong `reports/phase-07-spike-g7.md`; probe chỉ nằm trên nhánh spike, không bao giờ merge | **Bỏ 2026-10-28 (sếp quyết định): scan response được kiểm trong A7.1b/M7.1c trên máy thật.** Trước đây: Mac nhận được service data trong cả hai trường hợp; nếu không, bố cục quảng bá của 0.4.5 đổi (D) trước khi S7.1a chốt vector | +| T7.0b [test] | *(Thêm 2026-10-30 theo quyết định của Lead.)* Probe Wi-Fi Direct, làm trước mọi dòng code của SP4: các probe dùng một lần trên S25 (chủ group, `LEGACY_OR_R2`, SSID và passphrase sinh ngẫu nhiên) và trên Mac (join bằng CoreWLAN). Bốn điểm: (1) thời gian từ `direct/start` tới kết nối TLS đã pin trên group, so với `DIRECT_WIFI_JOIN_TIMEOUT` (8 s); (2) có xóa được mạng đã lưu sau khi rời group mà không hiện hộp thoại quyền quản trị không; (3) quyền Location trên Mac trong bản đã ký và notarize, và quét CoreWLAN có trả SSID khi thiếu quyền này không; (4) socket gắn vào interface P2P tới được điện thoại mà route mặc định không bị đụng | — (đo đạc) | Một mục trong tệp mới `reports/phase-07-spike-wifi-direct.md`; probe chỉ nằm trên nhánh spike, không bao giờ merge | Bốn điểm được đo hoặc có câu trả lời trên S25 + Mac, sếp xác nhận lượt chạy; nếu (1) hoặc (2) không đạt thì sửa 0.4.6 và AC4 (D) trước S7.1d, hoặc sếp quyết định bỏ SP4 | | D7.1 [docs] | Spec trước, cả hai ngôn ngữ. **0.11 và CONN:** điều kiện khởi động "có mạng hoặc Bluetooth đang bật và đã được cấp quyền"; mất mạng chỉ kết thúc phiên LAN/relay; chỉ phản ứng theo lỗi pre-auth trên route có TLS ghim tới điện thoại (`phoneAuthenticatedByTLS`), viết lại CONN-01 E4 và CONN-03 E9 theo quy tắc đó. **0.4.5 liên kết Bluetooth:** quảng bá (service UUID, gợi ý xoay vòng; đổi địa chỉ cùng lúc với gợi ý); lớp bảo mật link trước bất kỳ định danh nào (ví dụ bắt tay kiểu Noise khóa bằng `K_auth`, hoặc định danh mù hóa trên một nonce do điện thoại cấp) và một mã lỗi pre-auth chung; admission (trần toàn cục, một link pre-auth, nonce challenge từ điện thoại, lỗi xác nhận khóa tính như `AUTH_FAILED`); frame = channel id ‖ độ dài ‖ byte, điều khiển đi trước bulk, mảnh bulk vài KB, không có close code (kết thúc bằng `session/bye` đã mã hóa). **Ghép đôi qua BLE:** `pr` = HMAC(`ps`, …), frame đầu tiên có MAC bằng khóa từ `ps` trước khi cấp slot, một slot toàn cục với timeout ngắn; ~~từ chối PIN theo loại transport~~ *đổi ngày 2026-10-28 (sếp quyết định): PIN ghép qua Bluetooth bằng CPace (`CPACE-X25519-SHA512`), một lần đoán trực tuyến mỗi kênh, 3 lần mỗi PIN, có khóa tạm (D7.1e, luồng C của PAIR-01)*. **Bulk:** bắt tay stream cho `bulk` với khóa riêng mỗi kết nối (`K_stream` + cả hai nonce); cả lượt truyền (push, chunk, ack) đi trên làn bulk qua LAN, Wi-Fi Direct hoặc Bluetooth (*bỏ 2026-10-28:* bulk trong frame nhị phân HL của relay, op nhu cầu bulk, quy tắc giữ relay). **Wi-Fi Direct (0.4.6):** SSID/passphrase (sinh ngẫu nhiên cho mỗi group, passphrase ≥ 128 bit), quy tắc vào/ra của AC4, xóa mạng đã lưu sau khi rời, socket HandLive chỉ gắn vào giao diện P2P. ~~**Ghép đôi (AUDIO-01)**~~ *bỏ 2026-10-28 cùng SP5; AUDIO-01 giữ nguyên.* Cộng: hằng số, khóa cài đặt, mã lỗi, SET-01/02/03, leaf CONN-05/CONN-06, PAIR-01, CLIP-03 | — (hợp đồng; vector của S7.1 là test của nó) | `docs/detailed-design/*.md` + `.vi.md` | `validate_design_docs.py` in `problems=0`; `check_bilingual_docs.py` xanh; mọi loại message, mã lỗi, hằng số và chuỗi mới được định nghĩa trước khi viết code; AC8–AC9 truy được về các quy tắc trong spec | | S7.1a [shared] | *(Tách 2026-10-28 theo quyết định của Lead; trước là S7.1.)* Cho SP1, hợp đồng link: bắt tay lớp bảo mật link (`challenge`, `noise1`, `noise2`, prologue gắn `n_s`, `K_bt_disc`/`K_bt_link`), codec bản ghi (giới hạn `rlen`; 272, 1 040, 4 112) và codec frame (channel id, `more`/`long`, làn ngắn và làn dài của kênh điều khiển, đệm) kèm mẫu âm (`rlen` vượt giới hạn, `chan` lạ, `more` mà không có `long`, mảnh của làn dài vượt `BT_BULK_FRAGMENT_MAX`, message vượt `BT_MESSAGE_MAX`), service data quảng bá (chốt ngay theo bảng 0.4.5; kiểm trên máy thật ở A7.1b/M7.1c); chuỗi giao diện cho hộp thoại Bluetooth và dòng báo Bluetooth tắt; sự kiện bench `bt_seen`/`bt_lost` và trigger cho chúng trong `reconnect_time.py` | Chính các vector và mẫu âm là test: generator tạo ra, `check_*` kiểm tra | `shared/test-vectors/`, `shared/strings/`, `shared/tools/` (gồm `bench/`) | Generator tái tạo được; `check_schemas.py`, `check_strings.py` xanh; báo các nền tảng khác chạy lại test | | S7.1b [shared] | Cho SP2, làm trong lúc SP1 chạy: HMAC `pr_bt`, `K_bt_pdisc`/`K_bt_plink`, `BT_PIN_HINT`; CPace cho PIN qua Bluetooth (`cpace.json`, Kotlin và Swift: vector `CPACE-X25519-SHA512` của chính bản draft, rồi các ca HandLive và mẫu âm của 0.6.1), `K_bt_pinlink`/`K_bt_pin`; schema cho envelope ghép đôi trên link Bluetooth; chuỗi cho PIN qua Bluetooth và `BT_PIN_LOCKOUT` | Chính các vector và mẫu âm là test: generator tạo ra, `check_*` kiểm tra | `shared/test-vectors/`, `shared/schemas/`, `shared/strings/` | Generator tái tạo được; `check_schemas.py`, `check_strings.py` xanh; báo các nền tảng khác chạy lại test; pentester đã duyệt `cpace.json` trước khi A7.2a/M7.2a bắt đầu | -| S7.1c [shared] | Cho SP3: khóa `bulk` riêng mỗi kết nối (`K_bulk` từ `K_stream` và hai nonce, 0.4.7); schema cho `bulk/stream_hello` và `bulk/stream_welcome` | Chính các vector và mẫu âm là test: generator tạo ra, `check_*` kiểm tra | `shared/test-vectors/`, `shared/schemas/` | Generator tái tạo được; `check_schemas.py`, `check_strings.py` xanh; báo các nền tảng khác chạy lại test | +| S7.1c [shared] | Cho SP4 (*chuyển từ SP3 ngày 2026-10-30 theo quyết định của Lead: chỉ `/v1/stream/bulk` trên Wi-Fi Direct dùng các khóa này*): khóa `bulk` riêng mỗi kết nối (`K_bulk` từ `K_stream` và hai nonce, 0.4.7); schema cho `bulk/stream_hello` và `bulk/stream_welcome` | Chính các vector và mẫu âm là test: generator tạo ra, `check_*` kiểm tra | `shared/test-vectors/`, `shared/schemas/` | Generator tái tạo được; `check_schemas.py`, `check_strings.py` xanh; báo các nền tảng khác chạy lại test | | S7.1d [shared] | Cho SP4: `ssid` và `passphrase` Wi-Fi Direct sinh ngẫu nhiên cho mỗi group (0.4.6), nên không có vector suy khóa: mẫu kiểm định dạng và bộ ký tự, cả dương lẫn âm (tiền tố `DIRECT-`, độ dài, bảng chữ, entropy của passphrase); schema cho `direct/start`, `direct/stop`, `direct/want` và ack của `direct/start` `{ssid, passphrase, host, port}` (00-common-specs 0.7.1); sự kiện bench `wifi_assoc`; chuỗi cho hộp thoại Location | Chính các vector và mẫu âm là test: generator tạo ra, `check_*` kiểm tra | `shared/test-vectors/`, `shared/schemas/`, `shared/strings/`, `shared/tools/bench/` | Generator tái tạo được; `check_schemas.py`, `check_strings.py` xanh; báo các nền tảng khác chạy lại test | | A7.1a [android] | *(Tách 2026-10-28 theo quyết định của Lead; trước là A7.1.)* Lõi link, chưa có radio: codec bản ghi và codec frame, mọi độ dài được kiểm trước khi cấp phát; làn ngắn và làn dài của kênh điều khiển cùng việc ráp mảnh; bộ lập lịch của 0.4.5 "Ưu tiên" (điều khiển ngắn > điều khiển dài > bulk, `BT_BULK_INFLIGHT`; chuyển từ A7.4 sang đây vì điều kiện 1 của G7 coi nó là một phần của làn điều khiển); lớp bảo mật link (bên đáp Noise, thử psk của từng cặp, rekey link); admission Bluetooth cho cả listener (`BT_LINK_LIMIT`, `BT_PREAUTH_LIMIT`, `BT_PREAUTH_TIMEOUT`, `BT_AUTH_FAIL_BLOCK`; không bao giờ theo địa chỉ) | Vector codec và lớp bảo mật link của S7.1a; test qua ống giả: bắt tay, `noise1` phát lại trên kênh mới, rekey link, trần pre-auth, chặn sau `BT_AUTH_FAIL_BLOCK`, `rlen` quá cỡ, envelope ngắn gửi sau một envelope 256 KiB vẫn tới trước, hai envelope cùng `type` giữ thứ tự, giới hạn số bản ghi chưa đi hết | `android/core/transport` | Mọi test xanh | | A7.1b [android] | Radio và nối dây: bộ quảng bá và L2CAP server trong A-SVC (xoay vòng mỗi `BT_HINT_PERIOD`, dựng lại khi `STATE_ON`, độc lập với server LAN và với mạng), `SessionTransport.BLUETOOTH` và mọi `when` theo nó (`PeerSession.Channel` và nơi dùng nó trong các module feature), `BluetoothPeerSocket` (`WebSocketSession` ảo như `RelayPeerSocket`) phục vụ qua `ControlServer.serveBluetoothPeer`, `session/bye` trước mọi lần đóng trên Bluetooth; một **shim loopback chỉ có ở bản debug** cho bộ e2e (không cần cho phép đo SP1, có thể làm sau cùng) | Test hồi quy cho `serveRelayPeer`; test phiên qua ống giả; xoay vòng và dựng lại khi `STATE_ON` với adapter giả; test bản release khẳng định không có shim | `android/core/transport`, `android/feature/connection`, `android/feature/*` (chỉ phần ánh xạ route), `android/app` | Mọi test xanh; phủ các ngoại lệ của CONN-05; đo AC1 (Bluetooth) trên S25, khi rảnh và khi có tải điều khiển dài giả (chỉ bản debug) qua bộ lập lịch; scan response chứa service data đọc được trên Mac, kể cả khi app ở nền | @@ -119,11 +120,11 @@ và commit (ở trạng thái đỏ) trước code của thẻ đó. | A7.2 [android] | Ghép đôi lần đầu khi offline: quảng bá `pr` (dạng HMAC) trong chế độ QR; chỉ cấp slot ghép đôi duy nhất sau một frame đầu có MAC `ps` hợp lệ; PAIR-01 qua liên kết Bluetooth đã bảo mật; PIN qua Bluetooth bằng CPace trên primitive của A7.2a (luồng C của PAIR-01: quảng bá `BT_PIN_HINT`, `cpace1`/`cpace2`, đếm lần thử, `BT_PIN_LOCKOUT`) — *đổi ngày 2026-10-28 theo quyết định của sếp, trước là "từ chối PIN qua Bluetooth một cách tường minh"* | Vector PAIR-01 qua ống giả; vector CPace của S7.1 (`cpace.json`); test âm: PIN sai (3 lần rồi khóa tạm), `Ya` bậc thấp, `cpace1` ngoài cửa sổ PIN, `mode` không khớp loại link, MAC frame đầu thiếu/sai, chiếm giữ slot, người thứ hai tranh slot | `android/feature/pairing`, `android/core/transport` | Phủ các ngoại lệ PAIR-01 qua Bluetooth; đo AC6 | | M7.2a [macOS] | *(Thêm 2026-10-28 theo quyết định của Lead.)* Primitive CPace, cùng phạm vi với A7.2a (CryptoKit không có số học trường) | Như A7.2a | `apple/Packages/HLCrypto` | Như A7.2a | | M7.2 [macOS] | Ghép đôi lần đầu khi offline: quét `pr`, thử lần lượt mọi ứng viên có cùng `pr`, chạy PAIR-01 qua `BluetoothChannel` đã bảo mật, rồi ghim và lưu như hiện nay; PIN: quét `BT_PIN_HINT`, làm bên khởi tạo CPace trên primitive của M7.2a, đếm số lần sai PIN, sinh PIN mới khi tới 3 (*thêm ngày 2026-10-28, sếp quyết định*) | Cùng bộ vector cộng `cpace.json`; timeout, `pr` sai, máy quảng bá giả có đúng `pr`, điện thoại ở chế độ PIN với PIN khác, kẻ tấn công trả lời `cpace1` | `apple/Packages/HLTransport` (`Pairing*`) | Đo AC6; không đổi định dạng QR | -| A7.3 [android] | Hạ tầng stream channel: server `/v1/stream/*` trên A-SVC (`stream_hello`/`stream_welcome`, 0.6.3 bước 7) — **dùng lại nếu một thẻ sản phẩm của Phase 4 hoặc 5 đã xây trước** (quyết định I9 của plan) | Test vector bắt tay stream (viết trước) | `android/core/transport`, `android/core/crypto` | Bắt tay stream chạy trên LAN; `camera`/`call-audio` không bị ảnh hưởng | -| M7.3 [macOS] | Client stream channel, cùng phạm vi và quy tắc dùng lại như A7.3 | Như A7.3 | `apple/Packages/HLTransport` | Như A7.3 | +| A7.3 [android] | *(Chuyển sang SP4 ngày 2026-10-30 theo quyết định của Lead: qua Bluetooth, bulk đi kênh `0x02` bằng khóa phiên; chỉ `/v1/stream/bulk` trên Wi-Fi Direct mới cần server stream.)* Hạ tầng stream channel: server `/v1/stream/*` trên A-SVC (`stream_hello`/`stream_welcome`, 0.6.3 bước 7) — **dùng lại nếu một thẻ sản phẩm của Phase 4 hoặc 5 đã xây trước** (quyết định I9 của plan) | Test vector bắt tay stream (viết trước) | `android/core/transport`, `android/core/crypto` | Bắt tay stream chạy trên LAN; `camera`/`call-audio` không bị ảnh hưởng | +| M7.3 [macOS] | *(Chuyển sang SP4 ngày 2026-10-30, như A7.3.)* Client stream channel, cùng phạm vi và quy tắc dùng lại như A7.3 | Như A7.3 | `apple/Packages/HLTransport` | Như A7.3 | | R7.1 [relay] | **Bỏ 2026-10-28 (không deploy relay).** Trước đây: xác nhận relay chở bulk mà không cần sửa: frame nhị phân `HR` cỡ bulk với giới hạn hiện có; không đổi wire hay giới hạn | Test chuyển tiếp cho frame `HR` cỡ bulk (đỏ nếu relay xử lý sai) | Chỉ test trong `relay/` | `cargo test`, `cargo clippy` xanh; không đổi `wire.rs` hay giới hạn | -| A7.4 [android] | Làn bulk: stream `bulk` với khóa riêng mỗi kết nối; cả lượt truyền CLIP-03 đi trên làn bulk; chọn đường theo bảng định tuyến (LAN > Wi-Fi Direct > Bluetooth); cắm kênh bulk `0x02` vào bộ lập lịch của A7.1a (bộ lập lịch chuyển sang A7.1a ngày 2026-10-28). *Bỏ 2026-10-28:* bulk qua relay tách trong `RelayPeerMux`, op nhu cầu bulk, quy tắc giữ relay | Hồi quy CLIP-03 qua `/v1/ctl` trước; rồi đỏ: chunk tới trước push, AC1 khi đang truyền bulk 5 MB, một ảnh 10 MiB chỉ qua Bluetooth vẫn truyền xong ở chế độ nền (*bỏ:* hai test về relay) | `android/core/transport`, `android/feature/clipboard` | Các ngoại lệ CLIP-03 qua trên mọi đường; AC1 (LAN) không đổi; AC3 (LAN) không đổi | -| M7.4 [macOS] | Làn bulk phía Mac, cùng phạm vi với A7.4, trên bộ lập lịch của M7.1b (điểm cắm `ClipboardPeer`; bỏ phần tách trong `RelayLink`/`RelayPeerChannel` 2026-10-28) | Cùng thứ tự như A7.4 | `apple/Packages/HLTransport`, `apple/Packages/HLAppCore` (bảng nhớ tạm) | Như A7.4 | +| A7.4 [android] | *(Đổi phạm vi 2026-10-30 theo quyết định của Lead: SP3 chỉ là làn bulk qua Bluetooth; làn `/v1/stream/bulk` vào SP4 cùng A7.5.)* Làn bulk qua Bluetooth theo mục "Làn bulk trên link Bluetooth" của 00-common-specs 0.4.7: `EnvelopeLane` trên mọi đường gửi (luật bulk 2); cả lượt truyền CLIP-03 đi kênh `0x02` (luật 1); `BluetoothPeerSocket` là `BulkLaneSocket` với chặn ngược `BT_BULK_QUEUED` (luật 3–4); ráp và đưa kênh `0x02` lên trên (luật 5–6); chọn làn LAN > Bluetooth lúc bắt đầu lượt truyền, chừa sẵn chỗ trống cho làn Wi-Fi Direct (SP4 lấp vào; tới lúc đó ảnh cỡ nào cũng đi Bluetooth ở chế độ nền); làm lại từ đầu khi phiên LAN thay phiên Bluetooth (mục "Đổi làn giữa chừng" của 0.4.7, CLIP-03 E11). *Bỏ 2026-10-28:* bulk qua relay tách trong `RelayPeerMux`, op nhu cầu bulk, quy tắc giữ relay | Hồi quy CLIP-03 qua `/v1/ctl` trước; rồi đỏ, mỗi luật một test: luật 1 làn của từng envelope (push có `transfer`, chunk, cancel, `ack` của lượt truyền và `conflict` theo sau nó đi `0x02`; push gửi thẳng và `ack` của nó đi `0x01`); luật 2 `ack` giữ cho bản trùng được gửi lại trên làn của lần đầu; luật 4 chunk thứ ba phải chờ khi đã có 2 trong hàng đợi; luật 5 đóng link khi `long` = 1, khi `dlen` vượt `BT_BULK_FRAGMENT_MAX`, khi message vượt `BT_MESSAGE_MAX`; luật 6 envelope bulk tới bộ điều phối và vào `DEDUP_WINDOW`; luật 7 push gửi thẳng vượt `cancel` `superseded` trên làn bulk vẫn thay lượt truyền cũ; luật 8 message bulk ráp dở bị bỏ khi link đóng; AC1 khi đang truyền bulk 5 MB; một ảnh 10 MiB chỉ qua Bluetooth vẫn truyền xong ở chế độ nền (*bỏ:* hai test về relay) | `android/core/transport`, `android/feature/connection` (`session/`), `android/feature/clipboard` | Các ngoại lệ CLIP-03 qua trên LAN và qua Bluetooth; đo AC1 (Bluetooth, khi đang truyền bulk) trên S25 + Mac; AC1 (LAN) và AC3 (LAN) không đổi | +| M7.4 [macOS] | *(Đổi phạm vi 2026-10-30, như A7.4.)* Làn bulk phía Mac, cùng phạm vi với A7.4, trên bộ lập lịch của M7.1b: `EnvelopeLane` và `ChannelMessage.bulkText` (luật bulk 2); `BluetoothChannel` gửi `.bulkText` lên `0x02` có chặn ngược, đưa `0x02` lên dưới dạng `.text`, giới hạn bộ ráp ở `BT_MESSAGE_MAX`, ném lỗi với `.binary` (luật 3–6); điểm cắm `ClipboardPeer` truyền làn xuống (bỏ phần tách trong `RelayLink`/`RelayPeerChannel` 2026-10-28) | Cùng thứ tự và danh sách luật như A7.4 | `apple/Packages/HLTransport`, `apple/Packages/HLAppCore` (bảng nhớ tạm) | Như A7.4; iOS vẫn build và chạy như trước | | A7.5 [android] | Host Wi-Fi Direct (chỉ X, theo nhu cầu): nhóm với SSID và passphrase sinh ngẫu nhiên cho mỗi group (0.4.6), `LEGACY_OR_R2` trên API 36, bật/tắt theo yêu cầu qua làn điều khiển, giữ nguyên STA; `NEARBY_WIFI_DEVICES` (13+), `ACCESS_FINE_LOCATION` + dịch vụ vị trí (10–12); Wi-Fi tắt → báo, không bao giờ tự bật | Vector kiểm định dạng và bộ ký tự của S7.1d (*đổi 2026-10-28: không có suy khóa*); test vòng đời với `WifiP2pManager` giả (bật, dừng, sập, STA không đổi, dừng sau `DIRECT_WIFI_MAX`) | `android/feature/connection` (hoặc `feature/directlink` mới) | Kiểm AC4 trên S25; đo AC3 trong X | | M7.5 [macOS] | Join Wi-Fi Direct (chỉ X): quy tắc vào/ra của AC4 (theo nhu cầu, rảnh ≥ `DIRECT_WIFI_IDLE_MIN`, không thấy mạng quen, quét định kỳ, `DIRECT_WIFI_MAX`), join bằng CoreWLAN, kết nối gateway bằng chứng chỉ đã ghim, socket gắn vào giao diện P2P, xóa mạng đã lưu sau khi rời | Đỏ trước: không bao giờ join khi đang kết nối, không bao giờ join khi thấy mạng quen (trường hợp deauth), rời khi thấy mạng quen, rời khi hết thời gian tối đa, đã xóa mạng đã lưu | `apple/Packages/HLTransport`, `apple/macOS/HandLive` | Kiểm AC4, gồm cả kịch bản deauth; AC3 trong X ≤ 2 s | | A7.6 [android] | **Bỏ 2026-10-28 (SP5; Phase 4 đã dừng, không có HFP).** Trước đây: tự ghép đôi Bluetooth cổ điển, **khởi động ngay sau khi ghép đôi QR** (và một lần cho các cặp đã có khi tính năng ra mắt, hoặc bất cứ khi nào thiếu bond) , luồng từ G7 ((b) hoặc (c)): yêu cầu ghép đôi qua E2E, `createBond` tới địa chỉ Mac nhận qua E2E, gửi mã ghép đôi qua E2E khi đọc được | Test với adapter giả cho từng trạng thái; không bao giờ ghép đôi khi chưa có phiên đã xác thực; không yêu cầu chế độ cho phép tìm thấy | `android/feature/call` hoặc `android/feature/connection` | Đo AC5, AC10; phủ đường bị từ chối và timeout | @@ -146,11 +147,14 @@ và commit (ở trạng thái đỏ) trước code của thẻ đó. A7.1b ∥ M7.1c, cùng A7.7a ∥ M7.7a. Trên Android, A7.1b và A7.7a cùng sửa `android/app` và `feature/connection`: làm lần lượt. S7.1b chạy trong SP1 để `cpace.json` được duyệt trước SP2. 2. SP2: A7.2a ∥ M7.2a (bắt đầu được ngay khi S7.1b đã được duyệt), rồi A7.2 ∥ M7.2. - 3. SP3: S7.1c; A7.3 ∥ M7.3 (nếu chưa có), rồi A7.4 ∥ M7.4. - 4. SP4: S7.1d; A7.5 ∥ M7.5, cùng A7.7b ∥ M7.7b. + 3. SP3 (*đổi phạm vi 2026-10-30 theo quyết định của Lead*): A7.4 ∥ M7.4 theo contract ở mục "Làn bulk trên link + Bluetooth" của 0.4.7; không có thẻ shared (không thêm vector, schema hay chuỗi). SP3 phát hành riêng, trước SP4. + 4. SP4: T7.0b → S7.1c ∥ S7.1d → A7.3 ∥ M7.3 → A7.5 ∥ M7.5 → A7.7b ∥ M7.7b. Trong cùng một nền tảng các thẻ sửa chung + tệp nên làm lần lượt: Android A7.5 → A7.7b (`feature/connection`, `AndroidManifest.xml`); Mac M7.4 → M7.5 → M7.7b + (`ControlSession` và `ConnectionManager` trong `HLTransport`, rồi `apple/macOS/HandLive`). 5. T7.1. - Mỗi sub-project kết thúc bằng một lượt đo trên S25 + Mac rồi mới mở sub-project sau: SP1 đo AC1 (rảnh và khi có - tải), AC2, scan response; SP2 đo AC6. + tải), AC2, scan response; SP2 đo AC6; SP3 đo AC1 khi đang truyền bulk qua Bluetooth và một ảnh 10 MiB ở chế độ nền. - *Bỏ 2026-10-28:* SP5 (A7.6 ∥ M7.6) và quy tắc "nếu RFCOMM cần bond thì SP5 chuyển lên trước SP1" — lớp mang RFCOMM cần bond giờ coi là không đạt G7 (xem mục Lớp mang ở trên). - Mỗi bước logic một commit, `shared/` trước nền tảng, không bao giờ một commit trải qua hai repository. @@ -306,3 +310,12 @@ mức Light. (probe scan response) trước khi chốt vector quảng bá. - `ssid`/`passphrase` Wi-Fi Direct sinh ngẫu nhiên cho mỗi group (0.4.6): S7.1d và A7.5 kiểm định dạng và bộ ký tự, không có vector suy khóa. + +### Tách thẻ — 2026-10-30 (quyết định của Lead, chat của đội) +- Qua Bluetooth, bulk đi kênh `0x02` của link bằng khóa phiên (0.4.7); `/v1/stream/bulk` và `K_bulk` chỉ phục vụ + Wi-Fi Direct. Vì vậy S7.1c, A7.3 và M7.3 chuyển từ SP3 sang SP4; SP3 chỉ còn A7.4 ∥ M7.4 và phát hành trước SP4. + Tới khi có SP4, ảnh cỡ nào cũng đi Bluetooth ở chế độ nền. +- Contract của SP3 (bên gửi chọn làn, đưa kênh `0x02` lên trên, `BT_BULK_QUEUED`) được viết trong mục "Làn bulk trên + link Bluetooth" của 0.4.7 trước khi A7.4/M7.4 bắt đầu. +- Thêm T7.0b: Wi-Fi Direct trên Mac chưa từng được probe (G7 để lại cho SP4), nên thời gian join, việc xóa mạng đã lưu, + quyền Location và việc gắn socket vào interface P2P được đo trước mọi dòng code của SP4. diff --git a/plans/20260925-implementation/reports/phase-07-sp3.md b/plans/20260925-implementation/reports/phase-07-sp3.md new file mode 100644 index 0000000..03112a8 --- /dev/null +++ b/plans/20260925-implementation/reports/phase-07-sp3.md @@ -0,0 +1,73 @@ +English | [Tiếng Việt](phase-07-sp3.vi.md) + +# SP3 — bulk lane on the Bluetooth link (A7.4 ∥ M7.4, 0.4.7) + +Goal: carry the whole CLIP-03 transfer (push with `transfer`, chunks, cancel, ack, conflict) on channel `0x02` of the Bluetooth link, per "Bulk lane on the Bluetooth link (channel `0x02`)" in `docs/detailed-design/00-common-specs.md`. Every other envelope stays on `0x01`. The clipboard engine picks the lane, not the transport. Streaming is dropped from SP3. + +Status: **code done on Android, Mac and shared; not yet pushed**. Real-device measurement (AC1 on a Galaxy S25 and a real Mac) is owed and tracked as a separate task; the owner has no device for now. + +## Scope + +In scope: A7.4 (Android), M7.4 (Mac), the progress note strings in shared. + +Out of scope (moved to SP4): `/v1/stream/bulk` on Wi-Fi Direct (A7.3/M7.3, A7.5/M7.5), lane choice "LAN > Wi-Fi Direct > Bluetooth", restart on lane change. + +## Commits + +| Repo | Commits (oldest first) | +|---|---| +| `handlive` (spec 0.4.7) | `53f69a0` | +| `handlive-shared` | `3eb92ad` | +| `handlive-android` | `43b5b95`, `55ccf6f`, `091cdac`, `3f11939`, `be88bbe`, `0a29dd0`, `2cf835d`, `d2c37e2`, `cf64ef5` | +| `handlive-apple` | `c1a85c7`, `0cbea4b`, `abf5c3f`, `43aca3c`, `7473e93`, `cd217b5`, `fb94555`, `be4b18c`, `d02268b`, `fa9810d`, `e4026ae`, `f29efec` | + +`handlive-android` commits `be88bbe`, `0a29dd0` and `cf64ef5` are test, feature and comment steps of the same note; `2cf835d` and `d2c37e2` are the control-cuts-in-mid-bulk fix. + +## What changed + +- **Spec 0.4.7**: eight bulk rules, `BT_BULK_QUEUED` = 2. The receiver does not restrict envelope types on `0x02`. +- **Shared**: two strings, `clipboard.bulk_bluetooth_sending` and `clipboard.bulk_bluetooth_receiving` (CLIP-03 field 10), generated into both apps. Must reach the remote before Android so CI can generate them. +- **Android**: bulk lane in `BluetoothPeerSocket`, with the lane state moved out of it. `BluetoothLinkWritePump` lets short control envelopes cut in between bulk fragments (0.4.5 Priority). Progress shows from `BULK_DIRECT_MIN` (128 KiB) over Bluetooth, with the Bluetooth note under it. +- **Mac**: `EnvelopeLane` threads through `ControlSession` and `ClipboardPeer`; the clipboard engine picks `.bulk` for transfer envelopes. `BluetoothChannel` sends `.bulkText` on `0x02` with `BT_BULK_QUEUED` backpressure and hands bulk up as `.text`. Progress from 128 KiB over Bluetooth (LAN and relay keep 1 MiB), with the note in the menu bar. `WebSocketChannel` and `RelayPeerChannel` send `.bulkText` as a text frame. + +## Rules and where they live (Mac) + +| Rule | Where | +|---|---| +| Transfer on `0x02`, everything else on `0x01` | `ClipboardEngine+Sending.swift`, `+Receiving.swift`, `+Transfers.swift` pass `lane: .bulk` for transfer envelopes and `.control` for inline ones | +| Sender picks the lane, transport never guesses | `EnvelopeLane` on `ControlSession` and `ClipboardPeer`, default `.control` | +| Bulk envelopes queued | `BluetoothLinkScheduler.bulkEnvelopesQueued` | +| `BT_BULK_QUEUED` backpressure | `BluetoothChannel.Writer.send` waits on `bulkBackpressureWaiters` while the queue is full | +| Link closes on `long` = 1 on `0x02`, `dlen` > `BT_BULK_FRAGMENT_MAX`, message > `BT_MESSAGE_MAX` | `BtTransportFrame.decode`, `BluetoothBulkReassembler.accept` | +| Reassembled bulk delivered as `.text`; `.binary` rejected | `BluetoothChannel.readLoop`, `BluetoothChannel` send | +| Half-reassembled bulk dropped at link close | `BluetoothBulkReassembler` is per channel and dies with it | + +## Tests + +| Repo | Result | +|---|---| +| Android | `./gradlew check`: 1128 tests green (reported by Dev2). `BulkLaneContractTest` covers the contract; `be88bbe` was red by design until `0a29dd0`. | +| Mac | `HLTransportTests` 190, `HLAppCoreTests` 147, `HLMacUITests` 71, all green; Mac and iOS builds green. | + +Mac test files: `BluetoothBulkLaneTests` (lane per type, `long` = 1, `dlen`, oversize, multi-fragment, half-reassembled), `BluetoothBulkLaneTransferTests` (control overtakes bulk, full queue, 10 MiB image, `.binary` rejected, LAN text frame), `ClipboardBulkLaneTests` (lane of each envelope, 128 KiB threshold), `AppModelClipboardTests.bluetoothNote`. + +`HLMacUI` under `xcodebuild` is red from a CoreBluetooth crash already on `main`, not from SP3. + +## Reviews + +A7.4: Khoa, Trang and Huy approve; Huy's high finding (control starved during bulk) fixed in `d2c37e2`. M7.4: Khoa, Trang and Huy approve. Reviewers on Mac did not rerun the tests; the figures above are from the authors. + +## Open items + +- AC1 on a Galaxy S25 and a real Mac, for both A7.4 and M7.4. Runbook: `phase-07-sp1-measure.md`, session 2. +- Android: the progress threshold waits for Khoa's confirmation on the card. Two low notes from Trang remain. +- Mac, low and non-blocking: the sender does not cap at `BT_MESSAGE_MAX` itself (`BluetoothChannel.swift:105`, the peer closes the link); a bulk send cancelled while waiting on backpressure does not resume its continuation (cleaned up when the channel closes). +- CoreBluetooth tests red on `main` need their own task. + +## Push order + +shared `3eb92ad` → android → apple → hub, by fixed SHA with `tools/sp3-push.sh` (`--dry-run` first), to the remote branch `feat/p07-sp3-bulk-bluetooth`. The local branch `feat/p07-sp3-sp4-bulk-wifi-direct` also takes SP4 commits. + +## What's next + +SP4: T7.0b → S7.1c ∥ S7.1d → A7.3 ∥ M7.3 → A7.5 ∥ M7.5 → A7.7b ∥ M7.7b. diff --git a/plans/20260925-implementation/reports/phase-07-sp3.vi.md b/plans/20260925-implementation/reports/phase-07-sp3.vi.md new file mode 100644 index 0000000..a2eadd0 --- /dev/null +++ b/plans/20260925-implementation/reports/phase-07-sp3.vi.md @@ -0,0 +1,73 @@ +[English](phase-07-sp3.md) | Tiếng Việt + +# SP3 — làn bulk trên liên kết Bluetooth (A7.4 ∥ M7.4, 0.4.7) + +Mục tiêu: đưa toàn bộ một lần chuyển CLIP-03 (push kèm `transfer`, chunk, cancel, ack, conflict) lên kênh `0x02` của liên kết Bluetooth, theo mục "Bulk lane on the Bluetooth link (channel `0x02`)" trong `docs/detailed-design/00-common-specs.md`. Mọi envelope khác vẫn ở `0x01`. Clipboard engine chọn làn, không phải tầng transport. SP3 đã bỏ phần stream. + +Trạng thái: **code xong ở Android, Mac và shared; chưa push**. Đo trên máy thật (AC1 trên Galaxy S25 và Mac thật) còn nợ, tách thành việc riêng; sếp tạm chưa có máy. + +## Phạm vi + +Trong phạm vi: A7.4 (Android), M7.4 (Mac), chuỗi ghi chú tiến độ ở shared. + +Ngoài phạm vi (sang SP4): `/v1/stream/bulk` trên Wi-Fi Direct (A7.3/M7.3, A7.5/M7.5), chọn làn "LAN > Wi-Fi Direct > Bluetooth", khởi động lại khi đổi làn. + +## Commit + +| Repo | Commit (cũ đến mới) | +|---|---| +| `handlive` (spec 0.4.7) | `53f69a0` | +| `handlive-shared` | `3eb92ad` | +| `handlive-android` | `43b5b95`, `55ccf6f`, `091cdac`, `3f11939`, `be88bbe`, `0a29dd0`, `2cf835d`, `d2c37e2`, `cf64ef5` | +| `handlive-apple` | `c1a85c7`, `0cbea4b`, `abf5c3f`, `43aca3c`, `7473e93`, `cd217b5`, `fb94555`, `be4b18c`, `d02268b`, `fa9810d`, `e4026ae`, `f29efec` | + +Ở `handlive-android`, `be88bbe`, `0a29dd0` và `cf64ef5` là các bước test, tính năng và chú thích của cùng ghi chú Bluetooth; `2cf835d` và `d2c37e2` là bản sửa để control chen được giữa lúc gửi bulk. + +## Đã thay đổi + +- **Spec 0.4.7**: tám luật bulk, `BT_BULK_QUEUED` = 2. Bên nhận không giới hạn kiểu envelope trên `0x02`. +- **Shared**: hai chuỗi `clipboard.bulk_bluetooth_sending` và `clipboard.bulk_bluetooth_receiving` (CLIP-03 trường 10), sinh vào cả hai app. Phải lên remote trước Android để CI sinh được chuỗi. +- **Android**: làn bulk trong `BluetoothPeerSocket`, trạng thái làn tách ra khỏi file này. `BluetoothLinkWritePump` cho envelope control ngắn chen giữa các mảnh bulk (0.4.5 Priority). Qua Bluetooth, tiến độ hiện từ `BULK_DIRECT_MIN` (128 KiB), kèm ghi chú Bluetooth bên dưới. +- **Mac**: `EnvelopeLane` đi qua `ControlSession` và `ClipboardPeer`; clipboard engine chọn `.bulk` cho envelope của lần chuyển. `BluetoothChannel` gửi `.bulkText` trên `0x02` với backpressure `BT_BULK_QUEUED` và chuyển bulk lên dạng `.text`. Qua Bluetooth, tiến độ hiện từ 128 KiB (LAN và relay giữ 1 MiB), ghi chú nằm trên menu bar. `WebSocketChannel` và `RelayPeerChannel` gửi `.bulkText` như một text frame. + +## Luật và nơi thực hiện (Mac) + +| Luật | Nơi thực hiện | +|---|---| +| Lần chuyển ở `0x02`, mọi thứ khác ở `0x01` | `ClipboardEngine+Sending.swift`, `+Receiving.swift`, `+Transfers.swift` truyền `lane: .bulk` cho envelope của lần chuyển và `.control` cho envelope inline | +| Bên gửi chọn làn, transport không đoán | `EnvelopeLane` trên `ControlSession` và `ClipboardPeer`, mặc định `.control` | +| Envelope bulk được xếp hàng | `BluetoothLinkScheduler.bulkEnvelopesQueued` | +| Backpressure `BT_BULK_QUEUED` | `BluetoothChannel.Writer.send` chờ trên `bulkBackpressureWaiters` khi hàng đợi đầy | +| Đóng liên kết khi `long` = 1 trên `0x02`, `dlen` > `BT_BULK_FRAGMENT_MAX`, message > `BT_MESSAGE_MAX` | `BtTransportFrame.decode`, `BluetoothBulkReassembler.accept` | +| Bulk ráp xong đi lên dạng `.text`; `.binary` bị từ chối | `BluetoothChannel.readLoop`, phần gửi của `BluetoothChannel` | +| Bulk ráp dở bị bỏ khi đóng liên kết | `BluetoothBulkReassembler` thuộc từng kênh và mất cùng kênh | + +## Kiểm thử + +| Repo | Kết quả | +|---|---| +| Android | `./gradlew check`: 1128 test xanh (Dev2 báo). `BulkLaneContractTest` phủ contract; `be88bbe` đỏ có chủ đích cho đến `0a29dd0`. | +| Mac | `HLTransportTests` 190, `HLAppCoreTests` 147, `HLMacUITests` 71, đều xanh; build Mac và iOS xanh. | + +File test ở Mac: `BluetoothBulkLaneTests` (làn theo kiểu envelope, `long` = 1, `dlen`, vượt trần, nhiều mảnh, ráp dở), `BluetoothBulkLaneTransferTests` (control vượt bulk, hàng đợi đầy, ảnh 10 MiB, `.binary` bị từ chối, text frame trên LAN), `ClipboardBulkLaneTests` (làn của từng envelope, ngưỡng 128 KiB), `AppModelClipboardTests.bluetoothNote`. + +`HLMacUI` chạy bằng `xcodebuild` đang đỏ vì crash CoreBluetooth có sẵn trên `main`, không phải do SP3. + +## Review + +A7.4: Khoa, Trang và Huy approve; finding high của Huy (control bị bỏ đói khi gửi bulk) đã sửa ở `d2c37e2`. M7.4: Khoa, Trang và Huy approve. Reviewer phía Mac không chạy lại test; số liệu trên là của người viết code. + +## Việc còn nợ + +- AC1 trên Galaxy S25 và Mac thật, cho cả A7.4 và M7.4. Runbook: `phase-07-sp1-measure.md`, session 2. +- Android: ngưỡng tiến độ chờ Khoa xác nhận trên thẻ. Hai ghi chú low của Trang còn mở. +- Mac, low và không chặn: bên gửi chưa tự chặn `BT_MESSAGE_MAX` (`BluetoothChannel.swift:105`, bên kia sẽ đóng liên kết); bulk bị huỷ giữa lúc chờ backpressure không resume continuation (được dọn khi kênh đóng). +- Test CoreBluetooth đỏ sẵn trên `main` cần một việc riêng. + +## Thứ tự push + +shared `3eb92ad` → android → apple → hub, theo SHA cố định bằng `tools/sp3-push.sh` (chạy `--dry-run` trước), sang nhánh remote `feat/p07-sp3-bulk-bluetooth`. Nhánh local `feat/p07-sp3-sp4-bulk-wifi-direct` còn nhận thêm commit SP4. + +## Tiếp theo + +SP4: T7.0b → S7.1c ∥ S7.1d → A7.3 ∥ M7.3 → A7.5 ∥ M7.5 → A7.7b ∥ M7.7b. diff --git a/tools/git-status-all.sh b/tools/git-status-all.sh new file mode 100644 index 0000000..871ca21 --- /dev/null +++ b/tools/git-status-all.sh @@ -0,0 +1,43 @@ +#!/bin/zsh +# In trạng thái git của tất cả 6 repo (hub, shared, android, apple, relay, .github-org) +# theo bố cục cạnh nhau: mọi repo nằm ngang hàng trong thư mục cha của hub. +# +# zsh handlive/tools/git-status-all.sh +# +# Với mỗi repo: in '== ' rồi git status -sb, log -1 --oneline. +# Repo nào không có .git thì in 'không có repo' và đi tiếp. +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +HUB_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +WORKSPACE_ROOT="$(cd "$HUB_ROOT/.." && pwd)" + +typeset -a REPO_NAMES +typeset -a REPO_PATHS +REPO_NAMES=(hub shared android apple relay github-org) +REPO_PATHS=( + "$HUB_ROOT" + "$WORKSPACE_ROOT/handlive-shared" + "$WORKSPACE_ROOT/handlive-android" + "$WORKSPACE_ROOT/handlive-apple" + "$WORKSPACE_ROOT/handlive-relay" + "$WORKSPACE_ROOT/.github-org" +) + +i=1 +while [ "$i" -le "${#REPO_NAMES[@]}" ]; do + name="${REPO_NAMES[$i]}" + repo_path="${REPO_PATHS[$i]}" + i=$((i + 1)) + + echo "== $name ($repo_path) ==" + + if [ ! -d "$repo_path/.git" ]; then + echo "không có repo" + continue + fi + + echo "$(cd "$repo_path" && git status -sb)" + echo "$(cd "$repo_path" && git log -1 --oneline)" + echo +done diff --git a/tools/measure-sp1-auto.sh b/tools/measure-sp1-auto.sh index a279342..0268e09 100755 --- a/tools/measure-sp1-auto.sh +++ b/tools/measure-sp1-auto.sh @@ -1,18 +1,18 @@ #!/bin/zsh -# Chay NGOAI sandbox qua nut run cua sep (S25 cam qua USB, Mac nay la may do), khong co nguoi ngoi theo doi: +# Chạy NGOÀI sandbox qua nút run của sếp (S25 cắm qua USB, Mac này là máy đo), không có người ngồi theo dõi: # zsh handlive/tools/measure-sp1-auto.sh --session 1|2|3 [adb_serial] # -# Lam tron mot phien cua runbook handlive/plans/20260925-implementation/reports/phase-07-sp1-measure.md ma -# khong can thao tac tay: goi measure-sp1.sh nen de build/mo app + ghi log HLBENCH, cho link len Bluetooth, -# tu lam cac buoc copy/toggle/doze cua phien, roi dung ghi log va chay clip_latency.py/reconnect_time.py. -# Sandbox cua agent chan moi ket noi adb (xem #3717-#3720), nen toan bo khoi nay phai chay qua nut run. +# Làm trọn một phiên của runbook handlive/plans/20260925-implementation/reports/phase-07-sp1-measure.md mà +# không cần thao tác tay: gọi measure-sp1.sh nền để build/mở app + ghi log HLBENCH, chờ link lên Bluetooth, +# tự làm các bước copy/toggle/doze của phiên, rồi dừng ghi log và chạy clip_latency.py/reconnect_time.py. +# Sandbox của agent chặn mọi kết nối adb (xem #3717-#3720), nên toàn bộ khối này phải chạy qua nút run. # -# --session 1: phien day du (khong --log-only) — build + cai lai APK/app macOS tu dau. -# --session 2|3: dung lai APK/app macOS cua lan --session 1 gan nhat (them --log-only vao measure-sp1.sh). +# --session 1: phiên đầy đủ (không --log-only) — build + cài lại APK/app macOS từ đầu. +# --session 2|3: dùng lại APK/app macOS của lần --session 1 gần nhất (thêm --log-only vào measure-sp1.sh). # -# Chieu S25->Mac tu dong hoa bang broadcast cua app "Clipper" (ca.zgrs.clipper, trick quen thuoc de set -# clipboard Android qua adb ma khong can UI — Android chan ClipboardManager tu background tu Android 10). -# Khong co app do tren S25 thi buoc nay tu bo qua va in ro, chi con chieu Mac->S25 (pbcopy) nhu runbook cho phep. +# Chiều S25->Mac tự động hóa bằng broadcast của app "Clipper" (ca.zgrs.clipper, trick quen thuộc để set +# clipboard Android qua adb mà không cần UI — Android chặn ClipboardManager từ background từ Android 10). +# Không có app đó trên S25 thì bước này tự bỏ qua và in rõ, chỉ còn chiều Mac->S25 (pbcopy) như runbook cho phép. set -uo pipefail zmodload zsh/datetime @@ -30,7 +30,7 @@ while [ $# -gt 0 ]; do shift ;; -*) - echo "LOI: khong biet tuy chon: $1" >&2 + echo "LỖI: không biết tùy chọn: $1" >&2 exit 2 ;; *) @@ -43,7 +43,7 @@ done case "$SESSION" in 1|2|3) ;; *) - echo "Dung: zsh $0 --session 1|2|3 [adb_serial]" >&2 + echo "Dùng: zsh $0 --session 1|2|3 [adb_serial]" >&2 exit 2 ;; esac @@ -58,10 +58,10 @@ mkdir -p "$LOG_DIR" TS="$(date +%Y%m%d-%H%M%S)" : "${TMPDIR:=/tmp}" -[ -f "$MEASURE_SH" ] || { echo "LOI: khong thay $MEASURE_SH" >&2; exit 1; } -command -v adb >/dev/null || { echo "LOI: khong thay adb trong PATH" >&2; exit 1; } -command -v osascript >/dev/null || { echo "LOI: khong thay osascript trong PATH" >&2; exit 1; } -command -v pbcopy >/dev/null || { echo "LOI: khong thay pbcopy trong PATH" >&2; exit 1; } +[ -f "$MEASURE_SH" ] || { echo "LỖI: không thấy $MEASURE_SH" >&2; exit 1; } +command -v adb >/dev/null || { echo "LỖI: không thấy adb trong PATH" >&2; exit 1; } +command -v osascript >/dev/null || { echo "LỖI: không thấy osascript trong PATH" >&2; exit 1; } +command -v pbcopy >/dev/null || { echo "LỖI: không thấy pbcopy trong PATH" >&2; exit 1; } typeset -a ADB_ARGS if [ -n "$SERIAL" ]; then @@ -75,13 +75,13 @@ MEASURE_PID="" LOGGING_STOPPED=0 fail() { - echo "LOI: $1" >&2 + echo "LỖI: $1" >&2 if [ -n "$ANDROID_LOG" ] && [ -f "$ANDROID_LOG" ]; then - echo "--- 20 dong cuoi $ANDROID_LOG ---" >&2 + echo "--- 20 dòng cuối $ANDROID_LOG ---" >&2 tail -n 20 "$ANDROID_LOG" >&2 fi if [ -n "$MAC_LOG" ] && [ -f "$MAC_LOG" ]; then - echo "--- 20 dong cuoi $MAC_LOG ---" >&2 + echo "--- 20 dòng cuối $MAC_LOG ---" >&2 tail -n 20 "$MAC_LOG" >&2 fi exit 1 @@ -89,7 +89,7 @@ fail() { cleanup() { echo - echo "== Don dep: dung ghi log (neu con) va khoi phuc S25 ==" + echo "== Dọn dẹp: dừng ghi log (nếu còn) và khôi phục S25 ==" if [ -n "$MEASURE_PID" ] && [ "$LOGGING_STOPPED" -eq 0 ]; then kill -INT "$MEASURE_PID" 2>/dev/null || true wait "$MEASURE_PID" 2>/dev/null || true @@ -115,11 +115,11 @@ wait_for_connected() { return 1 } -# Trick "Clipper" (ca.zgrs.clipper): `am broadcast -a clipper.set -e text VALUE` set duoc clipboard Android tu -# adb ma khong can UI. `am broadcast` luon tra "result=0" du co receiver nhan hay khong (khong co cach phan -# biet qua ma ket qua), nen kiem truoc bang pm list packages (goi 1 lan trong session 1, bien CLIPPER_INSTALLED -# duoi day): khong cai app nay thi bo qua ngay, co cai thi gui broadcast va coi la thanh cong (khong co cach -# nao xac nhan sau hon tu phia adb). +# Trick "Clipper" (ca.zgrs.clipper): `am broadcast -a clipper.set -e text VALUE` set được clipboard Android từ +# adb mà không cần UI. `am broadcast` luôn trả "result=0" dù có receiver nhận hay không (không có cách phân +# biệt qua mã kết quả), nên kiểm trước bằng pm list packages (gọi 1 lần trong session 1, biến CLIPPER_INSTALLED +# dưới đây): không cài app này thì bỏ qua ngay, có cài thì gửi broadcast và coi là thành công (không có cách +# nào xác nhận sau hơn từ phía adb). CLIPPER_INSTALLED=0 android_copy_text() { @@ -128,8 +128,8 @@ android_copy_text() { adb "${ADB_ARGS[@]}" shell am broadcast -a clipper.set -e text "$text" >/dev/null 2>&1 } -# Tao mot anh ~5 MB (BMP ghi tay bang python3 stdlib roi doi sang PNG bang sips, khong can PIL) de dua len -# pasteboard Mac bang osascript — khong dung noi dung chep tu dau khac vi khong co file anh san co dung kich co. +# Tạo một ảnh ~5 MB (BMP ghi tay bằng python3 stdlib rồi đổi sang PNG bằng sips, không cần PIL) để đưa lên +# pasteboard Mac bằng osascript — không dùng nội dung chép từ đâu khác vì không có file ảnh sẵn có đúng kích cỡ. make_bench_image() { local out_bmp="$1" out_png="$2" python3 - "$out_bmp" <<'PYEOF' @@ -138,7 +138,7 @@ import struct import sys path = sys.argv[1] -width, height = 1700, 1024 # 24-bit BMP, hang khong can padding (1700*3=5100 chia het cho 4) -> khoang 5.2 MB. +width, height = 1700, 1024 # 24-bit BMP, hàng không cần padding (1700*3=5100 chia hết cho 4) -> khoảng 5.2 MB. row_bytes = width * 3 pixel_data_size = row_bytes * height file_size = 54 + pixel_data_size @@ -161,9 +161,9 @@ set_mac_image_clipboard() { echo "== 1. adb devices ==" adb devices -echo "== 2. Tat Wi-Fi + du lieu di dong tren S25 (Mac van bat Wi-Fi) ==" -adb "${ADB_ARGS[@]}" shell svc wifi disable || fail "svc wifi disable that bai" -adb "${ADB_ARGS[@]}" shell svc data disable || fail "svc data disable that bai" +echo "== 2. Tắt Wi-Fi + dữ liệu di động trên S25 (Mac vẫn bật Wi-Fi) ==" +adb "${ADB_ARGS[@]}" shell svc wifi disable || fail "svc wifi disable thất bại" +adb "${ADB_ARGS[@]}" shell svc data disable || fail "svc data disable thất bại" typeset -a MEASURE_ARGS if [ "$SESSION" != "1" ]; then @@ -174,16 +174,16 @@ if [ -n "$SERIAL" ]; then fi MEASURE_OUT="$LOG_DIR/sp1-auto-session${SESSION}-$TS-measure.out" -echo "== 3. Chay measure-sp1.sh nen (session $SESSION, args: ${MEASURE_ARGS[*]:-}) ==" +echo "== 3. Chạy measure-sp1.sh nền (session $SESSION, args: ${MEASURE_ARGS[*]:-}) ==" zsh "$MEASURE_SH" "${MEASURE_ARGS[@]}" > "$MEASURE_OUT" 2>&1 & MEASURE_PID=$! -echo "measure-sp1.sh PID=$MEASURE_PID, output day du tai: $MEASURE_OUT" +echo "measure-sp1.sh PID=$MEASURE_PID, output đầy đủ tại: $MEASURE_OUT" -echo "== 4. Doi measure-sp1.sh in duong dan log (toi da 600s, co build app) ==" +echo "== 4. Đợi measure-sp1.sh in đường dẫn log (tối đa 600s, có build app) ==" waited=0 while [ "$waited" -lt 600 ]; do if ! kill -0 "$MEASURE_PID" 2>/dev/null; then - fail "measure-sp1.sh da thoat truoc khi in duong dan log, xem $MEASURE_OUT" + fail "measure-sp1.sh đã thoát trước khi in đường dẫn log, xem $MEASURE_OUT" fi ANDROID_LOG="$(grep -m1 '^Android: ' "$MEASURE_OUT" 2>/dev/null | sed 's/^Android: //')" MAC_LOG="$(grep -m1 '^Mac:' "$MEASURE_OUT" 2>/dev/null | sed -E 's/^Mac:[[:space:]]*//')" @@ -194,30 +194,30 @@ while [ "$waited" -lt 600 ]; do sleep 2 waited=$((waited + 2)) done -[ -n "$ANDROID_LOG" ] && [ -n "$MAC_LOG" ] || fail "qua 600s khong thay measure-sp1.sh in duong dan log, xem $MEASURE_OUT" +[ -n "$ANDROID_LOG" ] && [ -n "$MAC_LOG" ] || fail "quá 600s không thấy measure-sp1.sh in đường dẫn log, xem $MEASURE_OUT" echo "Android log: $ANDROID_LOG" echo "Mac log: $MAC_LOG" [ -n "$MAC_APP_PATH" ] && echo "App macOS: $MAC_APP_PATH" -echo "== 5. Cho link len Bluetooth (toi da 120s) ==" -wait_for_connected 120 || fail "qua 120s khong thay 'to=Connected channel=bluetooth' trong $MAC_LOG" -echo "Da thay Bluetooth Connected." +echo "== 5. Chờ link lên Bluetooth (tối đa 120s) ==" +wait_for_connected 120 || fail "quá 120s không thấy 'to=Connected channel=bluetooth' trong $MAC_LOG" +echo "Đã thấy Bluetooth Connected." case "$SESSION" in 1) - echo "== 6. AC1 idle: 30 lan copy text, doi chieu, cach 3-4s ==" + echo "== 6. AC1 idle: 30 lần copy text, đối chiều, cách 3-4s ==" ANDROID_COPY_OK=0 if adb "${ADB_ARGS[@]}" shell pm list packages ca.zgrs.clipper 2>/dev/null | grep -q 'package:ca\.zgrs\.clipper'; then CLIPPER_INSTALLED=1 ANDROID_COPY_OK=1 else - echo "CANH BAO: khong thay app Clipper (ca.zgrs.clipper) tren S25 — khong co cach set clipboard Android" \ - "tu adb ma khong co UI, bo qua chieu S25->Mac, chi copy chieu Mac->S25 ca 30 lan." >&2 + echo "CẢNH BÁO: không thấy app Clipper (ca.zgrs.clipper) trên S25 — không có cách set clipboard Android" \ + "từ adb mà không có UI, bỏ qua chiều S25->Mac, chỉ copy chiều Mac->S25 cả 30 lần." >&2 fi for (( i = 1; i <= 30; i++ )); do if [ "$ANDROID_COPY_OK" -eq 1 ] && (( i % 2 == 0 )); then if ! android_copy_text "sp1 auto s25->mac $i $(epoch_ms)"; then - echo "CANH BAO: lan $i copy tu S25 that bai, bo qua lan nay" >&2 + echo "CẢNH BÁO: lần $i copy từ S25 thất bại, bỏ qua lần này" >&2 fi else print -n -- "sp1 auto mac->s25 $i $(epoch_ms)" | pbcopy @@ -225,49 +225,49 @@ case "$SESSION" in sleep $(( 3 + (RANDOM % 2) )) done - echo "== 7. 5 vong tat/bat Bluetooth tren S25, moi vong cho Connected lai (toi da 30s) ==" + echo "== 7. 5 vòng tắt/bật Bluetooth trên S25, mỗi vòng chờ Connected lại (tối đa 30s) ==" for (( round = 1; round <= 5; round++ )); do - echo "-- vong $round/5: tat bluetooth_manager --" + echo "-- vòng $round/5: tắt bluetooth_manager --" adb "${ADB_ARGS[@]}" shell cmd bluetooth_manager disable || true sleep 2 - echo "-- vong $round/5: bat lai bluetooth_manager --" + echo "-- vòng $round/5: bật lại bluetooth_manager --" adb "${ADB_ARGS[@]}" shell cmd bluetooth_manager enable || true if ! wait_for_connected 30; then - echo "CANH BAO: vong $round khong thay Connected lai trong 30s" >&2 + echo "CẢNH BÁO: vòng $round không thấy Connected lại trong 30s" >&2 fi done - echo "== 8. Scan response: khoa man hinh S25, cho 60s, Cmd-Q + mo lai app Mac ==" + echo "== 8. Scan response: khóa màn hình S25, chờ 60s, Cmd-Q + mở lại app Mac ==" BT_ACCEPT_BEFORE="$(grep -c 'ev=bt_accept' "$ANDROID_LOG" 2>/dev/null || true)" : "${BT_ACCEPT_BEFORE:=0}" adb "${ADB_ARGS[@]}" shell input keyevent KEYCODE_SLEEP || true sleep 60 - osascript -e 'quit app "HandLive"' || echo "CANH BAO: osascript quit app HandLive that bai" >&2 + osascript -e 'quit app "HandLive"' || echo "CẢNH BÁO: osascript quit app HandLive thất bại" >&2 if [ -n "$MAC_APP_PATH" ]; then - open "$MAC_APP_PATH" || echo "CANH BAO: mo lai app Mac that bai" >&2 + open "$MAC_APP_PATH" || echo "CẢNH BÁO: mở lại app Mac thất bại" >&2 else - echo "CANH BAO: khong doc duoc duong dan app macOS tu $MEASURE_OUT, bo qua mo lai app" >&2 + echo "CẢNH BÁO: không đọc được đường dẫn app macOS từ $MEASURE_OUT, bỏ qua mở lại app" >&2 fi sleep 5 adb "${ADB_ARGS[@]}" shell input keyevent KEYCODE_WAKEUP || true - wait_for_connected 30 || echo "CANH BAO: sau buoc scan-response khong thay Connected lai trong 30s" >&2 + wait_for_connected 30 || echo "CẢNH BÁO: sau bước scan-response không thấy Connected lại trong 30s" >&2 BT_ACCEPT_AFTER="$(grep -c 'ev=bt_accept' "$ANDROID_LOG" 2>/dev/null || true)" : "${BT_ACCEPT_AFTER:=0}" - echo "bt_accept truoc=$BT_ACCEPT_BEFORE sau=$BT_ACCEPT_AFTER" + echo "bt_accept trước=$BT_ACCEPT_BEFORE sau=$BT_ACCEPT_AFTER" if [ "$BT_ACCEPT_AFTER" -le "$BT_ACCEPT_BEFORE" ]; then - echo "CANH BAO: bt_accept khong tang sau buoc scan-response (xem 00-common-specs.md 0.4.5)" >&2 + echo "CẢNH BÁO: bt_accept không tăng sau bước scan-response (xem 00-common-specs.md 0.4.5)" >&2 fi ;; 2) - echo "== 6. AC1 duoi tai: anh ~5MB len pasteboard Mac, xen 10 lan copy text ==" + echo "== 6. AC1 dưới tải: ảnh ~5MB lên pasteboard Mac, xen 10 lần copy text ==" IMG_BMP="$TMPDIR/handlive-sp1-bench-$TS.bmp" IMG_PNG="$TMPDIR/handlive-sp1-bench-$TS.png" - make_bench_image "$IMG_BMP" "$IMG_PNG" || fail "tao anh bench that bai" + make_bench_image "$IMG_BMP" "$IMG_PNG" || fail "tạo ảnh bench thất bại" rm -f "$IMG_BMP" for (( i = 1; i <= 10; i++ )); do if (( i == 1 || i % 3 == 0 )); then - set_mac_image_clipboard "$IMG_PNG" || echo "CANH BAO: set anh len pasteboard Mac that bai (lan $i)" >&2 + set_mac_image_clipboard "$IMG_PNG" || echo "CẢNH BÁO: set ảnh lên pasteboard Mac thất bại (lần $i)" >&2 sleep 1 fi print -n -- "sp1 auto load mac->s25 $i $(epoch_ms)" | pbcopy @@ -277,25 +277,25 @@ case "$SESSION" in ;; 3) - echo "== 6. Doze: ep S25 vao Doze ==" + echo "== 6. Doze: ép S25 vào Doze ==" adb "${ADB_ARGS[@]}" shell dumpsys battery unplug || true adb "${ADB_ARGS[@]}" shell input keyevent KEYCODE_SLEEP || true adb "${ADB_ARGS[@]}" shell dumpsys deviceidle force-idle || true - echo "== 7. 30 lan copy text Mac->S25, cach 30s, man hinh S25 khong dong vao ==" + echo "== 7. 30 lần copy text Mac->S25, cách 30s, màn hình S25 không đụng vào ==" for (( i = 1; i <= 30; i++ )); do print -n -- "sp1 auto doze mac->s25 $i $(epoch_ms)" | pbcopy sleep 30 done - echo "== 8. Khoi phuc S25 sau Doze ==" + echo "== 8. Khôi phục S25 sau Doze ==" adb "${ADB_ARGS[@]}" shell dumpsys deviceidle unforce || true adb "${ADB_ARGS[@]}" shell dumpsys battery reset || true adb "${ADB_ARGS[@]}" shell input keyevent KEYCODE_WAKEUP || true ;; esac -echo "== 9. Dung ghi log ==" +echo "== 9. Dừng ghi log ==" kill -INT "$MEASURE_PID" 2>/dev/null || true wait "$MEASURE_PID" 2>/dev/null || true LOGGING_STOPPED=1 @@ -303,14 +303,14 @@ LOGGING_STOPPED=1 echo "Android log: $ANDROID_LOG" echo "Mac log: $MAC_LOG" -command -v python3 >/dev/null || fail "khong thay python3 trong PATH" +command -v python3 >/dev/null || fail "không thấy python3 trong PATH" echo "== 10. clip_latency.py (AC1) ==" python3 "$SHARED_DIR/tools/bench/clip_latency.py" "$ANDROID_LOG" "$MAC_LOG" --json \ - || echo "CANH BAO: clip_latency.py tra ma loi khac 0" >&2 + || echo "CẢNH BÁO: clip_latency.py trả mã lỗi khác 0" >&2 echo "== 11. reconnect_time.py (AC2) ==" python3 "$SHARED_DIR/tools/bench/reconnect_time.py" "$MAC_LOG" "$ANDROID_LOG" --json \ - || echo "CANH BAO: reconnect_time.py tra ma loi khac 0" >&2 + || echo "CẢNH BÁO: reconnect_time.py trả mã lỗi khác 0" >&2 echo "Xong session $SESSION." diff --git a/tools/measure-sp1-prep.sh b/tools/measure-sp1-prep.sh index 952d25d..589649b 100755 --- a/tools/measure-sp1-prep.sh +++ b/tools/measure-sp1-prep.sh @@ -1,14 +1,14 @@ #!/bin/zsh -# Chuan bi ghep doi SP1: go sach ban cu, build va cai ban debug len S25 va Mac, -# tat Wi-Fi, ghep qua Bluetooth. -# Chay NGOAI sandbox qua nut run cua sep (S25 cam qua USB): +# Chuẩn bị ghép đôi SP1: gỡ sạch bản cũ, build và cài bản debug lên S25 và Mac, +# tắt Wi-Fi, ghép qua Bluetooth. +# Chạy NGOÀI sandbox qua nút run của sếp (S25 cắm qua USB): # zsh handlive/tools/measure-sp1-prep.sh # -# Sau script thoat thanh cong, sxep tat Wi-Fi ca hai may, bat Bluetooth, roi ghep doi S25 voi Mac 1 lan (quét QR hoac nhap PIN, ~1 phut), -# roi team tu chay phien 1 do qua measure-sp1-auto.sh. +# Sau script thoát thành công, sếp tắt Wi-Fi cả hai máy, bật Bluetooth, rồi ghép đôi S25 với Mac 1 lần (quét QR hoặc nhập PIN, ~1 phút), +# rồi team tự chạy phiên 1 đo qua measure-sp1-auto.sh. # -# Go sach app + du lieu Android cu va ban HandLive.app cu tren Mac truoc khi cai, -# de dam bao cai sach. Khong tac dong Wi-Fi, khong ghi log. +# Gỡ sạch app + dữ liệu Android cũ và bản HandLive.app cũ trên Mac trước khi cài, +# để đảm bảo cài sạch. Không tác động Wi-Fi, không ghi log. set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -21,20 +21,20 @@ APP_ID="com.handlive.android" MAIN_ACTIVITY="$APP_ID/app.handlive.android.MainActivity" fail() { - echo "LOI: $1" >&2 + echo "LỖI: $1" >&2 exit 1 } -command -v adb >/dev/null || fail "khong thay adb trong PATH" -command -v xcodebuild >/dev/null || fail "khong thay xcodebuild trong PATH" -adb get-state >/dev/null 2>&1 || fail "khong thay thiet bi Android qua adb (cam S25, bat USB debugging)" +command -v adb >/dev/null || fail "không thấy adb trong PATH" +command -v xcodebuild >/dev/null || fail "không thấy xcodebuild trong PATH" +adb get-state >/dev/null 2>&1 || fail "không thấy thiết bị Android qua adb (cắm S25, bật USB debugging)" -echo "== 1. Go sach ban Android cu (app + du lieu) ==" +echo "== 1. Gỡ sạch bản Android cũ (app + dữ liệu) ==" if adb shell pm list packages "$APP_ID" 2>/dev/null | grep -q "^package:$APP_ID$"; then - adb uninstall "$APP_ID" || fail "adb uninstall $APP_ID that bai" - echo "Da go $APP_ID (ca du lieu) tren S25" + adb uninstall "$APP_ID" || fail "adb uninstall $APP_ID thất bại" + echo "Đã gỡ $APP_ID (cả dữ liệu) trên S25" else - echo "$APP_ID chua cai tren S25, bo qua go" + echo "$APP_ID chưa cài trên S25, bỏ qua gỡ" fi echo "== 2. Build APK debug (flavor foss) ==" @@ -43,51 +43,51 @@ echo "== 2. Build APK debug (flavor foss) ==" export JAVA_HOME ANDROID_HOME GRADLEW="$ANDROID_DIR/gradlew" -[ -f "$GRADLEW" ] || fail "khong thay $GRADLEW" +[ -f "$GRADLEW" ] || fail "không thấy $GRADLEW" env JAVA_HOME="$JAVA_HOME" ANDROID_HOME="$ANDROID_HOME" "$GRADLEW" -p "$ANDROID_DIR" ":app:assembleFossDebug" \ - || fail "gradlew :app:assembleFossDebug that bai" + || fail "gradlew :app:assembleFossDebug thất bại" APK_PATH="$(find "$ANDROID_DIR/app/build/outputs/apk/foss/debug" -name '*.apk' -print -quit 2>/dev/null)" -[ -n "$APK_PATH" ] || fail "khong tim thay APK debug duoi $ANDROID_DIR/app/build/outputs/apk/foss/debug" +[ -n "$APK_PATH" ] || fail "không tìm thấy APK debug dưới $ANDROID_DIR/app/build/outputs/apk/foss/debug" echo "APK: $APK_PATH" -echo "== 3. Cai APK len S25 ==" -adb install "$APK_PATH" || fail "adb install that bai" +echo "== 3. Cài APK lên S25 ==" +adb install "$APK_PATH" || fail "adb install thất bại" -echo "== 4. Cap quyen Bluetooth ==" +echo "== 4. Cấp quyền Bluetooth ==" adb shell pm grant "$APP_ID" android.permission.BLUETOOTH_ADVERTISE >/dev/null 2>&1 || true adb shell pm grant "$APP_ID" android.permission.BLUETOOTH_CONNECT >/dev/null 2>&1 || true -echo "== 5. Mo app tren S25 ==" -adb shell am start -n "$MAIN_ACTIVITY" || fail "adb shell am start that bai" +echo "== 5. Mở app trên S25 ==" +adb shell am start -n "$MAIN_ACTIVITY" || fail "adb shell am start thất bại" echo -echo "== 6. Go sach ban Mac cu ==" +echo "== 6. Gỡ sạch bản Mac cũ ==" pkill -x HandLive >/dev/null 2>&1 || true -echo "Da dung HandLive dang chay (neu co)" +echo "Đã dừng HandLive đang chạy (nếu có)" MAC_APP_SYSTEM="/Applications/HandLive.app" MAC_APP_USER="$HOME/Applications/HandLive.app" if [ -d "$MAC_APP_SYSTEM" ]; then - rm -rf "$MAC_APP_SYSTEM" || fail "khong xoa duoc $MAC_APP_SYSTEM" - echo "Da xoa $MAC_APP_SYSTEM" + rm -rf "$MAC_APP_SYSTEM" || fail "không xóa được $MAC_APP_SYSTEM" + echo "Đã xóa $MAC_APP_SYSTEM" fi if [ -d "$MAC_APP_USER" ]; then - rm -rf "$MAC_APP_USER" || fail "khong xoa duoc $MAC_APP_USER" - echo "Da xoa $MAC_APP_USER" + rm -rf "$MAC_APP_USER" || fail "không xóa được $MAC_APP_USER" + echo "Đã xóa $MAC_APP_USER" fi DERIVED_DATA="$APPLE_DIR/.build-logs/sp1-derived-data" if [ -d "$DERIVED_DATA" ]; then - rm -rf "$DERIVED_DATA" || fail "khong xoa duoc $DERIVED_DATA" - echo "Da xoa DerivedData cu $DERIVED_DATA" + rm -rf "$DERIVED_DATA" || fail "không xóa được $DERIVED_DATA" + echo "Đã xóa DerivedData cũ $DERIVED_DATA" fi echo echo "== 7. Build app macOS (debug) ==" -command -v xcodegen >/dev/null || fail "khong thay xcodegen trong PATH (brew install xcodegen)" -(cd "$APPLE_DIR" && xcodegen generate) || fail "xcodegen generate that bai" +command -v xcodegen >/dev/null || fail "không thấy xcodegen trong PATH (brew install xcodegen)" +(cd "$APPLE_DIR" && xcodegen generate) || fail "xcodegen generate thất bại" mkdir -p "$APPLE_DIR/.build-logs" xcodebuild build \ @@ -97,17 +97,17 @@ xcodebuild build \ -destination "platform=macOS" \ -derivedDataPath "$DERIVED_DATA" \ CODE_SIGNING_ALLOWED=NO \ - || fail "xcodebuild build HandLive (macOS) that bai" + || fail "xcodebuild build HandLive (macOS) thất bại" MAC_APP_PATH="$(find "$DERIVED_DATA/Build/Products/Debug" -maxdepth 1 -name 'HandLive.app' -print -quit 2>/dev/null)" -[ -n "$MAC_APP_PATH" ] || fail "khong tim thay HandLive.app duoi $DERIVED_DATA/Build/Products/Debug" +[ -n "$MAC_APP_PATH" ] || fail "không tìm thấy HandLive.app dưới $DERIVED_DATA/Build/Products/Debug" echo "App macOS: $MAC_APP_PATH" -echo "== 8. Ky ad-hoc de khong can provisioning profile ==" -codesign --force --deep --sign - "$MAC_APP_PATH" || fail "codesign ad-hoc that bai" +echo "== 8. Ký ad-hoc để không cần provisioning profile ==" +codesign --force --deep --sign - "$MAC_APP_PATH" || fail "codesign ad-hoc thất bại" -echo "== 9. Mo app tren Mac ==" -open "$MAC_APP_PATH" || fail "mo HandLive.app that bai (sandbox chan 'open'; chay script nay ngoai sandbox)" +echo "== 9. Mở app trên Mac ==" +open "$MAC_APP_PATH" || fail "mở HandLive.app thất bại (sandbox chặn 'open'; chạy script này ngoài sandbox)" echo -echo "SAN SANG: tat Wi-Fi ca hai may, bat Bluetooth, mo man ghep doi tren Mac roi quet QR (hoac nhap PIN) tren S25 — ghep qua Bluetooth" +echo "SẴN SÀNG: tắt Wi-Fi cả hai máy, bật Bluetooth, mở màn ghép đôi trên Mac rồi quét QR (hoặc nhập PIN) trên S25 — ghép qua Bluetooth" diff --git a/tools/measure-sp1.sh b/tools/measure-sp1.sh index 1403d18..f21d28a 100755 --- a/tools/measure-sp1.sh +++ b/tools/measure-sp1.sh @@ -1,22 +1,22 @@ #!/bin/zsh -# Chay NGOAI sandbox, voi S25 cam qua USB (adb) va may Mac nay la may do: +# Chạy NGOÀI sandbox, với S25 cắm qua USB (adb) và máy Mac này là máy đo: # zsh handlive/tools/measure-sp1.sh [--log-only] [adb_serial] # -# Chuan bi SP1 (kenh dieu khien Bluetooth) de do AC1/AC2 tren may that, theo +# Chuẩn bị SP1 (kênh điều khiển Bluetooth) để đo AC1/AC2 trên máy thật, theo # handlive/plans/20260925-implementation/reports/phase-07-sp1-measure.md: -# 1. Build APK debug (:app:assembleDebug), cai len S25 qua adb, cap quyen -# BLUETOOTH_ADVERTISE/BLUETOOTH_CONNECT (chua co man xin quyen), mo app. -# 2. Build app macOS (xcodegen generate + xcodebuild, scheme HandLive, cau hinh Debug) va mo app. -# 3. Goi logcat (Android) va `log stream` (Mac) loc dong HLBENCH vao -# handlive/.build-logs/sp1--android.log va sp1--mac.log. +# 1. Build APK debug (:app:assembleDebug), cài lên S25 qua adb, cấp quyền +# BLUETOOTH_ADVERTISE/BLUETOOTH_CONNECT (chưa có màn xin quyền), mở app. +# 2. Build app macOS (xcodegen generate + xcodebuild, scheme HandLive, cấu hình Debug) và mở app. +# 3. Gọi logcat (Android) và `log stream` (Mac) lọc dòng HLBENCH vào +# handlive/.build-logs/sp1--android.log và sp1--mac.log. # -# --log-only: bo qua build+cai APK va bo qua xcodegen+xcodebuild; dung lai APK/app macOS cua lan -# chay full gan nhat (danh cho phien 2/3 cua runbook, do lai nhanh khong phai build lai). Van cap -# quyen Bluetooth (idempotent) va mo lai app o hai may. That bai ngay voi thong bao ro neu chua -# chay full it nhat mot lan (chua co .app macOS de mo). +# --log-only: bỏ qua build+cài APK và bỏ qua xcodegen+xcodebuild; dùng lại APK/app macOS của lần +# chạy full gần nhất (dành cho phiên 2/3 của runbook, đo lại nhanh không phải build lại). Vẫn cấp +# quyền Bluetooth (idempotent) và mở lại app ở hai máy. Thất bại ngay với thông báo rõ nếu chưa +# chạy full ít nhất một lần (chưa có .app macOS để mở). # -# Dung tay: lam cac buoc do trong runbook (phase-07-sp1-measure.md), roi Ctrl-C de dung ghi log. -# Khong dung mang ngoai; chi dung JAVA_HOME/ANDROID_HOME nhu verify-phase7.sh. +# Dùng tay: làm các bước đo trong runbook (phase-07-sp1-measure.md), rồi Ctrl-C để dừng ghi log. +# Không dùng mạng ngoài; chỉ dùng JAVA_HOME/ANDROID_HOME như verify-phase7.sh. set -uo pipefail LOG_ONLY=0 @@ -24,7 +24,7 @@ SERIAL="" for arg in "$@"; do case "$arg" in --log-only) LOG_ONLY=1 ;; - -*) echo "LOI: khong biet tuy chon: $arg" >&2; exit 2 ;; + -*) echo "LỖI: không biết tùy chọn: $arg" >&2; exit 2 ;; *) SERIAL="$arg" ;; esac done @@ -49,52 +49,52 @@ if [ -n "$SERIAL" ]; then fi fail() { - echo "LOI: $1" >&2 + echo "LỖI: $1" >&2 exit 1 } -command -v adb >/dev/null || fail "khong thay adb (Android platform-tools) trong PATH" -command -v xcodebuild >/dev/null || fail "khong thay xcodebuild trong PATH" +command -v adb >/dev/null || fail "không thấy adb (Android platform-tools) trong PATH" +command -v xcodebuild >/dev/null || fail "không thấy xcodebuild trong PATH" -adb "${ADB_ARGS[@]}" get-state >/dev/null 2>&1 || fail "khong thay thiet bi Android qua adb (cam S25, bat USB debugging)" +adb "${ADB_ARGS[@]}" get-state >/dev/null 2>&1 || fail "không thấy thiết bị Android qua adb (cắm S25, bật USB debugging)" if [ "$LOG_ONLY" -eq 0 ]; then - echo "== 1. Build + cai APK debug len S25 ==" + echo "== 1. Build + cài APK debug lên S25 ==" : "${JAVA_HOME:=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home}" : "${ANDROID_HOME:=/opt/homebrew/share/android-commandlinetools}" export JAVA_HOME ANDROID_HOME GRADLEW="$ANDROID_DIR/gradlew" - [ -f "$GRADLEW" ] || fail "khong thay $GRADLEW" + [ -f "$GRADLEW" ] || fail "không thấy $GRADLEW" env JAVA_HOME="$JAVA_HOME" ANDROID_HOME="$ANDROID_HOME" "$GRADLEW" -p "$ANDROID_DIR" ":app:assembleFossDebug" \ - || fail "gradlew :app:assembleFossDebug that bai" + || fail "gradlew :app:assembleFossDebug thất bại" APK_PATH="$(find "$ANDROID_DIR/app/build/outputs/apk/foss/debug" -name '*.apk' -print -quit 2>/dev/null)" - [ -n "$APK_PATH" ] || fail "khong tim thay APK debug duoi $ANDROID_DIR/app/build/outputs/apk/foss/debug" + [ -n "$APK_PATH" ] || fail "không tìm thấy APK debug dưới $ANDROID_DIR/app/build/outputs/apk/foss/debug" echo "APK: $APK_PATH" - adb "${ADB_ARGS[@]}" install -r "$APK_PATH" || fail "adb install that bai" + adb "${ADB_ARGS[@]}" install -r "$APK_PATH" || fail "adb install thất bại" else - echo "== 1. --log-only: bo qua build + cai APK (dung lai ban da cai tu lan chay full) ==" + echo "== 1. --log-only: bỏ qua build + cài APK (dùng lại bản đã cài từ lần chạy full) ==" fi -echo "== 2. Cap quyen Bluetooth (chua co man xin quyen trong app) ==" +echo "== 2. Cấp quyền Bluetooth (chưa có màn xin quyền trong app) ==" adb "${ADB_ARGS[@]}" shell pm grant "$APP_ID" android.permission.BLUETOOTH_ADVERTISE \ - || echo " CANH BAO: pm grant BLUETOOTH_ADVERTISE that bai (co the da cap san)" + || echo " CẢNH BÁO: pm grant BLUETOOTH_ADVERTISE thất bại (có thể đã cấp sẵn)" adb "${ADB_ARGS[@]}" shell pm grant "$APP_ID" android.permission.BLUETOOTH_CONNECT \ - || echo " CANH BAO: pm grant BLUETOOTH_CONNECT that bai (co the da cap san)" + || echo " CẢNH BÁO: pm grant BLUETOOTH_CONNECT thất bại (có thể đã cấp sẵn)" -echo "== 3. Mo app tren S25 ==" -adb "${ADB_ARGS[@]}" shell am start -n "$MAIN_ACTIVITY" || fail "adb shell am start that bai" +echo "== 3. Mở app trên S25 ==" +adb "${ADB_ARGS[@]}" shell am start -n "$MAIN_ACTIVITY" || fail "adb shell am start thất bại" DERIVED_DATA="$APPLE_DIR/.build-logs/sp1-derived-data" echo if [ "$LOG_ONLY" -eq 0 ]; then echo "== 4. Build app macOS (debug) ==" - command -v xcodegen >/dev/null || fail "khong thay xcodegen trong PATH (brew install xcodegen)" - (cd "$APPLE_DIR" && xcodegen generate) || fail "xcodegen generate that bai" + command -v xcodegen >/dev/null || fail "không thấy xcodegen trong PATH (brew install xcodegen)" + (cd "$APPLE_DIR" && xcodegen generate) || fail "xcodegen generate thất bại" mkdir -p "$APPLE_DIR/.build-logs" xcodebuild build \ @@ -104,28 +104,28 @@ if [ "$LOG_ONLY" -eq 0 ]; then -destination "platform=macOS" \ -derivedDataPath "$DERIVED_DATA" \ CODE_SIGNING_ALLOWED=NO \ - || fail "xcodebuild build HandLive (macOS) that bai" + || fail "xcodebuild build HandLive (macOS) thất bại" else - echo "== 4. --log-only: bo qua xcodegen + xcodebuild (dung lai ban da build tu lan chay full) ==" + echo "== 4. --log-only: bỏ qua xcodegen + xcodebuild (dùng lại bản đã build từ lần chạy full) ==" fi MAC_APP_PATH="$(find "$DERIVED_DATA/Build/Products/Debug" -maxdepth 1 -name 'HandLive.app' -print -quit 2>/dev/null)" if [ -z "$MAC_APP_PATH" ]; then if [ "$LOG_ONLY" -eq 1 ]; then - fail "khong thay HandLive.app duoi $DERIVED_DATA/Build/Products/Debug — chay mot lan khong co --log-only truoc" + fail "không thấy HandLive.app dưới $DERIVED_DATA/Build/Products/Debug — chạy một lần không có --log-only trước" fi - fail "khong tim thay HandLive.app duoi $DERIVED_DATA/Build/Products/Debug" + fail "không tìm thấy HandLive.app dưới $DERIVED_DATA/Build/Products/Debug" fi echo "App macOS: $MAC_APP_PATH" -echo "Ky ad-hoc de khong can provisioning profile" -codesign --force --deep --sign - "$MAC_APP_PATH" || fail "codesign ad-hoc that bai" +echo "Ký ad-hoc để không cần provisioning profile" +codesign --force --deep --sign - "$MAC_APP_PATH" || fail "codesign ad-hoc thất bại" -echo "== 5. Mo app tren Mac ==" -open "$MAC_APP_PATH" || fail "mo HandLive.app that bai (sandbox chan 'open'; chay script nay ngoai sandbox)" +echo "== 5. Mở app trên Mac ==" +open "$MAC_APP_PATH" || fail "mở HandLive.app thất bại (sandbox chặn 'open'; chạy script này ngoài sandbox)" echo -echo "== 6. Ghi log HLBENCH (Ctrl-C de dung) ==" +echo "== 6. Ghi log HLBENCH (Ctrl-C để dừng) ==" echo "Android: $ANDROID_LOG" echo "Mac: $MAC_LOG" @@ -140,12 +140,12 @@ stop() { kill "$android_pid" "$mac_pid" 2>/dev/null || true wait 2>/dev/null || true echo - echo "Da dung ghi log." - echo " Android: $(grep -c 'HLBENCH/1 ' "$ANDROID_LOG" 2>/dev/null || echo 0) dong, tai $ANDROID_LOG" - echo " Mac: $(grep -c 'HLBENCH/1 ' "$MAC_LOG" 2>/dev/null || echo 0) dong, tai $MAC_LOG" - echo "Doc ket qua theo handlive/plans/20260925-implementation/reports/phase-07-sp1-measure.md" + echo "Đã dừng ghi log." + echo " Android: $(grep -c 'HLBENCH/1 ' "$ANDROID_LOG" 2>/dev/null || echo 0) dòng, tại $ANDROID_LOG" + echo " Mac: $(grep -c 'HLBENCH/1 ' "$MAC_LOG" 2>/dev/null || echo 0) dòng, tại $MAC_LOG" + echo "Đọc kết quả theo handlive/plans/20260925-implementation/reports/phase-07-sp1-measure.md" } trap stop INT TERM -echo "Lam cac buoc do trong runbook (phase-07-sp1-measure.md), roi an Ctrl-C de dung ghi log." +echo "Làm các bước đo trong runbook (phase-07-sp1-measure.md), rồi ấn Ctrl-C để dừng ghi log." wait diff --git a/tools/release-push-website.sh b/tools/release-push-website.sh new file mode 100644 index 0000000..3d0e37f --- /dev/null +++ b/tools/release-push-website.sh @@ -0,0 +1,140 @@ +#!/bin/zsh +# Đẩy nhánh website và tài liệu v0.2.0 lên origin và mở PR. +# Chạy NGOÀI sandbox qua nút run của sếp: +# zsh handlive/tools/release-push-website.sh # chạy thực +# zsh handlive/tools/release-push-website.sh --dry-run # chỉ in lệnh, không push/tạo PR +# +# Với từng repo hub và .github-org (theo thứ tự đó): +# Kiểm nhánh main, fetch origin main, so ahead (0 thì báo và dừng), +# ls-remote nhánh feat/docs-v0-2-0 (đã có đúng SHA thì bỏ qua push, khác SHA thì fail), +# push main:feat/docs-v0-2-0 (không -u), gh pr list, có thì in link, chưa có thì create. +# Trước push, kiểm .github-org có remote origin; không có thì dừng. +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +HUB_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +WORKSPACE_ROOT="$(cd "$HUB_ROOT/.." && pwd)" + +RELEASE_BRANCH="feat/docs-v0-2-0" +PR_TITLE="docs: v0.2.0 release links and status" +PR_BODY="Website (#265) và tài liệu (README, CLAUDE.md, roadmap, profile tổ chức) trỏ tới v0.2.0 đã phát hành. Không đổi mã nguồn app, không ảnh hưởng release đã tag." + +# Cập nhật mô tả PR nếu đã mở (dùng bản có dấu từ Bao commit c6ff51c) +update_pr_body() { + local repo_path="$1" + local existing_pr_url="$2" + + if [ -z "$existing_pr_url" ]; then + return 0 + fi + + echo "+ gh pr edit $existing_pr_url --title ... --body ..." + if [ "$DRY_RUN" -eq 1 ]; then + echo "$name: (dry-run) không cập nhật PR" + return 0 + fi + + (cd "$repo_path" && gh pr edit "$existing_pr_url" --title "$PR_TITLE" --body "$PR_BODY") \ + || fail "$name: gh pr edit thất bại" +} + +typeset -a REPO_NAMES +typeset -a REPO_PATHS +REPO_NAMES=(hub github-org) +REPO_PATHS=( + "$HUB_ROOT" + "$WORKSPACE_ROOT/.github-org" +) + +DRY_RUN=0 +if [ "${1:-}" = "--dry-run" ]; then + DRY_RUN=1 +fi + +fail() { + echo "LỖI: $1" >&2 + exit 1 +} + +i=1 +while [ "$i" -le "${#REPO_NAMES[@]}" ]; do + name="${REPO_NAMES[$i]}" + repo_path="${REPO_PATHS[$i]}" + i=$((i + 1)) + + echo + echo "== $name ($repo_path) ==" + [ -d "$repo_path" ] || fail "không thấy thư mục $repo_path" + + (cd "$repo_path" && git rev-parse --is-inside-work-tree >/dev/null 2>&1) \ + || fail "$name: không phải git repo" + + (cd "$repo_path" && git remote get-url origin >/dev/null 2>&1) \ + || fail "$name: không có remote origin" + + current_branch="$(cd "$repo_path" && git branch --show-current)" + [ "$current_branch" = "main" ] || fail "$name: đang ở nhánh '$current_branch', cần 'main'" + + echo "+ git fetch origin main" + (cd "$repo_path" && git fetch origin main) || fail "$name: git fetch thất bại" + + ahead="$(cd "$repo_path" && git rev-list --count origin/main..main 2>/dev/null)" + if [ -z "$ahead" ]; then + fail "$name: không đọc được số commit đi trước origin/main (fetch trước?)" + fi + if [ "$ahead" -eq 0 ]; then + echo "$name: main không đi trước origin/main, bỏ qua" + continue + fi + echo "$name: main đi trước origin/main $ahead commit" + + local_sha="$(cd "$repo_path" && git rev-parse main)" + remote_branch_sha="$(cd "$repo_path" && git ls-remote origin "refs/heads/$RELEASE_BRANCH" | awk '{print $1}')" + + echo "+ git ls-remote origin refs/heads/$RELEASE_BRANCH" + should_push=1 + if [ -n "$remote_branch_sha" ] && [ "$remote_branch_sha" = "$local_sha" ]; then + echo "$name: origin/$RELEASE_BRANCH đã đúng SHA ($local_sha), bỏ qua push" + should_push=0 + elif [ -n "$remote_branch_sha" ]; then + echo "+ git fetch origin $RELEASE_BRANCH" + (cd "$repo_path" && git fetch origin "$RELEASE_BRANCH") \ + || fail "$name: git fetch $RELEASE_BRANCH thất bại" + + (cd "$repo_path" && git merge-base --is-ancestor "$remote_branch_sha" "$local_sha") \ + || fail "$name: origin/$RELEASE_BRANCH ($remote_branch_sha) không phải tổ tiên của main ($local_sha), không tự --force" + echo "$name: origin/$RELEASE_BRANCH ($remote_branch_sha) là tổ tiên của main, đẩy fast-forward" + fi + + if [ "$should_push" -eq 1 ]; then + echo "+ git push origin main:$RELEASE_BRANCH" + if [ "$DRY_RUN" -eq 1 ]; then + echo "$name: (dry-run) không push" + else + (cd "$repo_path" && git push origin "main:$RELEASE_BRANCH") \ + || fail "$name: git push thất bại" + fi + fi + + echo "+ gh pr list --head $RELEASE_BRANCH --state open --json url" + if [ "$DRY_RUN" -eq 1 ]; then + echo "+ gh pr create --base main --head $RELEASE_BRANCH --title ... --body ..." + echo "$name: (dry-run) không tạo PR" + continue + fi + + existing_pr_url="$(cd "$repo_path" && gh pr list --head "$RELEASE_BRANCH" --state open --json url --jq '.[0].url' 2>/dev/null)" + if [ -n "$existing_pr_url" ]; then + echo "$name: PR đã có -> $existing_pr_url" + update_pr_body "$repo_path" "$existing_pr_url" + continue + fi + + pr_url="$(cd "$repo_path" && gh pr create --base main --head "$RELEASE_BRANCH" --title "$PR_TITLE" --body "$PR_BODY")" \ + || fail "$name: gh pr create thất bại" + + echo "$name: PR -> $pr_url" +done + +echo +echo "XONG. Merge PR trên GitHub bằng 'Create a merge commit'." diff --git a/tools/release-push.sh b/tools/release-push.sh index 0a4d2a3..9cd5fee 100755 --- a/tools/release-push.sh +++ b/tools/release-push.sh @@ -1,18 +1,18 @@ #!/bin/zsh -# Day nhanh release len origin va mo PR cho tung repo con. -# Chay NGOAI sandbox qua nut run cua sep: -# zsh handlive/tools/release-push.sh # chay thuc -# zsh handlive/tools/release-push.sh --dry-run # chi in lenh, khong push/tao PR +# Đẩy nhánh release lên origin và mở PR cho từng repo con. +# Chạy NGOÀI sandbox qua nút run của sếp: +# zsh handlive/tools/release-push.sh # chạy thực +# zsh handlive/tools/release-push.sh --dry-run # chỉ in lệnh, không push/tạo PR # -# Voi tung repo shared, android, apple, hub (theo thu tu do): -# - bo qua neu nhanh main local KHONG di truoc origin/main (khong co gi de day) -# - bo qua push neu origin/feat/p07-ghep-bluetooth-offline da co dung SHA cua main local -# - dung va bao loi neu origin/feat/p07-ghep-bluetooth-offline co SHA khac (khong --force) -# - git push origin main:feat/p07-ghep-bluetooth-offline (khong doi upstream cua main local) -# - bo qua tao PR neu da co PR open cho nhanh nay, chi in link +# Với từng repo shared, android, apple, hub (theo thứ tự đó): +# - bỏ qua nếu nhánh main local KHÔNG đi trước origin/main (không có gì để đẩy) +# - bỏ qua push nếu origin/feat/p07-ghep-bluetooth-offline đã có đúng SHA của main local +# - dừng và báo lỗi nếu origin/feat/p07-ghep-bluetooth-offline có SHA khác (không --force) +# - git push origin main:feat/p07-ghep-bluetooth-offline (không đổi upstream của main local) +# - bỏ qua tạo PR nếu đã có PR open cho nhánh này, chỉ in link # - gh pr create --base main --head feat/p07-ghep-bluetooth-offline --title ... --body ... # - in link PR -# Dung ngay khi mot buoc loi (khong tiep tuc repo sau). Chay lai nhieu lan an toan. +# Dừng ngay khi một bước lỗi (không tiếp tục repo sau). Chạy lại nhiều lần an toàn. set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -21,7 +21,7 @@ WORKSPACE_ROOT="$(cd "$HUB_ROOT/.." && pwd)" RELEASE_BRANCH="feat/p07-ghep-bluetooth-offline" PR_TITLE="Phase 7: Bluetooth offline pairing (v0.2.0)" -PR_BODY="Ghep doi Bluetooth lan dau khong can mang (QR luong B, PIN luong C dung CPace-X25519-SHA512), nhan 'Connected via Bluetooth'. Bao cao bao mat PASS o hub reports/security-scan-0.2.0-2026-10-30.md. Merge bang 'Create a merge commit'." +PR_BODY="Ghép đôi Bluetooth lần đầu không cần mạng (QR luồng B, PIN luồng C dùng CPace-X25519-SHA512), nhãn 'Connected via Bluetooth'. Báo cáo bảo mật PASS ở hub reports/security-scan-0.2.0-2026-10-30.md. Merge bằng 'Create a merge commit'." DRY_RUN=0 if [ "${1:-}" = "--dry-run" ]; then @@ -39,7 +39,7 @@ REPO_PATHS=( ) fail() { - echo "LOI: $1" >&2 + echo "LỖI: $1" >&2 exit 1 } @@ -51,63 +51,63 @@ while [ "$i" -le "${#REPO_NAMES[@]}" ]; do echo echo "== $name ($repo_path) ==" - [ -d "$repo_path" ] || fail "khong thay thu muc $repo_path" + [ -d "$repo_path" ] || fail "không thấy thư mục $repo_path" (cd "$repo_path" && git rev-parse --is-inside-work-tree >/dev/null 2>&1) \ - || fail "$name: khong phai git repo" + || fail "$name: không phải git repo" current_branch="$(cd "$repo_path" && git branch --show-current)" - [ "$current_branch" = "main" ] || fail "$name: dang o nhanh '$current_branch', can 'main'" + [ "$current_branch" = "main" ] || fail "$name: đang ở nhánh '$current_branch', cần 'main'" echo "+ git fetch origin main" - (cd "$repo_path" && git fetch origin main) || fail "$name: git fetch that bai" + (cd "$repo_path" && git fetch origin main) || fail "$name: git fetch thất bại" ahead="$(cd "$repo_path" && git rev-list --count origin/main..main 2>/dev/null)" if [ -z "$ahead" ]; then - fail "$name: khong doc duoc so commit di truoc origin/main (fetch truoc?)" + fail "$name: không đọc được số commit đi trước origin/main (fetch trước?)" fi if [ "$ahead" -eq 0 ]; then - echo "$name: main khong di truoc origin/main, bo qua" + echo "$name: main không đi trước origin/main, bỏ qua" continue fi - echo "$name: main di truoc origin/main $ahead commit" + echo "$name: main đi trước origin/main $ahead commit" local_sha="$(cd "$repo_path" && git rev-parse main)" remote_branch_sha="$(cd "$repo_path" && git ls-remote origin "refs/heads/$RELEASE_BRANCH" | awk '{print $1}')" echo "+ git ls-remote origin refs/heads/$RELEASE_BRANCH" if [ -n "$remote_branch_sha" ] && [ "$remote_branch_sha" = "$local_sha" ]; then - echo "$name: origin/$RELEASE_BRANCH da dung SHA ($local_sha), bo qua push" + echo "$name: origin/$RELEASE_BRANCH đã đúng SHA ($local_sha), bỏ qua push" elif [ -n "$remote_branch_sha" ]; then - fail "$name: origin/$RELEASE_BRANCH da ton tai voi SHA khac ($remote_branch_sha != $local_sha), khong tu --force" + fail "$name: origin/$RELEASE_BRANCH đã tồn tại với SHA khác ($remote_branch_sha != $local_sha), không tự --force" else echo "+ git push origin main:$RELEASE_BRANCH" if [ "$DRY_RUN" -eq 1 ]; then - echo "$name: (dry-run) khong push" + echo "$name: (dry-run) không push" else (cd "$repo_path" && git push origin "main:$RELEASE_BRANCH") \ - || fail "$name: git push that bai" + || fail "$name: git push thất bại" fi fi echo "+ gh pr list --head $RELEASE_BRANCH --state open --json url" if [ "$DRY_RUN" -eq 1 ]; then echo "+ gh pr create --base main --head $RELEASE_BRANCH --title ... --body ..." - echo "$name: (dry-run) khong tao PR" + echo "$name: (dry-run) không tạo PR" continue fi existing_pr_url="$(cd "$repo_path" && gh pr list --head "$RELEASE_BRANCH" --state open --json url --jq '.[0].url' 2>/dev/null)" if [ -n "$existing_pr_url" ]; then - echo "$name: PR da co -> $existing_pr_url" + echo "$name: PR đã có -> $existing_pr_url" continue fi pr_url="$(cd "$repo_path" && gh pr create --base main --head "$RELEASE_BRANCH" --title "$PR_TITLE" --body "$PR_BODY")" \ - || fail "$name: gh pr create that bai" + || fail "$name: gh pr create thất bại" echo "$name: PR -> $pr_url" done echo -echo "XONG. Merge tung PR tren GitHub bang 'Create a merge commit', roi chay: zsh handlive/tools/release-tag.sh " +echo "XONG. Merge từng PR trên GitHub bằng 'Create a merge commit', rồi chạy: zsh handlive/tools/release-tag.sh " diff --git a/tools/release-tag.sh b/tools/release-tag.sh index 79b9a78..a9b39a3 100755 --- a/tools/release-tag.sh +++ b/tools/release-tag.sh @@ -1,24 +1,24 @@ #!/bin/zsh -# Gan tag release va day len tung repo, sau khi PR cua release-push.sh da merge vao main. -# Chay NGOAI sandbox qua nut run cua sep: +# Gắn tag release và đẩy lên từng repo, sau khi PR của release-push.sh đã merge vào main. +# Chạy NGOÀI sandbox qua nút run của sếp: # zsh handlive/tools/release-tag.sh v0.2.0 # -# Voi tag (dang vN.N.N): -# 1. git fetch origin cho tung repo -# 2. neu main local khac origin/main: noi dung cay (tree) cua hai ben phai giong het nhau, -# khac la dung (origin/main co thu chua kiem, hoac main local co commit chua vao PR). -# Cay giong nhau thi dua main local ve origin/main: -# - main local la ancestor (merge commit): git merge --ff-only origin/main -# - lich su bi viet lai (rebase/squash, SHA moi): git reset --soft origin/main -# (cay giong het nen index va working tree khong doi; commit cu van con trong reflog) -# 3. kiem tag chua ton tai o local va tren origin (git ls-remote) -# Buoc 1-3 (Pha 1) chay du 4 repo truoc; repo nao loi thi dung, KHONG repo nao duoc gan tag. -# 4. (Pha 2) chi khi ca 4 repo qua Pha 1: git tag roi git push, thu tu hub -> shared -> android -> apple -# Truoc Pha 1: phan loi cua tag phai khop versionName (Android) va MARKETING_VERSION (Apple, ca 3 target). +# Với tag (dạng vN.N.N): +# 1. git fetch origin cho từng repo +# 2. nếu main local khác origin/main: nội dung cây (tree) của hai bên phải giống hệt nhau, +# khác là dừng (origin/main có thứ chưa kiểm, hoặc main local có commit chưa vào PR). +# Cây giống nhau thì đưa main local về origin/main: +# - main local là ancestor (merge commit): git merge --ff-only origin/main +# - lịch sử bị viết lại (rebase/squash, SHA mới): git reset --soft origin/main +# (cây giống hệt nên index và working tree không đổi; commit cũ vẫn còn trong reflog) +# 3. kiểm tag chưa tồn tại ở local và trên origin (git ls-remote) +# Bước 1-3 (Pha 1) chạy đủ 4 repo trước; repo nào lỗi thì dừng, KHÔNG repo nào được gắn tag. +# 4. (Pha 2) chỉ khi cả 4 repo qua Pha 1: git tag rồi git push, thứ tự hub -> shared -> android -> apple +# Trước Pha 1: phần lõi của tag phải khớp versionName (Android) và MARKETING_VERSION (Apple, cả 3 target). # -# Merge PR tren GitHub nen dung "Create a merge commit" (giu nguyen SHA cua commit da kiem). -# "Rebase and merge" va "Squash and merge" deu tao SHA moi; script van chay duoc nho buoc so -# cay o tren, nhung commit tren GitHub se khong trung SHA voi commit da review. +# Merge PR trên GitHub nên dùng "Create a merge commit" (giữ nguyên SHA của commit đã kiểm). +# "Rebase and merge" và "Squash and merge" đều tạo SHA mới; script vẫn chạy được nhờ bước so +# cây ở trên, nhưng commit trên GitHub sẽ không trùng SHA với commit đã review. set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -32,51 +32,51 @@ ANDROID_GRADLE="$ANDROID_DIR/app/build.gradle.kts" APPLE_PROJECT_YML="$APPLE_DIR/project.yml" fail() { - echo "LOI: $1" >&2 + echo "LỖI: $1" >&2 exit 1 } TAG="${1:-}" -[ -n "$TAG" ] || fail "thieu tham so tag, dung: zsh release-tag.sh (vi du v0.2.0)" +[ -n "$TAG" ] || fail "thiếu tham số tag, dùng: zsh release-tag.sh (ví dụ v0.2.0)" case "$TAG" in v*) TAG_CORE="${TAG#v}" ;; - *) fail "tag '$TAG' phai bat dau bang 'v' (vi du v0.2.0)" ;; + *) fail "tag '$TAG' phải bắt đầu bằng 'v' (ví dụ v0.2.0)" ;; esac -echo "== Doc version hien tai ==" -[ -f "$ANDROID_GRADLE" ] || fail "khong thay $ANDROID_GRADLE" +echo "== Đọc version hiện tại ==" +[ -f "$ANDROID_GRADLE" ] || fail "không thấy $ANDROID_GRADLE" VERSION_NAME="$(grep -E '^\s*versionName\s*=' "$ANDROID_GRADLE" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" -[ -n "$VERSION_NAME" ] || fail "khong doc duoc versionName tu $ANDROID_GRADLE" +[ -n "$VERSION_NAME" ] || fail "không đọc được versionName từ $ANDROID_GRADLE" echo "Android versionName = $VERSION_NAME" -[ -f "$APPLE_PROJECT_YML" ] || fail "khong thay $APPLE_PROJECT_YML" +[ -f "$APPLE_PROJECT_YML" ] || fail "không thấy $APPLE_PROJECT_YML" typeset -a MARKETING_VALUES MARKETING_VALUES=("${(@f)$(grep -E '^\s*MARKETING_VERSION:' "$APPLE_PROJECT_YML" | sed -E 's/.*"([^"]+)".*/\1/')}") -[ "${#MARKETING_VALUES[@]}" -gt 0 ] || fail "khong doc duoc MARKETING_VERSION tu $APPLE_PROJECT_YML" +[ "${#MARKETING_VALUES[@]}" -gt 0 ] || fail "không đọc được MARKETING_VERSION từ $APPLE_PROJECT_YML" MARKETING_VERSION="${MARKETING_VALUES[1]}" for v in "${MARKETING_VALUES[@]}"; do [ "$v" = "$MARKETING_VERSION" ] \ - || fail "$APPLE_PROJECT_YML co nhieu gia tri MARKETING_VERSION khac nhau ('$MARKETING_VERSION' va '$v') — bump version chua dong bo het target" + || fail "$APPLE_PROJECT_YML có nhiều giá trị MARKETING_VERSION khác nhau ('$MARKETING_VERSION' và '$v') — bump version chưa đồng bộ hết target" done -echo "Apple MARKETING_VERSION = $MARKETING_VERSION (${#MARKETING_VALUES[@]} target, deu khop)" +echo "Apple MARKETING_VERSION = $MARKETING_VERSION (${#MARKETING_VALUES[@]} target, đều khớp)" typeset -a PROJECT_VERSION_VALUES PROJECT_VERSION_VALUES=("${(@f)$(grep -E '^\s*CURRENT_PROJECT_VERSION:' "$APPLE_PROJECT_YML" | sed -E 's/.*"([^"]+)".*/\1/')}") -[ "${#PROJECT_VERSION_VALUES[@]}" -gt 0 ] || fail "khong doc duoc CURRENT_PROJECT_VERSION tu $APPLE_PROJECT_YML" +[ "${#PROJECT_VERSION_VALUES[@]}" -gt 0 ] || fail "không đọc được CURRENT_PROJECT_VERSION từ $APPLE_PROJECT_YML" FIRST_PROJECT_VERSION="${PROJECT_VERSION_VALUES[1]}" for v in "${PROJECT_VERSION_VALUES[@]}"; do [ "$v" = "$FIRST_PROJECT_VERSION" ] \ - || fail "$APPLE_PROJECT_YML co nhieu gia tri CURRENT_PROJECT_VERSION khac nhau ('$FIRST_PROJECT_VERSION' va '$v') — bump version chua dong bo het target" + || fail "$APPLE_PROJECT_YML có nhiều giá trị CURRENT_PROJECT_VERSION khác nhau ('$FIRST_PROJECT_VERSION' và '$v') — bump version chưa đồng bộ hết target" done -echo "Apple CURRENT_PROJECT_VERSION = $FIRST_PROJECT_VERSION (${#PROJECT_VERSION_VALUES[@]} target, deu khop)" +echo "Apple CURRENT_PROJECT_VERSION = $FIRST_PROJECT_VERSION (${#PROJECT_VERSION_VALUES[@]} target, đều khớp)" [ "$TAG_CORE" = "$VERSION_NAME" ] \ - || fail "tag '$TAG' (phan loi '$TAG_CORE') khong khop versionName Android '$VERSION_NAME'" + || fail "tag '$TAG' (phần lõi '$TAG_CORE') không khớp versionName Android '$VERSION_NAME'" [ "$TAG_CORE" = "$MARKETING_VERSION" ] \ - || fail "tag '$TAG' (phan loi '$TAG_CORE') khong khop MARKETING_VERSION Apple '$MARKETING_VERSION'" -echo "Tag '$TAG' khop versionName va MARKETING_VERSION ($TAG_CORE)" + || fail "tag '$TAG' (phần lõi '$TAG_CORE') không khớp MARKETING_VERSION Apple '$MARKETING_VERSION'" +echo "Tag '$TAG' khớp versionName và MARKETING_VERSION ($TAG_CORE)" typeset -a REPO_NAMES typeset -a REPO_PATHS @@ -90,9 +90,9 @@ REPO_PATHS=( typeset -a TAG_SHAS -# Pha 1: kiem du 4 repo, chua gan tag nao. Repo nao loi thi dung, khong repo nao co tag. +# Pha 1: kiểm đủ 4 repo, chưa gắn tag nào. Repo nào lỗi thì dừng, không repo nào có tag. echo -echo "==== Pha 1: kiem 4 repo ====" +echo "==== Pha 1: kiểm 4 repo ====" i=1 while [ "$i" -le "${#REPO_NAMES[@]}" ]; do name="${REPO_NAMES[$i]}" @@ -100,55 +100,55 @@ while [ "$i" -le "${#REPO_NAMES[@]}" ]; do echo echo "== $name ($repo_path) ==" - [ -d "$repo_path" ] || fail "khong thay thu muc $repo_path" + [ -d "$repo_path" ] || fail "không thấy thư mục $repo_path" (cd "$repo_path" && git rev-parse --is-inside-work-tree >/dev/null 2>&1) \ - || fail "$name: khong phai git repo" + || fail "$name: không phải git repo" echo "+ git fetch origin" - (cd "$repo_path" && git fetch origin) || fail "$name: git fetch that bai" + (cd "$repo_path" && git fetch origin) || fail "$name: git fetch thất bại" current_branch="$(cd "$repo_path" && git branch --show-current)" - [ "$current_branch" = "main" ] || fail "$name: dang o nhanh '$current_branch', can 'main'" + [ "$current_branch" = "main" ] || fail "$name: đang ở nhánh '$current_branch', cần 'main'" local_sha="$(cd "$repo_path" && git rev-parse main)" remote_sha="$(cd "$repo_path" && git rev-parse origin/main)" if [ "$local_sha" != "$remote_sha" ]; then (cd "$repo_path" && git diff --quiet main origin/main) \ - || fail "$name: noi dung main local khac origin/main — PR chua merge, origin/main co commit khac, hoac main local co commit chua vao PR" + || fail "$name: nội dung main local khác origin/main — PR chưa merge, origin/main có commit khác, hoặc main local có commit chưa vào PR" if (cd "$repo_path" && git merge-base --is-ancestor main origin/main); then echo "+ git merge --ff-only origin/main" (cd "$repo_path" && git merge --ff-only origin/main) \ - || fail "$name: git merge --ff-only origin/main that bai" + || fail "$name: git merge --ff-only origin/main thất bại" else - echo "+ git reset --soft origin/main (PR merge bang rebase/squash, cay giong het)" + echo "+ git reset --soft origin/main (PR merge bằng rebase/squash, cây giống hệt)" (cd "$repo_path" && git reset --soft origin/main) \ - || fail "$name: git reset --soft origin/main that bai" + || fail "$name: git reset --soft origin/main thất bại" fi local_sha="$(cd "$repo_path" && git rev-parse main)" fi [ "$local_sha" = "$remote_sha" ] \ - || fail "$name: main local ($local_sha) khac origin/main ($remote_sha) sau khi dong bo" - echo "$name: main khop origin/main ($local_sha)" + || fail "$name: main local ($local_sha) khác origin/main ($remote_sha) sau khi đồng bộ" + echo "$name: main khớp origin/main ($local_sha)" if (cd "$repo_path" && git rev-parse -q --verify "refs/tags/$TAG" >/dev/null); then - fail "$name: tag '$TAG' da ton tai o local" + fail "$name: tag '$TAG' đã tồn tại ở local" fi (cd "$repo_path" && git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null) ls_code=$? case "$ls_code" in 2) ;; - 0) fail "$name: tag '$TAG' da ton tai tren origin" ;; - *) fail "$name: git ls-remote that bai (ma $ls_code)" ;; + 0) fail "$name: tag '$TAG' đã tồn tại trên origin" ;; + *) fail "$name: git ls-remote thất bại (mã $ls_code)" ;; esac - echo "$name: tag '$TAG' chua co o local va origin" + echo "$name: tag '$TAG' chưa có ở local và origin" TAG_SHAS[$i]="$local_sha" i=$((i + 1)) done -# Pha 2: ca 4 repo da qua Pha 1, moi gan tag va day. +# Pha 2: cả 4 repo đã qua Pha 1, mới gắn tag và đẩy. echo -echo "==== Pha 2: tag va day ====" +echo "==== Pha 2: tag và đẩy ====" i=1 while [ "$i" -le "${#REPO_NAMES[@]}" ]; do name="${REPO_NAMES[$i]}" @@ -159,14 +159,14 @@ while [ "$i" -le "${#REPO_NAMES[@]}" ]; do echo echo "== $name ==" echo "+ git tag $TAG $sha" - (cd "$repo_path" && git tag "$TAG" "$sha") || fail "$name: git tag that bai" + (cd "$repo_path" && git tag "$TAG" "$sha") || fail "$name: git tag thất bại" echo "+ git push origin refs/tags/$TAG" (cd "$repo_path" && git push origin "refs/tags/$TAG") \ - || fail "$name: git push tag that bai — cac repo truoc da co tag tren origin; sua loi roi day tay: git -C $repo_path push origin refs/tags/$TAG" + || fail "$name: git push tag thất bại — các repo trước đã có tag trên origin; sửa lỗi rồi đẩy tay: git -C $repo_path push origin refs/tags/$TAG" - echo "$name: da tag va day $TAG" + echo "$name: đã tag và đẩy $TAG" done echo -echo "XONG: $TAG da tag va day theo thu tu hub -> shared -> android -> apple." +echo "XONG: $TAG đã tag và đẩy theo thứ tự hub -> shared -> android -> apple." diff --git a/tools/verify-android-ui.sh b/tools/verify-android-ui.sh index e01a460..d200213 100755 --- a/tools/verify-android-ui.sh +++ b/tools/verify-android-ui.sh @@ -1,14 +1,14 @@ #!/bin/zsh -# Chay NGOAI sandbox: zsh handlive/tools/verify-android-ui.sh +# Chạy NGOÀI sandbox: zsh handlive/tools/verify-android-ui.sh # -# Kiem tra Android UI (nhan Bluetooth trong danh sach thiet bi): -# 1. core:design:check — hieu ung, thanh phan UI -# 2. core:strings:check — chuoi tim kiem, catalog -# 3. feature:pairing:check — logic cap, UI pairing -# 4. app:testFossDebugUnitTest — test toan bo app (Foss flavor, Debug) -# Toan bo output ghi vao handlive/.build-logs/gradle-android-ui.log (gitignore), -# in 60 dong cuoi. Khong dung khi mot buoc do — chay het roi in tom tat; -# thoat 0 chi khi tat ca buoc xanh. +# Kiểm tra Android UI (nhãn Bluetooth trong danh sách thiết bị): +# 1. core:design:check — hiệu ứng, thành phần UI +# 2. core:strings:check — chuỗi tìm kiếm, catalog +# 3. feature:pairing:check — logic ghép, UI pairing +# 4. app:testFossDebugUnitTest — test toàn bộ app (Foss flavor, Debug) +# Toàn bộ output ghi vào handlive/.build-logs/gradle-android-ui.log (gitignore), +# in 60 dòng cuối. Không dừng khi một bước đỏ — chạy hết rồi in tóm tắt; +# thoát 0 chỉ khi tất cả bước xanh. set -uo pipefail HUB_ROOT="$(cd "$(dirname "$0")/.." && pwd)" @@ -22,14 +22,14 @@ GRADLE_LOG="$LOG_DIR/gradle-android-ui.log" overall_ok=1 TEST_SUMMARY="" -echo "== Android: gradlew check cho core:design, core:strings, feature:pairing va app:testFossDebugUnitTest ==" +echo "== Android: gradlew check cho core:design, core:strings, feature:pairing và app:testFossDebugUnitTest ==" : "${JAVA_HOME:=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home}" : "${ANDROID_HOME:=/opt/homebrew/share/android-commandlinetools}" export JAVA_HOME ANDROID_HOME GRADLEW="$ANDROID_DIR/gradlew" if [ ! -f "$GRADLEW" ]; then - echo " LOI: khong thay $GRADLEW" + echo " LỖI: không thấy $GRADLEW" overall_ok=0 EXIT=1 else @@ -37,11 +37,11 @@ else ":core:design:check" ":core:strings:check" ":feature:pairing:check" ":app:testFossDebugUnitTest" \ > "$GRADLE_LOG" 2>&1 EXIT=$? - echo " == 60 dong cuoi cua $GRADLE_LOG ==" + echo " == 60 dòng cuối của $GRADLE_LOG ==" tail -n 60 "$GRADLE_LOG" - # Gradle khong in so test ra console theo mac dinh (khong co testLogging trong build.gradle.kts), - # nen cong thuoc tinh tests= tu cac file JUnit XML ma gradle tu ghi cho moi module. + # Gradle không in số test ra console theo mặc định (không có testLogging trong build.gradle.kts), + # nên cộng thuộc tính tests= từ các file JUnit XML mà gradle tự ghi cho mỗi module. total_tests=0 xml_files=0 while IFS= read -r xml; do @@ -53,24 +53,24 @@ else -path '*/build/test-results/*' -name 'TEST-*.xml' 2>/dev/null) if [ "$xml_files" -gt 0 ]; then - TEST_SUMMARY="$total_tests test trong $xml_files file XML duoi */build/test-results" + TEST_SUMMARY="$total_tests test trong $xml_files file XML dưới */build/test-results" else - TEST_SUMMARY="khong tim thay file XML ket qua test duoi */build/test-results, xem $GRADLE_LOG" + TEST_SUMMARY="không tìm thấy file XML kết quả test dưới */build/test-results, xem $GRADLE_LOG" fi fi echo -echo "== Tom tat ==" +echo "== Tóm tắt ==" if [ "$EXIT" -eq 0 ]; then echo " core:design:check — XANH" echo " core:strings:check — XANH" echo " feature:pairing:check — XANH" echo " app:testFossDebugUnitTest — XANH ($TEST_SUMMARY)" - echo "XONG: tat ca buoc xanh." + echo "XONG: tất cả bước xanh." else echo " Gradle exit $EXIT" echo " Test summary: $TEST_SUMMARY" - echo "LOI: co buoc do, xem log tren." + echo "LỖI: có bước đỏ, xem log trên." overall_ok=0 fi diff --git a/tools/verify-phase7.sh b/tools/verify-phase7.sh index 7139337..24aac8a 100755 --- a/tools/verify-phase7.sh +++ b/tools/verify-phase7.sh @@ -1,14 +1,14 @@ #!/bin/zsh -# Chay NGOAI sandbox: zsh handlive/tools/verify-phase7.sh +# Chạy NGOÀI sandbox: zsh handlive/tools/verify-phase7.sh # -# Gop toan bo kiem tra Phase 7 (Bluetooth/Wi-Fi Direct offline) thanh mot lan bam: +# Gộp toàn bộ kiểm tra Phase 7 (Bluetooth/Wi-Fi Direct offline) thành một lần bấm: # 1. Apple: swift test cho HLProtocol, HLCrypto, HLTransport qua -# ../handlive-apple/Tools/run-package-tests.sh +# ../handlive-apple/Tools/run-package-tests.sh # 2. Android: ../handlive-android/gradlew check cho core/protocol, core/crypto, -# core/transport va testDebugUnitTest cho feature/connection; toan bo output -# ghi vao handlive/.build-logs/gradle-phase7.log (gitignore), in 60 dong cuoi. +# core/transport và testDebugUnitTest cho feature/connection; toàn bộ output +# ghi vào handlive/.build-logs/gradle-phase7.log (gitignore), in 60 dòng cuối. # -# Khong dung khi mot buoc do — chay het roi in tom tat; thoat 0 chi khi moi buoc xanh. +# Không dừng khi một bước đỏ — chạy hết rồi in tóm tắt; thoát 0 chỉ khi mọi bước xanh. set -uo pipefail HUB_ROOT="$(cd "$(dirname "$0")/.." && pwd)" @@ -28,18 +28,18 @@ RUNNER="$APPLE_DIR/Tools/run-package-tests.sh" for PKG in HLProtocol HLCrypto HLTransport; do echo "-- $PKG --" if [ ! -f "$RUNNER" ]; then - echo " LOI: khong thay $RUNNER" - RESULTS+=("Apple $PKG: LOI (khong thay run-package-tests.sh)") + echo " LỖI: không thấy $RUNNER" + RESULTS+=("Apple $PKG: LỖI (không thấy run-package-tests.sh)") overall_ok=0 continue fi if zsh "$RUNNER" "$PKG"; then PKG_LOG="$APPLE_DIR/.build-logs/$PKG-test.log" COUNT_LINE="$(grep -E 'Test run with [0-9]+ tests? in [0-9]+ suites? (passed|failed)' "$PKG_LOG" 2>/dev/null | tail -1 | sed 's/^[[:space:]]*//')" - [ -z "$COUNT_LINE" ] && COUNT_LINE="khong doc duoc so test tu $PKG_LOG" + [ -z "$COUNT_LINE" ] && COUNT_LINE="không đọc được số test từ $PKG_LOG" RESULTS+=("Apple $PKG: XANH — $COUNT_LINE") else - RESULTS+=("Apple $PKG: DO (xem $APPLE_DIR/.build-logs/$PKG-test.log)") + RESULTS+=("Apple $PKG: ĐỎ (xem $APPLE_DIR/.build-logs/$PKG-test.log)") overall_ok=0 fi done @@ -52,19 +52,19 @@ export JAVA_HOME ANDROID_HOME GRADLEW="$ANDROID_DIR/gradlew" if [ ! -f "$GRADLEW" ]; then - echo " LOI: khong thay $GRADLEW" - RESULTS+=("Android: LOI (khong thay gradlew)") + echo " LỖI: không thấy $GRADLEW" + RESULTS+=("Android: LỖI (không thấy gradlew)") overall_ok=0 else env JAVA_HOME="$JAVA_HOME" ANDROID_HOME="$ANDROID_HOME" "$GRADLEW" -p "$ANDROID_DIR" --continue \ ":core:protocol:check" ":core:crypto:check" ":core:transport:check" ":feature:connection:testDebugUnitTest" \ > "$GRADLE_LOG" 2>&1 GRADLE_EXIT=$? - echo " == 60 dong cuoi cua $GRADLE_LOG ==" + echo " == 60 dòng cuối của $GRADLE_LOG ==" tail -n 60 "$GRADLE_LOG" - # Gradle khong in so test ra console theo mac dinh (khong co testLogging trong build.gradle.kts), - # nen cong thuoc tinh tests= tu cac file JUnit XML ma gradle tu ghi cho moi module. + # Gradle không in số test ra console theo mặc định (không có testLogging trong build.gradle.kts), + # nên cộng thuộc tính tests= từ các file JUnit XML mà gradle tự ghi cho mỗi module. total_tests=0 xml_files=0 while IFS= read -r xml; do @@ -76,28 +76,28 @@ else -path '*/build/test-results/*' -name 'TEST-*.xml' 2>/dev/null) if [ "$xml_files" -gt 0 ]; then - TEST_SUMMARY="$total_tests test trong $xml_files file XML duoi */build/test-results" + TEST_SUMMARY="$total_tests test trong $xml_files file XML dưới */build/test-results" else - TEST_SUMMARY="khong tim thay file XML ket qua test duoi */build/test-results, xem $GRADLE_LOG" + TEST_SUMMARY="không tìm thấy file XML kết quả test dưới */build/test-results, xem $GRADLE_LOG" fi if [ "$GRADLE_EXIT" -eq 0 ]; then RESULTS+=("Android (4 module): XANH — $TEST_SUMMARY") else - RESULTS+=("Android (4 module): DO (exit=$GRADLE_EXIT) — $TEST_SUMMARY") + RESULTS+=("Android (4 module): ĐỎ (exit=$GRADLE_EXIT) — $TEST_SUMMARY") overall_ok=0 fi fi echo -echo "== Tom tat ==" +echo "== Tóm tắt ==" for r in "${RESULTS[@]}"; do echo " $r" done if [ "$overall_ok" -eq 1 ]; then - echo "XONG: tat ca buoc xanh." + echo "XONG: tất cả bước xanh." exit 0 else - echo "LOI: co buoc do, xem log tren." + echo "LỖI: có bước đỏ, xem log trên." exit 1 fi diff --git a/website/seed/content/en/home.html b/website/seed/content/en/home.html index cd1872a..f1e4022 100644 --- a/website/seed/content/en/home.html +++ b/website/seed/content/en/home.html @@ -1,7 +1,7 @@
-

Public beta · v0.1.1

+

Public beta · v0.2.0

diff --git a/website/seed/content/site.json b/website/seed/content/site.json index c401570..2fc102a 100644 --- a/website/seed/content/site.json +++ b/website/seed/content/site.json @@ -4,7 +4,7 @@ { "slug": "vi", "name": "Tiếng Việt", "locale": "vi", "flag": "vn" } ], "links": { - "release": "https://github.com/HandLive/handlive/releases/tag/v0.1.1", + "release": "https://github.com/HandLive/handlive/releases/tag/v0.2.0", "github": "https://github.com/HandLive" }, "category": { "en": "News", "vi": "Tin tức" }, diff --git a/website/seed/content/vi/home.html b/website/seed/content/vi/home.html index ad10cb8..8a4529b 100644 --- a/website/seed/content/vi/home.html +++ b/website/seed/content/vi/home.html @@ -1,7 +1,7 @@
-

Bản beta công khai · v0.1.1

+

Bản beta công khai · v0.2.0

diff --git a/website/theme/handlive/inc/setup.php b/website/theme/handlive/inc/setup.php index 2e66db6..17eceb5 100644 --- a/website/theme/handlive/inc/setup.php +++ b/website/theme/handlive/inc/setup.php @@ -9,7 +9,7 @@ const HANDLIVE_VERSION = '1.0.0'; const HANDLIVE_GITHUB_URL = 'https://github.com/HandLive'; -const HANDLIVE_RELEASE_URL = 'https://github.com/HandLive/handlive/releases/tag/v0.1.1'; +const HANDLIVE_RELEASE_URL = 'https://github.com/HandLive/handlive/releases/tag/v0.2.0'; add_action( 'after_setup_theme',