Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ For details about compatibility between different releases, see the **Commitment

### Fixed

- The timestamp of the udp packet is now always correct when the 'Schedule downlink late' is enabled for the gateway and downlink scheduling hits the duty cycle limit.

### Security

## [3.35.2] - 2026-01-30
Expand Down
7 changes: 6 additions & 1 deletion pkg/gatewayserver/io/udp/udp.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,11 @@ func (s *srv) handleDown(ctx context.Context, st *state) error {
// TODO: Report to Network Server: https://github.com/TheThingsNetwork/lorawan-stack/issues/76
break
}
concentratorTime, err := encoding.ConcentratorTimestampFromDownlinkMessage(down)
if err != nil {
logger.WithError(err).Warn("Failed to get concentrator timestamp from downlink message")
break
}
downlinkPath := st.lastDownlinkPath.Load()
if downlinkPath == nil {
logger.Debug("Received downlink message without an active downlink path")
Expand Down Expand Up @@ -521,7 +526,7 @@ func (s *srv) handleDown(ctx context.Context, st *state) error {
write()
break
}
serverTime := st.clock.ToServerTime(st.clock.FromTimestampTime(tx.Tmst))
serverTime := st.clock.ToServerTime(scheduling.ConcentratorTime(concentratorTime))
st.clockMu.RUnlock()
d := time.Until(serverTime.Add(-s.config.ScheduleLateTime))
logger.WithField("duration", d).Debug("Wait to schedule downlink message late")
Expand Down
9 changes: 9 additions & 0 deletions pkg/ttnpb/udp/translation.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,3 +441,12 @@ func FromDownlinkMessage(msg *ttnpb.DownlinkMessage) (*TxPacket, error) {
}
return tx, nil
}

// ConcentratorTimestampFromDownlinkMessage gets the concentrator timestamp from the downlink message.
func ConcentratorTimestampFromDownlinkMessage(msg *ttnpb.DownlinkMessage) (int64, error) {
scheduled := msg.GetScheduled()
if scheduled == nil {
return 0, errNotScheduled.New()
}
return scheduled.ConcentratorTimestamp, nil
}
31 changes: 31 additions & 0 deletions pkg/ttnpb/udp/translation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -574,3 +574,34 @@ func TestDownlinkRoundtrip(t *testing.T) {

a.So(actual, should.HaveEmptyDiff, expected)
}

func TestConcentratorTimestampFromDownlinkMessage(t *testing.T) {
t.Parallel()
a := assertions.New(t)

msg := &ttnpb.DownlinkMessage{
Settings: &ttnpb.DownlinkMessage_Scheduled{
Scheduled: &ttnpb.TxSettings{
Frequency: 925700000,
DataRate: &ttnpb.DataRate{
Modulation: &ttnpb.DataRate_Lora{
Lora: &ttnpb.LoRaDataRate{
SpreadingFactor: 10,
Bandwidth: 500000,
},
},
},
Downlink: &ttnpb.TxSettings_Downlink{
TxPower: 20,
InvertPolarization: true,
},
Timestamp: 3427261656,
ConcentratorTimestamp: 11020007658000,
},
},
RawPayload: []byte{0x7d, 0xf3, 0x8e},
}
timestamp, err := udp.ConcentratorTimestampFromDownlinkMessage(msg)
a.So(err, should.BeNil)
a.So(timestamp, should.Equal, 11020007658000)
}
Loading