diff --git a/cmd/average_position/main.go b/cmd/average_position/main.go index 19e504b..b7e7119 100644 --- a/cmd/average_position/main.go +++ b/cmd/average_position/main.go @@ -7,6 +7,7 @@ import ( "fmt" "log/slog" "os" + "time" reach "forge.cadoles.com/cadoles/go-emlid/reach/client" "forge.cadoles.com/cadoles/go-emlid/reach/client/logger" @@ -37,113 +38,277 @@ type Payload struct { AntennaOffset float64 `json:"antenna_offset"` } +type BaseConfig struct { + Latitude float64 + Longitude float64 + Height float64 + AntennaOffset float64 +} + func main() { flag.Parse() ctx := context.Background() - client := reach.NewClient(host) + client, err := initializeClient(ctx) + if err != nil { + fmt.Printf("[FATAL] %+v", err) + os.Exit(1) + } + defer closeClient(ctx, client) + + // Récupération de la configuration + config, err := retrieveAndProcessConfig(ctx, client) + if err != nil { + fmt.Printf("[FATAL] %+v", err) + os.Exit(1) + } + + baseConfig := extractBaseConfig(config) + fmt.Printf("Configuration base actuelle: lat=%v, lon=%v, height=%v, offset=%v\n\n NTRIPSettings: \n address:%s\n port:%d \n username: %s\n password:%s\n sendPositionToBase:%t\n", + baseConfig.Latitude, baseConfig.Longitude, baseConfig.Height, baseConfig.AntennaOffset, config.CorrectionInput.BaseCorrections.Settings.Ntripcli.Address, config.CorrectionInput.BaseCorrections.Settings.Ntripcli.Port, config.CorrectionInput.BaseCorrections.Settings.Ntripcli.Username, config.CorrectionInput.BaseCorrections.Settings.Ntripcli.Password, config.CorrectionInput.BaseCorrections.Settings.Ntripcli.SendPositionToBase) + + // Configuration des corrections NTRIP + if err := setupNTRIPCorrections(ctx, client, config); err != nil { + fmt.Printf("[FATAL] %+v", err) + os.Exit(1) + } + + //Configuration de la base + if err := setBaseToSingleAndHold(ctx, client, baseConfig); err != nil { + fmt.Printf("[FATAL] %+v", err) + os.Exit(1) + } + + // Collecte des données de positions + if err := averagePositionAndSave(ctx, client); err != nil { + fmt.Printf("[FATAL] %+v", err) + os.Exit(1) + } + + fmt.Println("Configuration terminée avec succès") +} + +func initializeClient(ctx context.Context) (*reach.Client, error) { logLevel, err := logger.ParseLevel(rawLogLevel) if err != nil { - fmt.Printf("[FATAL] %+v", errors.WithStack(err)) - os.Exit(1) + return nil, errors.WithStack(err) } slog.SetLogLoggerLevel(logLevel) + client := reach.NewClient(host) if err := client.Connect(ctx); err != nil { - fmt.Printf("[FATAL] %+v", errors.WithStack(err)) - os.Exit(1) + return nil, errors.WithStack(err) } - defer func() { - if err := client.Close(ctx); err != nil { - fmt.Printf("[FATAL] %+v", errors.WithStack(err)) - os.Exit(1) - } - }() - - // récupération de la configurationa actuelle de la base - config, err := retrieveAndProcessConfig(ctx, client) - if err != nil { - fmt.Printf("[FATAL] %+v", errors.WithStack(err)) - os.Exit(1) - } - latitude := config.BaseMode.BaseCoordinates.Coordinates.Latitude - longitude := config.BaseMode.BaseCoordinates.Coordinates.Longitude - height := config.BaseMode.BaseCoordinates.Coordinates.Height - antennaOffset := config.BaseMode.BaseCoordinates.AntennaOffset - fmt.Printf("setting base (latitude: %v, longitude: %v, height: %v, antennaOffset: %v", latitude, longitude, height, antennaOffset) - - opts := []protocol.SetBaseOptionFunc{ - protocol.WithBaseLatitude(latitude), - protocol.WithBaseLongitude(longitude), - protocol.WithBaseHeight(height), - protocol.WithBaseAntennaOffset(antennaOffset), - protocol.WithBaseMode("single-and-hold"), - } - // Passage de la base en "single-and-hold" permettant la collecte des données - if err := client.SetBase(ctx, opts...); err != nil { - fmt.Printf("[FATAL] %+v", errors.WithStack(err)) - return - } - - client.AveragePosition(ctx) - broadcasts, err := reach.OnMessageType(ctx, client, "task_status") - if err != nil { - fmt.Printf("[FATAL] %+v", errors.WithStack(err)) - os.Exit(1) - } - - for b := range broadcasts { - - data, err := json.MarshalIndent(b, "", " ") - if err != nil { - fmt.Printf("[ERROR] %+v", errors.WithStack(err)) - continue - } - fmt.Println(string(data)) - var payload Payload - err = mapstructure.Decode(b.Payload, &payload) - if err != nil { - fmt.Printf("Erreur de désérialisation : %v\n", err) - continue - } - - // la collecte est terminée, enregistrement du résultat en configuration - if b.State == "completed" { - fmt.Printf("lat: %g, long: %g, altitude:%g", payload.Coordinates.Latitude, payload.Coordinates.Longitude, payload.Coordinates.Height) - - opts := []protocol.SetBaseOptionFunc{ - protocol.WithBaseLatitude(payload.Coordinates.Latitude), - protocol.WithBaseLongitude(payload.Coordinates.Longitude), - protocol.WithBaseHeight(payload.Coordinates.Height), - protocol.WithBaseAntennaOffset(payload.AntennaOffset), - protocol.WithBaseMode("manual"), - } - // enregistrement du résultat en configuration - if err := client.SetBase(ctx, opts...); err != nil { - fmt.Printf("[FATAL] %+v", errors.WithStack(err)) - return - } - } + return client, nil +} +func closeClient(ctx context.Context, client *reach.Client) { + if err := client.Close(ctx); err != nil { + fmt.Printf("[ERROR] Erreur lors de la fermeture: %+v", errors.WithStack(err)) } } func retrieveAndProcessConfig(ctx context.Context, client *reach.Client) (*model.Configuration, error) { - configData, err := client.Configuration(ctx) if err != nil { - fmt.Printf("[ERROR] %+v", errors.WithStack(err)) - + return nil, errors.WithStack(err) } + var config model.Configuration - err = mapstructure.Decode(configData, &config) - if err != nil { - fmt.Printf("[ERROR] %+v", errors.WithStack(err)) - + if err := mapstructure.Decode(configData, &config); err != nil { + return nil, errors.WithStack(err) } - return &config, nil + return &config, nil +} + +func extractBaseConfig(config *model.Configuration) BaseConfig { + return BaseConfig{ + Latitude: config.BaseMode.BaseCoordinates.Coordinates.Latitude, + Longitude: config.BaseMode.BaseCoordinates.Coordinates.Longitude, + Height: config.BaseMode.BaseCoordinates.Coordinates.Height, + AntennaOffset: config.BaseMode.BaseCoordinates.AntennaOffset, + } +} + +func setupNTRIPCorrections(ctx context.Context, client *reach.Client, config *model.Configuration) error { + fmt.Println("Configuration des corrections NTRIP...") + + // Recherche de points de montage + if err := client.GetNTRIPMountPoint(ctx); err != nil { + return errors.WithStack(err) + } + + return processNTRIPStreams(ctx, client, config) +} + +func processNTRIPStreams(ctx context.Context, client *reach.Client, config *model.Configuration) error { + messages, err := reach.OnMessageType(ctx, client, "task_status") + if err != nil { + return errors.WithStack(err) + } + + timeout := time.NewTimer(30 * time.Second) + defer timeout.Stop() + + for { + select { + case b := <-messages: + if b.State == "completed" { + return handleNTRIPResponse(ctx, client, config, b) + } + + case <-timeout.C: + return errors.New("timeout lors de la récupération des streams NTRIP") + + case <-ctx.Done(): + return ctx.Err() + } + } +} + +func handleNTRIPResponse(ctx context.Context, client *reach.Client, config *model.Configuration, message reach.Broadcast) error { + var response model.NTRIPResponse + if err := mapstructure.Decode(message, &response); err != nil { + return errors.WithStack(err) + } + + displayAvailableStreams(response) + + return updateNTRIPMountPoint(ctx, client, config) +} + +func displayAvailableStreams(response model.NTRIPResponse) { + streams := response.Payload.Str + fmt.Printf("=== %d Streams disponibles ===\n", len(streams)) + for i, stream := range streams { + if stream.Distance < 50 { + fmt.Printf("%d. %-15s | %s (%s) | %.1fkm\n", + i+1, + stream.Mountpoint, + stream.ID, + stream.Country, + stream.Distance) + } + } +} + +func updateNTRIPMountPoint(ctx context.Context, client *reach.Client, config *model.Configuration) error { + opts := []protocol.SetBaseCorrectionsFunc{ + protocol.WithNTRIPAddress(config.CorrectionInput.BaseCorrections.Settings.Ntripcli.Address), + protocol.WithNTRIPPort(config.CorrectionInput.BaseCorrections.Settings.Ntripcli.Port), + protocol.WithNTRIPUsername(config.CorrectionInput.BaseCorrections.Settings.Ntripcli.Username), + protocol.WithNTRIPPassword(config.CorrectionInput.BaseCorrections.Settings.Ntripcli.Password), + // todo modification du point de montage ici + protocol.WithNTRIPMountPoint("EPI21"), + protocol.WithSendPositionToBase(true), + } + + if err := client.SetBaseCorrections(ctx, opts...); err != nil { + return errors.WithStack(err) + } + + fmt.Println("MountPoint NTRIP mis à jour") + return nil +} + +func setBaseToSingleAndHold(ctx context.Context, client *reach.Client, baseConfig BaseConfig) error { + fmt.Println("Configuration de la base en mode single-and-hold...") + + opts := []protocol.SetBaseOptionFunc{ + protocol.WithBaseLatitude(baseConfig.Latitude), + protocol.WithBaseLongitude(baseConfig.Longitude), + protocol.WithBaseHeight(baseConfig.Height), + protocol.WithBaseAntennaOffset(baseConfig.AntennaOffset), + protocol.WithBaseMode("single-and-hold"), + } + + if err := client.SetBase(ctx, opts...); err != nil { + return errors.WithStack(err) + } + + fmt.Println("Base configurée en mode single-and-hold") + return nil +} + +func averagePositionAndSave(ctx context.Context, client *reach.Client) error { + fmt.Println("Démarrage du moyennage de position...") + + if err := client.AveragePosition(ctx); err != nil { + return errors.WithStack(err) + } + + return waitForAverageCompletion(ctx, client) +} + +func waitForAverageCompletion(ctx context.Context, client *reach.Client) error { + broadcasts, err := reach.OnMessageType(ctx, client, "task_status") + if err != nil { + return errors.WithStack(err) + } + + timeout := time.NewTimer(5 * time.Minute) + defer timeout.Stop() + + for { + select { + case b := <-broadcasts: + if err := logTaskProgress(b); err != nil { + fmt.Printf("[WARNING] %+v", err) + continue + } + + if b.State == "completed" { + return handleAverageCompletion(ctx, client, b) + } + + case <-timeout.C: + return errors.New("timeout lors du moyennage de position") + + case <-ctx.Done(): + return ctx.Err() + } + } +} + +func logTaskProgress(message reach.Broadcast) error { + data, err := json.MarshalIndent(message, "", " ") + if err != nil { + return errors.WithStack(err) + } + fmt.Println(string(data)) + return nil +} + +func handleAverageCompletion(ctx context.Context, client *reach.Client, message reach.Broadcast) error { + var payload Payload + if err := mapstructure.Decode(message.Payload, &payload); err != nil { + return errors.WithStack(err) + } + + coords := payload.Coordinates + fmt.Printf("Position moyennée: lat=%g, lon=%g, altitude=%g\n", + coords.Latitude, coords.Longitude, coords.Height) + + return saveAveragedPosition(ctx, client, payload) +} + +func saveAveragedPosition(ctx context.Context, client *reach.Client, payload Payload) error { + opts := []protocol.SetBaseOptionFunc{ + protocol.WithBaseLatitude(payload.Coordinates.Latitude), + protocol.WithBaseLongitude(payload.Coordinates.Longitude), + protocol.WithBaseHeight(payload.Coordinates.Height), + protocol.WithBaseAntennaOffset(payload.AntennaOffset), + protocol.WithBaseMode("manual"), + } + + if err := client.SetBase(ctx, opts...); err != nil { + return errors.WithStack(err) + } + + fmt.Println("Position sauvegardée en configuration") + return nil } diff --git a/cmd/broadcast/main.go b/cmd/broadcast/main.go index e0e9015..699bd6a 100644 --- a/cmd/broadcast/main.go +++ b/cmd/broadcast/main.go @@ -17,12 +17,14 @@ var ( host string = "192.168.42.1" filter string = "" rawLogLevel string = "ERROR" + messageType string = "broadcast" ) func init() { flag.StringVar(&rawLogLevel, "log-level", rawLogLevel, "log level") flag.StringVar(&host, "host", host, "the reachrs module host") - flag.StringVar(&filter, "filter", filter, "filter the broadcast messages by name") + flag.StringVar(&filter, "filter", filter, "filter the socket messages by name") + flag.StringVar(&messageType, "messageType", messageType, "socket messages by name") } func main() { @@ -51,7 +53,7 @@ func main() { } }() - broadcasts, err := reach.OnBroadcast(ctx, client) + broadcasts, err := reach.OnMessageType(ctx, client, messageType) if err != nil { fmt.Printf("[FATAL] %+v", errors.WithStack(err)) os.Exit(1) diff --git a/reach/client/operations.go b/reach/client/operations.go index 313415c..1e8b8d7 100644 --- a/reach/client/operations.go +++ b/reach/client/operations.go @@ -166,4 +166,31 @@ func (c *Client) AveragePosition(ctx context.Context) error { return nil } +// GetNTRIPMountPoint implements protocol.Operations. +func (c *Client) GetNTRIPMountPoint(ctx context.Context) error { + _, ops, err := c.getProtocol(ctx) + if err != nil { + return errors.WithStack(err) + } + + if err := ops.GetNTRIPMountPoint(ctx); err != nil { + return errors.WithStack(err) + } + + return nil +} + +// GetNTRIPMountPoint implements protocol.Operations. +func (c *Client) SetBaseCorrections(ctx context.Context, funcs ...protocol.SetBaseCorrectionsFunc) error { + _, ops, err := c.getProtocol(ctx) + if err != nil { + return errors.WithStack(err) + } + + if err := ops.SetBaseCorrections(ctx, funcs...); err != nil { + return errors.WithStack(err) + } + return nil +} + var _ protocol.Operations = &Client{} diff --git a/reach/client/protocol/operations.go b/reach/client/protocol/operations.go index 29a3f7e..33edc62 100644 --- a/reach/client/protocol/operations.go +++ b/reach/client/protocol/operations.go @@ -42,5 +42,12 @@ type Operations interface { // Reboot restarts the module Reboot(ctx context.Context) error + // AveragePosition gathers data and computes the average position AveragePosition(ctx context.Context) error + + //GetNTRIPMountPoint retrieves availables mount point + GetNTRIPMountPoint(ctx context.Context) error + + //SetBaseCorrections updates the corrections obtaining station + SetBaseCorrections(ctx context.Context, funcs ...SetBaseCorrectionsFunc) error } diff --git a/reach/client/protocol/set_base_correction.go b/reach/client/protocol/set_base_correction.go new file mode 100644 index 0000000..f8cfcac --- /dev/null +++ b/reach/client/protocol/set_base_correction.go @@ -0,0 +1,56 @@ +package protocol + +type SetBaseCorrectionsOptions struct { + Address *string + Port *int + Username *string + Password *string + MountPoint *string + SendPositionToBase *bool +} + +type SetBaseCorrectionsFunc func(opts *SetBaseCorrectionsOptions) + +func NewSetBaseCorrectionsOptions(funcs ...SetBaseCorrectionsFunc) *SetBaseCorrectionsOptions { + opts := &SetBaseCorrectionsOptions{} + for _, fn := range funcs { + fn(opts) + } + return opts +} + +func WithNTRIPAddress(value string) SetBaseCorrectionsFunc { + return func(opts *SetBaseCorrectionsOptions) { + opts.Address = &value + } +} + +func WithNTRIPPort(value int) SetBaseCorrectionsFunc { + return func(opts *SetBaseCorrectionsOptions) { + opts.Port = &value + } +} + +func WithNTRIPUsername(value string) SetBaseCorrectionsFunc { + return func(opts *SetBaseCorrectionsOptions) { + opts.Username = &value + } +} + +func WithNTRIPPassword(value string) SetBaseCorrectionsFunc { + return func(opts *SetBaseCorrectionsOptions) { + opts.Password = &value + } +} + +func WithNTRIPMountPoint(value string) SetBaseCorrectionsFunc { + return func(opts *SetBaseCorrectionsOptions) { + opts.MountPoint = &value + } +} + +func WithSendPositionToBase(value bool) SetBaseCorrectionsFunc { + return func(opts *SetBaseCorrectionsOptions) { + opts.SendPositionToBase = &value + } +} diff --git a/reach/client/protocol/v1/operations.go b/reach/client/protocol/v1/operations.go index 3a910c0..45afc8e 100644 --- a/reach/client/protocol/v1/operations.go +++ b/reach/client/protocol/v1/operations.go @@ -2,6 +2,7 @@ package v1 import ( "context" + "encoding/json" "strconv" "strings" "sync" @@ -378,4 +379,43 @@ func (o *Operations) AveragePosition(ctx context.Context) error { return err } +// TODO À VOIR POUR LES VERSION 1 +func (o *Operations) GetNTRIPMountPoint(ctx context.Context) error { + var err error + + go func() { + <-ctx.Done() + err = ctx.Err() + }() + payloadJSON, err := json.Marshal(map[string]interface{}{ + "address": "crtk.net", + "port": 2101, + }) + if err = o.client.Emit("task", map[string]string{"name": "get_ntrip_mountpoints", "payload": string(payloadJSON)}); err != nil { + return err + } + + return err +} + +// todo +func (o *Operations) SetBaseCorrections(ctx context.Context, funcs ...protocol.SetBaseCorrectionsFunc) error { + var err error + + go func() { + <-ctx.Done() + err = ctx.Err() + }() + // todo + payloadJSON, err := json.Marshal(map[string]interface{}{ + "address": "crtk.net", + "port": 2101, + }) + if err = o.client.Emit("task", map[string]string{"name": "get_ntrip_mountpoints", "payload": string(payloadJSON)}); err != nil { + return err + } + + return err +} + var _ protocol.Operations = &Operations{} diff --git a/reach/client/protocol/v2/internal.go b/reach/client/protocol/v2/internal.go index 4a3aa4e..fa9cb88 100644 --- a/reach/client/protocol/v2/internal.go +++ b/reach/client/protocol/v2/internal.go @@ -126,6 +126,16 @@ func (o *Operations) PostDevice(ctx context.Context, device *model.Configuration return &updated, nil } +func (o *Operations) PostBaseCorrection(ctx context.Context, base *model.IOConfig) (*model.IOConfig, error) { + var updated model.IOConfig + + if err := o.PostJSON("/configuration/correction_input/base_corrections", base, &updated); err != nil { + return nil, errors.WithStack(err) + } + + return &updated, nil +} + func (o *Operations) GetUpdater(ctx context.Context) (*model.Updater, error) { updater := &model.Updater{} if err := o.GetJSON("/updater", updater); err != nil { diff --git a/reach/client/protocol/v2/model/action.go b/reach/client/protocol/v2/model/action.go index 57096f6..4c72142 100644 --- a/reach/client/protocol/v2/model/action.go +++ b/reach/client/protocol/v2/model/action.go @@ -1,5 +1,6 @@ package model type Action struct { - Name string `json:"name"` + Name string `json:"name"` + Paylaod map[string]interface{} `json:"payload"` } diff --git a/reach/client/protocol/v2/model/corrections_input.go b/reach/client/protocol/v2/model/corrections_input.go new file mode 100644 index 0000000..4f81281 --- /dev/null +++ b/reach/client/protocol/v2/model/corrections_input.go @@ -0,0 +1,81 @@ +package model + +type IOConfig struct { + IOType string `json:"io_type"` + Settings IOConfigSettings `json:"settings"` +} + +type IOConfigSettings struct { + NTRIPCli NTRIPCliConfig `json:"ntripcli"` +} + +type NTRIPCliConfig struct { + Address string `json:"address"` + Port int `json:"port"` + Username string `json:"username"` + Password string `json:"password"` + MountPoint string `json:"mount_point"` + SendPositionToBase bool `json:"send_position_to_base"` +} + +type NTRIPResponse struct { + Name string `json:"name"` + Payload NTRIPPayload `json:"payload"` + State string `json:"state"` +} + +type NTRIPPayload struct { + CAS []CasterInfo `json:"cas"` + Net []NetworkInfo `json:"net"` + Str []StreamInfo `json:"str"` +} + +type CasterInfo struct { + Country string `json:"country"` + Distance float64 `json:"distance"` + FallbackHost string `json:"fallback_host"` + FallbackPort string `json:"fallback_port"` + Host string `json:"host"` + ID string `json:"id"` + Latitude string `json:"latitude"` + Longitude string `json:"longitude"` + NMEA string `json:"nmea"` + Operator string `json:"operator"` + OtherDetails *string `json:"other_details"` + Port string `json:"port"` + Site string `json:"site"` +} + +type NetworkInfo struct { + Authentication string `json:"authentication"` + Distance *float64 `json:"distance"` + Fee string `json:"fee"` + ID string `json:"id"` + Operator string `json:"operator"` + OtherDetails string `json:"other_details"` + WebNet string `json:"web_net"` + WebReg string `json:"web_reg"` + WebStr string `json:"web_str"` +} + +type StreamInfo struct { + Authentication string `json:"authentication"` + Bitrate string `json:"bitrate"` + Carrier string `json:"carrier"` + ComprEncryp string `json:"compr_encryp"` + Country string `json:"country"` + Distance float64 `json:"distance"` + Fee string `json:"fee"` + Format string `json:"format"` + FormatDetails string `json:"format_details"` + Generator string `json:"generator"` + ID string `json:"id"` + Latitude string `json:"latitude"` + Longitude string `json:"longitude"` + Mountpoint string `json:"mountpoint"` + NavSystem string `json:"nav_system"` + Network string `json:"network"` + NMEA string `json:"nmea"` + OtherDetails string `json:"other_details"` + Solution string `json:"solution"` +} diff --git a/reach/client/protocol/v2/operations.go b/reach/client/protocol/v2/operations.go index 06e58f3..6a76fbb 100644 --- a/reach/client/protocol/v2/operations.go +++ b/reach/client/protocol/v2/operations.go @@ -279,4 +279,71 @@ func (o *Operations) AveragePosition(ctx context.Context) error { return err } +// GetNTRIPMountPoint implements protocol.Operations. +func (o *Operations) GetNTRIPMountPoint(ctx context.Context) error { + var err error + + config, err := o.GetConfiguration(ctx) + if err != nil { + return errors.WithStack(err) + } + + go func() { + <-ctx.Done() + err = ctx.Err() + }() + + payload := map[string]any{ + "address": config.CorrectionInput.BaseCorrections.Settings.Ntripcli.Address, + "port": config.CorrectionInput.BaseCorrections.Settings.Ntripcli.Port, + } + + if err = o.client.Emit("task", &model.Action{Name: "get_ntrip_mountpoints", Paylaod: payload}); err != nil { + return err + } + + return err +} + +// SetBaseCorrections implements protocol.Operations. +func (o *Operations) SetBaseCorrections(ctx context.Context, funcs ...protocol.SetBaseCorrectionsFunc) error { + opts := protocol.NewSetBaseCorrectionsOptions(funcs...) + if opts.Address == nil { + return errors.New("NTRIP address is required") + } + if opts.Port == nil { + return errors.New("NTRIP port is required") + } + if opts.Username == nil { + return errors.New("NTRIP username is required") + } + if opts.Password == nil { + return errors.New("NTRIP password is required") + } + if opts.MountPoint == nil { + return errors.New("NTRIP mount point is required") + } + + config := &model.IOConfig{ + // todo parametrage du type ???? + IOType: "ntripcli", + Settings: model.IOConfigSettings{ + NTRIPCli: model.NTRIPCliConfig{ + Address: *opts.Address, + Port: *opts.Port, + Username: *opts.Username, + Password: *opts.Password, + MountPoint: *opts.MountPoint, + SendPositionToBase: opts.SendPositionToBase != nil && *opts.SendPositionToBase, + }, + }, + } + + if _, err := o.PostBaseCorrection(ctx, config); err != nil { + return errors.WithStack(err) + } + + return nil +} + var _ protocol.Operations = &Operations{}