From be6783c872839008716a16333c301df4f815b365 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Wed, 27 Mar 2024 11:02:42 +0100 Subject: [PATCH 01/15] console: Add store logic for band definitions --- pkg/webui/console/store/actions/configuration.js | 9 +++++++++ .../console/store/middleware/logics/configuration.js | 12 +++++++++++- pkg/webui/console/store/reducers/configuration.js | 7 +++++++ pkg/webui/console/store/selectors/configuration.js | 12 ++++++++++++ sdk/js/src/service/configuration.js | 11 +++++++++++ 5 files changed, 50 insertions(+), 1 deletion(-) diff --git a/pkg/webui/console/store/actions/configuration.js b/pkg/webui/console/store/actions/configuration.js index 0d6d979af4..794e70ab22 100644 --- a/pkg/webui/console/store/actions/configuration.js +++ b/pkg/webui/console/store/actions/configuration.js @@ -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, +})) diff --git a/pkg/webui/console/store/middleware/logics/configuration.js b/pkg/webui/console/store/middleware/logics/configuration.js index f8484cd7c3..694ae3d038 100644 --- a/pkg/webui/console/store/middleware/logics/configuration.js +++ b/pkg/webui/console/store/middleware/logics/configuration.js @@ -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] diff --git a/pkg/webui/console/store/reducers/configuration.js b/pkg/webui/console/store/reducers/configuration.js index 7715498a72..3510dfb516 100644 --- a/pkg/webui/console/store/reducers/configuration.js +++ b/pkg/webui/console/store/reducers/configuration.js @@ -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 }) => { @@ -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 } diff --git a/pkg/webui/console/store/selectors/configuration.js b/pkg/webui/console/store/selectors/configuration.js index 0c7407be8c..791ed5f261 100644 --- a/pkg/webui/console/store/selectors/configuration.js +++ b/pkg/webui/console/store/selectors/configuration.js @@ -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 || [] +} diff --git a/sdk/js/src/service/configuration.js b/sdk/js/src/service/configuration.js index ee43bf354c..b884e8f4cd 100644 --- a/sdk/js/src/service/configuration.js +++ b/sdk/js/src/service/configuration.js @@ -38,6 +38,17 @@ class Configuration { const result = await this._api.GetPhyVersions() return Marshaler.payloadSingleResponse(result) } + + async listBands(bandId, phyVersion) { + const result = await this._api.ListBands({ + routeParams: { + band_id: bandId, + phy_version: phyVersion, + }, + }) + + return Marshaler.payloadSingleResponse(result) + } } export default Configuration From 495341758de5d016536ff121b7779200b72478a9 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Wed, 27 Mar 2024 11:25:21 +0100 Subject: [PATCH 02/15] console: Encode and decode nbTrans overrides --- .../components/mac-settings-section/index.js | 196 ++++++++++++++++-- .../network-server-form/index.js | 77 ++++++- .../network-server-form/validation-schema.js | 3 + 3 files changed, 260 insertions(+), 16 deletions(-) diff --git a/pkg/webui/console/components/mac-settings-section/index.js b/pkg/webui/console/components/mac-settings-section/index.js index 0c0260e4d4..6055baa95c 100644 --- a/pkg/webui/console/components/mac-settings-section/index.js +++ b/pkg/webui/console/components/mac-settings-section/index.js @@ -14,6 +14,7 @@ import React from 'react' import { defineMessages } from 'react-intl' +import { useSelector } from 'react-redux' import Form, { useFormContext } from '@ttn-lw/components/form' import Select from '@ttn-lw/components/select' @@ -22,6 +23,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' @@ -37,6 +40,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.', @@ -67,6 +72,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 @@ -107,6 +124,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}` + } +} + const MacSettingsSection = props => { const { activationMode, @@ -115,9 +147,19 @@ const MacSettingsSection = props => { lorawanVersion, isClassB, isClassC, + bandId, } = props - const { values } = useFormContext() + const { values, setFieldValue, setFieldTouched } = useFormContext() + const dataRates = useSelector(state => selectDataRates(state, bandId, values.lorawan_phy_version)) + const dataRateOverrideOptions = 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 @@ -151,6 +193,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 ( { {isDynamicAdr && ( - + <> + + + {showEditNbTrans && ( + <> + + + + + + + {!defaultNbTransDisabled && ( +
+ + +
+ )} + + {adrOverrides && + adrOverrides.map((override, index) => ( + + + + +