@@ -0,0 +1,422 @@
package node
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"io"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
)
const SensorSchema = "missioncore.nodedc/plugin-sdk/v0alpha2"
type SensorSession struct {
SessionID string ` json:"session_id" `
DeviceID string ` json:"device_id" `
}
type SensorCommand struct {
APIVersion string ` json:"api_version" `
Kind string ` json:"kind" `
ID string ` json:"operation_id" `
Session SensorSession ` json:"session" `
Action string ` json:"action_id" `
Requested string ` json:"requested_at" `
Deadline string ` json:"deadline_at" `
Idempotency string ` json:"idempotency_key" `
Parameters map [ string ] any ` json:"parameters" `
}
type SensorOperation struct {
Command SensorCommand ` json:"command" `
State string ` json:"state" `
Error string ` json:"error,omitempty" `
Result any ` json:"result,omitempty" `
Remote bool ` json:"remote,omitempty" `
Updated int64 ` json:"updated_at" `
}
type Sensors struct {
mu sync . Mutex
prepareMu sync . Mutex
root string
nodeID string
instance string
client * http . Client
operations map [ string ] * SensorOperation
names map [ string ] string
}
var sensorID = regexp . MustCompile ( ` ^rsd455_[0-9a-f] { 32}$ ` )
var operationID = regexp . MustCompile ( ` ^op_[0-9a-f] { 32}$ ` )
func OpenSensors ( root , nodeID string ) ( * Sensors , error ) {
dir := filepath . Join ( root , "sensors" )
if e := os . MkdirAll ( dir , 0700 ) ; e != nil {
return nil , e
}
s := & Sensors { root : dir , nodeID : nodeID , instance : "discovery_" + digest ( token ( ) ) [ : 24 ] , operations : map [ string ] * SensorOperation { } , names : map [ string ] string { } }
s . client = & http . Client { Timeout : 25 * time . Second , Transport : & http . Transport { DialContext : func ( ctx context . Context , _ , _ string ) ( net . Conn , error ) {
return ( & net . Dialer { } ) . DialContext ( ctx , "unix" , "/run/mission-core-sensors/driver.sock" )
} } }
files , _ := filepath . Glob ( filepath . Join ( dir , "op_*.json" ) )
for _ , p := range files {
data , e := os . ReadFile ( p )
if e != nil {
return nil , e
}
var v SensorOperation
if json . Unmarshal ( data , & v ) != nil {
return nil , errors . New ( "invalid sensor operation journal" )
}
if v . State == "running" {
v . State = "unknown"
v . Error = "Результат операции неизвестен после перезапуска. Проверьте состояние устройства."
}
s . operations [ v . Command . ID ] = & v
}
data , _ := os . ReadFile ( filepath . Join ( dir , "names.json" ) )
_ = json . Unmarshal ( data , & s . names )
return s , nil
}
func ( s * Sensors ) write ( name string , value any ) error {
data , e := json . Marshal ( value )
if e != nil {
return e
}
f , e := os . CreateTemp ( s . root , ".sensor-" )
if e != nil {
return e
}
defer os . Remove ( f . Name ( ) )
if _ , e = f . Write ( data ) ; e != nil {
f . Close ( )
return e
}
if e = f . Sync ( ) ; e != nil {
f . Close ( )
return e
}
f . Close ( )
if e = os . Rename ( f . Name ( ) , filepath . Join ( s . root , name ) ) ; e != nil {
return e
}
d , e := os . Open ( s . root )
if e != nil {
return e
}
defer d . Close ( )
return d . Sync ( )
}
func ( s * Sensors ) driver ( path string , body any ) ( map [ string ] any , error ) {
method := "GET"
var reader io . Reader
if body != nil {
method = "POST"
data , e := json . Marshal ( body )
if e != nil {
return nil , e
}
reader = bytes . NewReader ( data )
}
req , e := http . NewRequest ( method , "http://driver" + path , reader )
if e != nil {
return nil , e
}
req . Header . Set ( "X-Node-Id" , s . nodeID )
if body != nil {
req . Header . Set ( "Content-Type" , "application/json" )
}
response , e := s . client . Do ( req )
if e != nil {
return nil , errors . New ( "Служба камеры недоступна. Подготовьте устройство." )
}
defer response . Body . Close ( )
var result map [ string ] any
if json . NewDecoder ( io . LimitReader ( response . Body , 1024 * 1024 ) ) . Decode ( & result ) != nil {
return nil , errors . New ( "Не удалось прочитать результат драйвера." )
}
if response . StatusCode != 200 {
message , _ := result [ "error" ] . ( string )
return nil , errors . New ( message )
}
return result , nil
}
func ( s * Sensors ) Inventory ( ) map [ string ] any {
items := [ ] any { }
seen := map [ string ] bool { }
if result , e := s . driver ( "/inventory" , nil ) ; e == nil {
if found , ok := result [ "items" ] . ( [ ] any ) ; ok {
for _ , v := range found {
item , ok := v . ( map [ string ] any )
if ! ok {
continue
}
id , _ := item [ "id" ] . ( string )
seen [ id ] = true
s . mu . Lock ( )
if n := s . names [ id ] ; n != "" {
item [ "name" ] = n
}
s . mu . Unlock ( )
items = append ( items , item )
}
}
}
paths , _ := filepath . Glob ( "/sys/bus/usb/devices/*" )
for _ , path := range paths {
read := func ( n string ) string {
b , _ := os . ReadFile ( filepath . Join ( path , n ) )
return strings . TrimSpace ( string ( b ) )
}
if read ( "idVendor" ) != "8086" || read ( "idProduct" ) != "0b5c" {
continue
}
serial := read ( "serial" )
if serial == "" {
continue
}
h := sha256 . Sum256 ( [ ] byte ( serial ) )
id := "rsd455_" + hex . EncodeToString ( h [ : ] ) [ : 32 ]
if seen [ id ] {
continue
}
now := time . Now ( ) . UTC ( ) . Format ( time . RFC3339Nano )
name := "RealSense D455"
s . mu . Lock ( )
if n := s . names [ id ] ; n != "" {
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 } } )
}
var preparation any
if data , e := os . ReadFile ( "/var/lib/mission-core-node-drivers/preparation.json" ) ; e == nil && len ( data ) < 32768 {
_ = json . Unmarshal ( data , & preparation )
}
s . mu . Lock ( )
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 } )
}
}
s . mu . Unlock ( )
return map [ string ] any { "schema" : "missioncore.node.devices/v1" , "items" : items , "preparation" : preparation , "operations" : operations }
}
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 ] {
return nil , errors . New ( "Операция не поддерживается." )
}
deadline , e := time . Parse ( time . RFC3339Nano , c . Deadline )
requested , e2 := time . Parse ( time . RFC3339Nano , c . Requested )
if e != nil || e2 != nil || ! deadline . After ( requested ) || deadline . Sub ( requested ) > 6 * time . Minute {
return nil , errors . New ( "Некорректный срок команды." )
}
s . mu . Lock ( )
defer s . mu . Unlock ( )
if old := s . operations [ c . ID ] ; old != nil {
a , _ := json . Marshal ( old . Command )
b , _ := json . Marshal ( c )
if ! bytes . Equal ( a , b ) {
return nil , errors . New ( "Идентификатор операции уже использован." )
}
copy := * old
return & copy , nil
}
if ! deadline . After ( time . Now ( ) ) {
return nil , errors . New ( "Срок команды истёк. Устройство не изменено." )
}
for _ , v := range s . operations {
if v . State == "running" && v . Command . Session . DeviceID == c . Session . DeviceID {
return nil , errors . New ( "Другая операция устройства ещё выполняется." )
}
}
if len ( s . operations ) > 2000 {
for id , v := range s . operations {
if v . State != "running" && time . Now ( ) . Unix ( ) - v . Updated > 86400 {
delete ( s . operations , id )
os . Remove ( filepath . Join ( s . root , id + ".json" ) )
}
}
}
if len ( s . operations ) > 2000 {
return nil , errors . New ( "Журнал операций заполнен. Повторите позже." )
}
value := & SensorOperation { Command : c , State : "running" , Remote : remote , Updated : time . Now ( ) . Unix ( ) }
if e = s . write ( c . ID + ".json" , value ) ; e != nil {
return nil , e
}
s . operations [ c . ID ] = value
copy := * value
go s . execute ( c )
return & copy , nil
}
func ( s * Sensors ) execute ( c SensorCommand ) {
var result any
var err error
inv := s . Inventory ( )
var item map [ string ] any
for _ , v := range inv [ "items" ] . ( [ ] any ) {
i := v . ( map [ string ] any )
if i [ "id" ] == c . Session . DeviceID {
item = i
break
}
}
if item == nil {
err = errors . New ( "Камера не обнаружена. Проверьте подключение." )
} else if c . Action == "prepare" {
result , err = s . prepare ( c )
} else if c . Action == "rename" {
name , ok := c . Parameters [ "name" ] . ( string )
if ! ok || strings . TrimSpace ( name ) == "" || len ( [ ] rune ( name ) ) > 80 || strings . ContainsAny ( name , "\n\r\t" ) {
err = errors . New ( "Введите название до 80 символов." )
} else {
s . mu . Lock ( )
s . names [ c . Session . DeviceID ] = strings . TrimSpace ( name )
err = s . write ( "names.json" , s . names )
s . mu . Unlock ( )
result = map [ string ] bool { "ok" : err == nil }
}
} else {
var v map [ string ] any
v , err = s . driver ( "/operation" , c )
if err == nil {
if v [ "state" ] == "complete" {
result = v [ "result" ]
} else {
message , _ := v [ "error" ] . ( string )
err = errors . New ( message )
}
}
}
s . mu . Lock ( )
defer s . mu . Unlock ( )
v := s . operations [ c . ID ]
v . Updated = time . Now ( ) . Unix ( )
if err != nil {
v . State = "error"
v . Error = err . Error ( )
} else {
v . State = "complete"
v . Result = result
}
if s . write ( c . ID + ".json" , v ) != nil {
v . State = "unknown"
v . Error = "Не удалось сохранить результат операции. Обновите состояние устройства."
}
}
func ( s * Sensors ) prepare ( c SensorCommand ) ( any , error ) {
s . prepareMu . Lock ( )
defer s . prepareMu . Unlock ( )
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" )
cmd . Env = [ ] string { "PATH=/usr/bin:/bin" , "LANG=C" , "DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket" }
if cmd . Run ( ) != nil {
return nil , errors . New ( "Подготовка драйвера не завершена. Проверьте этапы и повторите действие." )
}
for i := 0 ; i < 12 ; i ++ {
inv := s . Inventory ( )
for _ , v := range inv [ "items" ] . ( [ ] any ) {
item := v . ( map [ string ] any )
if item [ "id" ] == c . Session . DeviceID && item [ "prepared" ] == true {
snap := item [ "snapshot" ] . ( map [ string ] any )
sc := snap [ "context" ] . ( map [ string ] any )
verify := c
verify . Action = "verify"
verify . Session . SessionID = sc [ "session_id" ] . ( string )
result , e := s . driver ( "/operation" , verify )
if e != nil {
return nil , e
}
if result [ "state" ] != "complete" {
message , _ := result [ "error" ] . ( string )
return nil , errors . New ( message )
}
return result [ "result" ] , nil
}
}
time . Sleep ( time . Second )
}
return nil , errors . New ( "Драйвер установлен, но камера не открылась. Проверьте USB-подключение." )
}
func ( s * Sensors ) Get ( id string ) * SensorOperation {
s . mu . Lock ( )
defer s . mu . Unlock ( )
if v := s . operations [ id ] ; v != nil {
copy := * v
return & copy
}
return nil
}
func ( s * Sensors ) RemoteResults ( ) [ ] any {
s . mu . Lock ( )
defer s . mu . Unlock ( )
out := [ ] any { }
for _ , v := range s . operations {
if v . Remote && time . Now ( ) . Unix ( ) - v . Updated < 600 {
copy := * v
out = append ( out , copy )
}
}
return out
}
func ( s * Sensors ) Routes ( mux * http . ServeMux , server * Server ) {
mux . HandleFunc ( "GET /api/devices" , func ( w http . ResponseWriter , r * http . Request ) {
if server . authorized ( w , r ) {
reply ( w , 200 , s . Inventory ( ) )
}
} )
mux . HandleFunc ( "POST /api/devices/operations" , func ( w http . ResponseWriter , r * http . Request ) {
if ! server . authorized ( w , r ) {
return
}
var c SensorCommand
r . Body = http . MaxBytesReader ( w , r . Body , 65536 )
if r . Header . Get ( "Content-Type" ) != "application/json" || json . NewDecoder ( r . Body ) . Decode ( & c ) != nil {
reply ( w , 400 , map [ string ] string { "error" : "Некорректная команда" } )
return
}
v , e := s . Submit ( c , false )
if e != nil {
reply ( w , 409 , map [ string ] string { "error" : e . Error ( ) } )
return
}
reply ( w , 202 , v )
} )
mux . HandleFunc ( "GET /api/devices/operations/{id}" , func ( w http . ResponseWriter , r * http . Request ) {
if ! server . authorized ( w , r ) {
return
}
v := s . Get ( r . PathValue ( "id" ) )
if v == nil {
reply ( w , 404 , map [ string ] string { "error" : "Операция не найдена" } )
return
}
reply ( w , 200 , v )
} )
}
func ( s * Sensors ) Acknowledge ( ids [ ] string ) {
s . mu . Lock ( )
defer s . mu . Unlock ( )
for _ , id := range ids {
if v := s . operations [ id ] ; v != nil && v . State != "running" {
v . Remote = false
_ = s . write ( id + ".json" , v )
}
}
}