src.dualinventive.com/go/dinet/service/device.go

237 lines
6.3 KiB
Go

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()
}