Add integration test and Http based payload decoders (#28)

* Initial refactoring of the LoRa low level library
* Added adapter message classes to avoid casts
* Split up library between multiple files
* Upgraded Runtime to .net core 2.1 to take advantage of new features
* Started migration to memory
* Leaner message processing for better performance

* fixed pktfwd region deployment

* Added changes related to server side and to make the simulator working woth the new refactoring classes

* fixed second RX windows, enabled local testing and other bugs

* changed gitignore to prevent .vscode folder from commiting

* Initial pipeline commit

* Initial pipeline commit

* Add deployment test template

* Use PKT_FWD_VERSION and NET_SRV_VERSION as version variables

* Fix deployment test json for Azure DevOps

* Remove mic check

* Add invalid devEUI integration test

* Switch device client connection AMQP

* Add integration test otaa, abp

* Add C2D message tests

* Set GatewayID in test devices based on IOTEDGE_DEVICEID env var

* Commit before merge

* Add C2D message test
Add Http based decoders

* Quote value decoder
This commit is contained in:
Francisco Beltrao 2018-11-21 09:42:32 +01:00 коммит произвёл ronniesa
Родитель 6572d8d6ab
Коммит 1f6fdd4b37
39 изменённых файлов: 3225 добавлений и 45 удалений

3
.gitignore поставляемый
Просмотреть файл

@ -291,3 +291,6 @@ Doc
.vscode/
/LoRaEngine/modules/LoRaSimulator/Logger
/LoRaEngine/modules/LoRaSimulator/LoraTools
# local settings file
appsettings.local.json

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

@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoRaWanTest", "modules\LoRa
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logger", "modules\LoRaWanNetworkSrvModule\Logger\Logger.csproj", "{0F8CDB3B-34C9-4C14-B4B2-34BCAD232D45}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoRaWan.IntegrationTest", "test\LoRaWan.IntegrationTest\LoRaWan.IntegrationTest.csproj", "{26DF2637-43E0-4FFF-92BD-F4395C88DD50}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -71,6 +73,14 @@ Global
{0F8CDB3B-34C9-4C14-B4B2-34BCAD232D45}.Release|Any CPU.Build.0 = Release|Any CPU
{0F8CDB3B-34C9-4C14-B4B2-34BCAD232D45}.Release|x86.ActiveCfg = Release|Any CPU
{0F8CDB3B-34C9-4C14-B4B2-34BCAD232D45}.Release|x86.Build.0 = Release|Any CPU
{26DF2637-43E0-4FFF-92BD-F4395C88DD50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{26DF2637-43E0-4FFF-92BD-F4395C88DD50}.Debug|Any CPU.Build.0 = Debug|Any CPU
{26DF2637-43E0-4FFF-92BD-F4395C88DD50}.Debug|x86.ActiveCfg = Debug|Any CPU
{26DF2637-43E0-4FFF-92BD-F4395C88DD50}.Debug|x86.Build.0 = Debug|Any CPU
{26DF2637-43E0-4FFF-92BD-F4395C88DD50}.Release|Any CPU.ActiveCfg = Release|Any CPU
{26DF2637-43E0-4FFF-92BD-F4395C88DD50}.Release|Any CPU.Build.0 = Release|Any CPU
{26DF2637-43E0-4FFF-92BD-F4395C88DD50}.Release|x86.ActiveCfg = Release|Any CPU
{26DF2637-43E0-4FFF-92BD-F4395C88DD50}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

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

@ -86,7 +86,7 @@
"version": "1.0",
"env": {
"LOG_LEVEL": {
"value": "1"
"value": "2"
},
"LOG_TO_HUB": {
"value": "true"
@ -145,4 +145,4 @@
}
}
}
}
}

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

@ -0,0 +1,133 @@
{
"moduleContent": {
"$edgeAgent": {
"properties.desired": {
"schemaVersion": "1.0",
"runtime": {
"type": "docker",
"settings": {
"minDockerVersion": "v1.25",
"loggingOptions": "",
"registryCredentials": {
"$CONTAINER_REGISTRY_USERNAME": {
"username": "$CONTAINER_REGISTRY_USERNAME",
"password": "$CONTAINER_REGISTRY_PASSWORD",
"address": "$CONTAINER_REGISTRY_ADDRESS"
}
}
}
},
"systemModules": {
"edgeAgent": {
"type": "docker",
"settings": {
"image": "mcr.microsoft.com/azureiotedge-agent:1.0.4",
"createOptions": ""
}
},
"edgeHub": {
"type": "docker",
"status": "running",
"restartPolicy": "always",
"settings": {
"image": "mcr.microsoft.com/azureiotedge-hub:1.0.4",
"createOptions": "{\"HostConfig\":{\"PortBindings\":{\"8883/tcp\":[{\"HostPort\":\"8883\"}],\"443/tcp\":[{\"HostPort\":\"443\"}]}}}"
},
"env": {
"OptimizeForPerformance": {
"value": "false"
}
}
}
},
"modules": {
"sensordecodermodule":{
"type": "docker",
"settings": {
"image": "fbeltrao/decodersample:1.0-arm32v7",
"createOptions": {}
},
"status": "running",
"restartPolicy": "always",
"version": "1.0"
},
"LoRaWanNetworkSrvModule": {
"type": "docker",
"settings": {
"image": "${MODULES.LoRaWanNetworkSrvModule.arm32v7}",
"createOptions": "{ \"ExposedPorts\": { \"1680/udp\": {}, \"1234/udp\": {} }, \"HostConfig\": { \"PortBindings\": { \"1680/udp\": [ { \"HostPort\": \"1680\", \"HostIp\":\"172.17.0.1\" } ], \"1234/udp\": [ { \"HostPort\": \"1234\" } ] } }}"
},
"version": "1.0",
"env": {
"LOG_LEVEL": {
"value": "0"
},
"LOG_TO_HUB": {
"value": "true"
},
"LOG_TO_TEST": {
"value": "true"
}
},
"status": "running",
"restartPolicy": "always"
},
"LoRaWanPktFwdModule": {
"type": "docker",
"settings": {
"image": "${MODULES.LoRaWanPktFwdModule.arm32v7}",
"createOptions": " {\r\n \"HostConfig\": {\r\n \"NetworkMode\": \"host\", \"Privileged\": true \r\n\r\n },\r\n \"NetworkingConfig\": {\r\n \"EndpointsConfig\": {\r\n \"host\": {}\r\n }\r\n }\r\n}\r\n \r\n \r\n \r\n"
},
"env": {
"RESET_PIN": {
"value": "7"
},
"REGION": {
"value": "$REGION"
}
},
"version": "1.0",
"status": "running",
"restartPolicy": "always"
},
"AzureDevOpsAgent": {
"version": "1.0",
"type": "docker",
"status": "running",
"restartPolicy": "always",
"settings": {
"image": "$CONTAINER_REGISTRY_ADDRESS/azuredevopsagent:0.0.1-arm32v7",
"createOptions": "{ \"HostConfig\": { \"Privileged\": true } }"
},
"env": {
"VSTS_SERVER_URL": {
"value": "$VSTS_SERVER_URL"
},
"VSTS_TOKEN": {
"value": "$VSTS_TOKEN"
}
}
}
}
}
},
"$edgeHub": {
"properties.desired": {
"schemaVersion": "1.0",
"routes": {
"route": "FROM /* INTO $upstream"
},
"storeAndForwardConfiguration": {
"timeToLiveSecs": 7200
}
}
},
"LoRaWanNetworkSrvModule": {
"properties.desired": {
"schemaVersion": "1.0",
"FacadeServerUrl": "$FACADE_SERVER_URL",
"FacadeAuthCode": "$FACADE_AUTH_CODE"
}
}
}
}

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

@ -148,7 +148,6 @@ namespace LoRaWan.NetworkServer
{
try
{
string partConnection = createIoTHubConnectionString();
string deviceConnectionStr = $"{partConnection}DeviceId={DevEUI};SharedAccessKey={PrimaryKey}";

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

@ -2,54 +2,126 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Web;
namespace LoRaWan.NetworkServer
{
class LoraDecoders
{
public static string DecodeMessage(byte[] payload, uint fport, string SensorDecoder)
public static async Task<JObject> DecodeMessage(byte[] payload, uint fport, string SensorDecoder)
{
Type decoderType = typeof(LoraDecoders);
MethodInfo toInvoke = decoderType.GetMethod(
SensorDecoder, BindingFlags.Static | BindingFlags.NonPublic);
string result;
var base64Payload = Convert.ToBase64String(payload);
if (toInvoke != null)
// Call local decoder (no "http://" in SensorDecoder)
if (!SensorDecoder.Contains("http://"))
{
return (string)toInvoke.Invoke(null, new object[] { payload, fport});
Type decoderType = typeof(LoraDecoders);
MethodInfo toInvoke = decoderType.GetMethod(
SensorDecoder, BindingFlags.Static | BindingFlags.NonPublic);
if (toInvoke != null)
{
result = (string)toInvoke.Invoke(null, new object[] { payload, fport });
}
else
{
result = $"{{\"error\": \"No '{SensorDecoder}' decoder found\", \"rawpayload\": \"{base64Payload}\"}}";
}
}
// Call SensorDecoderModule hosted in seperate container ("http://" in SensorDecoder)
// Format: http://containername/api/decodername
else
{
var base64Payload = Convert.ToBase64String(payload);
return $"{{\"error\": \"No '{SensorDecoder}' decoder found\", \"rawpayload\": \"{base64Payload}\"}}";
string toCall = SensorDecoder;
if (SensorDecoder.EndsWith("/"))
{
toCall = SensorDecoder.Substring(0, SensorDecoder.Length - 1);
}
// use HttpUtility to UrlEncode Fport and payload
string fportEncoded = HttpUtility.UrlEncode(fport.ToString());
string payloadEncoded = HttpUtility.UrlEncode(Encoding.ASCII.GetString(payload));
// Add Fport and Payload to URL
toCall = $"{toCall}?fport={fportEncoded}&payload={payloadEncoded}";
// Call SensorDecoderModule
result = await CallSensorDecoderModule(toCall, payload);
}
JObject resultJson;
// Verify that result is valid JSON.
try {
resultJson = JObject.Parse(result);
}
catch
{
resultJson = JObject.Parse($"{{\"error\": \"Invalid JSON returned from '{SensorDecoder}'\", \"rawpayload\": \"{base64Payload}\"}}");
}
return resultJson;
}
private static string DecoderGpsSensor(byte[] payload, uint fport)
private static async Task<string> CallSensorDecoderModule(string sensorDecoderModuleUrl, byte[] payload)
{
var result = Encoding.ASCII.GetString(payload);
string[] values = result.Split(':');
return String.Format("{{\"latitude\": {0} , \"longitude\": {1}}}", values[0], values[1]);
}
private static string DecoderTemperatureSensor(byte[] payload, uint fport)
{
var result = Encoding.ASCII.GetString(payload);
return String.Format("{{\"temperature\": {0}}}", result);
var base64Payload = Convert.ToBase64String(payload);
string result = "";
try
{
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Connection", "Keep-Alive");
client.DefaultRequestHeaders.Add("Keep-Alive", "timeout=86400");
HttpResponseMessage response = await client.GetAsync(sensorDecoderModuleUrl);
if (!response.IsSuccessStatusCode)
{
var badReqResult = await response.Content.ReadAsStringAsync();
result = JsonConvert.SerializeObject(new {
error = $"SensorDecoderModule '{sensorDecoderModuleUrl}' returned bad request.",
exceptionMessage = badReqResult ?? string.Empty,
rawpayload = base64Payload
});
}
else
{
result = await response.Content.ReadAsStringAsync();
}
}
catch (Exception ex)
{
Logger.Log($"Error in decoder handling: {ex.ToString()}", Logger.LoggingLevel.Error);
result = JsonConvert.SerializeObject(new {
error = $"Call to SensorDecoderModule '{sensorDecoderModuleUrl}' failed.",
exceptionMessage = ex.ToString(),
rawpayload = base64Payload
});
}
Logger.Log($"Result from {sensorDecoderModuleUrl} = {result}", Logger.LoggingLevel.Info);
return result;
}
private static string DecoderValueSensor(byte[] payload, uint fport)
{
var result = Encoding.ASCII.GetString(payload);
return String.Format("{{\"value\": {0}}}", result);
var result = Encoding.ASCII.GetString(payload);
return $"{{ \"value\":{result} }}";
}
}

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

@ -38,7 +38,7 @@ namespace LoRaWan.NetworkServer
}
string fcntDownString = response.Content.ReadAsStringAsync().Result;
string fcntDownString = await response.Content.ReadAsStringAsync();
//todo ronnie check for fcnt above ushort
ushort newFCntDown = ushort.Parse(fcntDownString);
@ -79,7 +79,7 @@ namespace LoRaWan.NetworkServer
return null;
}
var result = response.Content.ReadAsStringAsync().Result;
var result = await response.Content.ReadAsStringAsync();
//TODO enable multi devices with the same devaddr
List<IoTHubDeviceInfo> iotHubDeviceInfos = ((List<IoTHubDeviceInfo>)JsonConvert.DeserializeObject(result, typeof(List<IoTHubDeviceInfo>)));
@ -210,7 +210,7 @@ namespace LoRaWan.NetworkServer
{
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
{
var badReqResult = response.Content.ReadAsStringAsync().Result;
var badReqResult = await response.Content.ReadAsStringAsync();
if (!String.IsNullOrEmpty(badReqResult) && badReqResult == "UsedDevNonce")
{
@ -224,7 +224,7 @@ namespace LoRaWan.NetworkServer
return null;
}
var result = response.Content.ReadAsStringAsync().Result;
var result = await response.Content.ReadAsStringAsync();
List<IoTHubDeviceInfo> iotHubDeviceInfos = ((List<IoTHubDeviceInfo>)JsonConvert.DeserializeObject(result, typeof(List<IoTHubDeviceInfo>)));
if (iotHubDeviceInfos.Count == 0)

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

@ -158,17 +158,13 @@ namespace LoRaWan.NetworkServer
if (!isAckFromDevice)
{
if (String.IsNullOrEmpty(loraDeviceInfo.SensorDecoder))
{
jsonDataPayload = Convert.ToBase64String(decryptedMessage);
fullPayload.data = jsonDataPayload;
}
else
{
Logger.Log(loraDeviceInfo.DevEUI, $"decoding with: {loraDeviceInfo.SensorDecoder} port: {fportUp}", Logger.LoggingLevel.Info);
jsonDataPayload = LoraDecoders.DecodeMessage(decryptedMessage, fportUp, loraDeviceInfo.SensorDecoder);
fullPayload.data = JObject.Parse(jsonDataPayload);
}
jsonDataPayload = Convert.ToBase64String(decryptedMessage);
fullPayload.data = jsonDataPayload;
}
else
{
Logger.Log(loraDeviceInfo.DevEUI, $"decoding with: {loraDeviceInfo.SensorDecoder} port: {fportUp}", Logger.LoggingLevel.Info);
fullPayload.data = await LoraDecoders.DecodeMessage(decryptedMessage, fportUp, loraDeviceInfo.SensorDecoder);
}
fullPayload.eui = loraDeviceInfo.DevEUI;
@ -193,6 +189,7 @@ namespace LoRaWan.NetworkServer
}
string iotHubMsg = fullPayload.ToString(Newtonsoft.Json.Formatting.None);
await loraDeviceInfo.HubSender.SendMessageAsync(iotHubMsg, messageProperties);
if (isAckFromDevice)
{
Logger.Log(loraDeviceInfo.DevEUI, $"ack from device sent to hub", Logger.LoggingLevel.Info);
@ -200,7 +197,12 @@ namespace LoRaWan.NetworkServer
}
else
{
Logger.Log(loraDeviceInfo.DevEUI, $"message '{jsonDataPayload}' sent to hub", Logger.LoggingLevel.Info);
var fullPayloadAsString = fullPayload.data as string;
if (fullPayloadAsString == null)
{
fullPayloadAsString = ((JObject)fullPayload.data).ToString(Formatting.None);
}
Logger.Log(loraDeviceInfo.DevEUI, $"message '{fullPayloadAsString}' sent to hub", Logger.LoggingLevel.Info);
}
loraDeviceInfo.FCntUp = fcntup;
}

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

@ -32,7 +32,7 @@ namespace LoRaWan
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("LOG_TO_HUB")))
logToHub = bool.Parse(Environment.GetEnvironmentVariable("LOG_TO_HUB"));
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("LOG_LEVEL")))
loggingLevelSetting = int.Parse(Environment.GetEnvironmentVariable("LOG_LEVEL"));

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

