diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c49c16e0a..01622bbace 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/pkg/gatewayserver/io/udp/udp.go b/pkg/gatewayserver/io/udp/udp.go index 56705d2dcf..24758c9ab5 100644 --- a/pkg/gatewayserver/io/udp/udp.go +++ b/pkg/gatewayserver/io/udp/udp.go @@ -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") @@ -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") diff --git a/pkg/ttnpb/udp/translation.go b/pkg/ttnpb/udp/translation.go index d2eb31120b..928616722d 100644 --- a/pkg/ttnpb/udp/translation.go +++ b/pkg/ttnpb/udp/translation.go @@ -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 +} diff --git a/pkg/ttnpb/udp/translation_test.go b/pkg/ttnpb/udp/translation_test.go index 307e865573..e9b86c48e2 100644 --- a/pkg/ttnpb/udp/translation_test.go +++ b/pkg/ttnpb/udp/translation_test.go @@ -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) +}