package jsonrpc import ( "context" "fmt" "log" "net/http" "time" "forge.cadoles.com/Cadoles/owrt" "forge.cadoles.com/Pyxis/orion/emlid" "forge.cadoles.com/Pyxis/orion/emlid/reachview" "forge.cadoles.com/Pyxis/orion/emlid/updater" "github.com/gorilla/rpc" "github.com/gorilla/rpc/json" "github.com/pkg/errors" ) // OrionService is the JSON-RPC API type OrionService struct { UCI *owrt.UCI } // NewOrionService create a new OrionService ! func NewOrionService() *OrionService { uci := owrt.NewUCI() return &OrionService{ UCI: uci, } } // UCIArgs argument structure for exported method OwrtListWifiDevices type UCIArgs struct{} // UCIResponse is the response structure for exposed method OwrtListWifiDevices type UCIResponse struct { Devices []map[string]string } // OwrtListWifiDevices offers an RPC Method to list Wifi nics in a OpenWRT device. func (o *OrionService) OwrtListWifiDevices(r *http.Request, args *UCIArgs, reply *UCIResponse) error { o.UCI.LoadWirelessConf() devs := o.UCI.GetWifiDevices() reply.Devices = devs return nil } // ListIfaceArgs argument structure for exported method OwrtListWifiDevices type ListIfaceArgs struct{} // ListIfaceResponse is the response structure for exposed method OwrtListWifiDevices type ListIfaceResponse struct { Interfaces map[int]*owrt.UCIWirelessInterface } // OwrtListWifiInterfaces offers an RPC Method to list wifi interfaces in a OpenWRT device. func (o *OrionService) OwrtListWifiInterfaces(r *http.Request, args *ListIfaceArgs, reply *ListIfaceResponse) error { o.UCI.LoadWirelessConf() reply.Interfaces = o.UCI.GetWifiIfaces() return nil } // CreateIfaceArgs argument structure for exported method OwrtCreateWifiInterface type CreateIfaceArgs struct { Cleanup bool Iface *owrt.UCIWirelessInterface } // CreateIfaceResponse argument structure for exported method OwrtCreateWifiInterface type CreateIfaceResponse struct { Errors []*owrt.Action Iface *owrt.UCIWirelessInterface } // OwrtCreateWifiInterface Create a WifiInterface in openwrt func (o *OrionService) OwrtCreateWifiInterface(r *http.Request, args *CreateIfaceArgs, reply *CreateIfaceResponse) error { reply.Iface = nil if args.Cleanup { o.UCI.LoadWirelessConf() w := o.UCI.GetWifiIfaces() for _, iface := range w { iface.Delete(o.UCI) } o.UCI.Commit() o.UCI.Reload() time.Sleep(3 * time.Second) } o.UCI.LoadWirelessConf() create := args.Iface.Create(o.UCI) if create.ReturnCode != 0 { reply.Errors = append(reply.Errors, create) return nil } create = args.Iface.SysAdd(o.UCI) if create.ReturnCode == 233 { delete := args.Iface.SysDel(o.UCI) if delete.ReturnCode != 0 { reply.Errors = append(reply.Errors, delete) return nil } create = args.Iface.SysAdd(o.UCI) if create.ReturnCode != 0 { reply.Errors = append(reply.Errors, create) return nil } } create = args.Iface.Up(o.UCI) if create.ReturnCode != 0 { reply.Errors = append(reply.Errors, create) return nil } create = args.Iface.Save(o.UCI) if create.ReturnCode != 0 { reply.Errors = append(reply.Errors, create) return nil } reply.Iface = args.Iface return nil } // ConnectIfaceArgs argument structure for exported method OwrtCreateWifiInterface type ConnectIfaceArgs struct { Iface *owrt.UCIWirelessInterface SSID string Key string } // ConnectIfaceResponse argument structure for exported method OwrtCreateWifiInterface type ConnectIfaceResponse struct { status int Errors []string } // OwrtConnectWifiInterface connects a given Wifi Interface to a given SSID func (o *OrionService) OwrtConnectWifiInterface(r *http.Request, args *ConnectIfaceArgs, reply *ConnectIfaceResponse) error { o.UCI.LoadWirelessConf() iface := o.UCI.GetWifiIface(args.Iface.Index) cells := iface.Scan() for _, cell := range cells { if cell.Ssid == args.SSID { cn := iface.Connect(o.UCI, cell, args.Key) if cn.ReturnCode != 0 { reply.status = cn.ReturnCode reply.Errors = append(reply.Errors, cn.Stdout, cn.Stderr, cn.Command) } reply.status = 0 return nil } } reply.status = 1 msg := fmt.Sprintf("Wifi Cell with SSID %s is not available !", args.SSID) reply.Errors = append(reply.Errors, msg) return nil } // OrionBox describe an fresh Orion box (base or rover) type OrionBox struct { Address string NewAddress string SSID string Security string WifiKey string } // OrionServer describe the Orion master server type OrionServer struct { Address string SSID string Security string WifiKey string ClientIface *owrt.UCIWirelessInterface } // UpdateOrionBoxArgs argument structure for exported method OwrtCreateWifiInterface type UpdateOrionBoxArgs struct { Box *OrionBox Server *OrionServer } // UpdateOrionBoxResponse argument structure for exported method OwrtCreateWifiInterface type UpdateOrionBoxResponse struct { IP string Netmask string Synced bool Version string Status *updater.UpdateStatus Results *updater.TestResults } // connect wifi interface to a Orion box wifi hotspot! func (o *OrionService) connectBox(box *OrionBox, server *OrionServer) error { o.UCI.LoadWirelessConf() if server == nil { return fmt.Errorf("Server definition is empty") } if box == nil { return fmt.Errorf("Box definitioni is emtpy") } iface := server.ClientIface cells := iface.Scan() for _, cell := range cells { if cell.Ssid == box.SSID { cn := iface.Connect(o.UCI, cell, box.WifiKey) if cn.ReturnCode != 0 { return fmt.Errorf("%s\n%s", cn.Stdout, cn.Stderr) } dhcli := owrt.NewDhcpClient(iface.SysDevName) dhres := dhcli.AskForIP() if dhres.CmdRes.ReturnCode != 0 { return fmt.Errorf("%s\n%s", cn.Stdout, cn.Stderr) } re := o.UCI.Reload() if re.ReturnCode != 0 { return fmt.Errorf("%s\n%s", re.Stdout, re.Stderr) } return nil } } return fmt.Errorf("Wifi cell with SSID %s is not available", box.SSID) } func (o *OrionService) discoverService(rqContext context.Context) ([]emlid.Service, error) { ctx, cancelDiscover := context.WithTimeout(rqContext, 55*time.Second) defer cancelDiscover() return emlid.Discover(ctx) } func (o *OrionService) connectUpdater(rqContext context.Context, box *OrionBox) (*updater.Client, error) { var boxCli *updater.Client service, err := o.discoverService(rqContext) fmt.Printf("DEBUG len = %d\n", len(service)) if len(service) == 0 { fmt.Println("DEBUG Connecting to box with EndPoint !") boxCli = updater.NewClient( emlid.WithEndpoint(box.Address, 80), ) } else if err == nil { fmt.Println("DEBUG Connecting to box with Service !") boxCli = updater.NewClient( emlid.WithService(service[0]), ) } else { fmt.Printf("Non mais là c'est vraiment étrange !") return nil, err } //fmt.Printf("NAME: %s, IP: %s", service[0].Name, service[0].AddrV4) return boxCli, err } // setupMasterWifi connects to the box, add wifi network and join it ! func (o *OrionService) setupMasterWifi(rqContext context.Context, box *OrionBox, server *OrionServer) error { if err := o.connectBox(box, server); err != nil { return errors.Wrap(err, "Connect to box failed") } if server.Security == "" { server.Security = string(updater.SecurityWPAPSK) } boxCli, cliErr := o.connectUpdater(rqContext, box) if cliErr != nil { return errors.Wrap(cliErr, "Creation of updater client failed") } if err := boxCli.Connect(); err != nil { return errors.Wrap(err, "Connecting to Box failed") } defer boxCli.Close() fmt.Println("Running Tests !") ctx, testResultsCancel := context.WithTimeout(rqContext, 55*time.Second) defer testResultsCancel() _, err := boxCli.TestResults(ctx) if err != nil { return errors.Wrap(err, "Minimal test failed") } fmt.Println("Add Wifi !") ctx, addWifiCancel := context.WithTimeout(rqContext, 55*time.Second) defer addWifiCancel() done, err := boxCli.AddWifiNetwork(ctx, server.SSID, updater.WifiSecurity(server.Security), server.WifiKey) if err != nil { return errors.Wrap(err, "AddWifiNetworkFailed") } if !done { return fmt.Errorf("Impossible to add wifi network") } ctx, joinWifiCancel := context.WithTimeout(ctx, 55*time.Second) defer joinWifiCancel() fmt.Println("Join Wifi !") if err := boxCli.JoinWifiNetwork(ctx, server.SSID, true); err != nil { return errors.Wrap(err, "Time sync failed") } return nil } // updateAndReboot connects to the box with the New address and run basic tests on version dans updates func (o *OrionService) updateAndReboot(rqContext context.Context, box *OrionBox, reply *UpdateOrionBoxResponse) error { time.Sleep(3 * time.Second) boxCli, err := o.connectUpdater(rqContext, box) if err != nil { return errors.Wrap(err, "Problem connecting to updater") } if err := boxCli.Connect(); err != nil { fmt.Println("Waiting 3 seconds on error") time.Sleep(3 * time.Second) boxCli, clerr := o.connectUpdater(rqContext, box) if clerr != nil { return errors.Wrap(err, "Problem connecting to updater") } if err := boxCli.Connect(); err != nil { return errors.Wrap(err, "Connecting to Box on master wifi network failed") } } defer boxCli.Close() ctx, timeSyncedCancel := context.WithTimeout(rqContext, 55*time.Second) defer timeSyncedCancel() synced, err := boxCli.TimeSynced(ctx) if err != nil { return errors.Wrap(err, "Time sync failed") } reply.Synced = synced ctx, reachviewVersionCancel := context.WithTimeout(rqContext, 55*time.Second) defer reachviewVersionCancel() version, err := boxCli.ReachViewVersion(ctx) if err != nil { return err } reply.Version = version ctx, updateCancel := context.WithTimeout(rqContext, 55*time.Second) defer updateCancel() status, err := boxCli.Update(ctx) if err != nil { return errors.Wrap(err, "System update failed") } reply.Status = status if err := boxCli.SkipUpdate(); err != nil { log.Fatalf("error while skipping update: %s", err) } ctx, rebootCancel := context.WithTimeout(rqContext, 55*time.Second) defer rebootCancel() return boxCli.RebootNow(ctx, true) } // UpdateOrionBox starts provisionning process for an Orion box (base or rover) func (o *OrionService) UpdateOrionBox(r *http.Request, args *UpdateOrionBoxArgs, reply *UpdateOrionBoxResponse) error { // if err := o.setupMasterWifi(r.Context(), args.Box, args.Server); err != nil { // return err // } _ = o.setupMasterWifi(r.Context(), args.Box, args.Server) return o.updateAndReboot(r.Context(), args.Box, reply) } // connectReachview creates the client to a reachview module and returns it ! func (o *OrionService) connectReachView(rqContext context.Context, box *OrionBox) (*reachview.Client, error) { var boxCli *reachview.Client service, err := o.discoverService(rqContext) fmt.Printf("DEBUG len = %d\n", len(service)) if len(service) == 0 { fmt.Println("DEBUG Connecting to box with EndPoint !") boxCli = reachview.NewClient( emlid.WithEndpoint(box.Address, 80), ) } else if err == nil { fmt.Println("DEBUG Connecting to box with Service !") boxCli = reachview.NewClient( emlid.WithService(service[0]), ) } else { fmt.Printf("Non mais là c'est vraiment étrange !") return nil, err } //fmt.Printf("NAME: %s, IP: %s", service[0].Name, service[0].AddrV4) return boxCli, err } // ConfigOrionBoxArgs argument structure for exported method OwrtCreateWifiInterface type ConfigOrionBoxArgs struct { Box *OrionBox } // ConfigOrionBoxResponse argument structure for exported method OwrtCreateWifiInterface type ConfigOrionBoxResponse struct { } func (o *OrionService) resetConfiguration(rqContext context.Context, c *reachview.Client) { ctx, resetCancel := context.WithTimeout(rqContext, 55*time.Second) defer resetCancel() result, _, err := c.ResetConfiguration(ctx) if err != nil { log.Fatal(err) } if result != reachview.ConfigurationApplySuccess { log.Fatal("configuration reset failed !") } } // FIXME have to came from parameters ! func (o *OrionService) getCommonConfiguration() *reachview.Configuration { return &reachview.Configuration{ RTKSettings: &reachview.RTKSettings{ PositioningSystems: &reachview.PositionningSystems{ GPS: reachview.True, GLONASS: reachview.True, Galileo: reachview.True, SBAS: reachview.True, QZSS: reachview.True, }, UpdateRate: reachview.String("5"), }, LoRa: &reachview.LoRa{ AirRate: reachview.String("9.11"), Frequency: reachview.Float(868000), OutputPower: reachview.String("20"), }, PositionOutput: &reachview.PositionOutput{ Output1: &reachview.Output{ Enabled: reachview.False, }, Output2: &reachview.Output{ Enabled: reachview.False, }, }, } } func (o *OrionService) applyConfiguration(rqContext context.Context, c *reachview.Client, config *reachview.Configuration) error { ctx, applyConfCancel := context.WithTimeout(rqContext, 55*time.Second) defer applyConfCancel() result, _, err := c.ApplyConfiguration(ctx, config) if err != nil { return err } if result != reachview.ConfigurationApplySuccess { return fmt.Errorf("Configuration update failed") } return c.RestartRTKLib() } func (o *OrionService) configureRover(rqContext context.Context, c *reachview.Client) error { _ = c.Connect() defer c.Close() o.resetConfiguration(rqContext, c) config := o.getCommonConfiguration() config.RTKSettings.GPSARMode = reachview.GPSARModeFixAndHold config.RTKSettings.GLONASSARMode = reachview.On config.RTKSettings.PositionningMode = reachview.PositionningModeKinematic config.RTKSettings.UpdateRate = reachview.String("5") config.CorrectionInput = &reachview.CorrectionInput{ Input2: &reachview.Input2{ Input: reachview.Input{ Enabled: reachview.True, Format: reachview.IOFormatRTCM3, Type: reachview.IOTypeLoRa, Path: reachview.String("lora"), }, SendPositionToBase: reachview.Off, }, } config.BaseMode = &reachview.BaseMode{ Output: &reachview.Output{ Enabled: reachview.False, }, } log.Println("configuring module as rover") return o.applyConfiguration(rqContext, c, config) } // ConfigureOrionBox starts provisionning process for an Orion box (base or rover) func (o *OrionService) ConfigureOrionBox(r *http.Request, args *ConfigOrionBoxArgs, reply *ConfigOrionBoxResponse) error { reach, err := o.connectReachView(r.Context(), args.Box) if err != nil { return errors.Wrap(err, "Impossible to create ReachView client") } fmt.Println("Start Rover Configuraiton !") return o.configureRover(r.Context(), reach) } // NewServer returns a new configured JSON-RPC server func NewServer() *rpc.Server { server := rpc.NewServer() server.RegisterCodec(json.NewCodec(), "application/json") if err := server.RegisterService(NewOrionService(), "Orion"); err != nil { panic(err) } return server }