feat(x4): add per-camera manual wake and hide power action when connected
This commit is contained in:
@@ -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" })
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user