feat(x4): add per-camera manual wake and hide power action when connected
This commit is contained in:
@@ -5,7 +5,7 @@ import ts from 'typescript';
|
|||||||
|
|
||||||
const source=readFileSync(new URL('../../../plugins/insta360-x4/frontend/src/model.ts',import.meta.url),'utf8');
|
const source=readFileSync(new URL('../../../plugins/insta360-x4/frontend/src/model.ts',import.meta.url),'utf8');
|
||||||
const code=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.ESNext}}).outputText;
|
const code=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.ESNext}}).outputText;
|
||||||
const {cameraLabel,cameraStatus,resolutionLabel}=await import('data:text/javascript;base64,'+Buffer.from(code).toString('base64'));
|
const {cameraLabel,cameraStatus,resolutionLabel,cameraRowActions}=await import('data:text/javascript;base64,'+Buffer.from(code).toString('base64'));
|
||||||
const camera={id:'instax4_synthetic_a',online:true,configured:true,prepared:true,snapshot:{enrollment:'enrolled'},camera_status:{connected:true,recording:0,preview:0,function_mode:7}};
|
const camera={id:'instax4_synthetic_a',online:true,configured:true,prepared:true,snapshot:{enrollment:'enrolled'},camera_status:{connected:true,recording:0,preview:0,function_mode:7}};
|
||||||
|
|
||||||
test('SD recording remains visible after preview closes, independently for each camera',()=>{
|
test('SD recording remains visible after preview closes, independently for each camera',()=>{
|
||||||
@@ -27,3 +27,17 @@ test('installed model support does not enroll a new physical camera',()=>{
|
|||||||
test('SDK resolution names are presented with their actual dimensions and frame rate',()=>{
|
test('SDK resolution names are presented with their actual dimensions and frame rate',()=>{
|
||||||
assert.equal(resolutionLabel('3840_1920_30'),'3840 × 1920 · 30 кадр/с');
|
assert.equal(resolutionLabel('3840_1920_30'),'3840 × 1920 · 30 кадр/с');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('manual wake is available for an enrolled offline camera with automatic recovery disabled',()=>{
|
||||||
|
const offline={...camera,online:false,camera_status:{recovery:{supported:true,enabled:false,wake_available:true}}};
|
||||||
|
assert.equal(cameraRowActions(offline)[0].actionId,'power.wake');
|
||||||
|
assert.equal(cameraRowActions(offline)[0].disabled,false);
|
||||||
|
assert.deepEqual(cameraRowActions({...offline,online:true}),[]);
|
||||||
|
assert.equal(cameraRowActions({...offline,configured:false})[0].disabled,true);
|
||||||
|
assert.equal(cameraRowActions({...offline,prepared:false})[0].disabled,true);
|
||||||
|
assert.equal(offline.camera_status.recovery.enabled,false);
|
||||||
|
});
|
||||||
|
test('old drivers and unknown wake identities never offer an active power command',()=>{
|
||||||
|
assert.deepEqual(cameraRowActions(camera),[]);
|
||||||
|
assert.equal(cameraRowActions({...camera,online:false,camera_status:{recovery:{supported:true}}})[0].disabled,true);
|
||||||
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type sensorModel struct {
|
|||||||
Vendor, Product, USBName string
|
Vendor, Product, USBName string
|
||||||
Socket, PrepareUnit, Report string
|
Socket, PrepareUnit, Report string
|
||||||
Actions map[string]bool
|
Actions map[string]bool
|
||||||
|
ActionTimeouts map[string]time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func actions(names ...string) map[string]bool {
|
func actions(names ...string) map[string]bool {
|
||||||
@@ -41,7 +42,8 @@ var sensorModels = []sensorModel{
|
|||||||
// OS product descriptor as well; SDK identity is verified after prepare.
|
// OS product descriptor as well; SDK identity is verified after prepare.
|
||||||
Vendor: "2e1a", Product: "0002", USBName: "Insta360 X4", Socket: "/run/mission-core-insta360/driver.sock",
|
Vendor: "2e1a", Product: "0002", USBName: "Insta360 X4", Socket: "/run/mission-core-insta360/driver.sock",
|
||||||
PrepareUnit: "mission-core-node-insta360-x4-profile.service", Report: "/var/lib/mission-core-node-profiles/insta360-x4/preparation.json",
|
PrepareUnit: "mission-core-node-insta360-x4-profile.service", Report: "/var/lib/mission-core-node-profiles/insta360-x4/preparation.json",
|
||||||
Actions: actions("prepare", "details", "rename", "verify", "preview.start", "preview.stop", "record.start", "record.stop", "photo.capture", "settings.read", "settings.apply", "files.list", "offer", "close-peer", "recovery.configure")},
|
ActionTimeouts: map[string]time.Duration{"power.wake": 55 * time.Second},
|
||||||
|
Actions: actions("prepare", "details", "rename", "verify", "preview.start", "preview.stop", "record.start", "record.stop", "photo.capture", "settings.read", "settings.apply", "files.list", "offer", "close-peer", "recovery.configure", "power.wake")},
|
||||||
}
|
}
|
||||||
|
|
||||||
func modelForDevice(id string) *sensorModel {
|
func modelForDevice(id string) *sensorModel {
|
||||||
|
|||||||
@@ -48,6 +48,40 @@ func isolatedSensors(t *testing.T) *Sensors {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestManualWakeDeadlineReachesDriverWithoutChangingInventoryTimeout(t *testing.T) {
|
||||||
|
s := isolatedSensors(t)
|
||||||
|
model := &sensorModels[2]
|
||||||
|
var remaining time.Duration
|
||||||
|
s.clients[model.ID].Transport = sensorRoundTrip(func(r *http.Request) (*http.Response, error) {
|
||||||
|
deadline, ok := r.Context().Deadline()
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("driver request is unbounded")
|
||||||
|
}
|
||||||
|
remaining = time.Until(deadline)
|
||||||
|
return testSensorReply(map[string]any{"state": "complete"}), nil
|
||||||
|
})
|
||||||
|
for _, action := range []string{"power.wake", "details"} {
|
||||||
|
if _, err := s.modelDriver(context.Background(), model, "/operation", SensorCommand{Action: action}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := 25 * time.Second
|
||||||
|
if action == "power.wake" {
|
||||||
|
want = 55 * time.Second
|
||||||
|
}
|
||||||
|
if remaining > want || remaining < want-time.Second {
|
||||||
|
t.Fatalf("%s was cut off at %s", action, remaining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if _, err := s.modelDriver(ctx, model, "/operation", SensorCommand{Action: "power.wake"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if remaining > time.Second || s.clients[model.ID].Timeout != 25*time.Second {
|
||||||
|
t.Fatal("action extended the caller deadline or changed the shared client")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestModelDiscoveryIdentityAndHotplug(t *testing.T) {
|
func TestModelDiscoveryIdentityAndHotplug(t *testing.T) {
|
||||||
s := isolatedSensors(t)
|
s := isolatedSensors(t)
|
||||||
fakeUSB(t, s.usbRoot, "3-2", "synthetic-a", "Insta360 X4", "2")
|
fakeUSB(t, s.usbRoot, "3-2", "synthetic-a", "Insta360 X4", "2")
|
||||||
@@ -160,7 +194,9 @@ func TestNewCameraUsesReadyProfileWithoutInterruptingAnotherInstance(t *testing.
|
|||||||
item["preparation_safe"] = id != active
|
item["preparation_safe"] = id != active
|
||||||
snapshot := item["snapshot"].(map[string]any)
|
snapshot := item["snapshot"].(map[string]any)
|
||||||
snapshot["context"].(map[string]any)["session_id"] = "sdk_" + id
|
snapshot["context"].(map[string]any)["session_id"] = "sdk_" + id
|
||||||
if id == active { snapshot["acquisition"] = "streaming" }
|
if id == active {
|
||||||
|
snapshot["acquisition"] = "streaming"
|
||||||
|
}
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
return testSensorReply(map[string]any{"items": items}), nil
|
return testSensorReply(map[string]any{"items": items}), nil
|
||||||
@@ -180,16 +216,22 @@ func TestNewCameraUsesReadyProfileWithoutInterruptingAnotherInstance(t *testing.
|
|||||||
command := sensorTestCommand()
|
command := sensorTestCommand()
|
||||||
command.Action = "prepare"
|
command.Action = "prepare"
|
||||||
command.Session = SensorSession{DeviceID: newDevice, SessionID: "sdk_" + newDevice}
|
command.Session = SensorSession{DeviceID: newDevice, SessionID: "sdk_" + newDevice}
|
||||||
if _, err := s.Submit(command, true); err != nil { t.Fatal(err) }
|
if _, err := s.Submit(command, true); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
awaitSensor(t, func() bool { op := s.Get(command.ID); return op.Preparation != nil && op.Preparation.Phase == "verify" })
|
awaitSensor(t, func() bool { op := s.Get(command.ID); return op.Preparation != nil && op.Preparation.Phase == "verify" })
|
||||||
stop := sensorTestCommand()
|
stop := sensorTestCommand()
|
||||||
stop.ID = "op_" + strings.Repeat("b", 32)
|
stop.ID = "op_" + strings.Repeat("b", 32)
|
||||||
stop.Idempotency = stop.ID
|
stop.Idempotency = stop.ID
|
||||||
stop.Action = "preview.stop"
|
stop.Action = "preview.stop"
|
||||||
stop.Session = SensorSession{DeviceID: active, SessionID: "sdk_" + active}
|
stop.Session = SensorSession{DeviceID: active, SessionID: "sdk_" + active}
|
||||||
if _, err := s.Submit(stop, false); err != nil { t.Fatal(err) }
|
if _, err := s.Submit(stop, false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
awaitSensor(t, func() bool { return stopped.Load() })
|
awaitSensor(t, func() bool { return stopped.Load() })
|
||||||
if runs.Load() != 0 { t.Fatal("new-camera preparation redeployed the shared profile") }
|
if runs.Load() != 0 {
|
||||||
|
t.Fatal("new-camera preparation redeployed the shared profile")
|
||||||
|
}
|
||||||
releaseOnce.Do(func() { close(release) })
|
releaseOnce.Do(func() { close(release) })
|
||||||
awaitSensor(t, func() bool { return s.Get(command.ID).State == "complete" })
|
awaitSensor(t, func() bool { return s.Get(command.ID).State == "complete" })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,6 +163,15 @@ func (s *Sensors) modelDriver(ctx context.Context, model *sensorModel, path stri
|
|||||||
if client == nil {
|
if client == nil {
|
||||||
return nil, errors.New("Интеграция устройства не установлена.")
|
return nil, errors.New("Интеграция устройства не установлена.")
|
||||||
}
|
}
|
||||||
|
if command, ok := body.(SensorCommand); ok && path == "/operation" {
|
||||||
|
if timeout := model.ActionTimeouts[command.Action]; timeout > 0 {
|
||||||
|
// A model may need longer to confirm an action. Preserve the shared
|
||||||
|
// inventory client and the request's earlier context deadline.
|
||||||
|
bounded := *client
|
||||||
|
bounded.Timeout = timeout
|
||||||
|
client = &bounded
|
||||||
|
}
|
||||||
|
}
|
||||||
method := "GET"
|
method := "GET"
|
||||||
var reader io.Reader
|
var reader io.Reader
|
||||||
if body != nil {
|
if body != nil {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import sys
|
|||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
BINARY_VERSION = "0.8.20"
|
BINARY_VERSION = "0.8.21"
|
||||||
VERSION = "0.8.20"
|
VERSION = "0.8.21-3"
|
||||||
sys.path.insert(0, str(ROOT.parents[1] / "scripts/packaging"))
|
sys.path.insert(0, str(ROOT.parents[1] / "scripts/packaging"))
|
||||||
from debian import package
|
from debian import package
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"schema": "missioncore.node.bundled-model/v1",
|
"schema": "missioncore.node.bundled-model/v1",
|
||||||
"model_id": "insta360.x4",
|
"model_id": "insta360.x4",
|
||||||
"version": "0.1.3-7",
|
"version": "0.1.3-9",
|
||||||
"revision": "45e14de38a278914eedf5cbb",
|
"revision": "0b1085386d53e65760a9610f",
|
||||||
"bytes": 58210794,
|
"bytes": 58211732,
|
||||||
"sha256": "96ce6b52fa72d79ec6f8b0d13bc2af53afda61077b40a3358a3d145295a62d19"
|
"sha256": "eb6331680fbe8cac56eb745c31d7c37618a9dff67bc2a190681a384813cdc709"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,68 @@ def finish_legacy_monitor_pager():
|
|||||||
raise RuntimeError("Старая транзакция ещё не завершилась; она не прерывалась.")
|
raise RuntimeError("Старая транзакция ещё не завершилась; она не прерывалась.")
|
||||||
|
|
||||||
|
|
||||||
|
def update_installed_x4(run):
|
||||||
|
"""Upgrade an existing model through its fixed profile, even with camera off.
|
||||||
|
|
||||||
|
Bare Ubuntu still uses the application's first preparation. No SDK check or
|
||||||
|
wake is implied by updating installed package bytes.
|
||||||
|
"""
|
||||||
|
result = subprocess.run(
|
||||||
|
["/usr/bin/dpkg-query", "-W", "-f", "${Status}", "mission-core-insta360-x4"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if result.returncode or result.stdout.strip() != "install ok installed":
|
||||||
|
return {"state": "not-installed"}
|
||||||
|
profile = json.loads(
|
||||||
|
Path("/usr/share/mission-core-node/profiles/insta360-x4/profile.json").read_text()
|
||||||
|
)
|
||||||
|
installed = run(
|
||||||
|
"x4-installed-before",
|
||||||
|
[
|
||||||
|
"/usr/bin/dpkg-query",
|
||||||
|
"-W",
|
||||||
|
"-f",
|
||||||
|
"${Version}",
|
||||||
|
"mission-core-insta360-x4",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if installed == profile["version"]:
|
||||||
|
return {"state": "current", "version": installed}
|
||||||
|
# The shipped profile verifies root-owned bytes, lifecycle quiescence and
|
||||||
|
# dependency closure. It never requires a connected camera for package upgrade.
|
||||||
|
run(
|
||||||
|
"x4-bundle-update",
|
||||||
|
[
|
||||||
|
"/usr/bin/systemctl",
|
||||||
|
"start",
|
||||||
|
"mission-core-node-insta360-x4-profile.service",
|
||||||
|
],
|
||||||
|
timeout=None,
|
||||||
|
)
|
||||||
|
after = run(
|
||||||
|
"x4-installed-after",
|
||||||
|
[
|
||||||
|
"/usr/bin/dpkg-query",
|
||||||
|
"-W",
|
||||||
|
"-f",
|
||||||
|
"${Version}",
|
||||||
|
"mission-core-insta360-x4",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
state = json.loads(
|
||||||
|
Path("/var/lib/mission-core-node-profiles/insta360-x4/preparation.json").read_text()
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
after != profile["version"]
|
||||||
|
or state.get("state") != "complete"
|
||||||
|
or state.get("revision") != profile["revision"]
|
||||||
|
):
|
||||||
|
raise RuntimeError("Обновление установленного драйвера X4 не подтверждено.")
|
||||||
|
return {"state": "updated", "version": after, "camera_verification": "not-performed"}
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if os.geteuid() or sys.argv[1:]:
|
if os.geteuid() or sys.argv[1:]:
|
||||||
raise ValueError("Use the release's local Ubuntu installer")
|
raise ValueError("Use the release's local Ubuntu installer")
|
||||||
@@ -163,7 +225,7 @@ def main():
|
|||||||
"started_at": datetime.now(UTC).isoformat(),
|
"started_at": datetime.now(UTC).isoformat(),
|
||||||
"monotonic_started": time.monotonic(),
|
"monotonic_started": time.monotonic(),
|
||||||
"state": "running",
|
"state": "running",
|
||||||
"scope": "Node-package-and-UI-only; X4-prepare-remains-in-application",
|
"scope": "Node-package-UI-and-existing-model-update; first-X4-prepare-in-application",
|
||||||
"steps": [],
|
"steps": [],
|
||||||
}
|
}
|
||||||
env = {
|
env = {
|
||||||
@@ -238,6 +300,8 @@ def main():
|
|||||||
)
|
)
|
||||||
if installed != version + "\tinstall ok installed":
|
if installed != version + "\tinstall ok installed":
|
||||||
raise RuntimeError("Версия установленного Node не подтверждена.")
|
raise RuntimeError("Версия установленного Node не подтверждена.")
|
||||||
|
report["x4_bundle"] = update_installed_x4(run)
|
||||||
|
publish()
|
||||||
run(
|
run(
|
||||||
"node-service",
|
"node-service",
|
||||||
["/usr/bin/systemctl", "is-active", "--quiet", "mission-core-node.service"],
|
["/usr/bin/systemctl", "is-active", "--quiet", "mission-core-node.service"],
|
||||||
|
|||||||
@@ -1468,3 +1468,327 @@ d915221ddb18da1b37446571911e81bc9fe303de59fe37e8dec2c0715f19b623.
|
|||||||
Физическое выключение камеры владельцем ещё не выполнено/не подтверждено;
|
Физическое выключение камеры владельцем ещё не выполнено/не подтверждено;
|
||||||
отсутствие самопробуждения остаётся pending. USB должен оставаться подключённым.
|
отсутствие самопробуждения остаётся pending. USB должен оставаться подключённым.
|
||||||
После приёмки отмены вернуть рабочее recovery. Системных изменений вне пакета нет.
|
После приёмки отмены вернуть рабочее recovery. Системных изменений вне пакета нет.
|
||||||
|
|
||||||
|
### CANCEL01-OWNER / OBSERVE — ручное выключение при отключённом recovery
|
||||||
|
|
||||||
|
Владелец сообщил: выключил X4 кнопкой Power при оставленном USB; позже
|
||||||
|
появилась индикация зарядки, полное подключение не уверен. Readback
|
||||||
|
10.09.2026 11:40:54.276119 UTC,Mac monotonic460934.291810458: Rover online,
|
||||||
|
X4 offline, recovery disabled/preview_wanted=false, прежняя закрытая session;
|
||||||
|
D455 streaming с прежней session, color25605. Offline snapshot сохраняет
|
||||||
|
последнюю timestamp настройки11:34:29 и не считается свежим camera status.
|
||||||
|
Private cancel-owner-after.json SHA256
|
||||||
|
37b3f4d62bf253b23fa0d58ae5a133f7b7ace574236005e9e68a8d0475609474.
|
||||||
|
Независимое read-only sysfs чтение11:42:10.822955 UTC,
|
||||||
|
Ubuntu monotonic338838.704054695: SDK2e1a:0002 и Mass Storage070a:4026 отсутствуют.
|
||||||
|
Journal старого X4 source: завершён11:39:30 UTC, новый source не запускали.
|
||||||
|
|
||||||
|
CANCEL01-OBSERVE intent:160s только Core inventory с интервалом4s,
|
||||||
|
source SHA256e2fffc21d5dd15a3caf2d8f402680107e8cc68ca7ad8b4a7e9ac9377223bc543.
|
||||||
|
Complete11:42:14.958444–11:44:55.419450 UTC,
|
||||||
|
Mac monotonic461014.973603958,duration160.500588958s. Все40 samples:
|
||||||
|
Rover online,X4 offline,recovery=false/preview_wanted=false,D455 online и
|
||||||
|
прежняя session; color26804→29127. Private cancel-observe.json SHA256
|
||||||
|
d90483f72953d09f61238a0e4853cefe00157872dcd5c3bbbe7c86a240a1c656.
|
||||||
|
Observer штатно завершён; отсутствуют SDK/SD/USB/BLE writes или установки.
|
||||||
|
|
||||||
|
Независимый engineering reader только sysfs, BlueZ get-property и systemctl show;
|
||||||
|
source SHA256448913b34c1eeb3bf2275b4d5c746f3e630acbbe48462ff422277d31eba2d52d.
|
||||||
|
Передан на stdin обычного Ubuntu python, без staging/install/root/изменений ОС.
|
||||||
|
Readback11:45:08.981438 UTC,Ubuntu monotonic339016.86253016: SDK/Mass Storage
|
||||||
|
X4 отсутствуют, BlueZ ActiveInstances0, broker/wake/D455 active,NRestarts0,
|
||||||
|
прежние ActiveEnterTimestamp. Private cancel-usb-after.json SHA256
|
||||||
|
43e13e1c1427f585752b19515d3b8e8af607ba90bfe7527bdb058075931c3d14.
|
||||||
|
Принято отсутствие автоматического возврата в SDK в этом ограниченном окне.
|
||||||
|
Индикация зарядки не сопровождалась рабочим подключением; электрическое
|
||||||
|
power-off и непрерывная radio-активность не измерены.
|
||||||
|
|
||||||
|
Владелец получил следующий шаг — короткое Power при оставленном USB.
|
||||||
|
Пока X4/recovery выключены; вернуть recovery и preview можно только после
|
||||||
|
fresh exact-instance USB/SDK. Подготовлен, но не исполнен private re-enable
|
||||||
|
instrument SHA256b6ab26fdcb9f77ba59c35a6133f2f9bd691aca387dd498ffc9de752346124bc0.
|
||||||
|
Это штатный Core recovery.configure(enabled=true,wakeup_confirmed=true),
|
||||||
|
без обхода обязательного USB enrollment. Новых системных предпосылок нет.
|
||||||
|
|
||||||
|
### MANUAL01 / B16 — разовое включение из списка устройств
|
||||||
|
|
||||||
|
Новое поручение владельца заменило просьбу о физическом Power: добавить
|
||||||
|
Bluetooth-включение в строку X4 Core/Node. Камера остаётся выключенной с USB,
|
||||||
|
recovery=false. Подготовленный re-enable instrument не выполняется.
|
||||||
|
UI placement/state/installer ownership в docs16; используются существующие
|
||||||
|
DG Button/ResourceRow, без новой визуальной сущности или навигации.
|
||||||
|
|
||||||
|
Реализованы power.wake с durable per-instance receipt, private wake-once,
|
||||||
|
отдельное сохранение OS identity без auto-enable, общие rate limit/adapter lock;
|
||||||
|
типизированные rowActions в SensorUiContribution, один renderer Core/Node.
|
||||||
|
Node owner installer обновляет только уже установленную модель через fixed
|
||||||
|
bundled profile, не требуя online X4; первая установка остаётся в приложении.
|
||||||
|
Локальные synthetic recovery22 и installer4 tests passed; Ruff/diff check passed
|
||||||
|
после форматирования. Реальных команд wake/install пока нет.
|
||||||
|
B16 source84a4c65393cfe8adcd91ae05,58354434 bytes,SHA256
|
||||||
|
c2d894555b3497a462a1e4988a148c3036e81b433755970cbbde818dc8176adb.
|
||||||
|
Следующий шаг — квалификация model0.1.3-8 на той же Ubuntu, private build
|
||||||
|
limits1GiB/180 CPU seconds/nice10; не обращается к SDK/USB и не устанавливает пакет.
|
||||||
|
|
||||||
|
B16 complete10.09.2026 12:00:53.222213–12:01:16.437398 UTC,
|
||||||
|
Ubuntu monotonic339961.103303344,duration23.215211857s. 85 synthetic tests passed;
|
||||||
|
cold imports/ELF closure passed, SDK not executed, clean OS image not tested.
|
||||||
|
Model0.1.3-8 revision80467edbdccb8b2f2cf790a3,58211604 bytes,SHA256
|
||||||
|
0e9226aef58cc52835c8869ae97ee90500538061bdbaaf7558f469a6a87b18ec.
|
||||||
|
Result retrieved into private qualified-b16; regular single-component archive
|
||||||
|
entries checked and model bytes/hash verified. Node bundled profile pins these bytes.
|
||||||
|
Core fleet31 tests passed locally (one existing Starlette/httpx deprecation warning).
|
||||||
|
|
||||||
|
N12 source82d8db1dcade151ab3fdea9f,2731 files,309771215 bytes,SHA256
|
||||||
|
72148a335c9a17be7f8a7af36206ffb1f682a6bacd9c90ddf6cd57363d0646a6.
|
||||||
|
Copied to private /var/tmp artifact and matching SHA checked before execution.
|
||||||
|
Versioned builder started on the same Ubuntu: 3GiB/150%CPU/256tasks/1200s,
|
||||||
|
DG/Node/Core checks sequential, no OS install or camera access. Expected Node0.8.21.
|
||||||
|
No hardware wake or runtime install performed at this point.
|
||||||
|
|
||||||
|
MANUAL01 instruments prepared, not yet executed: Core activation source
|
||||||
|
b1f5ad50f5c945be2699b2b19476594b7a6ae1c5ed12220a6250e04b8ad31aae
|
||||||
|
verifies N12 source and dist hashes, preserves old dist/sensors and private
|
||||||
|
Core/binding identity hashes, restarts only canonical launchd8000, checks health.
|
||||||
|
P12 D455 stop sourcec3c9a05c0d1135a7ec4a3f2741293f22f764c58612bd9e277dfb8818dcdcc864;
|
||||||
|
restore source149c04db68567622568500dcbc07948dccc49f1c59fb02619d6720139bd307fe.
|
||||||
|
Both use only existing Core operations, refuse recording/playback, no SD replay.
|
||||||
|
Stop is required by shipped Node preinst before update; restore follows acceptance.
|
||||||
|
MANUAL01-UI read-only observer source
|
||||||
|
1f2218342dd7ea8be1f929d057d72f42408ba646ff65ea16d6a64493982a3957:
|
||||||
|
max100s inventory/operation readback around one actual canonical UI click;
|
||||||
|
requires offline registered X4/recoveryfalse, then one complete wake and
|
||||||
|
fresh online preview0/recording0 with policy unchanged. No helper/SDK direct call.
|
||||||
|
|
||||||
|
N12 complete12:07:41.257112–12:16:28.377875 UTC,duration527.120759539s,
|
||||||
|
peak cgroup memory3221225472 bytes within declared cap. DG build/typecheck/
|
||||||
|
registry/loading/catalog; Node UI test/build, Go format/race tests/binary;
|
||||||
|
Core architecture/typecheck821 tests/build all passed. Existing Python/Rerun
|
||||||
|
native-RRD generator case remains outside qualified build inputs. DG dependency
|
||||||
|
attempt1 exit146 and Node UI dependency attempt1 timeout45s recovered by existing
|
||||||
|
bounded builder retries with unchanged source/lock; logs preserved, no OS changes.
|
||||||
|
Node0.8.21,168274816 bytes,SHA256
|
||||||
|
ea47f4f67953962cc88bd08c3a6978afe3c83f0bc0f97ff47610faaef6df77e3.
|
||||||
|
Core dist23068985 bytes,SHA256
|
||||||
|
7c5bfbca84b127cb3bce1e13766f23019663d484cb3053e971b17dacdb1a51bd.
|
||||||
|
N12 result SHA256a5fc96d285a101dae63dcfbd7db87b1e7b36593525c0ab2997d3096a2b746006;
|
||||||
|
retrieved archive regular entries and every qualified payload size/hash verified.
|
||||||
|
Owner releaseb430d57dd7810d56b77029d3,168296413 bytes,SHA256
|
||||||
|
3194c6eec152edd7ee45392e18c0338d15f916405fb319e6ed5691d37c75a10c.
|
||||||
|
Transferred to /var/tmp and hash verified; next fixed --plan then owner-authenticated
|
||||||
|
--launch only after D455 idle. It upgrades existing model via packaged profile;
|
||||||
|
X4 remains off and recoveryfalse. Runtime installation not yet performed.
|
||||||
|
|
||||||
|
P12 Node/model update complete: APT --plan upgrades only Node, removes0.
|
||||||
|
D455 stopped via Core12:18:19.846114–12:18:25.904136 UTC,
|
||||||
|
Mac monotonic463179.862859875; reportSHA256
|
||||||
|
d4597f273de03122f6fd76875dd14dcc4c50ac3c14afedbde9456b52c9f9ef75.
|
||||||
|
Owner entered local Ubuntu password. Fixed installer complete12:20:01.318373–
|
||||||
|
12:20:28.737425 UTC,27.419046572s; Node0.8.21/model0.1.3-8, x4_bundle updated,
|
||||||
|
camera_verification not-performed, local UI ready. X4 OFF throughout upgrade.
|
||||||
|
D455 restored via Core start(record=false), private reportSHA256
|
||||||
|
818bdcb1be2354ca91aabe8b367c88a2f0907536d0ba1976c6d3a108d5ed4285.
|
||||||
|
Core activation complete12:18:55.842419–12:19:28.777985 UTC,
|
||||||
|
Mac monotonic463215.858595791,32.935406667s; PID65164→70307, identity/bindings preserved.
|
||||||
|
ReportSHA2565f40b4a82c2d81decaf05543dd4362b49704d5a2a6589c6e08b17a7ee501771d.
|
||||||
|
Fresh Core/Node readback OFF X4/prepared/configured/wake_availabletrue/recoveryfalse;
|
||||||
|
private installed.json SHA256228397f01e2d6c14c01395d9d8a311f6cddc3fe136cc64e06029dfcf48263cae.
|
||||||
|
|
||||||
|
MANUAL01-UI partial hardware acceptance and timing defect:
|
||||||
|
Core canonical list rendered active «Включить» beside settings/eye on offline X4.
|
||||||
|
One real Button click at12:22:23.870 UTC; in-button centered loader visible,
|
||||||
|
selected X4 controls disabled, D455 controls unaffected. No outside row loader.
|
||||||
|
Operation ended error12:22:46.592820 UTC (generic no USB return), but X4 returned
|
||||||
|
later: new SDK session12:22:57.633789, first fresh Core online12:23:00.849852 UTC,
|
||||||
|
about36.98s after click; no physical Power or second wake requested/performed.
|
||||||
|
X4 preview0/recording0; recoveryfalse/preview_wantedfalse throughout99 samples;
|
||||||
|
D455 retained post-update session, remained streaming. Observer100.77s ended
|
||||||
|
failed because receipt did not match eventual success; evidence preserved.
|
||||||
|
Private observe-button.json SHA256
|
||||||
|
4b664ffc50514a2f7139d83a8088c1b2720afd85a2d1022735cfc65dde2ec1b1.
|
||||||
|
UI became online/«Включить» disabled, but stale failure toast remained. This is
|
||||||
|
not accepted as correct end-to-end operation confirmation.
|
||||||
|
|
||||||
|
B17 fixes only bounded confirmation after single beacon: if helper's20s window
|
||||||
|
ends without USB, broker continues read-only supervisor observation within52s
|
||||||
|
from call start; exact device_id online required, no second beacon/SDK/SD action.
|
||||||
|
Other errors retain immediate refusal. Synthetic late37s/other-instance/deadline
|
||||||
|
regressions added; recovery24 tests and Ruff passed. UI code unchanged from N12.
|
||||||
|
B17 model0.1.3-9 sourcef516f986fe637261d37eab32,58354797 bytes,SHA256
|
||||||
|
f7616990ad2c3f5496fdfc05dd00147fc77128e80e5cdac0542a9622a840975c.
|
||||||
|
Next qualify model then Node0.8.21-1 bundle through existing node-only builder;
|
||||||
|
Core N12 already qualified and active. No runtime hot patch/ad-hoc install.
|
||||||
|
|
||||||
|
B17 complete12:27:59.001757–12:28:22.799881 UTC,Ubuntu mono341586.882843768,
|
||||||
|
23.798147363s;87 synthetic tests/import/ELF closure passed, no SDK/hardware.
|
||||||
|
Model0.1.3-9 revision0b1085386d53e65760a9610f,58211732 bytes,SHA256
|
||||||
|
eb6331680fbe8cac56eb745c31d7c37618a9dff67bc2a190681a384813cdc709.
|
||||||
|
ResultSHA256520d23f362a7017eb76d0f5a2d78a051096d3809617f17da7a3d2e2386174033,
|
||||||
|
retrieved/extracted with path/type checks, package manifest bytes verified.
|
||||||
|
Owner confirmed physical OFF again solely to prepare MANUAL02; USB retained.
|
||||||
|
Fresh readback X4offline/recoveryfalse/wake_availabletrue, D455streaming;
|
||||||
|
private off-for-retest.json SHA256
|
||||||
|
bcecc77fb546b6c9cf82b9bcc34dda407d27c43d9f099afd0f53a84860bfc496.
|
||||||
|
MANUAL0199 samples independently checked: policy/preview intentfalse, all54
|
||||||
|
online samples preview0/recording0, D455streaming throughout. No unreported effect.
|
||||||
|
|
||||||
|
N13 node-only source0e3e40dea941828d9d6ecda9,2731files,309775674 bytes,SHA256
|
||||||
|
17e3c48a840e78600b89ea0fe90b690725da0cb33265eb7de5e77f3a0872c879.
|
||||||
|
Versioned private /var/tmp build started after matching remote hash, same3GiB/
|
||||||
|
150%CPU/1200s cgroup. Core UI stays exactly qualified N12, no new Core build.
|
||||||
|
P13/MANUAL02 private instruments adapted without new effects/authority:
|
||||||
|
D455 stop7d9aae26179d4fe5ed096ac2d14eed629d08aea0d0c2cd98f08870b53088996d;
|
||||||
|
restore9b2c343db134c557e52a90725f28ef513b741abc8d994859837353e56bf57be1;
|
||||||
|
read-only button observerc793f7251a319114a0b1ef0e0ba9afb748f76bc5bff1d772bd7f032d0bd05a9c.
|
||||||
|
They are not yet executed; one-shot hardware acceptance remains pending.
|
||||||
|
|
||||||
|
Final hop audit found Node driver HTTP client25s would truncate B17's52s
|
||||||
|
confirmation. Added per-model ActionTimeouts: X4 power.wake55s only, client copy
|
||||||
|
retains shared inventory25s and earlier context deadline. Synthetic transport
|
||||||
|
regression tests actual request deadlines, no long sleep/hardware. This is code
|
||||||
|
needed for correct end-to-end confirmation, not a broader timeout increase.
|
||||||
|
N13a0e3e40dea941828d9d6ecda9 had already completed before attempted stop of its
|
||||||
|
exact owned transient user unit; no runtime service affected. Not released.
|
||||||
|
N13a resultSHA256a82e88487095d31aacf70b4fcfc3ada23af4af361cbcbd1f78ee28bf2fb201f8.
|
||||||
|
Three Go files formatted through stdin/stdout of already qualified private
|
||||||
|
Go formatter; no Ubuntu file/runtime changes. Runtime sourceSHA256
|
||||||
|
sensor_models.go9075e0d37e1467fe1a98a5e286248ec23de2765c32189cedeb1bb9b29c4b3e5f,
|
||||||
|
sensors.gof0150bdfd4614b8a9be2d398493fc231f42839fba4bf71364010f551aac96342;
|
||||||
|
regression sourceSHA256a674b44e2d924ba3517393dbec6c869393deab6be6ac6e6a028a06d878a3403d.
|
||||||
|
N13b final source9aba79359708eca5ec14c762,2731files,309776910 bytes,SHA256
|
||||||
|
b086b7ac3dd7e3ebc88db20d9af2b4dd21ca607f263706e475fb34e09598c1d9.
|
||||||
|
Next same versioned node-only builder; B17 and Core N12 remain unchanged.
|
||||||
|
|
||||||
|
N13b complete12:38:20.733418–12:42:10.406938 UTC,229.673523105s,
|
||||||
|
peak2884808704 bytes. All DG/Node UI/Go race tests and binary build passed,
|
||||||
|
including new actual driver deadline test; no failed/retried jobs. Core UI remains
|
||||||
|
N12 exact bytes; no need to repeat unchanged821 tests/build. Node0.8.21-1,
|
||||||
|
168275612 bytes,SHA256a88ef1a4349a3d3188ca88b5cb87bf87d38beb68a2bdd89eadfa192e464ba36d.
|
||||||
|
ResultSHA256de7a8d70e0a622b7cd3d03743e2a2a3b074cecdac1df1b6d5551100f7f4d2778;
|
||||||
|
retrieved, archive entries and qualified bytes verified. N13a is superseded and
|
||||||
|
not admitted for installation; its private qualification report retained separately.
|
||||||
|
Final owner releasea0f5da65627a49343e9b912a,168297217 bytes,SHA256
|
||||||
|
e698646dfe918efc64abdccf3ddcac838ef47f3a5599e7e4903f6abcef261f93.
|
||||||
|
Next --plan/--launch after D455 idle, fixed model profile update, no runtime
|
||||||
|
hot copy/SSH sudo package workaround. X4 stays OFF/recoveryfalse for MANUAL02.
|
||||||
|
|
||||||
|
P13 plan upgrades Node0.8.21→0.8.21-1 only, removes0. D455 idle through Core,
|
||||||
|
reportSHA256752b6b6769a90f5fca48826e1b53e5168d18202f2a7fa275ed392b38b6500d97.
|
||||||
|
Owner installer window0.8.21-1 opened; latest readback before subsequent UI QA
|
||||||
|
showed no install steps yet, waiting local sudo authentication. X4 remains OFF.
|
||||||
|
|
||||||
|
Normal-width UI QA exposed title collapse beside the new text Button: configured
|
||||||
|
OFF device incorrectly supplied a full StatusBadge into DG leading compact slot.
|
||||||
|
Corrected common SensorWorkspace to indicator for every configured status;
|
||||||
|
full label remains aria-label/title. Unconfigured trailing badge unchanged.
|
||||||
|
This uses existing DG ResourceRow/StatusBadge contract, no new CSS/control/icon.
|
||||||
|
DG8c53f73 remains unchanged. Error toast from MANUAL01 dismissed; normal/expanded
|
||||||
|
controls work; detailed geometry recheck follows qualified UI build.
|
||||||
|
Node0.8.21-2/Core UI rebuild required for this shared row fix; B17/native/wake
|
||||||
|
code and timing unchanged. No unqualified UI hot copy or Ubuntu runtime patch.
|
||||||
|
|
||||||
|
N14 source30bb5fd6e468f8f024e1d059,2731files,309778048 bytes,SHA256
|
||||||
|
37362a7e002f8034d55f6e8329b657297233fe04568ea248cef7198466387315.
|
||||||
|
Same immutable Node-Core builder/cgroup, final source includes compact configured
|
||||||
|
status plus B17 and per-action deadline. Full Core validation required for changed UI.
|
||||||
|
|
||||||
|
Latest P13 launcher readback still had zero steps/results: no installation yet.
|
||||||
|
Owner informed to defer0.8.21-1 password in favour of combined0.8.21-2.
|
||||||
|
D455 restored through the already declared P13 Core instrument while waiting;
|
||||||
|
no wake/preview/SD command to X4. P14 stop/restore instruments prepared with
|
||||||
|
same guards, sources048a3a859d3c68ae40a0df33714568a941b88d3b103f39fb1210cd7ad19a3746
|
||||||
|
and88310d000694459e3f13062ede7ed8b38f1c201975d5e1d853294c4b4b95f3e6.
|
||||||
|
MANUAL02 read-only observer source remainsc793f7251a319114a0b1ef0e0ba9afb748f76bc5bff1d772bd7f032d0bd05a9c.
|
||||||
|
Core N14 activation instrument source505cbf8e8459e0547b1d3c668e898be8c7a7e81ee57034de405606585a0dc8ee:
|
||||||
|
fixed source/dist hash checks, before backup, only canonical launchd restart,
|
||||||
|
identity/health verification. It has not run; N14 UI acceptance remains pending.
|
||||||
|
|
||||||
|
|
||||||
|
### N14 qualified / OWNER-MANUAL-NODE / N15 online action visibility
|
||||||
|
|
||||||
|
N14 complete12:52:26.280386–13:01:00.883274 UTC, mono343054.161483472,
|
||||||
|
514.602917375s. DG/Node/Go race/Core architecture/typecheck,821 Core tests and
|
||||||
|
production build passed; one bounded Node UI dependency timeout recovered on
|
||||||
|
attempt2. Known native RRD fixture remains explicitly unqualified, not a passed test.
|
||||||
|
Node0.8.21-2:168275654bytes,SHA25680279b8ea7babe66072a05b7a2b4c331fdc736fbe3b7e9698a5b35fb3e0a719e.
|
||||||
|
Core dist23068479bytes,SHA2566c55344b8fc63f090963900f8e4c6115f0cb6148dcb610c78605107befc2da35.
|
||||||
|
ResultSHA256faa4502455694acd00e17c41596ffe0c3fb03cd44949dfe1da2d20f6f986ad8f;
|
||||||
|
retrieved and all archive paths/payload hashes verified. N14 Node not installed.
|
||||||
|
Core activated13:07:38.339686–13:08:11.002978 UTC, mono466138.326959791,
|
||||||
|
32.663109750s; canonical PID70307→73387, identities preserved, operational8000.
|
||||||
|
Activation reportSHA2568f27abede8e0453fe578baeee26450620074ad883e706d40c20f1bcc47ca32bd.
|
||||||
|
Normal narrow row, expand/restore and settings Escape visually passed in canonical
|
||||||
|
in-app browser at13:08 UTC: title readable beside Button, compact status fits.
|
||||||
|
No viewer/preview/recording command or second backend created.
|
||||||
|
|
||||||
|
Owner independently clicked wake in the Ubuntu Node application and confirmed
|
||||||
|
camera ON. Read-only board versions remain Node0.8.21/model0.1.3-8: superseded
|
||||||
|
P13 update was not applied. Core fresh readback: same X4 instance/new SDK session,
|
||||||
|
online, battery100, recovery=false/preview_wanted=false, preview0/recording0;
|
||||||
|
D455 same streaming session. This is owner-reported Node UI wake success with
|
||||||
|
SDK readback, not a measured command receipt or acceptance of the B17 timing fix.
|
||||||
|
Owner requests hiding the power button once connected. Shared X4 rowActions
|
||||||
|
now returns no power action for online devices; offline eligibility unchanged.
|
||||||
|
Existing visibility regression updated. Node0.8.21-3/N15 bundles the final UI with
|
||||||
|
B17; full shared UI validation follows. No system/runtime hot patch.
|
||||||
|
|
||||||
|
N15 sourcefee6989f47dd42ee2bb56e75,2731files,309780200bytes,SHA256
|
||||||
|
0c915e46b0714d3fcc94bacf2774a9b763e9e7e739bc83fbd6a5d4fa73ed6d08.
|
||||||
|
Staged and SHA verified on existing Ubuntu; same bounded user-cgroup builder
|
||||||
|
started for full Node/Core qualification, no runtime changes. B17 bytes unchanged.
|
||||||
|
Core N15 activation prepared from N14 fixed instrument; sourceSHA256416b811889e2f680eeb2154ba3b5a87053a87d027e2922def395248647e013b0.
|
||||||
|
|
||||||
|
OWNER-MANUAL-NODE readback13:11:32.904441 UTC,mono466372.890483,
|
||||||
|
reportSHA2560d217f2701c6b62505beae99608882e6b2143e5cd7312a8e76319ab1070dcd9a.
|
||||||
|
Private report persisted before a display-only summary lookup used missing kind
|
||||||
|
key; re-read by stable device_id succeeded. No hardware command or retry occurred.
|
||||||
|
|
||||||
|
Late readback found superseded P13 had actually been submitted locally at
|
||||||
|
13:00:19.894546 UTC (after prior zero-step observation), and ended13:00:27.757015,
|
||||||
|
7.862448686s with package step exit100 after baseline/apt-plan complete.
|
||||||
|
No successful package/model update: fresh dpkg readback remains Node0.8.21 and
|
||||||
|
model0.1.3-8, both install ok installed. Detailed root step output was not read;
|
||||||
|
exit100 alone does not prove its cause. D455 was streaming at this period.
|
||||||
|
Private owner tee logSHA2562231854b4114766dc9be332d9f6b7c58ea8ce39fd3f7f2d33612f588fd06697b.
|
||||||
|
No dpkg transaction was killed or manually repaired. Final release must pass
|
||||||
|
its own apt plan and guarded idle installation; D455 stop/restore already prepared.
|
||||||
|
|
||||||
|
N15 complete13:11:06.346138–13:20:02.834198 UTC,mono344174.227231158,
|
||||||
|
536.488056009s,peak3221229568bytes. DG/Node/Go race/Core architecture/typecheck,
|
||||||
|
full Core tests and production build passed. Node UI dependency timeout45s recovered
|
||||||
|
on bounded attempt2. Native RRD generator-dependent case remains unqualified.
|
||||||
|
Final Node0.8.21-3:168275654bytes,SHA25649f3611b9a2f9a9f117c849d42df78a5fba2dafb6143b30f2855f92ea9a719ae.
|
||||||
|
Core dist23068797bytes,SHA256e7ae3a57b96c918811b380661dd1ea20ab13b7e2c4d73d92b468910d05e38b3e.
|
||||||
|
ResultSHA256de5a6130866d37172858ed30d03015c14083a741fa093298ec4e736cbdec2ab4;
|
||||||
|
archive/payload validation passed. All22 changed runtime/test/package source files
|
||||||
|
match the frozen N15 source; only human status documents updated afterward.
|
||||||
|
Owner releaseabbcb90ff14dcb05b99eeedf,168297259bytes,SHA256
|
||||||
|
5b58f654ec3136de5028603734868880a843c42ba5c38e86c12730eeb8456435.
|
||||||
|
Next guarded plan/install; bundle B17 exactly as qualified. No manual system repair.
|
||||||
|
|
||||||
|
N15 Core activated13:21:43.450275–13:22:15.064260 UTC,mono466983.423901458,
|
||||||
|
31.613767250s; sole canonical PID73387→74340, identities preserved/health operational.
|
||||||
|
ReportSHA25638ce422822b25f9f5f69a3aba320c76c302dc3c847947ffb16a85eb242acc12d.
|
||||||
|
Core tests:821 passed,0 failed. Final in-app browser QA13:23UTC: actual connected
|
||||||
|
X4 has no power button in normal/expanded row; settings/eye remain; settings Escape
|
||||||
|
closes without mutation. Narrow title readable, no extra loader. No preview/SD command.
|
||||||
|
|
||||||
|
Final P15 owner artifact SHA verified on Ubuntu; --plan admits one Node upgrade,
|
||||||
|
0new/0removed packages. Existing B17 profile is carried in Node. D455 stopped
|
||||||
|
only through Core stop operation before launch; reportSHA256
|
||||||
|
eeb4516e75e4280cdbbc3c916c4f9cdec3c4ebbf268bbf7c767f9475c4f0f6b6.
|
||||||
|
--launch opened local Ubuntu window0.8.21-3. Owner asked for required local OS
|
||||||
|
password in this final window, then camera Power-off with USB kept for MANUAL02.
|
||||||
|
No password accepted through SSH/chat; no ad-hoc privileged repair.
|
||||||
|
|
||||||
|
P15 complete13:24:18.998120–13:24:46.634699 UTC,mono344966.879222148,
|
||||||
|
27.636570683s. All steps complete; Node0.8.21-3, existing X4 model updated through
|
||||||
|
fixed bundled profile to0.1.3-9, ui_ready=true. camera_verification not-performed
|
||||||
|
as declared by package update. Both dpkg statuses install ok installed.
|
||||||
|
Owner tee logSHA256b9c687f468425d58f010a9574a36d82455a965659f17354e85ac5a9cf6f1b7db.
|
||||||
|
D455 restored with record=false after install, reportSHA256
|
||||||
|
67a8ee22d2d18fe85ed57f1a45fb8044b2b0ef8b99fb5c5e08a0def74b57bf11.
|
||||||
|
Fresh Core readback: same vehicle/node/device IDs, both cameras online;
|
||||||
|
D455 streaming; X4 idle/preview0/recording0/recoveryfalse/preview_wantedfalse.
|
||||||
|
Owner informed installation completed and no further password needed; only
|
||||||
|
Power-off with USB retained is awaited for MANUAL02. No new wake issued yet.
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
# Insta360 X4 — состояние реализации 10.09.2026
|
# Insta360 X4 — состояние реализации 10.09.2026
|
||||||
|
|
||||||
|
Актуально10.09.2026: установлены Node0.8.21-3 и X4 model0.1.3-9 (B17/N15).
|
||||||
|
Обновление27.64s прошло через штатный owner installer и bundled profile;
|
||||||
|
Core активирован на8000, identities сохранены. Ручное включение ранее
|
||||||
|
подтверждено из Core и приложения Ubuntu Node. После обнаруженной задержки
|
||||||
|
загрузки X4 до37s B17 ждёт подтверждение до52s после единственного20s BLE
|
||||||
|
beacon; Node transport ограничен55s.87 модельных тестов и полная N15
|
||||||
|
квалификация прошли:821 Core tests, architecture/typecheck/build, DG/Node/Go race.
|
||||||
|
|
||||||
|
По последнему требованию владельца кнопка «Включить» полностью скрыта у online
|
||||||
|
X4, возвращается при offline. Обе UI используют общий frontend. В Core
|
||||||
|
проверены реальная online камера, нормальная/развёрнутая строка и settings Escape.
|
||||||
|
X4 сейчас online/idle, recovery=false/preview_wanted=false, запись0; D455
|
||||||
|
восстановлена и ведёт захват. MANUAL02 для позднего complete receipt остаётся
|
||||||
|
pending: владелец получил шаг Power-off с сохранённым USB; повторного wake
|
||||||
|
в N15 ещё не было. [Контракт](16_INSTA360_X4_MANUAL_WAKE.md),
|
||||||
|
[план](10_INSTA360_X4_NEXT_STEPS.md).
|
||||||
|
|
||||||
|
Ниже — история доказательств; версии и прежние формулировки следующих шагов
|
||||||
|
относятся к указанным опытам. Последний статус задаёт абзац выше и ledger07.
|
||||||
|
|
||||||
## REC01–02 / LIVE08–09 — два последовательных цикла восстановления приняты
|
## REC01–02 / LIVE08–09 — два последовательных цикла восстановления приняты
|
||||||
|
|
||||||
[Per-instance recovery](15_INSTA360_X4_AUTOMATIC_RECOVERY.md) добавлен в broker
|
[Per-instance recovery](15_INSTA360_X4_AUTOMATIC_RECOVERY.md) добавлен в broker
|
||||||
@@ -36,11 +56,23 @@ REC02/LIVE09: второй предложенный USB out/in подтверж
|
|||||||
подтвердил 2 кадра за 124 ms. D455 сохранила session, color10318→17512;
|
подтвердил 2 кадра за 124 ms. D455 сохранила session, color10318→17512;
|
||||||
службы не перезапускались. Два последовательных возврата SDK/preview приняты.
|
службы не перезапускались. Два последовательных возврата SDK/preview приняты.
|
||||||
|
|
||||||
Текущее состояние CANCEL01: восстановление намеренно выключено через Core
|
CANCEL01 принят в ограниченном окне: после отключения recovery в Core владелец
|
||||||
11:34:30 UTC для следующего теста ручного Power-off с оставленным USB.
|
выключил X4 кнопкой Power, оставив USB, затем видел индикацию зарядки.
|
||||||
Readback enabled=false/phase=disabled/preview_wanted=false; просмотр продолжает
|
За 160.5 секунды, 40 последовательных samples, камера не вернулась в SDK,
|
||||||
работать, запись0. Владелец ещё должен выключить камеру; отсутствие самопробуждения
|
recovery disabled/preview_wanted=false; D455 сохранила session и поток.
|
||||||
не принято до наблюдения. После проверки нужно вернуть рабочее recovery.
|
Ubuntu read-only11:42:10 и11:45:08 UTC: нет SDK/Mass Storage enumeration X4;
|
||||||
|
последний BlueZ ActiveInstances0, службы без рестартов. Индикация зарядки
|
||||||
|
не сопровождалась рабочим подключением. Электрическое состояние питания и
|
||||||
|
непрерывная radio-активность не измерялись.
|
||||||
|
|
||||||
|
Исходное состояние MANUAL01: камера отключена, recovery намеренно выключено.
|
||||||
|
Владелец заменил физическое Power на запрос кнопки «Включить» в строке X4
|
||||||
|
Core/Node. Реализован разовый power.wake без auto-enable/preview/SD; B16
|
||||||
|
model0.1.3-8 прошёл85 Ubuntu tests, Core fleet31 tests passed. N12 Node0.8.21
|
||||||
|
позднее прошёл квалификацию и установлен в P12; MANUAL01 результат указан выше.
|
||||||
|
Обновление уже установленной модели выполняется owner installer через fixed
|
||||||
|
bundled profile даже при offline X4; первое prepare остаётся в приложении.
|
||||||
|
Подробный контракт и приёмка — [MANUAL01](16_INSTA360_X4_MANUAL_WAKE.md).
|
||||||
|
|
||||||
## WAKE01 — первый успешный Bluetooth wake с возвратом Android
|
## WAKE01 — первый успешный Bluetooth wake с возвратом Android
|
||||||
|
|
||||||
@@ -166,7 +198,7 @@ X4 — операторский несшитый просмотр и управ
|
|||||||
|
|
||||||
Форматирование, удаление файлов, firmware update и power/reset не включены. SD recording не повторялась для запуска LIVE04. SDK boolean capture status и поведение firmware при потере USB требуют отдельной аппаратной проверки.
|
Форматирование, удаление файлов, firmware update и power/reset не включены. SD recording не повторялась для запуска LIVE04. SDK boolean capture status и поведение firmware при потере USB требуют отдельной аппаратной проверки.
|
||||||
|
|
||||||
## Версии и состав принятого артефакта
|
## Исторический P08/P09 — версии и состав артефакта
|
||||||
|
|
||||||
- Node.deb0.8.18-1:168257000 bytes, SHA2569e5aaff4a8e4a2ed24cef56bcc9bd9721971df3154fa48be8595fef8591bdbb3. P09 complete09.09.2026 в04:14:52.735120 UTC; binary/UI0.8.18 сохранены.
|
- Node.deb0.8.18-1:168257000 bytes, SHA2569e5aaff4a8e4a2ed24cef56bcc9bd9721971df3154fa48be8595fef8591bdbb3. P09 complete09.09.2026 в04:14:52.735120 UTC; binary/UI0.8.18 сохранены.
|
||||||
- X4.deb0.1.3-2:58198804 bytes, SHA2562931a5206478fcac67f74bd1fb579b98c0fcc43eda4c05e5ac4a721947619130, runtime revisionf78a7ae557e3371022869025.
|
- X4.deb0.1.3-2:58198804 bytes, SHA2562931a5206478fcac67f74bd1fb579b98c0fcc43eda4c05e5ac4a721947619130, runtime revisionf78a7ae557e3371022869025.
|
||||||
|
|||||||
@@ -1,91 +1,41 @@
|
|||||||
# Insta360 X4 — следующий план после первого живого просмотра
|
# Insta360 X4 — следующий план
|
||||||
|
|
||||||
Срез10.09.2026. Основания: исходный Mission Core Node Context, разделы8/25/26/29; [план X4](06_INSTA360_X4_AND_MULTI_CAMERA_PLAN.md), [текущее доказательство](09_INSTA360_X4_IMPLEMENTATION_STATUS.md), [журнал](07_INSTA360_X4_INSTALLATION_LEDGER.md), последние решения владельца. Исторический нулевой baseline не отменяет PREP04/LIVE04; наличие live stream не закрывает весь P4/P4C/P7.
|
Срез10.09.2026. Основания: исходный Mission Core Node Context, разделы8/25/26/29;
|
||||||
|
[план X4](06_INSTA360_X4_AND_MULTI_CAMERA_PLAN.md),
|
||||||
|
[результаты](09_INSTA360_X4_IMPLEMENTATION_STATUS.md),
|
||||||
|
[журнал](07_INSTA360_X4_INSTALLATION_LEDGER.md) и последние решения владельца.
|
||||||
|
|
||||||
## Подтверждённая исходная точка
|
Ближайший шаг — завершить [MANUAL02](16_INSTA360_X4_MANUAL_WAKE.md).
|
||||||
|
Ручное включение уже вернуло камеру из Core и локального Node. Исправление B17
|
||||||
|
должно подтвердить возврат через52s окно, без повторного BLE beacon и ранней
|
||||||
|
ложной ошибки. N15 Node0.8.21-3 объединяет его с общей UI: у online X4 кнопка
|
||||||
|
«Включить» отсутствует, при offline возвращается; pending только внутри кнопки.
|
||||||
|
N15 прошёл полную квалификацию, Core активирован; исчезновение кнопки при
|
||||||
|
online и геометрия проверены. Node0.8.21-3/B17 установлен штатно, D455 восстановлена. Остался
|
||||||
|
новый hardware receipt MANUAL02 после физического Power-off владельцем;
|
||||||
|
USB остаётся подключённым, recovery выключено. Промежуточные Node0.8.21-1/-2 не установлены.
|
||||||
|
|
||||||
X4 подключена к SDK на той же Ubuntu Mini. Удалённая подготовка через Core разворачивает встроенный model package и проверяет exact instance. Без SD-записи работают первый WebRTC START, viewer reopen и STOP→START;1280×640, несшитые изображения двух объективов. SD START/STOP отдельно подтверждены ранее. Это одна физическая камера и ограниченная лабораторная проверка.
|
## Что уже подтверждено
|
||||||
|
|
||||||
Дополнение LIVE05: пользовательский stale preview восстановился после одного явного STOP→START,100-секундный приём подтвердил свежие кадры. Причина исходного зависания ещё не исправлена. Verify во время живого потока дал ложный отрицательный результат; ближайшая реализация должна проверять свежий общий decoder при активном preview и сохранять адресную причину ошибки, не создавая второго владельца SDK/не перезапуская capture неявно. Не путать это восстановление с завершённой приёмкой устойчивости.
|
- Подготовка и управление через один контракт Core/Node, versioned model package.
|
||||||
|
- Удалённый несшитый WebRTC1280×640, repeat START/verify/viewer reopen;
|
||||||
|
SD START/STOP проверены отдельно. Это не измерение glass-to-glass latency.
|
||||||
|
- Core–Node восстановлены через Tailscale с сохранением trust/identity.
|
||||||
|
- Два последовательных USB out/in при recovery=true вернули SDK и preview
|
||||||
|
без ручного START;339/326 WebRTC кадров за30s, D455 сохранила session.
|
||||||
|
- После recovery=false и ручного Power-off за160.5s/40samples SDK не вернулся.
|
||||||
|
- Один адресный BLE wake возвращает X4 в Android/SDK; ручная команда не включает
|
||||||
|
recovery и не запускает preview/SD. Корректное позднее подтверждение B17 pending.
|
||||||
|
- PORT01: root hubs Mac mini не поддерживают адресное снятие VBUS. Электрические
|
||||||
|
измерения отсутствуют; управляемый хаб остаётся отдельной аппаратной веткой.
|
||||||
|
|
||||||
B11/P09,09.09: эта реализация готова и прошла38 Ubuntu tests. Дополнительно исправлен доказанный code path, в котором idempotent повторный START очищал feed без нового keyframe. Первоначально model0.1.3-3 ожидал штатной подготовки; ниже зафиксирован завершённый PREP05/LIVE06 на Node0.8.19. Остальные причины возможных разрывов и длительная устойчивость остаются в плане.
|
## Порядок оставшейся работы
|
||||||
|
|
||||||
Предварительное условие NET01 закрыто NET02/NET03,10.09: Core–Node восстановили канал через Tailscale с прежними trust/identity. В Core подтверждены несколько свежих mTLS heartbeat, endpoint revision1. После восстановления заново читать актуальное состояние камеры перед любыми preview/model операциями; прежний stale inventory не подтверждает её текущее состояние.
|
|
||||||
|
|
||||||
P10/PREP05 complete: Node0.8.19 и X4 model0.1.3-3 установлены штатно. LIVE06 подтвердил первый и повторный START, active verify и viewer reopen:1083 декодированных кадра1280×640, без SD записи. REPLUG01 выявил отказ active USB replug; REPLUG02/LIVE07 восстановили камеру после camera off/on. OWNER03 подтвердил автоматический Android mode после этого включения. REPLUG03 подтвердил такой же отказ при fresh idle/preview0. Владелец исключил замену кабеля из ближайшего gate. PORT01: порты/контроллер активны, early_stop=no, перегрузок0; измерителей VBUS/тока нет. Root reader подтвердил No power switching у обоих xHCI root hubs: штатно снимать VBUS встроенного порта нельзя. RESEARCH01 уточнил приоритет: сначала беспроводное чтение внутреннего udisk_mode и проверка программного возврата Android либо BLE off/on самой камеры по плану12. Управляемый хаб — отдельная аппаратная альтернатива; отсутствие VBUS switching не доказывает невозможность программного обхода. Recovery SDK/preview — после возврата USB. Задержка NET03 recovery и длительная video stability остаются открытыми.
|
|
||||||
|
|
||||||
OWNER03: владелец видит изображение X4 и D455 через Core при переходах между карточками. Read-only Node snapshot обеих камер online/streaming. Длительная одновременная работа и измерения ещё не приняты.
|
|
||||||
|
|
||||||
BLE01/BLE02: [первый диагностический модуль](13_INSTA360_X4_BLE_DIAGNOSTIC.md)
|
|
||||||
реализован и включён в исходники model package0.1.3-4. На Ubuntu два bounded
|
|
||||||
наблюдения не обнаружили X4; экран камеры был тёмный. POWER03: владелец
|
|
||||||
подтвердил короткое Power с уже вставленным USB → Android без выбора режима;
|
|
||||||
тот же instance автоматически получил новую SDK session. BLE03 теперь видит
|
|
||||||
одного свежего BE80 кандидата с именем, совпадающим с USB serial suffix;
|
|
||||||
это не полная BLE authentication. Следующий hardware шаг — установить состояние
|
|
||||||
Bluetooth Wakeup и квалифицировать адресный wake artifact. GetOptions остаётся
|
|
||||||
отдельной веткой при отсутствии активного USB SDK owner. Ни setter, ни BLE
|
|
||||||
power cycle ещё не выполнялись; POWER03 не заменяет автоматический тест.
|
|
||||||
Node0.8.19 bundle пока остаётся0.1.3-3; его обновление не считается завершённым.
|
|
||||||
|
|
||||||
## Очередь реализации
|
|
||||||
|
|
||||||
**REC01 в работе:** код per-instance wake/preview recovery и настройка Core/Node
|
|
||||||
реализованы; B15 model0.1.3-7 прошёл75 Ubuntu tests. Node0.8.20 и X40.1.3-7
|
|
||||||
установлены штатно; общая UI активирована в Core, identities сохранены.
|
|
||||||
REC01/LIVE08 принял один реальный USB out/in: владелец подтвердил Android,
|
|
||||||
прежняя X4 получила новую SDK session и preview без ручного START. Получено
|
|
||||||
339 кадров 1280×640 за 30 секунд и успешный active verify; D455 сохранила
|
|
||||||
свою session. REC02/LIVE09 повторил результат: новая session X4, 326 кадров
|
|
||||||
и successful verify без START/STOP, D455 непрерывна. Два цикла приняты.
|
|
||||||
Ближайшее действие — CANCEL01: recovery уже штатно выключено через Core;
|
|
||||||
владелец выключает X4 кнопкой Power при оставленном USB, затем наблюдаем
|
|
||||||
отсутствие автоматического пробуждения. После проверки вернуть recovery;
|
|
||||||
следующий отдельный gate — cold start. Два цикла не закрывают всю матрицу
|
|
||||||
устойчивости/ограниченных retries.
|
|
||||||
Архитектура и границы — в docs15.
|
|
||||||
|
|
||||||
**Актуальный результат WAKE01:** один адресный Bluetooth wake с Ubuntu
|
|
||||||
разбудил X4 при подключённом USB; камера сама вернулась в Android/SDK,
|
|
||||||
владелец подтвердил экран. Node открыл прежний instance, D455 сохранила поток.
|
|
||||||
Модуль упакован в0.1.3-6,59 Ubuntu tests passed, новая модель не установлена.
|
|
||||||
REPLUG04 без wake остался неуспешным. Ближайший increment:
|
|
||||||
|
|
||||||
1. Сохранять полную identity и явное намерение восстановления отдельно для
|
|
||||||
каждого instance; не будить намеренно выключенную оператором камеру.
|
|
||||||
2. Подключить bounded wake и сериализацию adapter ownership; отменять при
|
|
||||||
появлении SDK, ограничивать retries, не повторять SD-команды при unknown.
|
|
||||||
3. Доставить через Node bundle и штатную подготовку из Core/Node, затем
|
|
||||||
проверить возврат SDK/preview, несколько циклов и независимость камер.
|
|
||||||
|
|
||||||
Camera-side Bluetooth Wakeup On остаётся явной предпосылкой. Wake при тёмном
|
|
||||||
дисплее не доказывает recovery зависшей включённой камеры или clean Ubuntu.
|
|
||||||
Следующий абзац сохраняет порядок предыдущих опытов; их исходы уточнены ниже.
|
|
||||||
|
|
||||||
Уточнение BLE04/POWER04: Bluetooth Wakeup теперь включён владельцем. GATT
|
|
||||||
Connect прошёл, но первый identity GetOptions получил неподдержанный7-byte
|
|
||||||
type5 packet, поэтому без retries/setters закрыт; это отдельный вопрос framing.
|
|
||||||
Обнаруженный штатный Power-off Charging → Charge and power on проверяется
|
|
||||||
раньше следующей BLE-команды. Observer зафиксировал возврат Mass Storage→SDK
|
|
||||||
и новой session; владелец подтвердил самостоятельный Android после camera off
|
|
||||||
→ USB out → USB in. Один запуск выключенной X4 от подачи питания подтверждён.
|
|
||||||
REPLUG04 включённой X4 завершился отказом; успех POWER04 его не закрывает.
|
|
||||||
Ручные camera-side flags фиксируются как prerequisites, не скрытая доработка
|
|
||||||
Ubuntu. «Зарядка и запись» не используется для операторского preview.
|
|
||||||
|
|
||||||
**Уточнение владельца10.09 после NET03:** ближайший приоритет после применения
|
|
||||||
B11 — автономность подключения и питание. Проверить сохранение Android-режима,
|
|
||||||
replug/cold start, автоматическое восстановление SDK и просмотра; определить
|
|
||||||
реальное VBUS switching существующего Mac mini. Это предшествует расширению
|
|
||||||
camera settings/file download. Конкретная матрица, доказательства и ограничения
|
|
||||||
в [плане автономности](11_INSTA360_X4_AUTONOMY_AND_USB_POWER.md); прежняя очередь
|
|
||||||
ниже сохраняет остальные работы.
|
|
||||||
|
|
||||||
| Порядок / связь с исходным планом | Работа | Конкретный выход |
|
| Порядок / связь с исходным планом | Работа | Конкретный выход |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Завершено, P5 | Общие состояния loading в DG: pending инициирующей кнопки, центр content region, отсутствие бесконечной загрузки после ошибки. Shared frontend для Core и Node, каталог и правила. | UI01/N09 установлен в Core8000 и Node0.8.18. Сборки/819 Core tests, DG tests/catalog и Node checks прошли. Browser geometry, normal/expanded/Escape, light/dark и error lifecycle проверены. Native Node visual QA ещё открыта. |
|
| Завершено, P5 | Общие состояния loading в DG: pending инициирующей кнопки, центр content region, отсутствие бесконечной загрузки после ошибки. Shared frontend для Core и Node, каталог и правила. | UI01/N09 установлен в Core8000 и Node0.8.18. Сборки/819 Core tests, DG tests/catalog и Node checks прошли. Browser geometry, normal/expanded/Escape, light/dark и error lifecycle проверены. Native Node visual QA ещё открыта. |
|
||||||
| B11 ограниченно принят, P3/P4 | Исправлено очищение feed при повторном START и проверка активного потока через новый decoder. PREP05/LIVE06: repeat START, live verify, viewer reopen complete. |1083 кадра1280×640; долгий capture и все возможные причины stale отдельно не приняты. |
|
| B11 ограниченно принят, P3/P4 | Исправлено очищение feed при повторном START и проверка активного потока через новый decoder. PREP05/LIVE06: repeat START, live verify, viewer reopen complete. |1083 кадра1280×640; долгий capture и все возможные причины stale отдельно не приняты. |
|
||||||
| Первый следующий, P3/P4 | RESEARCH01: через versioned artifact проверить BLE identity/framing/auth и прочитать Options.udisk_mode (95). Затем при подтверждённой семантике — ограниченный Android setter; резерв — BLE remote off/on при уже вставленном USB. [Доказательства и границы](12_INSTA360_X4_ANDROID_RECONNECT_RESEARCH.md). Хаб — альтернативная аппаратная ветка. После USB enumeration — SDK recovery/preview intent по плану11. | Прежняя X4 без рук возвращается в Android, новая SDK session и свежий кадр; прочие камеры/Node online. Наличие параметра в SDK и ACK не являются приёмкой. |
|
| Первый следующий, P3/P4 | Завершить MANUAL02 на B17/Node0.8.21-3: скрытие кнопки при online, один wake с корректным complete, policy=false и отсутствие SD/preview replay. Затем отдельный cold start с сохранённым recovery/preview intent и повторные disconnect/retry/cancel. | Прежний instance, новая SDK session, корректный receipt и UI; D455/Node сохраняют работу. Два уже принятых replug не заменяют cold start и всю fault matrix. |
|
||||||
| Следующий, P4 + P5 | Совместимый локальный просмотр в установленном Node/WebKit. Исходный план предусматривает HTTP/fMP4 carrier от той же медиа-сессии; переиспользуются общие media lifecycle/lease части K1, без импорта K1 protocol или второго SDK владельца X4. | На Ubuntu в приложении Node есть движущееся изображение; одновременно открыт удалённый Core. Закрытие одного зрителя сохраняет другого, не меняет SD recording. Все runtime dependencies входят в .deb/profile. |
|
| Следующий, P4 + P5 | Совместимый локальный просмотр в установленном Node/WebKit. Исходный план предусматривает HTTP/fMP4 carrier от той же медиа-сессии; переиспользуются общие media lifecycle/lease части K1, без импорта K1 protocol или второго SDK владельца X4. | На Ubuntu в приложении Node есть движущееся изображение; одновременно открыт удалённый Core. Закрытие одного зрителя сохраняет другого, не меняет SD recording. Все runtime dependencies входят в .deb/profile. |
|
||||||
| Затем, P4C | Матрица поддержанных действий по фактическим SDK headers и capabilities: camera settings, normal photo/video, ISO/exposure/WB/resolution, отдельные SD start/stop и фото. Проверять readback, mode dependencies и отображение результата. | Явные адресные команды из обеих UI, реальные результаты на камере, неизвестный outcome не переигрывается автоматически. Не обещать все режимы SDK до их проверки; shutter/EV setters и неподдержанные поля перечисляются отдельно. |
|
| Затем, P4C | Матрица поддержанных действий по фактическим SDK headers и capabilities: camera settings, normal photo/video, ISO/exposure/WB/resolution, отдельные SD start/stop и фото. Проверять readback, mode dependencies и отображение результата. | Явные адресные команды из обеих UI, реальные результаты на камере, неизвестный outcome не переигрывается автоматически. Не обещать все режимы SDK до их проверки; shutter/EV setters и неподдержанные поля перечисляются отдельно. |
|
||||||
| P4C + общий source-record workflow | Получение содержимого выбранного файла на Node и в Core, bounded transfer, прерывание/продолжение, длина/hash и происхождение. Каталог уже есть, download ещё нет. | Пользователь получает существующий файл без shell; повторы/обрыв связи не меняют оригинал на SD. Незавершённый записываемый файл обрабатывается явно. |
|
| P4C + общий source-record workflow | Получение содержимого выбранного файла на Node и в Core, bounded transfer, прерывание/продолжение, длина/hash и происхождение. Каталог уже есть, download ещё нет. | Пользователь получает существующий файл без shell; повторы/обрыв связи не меняют оригинал на SD. Незавершённый записываемый файл обрабатывается явно. |
|
||||||
@@ -93,10 +43,27 @@ camera settings/file download. Конкретная матрица, доказа
|
|||||||
| P1/P3/P6 | Несколько одинаковых X4 и D455, одновременная K1, exact-device routing, isolation/crash/replug. D455 shared-process migration остаётся отдельной реализацией. |2/4 реальные камеры независимы;500 synthetic descriptors не превращаются в обещание500 потоков. Текущий admission4 sources/4 peers/2 peers per camera пересматривается только по измерениям. При отсутствии устройств hardware часть остаётся открытой. |
|
| P1/P3/P6 | Несколько одинаковых X4 и D455, одновременная K1, exact-device routing, isolation/crash/replug. D455 shared-process migration остаётся отдельной реализацией. |2/4 реальные камеры независимы;500 synthetic descriptors не превращаются в обещание500 потоков. Текущий admission4 sources/4 peers/2 peers per camera пересматривается только по измерениям. При отсутствии устройств hardware часть остаётся открытой. |
|
||||||
| P7 | Чистая Ubuntu24.04 Desktop amd64 с закреплённым образом; полный installer → discovery → prepare из обеих UI → image → управление; upgrade/repair/rollback/failure matrix. | Никакой системной предпосылки вне installer, repeat/recovery сохраняют identity/trust/recordings. Текущий рабочий диск Mini не стирается. Отдельный чистый носитель/образ — необходимое условие, не предположение о наличии второй машины. |
|
| P7 | Чистая Ubuntu24.04 Desktop amd64 с закреплённым образом; полный installer → discovery → prepare из обеих UI → image → управление; upgrade/repair/rollback/failure matrix. | Никакой системной предпосылки вне installer, repeat/recovery сохраняют identity/trust/recordings. Текущий рабочий диск Mini не стирается. Отдельный чистый носитель/образ — необходимое условие, не предположение о наличии второй машины. |
|
||||||
|
|
||||||
Критический путь по последнему решению владельца: **беспроводной возврат Android (план12) → replug/cold start/SDK recovery/preview intent → локальное видео Node → приёмка управления/фото → получение файлов**. Адресное питание остаётся альтернативной веткой восстановления. B11 имеет ограниченную аппаратную приёмку; длительная устойчивость остаётся самостоятельным gate. Контракт и учёт clean install проверяются при каждом изменении, финальная установка на чистый носитель завершает воспроизводимость. Общее наблюдение K1/monitor и их открытые fault/backfill/source-import проверки сохраняются в исходной матрице; успех X4 их не закрывает.
|
|
||||||
|
|
||||||
Stitching не включается: текущая цель — операторский обзор с низкой задержкой. Worker, AI и управление движением не являются зависимостями. Если нужен сшитый обзор позднее, это отдельный профиль с собственной измеренной задержкой.
|
Критический путь: **ручная кнопка и корректный receipt → cold start и матрица
|
||||||
|
recovery → локальное видео Node → приёмка управления/фото → получение файлов**.
|
||||||
|
Чтение BLE Options/udisk_mode остаётся исследовательской альтернативой при
|
||||||
|
недостаточности подтверждённого wake; legacy PowerOff/SetMode не отправлять
|
||||||
|
без доказательства точной X4 семантики. Управляемое питание — отдельная ветка.
|
||||||
|
|
||||||
## Условия поставки и выполнения
|
## Условия выполнения
|
||||||
|
|
||||||
Работа идёт на существующей Ubuntu Mini через versioned artifacts с hashes/UTC/monotonic, без ручных apt/pip/служебных правок. Изменённый DG сначала становится общим компонентом, затем встраивается в обе UI. Внешний релиз vendor SDK требует отдельно разрешённых условий распространения. Полный интерфейс управления не включает неоговорённые destructive operations: delete/format/firmware/power.
|
Работа идёт на существующей Ubuntu Mini через versioned artifacts с hashes,
|
||||||
|
UTC/monotonic, без ручных apt/pip/правок служб. Каждая runtime предпосылка входит
|
||||||
|
в installer или подготовку модели с первого опыта. Общие controls приходят из
|
||||||
|
Design Guideline и используются в обеих UI. Текущий рабочий диск не стирается;
|
||||||
|
чистый носитель/образ требуется для clean-Ubuntu приёмки.
|
||||||
|
|
||||||
|
Camera-side Bluetooth Wakeup On остаётся явной предпосылкой. Перед намеренным
|
||||||
|
Power-off отключать recovery: исчезновение USB само по себе не различает
|
||||||
|
физическое выключение и потерю кабеля. Ручной wake сохраняет настройку recovery.
|
||||||
|
Внешний релиз vendor SDK требует отдельно подтверждённых условий распространения.
|
||||||
|
Delete/format/firmware/power-off не входят в проверенные команды.
|
||||||
|
|
||||||
|
Stitching, Worker, AI и управление движением не являются зависимостями.
|
||||||
|
Несколько сотен descriptors не означают сотни одновременных видеопотоков;
|
||||||
|
предел определяется измерениями и независимостью экземпляров.
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
# X4 — восстановление подключения и просмотра
|
# X4 — восстановление подключения и просмотра
|
||||||
|
|
||||||
REC01 / B15, 10.09.2026. Продолжение принятого WAKE01. Штатно установлены
|
REC01 / B15, 10.09.2026. Продолжение принятого WAKE01. На этапе REC01 штатно установлены
|
||||||
Node 0.8.20 и X4 0.1.3-7, общая UI активирована в Core. Настройка включена
|
Node0.8.20/X40.1.3-7; настройка была включена через Core. Последующие
|
||||||
через Core. REC01–02/LIVE08–09 приняли два последовательных USB out/in
|
версии и текущее состояние — в docs09/16. REC01–02/LIVE08–09 приняли два последовательных USB out/in
|
||||||
с возвратом SDK и preview без ручного START. Для CANCEL01 настройка временно
|
с возвратом SDK и preview без ручного START. CANCEL01 принял отсутствие возврата
|
||||||
выключена через Core; физическое ручное выключение и проверка отмены ещё открыты.
|
в SDK в течение 160.5s после ручного Power-off при отключённом recovery.
|
||||||
|
Настройка пока выключена. MANUAL01 добавляет явную кнопку включения без
|
||||||
|
auto-enable; физическое Power заменено этим запросом владельца.
|
||||||
|
См. [разовое включение](16_INSTA360_X4_MANUAL_WAKE.md).
|
||||||
|
|
||||||
## Поведение и полномочия
|
## Поведение и полномочия
|
||||||
|
|
||||||
@@ -35,8 +38,10 @@ OperationRequest; unknown не повторяется в той же сесси
|
|||||||
Новый `mission-core-insta360-wake.service` — отдельный root helper с Unix socket,
|
Новый `mission-core-insta360-wake.service` — отдельный root helper с Unix socket,
|
||||||
kernel peer credentials допускают только root и UID model broker. Нет public
|
kernel peer credentials допускают только root и UID model broker. Нет public
|
||||||
listener, SDK loading, USB writes, произвольных serial/path/command arguments.
|
listener, SDK loading, USB writes, произвольных serial/path/command arguments.
|
||||||
При включении helper сам читает единственный полный serial из текущего USB
|
Helper сам читает единственный полный serial из текущего USB binding и
|
||||||
binding и сохраняет в root-only StateDirectory. API принимает только device_id,
|
сохраняет в root-only StateDirectory. Начиная с model0.1.3-8, registry сохраняет
|
||||||
|
identity при обнаружении независимо от разрешения auto-recovery; выключение
|
||||||
|
политики не удаляет возможность отдельного manual wake. API принимает только device_id,
|
||||||
boolean control либо attempt UUID. Известные совпадения radio suffix запрещены.
|
boolean control либо attempt UUID. Известные совпадения radio suffix запрещены.
|
||||||
|
|
||||||
Один adapter lock сериализует wake для разных камер. Helper независимо
|
Один adapter lock сериализует wake для разных камер. Helper независимо
|
||||||
@@ -84,5 +89,10 @@ REC02/LIVE09 повторил возврат: новая session от 11:31:11.4
|
|||||||
326 кадров 1280×640 за 30 секунд и verify2/124ms; D455 без смены session.
|
326 кадров 1280×640 за 30 секунд и verify2/124ms; D455 без смены session.
|
||||||
CANCEL01-ARM 11:34:30 UTC подтвердил через штатный Core recovery.configure
|
CANCEL01-ARM 11:34:30 UTC подтвердил через штатный Core recovery.configure
|
||||||
enabled=false/preview_wanted=false/phase=disabled. Запись0, capture сохранён.
|
enabled=false/preview_wanted=false/phase=disabled. Запись0, capture сохранён.
|
||||||
Это проверка доставки отключения; отсутствие wake после ручного Power-off
|
После ручного Power-off с оставленным USB CANCEL01-OBSERVE получил 40 samples
|
||||||
нужно проверить отдельно. После опыта вернуть настройку восстановления.
|
за 160.5s без возврата X4 в SDK, с disabled/preview_wanted=false. D455 сохранила
|
||||||
|
session и поток; независимое чтение Ubuntu не видит SDK/Mass Storage X4,
|
||||||
|
BlueZ ActiveInstances0 в конечной точке. Это ограниченная проверка отмены,
|
||||||
|
не измерение потребления или непрерывной radio-активности. После этого камера оставалась выключенной для MANUAL01; позже кнопка
|
||||||
|
Core/Node вернула SDK. Recovery остаётся выключенным.
|
||||||
|
Она не меняет настройку восстановления и не запускает preview.
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# X4 — разовое включение из списка устройств
|
||||||
|
|
||||||
|
MANUAL01 / B16, 10.09.2026. Владелец запросил кнопку рядом с настройками и
|
||||||
|
просмотром в списке устройств Core и Node. X4 после CANCEL01 остаётся выключенной,
|
||||||
|
USB подключён; физическое Power для следующего теста больше не требуется.
|
||||||
|
|
||||||
|
## Продуктовое размещение
|
||||||
|
|
||||||
|
Задача оператора — включить известную камеру удалённо после завершённого цикла
|
||||||
|
работы. Сущность и полномочие принадлежат одному stable device_id на выбранном
|
||||||
|
борту. Используется существующая строка ResourceRow и Button «Включить»;
|
||||||
|
отдельный экран или действие только внутри просмотра не дают запрошенного
|
||||||
|
доступа из списка. Новый workspace/navigation/визуальный компонент не вводится.
|
||||||
|
Применены DG ResourceRow.actions, Button.loading, ToastStack и текущие токены;
|
||||||
|
DG8c53f73 не меняется. Платформенная SensorUiContribution.rowActions содержит
|
||||||
|
типизированные действия плагина; Core и Node используют один SensorWorkspace.
|
||||||
|
|
||||||
|
Кнопка доступна при свежей связи с Node, подготовленном/enrolled устройстве,
|
||||||
|
offline X4 и сохранённой identity для wake. После подтверждённого online кнопка полностью скрывается: включённой камере
|
||||||
|
повторное включение не требуется (уточнение владельца10.09). При следующем
|
||||||
|
offline она возвращается; при неизвестной identity действие недоступно. Лоудер находится только внутри
|
||||||
|
нажатой кнопки, повтор блокируется; соседние несовместимые действия disabled.
|
||||||
|
Подготовленные устройства используют compact StatusBadge indicator во всех
|
||||||
|
состояниях: leading-slot согласно DG допускает именно компактную лампу, полный
|
||||||
|
статус находится в aria-label/title. Это сохраняет ширину названия с новой кнопкой
|
||||||
|
в обычном узком окне. Не настроенные устройства сохраняют полный trailing badge.
|
||||||
|
Ошибка завершает ожидание и показывается через общий ToastStack. Строковый
|
||||||
|
ProgressBar остаётся только для многошаговой prepare, не дублирует button loading.
|
||||||
|
|
||||||
|
## Контракт и граница полномочий
|
||||||
|
|
||||||
|
`power.wake` принимает пустые parameters и обычный OperationRequest с точным
|
||||||
|
device/session/idempotency/deadline. Durable receipt создаётся до эффекта;
|
||||||
|
повтор operation_id не повторяет команду, результат failure не запускает retry.
|
||||||
|
Broker использует отдельный private `/wake-once`; клиент не передаёт serial,
|
||||||
|
Bluetooth MAC, payload, shell или путь. Один вызов ограничен существующим
|
||||||
|
20s advertisement/45s parent watchdog. После beacon broker продолжает только
|
||||||
|
read-only наблюдение supervisor до52s от начала команды: холодная X4 в MANUAL01
|
||||||
|
вернулась через34s SDK/37s Core. Повторной радиокоманды нет. Он сохраняет automatic recovery policy
|
||||||
|
и preview intent, не выполняет preview START, запись, фото, settings или Power-off.
|
||||||
|
Только подтверждённый возврат полного USB SDK identity даёт complete.
|
||||||
|
Node задаёт power.wake timeout55s в model ActionTimeouts; отдельная копия HTTP
|
||||||
|
client сохраняет прежние25s inventory/остальных действий, context deadline
|
||||||
|
запроса остаётся верхней границей. Regression проверяет фактический deadline
|
||||||
|
на входе driver transport и неизменность shared client.
|
||||||
|
|
||||||
|
Root helper `/registry` запоминает полные serials непосредственно из однозначных
|
||||||
|
OS USB bindings, по умолчанию enabled=false. Это часть работающего установленного
|
||||||
|
модуля после prepare, не включение автостратегии. Прежние serials сохраняются
|
||||||
|
после исчезновения USB и upgrade. В broker/public inventory выходят только
|
||||||
|
stable IDs и wake_available; root-only serials и raw reports не публикуются.
|
||||||
|
Несколько экземпляров независимы; известные suffix collisions отклоняются.
|
||||||
|
Manual и automatic wake используют один adapter lock и общий persisted3/10min
|
||||||
|
лимит; manual не сбрасывает бюджет. Включённое на камере Bluetooth Wakeup и
|
||||||
|
оставленный USB — необходимые условия; их наличие не имитируется программно.
|
||||||
|
|
||||||
|
## Доставка и приёмка
|
||||||
|
|
||||||
|
B16 model0.1.3-8 и Node0.8.21 квалифицированы и установлены при выключенной X4.
|
||||||
|
MANUAL01 показал реальный wake с сохранённой policy и preview0/recording0, но
|
||||||
|
ранний error receipt: SDK вернулся после20s окна beacon. B17 model0.1.3-9
|
||||||
|
добавляет ожидание до52s без повторной команды, Node0.8.21-3 объединяет его с компактной строкой и скрытием кнопки при online.
|
||||||
|
Промежуточные0.8.21-1/-2 не установлены; N14 прошёл полную квалификацию.
|
||||||
|
Владелец отдельно подтвердил ручное включение через приложение Node0.8.21;
|
||||||
|
readback подтвердил новую SDK session, recovery=false, preview0/recording0.
|
||||||
|
N15 Node0.8.21-3/Core прошёл DG/Node/Go race/Core architecture/typecheck,
|
||||||
|
821 Core tests и builds. Core активирован на8000 с сохранением identities;
|
||||||
|
реальная online X4 не имеет кнопки в normal/expanded, settings Escape работает.
|
||||||
|
P15 штатно установил Node0.8.21-3 и обновил X4 до0.1.3-9 через bundled
|
||||||
|
profile за27.64s. D455 восстановлена, X4 online/idle/recoveryfalse.
|
||||||
|
MANUAL02 ожидает Power-off владельцем и один новый UI click; аппаратное
|
||||||
|
подтверждение исправленного позднего receipt ещё pending.
|
||||||
|
Квалификация выполнялась на той же Ubuntu Mini через versioned builders. Никаких ad-hoc OS изменений. Node owner installer обновляет
|
||||||
|
уже установленный X4 model через свой fixed bundled profile, даже при выключенной
|
||||||
|
камере. На чистом Node без optional model первая подготовка остаётся в приложении.
|
||||||
|
Установка не заявляет SDK/image verification: это отдельное действие после wake.
|
||||||
|
Профиль сохраняет проверки hash/dependencies/lifecycle и не прерывает APT/dpkg.
|
||||||
|
|
||||||
|
MANUAL01 подтвердил wake, но не корректное завершение операции. Повторная
|
||||||
|
MANUAL02 на B17 остаётся необходимой для исправленного подтверждения.
|
||||||
|
Проверить: installer upgrade при X4 offline; кнопка Core в offline/pending/online;
|
||||||
|
разовый wake при recovery=false, новый SDK instance session, policy остаётся false,
|
||||||
|
без preview/SD replay; явный просмотр отдельно; D455 и identities сохранены.
|
||||||
|
Все versions/UTC/monotonic/hashes/results — в ledger07. Clean Ubuntu и полная
|
||||||
|
power-off команда камеры остаются отдельными открытыми проверками/реализациями.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import {Button} from '@nodedc/ui-react';
|
||||||
|
import type {Sensor} from './contracts';
|
||||||
|
import type {SensorRowAction} from './extensions';
|
||||||
|
|
||||||
|
export function SensorRowActions({device,actions=[],enabled,busy,pending,perform}:{
|
||||||
|
device:Sensor; actions?:readonly SensorRowAction[]; enabled:boolean; busy:boolean;
|
||||||
|
pending?:string; perform:(action:string,parameters?:Record<string,unknown>)=>void;
|
||||||
|
}) {
|
||||||
|
return <>{actions.filter(value=>actions.filter(other=>other.actionId===value.actionId).length===1).map(value=>
|
||||||
|
<Button key={value.actionId} aria-label={`${value.label}: ${device.name}`} title={value.description}
|
||||||
|
disabled={!enabled||busy||value.disabled} loading={pending===value.actionId}
|
||||||
|
onClick={()=>perform(value.actionId,value.parameters)}>{value.label}</Button>)}</>;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import {useCallback,useEffect,useRef,useState} from 'react';
|
|||||||
import {LoadingRegion,Button,Icon,IconButton,ResourceList,ResourceRow,SettingsCard,StatusBadge,TextField,ToastStack,Window,WindowFooterActions} from '@nodedc/ui-react';
|
import {LoadingRegion,Button,Icon,IconButton,ResourceList,ResourceRow,SettingsCard,StatusBadge,TextField,ToastStack,Window,WindowFooterActions} from '@nodedc/ui-react';
|
||||||
import {perform,enrolledDevice,type Sensor,type SensorInventory,type SensorTransport} from './contracts';
|
import {perform,enrolledDevice,type Sensor,type SensorInventory,type SensorTransport} from './contracts';
|
||||||
import {SensorDetail} from './SensorDetail';
|
import {SensorDetail} from './SensorDetail';
|
||||||
|
import {SensorRowActions} from './SensorRowActions';
|
||||||
import {SensorPreparations} from './SensorPreparations';
|
import {SensorPreparations} from './SensorPreparations';
|
||||||
import {devicePreparation,preparationProgress} from './preparation';
|
import {devicePreparation,preparationProgress} from './preparation';
|
||||||
import {sensorStatus} from './sensorStatus';
|
import {sensorStatus} from './sensorStatus';
|
||||||
@@ -11,7 +12,7 @@ import './sensors.css';
|
|||||||
import {WirelessEnrollmentWindow} from './WirelessEnrollmentWindow';
|
import {WirelessEnrollmentWindow} from './WirelessEnrollmentWindow';
|
||||||
export function SensorWorkspace({transport,enabled=true,onDetailChange,createRerunHost,contributions=[]}:{transport:SensorTransport;enabled?:boolean;onDetailChange?:(open:boolean)=>void;createRerunHost?:RerunHostFactory;contributions?:readonly SensorUiContribution[]}){
|
export function SensorWorkspace({transport,enabled=true,onDetailChange,createRerunHost,contributions=[]}:{transport:SensorTransport;enabled?:boolean;onDetailChange?:(open:boolean)=>void;createRerunHost?:RerunHostFactory;contributions?:readonly SensorUiContribution[]}){
|
||||||
const [adding,setAdding]=useState(false);
|
const [adding,setAdding]=useState(false);
|
||||||
const [inventory,setInventory]=useState<SensorInventory|null>(null);const [selected,setSelected]=useState<string|null>(null);const [editing,setEditing]=useState<Sensor|null>(null);const [name,setName]=useState('');const [localBusy,setBusy]=useState<ReadonlySet<string>>(()=>new Set());const activeActions=useRef(new Set<string>());const [error,setError]=useState('');const [fresh,setFresh]=useState(false);
|
const [inventory,setInventory]=useState<SensorInventory|null>(null);const [selected,setSelected]=useState<string|null>(null);const [editing,setEditing]=useState<Sensor|null>(null);const [name,setName]=useState('');const [localBusy,setBusy]=useState<ReadonlyMap<string,string>>(()=>new Map());const activeActions=useRef(new Map<string,string>());const [error,setError]=useState('');const [fresh,setFresh]=useState(false);
|
||||||
const failure=useCallback((e:unknown)=>{setError(e===null?'':e instanceof Error?e.message:'Не удалось выполнить действие устройства.');},[]);
|
const failure=useCallback((e:unknown)=>{setError(e===null?'':e instanceof Error?e.message:'Не удалось выполнить действие устройства.');},[]);
|
||||||
const refresh=useCallback(async()=>{if(!enabled){setFresh(false);return;}try{const value=await transport.inventory();setInventory(value);setFresh(value.fresh!==false);}catch(e){setFresh(false);failure(e);}},[transport,enabled,failure]);
|
const refresh=useCallback(async()=>{if(!enabled){setFresh(false);return;}try{const value=await transport.inventory();setInventory(value);setFresh(value.fresh!==false);}catch(e){setFresh(false);failure(e);}},[transport,enabled,failure]);
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
@@ -25,10 +26,10 @@ export function SensorWorkspace({transport,enabled=true,onDetailChange,createRer
|
|||||||
},[transport,enabled,refresh]);
|
},[transport,enabled,refresh]);
|
||||||
async function action(device:Sensor,action:string,parameters:Record<string,unknown>={}){
|
async function action(device:Sensor,action:string,parameters:Record<string,unknown>={}){
|
||||||
if(activeActions.current.has(device.id))return;
|
if(activeActions.current.has(device.id))return;
|
||||||
activeActions.current.add(device.id);setBusy(new Set(activeActions.current));setError('');
|
activeActions.current.set(device.id,action);setBusy(new Map(activeActions.current));setError('');
|
||||||
try{await perform(transport,device,action,parameters);await refresh();setEditing(current=>current?.id===device.id?null:current);}
|
try{await perform(transport,device,action,parameters);await refresh();setEditing(current=>current?.id===device.id?null:current);}
|
||||||
catch(e){failure(e);}
|
catch(e){failure(e);}
|
||||||
finally{activeActions.current.delete(device.id);setBusy(new Set(activeActions.current));}
|
finally{activeActions.current.delete(device.id);setBusy(new Map(activeActions.current));}
|
||||||
}
|
}
|
||||||
async function completeEnrollment(sessionId:string,signal?:AbortSignal){
|
async function completeEnrollment(sessionId:string,signal?:AbortSignal){
|
||||||
if(!enabled)throw new Error('БК недоступен. Обновите состояние устройства.');
|
if(!enabled)throw new Error('БК недоступен. Обновите состояние устройства.');
|
||||||
@@ -46,14 +47,15 @@ export function SensorWorkspace({transport,enabled=true,onDetailChange,createRer
|
|||||||
return <div className="sensor-workspace">{device?Detail?<Detail key={device.snapshot.context.session_id} enabled={enabled&&fresh} device={device} transport={transport} back={()=>setSelected(null)} refresh={refresh} failure={failure} createRerunHost={createRerunHost} reconnect={transport.enrollment?()=>setAdding(true):undefined}/>:<div className="sensor-content"><Button onClick={()=>setSelected(null)}>К устройствам</Button><SettingsCard title="Просмотр устройства недоступен" description="Интеграция этого устройства не установлена."/></div>:<>
|
return <div className="sensor-workspace">{device?Detail?<Detail key={device.snapshot.context.session_id} enabled={enabled&&fresh} device={device} transport={transport} back={()=>setSelected(null)} refresh={refresh} failure={failure} createRerunHost={createRerunHost} reconnect={transport.enrollment?()=>setAdding(true):undefined}/>:<div className="sensor-content"><Button onClick={()=>setSelected(null)}>К устройствам</Button><SettingsCard title="Просмотр устройства недоступен" description="Интеграция этого устройства не установлена."/></div>:<>
|
||||||
<div className="sensor-actions sensor-inventory-toolbar"><StatusBadge tone={fresh&&enabled?'success':'neutral'}>{!enabled?'БК недоступен':fresh?'Сведения с БК':'Нет свежих сведений'}</StatusBadge><div className="sensor-actions">{transport.enrollment&&wirelessContributions(contributions).length>0&&<IconButton label="Подключить беспроводное устройство к БК" disabled={!enabled} onClick={()=>setAdding(true)}><Icon name="plus"/></IconButton>}<IconButton label="Обновить устройства" disabled={!enabled} onClick={()=>{void refresh();}}><Icon name="refresh"/></IconButton></div></div>
|
<div className="sensor-actions sensor-inventory-toolbar"><StatusBadge tone={fresh&&enabled?'success':'neutral'}>{!enabled?'БК недоступен':fresh?'Сведения с БК':'Нет свежих сведений'}</StatusBadge><div className="sensor-actions">{transport.enrollment&&wirelessContributions(contributions).length>0&&<IconButton label="Подключить беспроводное устройство к БК" disabled={!enabled} onClick={()=>setAdding(true)}><Icon name="plus"/></IconButton>}<IconButton label="Обновить устройства" disabled={!enabled} onClick={()=>{void refresh();}}><Icon name="refresh"/></IconButton></div></div>
|
||||||
{!inventory?<LoadingRegion loading label="Получаем устройства БК"/>:connected.length===0?<SettingsCard title="Поддерживаемые устройства не обнаружены" description="Подключите устройство кабелем или добавьте беспроводное устройство через плюс."/>:<ResourceList aria-label="Устройства БК">{connected.map(item=>{
|
{!inventory?<LoadingRegion loading label="Получаем устройства БК"/>:connected.length===0?<SettingsCard title="Поддерживаемые устройства не обнаружены" description="Подключите устройство кабелем или добавьте беспроводное устройство через плюс."/>:<ResourceList aria-label="Устройства БК">{connected.map(item=>{
|
||||||
const operation=inventory.operations?.find(v=>v.device_id===item.id&&v.state==='running');const busy=!!operation||localBusy.has(item.id);
|
const operation=inventory.operations?.find(v=>v.device_id===item.id&&['queued','running'].includes(v.state));const busy=!!operation||localBusy.has(item.id);
|
||||||
|
const pending=localBusy.get(item.id)??operation?.action_id;
|
||||||
const configured=item.configured??item.snapshot.enrollment==='enrolled';
|
const configured=item.configured??item.snapshot.enrollment==='enrolled';
|
||||||
const prep=devicePreparation(inventory,item);
|
const prep=devicePreparation(inventory,item);
|
||||||
const status=(sensorContribution(contributions,item)?.status??sensorStatus)(item,enabled&&fresh);const label=busy?(operation?.action_id==='prepare'?'Подготовка устройства':'Выполняется команда'):status.label;
|
const status=(sensorContribution(contributions,item)?.status??sensorStatus)(item,enabled&&fresh);const label=busy?(operation?.action_id==='prepare'?'Подготовка устройства':'Выполняется команда'):status.label;
|
||||||
return <li key={item.id}><ResourceRow icon={<Icon name={sensorContribution(contributions,item)?.icon??'camera'}/>} title={item.name} description={item.model} metadata={<span>{item.connection_label||`USB ${item.usb}`}</span>} statusPlacement={configured?'leading':'trailing'} aria-busy={busy} progress={busy?preparationProgress(prep,label):undefined} status={<StatusBadge variant={configured&&item.online?'indicator':'badge'} tone={status.tone} aria-label={label} title={label}>{configured&&item.online?null:label}</StatusBadge>} actions={<>{!configured&&sensorContribution(contributions,item)?.supportsPreparation!==false&&<IconButton label={`Подготовить и проверить: ${item.name}`} disabled={!enabled||!fresh||busy||item.initializable===false||item.preparation_safe===false||!item.online||item.snapshot.acquisition==='streaming'||item.snapshot.acquisition==='stopping'} onClick={()=>void action(item,'prepare')}><Icon name="download"/></IconButton>}{sensorContribution(contributions,item)?.supportsRenaming!==false&&<IconButton label={`Настройки: ${item.name}`} disabled={!enabled||!fresh||busy} onClick={()=>{setEditing(item);setName(item.name);}}><Icon name="settings"/></IconButton>}<IconButton label={`Просмотр: ${item.name}`} disabled={!item.prepared||busy} onClick={()=>setSelected(item.id)}><Icon name="eye"/></IconButton></>}/></li>;})}</ResourceList>}
|
return <li key={item.id}><ResourceRow icon={<Icon name={sensorContribution(contributions,item)?.icon??'camera'}/>} title={item.name} description={item.model} metadata={<span>{item.connection_label||`USB ${item.usb}`}</span>} statusPlacement={configured?'leading':'trailing'} aria-busy={busy} progress={busy&&pending==='prepare'?preparationProgress(prep,label):undefined} status={<StatusBadge variant={configured?'indicator':'badge'} tone={status.tone} aria-label={label} title={label}>{configured?null:label}</StatusBadge>} actions={<><SensorRowActions device={item} actions={sensorContribution(contributions,item)?.rowActions?.(item)} enabled={enabled&&fresh} busy={busy} pending={pending} perform={(name,parameters)=>void action(item,name,parameters)}/>{!configured&&sensorContribution(contributions,item)?.supportsPreparation!==false&&<IconButton label={`Подготовить и проверить: ${item.name}`} disabled={!enabled||!fresh||busy||item.initializable===false||item.preparation_safe===false||!item.online||item.snapshot.acquisition==='streaming'||item.snapshot.acquisition==='stopping'} onClick={()=>void action(item,'prepare')}><Icon name="download"/></IconButton>}{sensorContribution(contributions,item)?.supportsRenaming!==false&&<IconButton label={`Настройки: ${item.name}`} disabled={!enabled||!fresh||busy} onClick={()=>{setEditing(item);setName(item.name);}}><Icon name="settings"/></IconButton>}<IconButton label={`Просмотр: ${item.name}`} disabled={!item.prepared||busy} onClick={()=>setSelected(item.id)}><Icon name="eye"/></IconButton></>}/></li>;})}</ResourceList>}
|
||||||
<SensorPreparations inventory={inventory}/>
|
<SensorPreparations inventory={inventory}/>
|
||||||
</>}
|
</>}
|
||||||
<Window open={editing!==null} title="Настройки устройства" onClose={()=>setEditing(null)} footer={<WindowFooterActions><Button disabled={localBusy.has(editing?.id??'')} onClick={()=>setEditing(null)}>Отмена</Button><Button disabled={localBusy.has(editing?.id??'')||!name.trim()||!enabled||!fresh} onClick={()=>{if(editing)void action(editing,'rename',{name});}}>Сохранить</Button></WindowFooterActions>}><div className="sensor-content"><TextField label="Название устройства" value={name} maxLength={80} onChange={e=>setName(e.target.value)} disabled={localBusy.has(editing?.id??'')}/>{editing&&sensorContribution(contributions,editing)?.supportsPreparation!==false&&(editing.configured??editing.snapshot.enrollment==='enrolled')&&<ResourceRow title="Конфигурация на БК" description="Повторно развернуть и проверить встроенный драйвер устройства." actions={<Button disabled={localBusy.has(editing?.id??'')||!enabled||!fresh||!editingCurrent?.online||editingCurrent?.initializable===false||editingCurrent?.preparation_safe===false||['streaming','starting','stopping'].includes(editingCurrent?.snapshot.acquisition??'offline')} onClick={()=>{const target=editing;setEditing(null);void action(target,'prepare');}}>Обновить</Button>}/>}</div></Window>
|
<Window open={editing!==null} title="Настройки устройства" onClose={()=>setEditing(null)} footer={<WindowFooterActions><Button disabled={localBusy.has(editing?.id??'')} onClick={()=>setEditing(null)}>Отмена</Button><Button loading={localBusy.get(editing?.id??'')==='rename'} disabled={localBusy.has(editing?.id??'')||!name.trim()||!enabled||!fresh} onClick={()=>{if(editing)void action(editing,'rename',{name});}}>Сохранить</Button></WindowFooterActions>}><div className="sensor-content"><TextField label="Название устройства" value={name} maxLength={80} onChange={e=>setName(e.target.value)} disabled={localBusy.has(editing?.id??'')}/>{editing&&sensorContribution(contributions,editing)?.supportsPreparation!==false&&(editing.configured??editing.snapshot.enrollment==='enrolled')&&<ResourceRow title="Конфигурация на БК" description="Повторно развернуть и проверить встроенный драйвер устройства." actions={<Button disabled={localBusy.has(editing?.id??'')||!enabled||!fresh||!editingCurrent?.online||editingCurrent?.initializable===false||editingCurrent?.preparation_safe===false||['streaming','starting','stopping'].includes(editingCurrent?.snapshot.acquisition??'offline')} onClick={()=>{const target=editing;setEditing(null);void action(target,'prepare');}}>Обновить</Button>}/>}</div></Window>
|
||||||
{adding&&transport.enrollment&&wirelessContributions(contributions).length>0&&<WirelessEnrollmentWindow contributions={contributions} transport={transport.enrollment} onClose={()=>setAdding(false)} onChange={()=>{void refresh();}} onComplete={completeEnrollment}/>}
|
{adding&&transport.enrollment&&wirelessContributions(contributions).length>0&&<WirelessEnrollmentWindow contributions={contributions} transport={transport.enrollment} onClose={()=>setAdding(false)} onChange={()=>{void refresh();}} onComplete={completeEnrollment}/>}
|
||||||
<ToastStack items={error?[{id:'sensor-error',tone:'error',title:error,durationMs:null}]:[]} onDismiss={()=>setError('')}/>
|
<ToastStack items={error?[{id:'sensor-error',tone:'error',title:error,durationMs:null}]:[]} onDismiss={()=>setError('')}/>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -21,10 +21,16 @@ export interface SensorUiContribution {
|
|||||||
retainOffline:boolean;
|
retainOffline:boolean;
|
||||||
supportsPreparation:boolean;
|
supportsPreparation:boolean;
|
||||||
supportsRenaming?:boolean;
|
supportsRenaming?:boolean;
|
||||||
|
rowActions?:(device:Sensor)=>readonly SensorRowAction[];
|
||||||
status?:(device:Sensor,fresh:boolean)=>{label:string;tone:'neutral'|'success'|'warning'|'danger'};
|
status?:(device:Sensor,fresh:boolean)=>{label:string;tone:'neutral'|'success'|'warning'|'danger'};
|
||||||
wirelessEnrollment?:{label:string;View:ComponentType<SensorEnrollmentProps>};
|
wirelessEnrollment?:{label:string;View:ComponentType<SensorEnrollmentProps>};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SensorRowAction {
|
||||||
|
actionId:string; label:string; description?:string; disabled?:boolean;
|
||||||
|
parameters?:Record<string,unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
export function wirelessContributions(contributions:readonly SensorUiContribution[]):SensorUiContribution[] {
|
export function wirelessContributions(contributions:readonly SensorUiContribution[]):SensorUiContribution[] {
|
||||||
return contributions.filter(value=>value.wirelessEnrollment&&contributions.filter(other=>other.kind===value.kind).length===1);
|
return contributions.filter(value=>value.wirelessEnrollment&&contributions.filter(other=>other.kind===value.kind).length===1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import type {Sensor} from '../../../../packages/sensor-ui/src/contracts';
|
import type {Sensor} from '../../../../packages/sensor-ui/src/contracts';
|
||||||
|
import type {SensorRowAction} from '../../../../packages/sensor-ui/src/extensions';
|
||||||
|
|
||||||
export interface RecoveryStatus {
|
export interface RecoveryStatus {
|
||||||
supported:boolean; enabled:boolean; preview_wanted:boolean; phase:string;
|
supported:boolean; enabled:boolean; preview_wanted:boolean; phase:string;
|
||||||
|
wake_available?:boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cameraRowActions(device:Sensor):readonly SensorRowAction[] {
|
||||||
|
if(device.online||!cameraStatus(device).recovery?.supported)return [];
|
||||||
|
return [{actionId:'power.wake',label:'Включить',
|
||||||
|
description:'На X4 должно быть включено «Пробуждение по Bluetooth». USB оставьте подключённым.',
|
||||||
|
disabled:!device.prepared||!(device.configured??device.snapshot.enrollment==='enrolled')||cameraStatus(device).recovery?.wake_available!==true}];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CameraStatus {
|
export interface CameraStatus {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type {SensorUiContribution} from '../../../../packages/sensor-ui/src/extensions';
|
import type {SensorUiContribution} from '../../../../packages/sensor-ui/src/extensions';
|
||||||
import {X4Detail} from './X4Detail';
|
import {X4Detail} from './X4Detail';
|
||||||
import {cameraLabel} from './model';
|
import {cameraLabel,cameraRowActions} from './model';
|
||||||
|
|
||||||
export const insta360X4SensorUi:SensorUiContribution={
|
export const insta360X4SensorUi:SensorUiContribution={
|
||||||
kind:'insta360.x4',Detail:X4Detail,icon:'camera',retainOffline:true,
|
kind:'insta360.x4',Detail:X4Detail,icon:'camera',retainOffline:true,
|
||||||
supportsPreparation:true,supportsRenaming:true,status:cameraLabel,
|
supportsPreparation:true,supportsRenaming:true,status:cameraLabel,
|
||||||
|
rowActions:cameraRowActions,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ sys.path.insert(0, str(REPOSITORY / "scripts/packaging"))
|
|||||||
from debian import package # noqa: E402
|
from debian import package # noqa: E402
|
||||||
from fetch_sdk import verify # noqa: E402
|
from fetch_sdk import verify # noqa: E402
|
||||||
|
|
||||||
VERSION = "0.1.3-7"
|
VERSION = "0.1.3-9"
|
||||||
WHEELS = {
|
WHEELS = {
|
||||||
"aiohappyeyeballs",
|
"aiohappyeyeballs",
|
||||||
"aiohttp",
|
"aiohttp",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ def build():
|
|||||||
paths += list((REPOSITORY / "packages/plugin-sdk/python/missioncore_plugin_sdk").rglob("*.py"))
|
paths += list((REPOSITORY / "packages/plugin-sdk/python/missioncore_plugin_sdk").rglob("*.py"))
|
||||||
paths += [REPOSITORY / "scripts/packaging/debian.py"]
|
paths += [REPOSITORY / "scripts/packaging/debian.py"]
|
||||||
paths += [REPOSITORY / "apps/node-agent/packaging/insta360_profile.py"]
|
paths += [REPOSITORY / "apps/node-agent/packaging/insta360_profile.py"]
|
||||||
|
paths += [REPOSITORY / "apps/node-agent/packaging/install_owner_release.py"]
|
||||||
paths += [
|
paths += [
|
||||||
REPOSITORY / name
|
REPOSITORY / name
|
||||||
for name in (
|
for name in (
|
||||||
|
|||||||
@@ -47,6 +47,28 @@ class WakeService:
|
|||||||
def save(self, ident, value):
|
def save(self, ident, value):
|
||||||
write(self.root / (ident + ".json"), json.dumps(value).encode(), 0o600)
|
write(self.root / (ident + ".json"), json.dumps(value).encode(), 0o600)
|
||||||
|
|
||||||
|
def registry(self):
|
||||||
|
"""Remember exact USB identities without enabling any recovery policy."""
|
||||||
|
with self.lock:
|
||||||
|
observed = connected()
|
||||||
|
known = {
|
||||||
|
p.stem for p in self.root.glob("instax4_*.json") if IDENTIFIER.fullmatch(p.stem)
|
||||||
|
}
|
||||||
|
for binding in observed:
|
||||||
|
ident = binding.device_id
|
||||||
|
if (
|
||||||
|
sum(x.device_id == ident for x in observed) != 1
|
||||||
|
or not re.fullmatch(r"[A-Za-z0-9]{7,64}", binding.serial)
|
||||||
|
or (ident not in known and len(known) >= 500)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
value = self.load(ident)
|
||||||
|
if "serial" not in value:
|
||||||
|
value["serial"] = binding.serial
|
||||||
|
self.save(ident, value)
|
||||||
|
known.add(ident)
|
||||||
|
return {"items": sorted(ident for ident in known if self.load(ident).get("serial"))}
|
||||||
|
|
||||||
def control(self, ident, enabled):
|
def control(self, ident, enabled):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
value = self.load(ident)
|
value = self.load(ident)
|
||||||
@@ -65,20 +87,20 @@ class WakeService:
|
|||||||
self.running[ident].terminate()
|
self.running[ident].terminate()
|
||||||
return {"enabled": enabled}
|
return {"enabled": enabled}
|
||||||
|
|
||||||
def wake(self, ident, attempt):
|
def wake(self, ident, attempt, manual=False):
|
||||||
if not self.advertiser.acquire(blocking=False):
|
if not self.advertiser.acquire(blocking=False):
|
||||||
return {"state": "busy"}
|
return {"state": "busy"}
|
||||||
try:
|
try:
|
||||||
# Installation cannot replace code/state while a wake is in flight.
|
# Installation cannot replace code/state while a wake is in flight.
|
||||||
with acquisition():
|
with acquisition():
|
||||||
return self.wake_locked(ident, attempt)
|
return self.wake_locked(ident, attempt, manual)
|
||||||
finally:
|
finally:
|
||||||
self.advertiser.release()
|
self.advertiser.release()
|
||||||
|
|
||||||
def wake_locked(self, ident, attempt):
|
def wake_locked(self, ident, attempt, manual=False):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
value = self.load(ident)
|
value = self.load(ident)
|
||||||
if not value["enabled"] or "serial" not in value:
|
if (not manual and not value["enabled"]) or "serial" not in value:
|
||||||
return {"state": "disabled"}
|
return {"state": "disabled"}
|
||||||
serial = value["serial"]
|
serial = value["serial"]
|
||||||
if any(x.device_id == ident for x in connected()):
|
if any(x.device_id == ident for x in connected()):
|
||||||
@@ -135,13 +157,15 @@ class WakeService:
|
|||||||
state = "connected"
|
state = "connected"
|
||||||
except (ValueError, OSError):
|
except (ValueError, OSError):
|
||||||
pass
|
pass
|
||||||
return {"state": state if self.load(ident)["enabled"] else "disabled"}
|
return {"state": state if manual or self.load(ident)["enabled"] else "disabled"}
|
||||||
|
|
||||||
def dispatch(self, method, route, value, _headers):
|
def dispatch(self, method, route, value, _headers):
|
||||||
if method == "GET" and route == "/health":
|
if method == "GET" and route == "/health":
|
||||||
return {"ready": True}
|
return {"ready": True}
|
||||||
if method != "POST" or not isinstance(value, dict):
|
if method != "POST" or not isinstance(value, dict):
|
||||||
raise ValueError("Unsupported recovery request")
|
raise ValueError("Unsupported recovery request")
|
||||||
|
if route == "/registry" and not value:
|
||||||
|
return self.registry()
|
||||||
ident = value.get("device_id")
|
ident = value.get("device_id")
|
||||||
if not isinstance(ident, str) or not IDENTIFIER.fullmatch(ident):
|
if not isinstance(ident, str) or not IDENTIFIER.fullmatch(ident):
|
||||||
raise ValueError("Invalid recovery identity")
|
raise ValueError("Invalid recovery identity")
|
||||||
@@ -149,12 +173,12 @@ class WakeService:
|
|||||||
if type(value["enabled"]) is not bool:
|
if type(value["enabled"]) is not bool:
|
||||||
raise ValueError("Recovery requires an explicit boolean")
|
raise ValueError("Recovery requires an explicit boolean")
|
||||||
return self.control(ident, value["enabled"])
|
return self.control(ident, value["enabled"])
|
||||||
if route == "/wake" and set(value) == {"device_id", "attempt"}:
|
if route in ("/wake", "/wake-once") and set(value) == {"device_id", "attempt"}:
|
||||||
if not isinstance(value["attempt"], str) or not re.fullmatch(
|
if not isinstance(value["attempt"], str) or not re.fullmatch(
|
||||||
r"[0-9a-f]{32}", value["attempt"]
|
r"[0-9a-f]{32}", value["attempt"]
|
||||||
):
|
):
|
||||||
raise ValueError("Invalid wake attempt")
|
raise ValueError("Invalid wake attempt")
|
||||||
return self.wake(ident, value["attempt"])
|
return self.wake(ident, value["attempt"], manual=route == "/wake-once")
|
||||||
raise ValueError("Unsupported recovery request")
|
raise ValueError("Unsupported recovery request")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ class Broker:
|
|||||||
return {"items": [self.item(item, node) for item in snapshots]}
|
return {"items": [self.item(item, node) for item in snapshots]}
|
||||||
if method == "POST" and route == "/operation":
|
if method == "POST" and route == "/operation":
|
||||||
identifier = value["session"]["device_id"]
|
identifier = value["session"]["device_id"]
|
||||||
if value.get("action_id") == "recovery.configure" and self.recovery:
|
if value.get("action_id") in ("recovery.configure", "power.wake") and self.recovery:
|
||||||
return self.recovery.operation(identifier, value)
|
return self.recovery.operation(identifier, value)
|
||||||
if value.get("action_id") == "preview.stop" and self.recovery:
|
if value.get("action_id") == "preview.stop" and self.recovery:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
"""Public messages are allowlisted; vendor error text never crosses the boundary."""
|
"""Public messages are allowlisted; vendor error text never crosses the boundary."""
|
||||||
|
|
||||||
MESSAGES = {
|
MESSAGES = {
|
||||||
|
"wake_adapter_busy": "Сейчас включается другая камера. Повторите действие позже.",
|
||||||
|
"wake_identity_missing": "Сначала подключите эту камеру по USB и подготовьте её на борту.",
|
||||||
|
"wake_identity_conflict": "Не удалось однозначно выбрать камеру для включения.",
|
||||||
|
"wake_rate_limited": "Лимит попыток включения исчерпан. Повторите через 10 минут.",
|
||||||
|
"wake_not_confirmed": (
|
||||||
|
"Камера не подключилась. Проверьте питание, USB "
|
||||||
|
"и настройку «Пробуждение по Bluetooth» на X4."
|
||||||
|
),
|
||||||
"unsupported_camera_parameter": "Параметр или действие недоступны в текущем режиме камеры.",
|
"unsupported_camera_parameter": "Параметр или действие недоступны в текущем режиме камеры.",
|
||||||
"verification_recording_active": "Для проверки изображения остановите запись на камере.",
|
"verification_recording_active": "Для проверки изображения остановите запись на камере.",
|
||||||
"preview_no_decodable_image": "Камера не передала декодируемое изображение.",
|
"preview_no_decodable_image": "Камера не передала декодируемое изображение.",
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ class VideoHeader(ctypes.Structure):
|
|||||||
|
|
||||||
def parameters(action, value):
|
def parameters(action, value):
|
||||||
"""Validate before any SDK call; parameters never select a file or library."""
|
"""Validate before any SDK call; parameters never select a file or library."""
|
||||||
|
if action == "power.wake":
|
||||||
|
if not isinstance(value, dict) or value:
|
||||||
|
raise ValueError("Wake uses the enrolled camera identity only")
|
||||||
|
return 0, "", 0.0
|
||||||
if action == "recovery.configure":
|
if action == "recovery.configure":
|
||||||
if (
|
if (
|
||||||
not isinstance(value, dict)
|
not isinstance(value, dict)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class Recovery:
|
|||||||
self.absent = {}
|
self.absent = {}
|
||||||
self.stable = {}
|
self.stable = {}
|
||||||
self.jobs = {}
|
self.jobs = {}
|
||||||
|
self.wakeable = set()
|
||||||
self.stopping = threading.Event()
|
self.stopping = threading.Event()
|
||||||
self.pool = ThreadPoolExecutor(max_workers=2, thread_name_prefix="x4-recovery")
|
self.pool = ThreadPoolExecutor(max_workers=2, thread_name_prefix="x4-recovery")
|
||||||
for path in sorted(root.glob("instax4_*.json"))[:500]:
|
for path in sorted(root.glob("instax4_*.json"))[:500]:
|
||||||
@@ -62,8 +63,50 @@ class Recovery:
|
|||||||
"enabled": state.get("enabled", False),
|
"enabled": state.get("enabled", False),
|
||||||
"preview_wanted": state.get("preview_wanted", False),
|
"preview_wanted": state.get("preview_wanted", False),
|
||||||
"phase": state.get("phase", "disabled"),
|
"phase": state.get("phase", "disabled"),
|
||||||
|
"wake_available": ident in self.wakeable,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def sync_registry(self):
|
||||||
|
values = request(HELPER, "/registry", {}, timeout=4)["items"]
|
||||||
|
if not isinstance(values, list) or len(values) > 500:
|
||||||
|
raise ValueError("Invalid wake registry")
|
||||||
|
for ident in values:
|
||||||
|
self.broker.validate_identifier(ident)
|
||||||
|
with self.lock:
|
||||||
|
self.wakeable = set(values)
|
||||||
|
for ident in values:
|
||||||
|
if ident not in self.states:
|
||||||
|
self.states[ident] = self.default()
|
||||||
|
self.save(ident, self.states[ident])
|
||||||
|
|
||||||
|
def wake_once(self, ident, command):
|
||||||
|
# Explicit operator action. Keep the automatic policy and preview intent intact.
|
||||||
|
deadline = time.monotonic() + 52
|
||||||
|
result = request(
|
||||||
|
HELPER,
|
||||||
|
"/wake-once",
|
||||||
|
{"device_id": ident, "attempt": command["operation_id"][3:]},
|
||||||
|
timeout=52,
|
||||||
|
)
|
||||||
|
state = result.get("state")
|
||||||
|
# A cold X4 can enumerate after the 20-second beacon has already ended.
|
||||||
|
# Observe the installed supervisor only; never send a second wake beacon.
|
||||||
|
if state == "unavailable":
|
||||||
|
while time.monotonic() < deadline and not self.stopping.is_set():
|
||||||
|
if any(x["id"] == ident and x.get("online") for x in self.broker.snapshots()):
|
||||||
|
state = "connected"
|
||||||
|
break
|
||||||
|
self.stopping.wait(1)
|
||||||
|
if state == "connected":
|
||||||
|
return {"state": "complete", "result": {"usb_connected": True}}
|
||||||
|
errors = {
|
||||||
|
"busy": "wake_adapter_busy",
|
||||||
|
"disabled": "wake_identity_missing",
|
||||||
|
"identity_conflict": "wake_identity_conflict",
|
||||||
|
"exhausted": "wake_rate_limited",
|
||||||
|
}
|
||||||
|
return {"state": "error", "error": errors.get(state, "wake_not_confirmed")}
|
||||||
|
|
||||||
def snapshot(self, ident):
|
def snapshot(self, ident):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
current = self.current.get(ident)
|
current = self.current.get(ident)
|
||||||
@@ -246,6 +289,11 @@ class Recovery:
|
|||||||
while not self.stopping.wait(2):
|
while not self.stopping.wait(2):
|
||||||
try:
|
try:
|
||||||
snapshots = self.broker.snapshots()
|
snapshots = self.broker.snapshots()
|
||||||
|
try:
|
||||||
|
self.sync_registry()
|
||||||
|
except (OSError, ValueError, RuntimeError, KeyError, TypeError):
|
||||||
|
with self.lock:
|
||||||
|
self.wakeable = set()
|
||||||
with self.lock:
|
with self.lock:
|
||||||
for ident, future in list(self.jobs.items()):
|
for ident, future in list(self.jobs.items()):
|
||||||
if future.done():
|
if future.done():
|
||||||
@@ -270,6 +318,8 @@ class RecoveryCalls:
|
|||||||
self.recovery, self.ident, self.command = recovery, ident, command
|
self.recovery, self.ident, self.command = recovery, ident, command
|
||||||
|
|
||||||
def call(self, action, params):
|
def call(self, action, params):
|
||||||
|
if action == "power.wake":
|
||||||
|
return self.recovery.wake_once(self.ident, self.command)
|
||||||
if action == "recovery.configure":
|
if action == "recovery.configure":
|
||||||
return self.recovery.configure(self.ident, params["enabled"])
|
return self.recovery.configure(self.ident, params["enabled"])
|
||||||
if action == "preview.stop":
|
if action == "preview.stop":
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"""Synthetic owner installer checks: no actual APT, systemd or camera calls."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "apps/node-agent/packaging"))
|
||||||
|
import install_owner_release as owner # noqa: E402
|
||||||
|
|
||||||
|
PROFILE = {"version": "0.1.3-8", "revision": "a" * 24}
|
||||||
|
|
||||||
|
|
||||||
|
class OwnerModelUpdateTests(unittest.TestCase):
|
||||||
|
def test_clean_host_does_not_install_optional_model_implicitly(self):
|
||||||
|
run = Mock()
|
||||||
|
with patch.object(owner.subprocess, "run", return_value=Mock(returncode=1, stdout="")):
|
||||||
|
self.assertEqual(owner.update_installed_x4(run), {"state": "not-installed"})
|
||||||
|
run.assert_not_called()
|
||||||
|
|
||||||
|
def test_current_installed_model_is_not_restarted(self):
|
||||||
|
run = Mock(return_value=PROFILE["version"])
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
owner.subprocess,
|
||||||
|
"run",
|
||||||
|
return_value=Mock(returncode=0, stdout="install ok installed"),
|
||||||
|
),
|
||||||
|
patch.object(owner.Path, "read_text", return_value=json.dumps(PROFILE)),
|
||||||
|
):
|
||||||
|
self.assertEqual(owner.update_installed_x4(run)["state"], "current")
|
||||||
|
self.assertEqual(run.call_count, 1)
|
||||||
|
|
||||||
|
def test_offline_model_update_uses_only_fixed_profile_and_checks_result(self):
|
||||||
|
run = Mock(side_effect=["0.1.3-7", "", PROFILE["version"]])
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
owner.subprocess,
|
||||||
|
"run",
|
||||||
|
return_value=Mock(returncode=0, stdout="install ok installed"),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
owner.Path,
|
||||||
|
"read_text",
|
||||||
|
side_effect=[
|
||||||
|
json.dumps(PROFILE),
|
||||||
|
json.dumps({"state": "complete", "revision": PROFILE["revision"]}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = owner.update_installed_x4(run)
|
||||||
|
self.assertEqual(
|
||||||
|
result,
|
||||||
|
{
|
||||||
|
"state": "updated",
|
||||||
|
"version": PROFILE["version"],
|
||||||
|
"camera_verification": "not-performed",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
run.call_args_list[1].args[1],
|
||||||
|
["/usr/bin/systemctl", "start", "mission-core-node-insta360-x4-profile.service"],
|
||||||
|
)
|
||||||
|
self.assertIsNone(run.call_args_list[1].kwargs["timeout"])
|
||||||
|
|
||||||
|
def test_stale_profile_completion_cannot_qualify_updated_model(self):
|
||||||
|
run = Mock(side_effect=["0.1.3-7", "", PROFILE["version"]])
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
owner.subprocess,
|
||||||
|
"run",
|
||||||
|
return_value=Mock(returncode=0, stdout="install ok installed"),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
owner.Path,
|
||||||
|
"read_text",
|
||||||
|
side_effect=[
|
||||||
|
json.dumps(PROFILE),
|
||||||
|
json.dumps({"state": "complete", "revision": "b" * 24}),
|
||||||
|
],
|
||||||
|
),self.assertRaises(RuntimeError)
|
||||||
|
):
|
||||||
|
owner.update_installed_x4(run)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Synthetic recovery/privilege-boundary tests. No USB, Bluetooth or service changes."""
|
"""Synthetic recovery/privilege-boundary tests. No USB, Bluetooth or service changes."""
|
||||||
|
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -149,6 +150,72 @@ class RecoveryTests(unittest.TestCase):
|
|||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
parameters("recovery.configure", value)
|
parameters("recovery.configure", value)
|
||||||
|
|
||||||
|
def test_registry_retains_offline_wake_identity_without_automatic_intent(self):
|
||||||
|
with patch.object(recovery, "request", return_value={"items": [A, B]}):
|
||||||
|
self.value.sync_registry()
|
||||||
|
self.assertTrue(self.value.view(A)["wake_available"])
|
||||||
|
self.assertFalse(self.value.view(A)["enabled"])
|
||||||
|
self.assertEqual(self.value.observe([], 900), [])
|
||||||
|
self.assertEqual(len(self.value.augment([])), 2)
|
||||||
|
|
||||||
|
def test_manual_wake_is_durable_once_and_does_not_enable_policy_or_preview(self):
|
||||||
|
self.value.states[A] = self.value.default()
|
||||||
|
session = self.value.states[A]["session_id"]
|
||||||
|
value = command(A, session, "power.wake")
|
||||||
|
with patch.object(recovery, "request", return_value={"state": "connected"}) as call:
|
||||||
|
self.assertEqual(self.value.operation(A, value)["state"], "complete")
|
||||||
|
self.assertEqual(self.value.operation(A, value)["state"], "complete")
|
||||||
|
call.assert_called_once_with(
|
||||||
|
recovery.HELPER,
|
||||||
|
"/wake-once",
|
||||||
|
{
|
||||||
|
"device_id": A,
|
||||||
|
"attempt": value["operation_id"][3:],
|
||||||
|
},
|
||||||
|
timeout=52,
|
||||||
|
)
|
||||||
|
self.assertFalse(self.value.view(A)["enabled"])
|
||||||
|
self.assertFalse(self.value.view(A)["preview_wanted"])
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
parameters("power.wake", {"serial": "ARBITRARY"})
|
||||||
|
|
||||||
|
def test_manual_wake_failure_does_not_schedule_automatic_retry(self):
|
||||||
|
self.value.states[A] = self.value.default()
|
||||||
|
value = command(A, self.value.states[A]["session_id"], "power.wake")
|
||||||
|
with (
|
||||||
|
patch.object(recovery, "request", return_value={"state": "unavailable"}) as call,
|
||||||
|
patch.object(recovery.time, "monotonic", side_effect=[0, 53]),
|
||||||
|
):
|
||||||
|
result = self.value.operation(A, value)
|
||||||
|
self.assertEqual(self.value.operation(A, value), result)
|
||||||
|
self.assertEqual(result["state"], "error")
|
||||||
|
call.assert_called_once()
|
||||||
|
self.assertEqual(self.value.observe([], 1000), [])
|
||||||
|
|
||||||
|
def test_manual_wake_waits_for_late_sdk_return_without_another_radio_command(self):
|
||||||
|
with (
|
||||||
|
patch.object(recovery, "request", return_value={"state": "unavailable"}) as call,
|
||||||
|
patch.object(recovery.time, "monotonic", side_effect=[0, 22, 37]),
|
||||||
|
patch.object(self.value.stopping, "wait") as wait,
|
||||||
|
patch.object(self.broker, "snapshots", side_effect=[[snapshot(B)], [snapshot(A)]]),
|
||||||
|
):
|
||||||
|
result = self.value.wake_once(A, command(A, "x4_one", "power.wake"))
|
||||||
|
self.assertEqual(result["state"], "complete")
|
||||||
|
call.assert_called_once()
|
||||||
|
wait.assert_called_once_with(1)
|
||||||
|
self.assertFalse(self.value.view(A)["enabled"])
|
||||||
|
|
||||||
|
def test_manual_wake_does_not_wait_for_other_camera_or_retry_after_deadline(self):
|
||||||
|
with (
|
||||||
|
patch.object(recovery, "request", return_value={"state": "unavailable"}) as call,
|
||||||
|
patch.object(recovery.time, "monotonic", side_effect=[0, 22, 53]),
|
||||||
|
patch.object(self.value.stopping, "wait"),
|
||||||
|
patch.object(self.broker, "snapshots", return_value=[snapshot(B)]),
|
||||||
|
):
|
||||||
|
result = self.value.wake_once(A, command(A, "x4_one", "power.wake"))
|
||||||
|
self.assertEqual(result["state"], "error")
|
||||||
|
call.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
class WakeServiceTests(unittest.TestCase):
|
class WakeServiceTests(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -231,6 +298,49 @@ class WakeServiceTests(unittest.TestCase):
|
|||||||
self.service.advertiser.release()
|
self.service.advertiser.release()
|
||||||
self.assertEqual(list(self.root.iterdir()), [])
|
self.assertEqual(list(self.root.iterdir()), [])
|
||||||
|
|
||||||
|
def test_registry_enrolls_usb_without_enabling_recovery_and_keeps_it_when_unplugged(self):
|
||||||
|
with patch.object(wake_service, "connected", return_value=[self.binding]):
|
||||||
|
self.assertEqual(self.service.registry(), {"items": [A]})
|
||||||
|
self.assertFalse(self.service.load(A)["enabled"])
|
||||||
|
self.assertEqual(self.service.load(A)["attempts"], [])
|
||||||
|
with patch.object(wake_service, "connected", return_value=[]):
|
||||||
|
self.assertEqual(self.service.registry(), {"items": [A]})
|
||||||
|
|
||||||
|
def test_registry_never_enrolls_ambiguous_usb_identity(self):
|
||||||
|
with patch.object(wake_service, "connected", return_value=[self.binding, self.binding]):
|
||||||
|
self.assertEqual(self.service.registry(), {"items": []})
|
||||||
|
|
||||||
|
def test_manual_wake_works_while_disabled_and_shares_the_persisted_attempt_budget(self):
|
||||||
|
self.enable()
|
||||||
|
self.service.control(A, False)
|
||||||
|
|
||||||
|
def start(*args, **kwargs):
|
||||||
|
kwargs["stdout"].write(b'{"sdk_usb_returned": true}')
|
||||||
|
kwargs["stdout"].flush()
|
||||||
|
return Mock(stdin=io.BytesIO(), returncode=0)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(wake_service, "connected", return_value=[]),
|
||||||
|
patch.object(
|
||||||
|
wake_service.subprocess,
|
||||||
|
"Popen",
|
||||||
|
side_effect=start,
|
||||||
|
) as spawn,
|
||||||
|
):
|
||||||
|
self.assertEqual(self.service.wake_locked(A, "1" * 32), {"state": "disabled"})
|
||||||
|
self.assertEqual(
|
||||||
|
self.service.wake_locked(A, "1" * 32, manual=True), {"state": "connected"}
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.service.wake_locked(A, "1" * 32, manual=True), {"state": "already_attempted"}
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.service.wake_locked(B, "2" * 32, manual=True), {"state": "disabled"}
|
||||||
|
)
|
||||||
|
spawn.assert_called_once()
|
||||||
|
self.assertFalse(self.service.load(A)["enabled"])
|
||||||
|
self.assertEqual(len(self.service.load(A)["attempts"]), 1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ ACTIONS = {
|
|||||||
"settings.apply",
|
"settings.apply",
|
||||||
"files.list",
|
"files.list",
|
||||||
"recovery.configure",
|
"recovery.configure",
|
||||||
|
"power.wake",
|
||||||
}
|
}
|
||||||
MAX_INVENTORY_ITEMS = 500
|
MAX_INVENTORY_ITEMS = 500
|
||||||
MAX_SENSOR_STATE_BYTES = 3 * 1024 * 1024
|
MAX_SENSOR_STATE_BYTES = 3 * 1024 * 1024
|
||||||
|
|||||||
Reference in New Issue
Block a user