Skip to content

Disassociate MAC settings profile #7544

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ For details about compatibility between different releases, see the **Commitment

### Added

- Support to associate/disassociate MAC settings profiles to end devices
- This feature is experimental and subject to change.

### Changed

- Support wildcards in the supported hosts for TLS certifictes obtained via ACME (`tls.acme.hosts`).
- Support wildcards in the supported hosts for TLS certificates obtained via ACME (`tls.acme.hosts`).
- Increase downlink capacity by raising duty-cycle budgets per priority.

### Deprecated
Expand Down
1 change: 1 addition & 0 deletions api/ttn/lorawan/v3/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4378,6 +4378,7 @@ This is used internally by the Network Server.
| ----- | ---- | ----- | ----------- |
| `ids` | [`MACSettingsProfileIdentifiers`](#ttn.lorawan.v3.MACSettingsProfileIdentifiers) | | Profile identifiers. |
| `mac_settings` | [`MACSettings`](#ttn.lorawan.v3.MACSettings) | | MAC settings. |
| `end_devices_ids` | [`EndDeviceIdentifiers`](#ttn.lorawan.v3.EndDeviceIdentifiers) | repeated | Associated end device identifiers. |

#### Field Rules

Expand Down
16 changes: 16 additions & 0 deletions api/ttn/lorawan/v3/api.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -26661,6 +26661,14 @@
"mac_settings": {
"$ref": "#/definitions/v3MACSettings",
"description": "MAC settings."
},
"end_devices_ids": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/v3EndDeviceIdentifiers"
},
"description": "Associated end device identifiers."
}
}
},
Expand Down Expand Up @@ -27978,6 +27986,14 @@
"mac_settings": {
"$ref": "#/definitions/v3MACSettings",
"description": "MAC settings."
},
"end_devices_ids": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/v3EndDeviceIdentifiers"
},
"description": "Associated end device identifiers."
}
},
"description": "The MAC settings profile to create.",
Expand Down
2 changes: 2 additions & 0 deletions api/ttn/lorawan/v3/end_device.proto
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,8 @@ message MACSettingsProfile {
];
// MAC settings.
MACSettings mac_settings = 2 [(validate.rules).message.required = true];
// Associated end device identifiers.
repeated EndDeviceIdentifiers end_devices_ids = 3;
}

