feat(x4): add per-camera manual wake and hide power action when connected

This commit is contained in:
DCCONSTRUCTIONS
2026-09-10 16:27:48 +03:00
parent db14ee5435
commit 99f29218d7
27 changed files with 992 additions and 126 deletions
@@ -19,6 +19,7 @@ type sensorModel struct {
Vendor, Product, USBName string
Socket, PrepareUnit, Report string
Actions map[string]bool
ActionTimeouts map[string]time.Duration
}
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.
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",
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 {
@@ -48,6 +48,40 @@ func isolatedSensors(t *testing.T) *Sensors {
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) {
s := isolatedSensors(t)
fakeUSB(t, s.usbRoot, "3-2", "synthetic-a", "Insta360 X4", "2")
@@ -160,7 +194,9 @@ func TestNewCameraUsesReadyProfileWithoutInterruptingAnotherInstance(t *testing.
item["preparation_safe"] = id != active
snapshot := item["snapshot"].(map[string]any)
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)
}
return testSensorReply(map[string]any{"items": items}), nil
@@ -180,16 +216,22 @@ func TestNewCameraUsesReadyProfileWithoutInterruptingAnotherInstance(t *testing.
command := sensorTestCommand()
command.Action = "prepare"
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" })
stop := sensorTestCommand()
stop.ID = "op_" + strings.Repeat("b", 32)
stop.Idempotency = stop.ID
stop.Action = "preview.stop"
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() })
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) })
awaitSensor(t, func() bool { return s.Get(command.ID).State == "complete" })
}
+9
View File
@@ -163,6 +163,15 @@ func (s *Sensors) modelDriver(ctx context.Context, model *sensorModel, path stri
if client == nil {
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"
var reader io.Reader
if body != nil {