Skip to content
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

Support NbTrans controls while using Dynamic ADR mode #7012

Merged
merged 16 commits into from Apr 10, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -11,6 +11,8 @@ For details about compatibility between different releases, see the **Commitment

### Added

- Support fine-grained NbTrans controls while using Dynamic ADR mode in the Console.

### Changed

### Deprecated
Expand Down
203 changes: 191 additions & 12 deletions pkg/webui/console/components/mac-settings-section/index.js
Expand Up @@ -14,6 +14,8 @@

import React from 'react'
import { defineMessages } from 'react-intl'
import { createSelector } from 'reselect'
import { useSelector } from 'react-redux'

import Form, { useFormContext } from '@ttn-lw/components/form'
import Select from '@ttn-lw/components/select'
Expand All @@ -22,6 +24,8 @@ import Input from '@ttn-lw/components/input'
import KeyValueMap from '@ttn-lw/components/key-value-map'
import Radio from '@ttn-lw/components/radio-button'
import UnitInput from '@ttn-lw/components/unit-input'
import Button from '@ttn-lw/components/button'
import Icon from '@ttn-lw/components/icon'

import Message from '@ttn-lw/lib/components/message'

Expand All @@ -37,6 +41,8 @@ import {
parseLorawanMacVersion,
} from '@console/lib/device-utils'

import { selectDataRates } from '@console/store/selectors/configuration'

