Skip to content

Commit

Permalink
Merge pull request #7012 from TheThingsNetwork/feature/adr-nbtrans-co…
Browse files Browse the repository at this point in the history
…ntrols

Support NbTrans controls while using Dynamic ADR mode
  • Loading branch information
ryaplots authored Apr 10, 2024
2 parents 9ab17b0 + 0ff8203 commit 47ecec5
Show file tree
Hide file tree
Showing 18 changed files with 567 additions and 89 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@

import React from 'react'

import { getDataRate, getSignalInformation } from '@console/components/events/utils'
import { getSignalInformation } from '@console/components/events/utils'

import PropTypes from '@ttn-lw/lib/prop-types'
import sharedMessages from '@ttn-lw/lib/shared-messages'

import getDataRate from '@console/lib/data-rate-utils'

import messages from '../messages'

import DescriptionList from './shared/description-list'
Expand Down
218 changes: 205 additions & 13 deletions pkg/webui/console/components/mac-settings-section/index.js
Original file line number Diff line number Diff line change
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 @@ -36,6 +41,9 @@ import {
fCntWidthDecode,
parseLorawanMacVersion,
} from '@console/lib/device-utils'
import getDataRate from '@console/lib/data-rate-utils'

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

const m = defineMessages({
delayValue: '{count, plural, one {{count} second} other {{count} seconds}}',
Expand Down Expand Up @@ -67,6 +75,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 @@ -115,10 +135,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(
createSelector(
state => selectDataRates(state, bandId, values.lorawan_phy_version),
dataRates =>
Object.keys(dataRates).reduce(
(result, key) =>
result.concat({
label: getDataRate({ settings: { data_rate: 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 +193,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 +566,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}
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}
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 +733,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
40 changes: 40 additions & 0 deletions pkg/webui/console/lib/data-rate-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright © 2024 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.

export default (data, selector = 'settings') => {
if (!data) {
return undefined
}
const { [selector]: container } = data
if (!container) {
return undefined
}
const { data_rate } = container
if (!data_rate) {
return undefined
}
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}`
}
return undefined
}
9 changes: 9 additions & 0 deletions pkg/webui/console/store/actions/configuration.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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]
Loading

0 comments on commit 47ecec5

Please sign in to comment.