package service import ( "errors" "sync" "time" "src.dualinventive.com/go/dinet" "src.dualinventive.com/go/dinet/rpc" "src.dualinventive.com/go/lib/dilog" ) // HeartbeatInterval is the heartbeat interval for the services const HeartbeatInterval = time.Minute // Compile-time interface validation var _ dinet.Device = &Device{} var _ dinet.ParentDevice = &Device{} // Device is a service device type Device struct { uid string conn dinet.DeviceConn router *dinet.Router logger dilog.Logger endpoint string appVersion string childrenLock sync.RWMutex children map[string]dinet.Device } // NewDevice creates a new device which is a service. BackendURI is the URI of SMP to connect to func NewDevice(logger dilog.Logger, backendURI string, devPrefix string, appVersion string) (*Device, error) { con, err := dinet.NewDeviceConn(dinet.TransportLowlevel) if err != nil { return nil, err } if err = con.Connect(backendURI); err != nil { return nil, err } uid, err := GenerateUIDFromMac(devPrefix) if err != nil { return nil, err } dLogger := logger.WithField("device:uid", uid) dLogger.Info("authenticating at SMP") if err = con.DeviceHandshake(uid); err != nil { dLogger.WithError(err).Error("handshake failed") if err2 := con.Close(); err2 != nil { dLogger.WithError(err2).Error("failed to close the connection") } return nil, err } rtr, err := dinet.NewRouter(logger, con) if err != nil { return nil, err } dev := &Device{ uid: uid, conn: con, router: rtr, logger: dLogger, endpoint: backendURI, appVersion: appVersion, children: make(map[string]dinet.Device), } rtr.Subscribe(uid, rpc.ClassMethodDeviceData, rpc.MsgTypeRequest, dev.requestDeviceData) rtr.Subscribe(uid, rpc.ClassMethodDeviceInfo, rpc.MsgTypeRequest, dev.deviceInfo) rtr.Subscribe(uid, rpc.ClassMethodConnectionInfo, rpc.MsgTypeRequest, dev.connectionInfo) dev.startBackgroundTasks() return dev, nil } func (d *Device) startBackgroundTasks() { ticker := time.NewTicker(HeartbeatInterval) go func() { for range ticker.C { err := d.sendDeviceData() if err != nil { d.logger.Fatal("device ping failed") return } } }() go func() { if err := d.router.ListenAndServe(); err != nil { d.logger.WithError(err).Error("service: listenAndServe failed") } err := d.Close() if err != nil { d.logger.WithError(err).Error("service: cannot close device") } }() } // RegisterUID registers the uid on the connection (dinetlowlevel message type Register) // DONT use this function, this is for internal use only. Use childdevice.Handshake() func (d *Device) RegisterUID(uid string) error { return d.conn.DeviceRegister(uid) } // UnregisterUID unregisters the uid on the connection (dinetlowlevel message type Unregister) // DONT use this function, this is for internal use only. Use childdevice.Close() func (d *Device) UnregisterUID(uid string) error { d.router.UnsubscribeDevice(uid) return d.conn.DeviceUnregister(uid) } // RegisterDevice registers a new device on behalf of the current device. The children will be updated with the given // device // DONT use this function, this is for internal use only. Use childdevice.Handshake() func (d *Device) RegisterDevice(device dinet.Device) error { d.childrenLock.Lock() d.children[device.UID()] = device d.childrenLock.Unlock() return d.conn.DeviceRegister(device.UID()) } // UnregisterDevice unregisters a device on behalf of the current device. The children will be updated with the given // device // DONT use this function, this is for internal use only. Use childdevice.Close() func (d *Device) UnregisterDevice(uid string) error { d.childrenLock.Lock() delete(d.children, uid) d.childrenLock.Unlock() return d.conn.DeviceUnregister(uid) } // Logger returns the logger func (d *Device) Logger() dilog.Logger { return d.logger } // send transfers a message for this device to the remote host func (d *Device) send(m *rpc.Msg) error { m.DeviceUID = d.uid return d.router.Send(m) } // Router returns the router func (d *Device) Router() *dinet.Router { return d.router } // requestDeviceData is the callback for device:data func (d *Device) requestDeviceData(req *rpc.Msg) *rpc.Msg { rep := req.CreateReply() rep.Result = rpc.NewResult([]rpc.DeviceData{{ State: rpc.DeviceStateIdle, Error: false, }}) return rep } // deviceInfo is the callback for device:info func (d *Device) deviceInfo(req *rpc.Msg) *rpc.Msg { rep := req.CreateReply() rep.Result = rpc.NewResult([]rpc.DeviceInfo{{ Type: rpc.DeviceTypeService, Version: rpc.VersionMap{rpc.VersionKeyFwService: d.appVersion}, }}) return rep } // connectionInfo is the callback for connection:info func (d *Device) connectionInfo(req *rpc.Msg) *rpc.Msg { rep := req.CreateReply() rep.Result = rpc.NewResult([]rpc.ConnectionInfo{{ Timeout: uint32(HeartbeatInterval.Seconds()), Transport: rpc.ConnectionTransportService, Service: &rpc.ConnectionInfoService{Endpoint: d.endpoint}, }}) return rep } // sendDeviceData publishes the device:data func (d *Device) sendDeviceData() error { return d.send(&rpc.Msg{ ClassMethod: rpc.ClassMethodDeviceData, Type: rpc.MsgTypePublish, Result: rpc.NewResult([]rpc.DeviceData{{ State: rpc.DeviceStateIdle, Error: false, }}), }) } // UID returns the UID of this service device func (d *Device) UID() string { return d.uid } // Search will search for the UID in the tree below this device func (d *Device) Search(uid string) (dinet.Device, error) { if uid == d.UID() { return d, nil } d.childrenLock.RLock() defer d.childrenLock.RUnlock() // First find via the direct children (fastest solution) for pUID, dev := range d.children { if pUID == uid { return dev, nil } } // Not found als direct child. Now find in the sub children for _, dev := range d.children { pDev, ok := dev.(dinet.ParentDevice) if !ok { continue } fDev, err := pDev.Search(uid) if err == nil { return fDev, nil } } return nil, errors.New("not found") } // Close closes all the children and closes the connection to SMP func (d *Device) Close() error { d.childrenLock.RLock() for _, c := range d.children { if err := c.Close(); err != nil { d.logger.WithError(err).WithField("child", c.UID()).Error("cannot close child") } } d.childrenLock.RUnlock() return d.conn.Close() }