const m = defineMessages({
delayValue: '{count, plural, one {{count} second} other {{count} seconds}}',
factoryPresetFreqDescription: 'List of factory-preset frequencies. Note: order is respected.',
Expand Down Expand Up @@ -67,6 +73,18 @@ const m = defineMessages({
adrAckValue: '{count, plural, one {every message} other {every {count} messages}}',
statusCountPeriodicity: 'Status count periodicity',
statusTimePeriodicity: 'Status time periodicity',
dataRate: 'Data Rate {n}',
dataRatePlaceholder: 'Data Rate',
minNbTrans: 'Min. NbTrans',
maxNbTrans: 'Max. NbTrans',
useDefaultNbTrans: 'Use default settings for number of retransmissions',
adrNbTrans: 'ADR number of retransmissions (NbTrans)',
overrideNbTrans: 'Override server defaults for NbTrans (all data rates)',
defaultForAllRates: '(Default for all data rates)',
defaultNbTransMessage:
'Overriding the default is not required for using data rate overrides (below)',
specificOverrides: 'Data rate specific overrides',
addSpecificOverride: 'Add data rate specific override',
})

// 0...7
Expand Down Expand Up @@ -107,6 +125,21 @@ const maxDutyCycleOptions = [
const encodeAdrMode = value => ({ [value]: {} })
const decodeAdrMode = value => (value !== undefined ? Object.keys(value)[0] : null)

const getDataRate = data_rate => {
const { lora, fsk, lrfhss } = data_rate
// The encoding below mimics the encoding of the `modu` field of the UDP packet forwarder.
if (lora) {
const { bandwidth, spreading_factor } = lora
return `SF${spreading_factor}BW${bandwidth / 1000}`
} else if (fsk) {
const { bit_rate } = fsk
return `${bit_rate}`
} else if (lrfhss) {
const { modulation_type, operating_channel_width } = lrfhss
return `M${modulation_type ?? 0}CW${operating_channel_width / 1000}`
}
}

ryaplots marked this conversation as resolved.
Show resolved Hide resolved
const MacSettingsSection = props => {
const {
activationMode,
Expand All @@ -115,9 +148,25 @@ const MacSettingsSection = props => {
lorawanVersion,
isClassB,
isClassC,
bandId,
} = props

const { values } = useFormContext()
const { values, setFieldValue, setFieldTouched } = useFormContext()
const dataRateOverrideOptions = useSelector(
ryaplots marked this conversation as resolved.
Show resolved Hide resolved
createSelector(
state => selectDataRates(state, bandId, values.lorawan_phy_version),
dataRates =>
Object.keys(dataRates).reduce(
(result, key) =>
result.concat({
label: getDataRate(dataRates[key].rate),
value: key,
}),
[],
),
),
)

const { mac_settings } = values
const isNewLorawanVersion = parseLorawanMacVersion(lorawanVersion) >= 110
const isABP = activationMode === ACTIVATION_MODES.ABP
Expand Down Expand Up @@ -151,6 +200,39 @@ const MacSettingsSection = props => {
}
}, [handleIsCollapsedChange, isABP, isClassB, isCollapsed, isMulticast, pingPeriodicityRequired])

const adrOverrides = mac_settings.adr.dynamic.overrides
const showEditNbTrans = !values.mac_settings?.adr.dynamic._use_default_nb_trans
const defaultNbTransDisabled = !values.mac_settings?.adr.dynamic._override_nb_trans_defaults
const addOverride = React.useCallback(() => {
setFieldValue(
'mac_settings.adr.dynamic.overrides',
adrOverrides
? [...adrOverrides, { data_rate: '', min_nb_trans: '', max_nb_trans: '' }]
: [{ data_rate: '', min_nb_trans: '', max_nb_trans: '' }],
)
setFieldTouched('mac_settings.adr.dynamic._overrides', true)
}, [setFieldValue, adrOverrides, setFieldTouched])
const handleRemoveButtonClick = React.useCallback(
(_, value) => {
setFieldValue(
'mac_settings.adr.dynamic.overrides',
adrOverrides.filter(override => override !== value),
)
},
[adrOverrides, setFieldValue],
)
const handleOverrideChange = (fieldName, index) => value => {
setFieldValue(
`mac_settings.adr.dynamic.overrides`,
adrOverrides.map((override, i) => {
if (index === i) {
return { ...override, [fieldName]: value }
}
return override
}),
)
}

return (
<Form.CollapseSection
id="mac-settings"
Expand Down Expand Up @@ -479,17 +561,113 @@ const MacSettingsSection = props => {
<Radio label={sharedMessages.disabled} value="disabled" />
</Form.Field>
{isDynamicAdr && (
<Form.Field
title={m.adrMargin}
name="mac_settings.adr.dynamic.margin"
component={Input}
type="number"
tooltipId={tooltipIds.ADR_MARGIN}
min={-100}
max={100}
inputWidth="xs"
append="dB"
/>
<>
<Form.Field
title={m.adrMargin}
name="mac_settings.adr.dynamic.margin"
component={Input}
type="number"
tooltipId={tooltipIds.ADR_MARGIN}
min={-100}
max={100}
inputWidth="xs"
append="dB"
/>
<Form.Field
label={m.useDefaultNbTrans}
name="mac_settings.adr.dynamic._use_default_nb_trans"
component={Checkbox}
/>
{showEditNbTrans && (
<>
<Form.Field
title={m.adrNbTrans}
name="mac_settings.adr.dynamic._override_nb_trans_defaults"
component={Checkbox}
label={m.overrideNbTrans}
tooltipId={tooltipIds.RESET_MAC}
ryaplots marked this conversation as resolved.
Show resolved Hide resolved
/>
<Form.FieldContainer horizontal className="al-end mb-cs-xs">
<Form.Field
title={m.minNbTrans}
name="mac_settings.adr.dynamic.min_nb_trans"
component={Input}
disabled={defaultNbTransDisabled}
inputWidth="xs"
className="d-flex direction-column"
/>
<Form.Field
title={m.maxNbTrans}
name="mac_settings.adr.dynamic.max_nb_trans"
component={Input}
disabled={defaultNbTransDisabled}
inputWidth="xs"
className="d-flex direction-column"
/>
<Message content={m.defaultForAllRates} className="mt-cs-xl" />
</Form.FieldContainer>
{!defaultNbTransDisabled && (
<div>
<Icon icon="info" nudgeUp className="mr-cs-xxs" />
<Message content={m.defaultNbTransMessage} />
</div>
)}
<Form.InfoField
title={m.specificOverrides}
tooltipId={tooltipIds.RESET_MAC}
ryaplots marked this conversation as resolved.
Show resolved Hide resolved
className="mt-cs-m"
>
{adrOverrides &&
adrOverrides.map((override, index) => (
<Form.FieldContainer horizontal className="al-end" key={index}>
<Form.Field
title={m.dataRatePlaceholder}
name={`mac_settings.adr.dynamic.overrides.data_rate`}
component={Select}
options={dataRateOverrideOptions}
inputWidth="s"
fieldWidth="xxs"
className="d-flex direction-column"
onChange={handleOverrideChange('data_rate', index)}
value={override.data_rate}
/>
<Form.Field
title={m.minNbTrans}
name={`mac_settings.adr.dynamic.overrides.min_nb_trans`}
component={Input}
ryaplots marked this conversation as resolved.
Show resolved Hide resolved
inputWidth="xs"
className="d-flex direction-column"
onChange={handleOverrideChange('min_nb_trans', index)}
value={override.min_nb_trans}
/>
<Form.Field
title={m.maxNbTrans}
name={`mac_settings.adr.dynamic.overrides.max_nb_trans`}
component={Input}
ryaplots marked this conversation as resolved.
Show resolved Hide resolved
inputWidth="xs"
className="d-flex direction-column"
onChange={handleOverrideChange('max_nb_trans', index)}
value={override.max_nb_trans}
/>
<Button
type="button"
onClick={handleRemoveButtonClick}
icon="delete"
message={sharedMessages.remove}
value={override}
/>
</Form.FieldContainer>
))}
<Button
type="button"
message={m.addSpecificOverride}
onClick={addOverride}
icon="add"
/>
</Form.InfoField>
</>
)}
</>
)}
{isStaticAdr && (
<>
Expand Down Expand Up @@ -542,6 +720,7 @@ const MacSettingsSection = props => {

MacSettingsSection.propTypes = {
activationMode: PropTypes.oneOf(Object.values(ACTIVATION_MODES)).isRequired,
bandId: PropTypes.string.isRequired,
initiallyCollapsed: PropTypes.bool,
isClassB: PropTypes.bool,
isClassC: PropTypes.bool,
Expand Down
9 changes: 9 additions & 0 deletions pkg/webui/console/store/actions/configuration.js
Expand Up @@ -41,3 +41,12 @@ export const [
failure: getGsFrequencyPlansFailure,
},
] = createRequestActions(GET_GS_FREQUENCY_PLANS_BASE)

export const GET_BANDS_LIST_BASE = 'GET_BANDS_LIST'
export const [
{ request: GET_BANDS_LIST, success: GET_BANDS_LIST_SUCCESS, failure: GET_BANDS_LIST_FAILURE },
{ request: getBandsList, success: getBandsListSuccess, failure: getBandsListFailure },
] = createRequestActions(GET_BANDS_LIST_BASE, (bandId, phyVersion) => ({
bandId,
phyVersion,
}))
12 changes: 11 additions & 1 deletion pkg/webui/console/store/middleware/logics/configuration.js
Expand Up @@ -57,4 +57,14 @@ const getGsFrequencyPlansLogic = createRequestLogic({
},
})

export default [getNsFrequencyPlansLogic, getGsFrequencyPlansLogic]
const getBandsListLogic = createRequestLogic({
type: configuration.GET_BANDS_LIST,
process: async ({ action }) => {
const { bandId, phyVersion } = action.payload
const bands = (await tts.Configuration.listBands(bandId, phyVersion)).descriptions

return bands
},
})

export default [getNsFrequencyPlansLogic, getGsFrequencyPlansLogic, getBandsListLogic]
7 changes: 7 additions & 0 deletions pkg/webui/console/store/reducers/configuration.js
Expand Up @@ -15,11 +15,13 @@
import {
GET_NS_FREQUENCY_PLANS_SUCCESS,
GET_GS_FREQUENCY_PLANS_SUCCESS,
GET_BANDS_LIST_SUCCESS,
} from '@console/store/actions/configuration'

const defaultState = {
nsFrequencyPlans: undefined,
gsFrequencyPlans: undefined,
bandDefinitions: undefined,
}

const configuration = (state = defaultState, { type, payload }) => {
Expand All @@ -34,6 +36,11 @@ const configuration = (state = defaultState, { type, payload }) => {
...state,
gsFrequencyPlans: payload,
}
case GET_BANDS_LIST_SUCCESS:
return {
...state,
bandDefinitions: payload,
}
default:
return state
}
Expand Down
12 changes: 12 additions & 0 deletions pkg/webui/console/store/selectors/configuration.js
Expand Up @@ -43,3 +43,15 @@ export const selectFrequencyPlansFetching = createFetchingSelector([
GET_NS_FREQUENCY_PLANS_BASE,
GET_GS_FREQUENCY_PLANS_BASE,
])

export const selectBandDefinitions = state => {
const store = selectConfigurationStore(state)

return store?.bandDefinitions || []
}

export const selectDataRates = (state, bandId, phyVersion) => {
const bandDefinitions = selectBandDefinitions(state)

return bandDefinitions[bandId]?.band[phyVersion]?.data_rates || []
}
@@ -1,4 +1,4 @@
// Copyright © 2019 The Things Network Foundation, The Things Industries B.V.

Check warning on line 1 in pkg/webui/console/views/device-general-settings/identity-server-form/index.js

View workflow job for this annotation

GitHub Actions / Check Mergeability

pkg/webui/console/views/device-general-settings/identity-server-form/index.js has a conflict when merging TheThingsIndustries/lorawan-stack:v3.30.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -319,7 +319,11 @@
onSubmitSuccess: PropTypes.func.isRequired,
onUnclaim: PropTypes.func.isRequired,
onUnclaimFailure: PropTypes.func.isRequired,
supportsClaiming: PropTypes.bool.isRequired,
supportsClaiming: PropTypes.bool,
}

IdentityServerForm.defaultProps = {
supportsClaiming: undefined,
}

export default IdentityServerForm