@ -0,0 +1,245 @@
using System;
using System.Threading.Tasks;
using Xunit;
namespace LoRaWan.IntegrationTest
{
// Tests ABP requests
[Collection("ArduinoSerialCollection")] // run in serial
public sealed class ABPTest : IClassFixture<IntegrationTestFixture>, IDisposable
{
private readonly IntegrationTestFixture testFixture;
private LoRaArduinoSerial lora;
public ABPTest(IntegrationTestFixture testFixture)
{
this.testFixture = testFixture;
this.lora = LoRaArduinoSerial.CreateFromPort(testFixture.Configuration.LeafDeviceSerialPort);
this.testFixture.ClearNetworkServerLogEvents();
}
public void Dispose()
{
this.lora?.Dispose();
this.lora = null;
GC.SuppressFinalize(this);
}
// Verifies that ABP confirmed and unconfirmed messages are working
// Uses Device5_ABP
[Fact]
public async Task Test_ABP_Confirmed_And_Unconfirmed_Message()
{
const int MESSAGES_COUNT = 10;
var device = this.testFixture.Device5_ABP;
Console.WriteLine($"Starting {nameof(Test_ABP_Confirmed_And_Unconfirmed_Message)} using device {device.DeviceID}");
await lora.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWABP);
await lora.setIdAsync(device.DevAddr, device.DeviceID, null);
await lora.setKeyAsync(device.NwkSKey, device.AppSKey, null);
await lora.SetupLora(this.testFixture.Configuration.LoraRegion);
// Sends 10x unconfirmed messages
for (var i=0; i < MESSAGES_COUNT; ++i)
{
var msg = (101 + i).ToString();
Console.WriteLine($"{device.DeviceID}: Sending unconfirmed '{msg}' {i+1}/{MESSAGES_COUNT}");
await lora.transferPacketAsync(msg, 10);
await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES);
// After transferPacket: Expectation from serial
// +MSG: Done
await AssertUtils.ContainsWithRetriesAsync("+MSG: Done", this.lora.SerialLogs);
// 0000000000000005: valid frame counter, msg: 1 server: 0
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: valid frame counter, msg:");
// 0000000000000005: decoding with: DecoderValueSensor port: 8
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: decoding with: {device.SensorDecoder} port:");
// 0000000000000005: message '{"value": 51}' sent to hub
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: message '{{\"value\":{msg}}}' sent to hub");
this.lora.ClearSerialLogs();
testFixture.ClearNetworkServerLogEvents();
}
// Sends 10x confirmed messages
for (var i=0; i < MESSAGES_COUNT; ++i)
{
var msg = (51 + i).ToString();
Console.WriteLine($"{device.DeviceID}: Sending confirmed '{msg}' {i+1}/{MESSAGES_COUNT}");
await lora.transferPacketWithConfirmedAsync(msg, 10);
await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES);
// After transferPacketWithConfirmed: Expectation from serial
// +CMSG: ACK Received
await AssertUtils.ContainsWithRetriesAsync("+CMSG: ACK Received", this.lora.SerialLogs);
// 0000000000000005: valid frame counter, msg: 1 server: 0
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: valid frame counter, msg:");
// 0000000000000005: decoding with: DecoderValueSensor port: 8
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: decoding with: {device.SensorDecoder} port:");
// 0000000000000005: message '{"value": 51}' sent to hub
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: message '{{\"value\":{msg}}}' sent to hub");
this.lora.ClearSerialLogs();
testFixture.ClearNetworkServerLogEvents();
}
}
// Verifies that ABP using wrong devAddr is ignored when sending messages
// Uses Device6_ABP
[Fact]
public async Task Test_ABP_Wrong_DevAddr_Is_Ignored()
{
var device = this.testFixture.Device6_ABP;
Console.WriteLine($"Starting {nameof(Test_ABP_Wrong_DevAddr_Is_Ignored)} using device {device.DeviceID}");
var devAddrToUse = "05060708";
Assert.NotEqual(devAddrToUse, device.DevAddr);
await lora.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWABP);
await lora.setIdAsync(devAddrToUse, device.DeviceID, null);
await lora.setKeyAsync(device.NwkSKey, device.AppSKey, null);
await lora.SetupLora(this.testFixture.Configuration.LoraRegion);
await lora.transferPacketAsync("100", 10);
await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_SENDING_PACKET);
// After transferPacket: Expectation from serial
// +MSG: Done
await AssertUtils.ContainsWithRetriesAsync("+MSG: Done", this.lora.SerialLogs);
// 05060708: device is not our device, ignore message
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{devAddrToUse}: device is not our device, ignore message");
await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES);
this.lora.ClearSerialLogs();
testFixture.ClearNetworkServerLogEvents();
// Try with confirmed message
await lora.transferPacketWithConfirmedAsync("51", 10);
// wait for serial logs to be ready
await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_SENDING_PACKET);
// After transferPacketWithConfirmed: Expectation from serial
// +CMSG: ACK Received -- should not be there!
Assert.DoesNotContain("+CMSG: ACK Received", this.lora.SerialLogs);
// 05060708: device is not our device, ignore message
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{devAddrToUse}: device is not our device, ignore message");
}
// Tests using a incorrect Network Session key, resulting device not ours
// AppSKey="2B7E151628AED2A6ABF7158809CF4F3C",
// NwkSKey="3B7E151628AED2A6ABF7158809CF4F3C",
// DevAddr="0028B1B2"
// Uses Device7_ABP
[Fact]
public async Task Test_ABP_Mismatch_NwkSKey_And_AppSKey_Fails_Mic_Validation()
{
var device = this.testFixture.Device7_ABP;
Console.WriteLine($"Starting {nameof(Test_ABP_Mismatch_NwkSKey_And_AppSKey_Fails_Mic_Validation)} using device {device.DeviceID}");
var appSKeyToUse = "000102030405060708090A0B0C0D0E0F";
var nwkSKeyToUse = "01020304050607080910111213141516";
Assert.NotEqual(appSKeyToUse, device.AppSKey);
Assert.NotEqual(nwkSKeyToUse, device.NwkSKey);
await lora.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWABP);
await lora.setIdAsync(device.DevAddr, device.DeviceID, null);
await lora.setKeyAsync(nwkSKeyToUse, appSKeyToUse, null);
await lora.SetupLora(this.testFixture.Configuration.LoraRegion);
await lora.transferPacketAsync("100", 10);
// wait for serial logs to be ready
await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_SENDING_PACKET);
// After transferPacket: Expectation from serial
// +MSG: Done
//await AssertUtils.ContainsWithRetriesAsync("+MSG: Done", this.lora.SerialLogs);
// 0000000000000005: with devAddr 0028B1B0 check MIC failed. Device will be ignored from now on
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: with devAddr {device.DevAddr} check MIC failed. Device will be ignored from now on");
await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES);
this.lora.ClearSerialLogs();
testFixture.ClearNetworkServerLogEvents();
// Try with confirmed message
await lora.transferPacketWithConfirmedAsync("51", 10);
await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_SENDING_PACKET);
// 0000000000000005: with devAddr 0028B1B0 check MIC failed. Device will be ignored from now on
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: with devAddr {device.DevAddr} check MIC failed. Device will be ignored from now on");
// wait until arduino stops trying to send confirmed msg
await this.lora.WaitForIdleAsync();
}
// Tests using a invalid Network Session key, resulting in mic failed
// Uses Device8_ABP
[Fact]
public async Task Test_ABP_Invalid_NwkSKey_Fails_With_Mic_Error()
{
var device = this.testFixture.Device8_ABP;
Console.WriteLine($"Starting {nameof(Test_ABP_Invalid_NwkSKey_Fails_With_Mic_Error)} using device {device.DeviceID}");
var nwkSKeyToUse = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF";
Assert.NotEqual(nwkSKeyToUse, device.NwkSKey);
await lora.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWABP);
await lora.setIdAsync(device.DevAddr, device.DeviceID, null);
await lora.setKeyAsync(nwkSKeyToUse, device.AppSKey, null);
await lora.SetupLora(this.testFixture.Configuration.LoraRegion);
lora.transferPacket("100", 10);
await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES);
// After transferPacket: Expectation from serial
// +MSG: Done
await AssertUtils.ContainsWithRetriesAsync("+MSG: Done", this.lora.SerialLogs);
// 0000000000000008: with devAddr 0028B1B3 check MIC failed. Device will be ignored from now on
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: with devAddr {device.DevAddr} check MIC failed. Device will be ignored from now on");
this.lora.ClearSerialLogs();
testFixture.ClearNetworkServerLogEvents();
// Try with confirmed message
await lora.transferPacketWithConfirmedAsync("51", 10);
await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES);
// 0000000000000008: with devAddr 0028B1B3 check MIC failed. Device will be ignored from now on
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: with devAddr {device.DevAddr} check MIC failed. Device will be ignored from now on");
// Before starting new test, wait until Lora drivers stops sending/receiving data
await lora.WaitForIdleAsync();
}
}
}

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

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;
using Xunit.Sdk;
public static class AssertUtils
{
public static async Task ContainsWithRetriesAsync<T>(T expected, IEnumerable<T> collection, int maxAttempts = 3, TimeSpan? interval = null)
{
var intervalToUse = interval ?? TimeSpan.FromSeconds(5);
for (var i=0; i < maxAttempts; ++i)
{
try
{
Assert.Contains<T>(expected, collection);
return;
}
catch (ContainsException )
{
if ((i+1) == maxAttempts)
throw;
}
await Task.Delay(intervalToUse);
}
}
public static async Task ContainsWithRetriesAsync<T>(Predicate<T> predicate, IEnumerable<T> collection, int maxAttempts = 3, TimeSpan? interval = null)
{
var intervalToUse = interval ?? TimeSpan.FromSeconds(5);
for (var i=0; i < maxAttempts; ++i)
{
try
{
Assert.Contains<T>(collection, predicate);
return;
}
catch (ContainsException )
{
if ((i+1) == maxAttempts)
throw;
}
await Task.Delay(intervalToUse);
}
}
}

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

