Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ For details about compatibility between different releases, see the **Commitment
- Parsing of the `rctx` field in the `upinfo` object of upstream messages received via the LoRa Basics Station LNS protocol. The field name was misspelled as `rtcx`, so the radio context reported by gateways was ignored. The antenna index in the uplink metadata now reflects the reported radio context and is echoed back in class A downlinks, instead of always being 0.
- The antenna gain sent to LoRa Basics Station gateways in the router configuration. Each board's radio configuration now uses the gain of its own antenna instead of applying the first antenna's gain to all boards, and fractional gains are no longer truncated.
- Prefilling the end device model, hardware version, firmware version and band in the Console when scanning an end device QR code that contains a LoRa Alliance vendor ID and vendor profile ID that are known in the Device Repository. Previously, only the brand was prefilled.
- Misleading schedule any time delay hints in the gateway LoRaWAN settings in the Console. The Console showed a "delay too short" warning for gateways without a delay, even though the Gateway Server then uses the default delay. The Console now shows the minimum (132.5ms) in a single description and shows the default delay (532.5ms) as placeholder when no delay is set.

## [3.36.2] - 2026-08-17

Expand Down
78 changes: 78 additions & 0 deletions cypress/e2e/console/gateways/edit.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ describe('Gateway general settings', () => {
let gateway
let gateway2
let gateway3
let gateway4
let gateway5
const collabUserId = 'test-collab-user'
const collabUser = {
ids: { user_id: collabUserId },
Expand Down Expand Up @@ -69,9 +71,26 @@ describe('Gateway general settings', () => {
gateway_server_address: 'localhost',
frequency_plan_ids: ['EU_863_870'],
}
gateway4 = {
ids: { gateway_id: 'test-gateway-default-delay', eui: '0000000000000003' },
name: 'Test Gateway Default Delay',
description: 'Gateway for testing the default schedule any time delay',
gateway_server_address: 'localhost',
frequency_plan_ids: ['EU_863_870'],
}
gateway5 = {
ids: { gateway_id: 'test-gateway-reset-delay', eui: '0000000000000004' },
name: 'Test Gateway Reset Delay',
description: 'Gateway for testing resetting the schedule any time delay',
schedule_anytime_delay: '523ms',
gateway_server_address: 'localhost',
frequency_plan_ids: ['EU_863_870'],
}
cy.createGateway(gateway, user.ids.user_id)
cy.createGateway(gateway2, user.ids.user_id)
cy.createGateway(gateway3, user.ids.user_id)
cy.createGateway(gateway4, user.ids.user_id)
cy.createGateway(gateway5, user.ids.user_id)
})

it('displays newly created gateway values', () => {
Expand Down Expand Up @@ -152,6 +171,65 @@ describe('Gateway general settings', () => {
.and('have.attr', 'value', '0.523')
})

it('displays the default schedule any time delay when no delay is set', () => {
cy.loginConsole({ user_id: user.ids.user_id, password: user.password })
cy.visit(
`${Cypress.config('consoleRootPath')}/gateways/${gateway4.ids.gateway_id}/general-settings`,
)

cy.findByText('LoRaWAN options', { selector: 'h3' })
.closest('[data-test-id="collapsible-section"]')
.within(() => {
cy.findByRole('button', { name: 'Expand' }).click()
cy.findByLabelText('Schedule any time delay')
.should('have.value', '')
.and('have.attr', 'placeholder', '532.5ms')
cy.findByText(
'Configure gateway delay. If the delay is too short, the minimum of 132.5ms will be used.',
).should('be.visible')
})
})

it('succeeds resetting the schedule any time delay to the default', () => {
cy.loginConsole({ user_id: user.ids.user_id, password: user.password })
cy.visit(
`${Cypress.config('consoleRootPath')}/gateways/${gateway5.ids.gateway_id}/general-settings`,
)
cy.intercept('PUT', `/api/v3/gateways/${gateway5.ids.gateway_id}`).as('update-gateway')

cy.findByText('LoRaWAN options', { selector: 'h3' })
.closest('[data-test-id="collapsible-section"]')
.within(() => {
cy.findByRole('button', { name: 'Expand' }).click()
cy.findByLabelText('Schedule any time delay').should('have.value', '0.523')
cy.findByLabelText('Schedule any time delay').clear()
cy.findByRole('button', { name: 'Save changes' }).click()
})

cy.wait('@update-gateway')
.its('request.body')
.should(body => {
expect(body.gateway.schedule_anytime_delay).to.equal('0s')
expect(body.field_mask.paths).to.include('schedule_anytime_delay')
})
cy.findByTestId('error-notification').should('not.exist')
cy.findByTestId('toast-notification-success').findByText('Gateway updated').should('be.visible')
cy.findByLabelText('Schedule any time delay')
.should('have.value', '')
.and('have.attr', 'placeholder', '532.5ms')

cy.reload()

cy.findByText('LoRaWAN options', { selector: 'h3' })
.closest('[data-test-id="collapsible-section"]')
.within(() => {
cy.findByRole('button', { name: 'Expand' }).click()
cy.findByLabelText('Schedule any time delay')
.should('have.value', '')
.and('have.attr', 'placeholder', '532.5ms')
})
})

it('succeeds changing gateway information', () => {
cy.loginConsole({ user_id: user.ids.user_id, password: user.password })
cy.visit(
Expand Down
2 changes: 1 addition & 1 deletion pkg/webui/components/unit-input/common/duration.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const decoder = (rawValue = '') => {
return { value: '', unit: null }
}
const value = rawValue.split(unitRegexp)[0]
const unit = rawValue.split(value)[1] || null
const unit = rawValue.slice(value.length) || null
return {
value: value ? Number(value) : undefined,
unit,
Expand Down
4 changes: 4 additions & 0 deletions pkg/webui/components/unit-input/unit-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const UnitInput = props => {
inputWidth,
onInputChange,
onUnitChange,
placeholder,
} = props
const maskedUnits = unitSelector ? units.filter(u => unitSelector.includes(u.value)) : units
return (
Expand All @@ -57,6 +58,7 @@ const UnitInput = props => {
disabled={disabled}
error={error}
inputWidth={inputWidth}
placeholder={placeholder}
/>
<Select
className={style.select}
Expand All @@ -83,6 +85,7 @@ UnitInput.propTypes = {
onBlur: PropTypes.func.isRequired,
onInputChange: PropTypes.func.isRequired,
onUnitChange: PropTypes.func.isRequired,
placeholder: PropTypes.message,
required: PropTypes.bool,
selectWidth: PropTypes.inputWidth,
unitSelector: PropTypes.arrayOf(PropTypes.string),
Expand All @@ -103,6 +106,7 @@ UnitInput.defaultProps = {
defaultUnit: undefined,
inputWidth: 'm',
disabled: false,
placeholder: undefined,
required: false,
value: undefined,
error: false,
Expand Down
4 changes: 2 additions & 2 deletions pkg/webui/console/constants/delays.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
// limitations under the License.

export default Object.freeze({
MINIMUM_GATEWAY_SCHEDULE_ANYTIME_DELAY: 130, // Miliseconds
DEFAULT_GATEWAY_SCHEDULE_ANYTIME_DELAY: 530,
MINIMUM_GATEWAY_SCHEDULE_ANYTIME_DELAY: 132.5, // Miliseconds
DEFAULT_GATEWAY_SCHEDULE_ANYTIME_DELAY: 532.5,
})
Original file line number Diff line number Diff line change
Expand Up @@ -29,59 +29,24 @@ import tooltipIds from '@ttn-lw/lib/constants/tooltip-ids'
import PropTypes from '@ttn-lw/lib/prop-types'
import sharedMessages from '@ttn-lw/lib/shared-messages'

import { unit as unitRegexp, emptyDuration as emptyDurationRegexp } from '@console/lib/regexp'

import validationSchema from './validation-schema'

const decodeDelayValue = value => {
if (emptyDurationRegexp.test(value)) {
return {
duration: undefined,
unit: value,
}
}
const duration = value.split(unitRegexp)[0]
const unit = value.split(duration)[1]
return {
duration: duration ? Number(duration) : undefined,
unit,
}
}

const isEmptyFrequencyPlan = value => value?.includes(frequencyPlans.EMPTY_FREQ_PLAN)

const isNotValidDuration = value => {
const { duration, unit } = decodeDelayValue(value)

switch (unit) {
case 'ms':
return duration < delay.MINIMUM_GATEWAY_SCHEDULE_ANYTIME_DELAY
case 's':
return duration < delay.MINIMUM_GATEWAY_SCHEDULE_ANYTIME_DELAY / 1000
case 'm':
return duration < delay.MINIMUM_GATEWAY_SCHEDULE_ANYTIME_DELAY / 60000
case 'h':
return duration < delay.MINIMUM_GATEWAY_SCHEDULE_ANYTIME_DELAY / 3600000
}
}
// A zero delay lets the Gateway Server use the default delay, so the field is left empty
// to show the default delay as placeholder instead.
const decodeDelay = value => (parseFloat(value) ? value : '')

const LorawanSettingsForm = React.memo(props => {
const { gateway, onSubmit } = props

const [error, setError] = React.useState(undefined)

const [shouldDisplayWarning, setShouldDisplayWarning] = React.useState(
isNotValidDuration(gateway.schedule_anytime_delay),
)

const onScheduleAnytimeDelayChange = React.useCallback(value => {
setShouldDisplayWarning(isNotValidDuration(value))
}, [])

const initialValues = React.useMemo(
() => ({
...validationSchema.cast(gateway),
frequency_plan_ids: gateway.frequency_plan_ids || [frequencyPlans.EMPTY_FREQ_PLAN],
schedule_anytime_delay: decodeDelay(gateway.schedule_anytime_delay),
}),
[gateway],
)
Expand All @@ -97,7 +62,12 @@ const LorawanSettingsForm = React.memo(props => {
setError(undefined)
try {
await onSubmit(castedValues)
resetForm({ values: castedValues })
resetForm({
values: {
...castedValues,
schedule_anytime_delay: decodeDelay(castedValues.schedule_anytime_delay),
Comment thread
PavelJankoski marked this conversation as resolved.
},
})
} catch (err) {
setSubmitting(false)
setError(err)
Expand Down Expand Up @@ -136,27 +106,15 @@ const LorawanSettingsForm = React.memo(props => {
unitSelector={['ms', 's']}
description={{
...sharedMessages.scheduleAnyTimeDescription,
values: {
minimumValue: delay.MINIMUM_GATEWAY_SCHEDULE_ANYTIME_DELAY,
defaultValue: delay.DEFAULT_GATEWAY_SCHEDULE_ANYTIME_DELAY,
},
values: { minimumValue: delay.MINIMUM_GATEWAY_SCHEDULE_ANYTIME_DELAY },
}}
placeholder={`${delay.DEFAULT_GATEWAY_SCHEDULE_ANYTIME_DELAY}ms`}
units={[
{ label: sharedMessages.milliseconds, value: 'ms' },
{ label: sharedMessages.seconds, value: 's' },
{ label: sharedMessages.minutes, value: 'm' },
{ label: sharedMessages.hours, value: 'h' },
]}
onChange={onScheduleAnytimeDelayChange}
warning={
shouldDisplayWarning
? {
...sharedMessages.delayWarning,
values: { minimumValue: delay.MINIMUM_GATEWAY_SCHEDULE_ANYTIME_DELAY },
}
: undefined
}
required
tooltipId={tooltipIds.SCHEDULE_ANYTIME_DELAY}
/>
<SubmitBar>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import { delay as delayRegexp } from '@console/lib/regexp'
const validationSchema = Yup.object().shape({
frequency_plan_ids: Yup.array(),
schedule_downlink_late: Yup.boolean().default(false),
schedule_anytime_delay: Yup.string().matches(
delayRegexp,
Yup.passValues(sharedMessages.validateDelayFormat),
),
schedule_anytime_delay: Yup.string()
// An empty delay lets the Gateway Server use the default delay.
.transform(value => (parseFloat(value) ? value : '0s'))
.matches(delayRegexp, Yup.passValues(sharedMessages.validateDelayFormat)),
})

export default validationSchema
4 changes: 1 addition & 3 deletions pkg/webui/lib/shared-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,6 @@ export default defineMessages({
data: 'Data',
dataSheet: 'Data sheet',
defineRights: 'Define rights',
delayWarning:
'Delay too short. The lower bound ({minimumValue}ms) will be used by the Gateway Server.',
deleteGateway: 'Delete gateway',
unclaimAndDeleteDevice: 'Unclaim and delete end device',
unclaimAndDeleteGateway: 'Unclaim and delete gateway',
Expand Down Expand Up @@ -553,7 +551,7 @@ export default defineMessages({
scanEndDevice: 'Scan end device QR code',
scheduleAnyTimeDelay: 'Schedule any time delay',
scheduleAnyTimeDescription:
'Configure gateway delay (minimum: {minimumValue}ms, default: {defaultValue}ms)',
'Configure gateway delay. If the delay is too short, the minimum of {minimumValue}ms will be used.',
scheduleDownlink: 'Schedule downlink',
scheduleDownlinkLateDescription: 'Enable server-side buffer of downlink messages',
searchApplications: 'Search applications',
Expand Down
3 changes: 1 addition & 2 deletions pkg/webui/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1345,7 +1345,6 @@
"lib.shared-messages.data": "Data",
"lib.shared-messages.dataSheet": "Data sheet",
"lib.shared-messages.defineRights": "Define rights",
"lib.shared-messages.delayWarning": "Delay too short. The lower bound ({minimumValue}ms) will be used by the Gateway Server.",
"lib.shared-messages.deleteGateway": "Delete gateway",
"lib.shared-messages.unclaimAndDeleteDevice": "Unclaim and delete end device",
"lib.shared-messages.unclaimAndDeleteGateway": "Unclaim and delete gateway",
Expand Down Expand Up @@ -1698,7 +1697,7 @@
"lib.shared-messages.scanAgain": "Scan again",
"lib.shared-messages.scanEndDevice": "Scan end device QR code",
"lib.shared-messages.scheduleAnyTimeDelay": "Schedule any time delay",
"lib.shared-messages.scheduleAnyTimeDescription": "Configure gateway delay (minimum: {minimumValue}ms, default: {defaultValue}ms)",
"lib.shared-messages.scheduleAnyTimeDescription": "Configure gateway delay. If the delay is too short, the minimum of {minimumValue}ms will be used.",
"lib.shared-messages.scheduleDownlink": "Schedule downlink",
"lib.shared-messages.scheduleDownlinkLateDescription": "Enable server-side buffer of downlink messages",
"lib.shared-messages.searchApplications": "Search applications",
Expand Down
3 changes: 1 addition & 2 deletions pkg/webui/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -1345,7 +1345,6 @@
"lib.shared-messages.data": "データ",
"lib.shared-messages.dataSheet": "",
"lib.shared-messages.defineRights": "権利の定義",
"lib.shared-messages.delayWarning": "遅延が短すぎます。ゲートウェイサーバーは下限値 ({minimumValue}ms) を使用します",
"lib.shared-messages.deleteGateway": "",
"lib.shared-messages.unclaimAndDeleteDevice": "",
"lib.shared-messages.unclaimAndDeleteGateway": "",
Expand Down Expand Up @@ -1698,7 +1697,7 @@
"lib.shared-messages.scanAgain": "",
"lib.shared-messages.scanEndDevice": "",
"lib.shared-messages.scheduleAnyTimeDelay": "任意の時間遅延をスケジュール",
"lib.shared-messages.scheduleAnyTimeDescription": "ゲートウェイ遅延の設定 (最小値: {minimumValue}ms, デフォルト値: {defaultValue}ms)",
"lib.shared-messages.scheduleAnyTimeDescription": "",
"lib.shared-messages.scheduleDownlink": "",
"lib.shared-messages.scheduleDownlinkLateDescription": "ダウンリンクメッセージのサーバ側バッファを有効にします",
"lib.shared-messages.searchApplications": "",
Expand Down
Loading