wip(k1): checkpoint connection recovery rewrite
Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
This commit is contained in:
@@ -111,6 +111,27 @@ handle from the operator's scan and connects that exact selected handle in the
|
||||
following network action. A fallback lookup remains only for non-UI callers
|
||||
that did not perform discovery first.
|
||||
|
||||
The public advertisement cache is deliberately separate from an admitted
|
||||
device session. Rows from the latest explicit scan generation remain stable
|
||||
without wall-clock expiry while the operator completes the form. They still
|
||||
grant no mutation authority without exact retained-handle capture and live
|
||||
GATT validation. An admitted selected session ends only on proven disconnect,
|
||||
explicit stop, app/backend restart, selection of another K1, or connection-mode
|
||||
switch. A later scan may replace unselected candidates but never auto-connects
|
||||
any of them.
|
||||
|
||||
An explicit Quick Connect to Bridge request for that same device can therefore
|
||||
continue when K1 no longer advertises after AP activation. The preconditions
|
||||
are: no active acquisition, no pending evidence cleanup, no active local source,
|
||||
and a terminal/released control session. A mode switch closes the old selected
|
||||
session first; the operator then scans, selects the K1, and creates a clean new
|
||||
GATT session for Bridge. Before the station command the code performs the
|
||||
normal internal `7f02` baseline read and allows exactly one 99-byte `7f01`
|
||||
write. A powered-off or unreachable peripheral ends that attempt. An
|
||||
unobserved post-write result is recorded as terminal `outcome-unknown`; it is
|
||||
never retried automatically and never blocks a later distinct explicit
|
||||
scan-select-connect attempt.
|
||||
|
||||
The 2026-07-20 prepared-host acceptance installed the exact firmware provider,
|
||||
found one expected K1 candidate, emitted one AP-enable write, observed AP-ready
|
||||
and completed one CoreWLAN association without an iPhone or manual credential.
|
||||
@@ -136,12 +157,19 @@ mode. It never fragments or retries the payload automatically.
|
||||
|
||||
A completed GATT write only proves transport completion. It does not prove that
|
||||
the K1 joined Wi-Fi or began beaconing. The application polls `7f02`; the
|
||||
observed response frame contains a fixed-width mode slot, an address slot, a
|
||||
status byte at offset 50 and the AP-ready flag at offset 51. The stale AP
|
||||
observed response frame contains a fixed-width text slot, an address slot, a
|
||||
status byte at offset 50 and the AP-ready flag at offset 51. The text slot is
|
||||
not a uniform mode enum: AP state uses the `WIFI_AP` control literal, while the
|
||||
2026-08-08 FW 3.0.2 Bridge observation returned the joined network name. The stale AP
|
||||
baseline reports `WIFI_AP / 192.168.56.1 / byte51=0`; the physically observed
|
||||
ready transition reports the same mode/address with `byte51=1`.
|
||||
|
||||
For Bridge/Direct Connect, acceptance requires at least one of:
|
||||
For Bridge/Direct, acceptance requires the post-write `7f02` text slot to match
|
||||
the exact requested network name and the address slot to contain a valid
|
||||
non-AP private IPv4. This proves the desired target even when the K1 was
|
||||
already joined to the same network before the explicit idempotent command. A
|
||||
legacy literal-only `WIFI_CLIENT` observation retains the older conservative
|
||||
cross-family rules and requires at least one of:
|
||||
|
||||
1. `7f02` reports a non-AP IPv4 address;
|
||||
2. the same address appears as a new router/ARP client after the write;
|
||||
@@ -150,6 +178,15 @@ For Bridge/Direct Connect, acceptance requires at least one of:
|
||||
|
||||
Do not infer success from a write callback alone.
|
||||
|
||||
An interrupted attempt with no exact post-write network-name observation is not
|
||||
made successful by a write callback, changed DHCP address, router/ARP row or
|
||||
reachable endpoint. Likewise, an already AP-ready baseline alone cannot prove
|
||||
the outcome of an interrupted Quick-to-Quick attempt. Such an attempt remains
|
||||
`outcome-unknown` in historical audit and is never replayed automatically. It
|
||||
does not create a permanent mutation barrier: after the old active operation
|
||||
and cleanup have terminated, a later explicit operator scan, selection, and
|
||||
connect is a distinct session with its own single reviewed write.
|
||||
|
||||
The Bridge/Direct Connect address is a DHCP lease, not configuration and not
|
||||
device identity. Mission Core re-reads `7f02` without writing before every new
|
||||
LAN control session, implicit-host acquisition and factory-calibration read.
|
||||
@@ -157,6 +194,10 @@ If the value changes, it rotates `device_session_id`; it never retargets an
|
||||
active acquisition. A correlated MQTT `DeviceInfo` response supplies the live
|
||||
model/firmware/serial identity barrier.
|
||||
|
||||
The joined network name is used only for exact in-process comparison with the
|
||||
current explicit request. Durable network audit stores the normalized semantic
|
||||
family and never stores or publishes the raw network name.
|
||||
|
||||
The 2026-07-20 reboot/power-cycle check observed the startup race directly:
|
||||
one read returned the earlier `.54` lease while that exact address had no ARP or
|
||||
application endpoint; a later read returned `.52`, where exact probes found
|
||||
@@ -166,7 +207,9 @@ and why a BLE lease observation alone is not reported as live DeviceInfo.
|
||||
|
||||
For Quick Connect, host association is not admitted until the canonical
|
||||
byte-51 ready flag is observed. CoreWLAN then searches only for the exact
|
||||
device-profile SSID for at most 15 seconds and performs at most one association.
|
||||
device-profile SSID for at most 30 seconds and performs at most one association.
|
||||
AP-ready is a device-state barrier, not proof that the host has already observed
|
||||
the RF beacon; a retained successful run required 18.142 seconds of discovery.
|
||||
|
||||
## Safety, recovery and stop conditions
|
||||
|
||||
@@ -176,9 +219,17 @@ device-profile SSID for at most 15 seconds and performs at most one association.
|
||||
secure store. Missing or mismatched firmware material fails before the AP
|
||||
write. Never extrapolate this provider to another firmware or model.
|
||||
- The macOS adapter materializes a device-scoped Keychain item from the exact
|
||||
firmware source, then performs one association. Standard Wi-Fi Keychain and
|
||||
native prompt paths remain compatibility fallbacks, not the reviewed
|
||||
zero-touch path. It never asks the browser for a password.
|
||||
firmware source before the BLE write, then performs one association using
|
||||
only that exact profile. Standard Wi-Fi Keychain lookup, native password
|
||||
prompts and post-write profile rewrites are prohibited. It never asks the
|
||||
browser for a password. Preflight reads are non-interactive and validate the
|
||||
exact SSID/source inside the helper before K1 changes network state.
|
||||
- The prepared-host laboratory adapter launches the reviewed Swift source only
|
||||
through `/usr/bin/xcrun swift`. Runtime `swiftc` compilation to an ad-hoc
|
||||
executable is prohibited because its unstable process identity regressed
|
||||
Keychain ACL and CoreWLAN behavior. Product packaging still requires a
|
||||
prebuilt, properly signed helper with a stable designated identity and
|
||||
explicit CoreWLAN authorization.
|
||||
- Do not alter Deco settings, scan the subnet, or guess any credential.
|
||||
- If the status does not change, do not retry automatically.
|
||||
- If the supplied credentials are wrong, reconnect over BLE and overwrite them
|
||||
|
||||
@@ -110,12 +110,11 @@ directly and use their sibling metadata receive timestamps when present.
|
||||
## Connect and stream live
|
||||
|
||||
1. Power K1 to its normal steady-green standby state.
|
||||
2. Confirm the manual power checklist in **Парк → Локальное устройство**.
|
||||
3. Run the real six-second BLE scan and select the intended device from the
|
||||
2. Run the real six-second BLE scan and select the intended device from the
|
||||
complete visible-device list.
|
||||
4. Enter the existing router SSID/password and explicitly authorize the reviewed
|
||||
3. Enter the existing router SSID/password and explicitly authorize the reviewed
|
||||
provisioning write. The backend does not retry the write automatically.
|
||||
5. Enter the required project name, confirm operator presence, closed LixelGO,
|
||||
4. Enter the required project name, confirm operator presence, closed LixelGO,
|
||||
storage/power and steady green, then choose **Запустить сканирование и
|
||||
локальный приём** once.
|
||||
6. Mission Core emits operations 1–6, waits for their correlated device
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -136,7 +136,7 @@ The current XGRIDS contribution maps its proven internal workflow into those
|
||||
platform states without changing the wire protocol:
|
||||
|
||||
```text
|
||||
confirm power -> scan BLE -> select candidate -> enter Wi-Fi
|
||||
scan BLE -> select candidate -> enter Wi-Fi
|
||||
-> provision once -> receive LAN address -> start source
|
||||
-> wait for first point frame -> streaming
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ADR 0013: explicit K1 local connection matrix
|
||||
|
||||
- Status: amended 2026-07-20; Bridge is the product path, Quick Connect retained as a prepared-host laboratory path
|
||||
- Status: amended 2026-08-08; Bridge is the product path, Quick Connect retained as a prepared-host laboratory path
|
||||
- Date: 2026-07-19
|
||||
- Extends: ADR 0004, ADR 0005 and ADR 0012
|
||||
|
||||
@@ -47,6 +47,23 @@ for the previous session. An active acquisition is never retargeted in place.
|
||||
The later correlated MQTT `DeviceInfo` response supplies model, firmware,
|
||||
serial and vendor identity; IP equality alone cannot identify a K1.
|
||||
|
||||
The 2026-08-08 physical Bridge trace corrected the earlier field model: the
|
||||
first `7f02` text slot is `WIFI_AP` in AP state but contains the joined network
|
||||
name in FW 3.0.2 station state. Mission Core therefore normalizes that station
|
||||
response to `WIFI_CLIENT` internally and admits Bridge/Direct only when the raw
|
||||
post-write name exactly matches the current explicit request plus a valid
|
||||
non-AP private address. The raw name is not persisted in the secret-free
|
||||
network audit or published through API state.
|
||||
|
||||
The limitation still applies when recording an interrupted attempt that has no
|
||||
exact post-write network-name observation. A changed private DHCP address alone
|
||||
cannot identify the selected network. An already AP-ready baseline likewise
|
||||
cannot prove the outcome of an interrupted Quick-to-Quick attempt. Mission Core
|
||||
therefore records that attempt as terminal `outcome-unknown` and never replays it automatically. The
|
||||
historical uncertainty is not a permanent barrier: after the old active
|
||||
operation and cleanup have ended, a later explicit operator scan, selection,
|
||||
and connect is a distinct session with its own one reviewed write.
|
||||
|
||||
Product decision on 2026-07-20: Bridge/direct-LAN is the continuing route.
|
||||
Quick Connect remains visible and executable on an already prepared host, but
|
||||
is not a deployment dependency or portability claim.
|
||||
@@ -86,12 +103,65 @@ and the following cold Swift/CoreWLAN process missed the beacon. The corrected
|
||||
implementation holds the selected `BleakClient` open through bounded native
|
||||
SSID discovery and the single association call.
|
||||
|
||||
BLE discovery and the selected device action form one host session. A physical
|
||||
run proved that immediately rediscovering the same K1 by its CoreBluetooth UUID
|
||||
can fail even though the preceding scan exposed it. Mission Core retains the
|
||||
non-serializable `BLEDevice` handle process-locally and uses that exact handle
|
||||
for the next selected network action; it never exposes the handle through API
|
||||
state or treats the macOS UUID as durable device identity.
|
||||
One explicit six-second BLE discovery and the later Apply action form one
|
||||
operator intent without a second discovery. A physical run proved that
|
||||
immediately rediscovering the same K1 by its CoreBluetooth UUID can fail even
|
||||
though the preceding scan exposed it. Mission Core retains the non-serializable
|
||||
`BLEDevice` handle process-locally and uses that exact handle for Apply; it never
|
||||
exposes the handle through API state or treats the macOS UUID as durable device
|
||||
identity. UI row selection itself performs no GATT or backend I/O.
|
||||
|
||||
The operator-visible candidate list, local selected draft and admitted active
|
||||
session are separate contracts. Every explicit scan replaces the candidate
|
||||
set. Selection only binds a local form to one result from the latest admitted
|
||||
generation. Wall-clock age does not remove that generation while the operator
|
||||
completes the form. Apply admits only its exact retained handle and live GATT
|
||||
validation may create the active session; a remembered UUID is never mutation
|
||||
authority. Proven disconnect, explicit stop, app/backend restart, another
|
||||
explicit Scan, or a committed mode transition revokes the applicable candidate
|
||||
or live session. Rediscovery never auto-connects.
|
||||
|
||||
Quick Connect to Bridge is an explicit topology transition, not another scan
|
||||
heuristic. Selecting Bridge — or choosing another K1 while Bridge is already
|
||||
selected — sends one idempotent local `reset_scenario` CAS. It seals retained
|
||||
receiver/camera/control ownership, invalidates candidates and credentials and
|
||||
retires old physical lineage truthfully, while sending no device command, BLE,
|
||||
host-network write or automatic Scan. The next explicit Scan starts the clean
|
||||
discovery flow, while Apply remains the topology and device-mutation boundary.
|
||||
The operator selects one discovered K1, sees the Bridge credentials immediately
|
||||
and submits once. The new GATT
|
||||
session reads internal baseline `7f02` and emits exactly one reviewed 99-byte
|
||||
station write. A connect failure ends that attempt. An ambiguous post-write
|
||||
failure is terminal `outcome-unknown` audit, not a permanent cross-session
|
||||
fence. There is no automatic BLE or network-write retry.
|
||||
|
||||
Likewise, an exact REST `network_applied` result with unready/unknown control
|
||||
spends that Apply and its credentials without making the read-model attempt a
|
||||
permanent topology lock. Recommended Verify is pinned to the backend
|
||||
current/configured target. A separately explicit new intent still requires
|
||||
current server policy: Bridge prepares `select-device` before a later fresh
|
||||
scan; Quick and Direct run an admitted fresh scan, select only its latest row
|
||||
and create a new idempotency Apply; a mode or same-mode new-device transition
|
||||
uses one idempotent local-only `reset_scenario` before that fresh scan. None of
|
||||
these new-intent UI paths reuses the old
|
||||
intent or runs as a hidden frontend/mutating continuation. The service-owned
|
||||
same-intent read-only bootstrap declared below is the sole post-ACK exception.
|
||||
|
||||
The Apply REST call returns as soon as the exact durable
|
||||
`network_applied` proof is available. The service may continue the same
|
||||
intent's supervised control bootstrap read-only after that ACK. This performs
|
||||
no BLE/host mutation or retry and creates no frontend Verify, Scan, Apply or
|
||||
blocking Apply loader. While the exact child is accepted/running, the UI may
|
||||
show only a passive **Сеть настроена · подтверждаем управление** indicator and
|
||||
must keep every recovery action disabled. Later control state arrives only as
|
||||
backend presentation convergence; terminal unready/unknown state then exposes
|
||||
the explicit server-policy recovery choices.
|
||||
|
||||
Bridge and Quick Connect were physically accepted as separate paths before
|
||||
this amendment. The combined Quick Connect to Bridge transition has automated
|
||||
contract coverage but remains a distinct physical acceptance gate; it must not
|
||||
be reported as field-accepted until one redacted live run records both sides of
|
||||
the transition.
|
||||
|
||||
The corrected host boundary derives a non-secret, device-scoped profile ID from
|
||||
the selected SSID. The reviewed client contains per-device `WiFiAP_SSID` and
|
||||
@@ -110,13 +180,40 @@ that opaque source before any BLE write. A missing provider fails closed. The
|
||||
browser, API, argv, logs, manifests and evidence never receive the secret; the
|
||||
importer's short-lived mutable buffer is zeroized after the Keychain handoff.
|
||||
|
||||
The 2026-08-06 field regression established that process identity is part of
|
||||
this prepared-host contract. Runtime `swiftc` compilation produced an ad-hoc
|
||||
helper with an unstable designated identity. macOS then requested Keychain
|
||||
authorization repeatedly and the same process context failed to expose the
|
||||
exact K1 SSID through CoreWLAN even after K1 had acknowledged AP-ready. That
|
||||
runtime-compiled route is rejected. The laboratory adapter uses the previously
|
||||
physically accepted Apple-signed interpreter path,
|
||||
`/usr/bin/xcrun swift <reviewed-source>`, and validates the source path before
|
||||
launch. A portable product implementation still requires a packaged,
|
||||
precompiled and properly signed helper with a stable bundle identifier,
|
||||
designated requirement, Location/CoreWLAN authorization and Keychain ACL; the
|
||||
current prepared-host path does not claim that packaging work is complete.
|
||||
|
||||
Before any BLE write, the helper's preflight is non-interactive. It first checks
|
||||
Keychain item existence through metadata, then validates the selected profile's
|
||||
SSID and `exact-firmware-profile` provenance inside the helper without returning
|
||||
secret data. Provider material is also read with interaction disabled if a
|
||||
missing device profile must be materialized. The association phase accepts only
|
||||
that already materialized exact profile. It never
|
||||
falls back to the system Wi-Fi Keychain, rewrites a profile opportunistically,
|
||||
or opens a password/authorization dialog after K1 has changed network state.
|
||||
An unavailable or unauthorized profile therefore fails closed with a precise
|
||||
reason code and no automatic device retry.
|
||||
|
||||
The host-network boundary, rather than the XGRIDS frontend, owns platform
|
||||
association. Browsers expose no Wi-Fi join API, and Apple's iOS
|
||||
`NEHotspotConfiguration` consent flow is unavailable on macOS. The current
|
||||
implementation therefore uses a short-lived Swift/CoreWLAN + macOS Keychain
|
||||
helper; Windows Credential Manager and Linux Secret Service adapters remain
|
||||
separate platform work. The helper performs repeated read-only exact-SSID scans
|
||||
inside one 15-second discovery window and at most one association. It never
|
||||
inside one 30-second discovery window and at most one association. The larger
|
||||
window covers the physically observed 18.142-second beacon-discovery case;
|
||||
AP-ready confirms K1 state but does not prove that macOS has already observed
|
||||
the RF beacon. It never
|
||||
repeats the BLE command, guesses a password or treats `7f01` as a credential-read
|
||||
command. The credential-bearing 99-byte station-provisioning frame and fixed
|
||||
100-byte AP-enable frame are separate reviewed payloads.
|
||||
@@ -128,12 +225,13 @@ The owner also observed no explicit device/account pairing in the normal
|
||||
LixelGo onboarding flow; this is consistent with a firmware-defined AP secret,
|
||||
but does not establish account-wide authorization for arbitrary scanners.
|
||||
|
||||
Connection verification refreshes the session-scoped lease with the same
|
||||
read-only BLE status operation. It does not write a characteristic, re-provision
|
||||
Apply may read BLE baseline internally while establishing a new selected
|
||||
session. Selection never does so, and the normal flow has no mandatory or hidden
|
||||
"verify without write" recovery step. The baseline read does not re-provision
|
||||
Wi-Fi, scan the subnet, change a host route, or touch VPN configuration. The
|
||||
later canonical MQTT session supplies the real data-plane connection and live
|
||||
`DeviceInfo` identity check. A BLE lease observation is therefore not by itself
|
||||
a claim that MQTT/RTSP is reachable.
|
||||
later canonical MQTT
|
||||
session supplies the real data-plane connection and live `DeviceInfo` identity
|
||||
check; BLE status alone is not a claim that MQTT/RTSP is reachable.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -148,6 +246,10 @@ a claim that MQTT/RTSP is reachable.
|
||||
therefore not scheduled for this Quick Connect path.
|
||||
- Direct Connect requires an already-running hotspot and a controller route;
|
||||
Mission Core does not create or manage that hotspot.
|
||||
- Discovery never auto-connects devices. Mode, selection and input are local
|
||||
only. App restart, disconnect, explicit stop and mode transition require a
|
||||
fresh explicit scan-select-Apply session. Apply performs no hidden rescan or
|
||||
Verify and may cross at most one device-mutation boundary.
|
||||
- The application-control, START/STOP and raw-first acquisition protocol is
|
||||
unchanged after a target address is admitted.
|
||||
- Direct Connect remains explicitly pending one owner-operated physical
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
# ADR 0014: long-lived macOS host-association observer
|
||||
|
||||
Status: planned production boundary; software contract may be developed behind
|
||||
a disabled feature flag.
|
||||
|
||||
Related acceptance item: `CONN-66` in
|
||||
[`../20_K1_CONNECTION_SUPERVISION_CANON.md`](../20_K1_CONNECTION_SUPERVISION_CANON.md).
|
||||
|
||||
## Context
|
||||
|
||||
Mission Core must distinguish a K1 that is configured for a network from a Mac
|
||||
that is currently attached to the same network. Route, TCP, DeviceInfo,
|
||||
control and data evidence are bound to a host-path epoch; a Wi-Fi switch,
|
||||
sleep/wake cycle or observer restart must invalidate that epoch before any late
|
||||
TCP/MQTT result can restore command authority.
|
||||
|
||||
The current laboratory implementation is fail-closed but not a production
|
||||
observer. One normal connection-monitor poll samples the host path before and
|
||||
after its TCP probe. Each sample synchronously invokes:
|
||||
|
||||
```text
|
||||
/usr/bin/xcrun swift plugins/xgrids-k1/macos/associate_wifi.swift
|
||||
```
|
||||
|
||||
under one process-local lock with a 30-second timeout. At the one-second
|
||||
monitor interval this can launch two Swift processes per second. A failed
|
||||
cycle can occupy the lock for roughly sixty seconds, and cancellation of the
|
||||
Python `asyncio.to_thread()` waiter does not terminate the native process or
|
||||
thread. Physical-command validation shares this observation path. Shorter
|
||||
timeouts, cached shell output or automatic fallback would hide rather than
|
||||
remove the lifecycle defect.
|
||||
|
||||
## Decision
|
||||
|
||||
Production host-association evidence will come from one signed, long-lived,
|
||||
read-only agent in the user's macOS login session.
|
||||
|
||||
- The agent owns one `CWWiFiClient` for its process lifetime.
|
||||
- It observes CoreWLAN link/association/power events and macOS sleep/wake.
|
||||
- It never scans BLE, changes Wi-Fi, reads K1 credentials, reconnects MQTT or
|
||||
sends START/STOP.
|
||||
- It is packaged in a minimal container app and registered with `SMAppService`;
|
||||
it is not a `LaunchDaemon` and is not launched through `xcrun` at runtime.
|
||||
- The required Wi-Fi event entitlement and Location authorization are checked
|
||||
before K1 network mutation is offered. Missing authorization produces
|
||||
explicit unavailable evidence, not a crash loop or guessed association.
|
||||
- The existing Wi-Fi mutator remains a separate component under the exclusive
|
||||
network process lease. Observer authority and mutation authority are never
|
||||
combined.
|
||||
|
||||
The backend communicates with the observer through bounded local IPC. Each
|
||||
backend session supplies a random HMAC key. SSID and BSSID remain inside the
|
||||
agent; only an opaque continuity token is returned and it cannot be correlated
|
||||
between backend processes. The token material is interface plus BSSID; SSID is
|
||||
used only to report evidence quality. This keeps one AP identity stable when
|
||||
macOS alternates between `ssid+bssid` and `bssid-only` disclosure.
|
||||
|
||||
## Observer contract
|
||||
|
||||
```text
|
||||
schema_version: missioncore.macos-host-association/v2
|
||||
agent_instance_id: random 128-bit process instance
|
||||
sequence: uint64
|
||||
association_epoch: uint64
|
||||
interface_name: string | null
|
||||
wifi_interface: true | false | null
|
||||
state: associated | not-associated | inactive | not-wifi | unavailable
|
||||
evidence_quality: ssid+bssid | bssid-only | not-wifi | unavailable
|
||||
continuity_token: 64 lowercase hex | null
|
||||
reason_code: string | null
|
||||
observed_monotonic_ns: uint64
|
||||
sample_age_ms: uint32
|
||||
cause: initial | link-change | association-change | power-change |
|
||||
permission-change | will-sleep | did-wake | poll-correction |
|
||||
observer-restart
|
||||
```
|
||||
|
||||
`sequence` changes for every event or heartbeat. `association_epoch` changes
|
||||
when interface, power, state, SSID or BSSID changes. Sleep and wake each create
|
||||
a barrier even if the visible network looks unchanged afterward. A new agent
|
||||
instance, IPC reconnect, sequence rollback/gap, malformed frame or timeout is
|
||||
also a discontinuity.
|
||||
|
||||
The backend adds its own `observer_session_epoch`; the effective host-route
|
||||
fingerprint includes the agent instance, observer session, association epoch
|
||||
and opaque token. A response from an old session or sequence is discarded.
|
||||
Unknown schema/state or incomplete evidence is `unavailable` and immediately
|
||||
revokes host authority.
|
||||
|
||||
## Timing and failure semantics
|
||||
|
||||
- Heartbeat: 1 second.
|
||||
- Maximum cached-snapshot age: 750 ms.
|
||||
- Snapshot RPC deadline: 250 ms.
|
||||
- Initial handshake deadline: 2 seconds.
|
||||
- Two missed heartbeats or one invalid IPC frame revoke authority immediately.
|
||||
- Reconnect backoff: 250 ms, 500 ms, 1 s, 2 s, then at most 5 s.
|
||||
- There is no automatic fallback to the Swift source runner.
|
||||
- Agent loss affects only read-only host evidence. It never triggers a K1
|
||||
network write, MQTT reconnect or physical command.
|
||||
- A discontinuity first marks the supervisor host path unavailable and rotates
|
||||
its epoch. Recovery then requires fresh route, TCP and DeviceInfo/control
|
||||
evidence in that order.
|
||||
|
||||
## Delivery phases
|
||||
|
||||
Phase A is safe without signing or a physical K1:
|
||||
|
||||
1. Define the Python observer protocol and validate the v2 schema.
|
||||
2. Add a fake/in-memory transport and backend session/sequence validator.
|
||||
3. Implement immediate epoch invalidation and bounded cached lookup.
|
||||
4. Inject the observer into the monitor behind a disabled feature flag.
|
||||
5. Implement the Swift reducer and local transport as a testable Swift package.
|
||||
6. Test sleep/wake, timeout, event gaps, delayed replies, crash/restart and
|
||||
manual network changes.
|
||||
7. Expose secret-free observer health and next action to the UI.
|
||||
8. Prove 10,000 samples launch no child process and cause no lock starvation.
|
||||
|
||||
Phase B requires the actual Mac signing and permission environment:
|
||||
|
||||
1. Package and register the user-session agent.
|
||||
2. Obtain the Wi-Fi events entitlement and complete Location onboarding.
|
||||
3. Run the observer in shadow mode beside the current fail-closed probe.
|
||||
4. Cut over only after the physical fault matrix and an eight-hour soak show no
|
||||
unexplained divergence.
|
||||
|
||||
## Acceptance gate
|
||||
|
||||
- No `xcrun`, `swift` or `swiftc` occurs on the observer path.
|
||||
- One agent and one CoreWLAN client serve one login session.
|
||||
- Snapshot p99 is below 50 ms, hard deadline 250 ms, monitor-cycle p99 below
|
||||
1.5 seconds.
|
||||
- No mutex is held across native or IPC calls.
|
||||
- Sleep, wake, agent restart, sequence gap and timeout always invalidate the
|
||||
effective host epoch.
|
||||
- Late TCP/DeviceInfo evidence from an old epoch is rejected.
|
||||
- SSID, BSSID and credentials never enter IPC logs, API state or artifacts.
|
||||
- Quick-to-Bridge, Bridge-to-Quick, manual Wi-Fi switch, Wi-Fi off/on,
|
||||
router loss/return with the same SSID/IP, backend restart and Location denial
|
||||
all revoke control authority within two seconds and recover only through
|
||||
fresh route, TCP and DeviceInfo evidence.
|
||||
|
||||
Until this gate passes, the current association probe remains explicitly a
|
||||
laboratory implementation and `CONN-66` remains open.
|
||||
|
||||
## Laboratory containment while Location evidence is hidden
|
||||
|
||||
The source-runner helper can return `association-identity-unavailable` on a
|
||||
connected Mac when macOS privacy rules hide SSID and BSSID from the CLI child
|
||||
process. Rotating a random fallback token on every one-second poll made a
|
||||
stable route and a successful TCP probe mutually impossible: every following
|
||||
sample revoked the preceding endpoint result as a fictitious network switch.
|
||||
|
||||
Until the signed observer above replaces the source runner, the laboratory
|
||||
probe uses one random, process-scoped token for the same interface and
|
||||
unavailable-evidence scope. This is not promoted to association evidence:
|
||||
|
||||
- the public evidence quality remains `unavailable`;
|
||||
- interface, source address, kernel route, availability, a proven different
|
||||
BSSID and process restart remain epoch barriers;
|
||||
- endpoint reachability alone remains `configured-unverified`;
|
||||
- only fresh exact DeviceInfo/control evidence can grant control authority;
|
||||
- `CONN-66`, sleep/wake and same-subnet network-switch acceptance remain open.
|
||||
|
||||
For an already reachable lease whose exact DeviceInfo identity and control
|
||||
session remain healthy, a temporary helper timeout or privacy-limited
|
||||
association sample may retain the preceding proven association fingerprint
|
||||
only while the kernel route fingerprint, interface, source, intent and target
|
||||
are unchanged. That retained sample still performs TCP contact and a second
|
||||
kernel-route check, refreshing only route/TCP observation TTLs. Endpoint loss,
|
||||
control loss, control-proof expiry, target/intent change, a proven association
|
||||
identity change or any raw route change revokes immediately. A
|
||||
`configured-unverified` path does not receive this bridge and remains bounded
|
||||
by the existing technical-failure debounce and transport TTL.
|
||||
|
||||
This containment removes the false per-poll epoch churn observed on the field
|
||||
Mac without claiming that the planned production observer has been delivered.
|
||||
@@ -0,0 +1,309 @@
|
||||
# ADR 0015: explicit K1 recovery beside the one-intent connection flow
|
||||
|
||||
Status: accepted product, recovery and presentation contract; executable
|
||||
coverage and remaining hardware acceptance are tracked in
|
||||
`docs/k1-connection-acceptance.manifest.json`.
|
||||
|
||||
Related acceptance items: `CONN-16` through `CONN-19`, `CONN-28`, `CONN-29`,
|
||||
`CONN-65`, and `CONN-68` through `CONN-78` in
|
||||
[`../20_K1_CONNECTION_SUPERVISION_CANON.md`](../20_K1_CONNECTION_SUPERVISION_CANON.md).
|
||||
|
||||
## Problem
|
||||
|
||||
Loss of K1 power, the router, Mac Wi-Fi, MQTT control or the backend does not
|
||||
prove whether K1 is physically scanning. Retained points, an open TCP port and a
|
||||
historical START are insufficient. Replaying START or STOP after an ambiguous
|
||||
dispatch boundary can create a second physical edge.
|
||||
|
||||
The durable physical-command ledger, exact read-only classification and
|
||||
fail-closed supervisor must remain. They must not make ordinary connection slow
|
||||
or surprising. In particular, selecting a device must not secretly connect,
|
||||
Verify, retire/reopen history or delay network credentials.
|
||||
|
||||
## Decision
|
||||
|
||||
### Existing product surface
|
||||
|
||||
K1 connection stays in the existing device plugin section headed
|
||||
**Подключение XGRIDS LixelKity K1**. The surrounding job, entity and lifecycle
|
||||
models do not change. This is novelty A: an improvement to an existing product
|
||||
surface. A separate wizard, modal flow and mandatory preflight/recovery surface
|
||||
are rejected.
|
||||
|
||||
The section reuses canonical shared `Button`, `TextField`, `ActivityIndicator`
|
||||
and `StatusBadge`. It creates no shared entity and uses no raw local HTML
|
||||
controls or literal local status colors.
|
||||
|
||||
### One-intent normal flow
|
||||
|
||||
The normal flow is:
|
||||
|
||||
1. choose Bridge, Direct Connect or Quick Connect locally;
|
||||
2. press the explicit Bluetooth search action;
|
||||
3. wait for exactly one six-second discovery;
|
||||
4. press **Выбрать** on one result;
|
||||
5. enter Bridge/Direct credentials immediately, or review the Quick Connect
|
||||
summary;
|
||||
6. press **Применить** once.
|
||||
|
||||
Opening the section, changing mode, selecting a row and every
|
||||
SSID/password keystroke perform zero browser-controller, device or host I/O.
|
||||
They create no backend operation and show no operation loader. An admitted fresh
|
||||
selection retains the selected card and exposes applicable inputs immediately.
|
||||
A candidate without current draft authority is omitted or presented only as
|
||||
non-actionable evidence; it never receives a misleading disabled primary.
|
||||
|
||||
Each explicit search owns exactly one bounded six-second discovery. It performs
|
||||
no connect, Verify, selection or mutation. Results are never auto-selected.
|
||||
|
||||
One Apply owns the normal connection intent. It may commit the local desired-mode
|
||||
draft under backend CAS and may cross at most one reviewed K1 mutation boundary.
|
||||
Its frontend handler performs no hidden Scan, Verify, reconnect, retirement,
|
||||
reopen, candidate substitution or retry. Quick, Bridge and Direct use the same single primary
|
||||
**Применить** action; credentials are required only for Bridge and Direct.
|
||||
|
||||
Ordinary Bridge Apply never opts into changing the controlling Mac's Wi-Fi
|
||||
association. Host switching is a separate future consequential operator action,
|
||||
not an Apply substep. K1 provisioning can therefore succeed as
|
||||
`network_applied` while control is `control_not_ready`. That result must not
|
||||
repeat that intent's BLE write. Recommended separately explicit read-only
|
||||
Verify/recovery may establish route, endpoint and DeviceInfo/control evidence
|
||||
for the applied topology; a new intent remains separately policy-gated.
|
||||
|
||||
The exact REST response owns completion of the Apply mutation. A snapshot with
|
||||
`connection_attempt.phase=network_applied` is accepted immediately when
|
||||
`control_state` is `control_not_ready` or `unknown`; the controller does not
|
||||
wait for WebSocket/poll convergence or call the full connection-ready
|
||||
requirement. The service may continue supervised same-intent control bootstrap
|
||||
after this fast durable ACK, but only read-only: no BLE/host mutation, mutation
|
||||
retry, new UI action or second Apply. This is not a hidden frontend Scan or
|
||||
Verify. Exact connection-ready remains mandatory before control or physical
|
||||
START. This separation spends the old intent before a delayed state channel
|
||||
could invite its duplicate replay. `connection_attempt` is a read model, not
|
||||
permanent lifecycle authority; current server policy may admit a separately
|
||||
explicit new intent.
|
||||
|
||||
While the exact service-owned bootstrap child is `accepted` or `running` and
|
||||
projects `safe_next_action=wait-for-current-attempt`, the UI shows only one
|
||||
passive **Сеть настроена · подтверждаем управление** indicator. It enables no
|
||||
Verify, mode change, Scan, row or Apply action. Terminal unready/unknown child
|
||||
state then exposes the separately explicit policy-gated recovery choices.
|
||||
|
||||
`network_applied` plus unready or unknown control spends the old Apply and gates
|
||||
ordinary mode change, Scan, row selection and Apply. It first waits passively
|
||||
for an exact active service child; after terminal settlement it presents an
|
||||
explicit recovery choice, regardless of browser-local mode. It never authorizes
|
||||
automatic or same-intent replay. Recommended Verify is pinned to the backend
|
||||
`serverBound` current/configured transport and mode; it never falls back to a
|
||||
selected browser row and is not a prerequisite for every new intent.
|
||||
|
||||
Current server policy may admit a distinct, explicit new-intent path. Bridge
|
||||
uses `prepare-select-device`, a local-only CAS with zero device/host I/O; only
|
||||
after its success may the operator initiate a fresh six-second Scan. Quick and
|
||||
Direct use explicit policy-gated `scan-ble`, then the latest fresh row and a new
|
||||
idempotency Apply. A mode change requires backend `mode_selection` authority and
|
||||
then a fresh explicit Scan. No recovery choice performs hidden Scan, selection,
|
||||
Verify, provisioning or continuation of the old Apply, and the browser never
|
||||
manufactures authority.
|
||||
|
||||
### Freshness and outcome semantics
|
||||
|
||||
Apply is admitted only for the exact selected transport, completed discovery
|
||||
generation, backend runtime, desired-mode revision, reconfiguration intent and
|
||||
policy snapshot. Authority drift before dispatch is a terminal, zero-device-I/O
|
||||
`stale` result. The UI keeps the result understandable, labels it explicitly and
|
||||
offers a new explicit six-second search. It never starts that search itself.
|
||||
|
||||
A failure before the reviewed mutation boundary is `not-dispatched` or
|
||||
`failed`, with zero K1 mutation. A lost response, timeout, power failure or
|
||||
process death after dispatch is `outcome-unknown`, with
|
||||
`safe_to_retry=false`. The durable network-attempt ledger prevents replay.
|
||||
Credentials are never reused automatically. A later operator Apply is a new
|
||||
intent and must pass all current gates.
|
||||
|
||||
## Physical safety remains separate
|
||||
|
||||
Network attempts are disposable; physical START/STOP ambiguity is durable:
|
||||
|
||||
- START and STOP never replay automatically;
|
||||
- control loss does not prove scanning stopped;
|
||||
- local receiver/camera/ingress cleanup is not physical STOP;
|
||||
- a wrong K1/transport/profile/project cannot reconcile the record;
|
||||
- READY records cessation without rewriting historical command outcome;
|
||||
- exact same-project SCANNING may mint one single-use confirmed STOP permit on
|
||||
the still-open exact control binding;
|
||||
- accepted STOP without READY or SCAN_STOPPING by the backend deadline closes
|
||||
only host-owned resources, records `timed_out` / `standby-unknown`, preserves
|
||||
the unresolved ledger and keeps every mutation fenced.
|
||||
|
||||
The composite supervisor and physical-command ledger can disable Apply before
|
||||
device I/O. Their denial does not turn mode, selection or input into recovery.
|
||||
|
||||
### Explicit read-only recovery
|
||||
|
||||
Recovery is a distinct, explicitly requested exceptional action. It is never a
|
||||
continuation of row selection or Apply. The browser supplies neither endpoint,
|
||||
substitute transport nor ledger authority. The backend pins the durable record's
|
||||
exact transport, identity/profile, operation/revision, acquisition/project,
|
||||
topology revision and host epoch.
|
||||
|
||||
The non-reconnecting observation is:
|
||||
|
||||
```text
|
||||
topology-probed
|
||||
-> pre-start-control-opened
|
||||
-> device-info-requested (ordinal 1; exactly one publish)
|
||||
-> device-info-verified
|
||||
-> awaiting-passive-fresh-status
|
||||
-> cessation | active-same-project | foreign-active | inconclusive | failed
|
||||
```
|
||||
|
||||
It publishes exactly one canonical DeviceInfo request and then accepts only a
|
||||
fresh non-retained DeviceStatus from the same socket generation after that
|
||||
barrier. It publishes no status solicitation, DeviceConfig, time sync,
|
||||
workspace, project, START or STOP; it never scans, reconnects, provisions or
|
||||
continues into Apply.
|
||||
|
||||
Canonical READY records cessation/standby. Initialized SCANNING may rebind only
|
||||
when operation/acquisition, identity/profile, transport, host epoch and project
|
||||
all match; it exposes one separate single-use confirmed STOP checkpoint.
|
||||
Foreign, stale or inconclusive evidence changes no topology or authority.
|
||||
|
||||
### Explicit retirement and reopen
|
||||
|
||||
`physical-command.retire-unavailable` is a separately confirmed local durable
|
||||
recovery action for one unresolved target that is truly unavailable or replaced.
|
||||
Admission requires stable idempotency identity and exact backend runtime,
|
||||
operation, ledger revision and transport CAS plus safe lifecycle ownership. It
|
||||
preserves the original unknown outcome, activates the exact-transport deny,
|
||||
performs zero device/host I/O and starts no discovery.
|
||||
|
||||
`physical-command.reopen-retired-reconciliation` is also separately confirmed.
|
||||
It requires an exact fresh same-transport candidate, stable `reopening_id`, exact
|
||||
runtime/revision/retirement/transport/discovery CAS and safe lifecycle ownership.
|
||||
It preserves retirement audit, removes only that retirement's active deny and
|
||||
performs zero device/host I/O. The explicit recovery intent may then run one
|
||||
exact read-only observation. **Выбрать** never invokes retirement, reopen or
|
||||
Verify. The only Apply exception is an internal, request-bound local reopen
|
||||
checkpoint for an explicit scenario reset plus its exact successor Scan. It is
|
||||
ordered after network PREPARED and before the sole dispatch edge, remains
|
||||
invisible in the wizard and grants no command authority. The same applied
|
||||
intent may then settle it read-only from fresh DeviceInfo plus non-retained
|
||||
READY/SCANNING evidence.
|
||||
|
||||
FW 3.0.2 BLE `7f02` contains no stable DeviceInfo identity. Mission Core cannot
|
||||
prove during BLE-only discovery that the same physical unit has a new
|
||||
CoreBluetooth UUID. This remains an explicit protocol/hardware gap.
|
||||
|
||||
### Bounded durable audit rollover
|
||||
|
||||
An explicit local scenario reset must not become unavailable merely because
|
||||
closed retire/reopen history filled the 64 KiB hot ledger. Before a transition
|
||||
would exceed that bound, Mission Core durably publishes the complete previous
|
||||
ledger as a private, owner-only, content-addressed archive segment and then
|
||||
atomically publishes a compact v4 main record. The main record retains every
|
||||
active retirement deny, the newest lost-response retire/reopen checkpoint, and
|
||||
all reconciliation/confirmation proof required by the current physical
|
||||
operation. Compaction never changes a device outcome and performs no device,
|
||||
network or host I/O.
|
||||
|
||||
Archive segments form a predecessor hash chain with exact sequence and byte
|
||||
accounting. Reload verifies directory and file ownership/mode, rejects symlink
|
||||
traversal, bounds total segments and bytes, reparses every embedded ledger and
|
||||
fails closed for a missing, replayed, reordered or tampered segment. Operation,
|
||||
reconciliation, verification, confirmation, retirement and reopening identities
|
||||
remain globally one-use across the hot record and archive. The archive segment
|
||||
is fsynced before the main-file replace: a crash may leave only an inert orphan,
|
||||
while retry of the same CAS reuses identical bytes and cannot duplicate the
|
||||
referenced chain.
|
||||
|
||||
Scenario reset asks the ledger to build the exact prospective retirement or
|
||||
prepared→not-dispatched plan before closing any local receiver, camera,
|
||||
control-session or network ownership. That shared planner applies the same hot
|
||||
serialization, compaction, segment, count and total-byte bounds as commit. When
|
||||
rollover is required, preflight may idempotently prepublish only the immutable
|
||||
content-addressed predecessor; the main revision/CAS and physical disposition
|
||||
remain unchanged. This also proves owner/mode, symlink and content-collision
|
||||
conditions before teardown.
|
||||
|
||||
Archive publication is restart-safe at the hard-link boundary. A process death
|
||||
after destination link and directory fsync but before temporary-name unlink may
|
||||
leave exactly two private names for one inode. Retry removes only a strictly
|
||||
named, owner-only temporary alias whose bytes and inode exactly match the
|
||||
expected destination and whose link count is exactly two, fsyncs that cleanup,
|
||||
then reuses the destination. Any unrelated hard link, extra temporary, symlink,
|
||||
metadata mismatch or byte mismatch remains a fail-closed corruption condition.
|
||||
|
||||
## Failure and restart semantics
|
||||
|
||||
- UI entry, mode, selection, input, polling, refresh and layout changes
|
||||
start no device operation.
|
||||
- Search starts only when pressed, runs once for six seconds and terminalizes.
|
||||
- Apply starts only when pressed, uses one exact fresh candidate and may perform
|
||||
at most one K1 mutation.
|
||||
- Candidate/runtime/intent drift is explicit stale, never hidden rescan.
|
||||
- Post-dispatch uncertainty is explicit outcome-unknown, never automatic replay.
|
||||
- K1 power loss revokes the active session without inventing standby.
|
||||
- Wi-Fi loss and WAN loss are distinct: local LAN control may survive WAN loss;
|
||||
route/association loss revokes only dependent host/control evidence.
|
||||
- Browser refresh restores no live local selection and causes no I/O.
|
||||
- Backend restart restores durable audit and safety ledgers, but no live BLE,
|
||||
control or operator intent.
|
||||
- Mac sleep/restart rotates host/runtime authority and rejects late work.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Mode, selection and input result in zero controller calls.
|
||||
- Each Search click issues exactly one scan with duration `6`; no effect, timer,
|
||||
selection or Apply path calls Scan.
|
||||
- Every result keeps the same ordinary **Выбрать** action. Selection retains
|
||||
the card, shows applicable inputs immediately, shows no loader and calls no
|
||||
controller. After an explicit committed scenario reset and its successfully
|
||||
completed successor Scan, this includes the exact UUID used by the retired
|
||||
prior scenario; the row never exposes a reconnect/reopen/Verify CTA.
|
||||
- During unresolved physical recovery, a completed explicit Scan still renders
|
||||
passive BLE evidence but cannot substitute a foreign target for the durable
|
||||
recovery record. Exact recovery remains a separate established-session
|
||||
action outside the cold result list; ordinary **Выбрать** never invokes its
|
||||
reopen or read-only Verify. A new network flow first requires explicit reset
|
||||
and a successor Scan.
|
||||
- Bridge/Direct show SSID/password; Quick Connect does not.
|
||||
- Exactly one primary **Применить** owns the connection request. Its frontend
|
||||
handler calls no Scan/Verify/reopen helper and it permits at most one device
|
||||
mutation. For an exact reset-owned retired UUID, the backend may append only
|
||||
the internal local settlement checkpoint described above before dispatch.
|
||||
A later SCANNING settlement grants only explicit STOP authority and never
|
||||
restarts the reset-owned receiver, camera, writer or acquisition.
|
||||
- Stale/pre-dispatch and unknown/post-dispatch outcomes are visibly distinct.
|
||||
- Applied-but-unready/unknown spends the old Apply and gates ordinary mode,
|
||||
Scan, selection and Apply behind an explicit recovery choice; recommended
|
||||
Verify has only a server-bound backend target and no browser fallback.
|
||||
- A new intent remains possible only through current backend policy. Bridge
|
||||
uses explicit local-only `prepare-select-device`; Quick/Direct use an explicit
|
||||
admitted Scan and latest fresh row; mode change requires `mode_selection`.
|
||||
Each route starts no hidden frontend or mutating continuation and ends in a
|
||||
later fresh Scan/new idempotency Apply. The declared service-owned
|
||||
same-intent read-only bootstrap after the durable ACK is the sole continuation
|
||||
exception and creates no UI action.
|
||||
- The exact Apply REST snapshot with `phase=network_applied` completes the
|
||||
network intent for both `control_not_ready` and `unknown`, without requiring
|
||||
connection-ready or waiting for WebSocket/poll convergence.
|
||||
- A service-owned supervised control bootstrap may continue read-only after
|
||||
that ACK. It performs no BLE/host mutation or retry and creates no frontend
|
||||
Scan/Verify/new-Apply action or blocking Apply loader. Its exact
|
||||
accepted/running state may own one passive settling indicator only.
|
||||
- Operator error copy comes only from an allowlisted public error-code mapping;
|
||||
unknown/raw messages use a canonical secret-free fallback and never render
|
||||
credentials, SSIDs, payloads or stack traces.
|
||||
- No timeout, disconnect, refresh, restart or state update starts a continuation
|
||||
or replays an ended action.
|
||||
- Supervisor, identity pin, network-attempt ledger, physical-command ledger,
|
||||
process/BLE lease and one-use recovery STOP remain authoritative.
|
||||
- The plugin uses shared `Button`, `TextField`, `ActivityIndicator` and
|
||||
`StatusBadge`; contract tests reject raw local controls and literal colors.
|
||||
- Geometry and long-copy tests keep all actions reachable without overlap.
|
||||
- Bridge and Quick Connect retain separate real-hardware acceptance.
|
||||
|
||||
This ADR does not itself declare hardware coverage. The manifest may mark a
|
||||
scenario software-covered only when named executable tests cover the software
|
||||
invariant; remaining K1/macOS/router and Quick Connect gaps stay explicit.
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"schema_version": "missioncore.k1-connection-acceptance/v1",
|
||||
"canonical_document": "docs/20_K1_CONNECTION_SUPERVISION_CANON.md",
|
||||
"meaning": {
|
||||
"software-covered": "The listed automated tests cover the software invariant; this is not hardware acceptance.",
|
||||
"partial": "At least one software layer is covered and an explicit remaining gap is listed.",
|
||||
"planned": "The scenario is specified but does not yet have adequate executable coverage."
|
||||
},
|
||||
"scenarios": [
|
||||
{"id":"CONN-01","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","apps/control-station/test/devicePluginContracts.test.mjs"],"remaining":["real K1 Quick-to-Bridge evidence"]},
|
||||
{"id":"CONN-02","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","apps/control-station/test/devicePluginContracts.test.mjs"],"remaining":["real K1 Bridge-to-Quick evidence"]},
|
||||
{"id":"CONN-03","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["hardware pre-dispatch fault injection"]},
|
||||
{"id":"CONN-04","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["hardware pre-dispatch fault injection"]},
|
||||
{"id":"CONN-05","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","tests/test_xgrids_device_identity_pin_store.py"],"remaining":["two-K1 hardware evidence"]},
|
||||
{"id":"CONN-06","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","tests/test_ble_scanner.py","apps/control-station/test/devicePluginFrontendBoundary.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["CoreBluetooth hardware evidence"]},
|
||||
{"id":"CONN-07","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real K1 wait-beyond-TTL acceptance"]},
|
||||
{"id":"CONN-08","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real app/backend restart reconnect acceptance"]},
|
||||
|
||||
{"id":"CONN-10","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["hardware pre-prepare power-loss fault injection"]},
|
||||
{"id":"CONN-11","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["process-kill acceptance at the prepared boundary"]},
|
||||
{"id":"CONN-12","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real K1 post-dispatch power-loss acceptance"]},
|
||||
{"id":"CONN-13","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real K1 observation-loss evidence"]},
|
||||
{"id":"CONN-14","status":"software-covered","test_files":["tests/test_xgrids_semantic_topology_store.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["hardware host-association loss"]},
|
||||
{"id":"CONN-15","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["hard-power hardware evidence"]},
|
||||
{"id":"CONN-16","status":"partial","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_acquisition_lifecycle.py","tests/test_xgrids_physical_command_ledger.py"],"remaining":["end-to-end passive Scan policy after acquisition power loss","exact-target read-only recovery/rebind integration","Bridge hardware power-loss acceptance","Quick Connect recovery not exercised"]},
|
||||
{"id":"CONN-17","status":"partial","test_files":["tests/test_xgrids_physical_command_ledger.py"],"remaining":["transport dispatch integration","restart acceptance"]},
|
||||
{"id":"CONN-18","status":"partial","test_files":["tests/test_xgrids_physical_command_ledger.py","tests/test_xgrids_application_mqtt.py"],"remaining":["facade exact-target resolved-active rebind","single-use explicit recovery STOP presentation/action integration","same-project Bridge hardware acceptance","Quick Connect recovery not exercised"]},
|
||||
{"id":"CONN-19","status":"partial","test_files":["tests/test_xgrids_physical_command_ledger.py","tests/test_xgrids_application_mqtt.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["facade exact-target resolved-active READY cessation integration","Bridge reboot hardware acceptance","Quick Connect recovery not exercised"]},
|
||||
|
||||
{"id":"CONN-20","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_macos_wifi.py"],"remaining":["router-loss hardware evidence"]},
|
||||
{"id":"CONN-21","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_macos_wifi.py"],"remaining":["same-SSID router-return evidence"]},
|
||||
{"id":"CONN-22","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_macos_wifi.py"],"remaining":["manual macOS switch evidence"]},
|
||||
{"id":"CONN-23","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real Quick AP leave/return"]},
|
||||
{"id":"CONN-24","status":"partial","test_files":["tests/test_connection_supervisor.py","tests/test_ble_scanner.py"],"remaining":["macOS sleep/wake hardware acceptance"]},
|
||||
{"id":"CONN-25","status":"software-covered","test_files":["tests/test_connection_supervisor.py"],"remaining":["route-race integration evidence"]},
|
||||
{"id":"CONN-26","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["wrong-service integration evidence"]},
|
||||
{"id":"CONN-27","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_application_mqtt.py","tests/test_xgrids_application_session.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["half-open MQTT hardware acceptance"]},
|
||||
{"id":"CONN-28","status":"partial","test_files":["tests/test_xgrids_application_mqtt.py","tests/test_xgrids_physical_command_coordinator.py","tests/test_xgrids_physical_command_ledger.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["full facade policy for passive Scan with unknown/active physical state","physical-record transport_ref pinning across bounded observation and wrong-K1 no-topology-change","resolved-active SCANNING one-STOP integration","real K1 passive READY/SCANNING DeviceStatus acceptance"]},
|
||||
{"id":"CONN-29","status":"planned","test_files":[],"remaining":["durable external-active takeover contract","operator-confirmed same-binding STOP"]},
|
||||
|
||||
{"id":"CONN-30","status":"planned","test_files":[],"remaining":["browser/app close clean-session acceptance at every stage"]},
|
||||
{"id":"CONN-31","status":"software-covered","test_files":["tests/test_xgrids_network_mutation_ledger.py","tests/test_xgrids_semantic_topology_store.py"],"remaining":["restart integration acceptance"]},
|
||||
{"id":"CONN-32","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real backend restart acceptance from a prepared network mutation"]},
|
||||
{"id":"CONN-33","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real backend restart acceptance from a dispatching network mutation"]},
|
||||
{"id":"CONN-34","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real backend restart acceptance from an observing network mutation"]},
|
||||
{"id":"CONN-35","status":"software-covered","test_files":["tests/test_xgrids_network_mutation_ledger.py","tests/test_xgrids_semantic_topology_store.py"],"remaining":["restart integration acceptance"]},
|
||||
{"id":"CONN-36","status":"planned","test_files":[],"remaining":["restart acceptance proving no old live session restoration"]},
|
||||
{"id":"CONN-37","status":"planned","test_files":[],"remaining":["corrupt historical audit quarantine without permanent K1 block","operator diagnosis UI"]},
|
||||
{"id":"CONN-38","status":"software-covered","test_files":["tests/test_xgrids_network_mutation_ledger.py","tests/test_xgrids_ble_runtime_arbiter.py"],"remaining":["two-service integration acceptance"]},
|
||||
{"id":"CONN-39","status":"software-covered","test_files":["tests/test_xgrids_network_mutation_ledger.py"],"remaining":[]},
|
||||
|
||||
{"id":"CONN-40","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_application_session.py"],"remaining":["real wrong/failed DeviceInfo evidence"]},
|
||||
{"id":"CONN-41","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["MQTT fault-injection integration"]},
|
||||
{"id":"CONN-42","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["camera and point stall integration"]},
|
||||
{"id":"CONN-43","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["late packet integration evidence"]},
|
||||
{"id":"CONN-44","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_application_session.py"],"remaining":["late DeviceInfo integration evidence"]},
|
||||
{"id":"CONN-45","status":"partial","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_application_control_process_lease.py"],"remaining":["durable physical-command integration"]},
|
||||
{"id":"CONN-46","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","tests/test_xgrids_camera_gateway.py"],"remaining":["combined MQTT/camera late-producer integration"]},
|
||||
{"id":"CONN-47","status":"software-covered","test_files":["tests/test_xgrids_application_control_process_lease.py"],"remaining":["two-service integration acceptance"]},
|
||||
{"id":"CONN-48","status":"software-covered","test_files":["tests/test_xgrids_camera_gateway.py"],"remaining":["drain-timeout integration evidence"]},
|
||||
{"id":"CONN-49","status":"software-covered","test_files":["tests/test_connection_supervisor.py"],"remaining":["long-running fault-injection acceptance"]},
|
||||
|
||||
{"id":"CONN-50","status":"partial","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["macOS sleep/wake hardware acceptance"]},
|
||||
{"id":"CONN-51","status":"software-covered","test_files":["tests/test_xgrids_macos_wifi.py","tests/test_connection_supervisor.py"],"remaining":["compiled association observer"]},
|
||||
{"id":"CONN-52","status":"software-covered","test_files":["tests/test_xgrids_device_identity_pin_store.py","tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["two-identity hardware evidence"]},
|
||||
{"id":"CONN-53","status":"software-covered","test_files":["tests/test_xgrids_semantic_topology_store.py","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["restart browser acceptance"]},
|
||||
{"id":"CONN-54","status":"planned","test_files":[],"remaining":["historical unknown audit does not block fresh explicit connect","restart browser acceptance"]},
|
||||
{"id":"CONN-55","status":"partial","test_files":["apps/control-station/test/devicePluginFrontendBoundary.test.mjs"],"remaining":["automated browser geometry matrix"]},
|
||||
{"id":"CONN-56","status":"partial","test_files":["apps/control-station/test/devicePluginFrontendBoundary.test.mjs"],"remaining":["automated long-copy browser geometry"]},
|
||||
{"id":"CONN-57","status":"planned","test_files":[],"remaining":["operator-confirmed physical-ledger archive and identity rotation"]},
|
||||
{"id":"CONN-58","status":"software-covered","test_files":["tests/test_xgrids_ble_runtime_arbiter.py","tests/test_xgrids_application_control_process_lease.py","tests/test_ble_scanner.py","tests/test_wifi_provisioning.py","tests/test_xgrids_ap_activation.py"],"remaining":["two-service CoreBluetooth hardware acceptance","native cleanup fault injection on macOS"]},
|
||||
{"id":"CONN-59","status":"partial","test_files":["tests/test_xgrids_acquisition_lifecycle.py","tests/test_xgrids_network_provisioning_idempotency_journal.py"],"remaining":["prove failed audit admission releases active ownership for a new explicit attempt"]},
|
||||
{"id":"CONN-60","status":"software-covered","test_files":["apps/control-station/test/devicePluginContracts.test.mjs"],"remaining":["manual browser confirmation-dismissal acceptance"]},
|
||||
{"id":"CONN-61","status":"planned","test_files":[],"remaining":["legacy unresolved record terminalization without BLE or cross-session block","process-kill acceptance"]},
|
||||
{"id":"CONN-62","status":"software-covered","test_files":["tests/test_web_validation_security.py"],"remaining":["manual browser refresh/close acceptance"]},
|
||||
{"id":"CONN-63","status":"planned","test_files":[],"remaining":["composite policy denies active contention but ignores terminal historical network audit","manual policy presentation acceptance"]},
|
||||
{"id":"CONN-64","status":"software-covered","test_files":["tests/test_connection_supervisor.py","tests/test_xgrids_acquisition_lifecycle.py","apps/control-station/test/devicePluginContracts.test.mjs"],"remaining":["real K1 control/data loss acceptance"]},
|
||||
{"id":"CONN-65","status":"partial","test_files":["tests/test_xgrids_acquisition_lifecycle.py","apps/control-station/test/devicePluginContracts.test.mjs","apps/control-station/test/devicePluginFrontendBoundary.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["resolved-active same-project one-STOP UI action acceptance","real restart/browser host-route and passive DeviceStatus acceptance","Quick Connect recovery not exercised"]},
|
||||
{"id":"CONN-66","status":"planned","test_files":[],"remaining":["compiled or long-lived macOS association observer","long-running monitor latency/fault acceptance"]},
|
||||
{"id":"CONN-67","status":"partial","test_files":["tests/test_xgrids_acquisition_lifecycle.py"],"remaining":["real K1 repeated same-mode and cross-mode reconnect acceptance"]},
|
||||
{"id":"CONN-68","status":"partial","test_files":["tests/test_xgrids_application_session.py","tests/test_xgrids_physical_command_ledger.py","tests/test_xgrids_acquisition_lifecycle.py","apps/control-station/test/devicePluginContracts.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["Bridge STOP-ack plus Wi-Fi-loss hardware rerun","Quick Connect recovery not exercised"]},
|
||||
{"id":"CONN-69","status":"partial","test_files":["tests/test_xgrids_physical_command_ledger.py","tests/test_xgrids_acquisition_lifecycle.py","apps/control-station/test/devicePluginContracts.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["resolved-active same-project one explicit STOP browser acceptance","Bridge hardware rerun with redacted evidence","Quick Connect recovery not exercised"]},
|
||||
{"id":"CONN-70","status":"software-covered","test_files":["apps/control-station/test/devicePluginContracts.test.mjs","apps/control-station/test/devicePluginFrontendBoundary.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["real Bridge one-scan/select/immediate-credentials/Apply acceptance","Quick Connect recovery not exercised"]},
|
||||
{"id":"CONN-71","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","apps/control-station/test/devicePluginContracts.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["real Bridge STOP-deadline fault injection","Quick Connect recovery not exercised"]},
|
||||
{"id":"CONN-72","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","tests/test_plugin_runtime.py","apps/control-station/test/devicePluginContracts.test.mjs","apps/control-station/test/devicePluginFrontendBoundary.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["real Bridge existing-plugin-section acceptance","manual two-tab browser acceptance","Quick Connect live acceptance remains separate"]},
|
||||
{"id":"CONN-73","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","tests/test_xgrids_semantic_topology_store.py","tests/test_xgrids_device_identity_pin_store.py"],"remaining":["real cold Bridge and two-K1 identity-mismatch/restart evidence","Quick Connect live acceptance remains separate"]},
|
||||
{"id":"CONN-74","status":"software-covered","test_files":["tests/test_xgrids_acquisition_lifecycle.py","tests/test_xgrids_network_mutation_ledger.py","tests/test_xgrids_network_provisioning_idempotency_journal.py","apps/control-station/test/devicePluginFrontendBoundary.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["real Bridge Apply with no hidden discovery/Verify","post-dispatch hardware fault injection","Quick Connect live acceptance remains separate"]},
|
||||
{"id":"CONN-75","status":"software-covered","test_files":["tests/test_xgrids_connection_scenario_reset.py","tests/test_xgrids_acquisition_lifecycle.py","tests/test_xgrids_application_control_process_lease.py","apps/control-station/test/devicePluginFrontendBoundary.test.mjs"],"remaining":["real disconnected/idle desired-mode draft no-I/O acceptance with unresolved durable physical history plus live-owner denial","real pre-START orphan and backend-runtime credential invalidation acceptance","manual top-right emergency-reset acceptance","Quick Connect live acceptance remains separate"]},
|
||||
{"id":"CONN-76","status":"partial","test_files":["tests/test_xgrids_physical_command_ledger.py","tests/test_xgrids_physical_command_coordinator.py","tests/test_xgrids_acquisition_lifecycle.py","tests/test_xgrids_application_control_process_lease.py","tests/test_xgrids_ble_runtime_arbiter.py","tests/test_xgrids_camera_gateway.py","tests/test_cli.py","tests/test_plugin_runtime.py","apps/control-station/test/devicePluginFrontendBoundary.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["real separate explicit retirement confirmation while selection and Apply remain mutation-free","same-hardware/new-CoreBluetooth-UUID cannot be identified before provisioning because FW 3.0.2 BLE 7f02 exposes no stable DeviceInfo identity","Quick Connect live acceptance remains separate"]},
|
||||
{"id":"CONN-77","status":"partial","test_files":["tests/test_xgrids_physical_command_ledger.py","tests/test_xgrids_physical_command_coordinator.py","tests/test_xgrids_acquisition_lifecycle.py","tests/test_plugin_runtime.py","apps/control-station/test/devicePluginFrontendBoundary.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["real retired exact-UUID selection remains local and Apply denied","real separately explicit reopen followed by READY and same-project SCANNING outcomes","Quick Connect live acceptance remains separate"]},
|
||||
{"id":"CONN-78","status":"software-covered","test_files":["apps/control-station/test/devicePluginFrontendBoundary.test.mjs","apps/control-station/test/k1SupervisorPresentation.test.mjs"],"remaining":["manual Bridge and Quick one-intent timing acceptance","manual top-right idle/pending accessible-label acceptance","real stale-before-dispatch and unknown-after-dispatch fault injection","real fast REST network_applied plus delayed service-owned read-only control-bootstrap convergence","real Bridge prepare-select-device and Quick/Direct scan-new-intent recovery acceptance","manual canonical shared-control visual acceptance"]}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
# K1 connection lifecycle and recovery runbook
|
||||
|
||||
Canonical model: [`../20_K1_CONNECTION_SUPERVISION_CANON.md`](../20_K1_CONNECTION_SUPERVISION_CANON.md).
|
||||
This runbook is the operator-facing projection of that model. Technical terms in
|
||||
the internal-safety sections are engineering evidence; they are not wizard copy.
|
||||
|
||||
## One operator wizard
|
||||
|
||||
The connection surface is one progressive wizard, not a recovery dashboard.
|
||||
Its only model-bearing heading is **Подключение XGRIDS LixelKity K1**. Inside
|
||||
the wizard the two step names are exactly **Подключение** and **Сеть**.
|
||||
|
||||
### Cold entry
|
||||
|
||||
On a clean cold entry show only:
|
||||
|
||||
- the connection-mode selector;
|
||||
- Step 01 **Подключение** with the explicit Bluetooth search action.
|
||||
|
||||
Historical K1 identity never adds a reconnect choice to cold entry. If local
|
||||
session ownership or an older connection scenario exists, one explicit
|
||||
`reset_scenario` CAS first closes only that local scenario. Only after the reset
|
||||
is accepted does clean Step 01 expose **Найти по Bluetooth** as a separate
|
||||
click; reset never starts Scan itself.
|
||||
|
||||
Do not render Step 02 yet and do not start discovery automatically. The mode
|
||||
selector and same-mode **Подключить новый K1** escape remain available through
|
||||
every other lifecycle state. The first gesture may supersede another pending
|
||||
local action. While that one bounded scenario reset itself owns `mode`, all
|
||||
three reset entry points show pending and dispatch no B intent. Each accepted
|
||||
gesture sends one idempotent local `reset_scenario` CAS: it queues behind old
|
||||
local lifecycle ownership, seals retained receiver/camera/control resources,
|
||||
invalidates candidates/drafts/credentials and retires the old physical lineage
|
||||
without resolving its outcome. It sends no BLE, device or host network command,
|
||||
MQTT publish, Verify, provisioning, START, STOP or automatic Scan. Opening and
|
||||
polling the surface still do nothing. Scan, Verify, provisioning and START
|
||||
retain separate backend gates.
|
||||
|
||||
The top-right refresh-shaped utility is the same explicit emergency reset, not
|
||||
a passive state refresh. Its accessible label is **Сбросить подключение**; while
|
||||
the request owns the current action it reads **Сбрасываем подключение** and
|
||||
does not dispatch a second reset until that bounded request settles. It remains
|
||||
available to supersede any other local action. The accepted revision always
|
||||
returns the new local scenario and even a dirty browser selector to canonical
|
||||
**Bridge**, clears the old browser/backend presentation and source owners, and
|
||||
leaves Scan as a separate click. It performs no hidden Scan, Verify, Connect,
|
||||
START, STOP, BLE or network write and never substitutes a passive `state.read`
|
||||
for the reset mutation. After settlement the product surface contains no prior
|
||||
UUID, result count, **Повторить поиск**, reconnect error, selected device,
|
||||
credentials or recovery card; it returns to **Найти по Bluetooth**. Late
|
||||
Scan/Verify settlements from the retired scenario cannot repopulate it.
|
||||
The retained reset marker fences only work that belonged to the retired
|
||||
scenario. A newly correlated post-reset network attempt that fails or has an
|
||||
unknown outcome must immediately render its current recovery/error surface;
|
||||
reload must neither hide that new failure nor resurrect the prior prompt.
|
||||
|
||||
### Step 01 — Подключение
|
||||
|
||||
Step 01 **Подключение** is visible immediately.
|
||||
|
||||
1. Bluetooth discovery starts only after the operator presses the search
|
||||
action.
|
||||
2. For the full bounded search, show an activity indicator and a visible
|
||||
seconds countdown in the same step.
|
||||
3. After search completes, show the result count or the empty result. Every
|
||||
connectable result row keeps the same one enabled **Выбрать** action,
|
||||
including an exact UUID used in an earlier scenario. That action is
|
||||
local-only and never invokes reopen or Verify. Fresh results never render a
|
||||
reconnect CTA, a disabled competing primary or an old/new-device decision.
|
||||
A successful admitted Scan first settles only the reset marker whose
|
||||
id/revision/mode it captured at action entry, recording the later admitted
|
||||
discovery generation while retaining the marker for idempotent reset replay.
|
||||
Failed/cancelled Scan and an older Scan racing a newer pending reset leave
|
||||
the marker active and do not expose stale recovery authority.
|
||||
4. After ordinary **Выбрать**, retain the chosen device card and reveal only
|
||||
the applicable local draft inputs. Selection itself has no loader and makes
|
||||
no controller call.
|
||||
5. Only an authoritative successful connection outcome renders Step 01 green
|
||||
as **Подключение установлено** and advances the normal connection flow.
|
||||
|
||||
The wizard never labels a candidate as saved, original, retired or physically
|
||||
ambiguous and never exposes ledger, CAS, retirement, reopen or reconnect
|
||||
terminology in the search list. Exact recovery belongs only to a previously
|
||||
established session after an actual interruption.
|
||||
|
||||
### Step 02 — Сеть
|
||||
|
||||
Step 02 **Сеть** exists only after Step 01 has a confirmed green connection.
|
||||
|
||||
- If the selected device is already usable on the chosen connection path,
|
||||
show the network result without asking for credentials.
|
||||
- If backend policy has safely admitted explicit network setup, show the exact
|
||||
retained device context and SSID/password fields here, never beside the
|
||||
candidate list.
|
||||
- One explicit submit owns any exact hidden revalidation and at most one
|
||||
reviewed network write.
|
||||
- **Изменить сеть** belongs only to this step and starts no Bluetooth search
|
||||
when its form opens.
|
||||
- A stale tab, changed runtime/binding or policy denial fails before a write and
|
||||
never exposes a foreign candidate.
|
||||
|
||||
## Ordinary selection
|
||||
|
||||
The ordinary **Выбрать** action is presentation simplification, not relaxed
|
||||
safety. It creates only a browser-local candidate draft and performs no
|
||||
controller I/O. It never selects an internal recovery path:
|
||||
|
||||
- an ordinary fresh candidate becomes only the selected local draft;
|
||||
- an exact prior candidate uses the same **Выбрать** action as every row;
|
||||
- no candidate selection retires old authority, reopens a ledger record, calls
|
||||
Verify, connects GATT, scans again or changes topology or the device;
|
||||
- a foreign, stale, non-connectable or policy-denied candidate remains
|
||||
unavailable and changes no topology, ledger or device.
|
||||
|
||||
After an explicit committed scenario reset, only its successfully completed
|
||||
successor Scan may make an exact previously retired transport eligible for a
|
||||
new network draft. Apply still captures that exact current-generation handle,
|
||||
validates the live GATT baseline and crosses at most one reviewed write edge.
|
||||
Selection performs no recovery action. At the final Apply boundary, the backend
|
||||
may append one exact request-bound local reopen checkpoint after network
|
||||
PREPARED and before write dispatch. That checkpoint preserves physical
|
||||
retirement/original-outcome audit, performs no device I/O and authorizes no
|
||||
START or STOP. The same applied intent then uses fresh DeviceInfo plus a
|
||||
non-retained DeviceStatus to settle READY as standby or identity-bound SCANNING as
|
||||
active, without a visible Verify step or command replay. In the SCANNING case
|
||||
it materializes only explicit STOP authority; it does not restart the retired
|
||||
receiver, camera, evidence writer or acquisition.
|
||||
|
||||
### Exact internal recovery for an established session
|
||||
|
||||
For an exact target belonging to a previously established session with one
|
||||
active retirement,
|
||||
`physical-command.reopen-retired-reconciliation` requires:
|
||||
|
||||
- `operator_confirmed=true`, bound to a separately explicit session-recovery
|
||||
action outside cold entry and the Bluetooth result list;
|
||||
- a stable `reopening_id` and reason
|
||||
`device-returned-for-explicit-reconciliation`;
|
||||
- exact `expected_snapshot_runtime_id`, `expected_revision`,
|
||||
`expected_retirement_id`, `expected_transport_ref` and
|
||||
`expected_discovery_generation` CAS;
|
||||
- a current connectable candidate and safe lifecycle/process ownership.
|
||||
|
||||
The transaction changes only the local durable ledger. It appends reopen audit,
|
||||
preserves the retirement and unknown command outcome as history, restores the
|
||||
original unresolved `dispatching` or `observing` stage and removes only that
|
||||
retirement's active deny. It performs zero BLE, Wi-Fi, MQTT, DeviceConfig,
|
||||
ModelingStatus, workspace, project, START or STOP I/O and starts no Scan.
|
||||
|
||||
The same still-current session-recovery action may then own one exact read-only
|
||||
Verify. Ordinary **Выбрать** never invokes either half. Recovery never replays
|
||||
historical START/STOP and never silently provisions:
|
||||
|
||||
- fresh non-retained READY resolves standby;
|
||||
- fresh exact same-project SCANNING resolves active and permits only the
|
||||
separately guarded stop path;
|
||||
- identity, GATT, CAS, route/control or policy failure leaves the outcome
|
||||
unknown and ends the established-session recovery without entering the new
|
||||
connection wizard.
|
||||
|
||||
If the action response is lost, refreshed state may continue the same click
|
||||
only when it proves that exact `reopening_id` audit was committed and every
|
||||
original runtime, candidate and authority fence still matches. A second tab,
|
||||
new discovery generation, new retirement or different reopening identity cannot
|
||||
inherit the continuation.
|
||||
|
||||
### Internal retirement
|
||||
|
||||
`physical-command.retire-unavailable` is a local durable primitive for an
|
||||
unresolved target that is truly unavailable or replaced. It may run only from
|
||||
its separately confirmed recovery/reset path, never from ordinary candidate
|
||||
selection, UI entry, polling or a timer. Admission requires explicit
|
||||
confirmation, stable `retirement_id` and exact backend runtime,
|
||||
operation, revision and transport CAS while every local owner is safe.
|
||||
|
||||
Retirement preserves the complete old attempt and unknown command outcome,
|
||||
activates an exact-transport deny and performs zero device I/O or automatic
|
||||
discovery. Retirement history remains durable even if an exact later recovery
|
||||
action uses the reopen transaction. The wizard exposes no retirement
|
||||
transaction or historical label. Any plain-language exact recovery CTA belongs
|
||||
only to the established-session surface when backend authority permits it.
|
||||
|
||||
Current FW 3.0.2 BLE `7f02` does not expose stable DeviceInfo identity. The same
|
||||
hardware under a new CoreBluetooth UUID cannot be recognized before DeviceInfo
|
||||
becomes available. This remains an explicit protocol/hardware acceptance gap;
|
||||
the wizard must not speculate.
|
||||
|
||||
## Session and freshness rules
|
||||
|
||||
A scan result is an unselected presence candidate owned by the latest explicit
|
||||
scan generation. Wall-clock age does not remove its row while the operator is
|
||||
reading or completing the form. A successor Scan, explicit scenario reset,
|
||||
runtime-owner teardown or proven exact-target GATT failure invalidates it. The
|
||||
row itself is never network authority: Apply still requires the exact captured
|
||||
CoreBluetooth object and live GATT validation before any write.
|
||||
|
||||
The selected session ends on proven disconnect, explicit lifecycle stop, a
|
||||
committed network transition, selection of another device, backend restart or
|
||||
proven native cleanup. A later connection always requires an explicit search
|
||||
and **Выбрать**. Polling can update presentation but starts neither operation.
|
||||
|
||||
Bridge, Quick Connect and Direct Connect are separate topologies. In
|
||||
any state, changing the mode or choosing another K1 in the same mode sends one
|
||||
local scenario-reset CAS. It can wait for and supersede live/recovery ownership,
|
||||
seal retained local producers and retire unresolved old lineage, but performs
|
||||
zero device/host I/O and starts no Scan. Scan, Verify, provisioning and START
|
||||
remain independently fenced until an explicit candidate intent crosses its
|
||||
reviewed transition. No old host route, endpoint, control, data or BLE authority
|
||||
crosses a committed reset boundary.
|
||||
|
||||
## Active scanning: transient host-path recovery
|
||||
|
||||
This is the sole automatic read-only rebind exception. It exists only after
|
||||
Mission Core itself has a composite-confirmed START and still owns the exact
|
||||
acquisition/runtime/device/connection/evidence lineage. It does not apply on a
|
||||
cold connection screen, after backend restart, to an external SCANNING K1 or to
|
||||
an unresolved/foreign START.
|
||||
|
||||
When the Mac loses Wi-Fi/route or the data socket while that acquisition is
|
||||
running, the active scanning pane shows a neutral spinner and
|
||||
**Восстанавливаем соединение** with attempt/elapsed time. Do not show a red
|
||||
terminal operation banner for the expected late failure of the superseded old
|
||||
control socket. Keep the acquisition and evidence session owned while the
|
||||
backend retries exact route/TCP and inspection-only DeviceInfo/status proof.
|
||||
|
||||
The recovery loop never sends BLE, changes Wi-Fi, writes DeviceConfig, repeats
|
||||
START or sends STOP. Outcomes are:
|
||||
|
||||
- exact same-device/same-project initialized `SCANNING`: silently resume the
|
||||
point stream/control binding and, when necessary, CAS-restart the dead or
|
||||
stalled acquisition-owned right-camera FFmpeg epoch;
|
||||
- fresh `READY`: interrupt/seal host-owned acquisition resources truthfully,
|
||||
without STOP;
|
||||
- fresh `SCAN_OVER`: persist cessation, interrupt/seal locally and retain a
|
||||
read-only `awaiting READY` fence that denies a new START;
|
||||
- wrong identity/same IP, changed lineage or failed camera CAS: remain blocked
|
||||
for explicit operator handling; and
|
||||
- device/system fault or unsafe status: show a truthful terminal fault, with no
|
||||
command retry.
|
||||
|
||||
While state is `reconnecting` or `blocked`, expose **Завершить локально**. The
|
||||
action `acquisition.force-finish-local` requires the current snapshot runtime,
|
||||
acquisition id/state revision, recovery generation, producer generation match,
|
||||
an idempotency key and explicit confirmation. It cancels recovery first, then
|
||||
seals only local receiver/camera/control/perception owners. It preserves the
|
||||
physical START ledger and sends no STOP. If a connection-mode reset races this
|
||||
action, the shared lifecycle gate makes cleanup idempotent; the loser cannot
|
||||
overwrite the new mode or revive the old acquisition.
|
||||
|
||||
If receiver, camera or evidence sealing fails, the recovery generation is
|
||||
still irrevocably cancelled first. The force-finish operation ends with a
|
||||
visible `local-cleanup-failed` result whose retryability applies only to local
|
||||
finalization; the terminal acquisition retains `cleanup_pending` and blocks a
|
||||
replacement session. A later explicit local stop or exact connection-scenario
|
||||
reset may retry those host resources. It must not retry START, STOP, BLE or a
|
||||
network write, and a late success from the retired recovery generation remains
|
||||
fenced.
|
||||
|
||||
## Failure matrix
|
||||
|
||||
| Event | Product result | Operator path |
|
||||
| --- | --- | --- |
|
||||
| Cold entry | Mode plus Step 01 and explicit Scan; zero device I/O before Scan | Start search explicitly |
|
||||
| Disconnected/idle mode or same-mode new-device request with unresolved durable physical history | Local session/audit lineage is retired under one reset CAS; zero device/host I/O and no automatic Scan | Start the clean Step 01 search explicitly; the old physical outcome remains auditable |
|
||||
| Mode reset while live, reconnecting or terminal cleanup still owns local sources | Reset supersedes recovery and locally seals receiver/camera/control; previous K1 may still scan | Wait for the bounded local cleanup or retry the same reset if local sealing fails |
|
||||
| Search running | Step 01 spinner and visible countdown | Wait or let the bounded search end |
|
||||
| Search finds no candidates | Step 01 reports no matches | Repeat search explicitly |
|
||||
| Search finds one or many candidates | Every connectable row has one enabled **Выбрать**, including the exact prior UUID | Select one row; no reconnect or recovery action appears in search results |
|
||||
| Wall-clock time passes after Scan before selection | Latest-generation rows remain stable; no operation starts | Select normally; exact capture and live GATT will gate Apply |
|
||||
| A new Scan/reset/runtime teardown or exact-target GATT failure invalidates the generation | Old rows disappear or the attempted action fails cleanly before mutation | Run one explicit new search if needed |
|
||||
| Selection is rejected by identity, GATT, CAS, lifecycle or safety policy | Loader ends; nothing changed; Step 02 remains absent | **Повторить** or **Выбрать другое** |
|
||||
| Selection completes exact device connection | Step 01 turns green | Continue in Step 02 **Сеть** |
|
||||
| Network setup is safely required | Credentials appear only in Step 02 | Submit once |
|
||||
| Network write becomes ambiguous after dispatch | Attempt ends unknown; no replay | Wait for cleanup, then create a distinct explicit attempt |
|
||||
| Device powers off or BLE disconnects | Live selection and authority revoke after proof | Search and select explicitly after the device is available |
|
||||
| Router, Mac Wi-Fi or MQTT control is lost while idle/pre-START | Host/control authority revokes; data may remain evidence only | Restore reachability, then use the same wizard flow |
|
||||
| Mac Wi-Fi/route is briefly lost during one composite-confirmed owned acquisition | Active pane remains neutral **Восстанавливаем соединение**; no START/STOP/network retry | Wait for exact automatic read-only rebind or press **Завершить локально** |
|
||||
| Active recovery returns READY or SCAN_OVER | Local receiver/camera seal without STOP; SCAN_OVER remains fenced until fresh READY | Start another scenario only after backend policy reports it safe |
|
||||
| Active recovery sees another K1 on the same IP or changed lineage | Recovery blocks fail-closed; no camera/data resurrection | Finish locally or explicitly choose/reset connection scenario |
|
||||
| A physical START/STOP edge is unresolved | Mutation stays fenced; no technical wizard ceremony | Search/select remains explicit; backend admits only a safe exact path |
|
||||
| Exact actively retired UUID is present after committed reset and successor Scan | The row exposes the same enabled **Выбрать** as every candidate | Select locally; Apply remains exact-handle/live-GATT gated and may append one internal local settlement checkpoint before its sole write; audit remains append-only and START/STOP stay denied until fresh read-only classification |
|
||||
| Another candidate is selected while old authority is unavailable and no reset-owned new scenario exists | Selection stays local and Apply remains denied | Start an explicit new connection scenario, then Scan and select again |
|
||||
| Browser refresh or backend restart | No automatic operation and no restored live selection | Begin from the cold progressive wizard |
|
||||
|
||||
## Physical START/STOP safety remains separate
|
||||
|
||||
The simplified wizard never weakens physical-command safety:
|
||||
|
||||
- loss of control does not prove that K1 stopped recording;
|
||||
- START and STOP are never replayed automatically;
|
||||
- local receiver/camera/ingress cleanup is not physical STOP;
|
||||
- an ambiguous post-dispatch command remains unknown until exact fresh proof;
|
||||
- read-only recovery is pinned to the durable transport, identity/profile,
|
||||
host epoch and project;
|
||||
- each observation publishes exactly one DeviceInfo request and may classify
|
||||
only a fresh non-retained DeviceStatus after that barrier;
|
||||
- READY records cessation without inventing a successful STOP;
|
||||
- SCAN_OVER records cessation without inventing STOP, but keeps a durable
|
||||
read-only fence until a later fresh unbound READY observation;
|
||||
- exact same-project SCANNING may mint one single-use, separately confirmed STOP
|
||||
checkpoint; it does not send STOP automatically;
|
||||
- a wrong transport/device/project changes no topology or ledger state;
|
||||
- accepted STOP without READY or SCAN_STOPPING by the backend deadline closes
|
||||
only host-owned resources, yields `timed_out` / `standby-unknown`, preserves
|
||||
the unresolved ledger and keeps mutation fenced.
|
||||
|
||||
Engineering logs and state APIs retain these distinctions. The connection
|
||||
wizard projects only the ordinary progressive flow and a non-technical terminal
|
||||
selection result.
|
||||
|
||||
## No automatic action rule
|
||||
|
||||
None of these events may scan, select, reconnect, Verify, provision, START or
|
||||
STOP:
|
||||
|
||||
- opening or resizing the connection surface;
|
||||
- backend event delivery or state polling;
|
||||
- an acknowledged scenario reset (it may perform only its explicit local
|
||||
retirement, never any listed device/network action or automatic Scan);
|
||||
- candidate list refresh after an ended search;
|
||||
- browser refresh, sleep/wake or backend restart;
|
||||
- timeout, disconnect or a historical audit record.
|
||||
|
||||
The only exception is the service-owned active-stream read-only rebind above.
|
||||
It is triggered by the already-owned receiver's transport loss, not UI entry or
|
||||
polling, and is limited to route/TCP, DeviceInfo/status inspection, receiver
|
||||
resubscribe and exact local camera-epoch restart. It never performs discovery,
|
||||
provisioning, START, STOP or any device/network write.
|
||||
|
||||
Only the currently pressed search, distinct exact recovery CTA, network submit
|
||||
or separately guarded acquisition control may own corresponding I/O. Ordinary
|
||||
**Выбрать** owns only a browser-local draft and never owns a loader. Every
|
||||
loader belongs to the explicit action that created it and ends with it.
|
||||
|
||||
## Hardware acceptance order
|
||||
|
||||
Software tests do not replace a real K1/macOS/router run. Accept sequentially:
|
||||
|
||||
1. Open cold and prove mode plus Step 01 and its explicit Scan action are
|
||||
visible, while Step 02 is absent and no discovery starts automatically. With both empty and
|
||||
unresolved durable physical history, change the mode and prove one local
|
||||
reset CAS, zero device/host calls and no automatic Scan. Repeat from active,
|
||||
reconnecting and terminal `cleanup_pending` states; prove local sources are
|
||||
sealed, the old K1 is not claimed stopped, and a local cleanup failure leaves
|
||||
the exact reset retryable. With an exact prior connection, prove cold entry
|
||||
contains no historical reconnect prompt; after one reset CAS and zero Scan,
|
||||
a separate clean **Найти по Bluetooth** action remains clean after reload.
|
||||
2. Start discovery and prove the spinner and seconds countdown remain visible
|
||||
for the bounded search, then the exact result count appears.
|
||||
3. With multiple advertisements, prove every ordinary connectable row keeps
|
||||
exactly one enabled **Выбрать** action and none auto-selects or auto-connects.
|
||||
Repeat with the exact prior UUID after reset and prove it has the same
|
||||
**Выбрать** action, with no reconnect/reopen/Verify path.
|
||||
4. Select a Bridge device, including that prior UUID, and prove the card remains visible through
|
||||
**Подключение…**, then Step 01 turns green before Step 02 **Сеть** appears.
|
||||
5. Prove network fields never coexist with candidate rows, and one explicit
|
||||
submit owns at most one write.
|
||||
6. Wait beyond the legacy candidate TTL and prove both the latest-generation
|
||||
unselected rows and an admitted selected session remain stable; then prove a
|
||||
missing exact handle/live GATT failure blocks Apply before any write.
|
||||
7. Exercise identity, GATT, stale-CAS, lifecycle-busy, disconnect and power-off
|
||||
failures; each ends the loader, leaves Step 02 absent and offers only ordinary
|
||||
retry/choose-another copy.
|
||||
8. Retire an unresolved target in controlled fault injection, perform one
|
||||
scenario reset and rediscover its exact UUID in the successor Scan. Prove its
|
||||
sole action is **Выбрать**, selection performs no I/O and Step 02 appears
|
||||
immediately. Apply once and prove exact current-generation handle capture,
|
||||
live GATT baseline, exactly one request-bound append-only physical reopen
|
||||
checkpoint and at most one network write. The original retirement/outcome
|
||||
audit remains immutable; the service-owned continuation uses only DeviceInfo
|
||||
and non-retained status, with zero START/STOP and no browser Verify. Inject
|
||||
failed and outcome-unknown network results; each current error/recovery
|
||||
surface remains visible after reload.
|
||||
9. Try a different device while old authority is unavailable and prove its
|
||||
ordinary selection triggers no hidden retirement/reopen/Verify and cannot
|
||||
bypass the durable target.
|
||||
10. Prove no row labels a device saved/original/retired, says
|
||||
**Переподключиться**, or exposes physical-state/ledger terminology. The model name
|
||||
appears only in the top heading; step names remain **Подключение / Сеть**.
|
||||
11. During a composite-confirmed live acquisition, remove host Wi-Fi for longer
|
||||
than the old control keepalive and restore it. Prove neutral reconnecting,
|
||||
same-lineage SCANNING resume, raw-writer continuity, exact camera epoch
|
||||
restart when stalled, and zero START/STOP/BLE/network mutation. Repeat with
|
||||
READY, SCAN_OVER, wrong identity and permanent loss plus
|
||||
**Завершить локально**.
|
||||
12. Repeat idle/pre-START Bridge network loss, Mac Wi-Fi switch, sleep/wake,
|
||||
hard K1 power loss, STOP deadline and backend restart; prove zero automatic
|
||||
command or retry outside the sole active-stream exception.
|
||||
13. Repeat the entire acceptance separately for Quick Connect before claiming
|
||||
Quick coverage.
|
||||
|
||||
The current software contract is not real-hardware acceptance. The acceptance
|
||||
manifest lists executable coverage and the remaining Bridge/Quick field gaps.
|
||||
Reference in New Issue
Block a user