@ -0,0 +1,259 @@
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace LoRaWan.IntegrationTest
{
// Tests Cloud to Device messages
[Collection("ArduinoSerialCollection")] // run in serial
public sealed class C2DMessageTest : IClassFixture<IntegrationTestFixture>, IDisposable
{
private readonly IntegrationTestFixture testFixture;
private LoRaArduinoSerial lora;
static Random random = new Random();
public C2DMessageTest(IntegrationTestFixture testFixture)
{
this.testFixture = testFixture;
this.lora = LoRaArduinoSerial.CreateFromPort(testFixture.Configuration.LeafDeviceSerialPort);
this.testFixture.ClearNetworkServerLogEvents();
}
public void Dispose()
{
this.lora?.Dispose();
this.lora = null;
GC.SuppressFinalize(this);
}
string ToHexString(string str)
{
var sb = new StringBuilder();
var bytes = Encoding.UTF8.GetBytes(str);
foreach (var t in bytes)
{
sb.Append(t.ToString("X2"));
}
return sb.ToString(); // returns: "48656C6C6F20776F726C64" for "Hello world"
}
// Ensures that C2D messages are received when working with confirmed messages
// Uses Device9_OTAA
[Fact]
public async Task Test_OTAA_Confirmed_Receives_C2D_Message()
{
var device = this.testFixture.Device9_OTAA;
Console.WriteLine($"Starting {nameof(Test_OTAA_Confirmed_Receives_C2D_Message)} using device {device.DeviceID}");
await lora.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWOTAA);
await lora.setIdAsync(device.DevAddr, device.DeviceID, device.AppEUI);
await lora.setKeyAsync(device.NwkSKey, device.AppSKey, device.AppKey);
await lora.SetupLora(this.testFixture.Configuration.LoraRegion);
var joinSucceeded = await lora.setOTAAJoinAsyncWithRetry(LoRaArduinoSerial._otaa_join_cmd_t.JOIN, 20000, 5);
if (!joinSucceeded)
{
Assert.True(joinSucceeded, "Join failed");
}
// wait 1 second after joined
await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_JOIN);
// Sends 2x confirmed messages
for (var i=1; i <= 2; ++i)
{
var msg = (10 + i).ToString();
Console.WriteLine($"{device.DeviceID}: Sending confirmed '{msg}' {i}/10");
await lora.transferPacketWithConfirmedAsync(msg, 10);
await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES);
// +CMSG: ACK Received
await AssertUtils.ContainsWithRetriesAsync("+CMSG: ACK Received", this.lora.SerialLogs);
this.lora.ClearSerialLogs();
testFixture.ClearNetworkServerLogEvents();
}
// sends C2D - between 10 and 99
var c2dMessageBody = (100 + random.Next(90)).ToString();
await this.testFixture.SendCloudToDeviceMessage(device.DeviceID, c2dMessageBody);
Console.WriteLine($"Message {c2dMessageBody} sent to device, need to check if it receives");
var foundC2DMessage = false;
var foundReceivePacket = false;
var expectedRxSerial = $"+CMSG: PORT: 1; RX: \"{ToHexString(c2dMessageBody)}\"";
Console.WriteLine($"Expected C2D received log is: {expectedRxSerial}");
// Sends 8x confirmed messages, stopping if C2D message is found
for (var i=2; i <= 10; ++i)
{
var msg = (10 + i).ToString();
Console.WriteLine($"{device.DeviceID}: Sending confirmed '{msg}' {i}/10");
await lora.transferPacketWithConfirmedAsync(msg, 10);
await Task.Delay(TimeSpan.FromSeconds(5));
// After transferPacketWithConfirmed: Expectation from serial
// +CMSG: ACK Received
await AssertUtils.ContainsWithRetriesAsync("+CMSG: ACK Received", this.lora.SerialLogs);
// check if c2d message was found
// 0000000000000009: C2D message: 58
(var foundC2DMessageInNetworkServerLog, _) = await this.testFixture.FindNetworkServerEventLog((e, deviceID, messageBody) => {
return messageBody.StartsWith($"{device.DeviceID}: C2D message: {c2dMessageBody}");
},
new FindNetworkServerEventLogOptions {
Description = $"{device.DeviceID}: C2D message: {c2dMessageBody}",
MaxAttempts = 1
});
// We should only receive the message once
if (foundC2DMessageInNetworkServerLog)
{
Console.WriteLine($"{device.DeviceID}: Found C2D message in log (after sending {i}/10) ? {foundC2DMessage}");
Assert.False(foundC2DMessage, "Cloud to Device message should have been detected in Network Service module only once");
foundC2DMessage = true;
}
var localFoundCloudToDeviceInSerial = this.lora.SerialLogs.Contains(expectedRxSerial);
if (localFoundCloudToDeviceInSerial)
{
Assert.False(foundReceivePacket, "Cloud to device message should have been received only once");
foundReceivePacket = true;
}
this.lora.ClearSerialLogs();
testFixture.ClearNetworkServerLogEvents();
await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES);
}
Assert.True(foundC2DMessage, $"Did not find '{device.DeviceID}: C2D message: {c2dMessageBody}' in logs");
// checks if log arrived
if (!foundReceivePacket)
foundReceivePacket = this.lora.SerialLogs.Contains(expectedRxSerial);
Assert.True(foundReceivePacket, $"Could not find lora receiving message '{expectedRxSerial}'");
}
// Ensures that C2D messages are received when working with unconfirmed messages
// Uses Device10_OTAA
[Fact]
public async Task Test_OTAA_Unconfirmed_Receives_C2D_Message()
{
var device = this.testFixture.Device10_OTAA;
Console.WriteLine($"Starting {nameof(Test_OTAA_Confirmed_Receives_C2D_Message)} using device {device.DeviceID}");
await lora.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWOTAA);
await lora.setIdAsync(device.DevAddr, device.DeviceID, device.AppEUI);
await lora.setKeyAsync(device.NwkSKey, device.AppSKey, device.AppKey);
await lora.SetupLora(this.testFixture.Configuration.LoraRegion);
var joinSucceeded = await lora.setOTAAJoinAsyncWithRetry(LoRaArduinoSerial._otaa_join_cmd_t.JOIN, 20000, 5);
if (!joinSucceeded)
{
Assert.True(joinSucceeded, "Join failed");
}
// wait 1 second after joined
await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_JOIN);
// Sends 2x confirmed messages
for (var i=1; i <= 2; ++i)
{
var msg = (10 + i).ToString();
Console.WriteLine($"{device.DeviceID}: Sending unconfirmed '{msg}' {i}/10");
lora.transferPacket(msg, 10);
await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES);
await AssertUtils.ContainsWithRetriesAsync("+MSG: Done", lora.SerialLogs);
this.lora.ClearSerialLogs();
testFixture.ClearNetworkServerLogEvents();
}
// sends C2D - between 10 and 99
var c2dMessageBody = (100 + random.Next(90)).ToString();
await this.testFixture.SendCloudToDeviceMessage(device.DeviceID, c2dMessageBody);
Console.WriteLine($"Message {c2dMessageBody} sent to device, need to check if it receives");
var foundC2DMessage = false;
var foundReceivePacket = false;
var expectedRxSerial = $"+MSG: PORT: 1; RX: \"{ToHexString(c2dMessageBody)}\"";
Console.WriteLine($"Expected C2D received log is: {expectedRxSerial}");
// Sends 8x confirmed messages, stopping if C2D message is found
for (var i=2; i <= 10; ++i)
{
var msg = (10 + i).ToString();
Console.WriteLine($"{device.DeviceID}: Sending unconfirmed '{msg}' {i}/10");
lora.transferPacket(msg, 10);
await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES);
await AssertUtils.ContainsWithRetriesAsync("+MSG: Done", lora.SerialLogs);
// check if c2d message was found
// 0000000000000009: C2D message: 58
(var foundC2DMessageInNetworkServerLog, _) = await this.testFixture.FindNetworkServerEventLog((e, deviceID, messageBody) => {
return messageBody.StartsWith($"{device.DeviceID}: C2D message: {c2dMessageBody}");
},
new FindNetworkServerEventLogOptions {
Description = $"{device.DeviceID}: C2D message: {c2dMessageBody}",
MaxAttempts = 1
});
// We should only receive the message once
if (foundC2DMessageInNetworkServerLog)
{
Console.WriteLine($"{device.DeviceID}: Found C2D message in log (after sending {i}/10) ? {foundC2DMessage}");
Assert.False(foundC2DMessage, "Cloud to Device message should have been detected in Network Service module only once");
foundC2DMessage = true;
}
var localFoundCloudToDeviceInSerial = this.lora.SerialLogs.Contains(expectedRxSerial);
if (localFoundCloudToDeviceInSerial)
{
Assert.False(foundReceivePacket, "Cloud to device message should have been received only once");
foundReceivePacket = true;
}
this.lora.ClearSerialLogs();
testFixture.ClearNetworkServerLogEvents();
await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES);
}
Assert.True(foundC2DMessage, $"Did not find '{device.DeviceID}: C2D message: {c2dMessageBody}' in logs");
// checks if log arrived
if (!foundReceivePacket)
foundReceivePacket = this.lora.SerialLogs.Contains(expectedRxSerial);
Assert.True(foundReceivePacket, $"Could not find lora receiving message '{expectedRxSerial}'");
}
}
}

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

@ -0,0 +1,16 @@
namespace LoRaWan.IntegrationTest
{
public static class Constants
{
// Time to wait between sending message in ms
public const int DELAY_BETWEEN_MESSAGES = 1000 * 5;
// Time to wait between sending messages and expecting a serial response
public const int DELAY_FOR_SERIAL_AFTER_SENDING_PACKET = 150;
// Time to wait between joining and expecting a serial response
public const int DELAY_FOR_SERIAL_AFTER_JOIN = 1000;
}
}

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

@ -0,0 +1,129 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Azure.EventHubs;
namespace LoRaWan.IntegrationTest
{
public class EventHubDataCollector : IPartitionReceiveHandler, IDisposable
{
private EventHubClient eventHubClient;
private readonly ConcurrentQueue<EventData> events;
private readonly string connectionString;
List<PartitionReceiver> receivers;
public bool LogToConsole { get; set; } = true;
public string ConsumerGroupName { get; set; } = "$Default";
public EventHubDataCollector(string connectionString) : this(connectionString, null)
{
}
public EventHubDataCollector(string connectionString, string consumerGroupName)
{
this.connectionString = connectionString;
this.eventHubClient = EventHubClient.CreateFromConnectionString(connectionString);
this.events = new ConcurrentQueue<EventData>();
this.receivers = new List<PartitionReceiver>();
if(!string.IsNullOrEmpty(consumerGroupName))
this.ConsumerGroupName = consumerGroupName;
}
public async Task Start()
{
if(this.receivers.Count > 0)
throw new InvalidOperationException("Already started");
if(this.LogToConsole)
{
Console.WriteLine($"Connecting to IoT Hub Event Hub @{this.connectionString} using consumer group {this.ConsumerGroupName}");
}
var rti = await this.eventHubClient.GetRuntimeInformationAsync();
foreach(var partitionId in rti.PartitionIds)
{
var receiver = this.eventHubClient.CreateReceiver(this.ConsumerGroupName, partitionId, EventPosition.FromEnqueuedTime(DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(1))));
receiver.SetReceiveHandler(this);
this.receivers.Add(receiver);
}
}
public void ResetEvents() => this.events.Clear();
public IReadOnlyCollection<EventData> GetEvents() => this.events;
Task IPartitionReceiveHandler.ProcessEventsAsync(IEnumerable<EventData> events)
{
foreach(var item in events)
{
this.events.Enqueue(item);
if(this.LogToConsole)
{
var bodyText = Encoding.UTF8.GetString(item.Body);
Console.WriteLine($"[EventHub]: {bodyText}");
}
}
return Task.FromResult(0);
}
Task IPartitionReceiveHandler.ProcessErrorAsync(Exception error)
{
Console.Error.WriteLine(error.ToString());
return Task.FromResult(0);
}
int maxBatchSize = 32;
int IPartitionReceiveHandler.MaxBatchSize { get => this.maxBatchSize; set => this.maxBatchSize = value; }
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if(!disposedValue)
{
if(disposing)
{
for(int i = this.receivers.Count - 1; i >= 0; i--)
{
try
{
this.receivers[i].SetReceiveHandler(null);
this.receivers[i].Close();
}
catch (Exception ex)
{
Console.WriteLine($"Error closing event hub receiver: {ex.ToString()}");
}
this.receivers.RemoveAt(i);
}
this.eventHubClient.Close();
this.eventHubClient = null;
}
// TODO: free unmanaged resources(unmanaged objects) and override a finalizer below.
// TODO: set large fields to null.
disposedValue = true;
}
}
// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}

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

@ -0,0 +1,10 @@
namespace LoRaWan.IntegrationTest
{
public class FindNetworkServerEventLogOptions
{
public string Description { get; set; }
public int? MaxAttempts { get; set; }
}
}

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

