Skip to content

Handle 32-bit xtime rollover of LBS gateways without session - #8099

Open
halimi wants to merge 2 commits into
v3.37from
fix/lbs-32bit-xtime
Open

halimi wants to merge 2 commits into
v3.37from
fix/lbs-32bit-xtime

Conversation

@halimi

@halimi halimi commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Some LoRa Basics Station implementations report the raw 32-bit concentrator timestamp as xtime, without a session (the upper bits are all zero) and without accounting for rollovers. When the timestamp rolled over (approximately every 72 minutes), the Gateway Server synchronized the gateway clock with the frontend-provided concentrator time, which went back in time by ~2³² µs. The emissions scheduled before the rollover then appeared to be in the future, and with the blocking window duty cycle style used for LBS gateways, all downlinks failed with sub band is blocked for up to 72 minutes.

Changes

  • Detect gateways that do not extend the concentrator timestamp by the session byte of the xtime (bits 48-55). LoRa Basics Station never uses session 0 (ts_newXtimeSession).
  • For these gateways, do not provide the frontend clock synchronization on uplinks, so that the scheduler synchronizes using the concentrator timestamp (RolloverClock.Sync / SyncWithGatewayAbsolute), which accounts for rollovers.
  • For these gateways, send the class A downlink xtime as a 32-bit concentrator timestamp (the RX delay is subtracted before truncating, so it wraps correctly).
  • Do not send time transfers (timesync) to these gateways.
  • Gateways with a session (conforming LoRa Basics Station) are unaffected.
  • Make the mock gateway clock in the semtechws tests never use session 0.

Testing

Steps
go test ./pkg/gatewayserver/io/semtechws/... ./pkg/gatewayserver/scheduling/...

TestNonExtendedXTimeRollover handles an uplink at 2³² - 60 s with a downlink, followed by an uplink after the rollover (xtime 780557) with a downlink, through a real io.Connection and scheduler with the blocking window duty cycle style.

Results

All tests pass. Without the change in upstream.go, TestNonExtendedXTimeRollover fails with sub band is blocked for 1h10m37s, which reproduces the issue observed on a gateway.

Regressions

LoRa Basics Station gateways with a session keep using the frontend clock synchronization and 48-bit xtime, covered by the existing TestTraffic, TestFromDownlinkMessage and TestTransferTime.

Notes for Reviewers

The downlink xtime truncation assumes that these gateways interpret the downlink xtime the same way they report it on uplinks, i.e. as the 32-bit concentrator timestamp to which the RX delay is added. Gateway-side logs confirm this before the rollover; a downlink right after a rollover still needs to be validated on a gateway.

Checklist

  • Scope: The referenced issue is addressed, there are no unrelated changes.
  • Compatibility: The changes are backwards compatible with existing API, storage, configuration and CLI, according to the compatibility commitments in README.md for the chosen target branch.
  • Documentation: Relevant documentation is added or updated.
  • Testing: The steps/process to test this feature are clearly explained including testing for regressions.
  • Infrastructure: If infrastructural changes (e.g., new RPC, configuration) are needed, a separate issue is created in the infrastructural repositories.
  • Changelog: Significant features, behavior changes, deprecations and fixes are added to CHANGELOG.md.
  • Commits: Commit messages follow guidelines in CONTRIBUTING.md, there are no fixup commits left.

🤖 Generated with Claude Code

Some LoRa Basics Station implementations report the raw 32-bit
concentrator timestamp as xtime, without a session and without
accounting for rollovers. When the timestamp rolled over, the gateway
clock went back in time and the sub band stayed blocked by the
emissions scheduled before the rollover for up to 72 minutes.

LoRa Basics Station never uses session 0, so for these gateways let the
scheduler synchronize using the concentrator timestamp, which accounts
for rollovers, and send downlink xtime values as 32-bit concentrator
timestamps. Time transfers are not sent to these gateways.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@halimi
halimi requested a review from a team as a code owner November 12, 2026 17:42
@halimi
halimi requested a review from johanstokking November 12, 2026 17:42
@github-actions github-actions Bot added the c/gateway server This is related to the Gateway Server label Nov 12, 2026
@halimi
halimi requested a balanced review from Copilot November 13, 2026 10:20
@halimi halimi self-assigned this Nov 13, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Hardware validation on an affected gateway is still needed to confirm that truncated 32-bit xtime values produce successful downlinks with correct RX delay across rollover.

No open issues 🎉

What changed in this PR

Fixes Gateway Server downlink scheduling after 32-bit xtime rollover on LoRa Basics Station gateways without a session, while preserving behavior for gateways with a session.

Changes:

  • Use rollover-aware scheduler clock synchronization for sessionless gateways.
  • Encode their timestamp-based downlinks with 32-bit timestamps and skip time transfers.
  • Add rollover regression tests, keep mock sessions nonzero, and document the fix.
File Description
pkg/​gatewayserver/​io/​semtechws/​ws_util_test.go Prevent mock clocks from using session zero.
pkg/​gatewayserver/​io/​semtechws/​time.go Add session detection and 32-bit xtime conversion helpers.
pkg/​gatewayserver/​io/​semtechws/​time_test.go Test session detection and timestamp wrapping.
pkg/​gatewayserver/​io/​semtechws/​lbslns/​xtime_test.go Add a scheduler regression test spanning rollover.
pkg/​gatewayserver/​io/​semtechws/​lbslns/​upstream.go Defer sessionless clock synchronization to the rollover-aware scheduler.
pkg/​gatewayserver/​io/​semtechws/​lbslns/​downstream.go Wrap sessionless downlink timestamps and suppress time transfers.
pkg/​gatewayserver/​io/​semtechws/​lbslns/​downstream_test.go Test downlink rollover encoding and time-transfer suppression.
CHANGELOG.md Document the sessionless gateway rollover fix.

Review effort: Balanced


sessionID, found := semtechws.GetSessionID(ctx)
if found && !semtechws.IsExtendedXTimeSession(sessionID) {
// Gateways that do not extend the concentrator timestamp do not implement time transfers.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is true though.

Comment on lines +129 to +133
// The gateway does not extend the 32-bit concentrator timestamp, so the XTime must wrap
// around in the same way. Subtract a second before truncating, since the RX delay is 1.
xTime = semtechws.ConcentratorTimeToTimestampXTime(
sessionID, concentratorTime-scheduling.ConcentratorTime(time.Second),
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the gateway does not use a session ID and rolling over timestamp, this apparently creates an xtime which does, right?

// ConcentratorTimeToTimestampXTime constructs the XTime associated with the provided
// session ID and concentrator time for sessions that do not extend the concentrator timestamp.
// Only the lower 32 bits of the concentrator time (in microseconds) are used, which wrap around
// on rollover in the same way as the concentrator timestamp of the gateway.
// See IsExtendedXTimeSession.
func ConcentratorTimeToTimestampXTime(id int32, t scheduling.ConcentratorTime) int64 {
return int64(id)<<48 | (int64(t) / int64(time.Microsecond) & XTime32BitLSBMask)
}

I would expect that gateways that simply send the rolling over 32-bit unsigned concentrator timestamp in xtime, also expect that for downlink.

Do not assume that LoRa Basics Station gateways without session do not
implement time transfers; send the 32-bit concentrator timestamp as
xtime instead. Remove the session ID from the 32-bit xtime conversion,
as these gateways use the plain 32-bit concentrator timestamp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c/gateway server This is related to the Gateway Server

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants