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:
Nora Abi Akar 2022-11-01 15:44:09 +01:00 коммит произвёл GitHub
Родитель 2b35a0e13f
Коммит 6ef52cb5a3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 270 добавлений и 11 удалений

Просмотреть файл

@ -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)]