diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2bfbe5d709..5b2b80dd6b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/pkg/webui/components/form/field/index.js b/pkg/webui/components/form/field/index.js
index 41adb999ef..9e2776a02f 100644
--- a/pkg/webui/components/form/field/index.js
+++ b/pkg/webui/components/form/field/index.js
@@ -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)
diff --git a/pkg/webui/console/components/events/previews/application-uplink.js b/pkg/webui/console/components/events/previews/application-uplink.js
index dfbe519603..d5371c6e0d 100644
--- a/pkg/webui/console/components/events/previews/application-uplink.js
+++ b/pkg/webui/console/components/events/previews/application-uplink.js
@@ -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'
diff --git a/pkg/webui/console/components/mac-settings-section/index.js b/pkg/webui/console/components/mac-settings-section/index.js
index 0c0260e4d4..a6faf3e57d 100644
--- a/pkg/webui/console/components/mac-settings-section/index.js
+++ b/pkg/webui/console/components/mac-settings-section/index.js
@@ -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'
@@ -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'
@@ -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}}',
@@ -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
@@ -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
@@ -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 (
{
{isDynamicAdr && (
-
+ <>
+
+
+ {showEditNbTrans && (
+ <>
+
+
+
+
+
+
+ {!defaultNbTransDisabled && (
+
+
+
+
+ )}
+
+ {adrOverrides &&
+ Object.keys(adrOverrides).map(index => (
+
+
+
+
+
+
+ ))}
+
+
+ >
+ )}
+ >
)}
{isStaticAdr && (
<>
@@ -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,
diff --git a/pkg/webui/console/lib/data-rate-utils.js b/pkg/webui/console/lib/data-rate-utils.js
new file mode 100644
index 0000000000..fbc2134d1b
--- /dev/null
+++ b/pkg/webui/console/lib/data-rate-utils.js
@@ -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
+}
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/pkg/webui/console/views/device-general-settings/identity-server-form/index.js b/pkg/webui/console/views/device-general-settings/identity-server-form/index.js
index eb04fb32ef..0cec5907c4 100644
--- a/pkg/webui/console/views/device-general-settings/identity-server-form/index.js
+++ b/pkg/webui/console/views/device-general-settings/identity-server-form/index.js
@@ -319,7 +319,11 @@ IdentityServerForm.propTypes = {
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
diff --git a/pkg/webui/console/views/device-general-settings/index.js b/pkg/webui/console/views/device-general-settings/index.js
index 8e6c895c45..e06aa78217 100644
--- a/pkg/webui/console/views/device-general-settings/index.js
+++ b/pkg/webui/console/views/device-general-settings/index.js
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import React, { useCallback } from 'react'
+import React, { useCallback, useState } from 'react'
import { Col, Row, Container } from 'react-grid-system'
import { useDispatch, useSelector } from 'react-redux'
import { useNavigate, useParams } from 'react-router-dom'
@@ -25,6 +25,7 @@ import toast from '@ttn-lw/components/toast'
import Collapse from '@ttn-lw/components/collapse'
import IntlHelmet from '@ttn-lw/lib/components/intl-helmet'
+import RequireRequest from '@ttn-lw/lib/components/require-request'
import sharedMessages from '@ttn-lw/lib/shared-messages'
import getHostnameFromUrl from '@ttn-lw/lib/host-from-url'
@@ -35,6 +36,7 @@ import {
selectJsConfig,
selectNsConfig,
} from '@ttn-lw/lib/selectors/env'
+import { isBackend, getBackendErrorName } from '@ttn-lw/lib/errors/utils'
import {
mayEditApplicationDeviceKeys,
@@ -43,11 +45,13 @@ import {
import { updateDevice, resetDevice, resetUsedDevNonces } from '@console/store/actions/devices'
import { unclaimDevice } from '@console/store/actions/claim'
+import { getBandsList, getNsFrequencyPlans } from '@console/store/actions/configuration'
import {
selectSelectedDevice,
selectSelectedDeviceClaimable,
} from '@console/store/selectors/devices'
+import { selectNsFrequencyPlans } from '@console/store/selectors/configuration'
import IdentityServerForm from './identity-server-form'
import ApplicationServerForm from './application-server-form'
@@ -74,6 +78,9 @@ const DeviceGeneralSettings = () => {
const asConfig = selectAsConfig()
const jsConfig = selectJsConfig()
const nsConfig = selectNsConfig()
+ const storeFrequencyPlans = useSelector(selectNsFrequencyPlans)
+ const [defaultMacSettings, setMacSettings] = useState({})
+ const [bandId, setBandId] = useState(undefined)
useBreadcrumbs(
'device.general',
@@ -196,65 +203,114 @@ const DeviceGeneralSettings = () => {
nsDescription = m.notInCluster
}
+ const fetchData = useCallback(
+ async dispatch => {
+ if (device.frequency_plan_id && device.lorawan_phy_version) {
+ let frequencyPlans = storeFrequencyPlans
+ if (frequencyPlans.length === 0) {
+ frequencyPlans = await dispatch(attachPromise(getNsFrequencyPlans()))
+ }
+ const bandId = frequencyPlans.find(fp => fp.id === device.frequency_plan_id).band_id
+ setBandId(bandId)
+ await dispatch(getBandsList(bandId, device.lorawan_phy_version))
+ }
+ if (device.lorawan_phy_version && device.frequency_plan_id) {
+ try {
+ const settings = await tts.Ns.getDefaultMacSettings(
+ device.frequency_plan_id,
+ device.lorawan_phy_version,
+ )
+ setMacSettings(settings)
+ } catch (err) {
+ if (isBackend(err) && getBackendErrorName(err) === 'no_band_version') {
+ toast({
+ type: toast.types.ERROR,
+ message: sharedMessages.fpNotFoundError,
+ messageValues: {
+ lorawanVersion: device.lorawan_phy_version,
+ freqPlan: device.frequency_plan_id,
+ code: msg => {msg}
,
+ },
+ })
+ } else {
+ toast({
+ type: toast.types.ERROR,
+ message: m.macSettingsError,
+ messageValues: {
+ freqPlan: device.frequency_plan_id,
+ code: msg => {msg}
,
+ },
+ })
+ }
+ }
+ }
+ },
+ [device.lorawan_phy_version, device.frequency_plan_id, storeFrequencyPlans],
+ )
+
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/pkg/webui/console/views/device-general-settings/network-server-form/index.js b/pkg/webui/console/views/device-general-settings/network-server-form/index.js
index 29e4673bf5..730051c7ad 100644
--- a/pkg/webui/console/views/device-general-settings/network-server-form/index.js
+++ b/pkg/webui/console/views/device-general-settings/network-server-form/index.js
@@ -83,20 +83,22 @@ const NetworkServerForm = React.memo(props => {
mayEditKeys,
mayReadKeys,
onMacReset,
+ defaultMacSettings,
getDefaultMacSettings,
+ bandId,
} = props
const {
multicast = false,
supports_join = false,
supports_class_b = false,
supports_class_c = false,
- version_ids = {},
} = device
+ const dispatch = useDispatch()
+
const isABP = isDeviceABP(device)
const isMulticast = isDeviceMulticast(device)
const isJoinedOTAA = isDeviceOTAA(device) && isDeviceJoined(device)
- const bandId = version_ids.band_id
const validationContext = React.useMemo(
() => ({
@@ -110,7 +112,7 @@ const NetworkServerForm = React.memo(props => {
const formRef = React.useRef(null)
- const [macSettings, setMacSettings] = React.useState({})
+ const [macSettings, setMacSettings] = React.useState(defaultMacSettings)
const [phyVersion, setPhyVersion] = React.useState(device.lorawan_phy_version)
const phyVersionRef = React.useRef()
@@ -147,6 +149,7 @@ const NetworkServerForm = React.memo(props => {
setMacSettings(settings)
if (formRef.current) {
const { setValues, values } = formRef.current
+ const adrDynamic = values.mac_settings?.adr?.dynamic
setValues(
validationSchema.cast(
{
@@ -158,13 +161,23 @@ const NetworkServerForm = React.memo(props => {
// And to make sure that, if there is already a value set for `adr`, it is not overwritten
// by the default mac settings.
adr:
- 'dynamic' in values.mac_settings.adr
+ 'dynamic' in values.mac_settings?.adr
? {
dynamic: {
- margin: values.mac_settings.adr.dynamic?.margin ?? settings.adr_margin,
+ ...adrDynamic,
+ margin: adrDynamic?.margin ?? settings.adr_margin,
+ _use_default_nb_trans:
+ Boolean(adrDynamic?.min_nb_trans) || Boolean(adrDynamic?.max_nb_trans)
+ ? false
+ : !(Object.keys(adrDynamic?.overrides || {}).length > 0),
+ min_nb_trans: adrDynamic?.min_nb_trans ?? 1,
+ max_nb_trans: adrDynamic?.max_nb_trans ?? 3,
+ _override_nb_trans_defaults:
+ Boolean(adrDynamic?.min_nb_trans) &&
+ Boolean(adrDynamic?.max_nb_trans),
},
}
- : values.mac_settings.adr,
+ : values.mac_settings?.adr,
},
},
{ context: validationContext },
@@ -203,7 +216,16 @@ const NetworkServerForm = React.memo(props => {
getMacSettings(freqPlan, phyVersion)
}
}
- }, [freqPlan, getDefaultMacSettings, lorawanVersion, phyVersion, validationContext])
+ }, [
+ freqPlan,
+ getDefaultMacSettings,
+ defaultMacSettings,
+ lorawanVersion,
+ phyVersion,
+ validationContext,
+ bandId,
+ dispatch,
+ ])
const initialActivationMode = supports_join
? ACTIVATION_MODES.OTAA
@@ -225,6 +247,13 @@ const NetworkServerForm = React.memo(props => {
...defaultValues.mac_settings,
...macSettings,
...device.mac_settings,
+ adr: {
+ dynamic: {
+ ...device.mac_settings?.adr?.dynamic,
+ min_nb_trans: device.mac_settings?.adr?.dynamic?.min_nb_trans ?? null,
+ max_nb_trans: device.mac_settings?.adr?.dynamic?.max_nb_trans ?? null,
+ },
+ },
},
},
{ context: validationContext, stripUnknown: true },
@@ -232,7 +261,6 @@ const NetworkServerForm = React.memo(props => {
[device, initialActivationMode, isClassB, isClassC, macSettings, validationContext],
)
- const dispatch = useDispatch()
const appId = device.ids.application_ids.application_id
const devId = device.ids.device_id
const handleMacReset = React.useCallback(async () => {
@@ -252,7 +280,25 @@ const NetworkServerForm = React.memo(props => {
const handleSubmit = React.useCallback(
async (values, { resetForm, setSubmitting }) => {
- const castedValues = validationSchema.cast(values, {
+ let parsedValues = values
+ // If the nbTrans values are not overridden, remove them from the payload.
+ if (!values.mac_settings?.adr.dynamic._override_nb_trans_defaults) {
+ const { max_nb_trans, min_nb_trans, ...rest } = parsedValues.mac_settings?.adr.dynamic
+ parsedValues = {
+ ...parsedValues,
+ mac_settings: {
+ ...parsedValues.mac_settings,
+ adr: {
+ ...parsedValues.mac_settings?.adr,
+ dynamic: {
+ ...rest,
+ },
+ },
+ },
+ }
+ }
+
+ const castedValues = validationSchema.cast(parsedValues, {
context: validationContext,
stripUnknown: true,
})
@@ -293,7 +339,7 @@ const NetworkServerForm = React.memo(props => {
delete patch.session
}
- if (patch.mac_settings.adr) {
+ if (patch.mac_settings?.adr) {
patch.mac_settings.adr_margin = null
patch.mac_settings.use_adr = null
}
@@ -541,6 +587,7 @@ const NetworkServerForm = React.memo(props => {
lorawanVersion={lorawanVersion}
isClassB={isClassB}
isClassC={isClassC}
+ bandId={bandId}
/>
@@ -550,6 +597,10 @@ const NetworkServerForm = React.memo(props => {
})
NetworkServerForm.propTypes = {
+ bandId: PropTypes.string.isRequired,
+ defaultMacSettings: PropTypes.shape({
+ adr_margin: PropTypes.number,
+ }).isRequired,
device: PropTypes.device.isRequired,
getDefaultMacSettings: PropTypes.func.isRequired,
mayEditKeys: PropTypes.bool.isRequired,
diff --git a/pkg/webui/console/views/device-general-settings/network-server-form/validation-schema.js b/pkg/webui/console/views/device-general-settings/network-server-form/validation-schema.js
index 60af33729e..2d0c67df83 100644
--- a/pkg/webui/console/views/device-general-settings/network-server-form/validation-schema.js
+++ b/pkg/webui/console/views/device-general-settings/network-server-form/validation-schema.js
@@ -367,9 +367,50 @@ const validationSchema = Yup.object()
}
return Yup.object().shape({
- dynamic: Yup.object().shape({
- margin: Yup.number().nullable(),
- }),
+ dynamic: Yup.lazy(value =>
+ Yup.object().shape({
+ margin: Yup.number().nullable(),
+ min_nb_trans: Yup.number()
+ .min(1, Yup.passValues(sharedMessages.validateNumberGte))
+ .max(value?.max_nb_trans || 3, Yup.passValues(sharedMessages.validateNumberLte))
+ .nullable(),
+ max_nb_trans: Yup.number()
+ .min(value?.min_nb_trans || 1, Yup.passValues(sharedMessages.validateNumberGte))
+ .max(3, Yup.passValues(sharedMessages.validateNumberLte))
+ .nullable(),
+ overrides: Yup.lazy(value => {
+ if (!Boolean(value)) {
+ return Yup.object().nullable()
+ }
+
+ return Yup.object().shape(
+ Object.keys(value).reduce((acc, key) => {
+ acc[key] = Yup.object().shape({
+ _data_rate_index: Yup.string()
+ .default(key.startsWith('_') ? null : key)
+ .required(sharedMessages.validateRequired),
+ min_nb_trans: Yup.number()
+ .min(1, Yup.passValues(sharedMessages.validateNumberGte))
+ .max(
+ value[key]?.max_nb_trans || 3,
+ Yup.passValues(sharedMessages.validateNumberLte),
+ )
+ .required(sharedMessages.validateRequired),
+ max_nb_trans: Yup.number()
+ .min(
+ value[key]?.min_nb_trans || 1,
+ Yup.passValues(sharedMessages.validateNumberGte),
+ )
+ .max(3, Yup.passValues(sharedMessages.validateNumberLte))
+ .required(sharedMessages.validateRequired),
+ })
+
+ return acc
+ }, {}),
+ )
+ }),
+ }),
+ ),
})
}),
desired_adr_ack_limit_exponent: Yup.string().when(['adr'], ([adr], schema) => {
diff --git a/pkg/webui/lib/constants/tooltip-ids.js b/pkg/webui/lib/constants/tooltip-ids.js
index 29d785ef35..efc89da852 100644
--- a/pkg/webui/lib/constants/tooltip-ids.js
+++ b/pkg/webui/lib/constants/tooltip-ids.js
@@ -83,4 +83,6 @@ export default Object.freeze({
STATUS_TIME_PERIODICITY: 'status-time-periodicity',
UPDATE_LOCATION_FROM_STATUS: 'update-location-from-status',
INPUT_METHOD: 'input-method',
+ USE_DEFAULT_NB_TRANS: 'use-default-nb-trans',
+ DATA_RATE_SPECIFIC_OVERRIDES: 'data-rate-specific-overrides',
})
diff --git a/pkg/webui/lib/field-description-messages.js b/pkg/webui/lib/field-description-messages.js
index e4d9390571..61f178304e 100644
--- a/pkg/webui/lib/field-description-messages.js
+++ b/pkg/webui/lib/field-description-messages.js
@@ -209,6 +209,10 @@ const m = defineMessages({
'The device nonces ensure that join requests cannot be replayed by attackers. Resetting the device nonces enables the end device to re-use a previously used nonce. Do not use this option unless you are sure that you would like the nonces to be usable again.',
alcsyncDescription:
'The Application Layer Clock Synchronization package is part of the LoRa TS003 specification, it synchronizes the real-time clock of an end-device to the network’s Global Positioning System (GPS) clock with near-second accuracy. It is useful for end-devices that do not have access to another accurate time source.',
+ useDefaultNbTransDescription:
+ 'The number of retransmissions (NbTrans) controls how many times a frame will be transmitted over the air. The redundancy introduced by retransmissions improves the chances that a packet will be received, at the expense of more power usage. By default, depending on the number of missed frames, the same frame may be transmitted 3 times.',
+ dataRateSpecificOverridesDescription:
+ 'Data rate specific overrides allow the number of transmissions to be limited on a per data rate basis. This may be used to limit power usage for low data rates.',
})
const descriptions = Object.freeze({
@@ -480,6 +484,12 @@ const descriptions = Object.freeze({
[TOOLTIP_IDS.ALCSYNC]: {
description: m.alcsyncDescription,
},
+ [TOOLTIP_IDS.USE_DEFAULT_NB_TRANS]: {
+ description: m.useDefaultNbTransDescription,
+ },
+ [TOOLTIP_IDS.DATA_RATE_SPECIFIC_OVERRIDES]: {
+ description: m.dataRateSpecificOverridesDescription,
+ },
})
const links = Object.freeze({
diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json
index ae973f98b4..f81cb36f0f 100644
--- a/pkg/webui/locales/en.json
+++ b/pkg/webui/locales/en.json
@@ -296,6 +296,17 @@
"console.components.mac-settings-section.index.adrAckValue": "{count, plural, one {every message} other {every {count} messages}}",
"console.components.mac-settings-section.index.statusCountPeriodicity": "Status count periodicity",
"console.components.mac-settings-section.index.statusTimePeriodicity": "Status time periodicity",
+ "console.components.mac-settings-section.index.dataRate": "Data Rate {n}",
+ "console.components.mac-settings-section.index.dataRatePlaceholder": "Data Rate",
+ "console.components.mac-settings-section.index.minNbTrans": "Min. NbTrans",
+ "console.components.mac-settings-section.index.maxNbTrans": "Max. NbTrans",
+ "console.components.mac-settings-section.index.useDefaultNbTrans": "Use default settings for number of retransmissions",
+ "console.components.mac-settings-section.index.adrNbTrans": "ADR number of retransmissions (NbTrans)",
+ "console.components.mac-settings-section.index.overrideNbTrans": "Override server defaults for NbTrans (all data rates)",
+ "console.components.mac-settings-section.index.defaultForAllRates": "(Default for all data rates)",
+ "console.components.mac-settings-section.index.defaultNbTransMessage": "Overriding the default is not required for using data rate overrides (below)",
+ "console.components.mac-settings-section.index.specificOverrides": "Data rate specific overrides",
+ "console.components.mac-settings-section.index.addSpecificOverride": "Add data rate specific override",
"console.components.payload-formatters-form.index.repository": "Use Device Repository formatters",
"console.components.payload-formatters-form.index.customJavascipt": "Custom Javascript formatter",
"console.components.payload-formatters-form.index.formatterType": "Formatter type",
@@ -973,6 +984,8 @@
"lib.field-description-messages.resetsJoinNoncesDescription": "Allowing join nonces to be reset disables any reuse checks for the device nonces and join nonces. The join requests can be replayed indefinitely when this option is enabled. This behavior is non compliant with the LoRaWAN specifications and must not be used outside of development environments.",
"lib.field-description-messages.resetUsedDevNoncesDescription": "The device nonces ensure that join requests cannot be replayed by attackers. Resetting the device nonces enables the end device to re-use a previously used nonce. Do not use this option unless you are sure that you would like the nonces to be usable again.",
"lib.field-description-messages.alcsyncDescription": "The Application Layer Clock Synchronization package is part of the LoRa TS003 specification, it synchronizes the real-time clock of an end-device to the network’s Global Positioning System (GPS) clock with near-second accuracy. It is useful for end-devices that do not have access to another accurate time source.",
+ "lib.field-description-messages.useDefaultNbTransDescription": "The number of retransmissions (NbTrans) controls how many times a frame will be transmitted over the air. The redundancy introduced by retransmissions improves the chances that a packet will be received, at the expense of more power usage. By default, depending on the number of missed frames, the same frame may be transmitted 3 times.",
+ "lib.field-description-messages.dataRateSpecificOverridesDescription": "Data rate specific overrides allow the number of transmissions to be limited on a per data rate basis. This may be used to limit power usage for low data rates.",
"lib.payload-formatter-messages.repository": "Repository",
"lib.payload-formatter-messages.javascript": "Javascript",
"lib.payload-formatter-messages.cayennelpp": "CayenneLPP",
diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json
index 273b21cb4d..c20d9c444c 100644
--- a/pkg/webui/locales/ja.json
+++ b/pkg/webui/locales/ja.json
@@ -296,6 +296,17 @@
"console.components.mac-settings-section.index.adrAckValue": "{count, plural, one {every message} other {every {count} messages}}",
"console.components.mac-settings-section.index.statusCountPeriodicity": "ステータスカウントの周期性",
"console.components.mac-settings-section.index.statusTimePeriodicity": "ステータス時間の周期性",
+ "console.components.mac-settings-section.index.dataRate": "",
+ "console.components.mac-settings-section.index.dataRatePlaceholder": "",
+ "console.components.mac-settings-section.index.minNbTrans": "",
+ "console.components.mac-settings-section.index.maxNbTrans": "",
+ "console.components.mac-settings-section.index.useDefaultNbTrans": "",
+ "console.components.mac-settings-section.index.adrNbTrans": "",
+ "console.components.mac-settings-section.index.overrideNbTrans": "",
+ "console.components.mac-settings-section.index.defaultForAllRates": "",
+ "console.components.mac-settings-section.index.defaultNbTransMessage": "",
+ "console.components.mac-settings-section.index.specificOverrides": "",
+ "console.components.mac-settings-section.index.addSpecificOverride": "",
"console.components.payload-formatters-form.index.repository": "デバイスリポジトリのフォーマッタを使用",
"console.components.payload-formatters-form.index.customJavascipt": "カスタムJavascriptフォーマッター",
"console.components.payload-formatters-form.index.formatterType": "フォーマッタータイプ",
@@ -973,6 +984,8 @@
"lib.field-description-messages.resetsJoinNoncesDescription": "join noncesのリセットを許可すると、デバイスnoncesとjoin noncesの再利用チェックが無効になります。このオプションを有効にすると、参加リクエストを無限に再生することができます。この動作はLoRaWANの仕様に準拠していないため、開発環境以外では使用しないでください。",
"lib.field-description-messages.resetUsedDevNoncesDescription": "デバイスのnoncesは、攻撃者によってjoin要求が再生されないことを保証します。デバイスノンスをリセットすると、エンドデバイスは以前に使用したノンスを再利用することができます。このオプションは、noncesを再び使用できるようにすることが確実でない限り、使用しないでください",
"lib.field-description-messages.alcsyncDescription": "アプリケーション層クロック同期パッケージは、LoRa TS003仕様の一部であり、エンドデバイスのリアルタイムクロックをネットワークの全地球測位システム(GPS)クロックに秒に近い精度で同期させます。これは、他の正確な時間ソースにアクセスできないエンドデバイスに便利です",
+ "lib.field-description-messages.useDefaultNbTransDescription": "",
+ "lib.field-description-messages.dataRateSpecificOverridesDescription": "",
"lib.payload-formatter-messages.repository": "レポジトリー",
"lib.payload-formatter-messages.javascript": "Javascript",
"lib.payload-formatter-messages.cayennelpp": "CayenneLPP",
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