diff --git a/config/messages.json b/config/messages.json index 7e578d21248..04dc8866b00 100644 --- a/config/messages.json +++ b/config/messages.json @@ -7874,6 +7874,15 @@ "file": "grpc_gsns.go" } }, + "error:pkg/networkserver:relay_already_exists": { + "translations": { + "en": "relay already exists" + }, + "description": { + "package": "pkg/networkserver", + "file": "grpc_relay.go" + } + }, "error:pkg/networkserver:relay_full_f_cnt": { "translations": { "en": "invalid full FCnt" @@ -7892,6 +7901,15 @@ "file": "relay.go" } }, + "error:pkg/networkserver:relay_not_found": { + "translations": { + "en": "relay not found" + }, + "description": { + "package": "pkg/networkserver", + "file": "grpc_relay.go" + } + }, "error:pkg/networkserver:relay_recent_uplinks": { "translations": { "en": "recent uplinks not found" diff --git a/pkg/networkserver/grpc_deviceregistry.go b/pkg/networkserver/grpc_deviceregistry.go index 78618824130..711cfd616a9 100644 --- a/pkg/networkserver/grpc_deviceregistry.go +++ b/pkg/networkserver/grpc_deviceregistry.go @@ -3565,7 +3565,7 @@ func (ns *NetworkServer) Delete(ctx context.Context, req *ttnpb.EndDeviceIdentif type nsEndDeviceBatchRegistry struct { ttnpb.UnimplementedNsEndDeviceBatchRegistryServer - NS *NetworkServer + devices DeviceRegistry } // Delete implements ttipb.NsEndDeviceBatchRegistryServer. @@ -3581,7 +3581,7 @@ func (srv *nsEndDeviceBatchRegistry) Delete( ); err != nil { return nil, err } - deleted, err := srv.NS.devices.BatchDelete(ctx, req.ApplicationIds, req.DeviceIds) + deleted, err := srv.devices.BatchDelete(ctx, req.ApplicationIds, req.DeviceIds) if err != nil { logRegistryRPCError(ctx, err, "Failed to delete device from registry") return nil, err diff --git a/pkg/networkserver/grpc_relay.go b/pkg/networkserver/grpc_relay.go new file mode 100644 index 00000000000..e39ff0f508d --- /dev/null +++ b/pkg/networkserver/grpc_relay.go @@ -0,0 +1,287 @@ +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package networkserver + +import ( + "context" + "fmt" + "strings" + + "go.thethings.network/lorawan-stack/v3/pkg/auth/rights" + "go.thethings.network/lorawan-stack/v3/pkg/band" + "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/frequencyplans" + . "go.thethings.network/lorawan-stack/v3/pkg/networkserver/internal" + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" +) + +var ( + errRelayAlreadyExists = errors.DefineAlreadyExists("relay_already_exists", "relay already exists") + errRelayNotFound = errors.DefineNotFound("relay_not_found", "relay not found") +) + +func validateRelaySecondChannel(secondCh *ttnpb.RelaySecondChannel, phy *band.Band, path ...string) error { + if secondCh == nil { + return nil + } + if _, ok := phy.DataRates[secondCh.DataRateIndex]; !ok { + return newInvalidFieldValueError(strings.Join(append(path, "data_rate_index"), ".")) + } + inSubBand := false + for _, sb := range phy.SubBands { + if sb.MinFrequency >= secondCh.Frequency && secondCh.Frequency <= sb.MaxFrequency { + inSubBand = true + break + } + } + if !inSubBand { + return newInvalidFieldValueError(strings.Join(append(path, "frequency"), ".")) + } + return nil +} + +func validateRelayConfigurationServed(served *ttnpb.RelayConfiguration_Served, phy *band.Band, path ...string) error { + if served == nil { + return nil + } + if err := validateRelaySecondChannel(served.SecondChannel, phy, append(path, "second_channel")...); err != nil { + return err + } + return nil +} + +func validateRelayConfigurationServing(serving *ttnpb.RelayConfiguration_Serving, phy *band.Band, path ...string) error { + if serving == nil { + return nil + } + if err := validateRelaySecondChannel(serving.SecondChannel, phy, append(path, "second_channel")...); err != nil { + return err + } + return nil +} + +func validateRelayConfiguration(conf *ttnpb.RelayConfiguration, phy *band.Band, path ...string) error { + if conf == nil { + return nil + } + switch mode := conf.Mode.(type) { + case *ttnpb.RelayConfiguration_Served_: + return validateRelayConfigurationServed(mode.Served, phy, append(path, "mode", "served")...) + case *ttnpb.RelayConfiguration_Serving_: + return validateRelayConfigurationServing(mode.Serving, phy, append(path, "mode", "serving")...) + case nil: + return nil + default: + panic(fmt.Sprintf("unknown mode %T", mode)) + } +} + +func relayParametersFromConfiguration(conf *ttnpb.RelayConfiguration) *ttnpb.RelayParameters { + if conf == nil { + return nil + } + switch mode := conf.Mode.(type) { + case *ttnpb.RelayConfiguration_Served_: + served := &ttnpb.ServedRelayParameters{ + Backoff: mode.Served.Backoff, + SecondChannel: mode.Served.SecondChannel, + ServingDeviceId: mode.Served.ServingDeviceId, + } + switch mode := mode.Served.Mode.(type) { + case *ttnpb.RelayConfiguration_Served_Always: + served.Mode = &ttnpb.ServedRelayParameters_Always{ + Always: mode.Always, + } + case *ttnpb.RelayConfiguration_Served_Dynamic: + served.Mode = &ttnpb.ServedRelayParameters_Dynamic{ + Dynamic: mode.Dynamic, + } + case *ttnpb.RelayConfiguration_Served_EndDeviceControlled: + served.Mode = &ttnpb.ServedRelayParameters_EndDeviceControlled{ + EndDeviceControlled: mode.EndDeviceControlled, + } + case nil: + default: + panic(fmt.Sprintf("unknown mode %T", mode)) + } + return &ttnpb.RelayParameters{ + Mode: &ttnpb.RelayParameters_Served{ + Served: served, + }, + } + case *ttnpb.RelayConfiguration_Serving_: + return &ttnpb.RelayParameters{ + Mode: &ttnpb.RelayParameters_Serving{ + Serving: &ttnpb.ServingRelayParameters{ + SecondChannel: mode.Serving.SecondChannel, + DefaultChannelIndex: mode.Serving.DefaultChannelIndex, + CadPeriodicity: mode.Serving.CadPeriodicity, + Limits: mode.Serving.Limits, + }, + }, + } + case nil: + return &ttnpb.RelayParameters{} + default: + panic(fmt.Sprintf("unknown mode %T", mode)) + } +} + +func relayConfigurationFromParameters(params *ttnpb.RelayParameters) *ttnpb.RelayConfiguration { + if params == nil { + return nil + } + switch mode := params.Mode.(type) { + case *ttnpb.RelayParameters_Served: + served := &ttnpb.RelayConfiguration_Served{ + Backoff: mode.Served.Backoff, + SecondChannel: mode.Served.SecondChannel, + ServingDeviceId: mode.Served.ServingDeviceId, + } + switch mode := mode.Served.Mode.(type) { + case *ttnpb.ServedRelayParameters_Always: + served.Mode = &ttnpb.RelayConfiguration_Served_Always{ + Always: mode.Always, + } + case *ttnpb.ServedRelayParameters_Dynamic: + served.Mode = &ttnpb.RelayConfiguration_Served_Dynamic{ + Dynamic: mode.Dynamic, + } + case *ttnpb.ServedRelayParameters_EndDeviceControlled: + served.Mode = &ttnpb.RelayConfiguration_Served_EndDeviceControlled{ + EndDeviceControlled: mode.EndDeviceControlled, + } + case nil: + default: + panic(fmt.Sprintf("unknown mode %T", mode)) + } + return &ttnpb.RelayConfiguration{ + Mode: &ttnpb.RelayConfiguration_Served_{ + Served: served, + }, + } + case *ttnpb.RelayParameters_Serving: + return &ttnpb.RelayConfiguration{ + Mode: &ttnpb.RelayConfiguration_Serving_{ + Serving: &ttnpb.RelayConfiguration_Serving{ + SecondChannel: mode.Serving.SecondChannel, + DefaultChannelIndex: mode.Serving.DefaultChannelIndex, + CadPeriodicity: mode.Serving.CadPeriodicity, + Limits: mode.Serving.Limits, + }, + }, + } + case nil: + return &ttnpb.RelayConfiguration{} + default: + panic(fmt.Sprintf("unknown mode %T", mode)) + } +} + +type nsRelayConfigurationService struct { + ttnpb.UnimplementedNsRelayConfigurationServiceServer + + devices DeviceRegistry + frequencyPlans func(context.Context) (*frequencyplans.Store, error) +} + +// CreateRelay implements ttnpb.NsRelayConfigurationServiceServer. +func (s *nsRelayConfigurationService) CreateRelay( + ctx context.Context, req *ttnpb.CreateRelayRequest, +) (*ttnpb.CreateRelayResponse, error) { + if err := rights.RequireApplication( + ctx, req.EndDeviceIds.ApplicationIds, ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + ); err != nil { + return nil, err + } + fps, err := s.frequencyPlans(ctx) + if err != nil { + return nil, err + } + if _, ctx, err := s.devices.SetByID( + ctx, + req.EndDeviceIds.ApplicationIds, + req.EndDeviceIds.DeviceId, + []string{ + "frequency_plan_id", + "lorawan_phy_version", + "mac_settings.desired_relay", + "mac_state.desired_parameters", + "pending_mac_state.desired_parameters", + }, + func(ctx context.Context, dev *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error) { + if dev == nil { + return nil, nil, errDeviceNotFound.New() + } + if dev.MacSettings.GetDesiredRelay() != nil { + return nil, nil, errRelayAlreadyExists.New() + } + phy, err := DeviceBand(dev, fps) + if err != nil { + return nil, nil, err + } + if err := validateRelayConfiguration(req.Configuration, phy, "configuration"); err != nil { + return nil, nil, err + } + parameters := relayParametersFromConfiguration(req.Configuration) + dev.MacSettings = &ttnpb.MACSettings{DesiredRelay: parameters} + paths := []string{"mac_settings.desired_relay"} + for path, desiredParameters := range map[string]*ttnpb.MACParameters{ + "mac_state.desired_parameters.relay": dev.MacState.GetDesiredParameters(), + "pending_mac_state.desired_parameters.relay": dev.PendingMacState.GetDesiredParameters(), + } { + if desiredParameters == nil { + continue + } + desiredParameters.Relay = parameters + paths = ttnpb.AddFields(paths, path) + } + return dev, paths, nil + }, + ); err != nil { + logRegistryRPCError(ctx, err, "Failed to create relay") + return nil, err + } + return &ttnpb.CreateRelayResponse{ + Configuration: req.Configuration, + }, nil +} + +// GetRelay implements ttnpb.NsRelayConfigurationServiceServer. +func (s *nsRelayConfigurationService) GetRelay( + ctx context.Context, req *ttnpb.GetRelayRequest, +) (*ttnpb.GetRelayResponse, error) { + if err := rights.RequireApplication( + ctx, req.EndDeviceIds.ApplicationIds, ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ, + ); err != nil { + return nil, err + } + dev, ctx, err := s.devices.GetByID( + ctx, + req.EndDeviceIds.ApplicationIds, + req.EndDeviceIds.DeviceId, + ttnpb.FieldsWithPrefix("mac_settings.desired_relay", req.FieldMask.GetPaths()...), + ) + if err != nil { + logRegistryRPCError(ctx, err, "Failed to get relay") + return nil, err + } + if dev.MacSettings.GetDesiredRelay() == nil { + return nil, errRelayNotFound.New() + } + return &ttnpb.GetRelayResponse{ + Configuration: relayConfigurationFromParameters(dev.MacSettings.DesiredRelay), + }, nil +} diff --git a/pkg/networkserver/networkserver.go b/pkg/networkserver/networkserver.go index 306df5244bd..0df37cf2ff4 100644 --- a/pkg/networkserver/networkserver.go +++ b/pkg/networkserver/networkserver.go @@ -157,8 +157,10 @@ type NetworkServer struct { *component.Component ctx context.Context - devices DeviceRegistry - batchDevices *nsEndDeviceBatchRegistry + devices DeviceRegistry + + batchDevices ttnpb.NsEndDeviceBatchRegistryServer + relayConfiguration ttnpb.NsRelayConfigurationServiceServer netID netIDFunc nsID nsIDFunc @@ -284,6 +286,8 @@ func New(c *component.Component, conf *Config, opts ...Option) (*NetworkServer, deduplicationWindow: makeWindowDurationFunc(conf.DeduplicationWindow), collectionWindow: makeWindowDurationFunc(conf.DeduplicationWindow + conf.CooldownWindow), devices: wrapEndDeviceRegistryWithReplacedFields(conf.Devices, replacedEndDeviceFields...), + batchDevices: &nsEndDeviceBatchRegistry{devices: conf.Devices}, + relayConfiguration: &nsRelayConfigurationService{devices: conf.Devices, frequencyPlans: c.FrequencyPlansStore}, downlinkTasks: conf.DownlinkTaskQueue.Queue, downlinkPriorities: downlinkPriorities, defaultMACSettings: defaultMACSettings, @@ -301,9 +305,6 @@ func New(c *component.Component, conf *Config, opts ...Option) (*NetworkServer, QueueSize: int(conf.ApplicationUplinkQueue.FastBufferSize), MaxWorkers: int(conf.ApplicationUplinkQueue.FastNumConsumers), }) - ns.batchDevices = &nsEndDeviceBatchRegistry{ - NS: ns, - } ctx = ns.Context() if len(opts) == 0 { @@ -326,6 +327,7 @@ func New(c *component.Component, conf *Config, opts ...Option) (*NetworkServer, "/ttn.lorawan.v3.NsEndDeviceRegistry", "/ttn.lorawan.v3.NsEndDeviceBatchRegistry", "/ttn.lorawan.v3.Ns", + "/ttn.lorawan.v3.RelayConfigurationService", } { c.GRPC.RegisterUnaryHook(filter, hook.name, hook.middleware) } @@ -395,6 +397,7 @@ func (ns *NetworkServer) RegisterServices(s *grpc.Server) { ttnpb.RegisterNsEndDeviceRegistryServer(s, ns) ttnpb.RegisterNsEndDeviceBatchRegistryServer(s, ns.batchDevices) ttnpb.RegisterNsServer(s, ns) + ttnpb.RegisterNsRelayConfigurationServiceServer(s, ns.relayConfiguration) } // RegisterHandlers registers gRPC handlers. @@ -402,6 +405,7 @@ func (ns *NetworkServer) RegisterHandlers(s *runtime.ServeMux, conn *grpc.Client ttnpb.RegisterNsEndDeviceRegistryHandler(ns.Context(), s, conn) ttnpb.RegisterNsEndDeviceBatchRegistryHandler(ns.Context(), s, conn) // nolint:errcheck ttnpb.RegisterNsHandler(ns.Context(), s, conn) + ttnpb.RegisterNsRelayConfigurationServiceHandler(ns.Context(), s, conn) // nolint:errcheck } // Roles returns the roles that the Network Server fulfills. diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index 2f9f10b27d4..1a230f852d5 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -2367,8 +2367,10 @@ "error:pkg/networkserver:payload": "無効なペイロード", "error:pkg/networkserver:raw_payload_too_short": "RawPayload長は4より大きい値でなければいけません", "error:pkg/networkserver:rejoin_request": "再ジョイン要求制御が未実装", + "error:pkg/networkserver:relay_already_exists": "", "error:pkg/networkserver:relay_full_f_cnt": "", "error:pkg/networkserver:relay_m_type": "", + "error:pkg/networkserver:relay_not_found": "", "error:pkg/networkserver:relay_recent_uplinks": "", "error:pkg/networkserver:relay_rx_windows_available": "", "error:pkg/networkserver:relay_schedule": "",