package v1 import ( "context" "strconv" "strings" "sync" "forge.cadoles.com/cadoles/go-emlid/reach/client/protocol" "forge.cadoles.com/cadoles/go-emlid/reach/client/protocol/v1/model" "forge.cadoles.com/cadoles/go-emlid/reach/client/socketio" "github.com/pkg/errors" ) type Operations struct { addr string client *socketio.Client mutex sync.RWMutex } // Close implements protocol.Operations. func (o *Operations) Close(ctx context.Context) error { o.mutex.Lock() defer o.mutex.Unlock() if o.client == nil { return nil } o.client.Close() return nil } const ( // EventBrowserConnected is emitted after the initial connection to the // ReachView endpoint eventBrowserConnected = "browser connected" ) // Connect implements protocol.Operations. func (o *Operations) Connect(ctx context.Context) error { o.mutex.Lock() defer o.mutex.Unlock() if o.client != nil { o.client.Close() } endpoint, err := socketio.EndpointFromHAddr(o.addr) if err != nil { return errors.WithStack(err) } client := socketio.NewClient(endpoint) if err := client.Connect(); err != nil { return errors.WithStack(err) } // Notifies the ReachView endpoint of a new connection. // See misc/reachview/update_main.js line 297 payload := map[string]string{ "data": "I'm connected", } if err := client.Emit(eventBrowserConnected, payload); err != nil { return errors.WithStack(err) } o.client = client return nil } // Emit implements protocol.Operations. func (o *Operations) Emit(ctx context.Context, mType string, message any) error { o.mutex.RLock() defer o.mutex.RUnlock() if o.client == nil || !o.client.Alive() { return errors.WithStack(protocol.ErrClosed) } if err := o.client.Emit(mType, message); err != nil { return errors.WithStack(err) } return nil } // On implements protocol.Operations. func (o *Operations) On(ctx context.Context, event string) (chan any, error) { o.mutex.RLock() defer o.mutex.RUnlock() if o.client == nil || !o.client.Alive() { return nil, errors.WithStack(protocol.ErrClosed) } out := make(chan any) closer := new(sync.Once) handler := func(ch *socketio.Channel, data any) { select { case <-ctx.Done(): closer.Do(func() { o.mutex.RLock() defer o.mutex.RUnlock() ch.Close() close(out) if o.client == nil { return } }) return default: out <- data } } if err := o.client.On(event, handler); err != nil { return nil, errors.WithStack(err) } return out, nil } // Alive implements protocol.Operations. func (o *Operations) Alive(ctx context.Context) (bool, error) { o.mutex.RLock() defer o.mutex.RUnlock() if o.client == nil || !o.client.Alive() { return false, errors.WithStack(protocol.ErrClosed) } return o.client.Alive(), nil } // Configuration implements protocol.Operations. func (o *Operations) Configuration(ctx context.Context) (any, error) { config, err := o.RequestConfiguration(ctx) if err != nil { return nil, errors.WithStack(err) } return config, nil } const ( eventReboot = "reboot" ) // Reboot implements protocol.Operations. func (o *Operations) Reboot(ctx context.Context) error { o.mutex.RLock() defer o.mutex.RUnlock() if o.client == nil || !o.client.Alive() { return errors.WithStack(protocol.ErrClosed) } var err error var wg sync.WaitGroup var once sync.Once done := func() { o.client.Off(socketio.OnDisconnection) wg.Done() } wg.Add(1) go func() { <-ctx.Done() err = ctx.Err() once.Do(done) }() err = o.client.On(socketio.OnDisconnection, func(h *socketio.Channel, data any) { once.Do(done) }) if err != nil { return errors.Wrapf(err, "error while binding to '%s' event", socketio.OnDisconnection) } if err = o.client.Emit(eventReboot, nil); err != nil { return err } wg.Wait() return err } const ( // ConfigurationApplySuccess - configurationApplySuccess = "success" // ConfigurationApplyFailed - configurationApplyFailed = "failed" ) // SetBase implements protocol.Operations. func (o *Operations) SetBase(ctx context.Context, funcs ...protocol.SetBaseOptionFunc) error { opts := protocol.NewSetBaseOptions(funcs...) var lat string if opts.Latitude != nil { lat = strconv.FormatFloat(*opts.Latitude, 'f', -1, 64) } var lon string if opts.Longitude != nil { lon = strconv.FormatFloat(*opts.Longitude, 'f', -1, 64) } var alt string if opts.Height != nil { alt = strconv.FormatFloat(*opts.Height, 'f', -1, 64) } var antennaHeight string if opts.AntennaOffset != nil { antennaHeight = strconv.FormatFloat(*opts.AntennaOffset, 'f', -1, 64) } config := &model.Configuration{ BaseMode: &model.BaseMode{ BaseCoordinates: &model.BaseCoordinates{ Accumulation: model.String("1"), Coordinates: []*string{ model.String(lat), model.String(lon), model.String(alt), }, AntennaOffset: &model.AntennaOffset{ East: model.String("0"), North: model.String("0"), Up: model.String(antennaHeight), }, Format: model.BaseCoordinatesFormatLLH, Mode: model.BaseCoordinatesModeManual, }, }, } result, _, err := o.ApplyConfiguration(ctx, config) if err != nil { return errors.New("configuration update failed") } if result != configurationApplySuccess { return errors.New("configuration update failed") } return nil } const ( eventGetReachViewVersion = "get reachview version" eventReachViewVersionResults = "current reachview version" ) type reachViewVersion struct { Version string `json:"version"` Stable bool `json:"bool"` } // Version implements protocol.Operations. func (o *Operations) Version(ctx context.Context) (string, bool, error) { res := &reachViewVersion{} if err := o.ReqResp(ctx, eventGetReachViewVersion, nil, eventReachViewVersionResults, res); err != nil { return "", false, err } return strings.TrimSpace(res.Version), res.Stable, nil } var _ protocol.Operations = &Operations{}