@ -0,0 +1,411 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Azure.Devices;
using Microsoft.Azure.Devices.Shared;
using Microsoft.Azure.EventHubs;
using Newtonsoft.Json;
using Xunit;
namespace LoRaWan.IntegrationTest
{
public partial class IntegrationTestFixture : IDisposable, IAsyncLifetime
{
public const string MESSAGE_IDENTIFIER_PROPERTY_NAME = "messageIdentifier";
RegistryManager registryManager;
public TestConfiguration Configuration { get; }
public EventHubDataCollector NetworkServerLogEvents { get; private set; }
public void ClearNetworkServerLogEvents() => this.NetworkServerLogEvents?.ResetEvents();
// Device1_OTAA: used for join test only
public TestDeviceInfo Device1_OTAA { get; }
// Device2_OTAA: used for failed join (wrong devEUI)
public TestDeviceInfo Device2_OTAA { get; }
// Device3_OTAA: used for failed join (wrong appKey)
public TestDeviceInfo Device3_OTAA { get; }
// Device4_OTAA: used for OTAA confirmed & unconfirmed messaging
public TestDeviceInfo Device4_OTAA { get; }
// Device5_ABP: used for ABP confirmed & unconfirmed messaging
public TestDeviceInfo Device5_ABP { get; }
// Device6_ABP: used for ABP wrong devaddr
public TestDeviceInfo Device6_ABP { get; }
// Device7_ABP: used for ABP wrong nwkskey
public TestDeviceInfo Device7_ABP { get; }
// Device8_ABP: used for ABP invalid nwkskey (mic fails)
public TestDeviceInfo Device8_ABP { get; }
// Device9_OTAA: used for OTAA confirmed messages, C2D test
public TestDeviceInfo Device9_OTAA { get; }
// Device10_OTAA: used for OTAA unconfirmed messages, C2D test
public TestDeviceInfo Device10_OTAA { get; }
// Device11_OTAA: used for http decoder
public TestDeviceInfo Device11_OTAA { get; }
// Device12_OTAA: used for reflection based decoder
public TestDeviceInfo Device12_OTAA { get; }
// Device13_OTAA: used for wrong AppEUI OTAA join
public TestDeviceInfo Device13_OTAA { get; }
public IntegrationTestFixture()
{
this.Configuration = TestConfiguration.GetConfiguration();
if (!string.IsNullOrEmpty(Configuration.IoTHubEventHubConnectionString) && this.Configuration.NetworkServerModuleLogAssertLevel != NetworkServerModuleLogAssertLevel.Ignore)
{
this.NetworkServerLogEvents = new EventHubDataCollector(Configuration.IoTHubEventHubConnectionString, Configuration.IoTHubEventHubConsumerGroup);
var startTask = this.NetworkServerLogEvents.Start();
startTask.ConfigureAwait(false).GetAwaiter().GetResult();
}
var gatewayID = Environment.GetEnvironmentVariable("IOTEDGE_DEVICEID") ?? this.Configuration.LeafDeviceGatewayID;
// Device1_OTAA: used for join test only
this.Device1_OTAA = new TestDeviceInfo()
{
DeviceID = "0000000000000001",
AppEUI = "BE7A0000000014E3",
AppKey = "8AFE71A145B253E49C3031AD068277A3",
GatewayID = gatewayID,
RealDevice = true
};
// Device2_OTAA: used for failed join (wrong devEUI)
this.Device2_OTAA = new TestDeviceInfo()
{
DeviceID = "0000000000000002",
AppEUI = "BE7A0000000014E3",
AppKey = "8AFE71A145B253E49C3031AD068277A3",
GatewayID = gatewayID,
SensorDecoder = "DecoderValueSensor",
RealDevice = false,
};
// Device3_OTAA: used for failed join (wrong appKey)
this.Device3_OTAA = new TestDeviceInfo()
{
DeviceID = "0000000000000003",
AppEUI = "BE7A00000000FFE3",
AppKey = "8AFE71A145B253E49C3031AD068277A3",
GatewayID = gatewayID,
SensorDecoder = "DecoderValueSensor",
RealDevice = true,
};
// Device4_OTAA: used for OTAA confirmed & unconfirmed messaging
this.Device4_OTAA = new TestDeviceInfo()
{
DeviceID = "0000000000000004",
AppEUI = "BE7A0000000014E3",
AppKey = "8AFE71A145B253E49C3031AD068277A3",
GatewayID = gatewayID,
SensorDecoder = "DecoderValueSensor",
RealDevice = true,
};
// Device5_ABP: used for ABP confirmed & unconfirmed messaging
this.Device5_ABP = new TestDeviceInfo()
{
DeviceID = "0000000000000005",
AppEUI = "0000000000000005",
GatewayID = gatewayID,
SensorDecoder = "DecoderValueSensor",
RealDevice = true,
AppSKey="2B7E151628AED2A6ABF7158809CF4F3C",
NwkSKey="3B7E151628AED2A6ABF7158809CF4F3C",
DevAddr="0028B1B0"
};
// Device6_ABP: used for ABP wrong devaddr
this.Device6_ABP = new TestDeviceInfo()
{
DeviceID = "0000000000000006",
AppEUI = "0000000000000006",
GatewayID = gatewayID,
SensorDecoder = "DecoderValueSensor",
RealDevice = false,
AppSKey="2B7E151628AED2A6ABF7158809CF4F3C",
NwkSKey="3B7E151628AED2A6ABF7158809CF4F3C",
DevAddr="0028B1B1",
};
// Device7_ABP: used for ABP wrong nwkskey
this.Device7_ABP = new TestDeviceInfo()
{
DeviceID = "0000000000000007",
AppEUI = "0000000000000007",
GatewayID = gatewayID,
SensorDecoder = "DecoderValueSensor",
RealDevice = true,
AppSKey="2B7E151628AED2A6ABF7158809CF4F3C",
NwkSKey="3B7E151628AED2A6ABF7158809CF4F3C",
DevAddr="0028B1B2"
};
// Device8_ABP: used for ABP invalid nwkskey (mic fails)
this.Device8_ABP = new TestDeviceInfo()
{
DeviceID = "0000000000000008",
AppEUI = "0000000000000008",
GatewayID = gatewayID,
SensorDecoder = "DecoderValueSensor",
RealDevice = true,
AppSKey="2B7E151628AED2A6ABF7158809CF4F3C",
NwkSKey="3B7E151628AED2A6ABF7158809CF4F3C",
DevAddr="0028B1B3"
};
// Device9_OTAA: used for confirmed message & C2D
this.Device9_OTAA = new TestDeviceInfo()
{
DeviceID = "0000000000000009",
AppEUI = "BE7A0000000014E3",
AppKey = "8AFE71A145B253E49C3031AD068277A3",
GatewayID = gatewayID,
RealDevice = true
};
// Device10_OTAA: used for unconfirmed message & C2D
this.Device10_OTAA = new TestDeviceInfo()
{
DeviceID = "0000000000000010",
AppEUI = "BE7A0000000014E3",
AppKey = "8AFE71A145B253E49C3031AD068277A3",
GatewayID = gatewayID,
RealDevice = true
};
// Device11_OTAA: used for http decoder
this.Device11_OTAA = new TestDeviceInfo()
{
DeviceID = "0000000000000011",
AppEUI = "BE7A0000000014E3",
AppKey = "8AFE71A145B253E49C3031AD068277A3",
GatewayID = gatewayID,
RealDevice = true,
SensorDecoder = "http://sensordecodermodule/api/DecoderValueSensor",
};
// Device12_OTAA: used for reflection based decoder
this.Device12_OTAA = new TestDeviceInfo()
{
DeviceID = "0000000000000012",
AppEUI = "BE7A0000000014E3",
AppKey = "8AFE71A145B253E49C3031AD068277A3",
GatewayID = gatewayID,
RealDevice = true,
SensorDecoder = "DecoderValueSensor",
};
// Device13_OTAA: used for Join with wrong AppEUI
this.Device13_OTAA = new TestDeviceInfo()
{
DeviceID = "0000000000000013",
AppEUI = "BE7A00000000FEE3",
AppKey = "8AFE71A145B253E49C3031AD068277A3",
GatewayID = gatewayID,
RealDevice = true,
SensorDecoder = "DecoderValueSensor",
};
}
internal string GetMessageIdentifier(EventData eventData)
{
eventData.Properties.TryGetValue("messageIdentifier", out var actualMessageIdentifier);
return actualMessageIdentifier?.ToString();
}
// Validate the network server log for the existence of a message
public async Task ValidateNetworkServerEventLogStartsWithAsync(string logMessageStart)
{
if (this.Configuration.NetworkServerModuleLogAssertLevel != NetworkServerModuleLogAssertLevel.Ignore)
{
var findResult = await this.FindNetworkServerEventLog((e, deviceID, messageBody) => messageBody.StartsWith(logMessageStart),
new FindNetworkServerEventLogOptions
{
Description = logMessageStart
});
if (this.Configuration.NetworkServerModuleLogAssertLevel == NetworkServerModuleLogAssertLevel.Error)
{
var logs = string.Join("\n\t", findResult.Item2.TakeLast(5));
Assert.True(findResult.Item1, $"Did not find '{logMessageStart}' in logs [{logs}]");
}
else if (this.Configuration.NetworkServerModuleLogAssertLevel == NetworkServerModuleLogAssertLevel.Warning)
{
if (findResult.Item1)
{
Console.WriteLine($"'{logMessageStart}' found in logs? {findResult.Item1}");
}
else
{
var logs = string.Join("\n\t", findResult.Item2.TakeLast(5));
Console.WriteLine($"'{logMessageStart}' found in logs? {findResult.Item1}. Logs: [{logs}]");
}
}
}
}
// Search the network server logs for a value
//internal async Task<Tuple<bool, HashSet<string>>> FindNetworkServerEventLog(Func<EventData, string, string, bool> predicate)
internal async Task<(bool found, HashSet<string> logs)> FindNetworkServerEventLog(Func<EventData, string, string, bool> predicate, FindNetworkServerEventLogOptions options = null)
{
var maxAttempts = options?.MaxAttempts ?? this.Configuration.EnsureHasEventMaximumTries;
var processedEvents = new HashSet<string>();
for (int i = 0; i < maxAttempts; i++)
{
if (i > 0)
{
var timeToWait = i * this.Configuration.EnsureHasEventDelayBetweenReadsInSeconds;
if (!string.IsNullOrEmpty(options?.Description))
{
Console.WriteLine($"Network server event log '{options.Description}' not found, attempt {i}/{maxAttempts}, waiting {timeToWait} secs");
}
else
{
Console.WriteLine($"Network server event log not found, attempt {i}/{maxAttempts}, waiting {timeToWait} secs");
}
await Task.Delay(TimeSpan.FromSeconds(timeToWait));
}
foreach (var item in this.NetworkServerLogEvents.GetEvents())
{
var bodyText = System.Text.Encoding.UTF8.GetString(item.Body);
processedEvents.Add(bodyText);
item.SystemProperties.TryGetValue("iothub-connection-device-id", out var deviceId);
if (predicate(item, deviceId?.ToString(), bodyText))
{
return (found: true, logs: processedEvents);// new Tuple<bool, HashSet<string>>(true, processedEvents);
}
}
}
return (found: false, logs: processedEvents);
//return new Tuple<bool, HashSet<string>>(false, processedEvents);
}
public void Dispose()
{
this.NetworkServerLogEvents?.Dispose();
this.NetworkServerLogEvents = null;
this.registryManager?.Dispose();
this.registryManager = null;
GC.SuppressFinalize(this);
}
RegistryManager GetRegistryManager()
{
return (this.registryManager ?? (this.registryManager = RegistryManager.CreateFromConnectionString(this.Configuration.IoTHubConnectionString)));
}
internal async Task<Twin> GetTwinAsync(string deviceId)
{
return await GetRegistryManager().GetTwinAsync(deviceId);
}
internal async Task SendCloudToDeviceMessage(string deviceId, string messageText)
{
var msg = new Message(Encoding.UTF8.GetBytes(messageText));
await SendCloudToDeviceMessage(deviceId, msg);
}
internal async Task SendCloudToDeviceMessage(string deviceId, Message message)
{
ServiceClient sc = ServiceClient.CreateFromConnectionString(this.Configuration.IoTHubConnectionString);
await sc.SendAsync(deviceId, message);
}
internal async Task<Twin> ReplaceTwinAsync(string deviceId, Twin updatedTwin, string etag)
{
try
{
return await GetRegistryManager().ReplaceTwinAsync(deviceId, updatedTwin, etag);
}
catch (Exception ex)
{
Console.WriteLine($"Error replacing twin for device {deviceId}: {ex.ToString()}");
throw;
}
}
public async Task InitializeAsync()
{
if (this.Configuration.CreateDevices)
{
var devices = new TestDeviceInfo[]
{
this.Device1_OTAA,
this.Device2_OTAA,
this.Device3_OTAA,
this.Device4_OTAA,
this.Device5_ABP,
this.Device6_ABP,
this.Device7_ABP,
this.Device8_ABP,
this.Device9_OTAA,
this.Device10_OTAA,
this.Device11_OTAA,
this.Device12_OTAA,
this.Device13_OTAA,
};
var registryManager = GetRegistryManager();
foreach (var testDevice in devices.Where(x => x.RealDevice))
{
var getDeviceResult = await registryManager.GetDeviceAsync(testDevice.DeviceID);
if (getDeviceResult == null)
{
Console.WriteLine($"Device {testDevice.DeviceID} does not exist. Creating");
var device = new Device(testDevice.DeviceID);
var twin = new Twin(testDevice.DeviceID);
twin.Properties.Desired = new TwinCollection(JsonConvert.SerializeObject(testDevice.GetDesiredProperties()));
Console.WriteLine($"Creating device {testDevice.DeviceID}");
await registryManager.AddDeviceWithTwinAsync(device, twin);
}
else
{
// compare device twin and make changes if needed
var deviceTwin = await registryManager.GetTwinAsync(testDevice.DeviceID);
var desiredProperties = testDevice.GetDesiredProperties();
foreach (var kv in desiredProperties)
{
if (!deviceTwin.Properties.Desired.Contains(kv.Key) || (string)deviceTwin.Properties.Desired[kv.Key] != kv.Value)
{
Console.WriteLine($"Unexpected value for device {testDevice.DeviceID} twin property {kv.Key}, expecting '{kv.Value}', actual is '{(string)deviceTwin.Properties.Desired[kv.Key]}'");
var patch = new Twin();
patch.Properties.Desired = new TwinCollection(JsonConvert.SerializeObject(desiredProperties));
await registryManager.UpdateTwinAsync(testDevice.DeviceID, patch, deviceTwin.ETag);
Console.WriteLine($"Update twin for device {testDevice.DeviceID}");
break;
}
}
}
}
}
}
public Task DisposeAsync() => Task.FromResult(0);
}
}

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

