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 <mikhail.chatillon@hotmail.com> Co-authored-by: Nora Abi Akar <noraabiakar@microsoft.com>
This commit is contained in:
Родитель
2b35a0e13f
Коммит
6ef52cb5a3
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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<Region> 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();
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -37,6 +37,8 @@ namespace LoRaWan.NetworkServer.BasicsStation.Processors
|
|||
private readonly Counter<int> uplinkMessageCounter;
|
||||
private readonly Counter<int> unhandledExceptionCount;
|
||||
|
||||
public static readonly DateTime GpsEpoch = new DateTime(1980, 1, 6, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
public LnsProtocolMessageProcessor(IBasicsStationConfigurationService basicsStationConfigurationService,
|
||||
WebSocketWriterRegistry<StationEui, string> 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<TimeSyncMessage>(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());
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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<ArraySegment<byte>>(), It.IsAny<WebSocketMessageType>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<ArraySegment<byte>, 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<TimeSyncMessage>(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)]
|
||||
|
|
Загрузка…
Ссылка в новой задаче