package v1 import ( "context" "strconv" "strings" "sync" "forge.cadoles.com/cadoles/go-emlid/reach/client/logger" "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 logger logger.Logger dial protocol.DialFunc } // 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.EndpointFromAddr(o.addr) if err != nil { return errors.WithStack(err) } client := socketio.NewClient(endpoint, socketio.WithDialFunc(socketio.DialFunc(o.dial))) o.logger.Debug("connecting", logger.Attr("endpoint", 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.logger.Debug("connected") 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 { rawConfig, err := o.Configuration(ctx) if err != nil { return errors.WithStack(err) } config := rawConfig.(*model.Configuration) baseMode := config.BaseMode var baseCoordinates *model.BaseCoordinates if baseMode != nil && baseMode.BaseCoordinates != nil { baseCoordinates = baseMode.BaseCoordinates } opts := protocol.NewSetBaseOptions(funcs...) var lat string if opts.Latitude != nil { lat = strconv.FormatFloat(*opts.Latitude, 'f', -1, 64) } else if baseCoordinates != nil && baseCoordinates.Coordinates != nil && len(baseCoordinates.Coordinates) > 1 { lat = *baseCoordinates.Coordinates[0] } else { lat = "0" } var lon string if opts.Longitude != nil { lon = strconv.FormatFloat(*opts.Longitude, 'f', -1, 64) } else if baseCoordinates != nil && baseCoordinates.Coordinates != nil && len(baseCoordinates.Coordinates) > 1 { lon = *baseCoordinates.Coordinates[1] } else { lon = "0" } var alt string if opts.Height != nil { alt = strconv.FormatFloat(*opts.Height, 'f', -1, 64) } else if baseCoordinates != nil && baseCoordinates.Coordinates != nil && len(baseCoordinates.Coordinates) > 1 { alt = *baseCoordinates.Coordinates[2] } else { alt = "0" } var antennaHeight string if opts.AntennaOffset != nil { antennaHeight = strconv.FormatFloat(*opts.AntennaOffset, 'f', -1, 64) } else if baseCoordinates != nil && baseCoordinates.AntennaOffset != nil && baseCoordinates.AntennaOffset.Up != nil { antennaHeight = *baseCoordinates.AntennaOffset.Up } else { antennaHeight = "0" } newConfig := &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, newConfig) if err != nil { return errors.New("configuration update failed") } if result != configurationApplySuccess { return errors.New("configuration update failed") } return nil } // GetBaseInfo implements protocol.Operations. func (o *Operations) GetBaseInfo(ctx context.Context) (*protocol.BaseInfo, error) { rawConfig, err := o.Configuration(ctx) if err != nil { return nil, errors.WithStack(err) } config := rawConfig.(*model.Configuration) baseMode := config.BaseMode var baseCoordinates *model.BaseCoordinates if baseMode != nil && baseMode.BaseCoordinates != nil { baseCoordinates = baseMode.BaseCoordinates } antennaOffset, err := strconv.ParseFloat(*baseCoordinates.AntennaOffset.Up, 64) if err != nil { return nil, errors.WithStack(err) } latitude, err := strconv.ParseFloat(*baseCoordinates.Coordinates[0], 64) if err != nil { return nil, errors.WithStack(err) } longitude, err := strconv.ParseFloat(*baseCoordinates.Coordinates[1], 64) if err != nil { return nil, errors.WithStack(err) } height, err := strconv.ParseFloat(*baseCoordinates.Coordinates[2], 64) if err != nil { return nil, errors.WithStack(err) } baseInfo := &protocol.BaseInfo{ Mode: *config.BaseMode.BaseCoordinates.Mode, AntennaOffset: antennaOffset, Height: height, Latitude: latitude, Longitude: longitude, } return baseInfo, 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{}