fix(node): verify D455 access and share sensor progress and recording UI

This commit is contained in:
DCCONSTRUCTIONS
2026-09-05 23:25:11 +03:00
parent b1aaa40508
commit a8647c4d87
25 changed files with 497 additions and 61 deletions
+23 -4
View File
@@ -196,7 +196,7 @@ func (s *Sensors) Inventory() map[string]any {
name = n
}
s.mu.Unlock()
items = append(items, map[string]any{"id": id, "name": name, "model": "RealSense D455", "prepared": false, "verified": false, "online": true, "usb": read("speed") + " Мбит/с", "layers": []any{}, "snapshot": map[string]any{"context": map[string]any{"session_id": s.instance + "_" + id, "device": map[string]any{"device_id": id, "model": map[string]string{"plugin_id": "missioncore.realsense", "plugin_version": "0.6.0", "model_id": "realsense.d455"}, "stability": "stable", "basis": "hardware-identifier"}, "execution": map[string]string{"node_id": s.nodeID, "agent_instance_id": s.instance, "platform": "linux"}, "opened_at": now}, "revision": 0, "enrollment": "empty", "connectivity": "connected", "acquisition": "idle", "observed_at": now}})
items = append(items, map[string]any{"id": id, "name": name, "model": "RealSense D455", "prepared": false, "verified": false, "online": true, "usb": read("speed") + " Мбит/с", "layers": []any{}, "snapshot": map[string]any{"context": map[string]any{"session_id": s.instance + "_" + id, "device": map[string]any{"device_id": id, "model": map[string]string{"plugin_id": "missioncore.realsense", "plugin_version": "0.6.6", "model_id": "realsense.d455"}, "stability": "stable", "basis": "hardware-identifier"}, "execution": map[string]string{"node_id": s.nodeID, "agent_instance_id": s.instance, "platform": "linux"}, "opened_at": now}, "revision": 0, "enrollment": "empty", "connectivity": "connected", "acquisition": "idle", "observed_at": now}})
}
var preparation any
if data, e := os.ReadFile("/var/lib/mission-core-node-drivers/preparation.json"); e == nil && len(data) < 32768 {
@@ -206,17 +206,21 @@ func (s *Sensors) Inventory() map[string]any {
operations := []any{}
for _, v := range s.operations {
if time.Now().Unix()-v.Updated < 600 {
operations = append(operations, map[string]any{"operation_id": v.Command.ID, "device_id": v.Command.Session.DeviceID, "action_id": v.Command.Action, "state": v.State, "error": v.Error})
operations = append(operations, map[string]any{"operation_id": v.Command.ID, "device_id": v.Command.Session.DeviceID, "action_id": v.Command.Action, "requested_at": v.Command.Requested, "state": v.State, "error": v.Error})
}
}
s.mu.Unlock()
return map[string]any{"schema": "missioncore.node.devices/v1", "items": items, "preparation": preparation, "operations": operations}
}
func sensorViewAction(action string) bool {
return action == "details" || action == "offer" || action == "close-peer"
}
func (s *Sensors) Submit(c SensorCommand, remote bool) (*SensorOperation, error) {
if c.APIVersion != SensorSchema || c.Kind != "OperationRequest" || !operationID.MatchString(c.ID) || c.Idempotency != c.ID || !sensorID.MatchString(c.Session.DeviceID) || len(c.Session.SessionID) > 192 {
return nil, errors.New("Некорректная команда устройства.")
}
if !map[string]bool{"prepare": true, "details": true, "rename": true, "verify": true, "start": true, "stop": true, "option": true, "offer": true, "close-peer": true}[c.Action] {
if !map[string]bool{"prepare": true, "details": true, "rename": true, "verify": true, "start": true, "replay": true, "stop": true, "option": true, "offer": true, "close-peer": true}[c.Action] {
return nil, errors.New("Операция не поддерживается.")
}
deadline, e := time.Parse(time.RFC3339Nano, c.Deadline)
@@ -239,7 +243,10 @@ func (s *Sensors) Submit(c SensorCommand, remote bool) (*SensorOperation, error)
return nil, errors.New("Срок команды истёк. Устройство не изменено.")
}
for _, v := range s.operations {
if v.State == "running" && v.Command.Session.DeviceID == c.Session.DeviceID {
if v.State == "running" && v.Command.Action == "prepare" {
return nil, errors.New("Подготовка модели ещё выполняется.")
}
if v.State == "running" && v.Command.Session.DeviceID == c.Session.DeviceID && !sensorViewAction(c.Action) && !sensorViewAction(v.Command.Action) {
return nil, errors.New("Другая операция устройства ещё выполняется.")
}
}
@@ -266,6 +273,7 @@ func (s *Sensors) Submit(c SensorCommand, remote bool) (*SensorOperation, error)
func (s *Sensors) execute(c SensorCommand) {
var result any
var err error
uncertain := false
inv := s.Inventory()
var item map[string]any
for _, v := range inv["items"].([]any) {
@@ -293,6 +301,7 @@ func (s *Sensors) execute(c SensorCommand) {
} else {
var v map[string]any
v, err = s.driver("/operation", c)
uncertain = err != nil || v["state"] == "unknown"
if err == nil {
if v["state"] == "complete" {
result = v["result"]
@@ -308,6 +317,9 @@ func (s *Sensors) execute(c SensorCommand) {
v.Updated = time.Now().Unix()
if err != nil {
v.State = "error"
if uncertain {
v.State = "unknown"
}
v.Error = err.Error()
} else {
v.State = "complete"
@@ -321,6 +333,13 @@ func (s *Sensors) execute(c SensorCommand) {
func (s *Sensors) prepare(c SensorCommand) (any, error) {
s.prepareMu.Lock()
defer s.prepareMu.Unlock()
for _, raw := range s.Inventory()["items"].([]any) {
item := raw.(map[string]any)
snap := item["snapshot"].(map[string]any)
if state := snap["acquisition"]; state != "idle" && state != "failed" {
return nil, errors.New("Остановите захват камер перед подготовкой модели.")
}
}
ctx, cancel := context.WithTimeout(context.Background(), 310*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "/usr/bin/systemctl", "start", "mission-core-node-realsense-prepare.service")
+81 -5
View File
@@ -1,10 +1,86 @@
package node
import (
"testing"
"time"
"errors"
"io"
"net/http"
"strings"
"testing"
"time"
)
func sensorTestCommand() SensorCommand {now:=time.Now();id:="op_01234567890123456789012345678901";return SensorCommand{APIVersion:SensorSchema,Kind:"OperationRequest",ID:id,Idempotency:id,Session:SensorSession{SessionID:"session_test",DeviceID:"rsd455_01234567890123456789012345678901"},Action:"start",Requested:now.UTC().Format(time.RFC3339Nano),Deadline:now.Add(time.Minute).UTC().Format(time.RFC3339Nano),Parameters:map[string]any{}}}
func TestSensorRejectsAuthorityAndExpiredRequests(t *testing.T){s,e:=OpenSensors(t.TempDir(),"node_test");if e!=nil{t.Fatal(e)};c:=sensorTestCommand();c.Action="shell";if _,e=s.Submit(c,false);e==nil{t.Fatal("arbitrary action admitted")};c=sensorTestCommand();c.ID="../../owned";if _,e=s.Submit(c,false);e==nil{t.Fatal("path admitted")};c=sensorTestCommand();c.Requested=time.Now().Add(-2*time.Minute).Format(time.RFC3339Nano);c.Deadline=time.Now().Add(-time.Minute).Format(time.RFC3339Nano);if _,e=s.Submit(c,false);e==nil{t.Fatal("expired command admitted")}}
func TestSensorUncertainCrashDoesNotReplay(t *testing.T){root:=t.TempDir();s,_:=OpenSensors(root,"node_test");c:=sensorTestCommand();old:=&SensorOperation{Command:c,State:"running",Updated:time.Now().Unix()};if e:=s.write(c.ID+".json",old);e!=nil{t.Fatal(e)};s,e:=OpenSensors(root,"node_test");if e!=nil{t.Fatal(e)};v,e:=s.Submit(c,false);if e!=nil||v.State!="unknown"{t.Fatalf("replay: %+v %v",v,e)};c.Parameters=map[string]any{"record":true};if _,e=s.Submit(c,false);e==nil{t.Fatal("id collision did not reject different command")}}
func sensorTestCommand() SensorCommand {
now := time.Now()
id := "op_01234567890123456789012345678901"
return SensorCommand{APIVersion: SensorSchema, Kind: "OperationRequest", ID: id, Idempotency: id, Session: SensorSession{SessionID: "session_test", DeviceID: "rsd455_01234567890123456789012345678901"}, Action: "start", Requested: now.UTC().Format(time.RFC3339Nano), Deadline: now.Add(time.Minute).UTC().Format(time.RFC3339Nano), Parameters: map[string]any{}}
}
func TestSensorRejectsAuthorityAndExpiredRequests(t *testing.T) {
s, e := OpenSensors(t.TempDir(), "node_test")
if e != nil {
t.Fatal(e)
}
c := sensorTestCommand()
c.Action = "shell"
if _, e = s.Submit(c, false); e == nil {
t.Fatal("arbitrary action admitted")
}
c = sensorTestCommand()
c.ID = "../../owned"
if _, e = s.Submit(c, false); e == nil {
t.Fatal("path admitted")
}
c = sensorTestCommand()
c.Requested = time.Now().Add(-2 * time.Minute).Format(time.RFC3339Nano)
c.Deadline = time.Now().Add(-time.Minute).Format(time.RFC3339Nano)
if _, e = s.Submit(c, false); e == nil {
t.Fatal("expired command admitted")
}
}
func TestSensorUncertainCrashDoesNotReplay(t *testing.T) {
root := t.TempDir()
s, _ := OpenSensors(root, "node_test")
c := sensorTestCommand()
old := &SensorOperation{Command: c, State: "running", Updated: time.Now().Unix()}
if e := s.write(c.ID+".json", old); e != nil {
t.Fatal(e)
}
s, e := OpenSensors(root, "node_test")
if e != nil {
t.Fatal(e)
}
v, e := s.Submit(c, false)
if e != nil || v.State != "unknown" {
t.Fatalf("replay: %+v %v", v, e)
}
c.Parameters = map[string]any{"record": true}
if _, e = s.Submit(c, false); e == nil {
t.Fatal("id collision did not reject different command")
}
}
type sensorRoundTrip func(*http.Request) (*http.Response, error)
func (f sensorRoundTrip) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
func TestSensorPreservesUncertainDriverOutcome(t *testing.T) {
for _, response := range []string{`{"state":"unknown","error":"uncertain"}`, "transport-failure"} {
t.Run(response, func(t *testing.T) {
s, _ := OpenSensors(t.TempDir(), "node_test")
c := sensorTestCommand()
s.operations[c.ID] = &SensorOperation{Command: c, State: "running"}
s.client = &http.Client{Transport: sensorRoundTrip(func(r *http.Request) (*http.Response, error) {
body := `{"items":[{"id":"` + c.Session.DeviceID + `"}]}`
if r.URL.Path == "/operation" {
if response == "transport-failure" {
return nil, errors.New("connection lost")
}
body = response
}
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body)), Header: http.Header{}}, nil
})}
s.execute(c)
if s.Get(c.ID).State != "unknown" {
t.Fatal("uncertain hardware effect reported as definite failure")
}
})
}
}