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:
Родитель
6572d8d6ab
Коммит
1f6fdd4b37
|
@ -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));
|
||||
|
||||
}
|
||||
}
|
|
@ -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": "*"
|
||||
}
|
Загрузка…
Ссылка в новой задаче