feat(camera): pairing and camera-list UI for both desktop apps - #78
Conversation
Until now, connecting the two apps meant reading a Listener ID, a Capability and a Connection ID off one screen and typing them into another. Phases 1-3 put grants, pairing codes and discovery behind the proxy and gave the crates methods for them; this wires those methods to the UI, so the manual exchange stops being the way the apps are used. camera-server grows a pairing panel: one button issues a code, the code is shown with the time left on it and as a QR image, and the panel goes back to the button when it lapses. Below it, the grants the listener has handed out, each with a revoke, and an activity feed fed by the Phase 3 signalling broadcast, which is what tells the operator that a peer bound, that one was displaced, or that a bind failed. camera-client replaces the Listener ID field with a list of the cameras the proxy says this Endpoint may reach. Picking one fills in the listener and clears the Capability, which is what makes the connect take the grant path. A pairing code typed in next to Refresh adds a camera to that list, and the list is re-read on success so the new camera is there to pick. The old fields are not gone, only moved under a collapsing header, together with the Endpoint ID button that was step 1 of the manual exchange. A proxy without grants still works, and so does anything the automatic path gets wrong. Also folds the client's two P2pConfig constructions into one helper, which moves the key load ahead of the channel wiring, so a bad key path now leaves the app in the state it was already in rather than half-connected. iOS (FFI, SwiftUI, QR capture) is the remainder of Phase 4 and is not here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
概要フェーズ 1〜3 でプロキシとクレートに積んだものを、両デスクトップアプリの画面に繋ぐ変更。camera-server にペアリングパネル(コード発行・QR・残り時間・許可済みデバイス・アクティビティ)、camera-client に到達可能なカメラ一覧とコード入力。手作業の交換は畳んだヘッダの下に残す、という落とし方も妥当だと思います。 egui のコードとして気をつけるべきところが押さえられています。
指摘1. 起動直後の一覧が空のままで、それが事実のように読める
どちらも「まだ無い」という断定の文言なので、実態と食い違うと操作者は「ペアリングが消えた」と受け取ります。camera-server は 併せて、camera-server 側は 2. Refresh / Pair のたびに Endpoint Token と制御プレーン接続を張り直す
3. ペアリングコードの残り時間が 300 秒のハードコードlet ttl = Duration::from_secs(300);
match code_remaining(shown_at, ttl) {一方
パネルの目的が「切れかけていることを操作者に見せる(誰かがペアリングに失敗して初めて気づく、を避ける)」ことなので、ここは 4. QR の中身が生のコードなので、スキャンしても何も起きない
5. 細かい点
検証について「表示されたコードを打ってその grant で繋ぐ」「displaced な接続を再 bind する」が未実施と書かれていますが、後者は #76 のレビュー後に入った if state.current.is_none() { return connections.first(); }の分岐を通る経路です。B が去ったあと A を拾い直せるか(A がまだ 総評指摘 1 は操作者の誤解を生むので直しておきたいところ、2 は #77 からの持ち越しでまとめて解消できる話、3 は表示の正しさ、4 は次フェーズの形を決める判断です。5 は任意。UI としての作りは素直で、egui で壊れやすい部分(ロック保持、毎フレームの再描画・通信)を避けられているのは良いと思います。 🤖 Generated with Claude Code |
Five things, in the order they matter. An empty list is shown as empty before anything has been asked. Both apps said "none yet" from the moment they started, so an operator who paired a device yesterday was told, in as many words, that it was gone. Neither list is a `Vec` any more: `None` means the proxy has not answered yet and reads as "not read yet" on screen. The server also asks once when its session comes up, and again whenever a peer binds — the moment the list is most likely to be one device out of date. The client asks once as soon as there is a token to ask with, and not while it is still being typed. Every listing and pairing opened its own control plane. `PeerDirectory::open` issues an Endpoint Token and opens a QUIC connection to the proxy, so five button presses were five of each, and the connect that followed made a sixth. The client now holds one open and replaces it when its token is nearly out; listing, pairing and the grant connect all go over it. That closes the "listing and connecting make two connections" point from #77 at the same time. The countdown on the pairing code was 300 seconds hardcoded, started when the response arrived. It was a round trip late, and would have gone on being confident about a number the proxy is free to change. It now comes from the `expires_at` the proxy set. An unreadable timestamp leaves the code up without a countdown rather than taking a working code off the screen. The QR carried the bare code, so scanning it showed a phone user eight characters and left them to find the app. It now carries `isekai://pair?code=...`, decided here rather than when the mobile capture lands and has nothing to agree with. `pairing_uri` and `pairing_code_from_input` are in `isekai-p2p-core` for the mobile side to share, and the client's field takes the URI or the bare code, so nothing has to migrate. Smaller: the QR side comes from `QrCode::width()` rather than a square root; `qrcode` moved to where the workspace manifest is otherwise alphabetical; pairing success names the camera rather than its listener id, from the listing it re-reads anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks — all five addressed. 1. An empty list read as an assertion. Both lists are now 2. A control plane per button press. The client now holds one 3. A hardcoded 300s countdown. Gone. 4. QR content. Now 5.
New tests: the URI round trip (including bare code and scanner-appended parameters), the countdown against a fixed clock, and that the QR comes out square with its quiet zone intact. On verification — noted, and thank you for pinning down what actually decides it. I will watch whether A is still listed as One note on process: these crates are not rustfmt-clean on |
Until now, connecting the two apps meant reading a Listener ID, a Capability
and a Connection ID off one screen and typing them into another. Phases 1-3
put grants, pairing codes and discovery behind the proxy and gave the crates
methods for them; this wires those methods to the UI, so the manual exchange
stops being the way the apps are used.
camera-server grows a pairing panel: one button issues a code, the code is
shown with the time left on it and as a QR image, and the panel goes back to
the button when it lapses. The time left comes from the
expires_atthe proxyset, not from a lifetime assumed at this end; a timestamp this cannot read
leaves the code up without a countdown rather than taking a working code off
the screen. The QR carries
isekai://pair?code=...rather than the bare code,because a scan of eight characters shows a phone user some text and leaves
them to find the app themselves — settled here rather than when the mobile
capture lands and has nothing to agree with.
pairing_uriandpairing_code_from_inputlive inisekai-p2p-corefor the mobile side toshare.
Below that, the grants the listener has handed out, each with a revoke, and an
activity feed fed by the Phase 3 signalling broadcast, which is what tells the
operator that a peer bound, that one was displaced, or that a bind failed. The
grant list is read when the session comes up and again whenever a peer binds,
so a device that paired a moment ago is there without anyone pressing Refresh.
camera-client replaces the Listener ID field with a list of the cameras the
proxy says this Endpoint may reach, read as soon as there is a token to read
it with. Picking one fills in the listener and clears the Capability, which is
what makes the connect take the grant path. A pairing code typed or scanned in
next to Refresh adds a camera to that list, and the list is re-read on success
— which is also where the camera's name comes from, so the confirmation says
what the operator will recognise.
Neither list is a
Vec.PeerDirectory::openissues an Endpoint Token andopens a QUIC connection to the proxy, so the client holds one open across
button presses and replaces it when the token is nearly out; listing, pairing
and the grant connect all go over it, which is also what stops a listing and
the connect that acts on it from making two connections.
The old fields are not gone, only moved under a collapsing header, together
with the Endpoint ID button that was step 1 of the manual exchange. A proxy
without grants still works, and so does anything the automatic path gets
wrong.
Also folds the client's two P2pConfig constructions into one helper, which
moves the key load ahead of the channel wiring, so a bad key path now leaves
the app in the state it was already in rather than half-connected.
iOS (FFI, SwiftUI, QR capture) is the remainder of Phase 4 and is not here.
Verification
LIBCLANG_PATH=/usr/lib/llvm-18/lib RUSTFLAGS="-L /usr/lib/llvm-18/lib" cargo test --manifest-path rust/Cargo.toml --workspace— all passing, including new tests for the pairing URI round trip, the countdown against a fixed clock, and the QR's quiet zonecargo clippyon the touched crates introduces no new warningscargo fmton the touched files only; these crates are not rustfmt-clean on main, so running it per package would rewrite files this branch does not touchNot yet done on hardware: typing a displayed code and connecting on the grant
it creates, and re-binding a displaced connection. For the second, what is
worth watching is whether A is still listed as
relayafter B leaves andwhether the proxy's relay edge is still alive at that point — those are what
decide whether the
state.current.is_none()branch can do anything. I willrun both and report what happens.
🤖 Generated with Claude Code