@ -0,0 +1,91 @@
using System;
using System.IO.Ports;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace LoRaWan.IntegrationTest
{
public sealed partial class LoRaArduinoSerial
{
public static LoRaArduinoSerial CreateFromPort (string port)
{
var isWindows = RuntimeInformation.IsOSPlatform (OSPlatform.Windows);
LoRaArduinoSerial result = null;
if (!isWindows)
{
Console.WriteLine ($"Starting serial port '{port}' on non-Windows");
var serialPort = new SerialDevice (port, BaudRate.B115200);
result = new LoRaArduinoSerial (serialPort);
Console.WriteLine ($"Opening serial port");
try
{
serialPort.Open ();
}
catch (Exception ex)
{
Console.WriteLine ($"Error opening serial port '{port}': {ex.ToString()}");
throw;
}
}
else
{
Console.WriteLine ($"Starting serial port '{port}' on Windows");
var serialPortWin = new SerialPort (port)
{
BaudRate = 115200,
Parity = Parity.None,
StopBits = StopBits.One,
DataBits = 8,
DtrEnable = true,
Handshake = Handshake.None
};
result = new LoRaArduinoSerial (serialPortWin);
try
{
serialPortWin.Open();
}
catch (Exception ex)
{
Console.WriteLine ($"Error opening serial port '{port}': {ex.ToString()}");
throw;
}
}
return result;
}
// Setup lora for a given region
public async Task SetupLora(LoraRegion region)
{
if (region == LoraRegion.EU)
{
await this.setDataRateAsync(LoRaArduinoSerial._data_rate_t.DR6, LoRaArduinoSerial._physical_type_t.EU868);
await this.setChannelAsync(0, 868.1F);
await this.setChannelAsync(1, 868.3F);
await this.setChannelAsync(2, 868.5F);
await this.setReceiceWindowFirstAsync(0, 868.1F);
await this.setReceiceWindowSecondAsync(868.5F, LoRaArduinoSerial._data_rate_t.DR2);
}
else
{
await this.setDataRateAsync(LoRaArduinoSerial._data_rate_t.DR0, LoRaArduinoSerial._physical_type_t.US915HYBRID);
}
await this.setConfirmedMessageRetryTimeAsync(10);
await this.setAdaptiveDataRateAsync(false);
await this.setDutyCycleAsync(false);
await this.setJoinDutyCycleAsync(false);
await this.setPowerAsync(14);
}
}
}

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

@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Devices" Version="1.17.1" />
<PackageReference Include="Microsoft.Azure.EventHubs" Version="2.2.1" />
<PackageReference Include="Microsoft.Azure.EventHubs.Processor" Version="2.2.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.1" />
<PackageReference Include="NetCoreSerial" Version="1.1.1" />
<PackageReference Include="System.IO.Ports" Version="4.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0-preview-20180924-03" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="appsettings.local.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

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

