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 15 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
5 changes: 4 additions & 1 deletion pkg/webui/components/form/field/index.js
Expand Up @@ -191,7 +191,10 @@ const FormField = props => {
const disabled = inputDisabled || formDisabled
const hasTooltip = Boolean(tooltipId)
const hasTitle = Boolean(title)
const showError = touched && !isEmpty(errors)
const showError =
touched &&
!isEmpty(errors) &&
Boolean(errors[0].message?.id || errors[0].id || typeof errors[0] === 'string')
const showWarning = !showError && Boolean(warning)
const error = showError && errors[0]
const showDescription = !showError && !showWarning && Boolean(description)
Expand Down
232 changes: 219 additions & 13 deletions pkg/webui/console/components/mac-settings-section/index.js
Expand Up @@ -12,8 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import React from 'react'
import React, { useCallback } from 'react'
import { defineMessages } from 'react-intl'
import { createSelector } from 'reselect'
import { useSelector } from 'react-redux'
import { get, set } from 'lodash'

import Form, { useFormContext } from '@ttn-lw/components/form'
import Select from '@ttn-lw/components/select'
Expand All @@ -22,6 +25,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 +42,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 +74,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 +126,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,10 +149,32 @@ const MacSettingsSection = props => {
lorawanVersion,
isClassB,
isClassC,
bandId,
} = props

const { values } = useFormContext()
const { values, setFieldValue, setFieldTouched } = useFormContext()
const { mac_settings } = values
const alreadySelectedDataRates = Object.keys(mac_settings?.adr?.dynamic?.overrides || [])
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: `data_rate_${key}`,
}),
[],
),
),
)
// Filter out the already selected data rate indices.
const dataRateFilterOption = useCallback(
option => !alreadySelectedDataRates.includes(option.value),
[alreadySelectedDataRates],
)

const isNewLorawanVersion = parseLorawanMacVersion(lorawanVersion) >= 110
const isABP = activationMode === ACTIVATION_MODES.ABP
const isMulticast = activationMode === ACTIVATION_MODES.MULTICAST
Expand Down Expand Up @@ -151,6 +207,51 @@ 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(() => {
const newOverride = { _data_rate_index: '', min_nb_trans: '', max_nb_trans: '' }
setFieldValue(
'mac_settings.adr.dynamic.overrides',
adrOverrides
? { ...adrOverrides, [`_empty-${Date.now()}`]: newOverride }
: { [`_empty-${Date.now()}`]: newOverride },
)
setFieldTouched('mac_settings.adr.dynamic._overrides', true)
}, [setFieldValue, adrOverrides, setFieldTouched])
const handleRemoveButtonClick = useCallback(
(_, index) => {
setFieldValue(
'mac_settings.adr.dynamic.overrides',
Object.keys(adrOverrides)
.filter(key => key !== index)
.reduce((acc, key) => ({ ...acc, [key]: adrOverrides[key] }), {}),
)
},
[adrOverrides, setFieldValue],
)

// Define a value setter for the data rate index field which
// handles setting the object keys correctly, since the index
// is set as the object key in the API schema.
// A similar result could be done without pseudo values, purely
// with decoder/encoder, but it would make error mapping
// more complex.
const dataRateValueSetter = useCallback(
({ setValues }, { name, value }) => {
const index = name.split('.').slice(-2)[0] // Would be: data_rate_{x}.
const oldOverride = get(values, `mac_settings.adr.dynamic.overrides.${index}`, {})
const overrides = { ...get(values, 'mac_settings.adr.dynamic.overrides', {}) }
// Empty data rate index objects, are stored with a pseudo key. Remove it.
delete overrides[index]
// Move the existing values to the new data rate key.
overrides[value] = { ...oldOverride, _data_rate_index: value }
setValues(values => set(values, 'mac_settings.adr.dynamic.overrides', overrides))
},
[values],
)

return (
<Form.CollapseSection
id="mac-settings"
Expand Down Expand Up @@ -479,17 +580,121 @@ 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}
tooltipId={tooltipIds.USE_DEFAULT_NB_TRANS}
/>
{showEditNbTrans && (
<>
<Form.Field
title={m.adrNbTrans}
name="mac_settings.adr.dynamic._override_nb_trans_defaults"
component={Checkbox}
label={m.overrideNbTrans}
/>
<Form.FieldContainer horizontal className="al-end mb-cs-xs">
<Form.Field
title={m.minNbTrans}
name="mac_settings.adr.dynamic.min_nb_trans"
component={Input}
type="number"
min={1}
max={3}
disabled={defaultNbTransDisabled}
inputWidth="xs"
className="d-flex direction-column"
/>
<Form.Field
title={m.maxNbTrans}
name="mac_settings.adr.dynamic.max_nb_trans"
component={Input}
type="number"
min={1}
max={3}
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.DATA_RATE_SPECIFIC_OVERRIDES}
className="mt-cs-m"
>
{adrOverrides &&
Object.keys(adrOverrides).map(index => (
<Form.FieldContainer horizontal className="al-end" key={index}>
<Form.Field
title={m.dataRatePlaceholder}
name={`mac_settings.adr.dynamic.overrides.${index}._data_rate_index`}
valueSetter={dataRateValueSetter}
component={Select}
options={dataRateOverrideOptions}
filterOption={dataRateFilterOption}
inputWidth="s"
fieldWidth="xxs"
className="d-flex direction-column"
/>
<Form.Field
title={m.minNbTrans}
name={`mac_settings.adr.dynamic.overrides.${index}.min_nb_trans`}
component={Input}
ryaplots marked this conversation as resolved.
Show resolved Hide resolved
fieldWidth="xxs"
className="d-flex direction-column"
type="number"
min={1}
max={3}
/>
<Form.Field
title={m.maxNbTrans}
name={`mac_settings.adr.dynamic.overrides.${index}.max_nb_trans`}
component={Input}
ryaplots marked this conversation as resolved.
Show resolved Hide resolved
fieldWidth="xxs"
className="d-flex direction-column"
type="number"
min={1}
max={3}
/>
<Button
type="button"
onClick={handleRemoveButtonClick}
icon="delete"
message={sharedMessages.remove}
value={index}
/>
</Form.FieldContainer>
))}
<Button
type="button"
message={m.addSpecificOverride}
onClick={addOverride}
icon="add"
/>
</Form.InfoField>
</>
)}
</>
)}
{isStaticAdr && (
<>
Expand Down Expand Up @@ -542,6 +747,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 || []
}