// MACState represents the state of MAC layer of the device.
Expand Down
18 changes: 18 additions & 0 deletions config/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -8270,6 +8270,15 @@
"file": "device_state.go"
}
},
"error:pkg/networkserver:field_mask": {
"translations": {
"en": "invalid field mask"
},
"description": {
"package": "pkg/networkserver",
"file": "grpc_mac_settings_profile.go"
}
},
"error:pkg/networkserver:field_not_zero": {
"translations": {
"en": "field `{name}` is not zero"
Expand Down Expand Up @@ -8324,6 +8333,15 @@
"file": "grpc_mac_settings_profile.go"
}
},
"error:pkg/networkserver:mac_settings_profile_used": {
"translations": {
"en": "MAC settings profile is used"
},
"description": {
"package": "pkg/networkserver",
"file": "grpc_mac_settings_profile.go"
}
},
"error:pkg/networkserver:no_downlink": {
"translations": {
"en": "no downlink to send"
Expand Down
96 changes: 81 additions & 15 deletions pkg/networkserver/grpc_deviceregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package networkserver
import (
"bytes"
"context"
"slices"
"strings"

"go.thethings.network/lorawan-stack/v3/pkg/auth/rights"
Expand Down Expand Up @@ -410,29 +411,58 @@ func (ns *NetworkServer) Set(ctx context.Context, req *ttnpb.SetEndDeviceRequest
return nil, err
}

var profile *ttnpb.MACSettingsProfile
var (
profile *ttnpb.MACSettingsProfile
removeMacSettingsProfile bool
)

if st.HasSetField(
"mac_settings_profile_ids",
"mac_settings_profile_ids.application_ids",
"mac_settings_profile_ids.application_ids.application_id",
"mac_settings_profile_ids.profile_id",
) {
// If mac_settings_profile_ids is set, mac_settings must not be set.
if st.HasSetField(macSettingsFields...) {
return nil, newInvalidFieldValueError("mac_settings")
}
profile, err = ns.macSettingsProfiles.Get(ctx, st.Device.MacSettingsProfileIds, []string{"mac_settings"})
if err != nil {
return nil, err
}
if st.Device.MacSettingsProfileIds != nil {
// If mac_settings_profile_ids is set, mac_settings must not be set.
if st.HasSetField(macSettingsFields...) {
return nil, newInvalidFieldValueError("mac_settings")
}
profile, err = ns.macSettingsProfiles.Get(
ctx,
st.Device.MacSettingsProfileIds,
[]string{"ids", "mac_settings", "end_devices_ids"},
)
if err != nil {
return nil, err
}

if err = validateProfile(profile.GetMacSettings(), st, fps); err != nil {
return nil, err
}
if err = validateProfile(profile.GetMacSettings(), st, fps); err != nil {
return nil, err
}

if !slices.ContainsFunc(profile.EndDevicesIds, func(id *ttnpb.EndDeviceIdentifiers) bool {
return id.ApplicationIds.ApplicationId == st.Device.Ids.ApplicationIds.ApplicationId &&
id.DeviceId == st.Device.Ids.DeviceId
}) {
profile.EndDevicesIds = append(profile.EndDevicesIds, st.Device.Ids)
_, err := ns.macSettingsProfiles.Set(
ctx,
st.Device.MacSettingsProfileIds,
[]string{"end_devices_ids"},
func(context.Context, *ttnpb.MACSettingsProfile) (*ttnpb.MACSettingsProfile, []string, error) {
return profile, []string{"end_devices_ids"}, nil
})
if err != nil {
return nil, err
}
}

// If mac_settings_profile_ids is set, mac_settings must not be set.
st.Device.MacSettings = nil
st.AddSetFields(macSettingsFields...)
// If mac_settings_profile_ids is set, mac_settings must not be set.
st.Device.MacSettings = nil
st.AddSetFields(macSettingsFields...)
} else {
removeMacSettingsProfile = true
}
}

if err := validateADR(st); err != nil {
Expand Down Expand Up @@ -1434,6 +1464,42 @@ func (ns *NetworkServer) Set(ctx context.Context, req *ttnpb.SetEndDeviceRequest
)
}
}
if removeMacSettingsProfile {
if stored.MacSettingsProfileIds != nil {
profile, err = ns.macSettingsProfiles.Get(
ctx,
stored.MacSettingsProfileIds,
[]string{"ids", "mac_settings", "end_devices_ids"},
)
if err != nil {
return err
}
idx := slices.IndexFunc(profile.EndDevicesIds, func(id *ttnpb.EndDeviceIdentifiers) bool {
return id.ApplicationIds.ApplicationId == st.Device.Ids.ApplicationIds.ApplicationId &&
id.DeviceId == st.Device.Ids.DeviceId
})
if idx >= 0 {
if idx == len(profile.EndDevicesIds)-1 {
profile.EndDevicesIds = profile.EndDevicesIds[:idx]
} else {
profile.EndDevicesIds = append(profile.EndDevicesIds[:idx], profile.EndDevicesIds[idx+1:]...)
}
_, err := ns.macSettingsProfiles.Set(
ctx,
stored.MacSettingsProfileIds,
[]string{"end_devices_ids"},
func(context.Context, *ttnpb.MACSettingsProfile) (*ttnpb.MACSettingsProfile, []string, error) {
return profile, []string{"end_devices_ids"}, nil
})
if err != nil {
return err
}
}

st.Device.MacSettings = profile.MacSettings
st.AddSetFields(macSettingsFields...)
}
}

if stored == nil {
evt = evtCreateEndDevice.NewWithIdentifiersAndData(ctx, st.Device.Ids, nil)
Expand Down
45 changes: 45 additions & 0 deletions pkg/networkserver/grpc_deviceregistry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ func TestDeviceRegistrySet(t *testing.T) {
}

macSettingsProfileOpt := EndDeviceOptions.WithMacSettingsProfileIds(macSettingsProfileID)
emptyMacSettingsProfileOpt := EndDeviceOptions.WithMacSettingsProfileIds(nil)

for createDevice, tcs := range map[*ttnpb.EndDevice][]struct {
SetDevice SetDeviceRequest
Expand Down Expand Up @@ -811,6 +812,50 @@ func TestDeviceRegistrySet(t *testing.T) {
StoredDevice: multicastClassBMACSettingsOpt(MakeMulticastEndDevice(ttnpb.Class_CLASS_B, defaultMACSettings, true, activeSessionOptsWithStartedAt, nil)),
},
},
// Update with MAC settings profile
MakeOTAAEndDevice(): {
{
SetDevice: *makeUpdateDeviceRequest([]test.EndDeviceOption{
EndDeviceOptions.WithLorawanVersion(ttnpb.MACVersion_MAC_V1_0_3),
EndDeviceOptions.WithLorawanPhyVersion(ttnpb.PHYVersion_RP001_V1_0_3_REV_A),
EndDeviceOptions.WithDefaultFrequencyPlanID(),
macSettingsProfileOpt,
},
"frequency_plan_id",
"lorawan_version",
"lorawan_phy_version",
"mac_settings_profile_ids",
),

ReturnedDevice: MakeOTAAEndDevice(
EndDeviceOptions.WithLorawanVersion(ttnpb.MACVersion_MAC_V1_0_3),
EndDeviceOptions.WithLorawanPhyVersion(ttnpb.PHYVersion_RP001_V1_0_3_REV_A),
EndDeviceOptions.WithDefaultFrequencyPlanID(),
macSettingsProfileOpt,
),
StoredDevice: MakeOTAAEndDevice(
EndDeviceOptions.WithLorawanVersion(ttnpb.MACVersion_MAC_V1_0_3),
EndDeviceOptions.WithLorawanPhyVersion(ttnpb.PHYVersion_RP001_V1_0_3_REV_A),
EndDeviceOptions.WithDefaultFrequencyPlanID(),
macSettingsProfileOpt,
),
},
},
// Update with empty MAC settings profile
MakeOTAAEndDevice(macSettingsProfileOpt): {
{
SetDevice: *makeUpdateDeviceRequest([]test.EndDeviceOption{
emptyMacSettingsProfileOpt,
},
"mac_settings_profile_ids",
),

ReturnedDevice: MakeOTAAEndDevice(),
StoredDevice: MakeOTAAEndDevice(
customMACSettingsOpt,
),
},
},
} {
for _, tc := range tcs {
createDevice := createDevice
Expand Down
14 changes: 11 additions & 3 deletions pkg/networkserver/grpc_mac_settings_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import (
var (
errMACSettingsProfileAlreadyExists = errors.DefineAlreadyExists("mac_settings_profile_already_exists", "MAC settings profile already exists") // nolint: lll
errMACSettingsProfileNotFound = errors.DefineNotFound("mac_settings_profile_not_found", "MAC settings profile not found") // nolint: lll
errMACSettingsProfileUsed = errors.DefineFailedPrecondition("mac_settings_profile_used", "MAC settings profile is used") // nolint: lll
errInvalidFieldMask = errors.DefineInvalidArgument("field_mask", "invalid field mask")
)

func setTotalHeader(ctx context.Context, total int64) {
Expand Down Expand Up @@ -79,7 +81,7 @@ func (m *NsMACSettingsProfileRegistry) Get(ctx context.Context, req *ttnpb.GetMA
); err != nil {
return nil, err
}
paths := []string{"ids", "mac_settings"}
paths := []string{"ids", "mac_settings", "end_devices_ids"}
if req.FieldMask != nil {
paths = req.FieldMask.GetPaths()
}
Expand All @@ -106,6 +108,9 @@ func (m *NsMACSettingsProfileRegistry) Update(ctx context.Context, req *ttnpb.Up
if req.FieldMask != nil {
paths = req.FieldMask.GetPaths()
}
if ttnpb.HasAnyField(paths, "end_devices_ids") {
return nil, errInvalidFieldMask.WithAttributes("field_mask", "end_devices_ids")
}
profile, err := m.registry.Set(
ctx,
req.MacSettingsProfile.Ids,
Expand Down Expand Up @@ -134,7 +139,7 @@ func (m *NsMACSettingsProfileRegistry) Delete(ctx context.Context, req *ttnpb.De
); err != nil {
return nil, err
}
paths := []string{"ids", "mac_settings"}
paths := []string{"ids", "mac_settings", "end_devices_ids"}
_, err := m.registry.Set(
ctx,
req.MacSettingsProfileIds,
Expand All @@ -143,6 +148,9 @@ func (m *NsMACSettingsProfileRegistry) Delete(ctx context.Context, req *ttnpb.De
if profile == nil {
return nil, nil, errMACSettingsProfileNotFound.New()
}
if len(profile.EndDevicesIds) > 0 {
return nil, nil, errMACSettingsProfileUsed.New()
}
return nil, nil, nil
})
if err != nil {
Expand All @@ -161,7 +169,7 @@ func (m *NsMACSettingsProfileRegistry) List(ctx context.Context, req *ttnpb.List
); err != nil {
return nil, err
}
paths := []string{"ids", "mac_settings"}
paths := []string{"ids", "mac_settings", "end_devices_ids"}
if req.FieldMask != nil {
paths = req.FieldMask.GetPaths()
}
Expand Down
Loading
Loading