@ -0,0 +1,821 @@
/*
LoRaWAN.h
2013 Copyright (c) Seeed Technology Inc. All right reserved.
Author: Wayne Weng
Date: 2016-10-17
add rgb backlight fucnction @ 2013-10-15
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.1 USA
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace LoRaWan.IntegrationTest
{
public sealed partial class LoRaArduinoSerial : IDisposable
{
private SerialPort serialPortWin;
private SerialDevice serialPort;
const int DEFAULT_TIMEWAIT = 100;
public enum _class_type_t { CLASS_A = 0, CLASS_C };
public enum _physical_type_t { EU434 = 0, EU868, US915, US915HYBRID, AU915, AU915OLD, CN470, CN779, AS923, KR920, IN865 };
public enum _device_mode_t { LWABP = 0, LWOTAA, TEST };
public enum _otaa_join_cmd_t { JOIN = 0, FORCE };
public enum _window_delay_t { RECEIVE_DELAY1 = 0, RECEIVE_DELAY2, JOIN_ACCEPT_DELAY1, JOIN_ACCEPT_DELAY2 };
public enum _band_width_t { BW125 = 125, BW250 = 250, BW500 = 500 };
public enum _spreading_factor_t { SF12 = 12, SF11 = 11, SF10 = 10, SF9 = 9, SF8 = 8, SF7 = 7 };
public enum _data_rate_t { DR0 = 0, DR1, DR2, DR3, DR4, DR5, DR6, DR7, DR8, DR9, DR10, DR11, DR12, DR13, DR14, DR15 };
// Creates a new instance based on port identifier
LoRaArduinoSerial (SerialDevice serialPort)
{
this.serialPort = serialPort;
this.serialPort.DataReceived += (object sender, byte[] data) =>
{
var dataread = System.Text.Encoding.UTF8.GetString (data);
OnSerialDataReceived (dataread);
};
}
byte[] windowsSerialPortBuffer = null;
LoRaArduinoSerial(SerialPort sp)
{
serialPortWin = sp;
this.windowsSerialPortBuffer = new byte[this.serialPortWin.ReadBufferSize];
this.serialPortWin.DataReceived += (object sender, SerialDataReceivedEventArgs e) =>
{
var myserialPort = (SerialPort) sender;
var readCount = myserialPort.Read(this.windowsSerialPortBuffer, 0, this.windowsSerialPortBuffer.Length);
var dataread = System.Text.Encoding.UTF8.GetString (this.windowsSerialPortBuffer, 0, readCount);
OnSerialDataReceived(dataread);
};
}
void OnSerialDataReceived(string rawData)
{
var data = rawData.Replace("\r", "");
var lines = string.Concat(buff, data).Split('\n');
buff = string.Empty;
if (lines.Length > 0)
{
for (var i = 0; i < lines.Length - 1; i++)
{
if (!string.IsNullOrEmpty(lines[i]))
{
AppendSerialLog(lines[i]);
}
}
// last line: does the input ends with a new line?
var lastParsedLine = lines[lines.Length - 1];
if (!string.IsNullOrEmpty(lastParsedLine))
{
if (data.EndsWith('\n'))
{
// add as finished line
AppendSerialLog(lastParsedLine);
}
else
{
// buffer it for next line
buff = lastParsedLine;
}
}
}
}
ConcurrentQueue<string> serialLogs = new ConcurrentQueue<string>();
string buff = "";
void AppendSerialLog(string message)
{
Console.WriteLine($"[Serial log] {message}");
this.serialLogs.Enqueue(message);
}
public IReadOnlyCollection<string> SerialLogs { get { return this.serialLogs; } }
public void ClearSerialLogs() => this.serialLogs.Clear();
public LoRaArduinoSerial setId (string DevAddr, string DevEUI, string AppEUI)
{
if (!String.IsNullOrEmpty (DevAddr))
{
string cmd = $"AT+ID=DevAddr,{DevAddr}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
}
if (!String.IsNullOrEmpty (DevEUI))
{
string cmd = $"AT+ID=DevEui,{DevEUI}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
}
if (!String.IsNullOrEmpty (AppEUI))
{
string cmd = $"AT+ID=AppEui,{AppEUI}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
}
return this;
}
public async Task setIdAsync (string DevAddr, string DevEUI, string AppEUI)
{
if (!String.IsNullOrEmpty (DevAddr))
{
string cmd = $"AT+ID=DevAddr,{DevAddr}\r\n";
sendCommand (cmd);
await Task.Delay (DEFAULT_TIMEWAIT);
}
if (!String.IsNullOrEmpty (DevEUI))
{
string cmd = $"AT+ID=DevEui,{DevEUI}\r\n";
sendCommand (cmd);
await Task.Delay (DEFAULT_TIMEWAIT);
}
if (!String.IsNullOrEmpty (AppEUI))
{
string cmd = $"AT+ID=AppEui,{AppEUI}\r\n";
sendCommand (cmd);
await Task.Delay (DEFAULT_TIMEWAIT);
}
}
public LoRaArduinoSerial setKey (string NwkSKey, string AppSKey, string AppKey)
{
if (!String.IsNullOrEmpty (NwkSKey))
{
string cmd = $"AT+KEY=NWKSKEY,{NwkSKey}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
}
if (!String.IsNullOrEmpty (AppSKey))
{
string cmd = $"AT+KEY=APPSKEY,{AppSKey}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
}
if (!String.IsNullOrEmpty (AppKey))
{
string cmd = $"AT+KEY= APPKEY,{AppKey}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
}
return this;
}
public async Task setKeyAsync (string NwkSKey, string AppSKey, string AppKey)
{
if (!String.IsNullOrEmpty (NwkSKey))
{
string cmd = $"AT+KEY=NWKSKEY,{NwkSKey}\r\n";
sendCommand (cmd);
await Task.Delay (DEFAULT_TIMEWAIT);
}
if (!String.IsNullOrEmpty (AppSKey))
{
string cmd = $"AT+KEY=APPSKEY,{AppSKey}\r\n";
sendCommand (cmd);
await Task.Delay (DEFAULT_TIMEWAIT);
}
if (!String.IsNullOrEmpty (AppKey))
{
string cmd = $"AT+KEY= APPKEY,{AppKey}\r\n";
sendCommand (cmd);
await Task.Delay (DEFAULT_TIMEWAIT);
}
}
public LoRaArduinoSerial setDataRate (_data_rate_t dataRate, _physical_type_t physicalType)
{
if (physicalType == _physical_type_t.EU434) sendCommand ("AT+DR=EU433\r\n");
else if (physicalType == _physical_type_t.EU868) sendCommand ("AT+DR=EU868\r\n");
else if (physicalType == _physical_type_t.US915) sendCommand ("AT+DR=US915\r\n");
else if (physicalType == _physical_type_t.US915HYBRID) sendCommand ("AT+DR=US915HYBRID\r\n");
else if (physicalType == _physical_type_t.AU915) sendCommand ("AT+DR=AU915\r\n");
else if (physicalType == _physical_type_t.AU915OLD) sendCommand ("AT+DR=AU915OLD\r\n");
else if (physicalType == _physical_type_t.CN470) sendCommand ("AT+DR=CN470\r\n");
else if (physicalType == _physical_type_t.CN779) sendCommand ("AT+DR=CN779\r\n");
else if (physicalType == _physical_type_t.AS923) sendCommand ("AT+DR=AS923\r\n");
else if (physicalType == _physical_type_t.KR920) sendCommand ("AT+DR=KR920\r\n");
else if (physicalType == _physical_type_t.IN865) sendCommand ("AT+DR=IN865\r\n");
Thread.Sleep (DEFAULT_TIMEWAIT);
string cmd = $"AT+DR={dataRate}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public async Task setDataRateAsync (_data_rate_t dataRate, _physical_type_t physicalType)
{
if (physicalType == _physical_type_t.EU434) sendCommand ("AT+DR=EU433\r\n");
else if (physicalType == _physical_type_t.EU868) sendCommand ("AT+DR=EU868\r\n");
else if (physicalType == _physical_type_t.US915) sendCommand ("AT+DR=US915\r\n");
else if (physicalType == _physical_type_t.US915HYBRID) sendCommand ("AT+DR=US915HYBRID\r\n");
else if (physicalType == _physical_type_t.AU915) sendCommand ("AT+DR=AU915\r\n");
else if (physicalType == _physical_type_t.AU915OLD) sendCommand ("AT+DR=AU915OLD\r\n");
else if (physicalType == _physical_type_t.CN470) sendCommand ("AT+DR=CN470\r\n");
else if (physicalType == _physical_type_t.CN779) sendCommand ("AT+DR=CN779\r\n");
else if (physicalType == _physical_type_t.AS923) sendCommand ("AT+DR=AS923\r\n");
else if (physicalType == _physical_type_t.KR920) sendCommand ("AT+DR=KR920\r\n");
else if (physicalType == _physical_type_t.IN865) sendCommand ("AT+DR=IN865\r\n");
await Task.Delay (DEFAULT_TIMEWAIT);
string cmd = $"AT+DR={dataRate}\r\n";
sendCommand (cmd);
await Task.Delay (DEFAULT_TIMEWAIT);
}
public LoRaArduinoSerial setPower (short power)
{
string cmd = $"AT+POWER={power}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public async Task setPowerAsync (short power)
{
string cmd = $"AT+POWER={power}\r\n";
sendCommand (cmd);
await Task.Delay (DEFAULT_TIMEWAIT);
}
public LoRaArduinoSerial setPort (int port)
{
string cmd = "AT+PORT={port}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public LoRaArduinoSerial setAdaptiveDataRate (bool command)
{
if (command) sendCommand ("AT+ADR=ON\r\n");
else sendCommand ("AT+ADR=OFF\r\n");
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public async Task setAdaptiveDataRateAsync (bool command)
{
if (command) sendCommand ("AT+ADR=ON\r\n");
else sendCommand ("AT+ADR=OFF\r\n");
await Task.Delay (DEFAULT_TIMEWAIT);
}
// Wait until the serial data is empty
internal async Task<bool> WaitForIdleAsync(TimeSpan? timeout = null)
{
var timeoutToUse = timeout ?? TimeSpan.FromSeconds(60);
var start = DateTime.UtcNow;
do
{
this.ClearSerialLogs();
await Task.Delay(TimeSpan.FromSeconds(30));
if (!SerialLogs.Any())
return true;
} while (start.Subtract(DateTime.UtcNow) <= timeoutToUse);
return false;
}
public LoRaArduinoSerial setChannel (int channel, float frequency)
{
string cmd = $"AT+CH={channel},{(short)frequency}.{(short)(frequency * 10) % 10}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public async Task setChannelAsync (int channel, float frequency)
{
string cmd = $"AT+CH={channel},{(short)frequency}.{(short)(frequency * 10) % 10}\r\n";
sendCommand (cmd);
await Task.Delay (DEFAULT_TIMEWAIT);
}
public LoRaArduinoSerial setChannel (char channel, float frequency, _data_rate_t dataRata)
{
string cmd = $"AT+CH={channel},{(short)frequency}.{(short)(frequency * 10) % 10},{dataRata}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public LoRaArduinoSerial setChannel (char channel, float frequency, _data_rate_t dataRataMin, _data_rate_t dataRataMax)
{
string cmd = $"AT+CH={channel},{(short)frequency}.{(short)(frequency * 10) % 10},{dataRataMin},{dataRataMax}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public async Task<bool> transferPacketAsync (string buffer, int timeout)
{
sendCommand ("AT+MSG=\"");
sendCommand (buffer);
sendCommand ("\"\r\n");
DateTime start = DateTime.Now;
while (true)
{
if (this.ReceivedSerial(x => x.StartsWith("+MSG: Done")))
return true;
else if (start.AddSeconds (timeout) < DateTime.Now)
return false;
await Task.Delay(100);
}
}
public bool transferPacket (string buffer, int timeout)
{
sendCommand ("AT+MSG=\"");
sendCommand (buffer);
sendCommand ("\"\r\n");
DateTime start = DateTime.Now;
while (true)
{
if (this.ReceivedSerial(x => x.StartsWith("+MSG: Done")))
return true;
else if (start.AddSeconds (timeout) < DateTime.Now)
return false;
}
}
private bool ReceivedSerial(Func<string, bool> predicate)
{
foreach (var serialLine in this.SerialLogs)
{
if (predicate(serialLine))
return true;
}
return false;
}
public bool transferPacketWithConfirmed (string buffer, int timeout)
{
sendCommand ("AT+CMSG=\"");
sendCommand (buffer);
sendCommand ("\"\r\n");
DateTime start = DateTime.Now;
while (true)
{
if (ReceivedSerial(x => x.StartsWith ("+CMSG: ACK Received")))
return true;
else if (start.AddSeconds (timeout) < DateTime.Now)
return false;
}
//if (_buffer.Contains("+CMSG: ACK Received")) return true;
}
public async Task<bool> transferPacketWithConfirmedAsync (string buffer, int timeout)
{
sendCommand ("AT+CMSG=\"");
sendCommand (buffer);
sendCommand ("\"\r\n");
DateTime start = DateTime.Now;
while (true)
{
if (ReceivedSerial(x => x.StartsWith ("+CMSG: ACK Received")))
return true;
else if (start.AddSeconds (timeout) < DateTime.Now)
return false;
await Task.Delay(100);
}
//if (_buffer.Contains("+CMSG: ACK Received")) return true;
}
public LoRaArduinoSerial setUnconfirmedMessageRepeatTime (uint time)
{
if (time > 15)
time = 15;
else if (time == 0)
time = 1;
string cmd = $"AT+REPT={time}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public LoRaArduinoSerial setConfirmedMessageRetryTime (uint time)
{
if (time > 15) time = 15;
else if (time == 0) time = 1;
string cmd = $"AT+RETRY={time}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public async Task setConfirmedMessageRetryTimeAsync(uint time)
{
if (time > 15) time = 15;
else if (time == 0) time = 1;
string cmd = $"AT+RETRY={time}\r\n";
sendCommand (cmd);
await Task.Delay(DEFAULT_TIMEWAIT);
}
public LoRaArduinoSerial setReceiceWindowFirst (bool command)
{
if (command) sendCommand ("AT+RXWIN1=ON\r\n");
else sendCommand ("AT+RXWIN1=OFF\r\n");
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public LoRaArduinoSerial setReceiceWindowFirst (int channel, float frequency)
{
string cmd = $"AT+RXWIN1={channel},{(short)frequency}.{(short)(frequency * 10) % 10}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public async Task setReceiceWindowFirstAsync (int channel, float frequency)
{
string cmd = $"AT+RXWIN1={channel},{(short)frequency}.{(short)(frequency * 10) % 10}\r\n";
sendCommand (cmd);
await Task.Delay (DEFAULT_TIMEWAIT);
}
public LoRaArduinoSerial setReceiceWindowSecond (float frequency, _data_rate_t dataRate)
{
string cmd = $"AT+RXWIN2={(short)frequency}.{(short)(frequency * 10) % 10},{dataRate}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public async Task setReceiceWindowSecondAsync (float frequency, _data_rate_t dataRate)
{
string cmd = $"AT+RXWIN2={(short)frequency}.{(short)(frequency * 10) % 10},{dataRate}\r\n";
sendCommand (cmd);
await Task.Delay (DEFAULT_TIMEWAIT);
}
public LoRaArduinoSerial setReceiceWindowSecond (float frequency, _spreading_factor_t spreadingFactor, _band_width_t bandwidth)
{
string cmd = $"AT+RXWIN2={(short)frequency}.{(short)(frequency * 10) % 10},{spreadingFactor},{bandwidth}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public LoRaArduinoSerial setDutyCycle (bool command)
{
if (command) sendCommand ("AT+LW=DC, ON\r\n");
else sendCommand ("AT+LW=DC, OFF\r\n");
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public async Task setDutyCycleAsync (bool command)
{
if (command) sendCommand ("AT+LW=DC, ON\r\n");
else sendCommand ("AT+LW=DC, OFF\r\n");
await Task.Delay (DEFAULT_TIMEWAIT);
}
public LoRaArduinoSerial setJoinDutyCycle (bool command)
{
if (command) sendCommand ("AT+LW=JDC,ON\r\n");
else sendCommand ("AT+LW=JDC,OFF\r\n");
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public async Task setJoinDutyCycleAsync (bool command)
{
if (command) sendCommand ("AT+LW=JDC,ON\r\n");
else sendCommand ("AT+LW=JDC,OFF\r\n");
await Task.Delay (DEFAULT_TIMEWAIT);
}
public LoRaArduinoSerial setReceiceWindowDelay (_window_delay_t command, short _delay)
{
string cmd = "";
if (command == _window_delay_t.RECEIVE_DELAY1) cmd = $"AT+DELAY=RX1,{_delay}\r\n";
else if (command == _window_delay_t.RECEIVE_DELAY2) cmd = $"AT+DELAY=RX2,{_delay}\r\n";
else if (command == _window_delay_t.JOIN_ACCEPT_DELAY1) cmd = $"AT+DELAY=JRX1,{_delay}\r\n";
else if (command == _window_delay_t.JOIN_ACCEPT_DELAY2) cmd = $"AT+DELAY=JRX2,{_delay}\r\n";
sendCommand (cmd);
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public LoRaArduinoSerial setClassType (_class_type_t type)
{
if (type == _class_type_t.CLASS_A) sendCommand ("AT+CLASS=A\r\n");
else if (type == _class_type_t.CLASS_C) sendCommand ("AT+CLASS=C\r\n");
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public LoRaArduinoSerial setDeciveMode (_device_mode_t mode)
{
if (mode == _device_mode_t.LWABP) sendCommand ("AT+MODE=LWABP\r\n");
else if (mode == _device_mode_t.LWOTAA) sendCommand ("AT+MODE=LWOTAA\r\n");
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public async Task setDeviceModeAsync (_device_mode_t mode)
{
if (mode == _device_mode_t.LWABP) sendCommand ("AT+MODE=LWABP\r\n");
else if (mode == _device_mode_t.LWOTAA) sendCommand ("AT+MODE=LWOTAA\r\n");
await Task.Delay (DEFAULT_TIMEWAIT);
}
public bool setOTAAJoin (_otaa_join_cmd_t command, int timeout)
{
if (command == _otaa_join_cmd_t.JOIN) sendCommand ("AT+JOIN\r\n");
else if (command == _otaa_join_cmd_t.FORCE) sendCommand ("AT+JOIN=FORCE\r\n");
Thread.Sleep (DEFAULT_TIMEWAIT);
DateTime start = DateTime.Now;
while (true)
{
if (ReceivedSerial(x => x.StartsWith ("+JOIN: Done")))
return true;
else if (ReceivedSerial(x => x.StartsWith ("+JOIN: LoRaWAN modem is busy")))
return false;
else if (ReceivedSerial(x => x.StartsWith ("+JOIN: Join failed")))
return false;
else if (start.AddMilliseconds (timeout) < DateTime.Now)
return false;
}
// ptr = _buffer.Contains("+JOIN: Join failed");
//if (ptr) return false;
//ptr = _buffer.Contains("+JOIN: LoRaWAN modem is busy");
//if (ptr) return false;
}
public async Task<bool> setOTAAJoinAsyncWithRetry(_otaa_join_cmd_t command, int timeoutPerTry, int retries, int delayBetweenRetries = 5000)
{
for (var attempt=1; attempt <= retries; ++attempt)
{
Console.WriteLine($"Join attempt #{attempt}/{retries}");
if (command == _otaa_join_cmd_t.JOIN)
sendCommand ("AT+JOIN\r\n");
else if (command == _otaa_join_cmd_t.FORCE)
sendCommand ("AT+JOIN=FORCE\r\n");
await Task.Delay (DEFAULT_TIMEWAIT);
DateTime start = DateTime.Now;
while (DateTime.Now.Subtract(start).TotalMilliseconds < timeoutPerTry)
{
if (ReceivedSerial((s) => s.StartsWith("+JOIN: Network joined", StringComparison.Ordinal)))
return true;
else if (ReceivedSerial(x => x.StartsWith ("+JOIN: LoRaWAN modem is busy", StringComparison.Ordinal)))
break;
else if (ReceivedSerial(x => x.StartsWith ("+JOIN: Join failed", StringComparison.Ordinal)))
break;
// wait a bit to not starve CPU, still waiting for a response from serial port
await Task.Delay(50);
}
await Task.Delay(delayBetweenRetries);
// check serial log again before sending another request
if (ReceivedSerial((s) => s.StartsWith("+JOIN: Network joined", StringComparison.Ordinal)))
return true;
}
return false;
}
public LoRaArduinoSerial setDeviceLowPower ()
{
sendCommand ("AT+LOWPOWER\r\n");
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public LoRaArduinoSerial setDeviceReset ()
{
sendCommand ("AT+RESET\r\n");
Thread.Sleep (DEFAULT_TIMEWAIT);
return this;
}
public void setDeviceDefault ()
{
sendCommand ("AT+FDEFAULT=RISINGHF\r\n");
Thread.Sleep (DEFAULT_TIMEWAIT);
}
short getBatteryVoltage ()
{
short battery = 0;
//pinMode(CHARGE_STATUS_PIN, OUTPUT);
//digitalWrite(CHARGE_STATUS_PIN, LOW);
//Thread.Sleep(DEFAULT_TIMEWAIT);
//battery = (analogRead(BATTERY_POWER_PIN) * 3300 * 11) >> 10;
//pinMode(CHARGE_STATUS_PIN, INPUT);
return battery;
}
public void sendCommand(string command)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
serialPort.Write (Encoding.UTF8.GetBytes (command));
else
serialPortWin.Write (command);
}
public void Dispose ()
{
this.serialPort?.Close();
this.serialPortWin?.Close();
this.serialPort = null;
this.serialPortWin = null;
GC.SuppressFinalize(this);
}
}
}

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

@ -0,0 +1,8 @@
namespace LoRaWan.IntegrationTest
{
public enum LoraRegion
{
EU,
US
}
}

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

@ -0,0 +1,16 @@
namespace LoRaWan.IntegrationTest
{
// Defines how NetworkServerModule log validation should occur
public enum NetworkServerModuleLogAssertLevel
{
// Ignore it
Ignore,
// Validate returning warnings if something does not work as expected
Warning,
// Threat unexpected behavior as error
Error,
}
}

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

@ -0,0 +1,205 @@
using System;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Xunit;
namespace LoRaWan.IntegrationTest
{
// Tests OTAA join requests
// OTAA joins requires the following information:
// - DevEUI: a globally unique end-device identifier
// - AppEUI: application identifier
// - AppKey: a AES-128 key
[Collection("ArduinoSerialCollection")] // run in serial
public sealed class OTAAJoinTest : IClassFixture<IntegrationTestFixture>, IDisposable
{
private readonly IntegrationTestFixture testFixture;
private readonly LoraRegion loraRegion;
private LoRaArduinoSerial lora;
public OTAAJoinTest(IntegrationTestFixture testFixture)
{
this.testFixture = testFixture;
this.loraRegion = LoraRegion.EU;
this.lora = LoRaArduinoSerial.CreateFromPort(testFixture.Configuration.LeafDeviceSerialPort);
this.testFixture.ClearNetworkServerLogEvents();
}
public void Dispose()
{
this.lora?.Dispose();
this.lora = null;
GC.SuppressFinalize(this);
}
// Ensures that an OTAA join will update the device twin
// Uses Device1_OTAA
[Fact]
public async Task OTAA_Join_With_Valid_Device_Updates_DeviceTwin()
{
var device = this.testFixture.Device1_OTAA;
Console.WriteLine($"Starting {nameof(OTAA_Join_With_Valid_Device_Updates_DeviceTwin)} using device {device.DeviceID}");
var twinBeforeJoin = await testFixture.GetTwinAsync(device.DeviceID);
await lora.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWOTAA);
await lora.setIdAsync(device.DevAddr, device.DeviceID, device.AppEUI);
await lora.setKeyAsync(device.NwkSKey, device.AppSKey, device.AppKey);
await lora.SetupLora(this.loraRegion);
var joinSucceeded = await lora.setOTAAJoinAsyncWithRetry(LoRaArduinoSerial._otaa_join_cmd_t.JOIN, 20000, 5);
if (!joinSucceeded)
{
Assert.True(joinSucceeded, "Join failed");
}
await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_JOIN);
// After join: Expectation on serial
// +JOIN: Network joined
// +JOIN: NetID 010000 DevAddr 02:9B:0D:3E
//Assert.Contains("+JOIN: Network joined", this.lora.SerialLogs);
await AssertUtils.ContainsWithRetriesAsync(
(s) => s.StartsWith("+JOIN: NetID", StringComparison.Ordinal),
this.lora.SerialLogs
);
// verify status in device twin
await Task.Delay(TimeSpan.FromSeconds(60));
var twinAfterJoin = await this.testFixture.GetTwinAsync(device.DeviceID);
Assert.NotNull(twinAfterJoin);
Assert.NotNull(twinAfterJoin.Properties.Reported);
Assert.True(twinAfterJoin.Properties.Reported.Contains("FCntUp"), "Property FCntUp does not exist");
Assert.True(twinAfterJoin.Properties.Reported.Contains("FCntDown"), "Property FCntDown does not exist");
Assert.True(twinAfterJoin.Properties.Reported.Contains("NetId"), "Property NetId does not exist");
Assert.True(twinAfterJoin.Properties.Reported.Contains("DevAddr"), "Property DevAddr does not exist");
Assert.True(twinAfterJoin.Properties.Reported.Contains("DevNonce"), "Property DevNonce does not exist");
Assert.True(twinAfterJoin.Properties.Reported.Contains("NwkSKey"), "Property NwkSKey does not exist");
Assert.True(twinAfterJoin.Properties.Reported.Contains("AppSKey"), "Property AppSKey does not exist");
Assert.True(twinAfterJoin.Properties.Reported.Contains("DevEUI"), "Property DevEUI does not exist");
var devAddrBefore = (string)twinBeforeJoin.Properties.Reported["DevAddr"];
var devAddrAfter = (string)twinAfterJoin.Properties.Reported["DevAddr"];
var actualReportedDevEUI = (string)twinAfterJoin.Properties.Reported["DevEUI"];
Assert.NotEqual(devAddrAfter, devAddrBefore);
Assert.Equal(device.DeviceID, actualReportedDevEUI);
Assert.True((twinBeforeJoin.Properties.Reported.Version) < (twinAfterJoin.Properties.Reported.Version), "Twin was not updated after join");
// Verify network server log
// 0000000000000001: join request received
await testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: join request received");
// 0000000000000001: querying the registry for device key
await testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: querying the registry for device key");
// 0000000000000001: saving join properties twins
await testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: saving join properties twins");
// 0000000000000001: join accept sent
await testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: join accept sent");
}
// Ensure that a join with an invalid DevEUI fails
// Does not need a real device, because the goal is no to have one that matches the DevEUI
// Uses Device2_OTAA
[Fact]
public async Task OTAA_Join_With_Wrong_DevEUI_Fails()
{
var device = this.testFixture.Device2_OTAA;
Console.WriteLine($"Starting {nameof(OTAA_Join_With_Wrong_DevEUI_Fails)} using device {device.DeviceID}");
await lora.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWOTAA);
await lora.setIdAsync(device.DevAddr, device.DeviceID, device.AppEUI);
await lora.setKeyAsync(device.NwkSKey, device.AppSKey, device.AppKey);
await lora.SetupLora(this.testFixture.Configuration.LoraRegion);
var joinSucceeded = await lora.setOTAAJoinAsyncWithRetry(LoRaArduinoSerial._otaa_join_cmd_t.JOIN, 20000, 5);
Assert.False(joinSucceeded, "Join suceeded for invalid DevEUI");
if (this.testFixture.Configuration.NetworkServerModuleLogAssertLevel != NetworkServerModuleLogAssertLevel.Ignore)
{
// Expected messages in log
// 0000000000000002: join request received
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: join request received");
// 0000000000000002: querying the registry for device key
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: querying the registry for device key");
// 0000000000000002: join request refused
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: join request refused");
}
}
// Ensure that a join with an invalid AppKey fails
// Uses Device3_OTAA
[Fact]
public async Task OTAA_Join_With_Wrong_AppKey_Fails()
{
var device = this.testFixture.Device3_OTAA;
Console.WriteLine($"Starting {nameof(OTAA_Join_With_Wrong_AppKey_Fails)} using device {device.DeviceID}");
var appKeyToUse = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF";
Assert.NotEqual(appKeyToUse, device.AppKey);
await lora.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWOTAA);
await lora.setIdAsync(device.DevAddr, device.DeviceID, device.AppEUI);
await lora.setKeyAsync(device.NwkSKey, device.AppSKey, appKeyToUse);
await lora.SetupLora(this.testFixture.Configuration.LoraRegion);
var joinSucceeded = await lora.setOTAAJoinAsyncWithRetry(LoRaArduinoSerial._otaa_join_cmd_t.JOIN, 20000, 5);
Assert.False(joinSucceeded, "Join suceeded for invalid AppKey");
if (this.testFixture.Configuration.NetworkServerModuleLogAssertLevel != NetworkServerModuleLogAssertLevel.Ignore)
{
// Expected messages in log
// 0000000000000002: join request received
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: join request received");
// 0000000000000002: querying the registry for device key
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: querying the registry for device key");
// 0000000000000002: join request refused
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: join request refused");
}
}
// Ensure that a join with an invalid AppKey fails
// Uses Device13_OTAA
[Fact]
public async Task OTAA_Join_With_Wrong_AppEUI_Fails()
{
var device = this.testFixture.Device13_OTAA;
Console.WriteLine($"Starting {nameof(OTAA_Join_With_Wrong_AppEUI_Fails)} using device {device.DeviceID}");
var appEUIToUse = "FF7A00000000FCE3";
Assert.NotEqual(appEUIToUse, device.AppEUI);
await lora.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWOTAA);
await lora.setIdAsync(device.DevAddr, device.DeviceID, appEUIToUse);
await lora.setKeyAsync(device.NwkSKey, device.AppSKey, device.AppKey);
await lora.SetupLora(this.testFixture.Configuration.LoraRegion);
var joinSucceeded = await lora.setOTAAJoinAsyncWithRetry(LoRaArduinoSerial._otaa_join_cmd_t.JOIN, 20000, 5);
Assert.False(joinSucceeded, "Join suceeded for invalid AppKey");
if (this.testFixture.Configuration.NetworkServerModuleLogAssertLevel != NetworkServerModuleLogAssertLevel.Ignore)
{
// Expected messages in log
// 0000000000000013: join request received
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: join request received");
// 0000000000000013: querying the registry for device key
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: querying the registry for device key");
// 0000000000000013: join request refused
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: join request refused");
// 0000000000000013: AppEUI for OTAA does not match for device
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: AppEUI for OTAA does not match for device");
}
}
}
}

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

@ -0,0 +1,109 @@
using System;
using System.Threading.Tasks;
using Xunit;
namespace LoRaWan.IntegrationTest
{
// Tests OTAA requests
[Collection("ArduinoSerialCollection")] // run in serial
public sealed class OTAATest : IClassFixture<IntegrationTestFixture>, IDisposable
{
private readonly IntegrationTestFixture testFixture;
private LoRaArduinoSerial lora;
public OTAATest(IntegrationTestFixture testFixture)
{
this.testFixture = testFixture;
this.lora = LoRaArduinoSerial.CreateFromPort(testFixture.Configuration.LeafDeviceSerialPort);
this.testFixture.ClearNetworkServerLogEvents();
}
public void Dispose()
{
this.lora?.Dispose();
this.lora = null;
GC.SuppressFinalize(this);
}
[Fact]
public async Task Test_OTAA_Confirmed_And_Unconfirmed_Message()
{
const int MESSAGES_COUNT = 10;
var device = this.testFixture.Device4_OTAA;
Console.WriteLine($"Starting {nameof(Test_OTAA_Confirmed_And_Unconfirmed_Message)} using device {device.DeviceID}");
await lora.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWOTAA);
await lora.setIdAsync(device.DevAddr, device.DeviceID, device.AppEUI);
await lora.setKeyAsync(device.NwkSKey, device.AppSKey, device.AppKey);
await lora.SetupLora(this.testFixture.Configuration.LoraRegion);
var joinSucceeded = await lora.setOTAAJoinAsyncWithRetry(LoRaArduinoSerial._otaa_join_cmd_t.JOIN, 20000, 5);
if (!joinSucceeded)
{
Assert.True(joinSucceeded, "Join failed");
}
// wait 1 second after joined
await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_JOIN);
// Sends 10x unconfirmed messages
for (var i=0; i < MESSAGES_COUNT; ++i)
{
var msg = (100 + i).ToString();
await lora.transferPacketAsync(msg, 10);
await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_SENDING_PACKET);
// After transferPacket: Expectation from serial
// +MSG: Done
await AssertUtils.ContainsWithRetriesAsync("+MSG: Done", this.lora.SerialLogs);
// 0000000000000005: valid frame counter, msg: 1 server: 0
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: valid frame counter, msg:");
// 0000000000000005: decoding with: DecoderValueSensor port: 8
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: decoding with: {device.SensorDecoder} port:");
// 0000000000000005: message '{"value": 51}' sent to hub
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: message '{{\"value\":{msg}}}' sent to hub");
this.lora.ClearSerialLogs();
testFixture.ClearNetworkServerLogEvents();
await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES);
}
// Sends 10x confirmed messages
for (var i=0; i < MESSAGES_COUNT; ++i)
{
var msg = (50 + i).ToString();
await lora.transferPacketWithConfirmedAsync(msg, 10);
await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_SENDING_PACKET);
// After transferPacketWithConfirmed: Expectation from serial
// +CMSG: ACK Received
await AssertUtils.ContainsWithRetriesAsync("+CMSG: ACK Received", this.lora.SerialLogs);
// 0000000000000005: valid frame counter, msg: 1 server: 0
//await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: valid frame counter, msg:");
// 0000000000000005: decoding with: DecoderValueSensor port: 8
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: decoding with: {device.SensorDecoder} port:");
// 0000000000000005: message '{"value": 51}' sent to hub
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: message '{{\"value\":{msg}}}' sent to hub");
this.lora.ClearSerialLogs();
testFixture.ClearNetworkServerLogEvents();
await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES);
}
}
}
}

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

@ -0,0 +1,112 @@
using System;
using System.Threading.Tasks;
using Xunit;
namespace LoRaWan.IntegrationTest
{
// Tests sensor decoding test (http, reflection)
[Collection("ArduinoSerialCollection")] // run in serial
public class SensorDecodingTest: IClassFixture<IntegrationTestFixture>, IDisposable
{
private readonly IntegrationTestFixture testFixture;
private LoRaArduinoSerial lora;
public SensorDecodingTest(IntegrationTestFixture testFixture)
{
this.testFixture = testFixture;
this.lora = LoRaArduinoSerial.CreateFromPort(testFixture.Configuration.LeafDeviceSerialPort);
this.testFixture.ClearNetworkServerLogEvents();
}
public void Dispose()
{
this.lora?.Dispose();
this.lora = null;
GC.SuppressFinalize(this);
}
// Ensures that http sensor decoder decodes payload
// Uses device Device11_OTAA
[Fact]
public async Task SensorDecoder_HttpBased_ValueSensorDecoder_DecodesPayload()
{
var device = this.testFixture.Device11_OTAA;
Console.WriteLine($"Starting {nameof(SensorDecoder_HttpBased_ValueSensorDecoder_DecodesPayload)} using device {device.DeviceID}");
await lora.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWOTAA);
await lora.setIdAsync(device.DevAddr, device.DeviceID, device.AppEUI);
await lora.setKeyAsync(device.NwkSKey, device.AppSKey, device.AppKey);
await lora.SetupLora(this.testFixture.Configuration.LoraRegion);
var joinSucceeded = await lora.setOTAAJoinAsyncWithRetry(LoRaArduinoSerial._otaa_join_cmd_t.JOIN, 20000, 5);
if (!joinSucceeded)
{
Assert.True(joinSucceeded, "Join failed");
}
// wait 1 second after joined
await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_JOIN);
await lora.transferPacketWithConfirmedAsync("1234", 10);
await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES);
// +CMSG: ACK Received
await AssertUtils.ContainsWithRetriesAsync("+CMSG: ACK Received", this.lora.SerialLogs);
// Find "0000000000000011: message '{"value":1234}' sent to hub" in network server logs
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: message '{{\"value\":1234}}' sent to hub");
this.lora.ClearSerialLogs();
testFixture.ClearNetworkServerLogEvents();
}
// Ensures that reflect based sensor decoder decodes payload
// Uses Device12_OTAA
[Fact]
public async Task SensorDecoder_ReflectionBased_ValueSensorDecoder_DecodesPayload()
{
var device = this.testFixture.Device12_OTAA;
Console.WriteLine($"Starting {nameof(SensorDecoder_HttpBased_ValueSensorDecoder_DecodesPayload)} using device {device.DeviceID}");
await lora.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWOTAA);
await lora.setIdAsync(device.DevAddr, device.DeviceID, device.AppEUI);
await lora.setKeyAsync(device.NwkSKey, device.AppSKey, device.AppKey);
await lora.SetupLora(this.testFixture.Configuration.LoraRegion);
var joinSucceeded = await lora.setOTAAJoinAsyncWithRetry(LoRaArduinoSerial._otaa_join_cmd_t.JOIN, 20000, 5);
if (!joinSucceeded)
{
Assert.True(joinSucceeded, "Join failed");
}
// wait 1 second after joined
await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_JOIN);
await lora.transferPacketWithConfirmedAsync("4321", 10);
await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES);
// +CMSG: ACK Received
await AssertUtils.ContainsWithRetriesAsync("+CMSG: ACK Received", this.lora.SerialLogs);
// Find "0000000000000011: message '{"value":1234}' sent to hub" in network server logs
await this.testFixture.ValidateNetworkServerEventLogStartsWithAsync($"{device.DeviceID}: message '{{\"value\":4321}}' sent to hub");
this.lora.ClearSerialLogs();
testFixture.ClearNetworkServerLogEvents();
}
}
}

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

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.Configuration;
namespace LoRaWan.IntegrationTest
{
public class TestConfiguration
{
public static TestConfiguration GetConfiguration()
{
var result = new TestConfiguration();
new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile("appsettings.local.json", optional: true)
.AddEnvironmentVariables()
.Build()
.GetSection("testConfiguration")
.Bind(result);
return result;
}
public string IoTHubEventHubConnectionString { get; set; }
public string IoTHubConnectionString { get; set; }
public int EnsureHasEventDelayBetweenReadsInSeconds { get; set; } = 15;
public int EnsureHasEventMaximumTries { get; set; } = 5;
public string IoTHubEventHubConsumerGroup { get; set; } = "$Default";
public string LeafDeviceSerialPort { get; set; } = "/dev/ttyACM";
public string LeafDeviceGatewayID { get; set; }
public bool CreateDevices { get; set; } = true;
public NetworkServerModuleLogAssertLevel NetworkServerModuleLogAssertLevel { get; set; } = NetworkServerModuleLogAssertLevel.Warning;
public LoraRegion LoraRegion { get; set; } = LoraRegion.EU;
}
}

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

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using Microsoft.Azure.Devices.Shared;
namespace LoRaWan.IntegrationTest
{
public class TestDeviceInfo
{
// Device ID in IoT Hub
public string DeviceID { get; set; }
// Indicates if the device actually exists
public bool RealDevice { get; set; }
// Application Identifier
// Used by OTAA devices
public string AppEUI { get; set; }
// Application Key
// Dynamically activated devices (OTAA) use the Application Key (AppKey)
// to derive the two session keys during the activation procedure
public string AppKey { get; set; }
// 32 bit device address (non-unique)
// LoRaWAN devices have a 64 bit unique identifier that is assigned to the device
// by the chip manufacturer, but communication uses 32 bit device address
// In Over-the-Air Activation (OTAA) devices performs network join, where a DevAddr and security key are negotiated with the device.
public string DevAddr { get; set; }
// Application Session Key
// Used for encryption and decryption of the payload
public string AppSKey { get; set; }
// Network Session Key
// Used for interaction between the device and Network Server.
// This key is used to check the validity of messages (MIC check)
public string NwkSKey { get; set; }
// Associated IoT Edge device
public string GatewayID { get; set; }
// Decoder used by the device
// Project supports following values: DecoderGpsSensor, DecoderTemperatureSensor, DecoderValueSensor
public string SensorDecoder { get; set; } = "DecoderValueSensor";
internal Dictionary<string, string> GetDesiredProperties()
{
var desiredProperties = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(this.AppEUI))
desiredProperties[nameof(AppEUI)] = this.AppEUI;
if (!string.IsNullOrEmpty(this.AppKey))
desiredProperties[nameof(AppKey)] = this.AppKey;
if (!string.IsNullOrEmpty(this.GatewayID))
desiredProperties[nameof(GatewayID)] = this.GatewayID;
if (!string.IsNullOrEmpty(this.SensorDecoder))
desiredProperties[nameof(SensorDecoder)] = this.SensorDecoder;
if (!string.IsNullOrEmpty(this.AppSKey))
desiredProperties[nameof(AppSKey)] = this.AppSKey;
if (!string.IsNullOrEmpty(this.NwkSKey))
desiredProperties[nameof(NwkSKey)] = this.NwkSKey;
if (!string.IsNullOrEmpty(this.DevAddr))
desiredProperties[nameof(DevAddr)] = this.DevAddr;
return desiredProperties;
}
}
}

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

@ -0,0 +1,17 @@
{
"testConfiguration": {
"IoTHubEventHubConnectionString": "#{INTEGRATIONTEST_IoTHubEventHubConnectionString}#",
"IoTHubEventHubConsumerGroup": "#{INTEGRATIONTEST_IoTHubEventHubConsumerGroup}#",
"IoTHubConnectionString": "#{INTEGRATIONTEST_IoTHubConnectionString}#",
"EnsureHasEventDelayBetweenReadsInSeconds": "#{INTEGRATIONTEST_EnsureHasEventDelayBetweenReadsInSeconds}#",
"EnsureHasEventMaximumTries": "#{INTEGRATIONTEST_EnsureHasEventMaximumTries}#",
"LeafDeviceSerialPort": "#{INTEGRATIONTEST_LeafDeviceSerialPort}#",
"LeafDeviceOTAAId": "#{INTEGRATIONTEST_LeafDeviceOTAAId}#",
"LeafDeviceAppKey": "#{INTEGRATIONTEST_LeafDeviceAppKey}#",
"LeafDeviceAppEui": "#{INTEGRATIONTEST_LeafDeviceAppEui}#",
"LeafDeviceABPId": "#{INTEGRATIONTEST_LeafDeviceABPId}#",
"LeafDeviceABPAddr": "#{INTEGRATIONTEST_LeafDeviceABPAddr}#",
"LeafDeviceABPAppSKey": "#{INTEGRATIONTEST_LeafDeviceABPAppSKey}#",
"LeafDeviceABPNetworkSKey": "#{INTEGRATIONTEST_LeafDeviceABPNetworkSKey}#"
}
}

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

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SensorDecoderModule.Classes
{
internal static class LoraDecoders
{
private static string DecoderValueSensor(byte[] payload, uint fport)
{
var result = Encoding.ASCII.GetString(payload);
return JsonConvert.SerializeObject(new { value = result });
}
}
}

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

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
namespace SensorDecoderModule.Classes
{
public static class Validator
{
public static void ValidateParameters(string fport, string payload)
{
var error = "";
if (fport == null)
{
error += "Fport missing";
}
if (payload == null)
{
if (error != "")
error += " and ";
error += "Payload missing";
}
if (error != "")
{
throw new WebException(error);
}
}
}
}

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

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Net;
using SensorDecoderModule.Classes;
using System.Reflection;
using System.Text;
namespace SensorDecoderModule.Controllers
{
[Route("api")]
[ApiController]
public class DecoderController : ControllerBase
{
// GET: api/TestDecoder
[HttpGet("{decoder}", Name = "Get")]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
public ActionResult<string> Get(string decoder, string fport, string payload)
{
// Validate that fport and payload URL parameters are present.
Validator.ValidateParameters(fport, payload);
Type decoderType = typeof(LoraDecoders);
MethodInfo toInvoke = decoderType.GetMethod(decoder, BindingFlags.Static | BindingFlags.NonPublic);
if (toInvoke != null)
{
return (string)toInvoke.Invoke(null, new object[] { Encoding.UTF8.GetBytes(payload), Convert.ToUInt16(fport) });
}
else
{
throw new WebException( $"Decoder {decoder} not found.");
}
}
}
}

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

@ -0,0 +1,16 @@
FROM microsoft/dotnet:2.1-sdk AS build
WORKDIR /app
# copy csproj and restore as distinct layers
COPY *.csproj .
RUN dotnet restore
# copy everything else and build app
COPY . ./
WORKDIR /app
RUN dotnet publish -c Release -o out
FROM microsoft/dotnet:2.1-aspnetcore-runtime AS runtime
WORKDIR /app
COPY --from=build /app/out ./
ENTRYPOINT ["dotnet", "SensorDecoderModule.dll"]

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

@ -0,0 +1,16 @@
FROM microsoft/dotnet:2.1-sdk AS build
WORKDIR /app
# copy csproj and restore as distinct layers
COPY *.csproj .
RUN dotnet restore
# copy everything else and build app
COPY . ./
WORKDIR /app
RUN dotnet publish -c Release -o out
FROM microsoft/dotnet:2.1-aspnetcore-runtime-stretch-slim-arm32v7 AS runtime
WORKDIR /app
COPY --from=build /app/out ./
ENTRYPOINT ["dotnet", "SensorDecoderModule.dll"]

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

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace SensorDecoderModule
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseKestrel(x => x.Limits.KeepAliveTimeout = TimeSpan.FromDays(10));
}
}

27
Samples/DecoderSample/ReadMe.md Executable file
Просмотреть файл

@ -0,0 +1,27 @@
To add a new decoder, simply copy one of the sample methods from *LoraDecoders.cs*. The payload sent to the decoder is passed as byte[] payload and the Fport as uint fport.
After decoding the message, return a string containing the response to be sent upstream.
Call the decoder by:
http://containername/api/decodername?fport=X&payload=XXXXXXXXX if it is running on the same machine running LoRaWAN IoT Edge engine.
http://machinename:port/api/decodername?fport=X&payload=XXXXXXXXX if it is running on a seperate machine.
To be able to call container webserver only from other containers port 80:
{
"ExposedPorts": {
"80/tcp": {}
}
}
OLD: To be able to call container webserver on machine port 8881:
{
"ExposedPorts": {
"80/tcp": {}
},
"HostConfig": {
"PortBindings": {
"80/tcp": [{
"HostPort": "8881"
}]
}
}
}

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

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.1.2" PrivateAssets="All" />
</ItemGroup>
</Project>

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

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.28010.2050
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SensorDecoderModule", "SensorDecoderModule.csproj", "{067DCA55-90EA-47C0-ABFA-B9CB32603992}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{067DCA55-90EA-47C0-ABFA-B9CB32603992}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{067DCA55-90EA-47C0-ABFA-B9CB32603992}.Debug|Any CPU.Build.0 = Debug|Any CPU
{067DCA55-90EA-47C0-ABFA-B9CB32603992}.Release|Any CPU.ActiveCfg = Release|Any CPU
{067DCA55-90EA-47C0-ABFA-B9CB32603992}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0363BFE0-78B2-4322-BC4F-5251526C9B06}
EndGlobalSection
EndGlobal

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

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace SensorDecoderModule
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
//app.UseHsts();
}
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = 400; // We are using HTTP Status Code 400 - Bad Request.
context.Response.ContentType = "text/plain";
var error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null)
{
var ex = error.Error;
string exMessage;
if (ex.InnerException != null)
exMessage = $"Decoder error: {ex.InnerException.Message}";
else
exMessage = ex.Message;
Console.WriteLine($"Exception at: {System.DateTime.UtcNow}: {exMessage}");
await context.Response.WriteAsync(exMessage, Encoding.UTF8);
}
});
});
//app.UseHttpsRedirection();
app.UseMvc();
}
}
}

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

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

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

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}