From 6ef52cb5a3c6250507c50847603a9fdb9f57b831 Mon Sep 17 00:00:00 2001 From: Nora Abi Akar Date: Tue, 1 Nov 2022 15:44:09 +0100 Subject: [PATCH] Add preliminary beaconing support (#1935) ## What is being addressed - Support parsing and writing json router configurations with beaconing settings. - Respond to `timesync` messages from basics station with corresponding `timesync` message including GPS time. ## How is this addressed - `LnsStationConfiguration.RouterConfigurationConverter` works with optional `bcning` settings in the device twins. - `LnsProtocolMessageProcessor.HandleDataMessageAsync` responds to messages of type `LnsMessageType.TimeSync`. Co-authored-by: Mikhail Chatillon Co-authored-by: Nora Abi Akar --- .../BasicsStation/JsonHandlers/Beaconing.cs | 20 +++ .../JsonHandlers/LnsStationConfiguration.cs | 37 ++++- .../JsonHandlers/TimeSyncMessage.cs | 20 +++ .../Processors/LnsProtocolMessageProcessor.cs | 9 +- .../LnsStationConfigurationTests.cs | 149 +++++++++++++++++- .../LnsProtocolMessageProcessorTests.cs | 46 ++++++ 6 files changed, 270 insertions(+), 11 deletions(-) create mode 100644 LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/Beaconing.cs create mode 100644 LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/TimeSyncMessage.cs diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/Beaconing.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/Beaconing.cs new file mode 100644 index 000000000..87a1b2323 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/Beaconing.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.NetworkServer.BasicsStation.JsonHandlers +{ + internal class Beaconing + { + + public Beaconing(uint dR, uint[] layout, uint[] freqs) + { + DR = dR; + this.Layout = layout; + this.Freqs = freqs; + } + + public uint DR { get; set; } + public uint[] Layout { get; set; } + public uint[] Freqs { get; set; } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/LnsStationConfiguration.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/LnsStationConfiguration.cs index 541c41ab6..e131ed9a6 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/LnsStationConfiguration.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/LnsStationConfiguration.cs @@ -190,9 +190,17 @@ namespace LoRaWan.NetworkServer.BasicsStation.JsonHandlers JsonReader.Property("nocca", JsonReader.Boolean()), JsonReader.Property("nodc", JsonReader.Boolean()), JsonReader.Property("nodwell", JsonReader.Boolean()), - (netId, joinEui, region, hwspec, freqRange, drs, sx1301conf, nocca, nodc, nodwell) => + JsonReader.Property("bcning", + JsonReader.Object( + JsonReader.Property("DR", JsonReader.UInt32()), + JsonReader.Property("layout", JsonReader.Array(JsonReader.UInt32())), + JsonReader.Property("freqs", JsonReader.Array(JsonReader.UInt32())), + (dRs, layout, freqs) => new Beaconing(dRs, layout, freqs)), + (true, null)), + (netId, joinEui, region, hwspec, freqRange, drs, sx1301conf, nocca, nodc, nodwell, bcning) => WriteRouterConfig(netId, joinEui, region, hwspec, freqRange, drs, - sx1301conf, nocca, nodc, nodwell)); + sx1301conf, nocca, nodc, nodwell, bcning)); + private static readonly IJsonReader RegionConfigurationConverter = JsonReader.Object(JsonReader.Property("region", from s in JsonReader.String() @@ -230,6 +238,7 @@ namespace LoRaWan.NetworkServer.BasicsStation.JsonHandlers "nocca" : BOOL "nodc" : BOOL "nodwell" : BOOL + "bcning" : { "DR": INT, "layout": [INT,INT, ..], "freqs": [INT,INT,..] } } */ @@ -244,7 +253,7 @@ namespace LoRaWan.NetworkServer.BasicsStation.JsonHandlers (Hertz Min, Hertz Max) freqRange, IEnumerable<(SpreadingFactor SpreadingFactor, Bandwidth Bandwidth, bool DnOnly)> dataRates, Sx1301Config[] sx1301Config, - bool nocca, bool nodc, bool nodwell) + bool nocca, bool nodc, bool nodwell, Beaconing bcning) { if (string.IsNullOrEmpty(region)) throw new JsonException("Region must not be null."); if (string.IsNullOrEmpty(hwspec)) throw new JsonException("hwspec must not be null."); @@ -332,6 +341,28 @@ namespace LoRaWan.NetworkServer.BasicsStation.JsonHandlers writer.WriteBoolean("nodc", nodc); writer.WriteBoolean("nodwell", nodwell); + if (bcning != null) + { + writer.WritePropertyName("bcning"); // start beaconing + + writer.WriteStartObject(); + writer.WriteNumber("DR", bcning.DR); + writer.WritePropertyName("layout"); + writer.WriteStartArray(); + foreach (var layout in bcning.Layout) + { + writer.WriteNumberValue(layout); + } + writer.WriteEndArray(); + writer.WritePropertyName("freqs"); + writer.WriteStartArray(); + foreach (var freq in bcning.Freqs) + { + writer.WriteNumberValue(freq); + } + writer.WriteEndArray(); + writer.WriteEndObject(); // end beaconing + } writer.WriteEndObject(); writer.Flush(); diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/TimeSyncMessage.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/TimeSyncMessage.cs new file mode 100644 index 000000000..ffaebc177 --- /dev/null +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/JsonHandlers/TimeSyncMessage.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace LoRaWan.NetworkServer.BasicsStation.JsonHandlers +{ + using System.Text.Json.Serialization; + using LoRaWan.NetworkServer.BasicsStation; + + internal class TimeSyncMessage + { + [JsonPropertyName("txtime")] + public ulong TxTime { get; set; } + + [JsonPropertyName("gpstime")] + public ulong GpsTime { get; set; } + + [JsonPropertyName("msgtype")] + public string MsgType { get; set; } + } +} diff --git a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/Processors/LnsProtocolMessageProcessor.cs b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/Processors/LnsProtocolMessageProcessor.cs index 72d672066..6320056dd 100644 --- a/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/Processors/LnsProtocolMessageProcessor.cs +++ b/LoRaEngine/modules/LoRaWanNetworkSrvModule/LoRaWan.NetworkServer/BasicsStation/Processors/LnsProtocolMessageProcessor.cs @@ -37,6 +37,8 @@ namespace LoRaWan.NetworkServer.BasicsStation.Processors private readonly Counter uplinkMessageCounter; private readonly Counter unhandledExceptionCount; + public static readonly DateTime GpsEpoch = new DateTime(1980, 1, 6, 0, 0, 0, DateTimeKind.Utc); + public LnsProtocolMessageProcessor(IBasicsStationConfigurationService basicsStationConfigurationService, WebSocketWriterRegistry socketWriterRegistry, IDownstreamMessageSender downstreamMessageSender, @@ -196,9 +198,14 @@ namespace LoRaWan.NetworkServer.BasicsStation.Processors break; case var messageType and (LnsMessageType.DownlinkMessage or LnsMessageType.RouterConfig): throw new NotSupportedException($"'{messageType}' is not a valid message type for this endpoint and is only valid for 'downstream' messages."); + case LnsMessageType.TimeSync: + var timeSyncData = JsonSerializer.Deserialize(json); + LogReceivedMessage(this.logger, "TimeSync", json, null); + timeSyncData.GpsTime = (ulong)DateTime.UtcNow.Subtract(GpsEpoch).TotalMilliseconds * 1000; // to microseconds + await socket.SendAsync(JsonSerializer.Serialize(timeSyncData), cancellationToken); + break; case var messageType and (LnsMessageType.ProprietaryDataFrame or LnsMessageType.MulticastSchedule - or LnsMessageType.TimeSync or LnsMessageType.RunCommand or LnsMessageType.RemoteShell): this.logger.LogWarning("'{MessageType}' ({MessageTypeBasicStationString}) is not handled in current LoRaWan Network Server implementation.", messageType, messageType.ToBasicStationString()); diff --git a/Tests/Unit/NetworkServer/JsonHandlers/LnsStationConfigurationTests.cs b/Tests/Unit/NetworkServer/JsonHandlers/LnsStationConfigurationTests.cs index 18347a5e6..bc9934995 100644 --- a/Tests/Unit/NetworkServer/JsonHandlers/LnsStationConfigurationTests.cs +++ b/Tests/Unit/NetworkServer/JsonHandlers/LnsStationConfigurationTests.cs @@ -19,7 +19,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer.BasicsStation.JsonHandlers public class LnsStationConfigurationTests { - internal static string ValidStationConfiguration = + internal static readonly string ValidStationConfiguration = GetTwinConfigurationJson(new[] { new NetId(1) }, new[] { (new JoinEui(ulong.MinValue), new JoinEui(ulong.MaxValue)) }, "EU863", @@ -36,7 +36,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer.BasicsStation.JsonHandlers }, flags: RouterConfigStationFlags.NoClearChannelAssessment | RouterConfigStationFlags.NoDutyCycle | RouterConfigStationFlags.NoDwellTimeLimitations); - internal static string ValidRouterConfigMessage = JsonUtil.Strictify(@"{ + internal static readonly string ValidRouterConfigMessage = JsonUtil.Strictify(/*lang=json*/ @"{ 'msgtype': 'router_config', 'NetID': [1], 'JoinEui': [[0, 18446744073709551615]], @@ -123,7 +123,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer.BasicsStation.JsonHandlers public void WriteRouterConfig_WithEmptyOrNullJoinEuiFilter(int? JoinEuiCount) { // arrange - var expected = JsonUtil.Strictify(@"{ + var expected = JsonUtil.Strictify(/*lang=json*/ @"{ 'msgtype': 'router_config', 'NetID': [1], 'JoinEui': [], @@ -256,6 +256,125 @@ namespace LoRaWan.Tests.Unit.NetworkServer.BasicsStation.JsonHandlers Assert.Equal(JsonUtil.Minify(ValidRouterConfigMessage), actual); } + [Fact] + public void WriteRouterConfigWithBcning() + { + var ValidStationConfigurationWithBcning = + GetTwinConfigurationJson(new[] { new NetId(1) }, + new[] { (new JoinEui(ulong.MinValue), new JoinEui(ulong.MaxValue)) }, + "EU863", + "sx1301/1", + (new Hertz(863000000), new Hertz(870000000)), + new[] + { + (SF11, BW125, false), + (SF10, BW125, false), + (SF9 , BW125, false), + (SF8 , BW125, false), + (SF7 , BW125, false), + (SF7 , BW250, false), + }, + includeBcning: true, + flags: RouterConfigStationFlags.NoClearChannelAssessment | RouterConfigStationFlags.NoDutyCycle | RouterConfigStationFlags.NoDwellTimeLimitations) ; + + var ValidRouterConfigMessageWithBcning = JsonUtil.Strictify(@"{ + 'msgtype': 'router_config', + 'NetID': [1], + 'JoinEui': [[0, 18446744073709551615]], + 'region': 'EU863', + 'hwspec': 'sx1301/1', + 'freq_range': [ 863000000, 870000000 ], + 'DRs': [ [ 11, 125, 0 ], + [ 10, 125, 0 ], + [ 9, 125, 0 ], + [ 8, 125, 0 ], + [ 7, 125, 0 ], + [ 7, 250, 0 ] ], + 'sx1301_conf': [ + { + 'radio_0': { + 'enable': true, + 'freq': 867500000 + }, + 'radio_1': { + 'enable': true, + 'freq': 868500000 + }, + 'chan_FSK': { + 'enable': true, + 'radio': 1, + 'if': 300000 + }, + 'chan_Lora_std': { + 'enable': true, + 'radio': 1, + 'if': -200000, + 'bandwidth': 250000, + 'spread_factor': 7 + }, + 'chan_multiSF_0': { + 'enable': true, + 'radio': 1, + 'if': -400000 + }, + 'chan_multiSF_1': { + 'enable': true, + 'radio': 1, + 'if': -200000 + }, + 'chan_multiSF_2': { + 'enable': true, + 'radio': 1, + 'if': 0 + }, + 'chan_multiSF_3': { + 'enable': true, + 'radio': 0, + 'if': -400000 + }, + 'chan_multiSF_4': { + 'enable': true, + 'radio': 0, + 'if': -200000 + }, + 'chan_multiSF_5': { + 'enable': true, + 'radio': 0, + 'if': 0 + }, + 'chan_multiSF_6': { + 'enable': true, + 'radio': 0, + 'if': 200000 + }, + 'chan_multiSF_7': { + 'enable': true, + 'radio': 0, + 'if': 400000 + } + } + ], + 'nocca': true, + 'nodc': true, + 'nodwell': true, + 'bcning': { + 'DR': 3, + 'layout': [ + 2, + 8, + 17 + ], + 'freqs': [ + 869525000 + ] + }}"); + // act + var actual = LnsStationConfiguration.GetConfiguration(ValidStationConfigurationWithBcning); + + // assert + Assert.Equal(JsonUtil.Minify(ValidRouterConfigMessageWithBcning), actual); + } + [Fact] public void WriteRouterConfig_ThrowsArgumentException_WithInvalidFrequencyRange() { @@ -322,7 +441,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer.BasicsStation.JsonHandlers [Theory] [InlineData("null")] [InlineData("[]")] - [InlineData(@"[{ ""radio_0"": { ""enable"": true, ""freq"": 867500000 } }]")] + [InlineData(/*lang=json,strict*/ @"[{ ""radio_0"": { ""enable"": true, ""freq"": 867500000 } }]")] public void WriteRouterConfig_Throws_WhenInvalidSx1301Conf(string sx1301Conf) { // arrange @@ -345,9 +464,10 @@ namespace LoRaWan.Tests.Unit.NetworkServer.BasicsStation.JsonHandlers (Hertz Min, Hertz Max) freqRange, IEnumerable<(SpreadingFactor SpreadingFactor, Bandwidth Bandwidth, bool DnOnly)> dataRates, string sx1301Conf = null, + bool includeBcning = false, RouterConfigStationFlags flags = RouterConfigStationFlags.None) { - var defaultSx1301Conf = JsonUtil.Strictify(@"[{ + var defaultSx1301Conf = JsonUtil.Strictify(/*lang=json*/ @"[{ 'radio_0': { 'enable': true, 'freq': 867500000 @@ -410,7 +530,20 @@ namespace LoRaWan.Tests.Unit.NetworkServer.BasicsStation.JsonHandlers } }]"); - const string template = @"{{ + var defaultBcning = JsonUtil.Strictify(/*lang=json*/ @"{ + 'DR': 3, + 'layout': [ + 2, + 8, + 17 + ], + 'freqs': [ + 869525000 + ] + }"); + + const string template = @"{ + { ""msgtype"": ""router_config"", ""NetID"": {0}, ""JoinEui"": {1}, @@ -422,6 +555,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer.BasicsStation.JsonHandlers ""nocca"": {7}, ""nodc"": {8}, ""nodwell"": {9} + {10} }}"; static string Serialize(object obj) => JsonSerializer.Serialize(obj); @@ -435,7 +569,8 @@ namespace LoRaWan.Tests.Unit.NetworkServer.BasicsStation.JsonHandlers sx1301Conf ?? defaultSx1301Conf, Serialize((flags & RouterConfigStationFlags.NoClearChannelAssessment) == RouterConfigStationFlags.NoClearChannelAssessment), Serialize((flags & RouterConfigStationFlags.NoDutyCycle) == RouterConfigStationFlags.NoDutyCycle), - Serialize((flags & RouterConfigStationFlags.NoDwellTimeLimitations) == RouterConfigStationFlags.NoDwellTimeLimitations)); + Serialize((flags & RouterConfigStationFlags.NoDwellTimeLimitations) == RouterConfigStationFlags.NoDwellTimeLimitations), + includeBcning? ",\"bcning\": "+defaultBcning: ""); } [Fact] diff --git a/Tests/Unit/NetworkServer/LnsProtocolMessageProcessorTests.cs b/Tests/Unit/NetworkServer/LnsProtocolMessageProcessorTests.cs index 477cab380..c1e8bcb33 100644 --- a/Tests/Unit/NetworkServer/LnsProtocolMessageProcessorTests.cs +++ b/Tests/Unit/NetworkServer/LnsProtocolMessageProcessorTests.cs @@ -8,6 +8,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer using System.Net; using System.Net.WebSockets; using System.Text; + using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Common; @@ -16,6 +17,7 @@ namespace LoRaWan.Tests.Unit.NetworkServer using global::LoRaTools.Regions; using LoRaWan.NetworkServer; using LoRaWan.NetworkServer.BasicsStation; + using LoRaWan.NetworkServer.BasicsStation.JsonHandlers; using LoRaWan.NetworkServer.BasicsStation.Processors; using LoRaWan.Tests.Unit.LoRaTools; using Microsoft.AspNetCore.Http; @@ -190,11 +192,55 @@ namespace LoRaWan.Tests.Unit.NetworkServer CancellationToken.None); // assert + Assert.NotNull(sentType); + Assert.NotNull(sentEnd); Assert.Contains(expectedSubstring, sentString, StringComparison.Ordinal); Assert.Equal(WebSocketMessageType.Text, sentType.Value); Assert.True(sentEnd.Value); } + [Fact] + public async Task InternalHandleDataAsync_ShouldSendExpectedJsonResponseType_ForTimeSyncMessage() + { + // arrange + var receievedMsgType = "timesync"; + ulong receivedTxTime = 1023024197; + var minimumExpectedGpsTime = (ulong)DateTime.UtcNow.AddMinutes(-10) + .Subtract(LnsProtocolMessageProcessor.GpsEpoch).TotalMilliseconds * 1000; + var maximumExpectedGpsTime = (ulong)DateTime.UtcNow.AddMinutes(10) + .Subtract(LnsProtocolMessageProcessor.GpsEpoch).TotalMilliseconds * 1000; + + InitializeConfigurationServiceMock(); + SetDataPathParameter(); + + SetupSocketReceiveAsync("{ msgtype: '" + receievedMsgType + "', txtime: " + receivedTxTime + "}"); + + // intercepting the SendAsync to verify that what we sent is actually what we expected + var sentString = string.Empty; + WebSocketMessageType? sentType = null; + bool? sentEnd = null; + this.socketMock.Setup(x => x.SendAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback, WebSocketMessageType, bool, CancellationToken>((message, type, end, _) => + { + sentString = Encoding.UTF8.GetString(message); + sentType = type; + sentEnd = end; + }); + + // act + await this.lnsMessageProcessorMock.InternalHandleDataAsync(this.httpContextMock.Object.Request.RouteValues, + this.socketMock.Object, + CancellationToken.None); + + // assert + var sentJson = JsonSerializer.Deserialize(sentString); + Assert.Equal(sentJson.MsgType, receievedMsgType); + Assert.Equal(sentJson.TxTime, receivedTxTime); + Assert.True(sentJson.GpsTime > minimumExpectedGpsTime); + Assert.True(sentJson.GpsTime < maximumExpectedGpsTime); + Assert.Equal(WebSocketMessageType.Text, sentType.Value); + Assert.True(sentEnd.Value); + } [Theory] [InlineData(LnsMessageType.ProprietaryDataFrame)]