[feature] add Modbus RTU connectivity

[update] doc
This commit is contained in:
Stephen Chen (ManPower) 2017-12-04 12:39:34 +08:00
Родитель 5b4e304359
Коммит 20aabc5766
7 изменённых файлов: 1124 добавлений и 703 удалений

249
README.md
Просмотреть файл

@ -1,89 +1,127 @@
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments
# Azure IoT Edge Modbus Module Preview #
Using this module, developers can build Azure IoT Edge solutions with Modbus TCP connectivity. The Modbus module is an [Azure IoT Edge](https://github.com/Azure/iot-edge) module, capable of reading data from Modbus devices and publishing data to the Azure IoT Hub via the Edge framework. Developers can modify the module tailoring to any scenario.
![](./doc/diagram.png)
If you are using V1 version of IoT Edge (previously known as Azure IoT Gateway), please use V1 version of this module, all materials can be found in [V1](https://github.com/Azure/iot-gateway-modbus/tree/master/V1) folder.
Using this module, developers can build Azure IoT Edge solutions with Modbus TCP/RTU connectivity. The Modbus module is an [Azure IoT Edge](https://github.com/Azure/iot-edge) module, capable of reading data from Modbus devices and publishing data to the Azure IoT Hub via the Edge framework. Developers can modify the module tailoring to any scenario. Alternatively, the module can also be run in standalone mode for debug purpose, which doesn't require IoT Edge framework.
![](./doc/diagram.png)
There are prebuilt Modbus TCP module container images ready at [microsoft/azureiotedge-modbus-tcp:1.0-preview](https://hub.docker.com/r/microsoft/azureiotedge-modbus-tcp) for you to quickstart the experience of Azure IoT Edge on your target device or simulated device.
Visit http://azure.com/iotdev to learn more about developing applications for Azure IoT.
## Azure IoT Edge Compatibility ##
Current version of the module is targeted for the Azure IoT Edge 1.0 preview version.
Current version of the module is targeted for the [Azure IoT Edge (second version in public preview)](https://github.com/Azure/azure-iot-edge).
If you are using [v1 version of IoT Edge](https://github.com/Azure/iot-edge/tree/master/v1) (previously known as Azure IoT Gateway), please use v1 version of this module, all materials can be found in [v1](https://github.com/Azure/iot-gateway-modbus/tree/master/v1) folder.
## Operating System Compatibility ##
Refer to [Azure IoT Edge](https://github.com/Azure/azure-iot-edge)
Find more information about Azure IoT Edge at [here](https://docs.microsoft.com/en-us/azure/iot-edge/how-iot-edge-works).
## Hardware Compatibility ##
Refer to [Azure IoT Edge](https://github.com/Azure/azure-iot-edge)
## Target Device Setup ##
## HowTo Run ##
This section will help you download the prebuilt module image from docker hub, and run it with IoT Edge directly.
1. Setup Azure IoT Edge [Windows](https://docs.microsoft.com/en-us/azure/iot-edge/quickstart) or [Linux](https://docs.microsoft.com/en-us/azure/iot-edge/quickstart-linux) with compatible version on your machine.
2. Follow [this](https://docs.microsoft.com/en-us/azure/iot-edge/tutorial-deploy-modbus-tcp) to deploy a custom IoT Edge module.
3. In the Image field, enter **microsoft/azureiotedge-modbus-tcp:1.0-preview**.
4. You may also want to provide configuration to the module when it starts, paste the configuration in the desired property field. For more about configuration, see [here](https://github.com/Azure/iot-edge-modbus#configuration).
### Platform Compatibility ###
Azure IoT Edge is designed to be used with a broad range of operating system platforms. Modbus module has been tested on the following platforms:
- Windows 10 Enterprise (version 1709) x64
- Windows 10 IoT Core (version 1709) x64
- Linux x64
- Linux arm32v7
### Device Setup ###
- [Windows 10 Desktop](https://docs.microsoft.com/en-us/azure/iot-edge/quickstart)
- [Windows 10 IoT Core](https://docs.microsoft.com/en-us/azure/iot-edge/how-to-install-iot-core)
- [Linux](https://docs.microsoft.com/en-us/azure/iot-edge/quickstart-linux)
## Build Environment Setup ##
Modbus module is a .NET Core 2.0 application, which is developed and built based on the guidelines in Azure IoT Edge document.
Please follow [this link](https://docs.microsoft.com/en-us/azure/iot-edge/tutorial-csharp-module) to setup the build environment.
Basic requirement:
- Docker CE
- .NET Core 2.0 SDK (optional, if you prefer to manually build application on host machine)
## HowTo Build ##
If you prefer to build your own module, use the following script. Dockerfiles are located under [Docker](https://github.com/Azure/iot-edge-modbus/tree/master/Docker) folder, you should be able to find one for your platform. There are two Dockerfiles in each platform, the multi-stage "Dockerfile-auto" will automatically build source code and Docker image. The other "Dockerfile" requires you to build source code first and then copy binary to the image.
**Note**: Arm32 multi-stage build doesn't work at this moment, please build it manually.
**Note**: Please replace **PlatForm** in below scripts with the actual platform path you are trying to build.
In this section, the Modbus TCP module we be built as an IoT Edge module.
### Multi-stage build ###
```cmd
>cd iot-edge-modbus/
>docker build -t modbusModule -f Docker/<PlatForm>/Dockerfile-auto .
Dockerfiles are located under [Docker](https://github.com/Azure/iot-edge-modbus/tree/master/Docker) folder, you should be able to find one for your platform. There are two Dockerfiles in each platform, choose either one for your build preference.
- "Dockerfile-auto": For multi-stage build, which will build application inside container and generate a target container image.
- "Dockerfile": This requires you to build applicaiton on host machine and then use this Dockerfile to copy application binary to generate target container image.
**Note**: [Multi-stage](https://github.com/Azure/iot-edge-modbus#multi-stage-build) build for Linux-arm32 doesn't work at this moment, please do with [single-stage](https://github.com/Azure/iot-edge-modbus#single-stage-build) build.
**Note**: Please replace \<platform\> in below scripts with the actual platform path you are trying to build.
### Build as an IoT Edge module ###
1. Open "Program.cs" file in [src](https://github.com/Azure/iot-edge-modbus/tree/master/src) folder.
2. Confirm **IOT_EDGE** flag is enabled at first line code.
```csharp
#define IOT_EDGE
```
### Manually build ###
The application requires the [.NET Core SDK 2.0](https://www.microsoft.com/net/download/windows).
```cmd
>cd iot-edge-modbus/src/
>dotnet restore
>dotnet build
>dotnet publish -f netcoreapp2.0
>cd ../
>docker build --build-arg EXE_DIR=./src/bin/Debug/netcoreapp2.0/publish -t modbusModule -f Docker/<PlatForm>/Dockerfile .
#### Multi-stage build ###
Run docker build with the following commands.
```sh
$cd iot-edge-modbus/
$docker build -t "modbus:latest" -f Docker/<PlatForm>/Dockerfile-auto .
```
#### Single-stage build ####
Run docker build with the following commands.
```sh
$cd iot-edge-modbus/src/
$dotnet restore
$dotnet build
$dotnet publish -f netcoreapp2.0 -c Release
$cd ../
$docker build --build-arg EXE_DIR=./src/bin/Release/netcoreapp2.0/publish -t "modbus:latest" -f Docker/<PlatForm>/Dockerfile .
```
## Configuration ##
The Modbus module uses module twin as its configuration. Here is a sample configuration for your reference.
Before running the module, proper configuration is required. Here is a sample configuration for your reference.
```json
{
"Interval": "1500",
"PublishInterval": "2000",
"SlaveConfigs": {
"Slave01": {
"SlaveConnection": "192.168.0.1",
"HwId": "PowerMeter-0a:01:01:01:01:01",
"Operations": {
"Op01": {
"PollingInterval": "1000",
"UnitId": "1",
"StartAddress": "400001",
"Count": "2",
"Count": "1",
"DisplayName": "Voltage"
},
"Op02": {
"PollingInterval": "1000",
"UnitId": "1",
"StartAddress": "400002",
"Count": "2",
"Count": "1",
"DisplayName": "Current"
}
}
},
"Slave02": {
"SlaveConnection": "192.168.0.2",
"HwId": "PowerMeter-0a:01:01:01:01:02",
"SlaveConnection": "ttyS0",
"HwId": "PowerMeter-0a:01:01:01:01:02",
"BaudRate": "9600",
"DataBits": "8",
"StopBits": "1",
"Parity": "ODD",
"FlowControl": "NONE",
"Operations": {
"Op01": {
"PollingInterval": "2000",
"UnitId": "1",
"StartAddress": "40001",
"Count": "2",
"DisplayName": "Voltage"
"StartAddress": "40003",
"Count": "1",
"DisplayName": "Power"
},
"Op02": {
"PollingInterval": "2000",
"UnitId": "1",
"StartAddress": "40002",
"Count": "2",
"DisplayName": "Current"
"StartAddress": "40004",
"Count": "1",
"DisplayName": "Status"
}
}
}
@ -92,54 +130,127 @@ The Modbus module uses module twin as its configuration. Here is a sample config
```
Meaning of each field:
* "Interval" – Interval between each push to IoT Hub in millisecond
* "PublishInterval" – Interval between each push to IoT Hub in millisecond
* "SlaveConfigs" – Contains one or more Modbus slaves' configuration. In this sample, we have "Slave01" and "Slave02" two devices:
* "Slave01", "Slave02" - User defined names for each Modbus slave, cannot have duplicates
* "SlaveConnection" – IPV4 address of the Modbus slave
* "HwId" – Unique Id for each Modbus slave (user defined)
* "Operations" – Contains one or more Modbus read requests. In this sample, we have "Op01" and "Op02" two read requests in both Slave01 and Slave02:
* "UnitId" – The unit id to be read
* "StartAddress" – The starting address of Modbus read request, currently supports both 5-digit and 6-digit [format](https://en.wikipedia.org/wiki/Modbus#Coil.2C_discrete_input.2C_input_register.2C_holding_register_numbers_and_addresses)
* "Count" – Number of registers/bits to be read
* "DisplayName" – Alternative name for the "StartAddress" register(s)(user defined)
* "Op01", "Op02" - User defined names for each read request, cannot have duplicates under the same "SlaveConfig"
* "Slave01", "Slave02" - User defined names for each Modbus slave, cannot have duplicates under "SlaveConfigs".
* "SlaveConnection" – Ipv4 address or the serial port name of the Modbus slave
* "HwId" – Unique Id for each Modbus slave (user defined)
* "BaudRate" – Serial port communication parameter. (valid values: ...9600, 14400,19200...)
* "DataBits" – Serial port communication parameter. (valid values: 7, 8)
* "StopBits" – Serial port communication parameter. (valid values: 1, 1.5, 2)
* "Parity" – Serial port communication parameter. (valid values: ODD, EVEN, NONE)
* "FlowControl" – Serial port communication parameter. (valid values: only support NONE now)
* "Operations" – Contains one or more Modbus read requests. In this sample, we have "Op01" and "Op02" two read requests in both Slave01 and Slave02:
* "Op01", "Op02" - User defined names for each read request, cannot have duplicates under the same "Operations" section.
* "PollingInterval": Interval between each read request in millisecond
* "UnitId" – The unit id to be read
* "StartAddress" – The starting address of Modbus read request, currently supports both 5-digit and 6-digit [format](https://en.wikipedia.org/wiki/Modbus#Coil.2C_discrete_input.2C_input_register.2C_holding_register_numbers_and_addresses)
* "Count" – Number of registers/bits to be read
* "DisplayName" – Alternative name for the "StartAddress" register(s)(user defined)
For more about Modbus, please refer to the [Wiki](https://en.wikipedia.org/wiki/Modbus) link.
## Module Endpoints and Routing ##
All telemetry are sent out from modbusOutput endpoint by default. Routing is enabled by specifying rules like below.
There are two endpoints defined in Modbus TCP module:
- "modbusOutput": This is a output endpoint for telemetries. All read operations defined in configuration will be composed as telemetry messages output to this endpoint.
- "input1": This is an input endpoint for write commands.
### Route to IoT Hub ###
Input/Output message format and Routing rules are introduced below.
### Read from Modbus ###
#### Telemetry Message ####
Message Properties:
```json
"content-type": "application/edge-modbus-json"
```
Message Payload:
```json
[
{
"DisplayName":"Voltage",
"HwId":"PowerMeter-0a:01:01:01:01:01",
"Address":"400001",
"Value":"19775",
"SourceTimestamp":"2017-11-17 08:43:50"
},
{
"DisplayName":"Current",
"HwId":"PowerMeter-0a:01:01:01:01:01",
"Address":"400002",
"Value":"15650",
"SourceTimestamp":"2017-11-17 08:43:50"
}
]
```
#### Route to IoT Hub ####
```json
{
"modbusToIoTHub":"FROM /messages/modules/modbus/outputs/modbusOutput INTO $upstream"
"rotues": {
"modbusToIoTHub":"FROM /messages/modules/modbus/outputs/modbusOutput INTO $upstream"
}
}
```
### Route to other (filter) modules ###
#### Route to other (filter) modules ####
```json
{
"modbusToFilter":"FROM /messages/modules/modbus/outputs/modbusOutput INTO BrokeredEndpoint(\"/modules/filtermodule/inputs/input1\")"
"rotues": {
"modbusToFilter":"FROM /messages/modules/modbus/outputs/modbusOutput INTO BrokeredEndpoint(\"/modules/filtermodule/inputs/input1\")"
}
}
```
### Write to Modbus ###
Modbus module also has an input enpoint to receive message/commands. Currently it supports writing back to a single register/cell in a Modbus slave. The content of command must be the following format.
Modbus module use input endpoint "input1" to receive commands. Currently it supports writing back to a single register/cell in a Modbus slave.
#### Command Message ####
The content of command must be the following message format.
Message Properties:
```json
{
"HwId":"PowerMeter-0a:01:01:01:01:01",
"UId":"1",
"Address":"40001",
"Value":"15"
}
"command-type": "ModbusWrite"
```
The command should have a property "command-type" with value "ModbusWrite". Also, routing must be enabled by specifying rule like below.
Message Payload:
```json
{
"filterToModbus":"FROM /messages/modules/filtermodule/outputs/output1 INTO BrokeredEndpoint(\"/modules/modbus/inputs/input1\")"
"HwId":"PowerMeter-0a:01:01:01:01:01",
"UId":"1",
"Address":"40001",
"Value":"15"
}
```
## Debug ##
There is a flag **IOT_EDGE** at the first line in Program.cs, which can be turn off to debug the Modbus module in console mode. Running console mode requires IoT device connection string being inserted as a environment variable named **EdgeHubConnectionString**, and a local configuration file "iot-edge-modbus.json" since module twin is not available. You can copy "iot-edge-modbus.json" template from project root directory.
**Note**: running in console mode means none of the IoT Edge features is available. This mode is only to debug non edge-related functions.
#### Route from other (filter) modules ####
The command should have a property "command-type" with value "ModbusWrite". Also, routing must be enabled by specifying rule like below.
```json
{
"rotues": {
"filterToModbus":"FROM /messages/modules/filtermodule/outputs/output1 INTO BrokeredEndpoint(\"/modules/modbus/inputs/input1\")"
}
}
```
## HowTo Run ##
### Run as an IoT Edge module ###
Please follow the [link](https://docs.microsoft.com/en-us/azure/iot-edge/tutorial-csharp-module) to deploy the module as an IoT Edge module.
## Debug from Visual Studio 2017 ##
Running debug mode requires IoT device connection string being inserted as a environment variable named **EdgeHubConnectionString**, and a local configuration file "iot-edge-modbus.json" since module twin is not available under debug mode. You can copy "iot-edge-modbus.json" template from project root directory to working directory and modify the content to fit your test case.
**Note**: running in debug mode means none of the IoT Edge features is available. This mode is only to debug non edge-related functions.
1. Open "iot-edge-modbus.csproj" with **Visual Studio 2017**.
2. Open "Program.cs" file in [src](https://github.com/Azure/iot-edge-modbus/tree/master/src) folder.
3. Confirm **IOT_EDGE** flag is disabled at first line code.
```csharp
// #define IOT_EDGE
```
4. Press **Ctrl+Shift+B** to build project.
5. Go to **Project** and select **iot-edge-modbus Properties...**
6. Switch to **Debug** tab.
7. Add **Environment variables** with **Name** = "EdgeHubConnectionString", and **Value** = "\<your IoT device connection string\>". The connection string can be found from Azure Portal.
8. Press F5 to run project.

Двоичные данные
doc/diagram.png

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 48 KiB

После

Ширина:  |  Высота:  |  Размер: 48 KiB

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

@ -1,17 +1,23 @@
{
"Interval": "push interval in millisecond",
"SlaveConfigs": {
"Slave01": {
"SlaveConnection": "ipv4 address to Modbus device",
"HwId": "unique HW Id defined by user",
"Operations": {
"Op01": {
"UnitId": "unit id of Modbus device",
"StartAddress": "starting address of read request",
"Count": "unit count of read request",
"DisplayName": "alternative name defined by user"
}
}
}
}
{
"PublishInterval": "push interval in millisecond",
"SlaveConfigs": {
"Slave01": {
"SlaveConnection": "ipv4 address or the serial port name to Modbus device",
"HwId": "unique HW Id defined by user",
"BaudRate": "baud rate of serial communication (Modbus RTU only)",
"DataBits": "data bits of serial communication (Modbus RTU only)",
"StopBits": "stop bits of serial communication (Modbus RTU only)",
"Parity": "parity of serial communication (Modbus RTU only)",
"FlowControl": "flow control of serial communication (Modbus RTU only)",
"Operations": {
"Op01": {
"PollingInterval": "polling interval in millisecond",
"UnitId": "unit id of Modbus device",
"StartAddress": "starting address of read request",
"Count": "unit count of read request",
"DisplayName": "alternative name defined by user"
}
}
}
}
}

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

@ -1,41 +1,46 @@
{
"Interval": "1500",
"SlaveConfigs": {
"Slave01": {
"SlaveConnection": "127.0.0.1",
"HwId": "PowerMeter-0a:01:01:01:01:01",
"Operations": {
"Op01": {
"UnitId": "1",
"StartAddress": "400001",
"Count": "2",
"DisplayName": "Voltage"
},
"Op02": {
"UnitId": "1",
"StartAddress": "400002",
"Count": "2",
"DisplayName": "Current"
}
}
},
"Slave02": {
"SlaveConnection": "127.0.0.1",
"HwId": "PowerMeter-0a:01:01:01:01:02",
"Operations": {
"Op01": {
"UnitId": "1",
"StartAddress": "40001",
"Count": "2",
"DisplayName": "Voltage"
},
"Op02": {
"UnitId": "1",
"StartAddress": "40002",
"Count": "2",
"DisplayName": "Current"
}
}
}
}
{
"Interval": "1500",
"SlaveConfigs": {
"Slave01": {
"SlaveConnection": "127.0.0.1",
"HwId": "PowerMeter-0a:01:01:01:01:01",
"Operations": {
"Op01": {
"UnitId": "1",
"StartAddress": "400001",
"Count": "2",
"DisplayName": "Voltage"
},
"Op02": {
"UnitId": "1",
"StartAddress": "400002",
"Count": "2",
"DisplayName": "Current"
}
}
},
"Slave02": {
"SlaveConnection": "ttyS1",
"HwId": "PowerMeter-0a:01:01:01:01:02",
"BaudRate": "9600",
"DataBits": "8",
"StopBits": "1",
"Parity": "ODD",
"FlowControl": "NONE",
"Operations": {
"Op01": {
"UnitId": "1",
"StartAddress": "40001",
"Count": "2",
"DisplayName": "Voltage"
},
"Op02": {
"UnitId": "1",
"StartAddress": "40002",
"Count": "2",
"DisplayName": "Current"
}
}
}
}
}

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

@ -1,6 +1,4 @@

namespace Modbus.Slaves
namespace Modbus.Slaves
{
using System;
using System.Collections.Generic;
@ -8,13 +6,30 @@ namespace Modbus.Slaves
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using System.IO.Ports;
/* Modbus Frame Details
----------------------- --------
|MBAP Header description|Length |
----------------------- --------
|Transaction Identifier |2 bytes |
----------------------- --------
|Protocol Identifier |2 bytes |
----------------------- --------
|Length 2bytes |2 bytes |
----------------------- --------
|Unit Identifier |1 byte |
----------------------- --------
|Body |variable|
----------------------- --------
*/
/// <summary>
/// This class contains the handle for this module. In this case, it is a list of active Modbus sessions.
/// </summary>
class ModuleHandle
{
public static async Task<ModuleHandle> CreateHandleFromConfiguration(ModuleConfig config)
public static async Task<ModuleHandle> CreateHandleFromConfiguration(ModuleConfig config, ModbusSlaveSession.HandleResultDelegate resultHandler)
{
Modbus.Slaves.ModuleHandle moduleHandle = null;
foreach (var config_pair in config.SlaveConfigs)
@ -29,13 +44,21 @@ namespace Modbus.Slaves
moduleHandle = new Modbus.Slaves.ModuleHandle();
}
ModbusSlaveSession slave = new ModbusTCPSlaveSession(slaveConfig);
ModbusSlaveSession slave = new ModbusTCPSlaveSession(slaveConfig, resultHandler);
await slave.InitSession();
moduleHandle.ModbusSessionList.Add(slave);
break;
}
case ModbusConstants.ConnectionType.ModbusRTU:
{
if (moduleHandle == null)
{
moduleHandle = new Modbus.Slaves.ModuleHandle();
}
ModbusSlaveSession slave = new ModbusRTUSlaveSession(slaveConfig, resultHandler);
await slave.InitSession();
moduleHandle.ModbusSessionList.Add(slave);
break;
}
case ModbusConstants.ConnectionType.ModbusASCII:
@ -64,88 +87,32 @@ namespace Modbus.Slaves
ModbusSessionList.Clear();
}
}
/// <summary>
/// Base class of Modbus session.
/// </summary>
abstract class ModbusSlaveSession
{
public ModbusSlaveConfig config;
public abstract Task<List<ModbusOutMessage>> ProcessOperations();
public abstract Task WriteCB(string uid, string address, string value);
public abstract Task InitSession();
public abstract void ReleaseSession();
}
/*
----------------------- --------
|MBAP Header description|Length |
----------------------- --------
|Transaction Identifier |2 bytes |
----------------------- --------
|Protocol Identifier |2 bytes |
----------------------- --------
|Length 2bytes |2 bytes |
----------------------- --------
|Unit Identifier |1 byte |
----------------------- --------
|Body |variable|
----------------------- --------
*/
/// <summary>
/// This class is Modbus TCP session.
/// </summary>
class ModbusTCPSlaveSession : ModbusSlaveSession
{
protected HandleResultDelegate messageDelegate;
protected const int m_bufSize = 512;
protected bool m_run = false;
protected List<Task> m_taskList = new List<Task>();
protected virtual int m_reqSize { get; }
protected virtual int m_dataBodyOffset { get; }
#region Constructors
public ModbusTCPSlaveSession(ModbusSlaveConfig conf)
public ModbusSlaveSession(ModbusSlaveConfig conf, HandleResultDelegate resultHandler)
{
config = conf;
}
messageDelegate = resultHandler;
}
#endregion
#region Private Properties
private const int m_reqSize = 12;
private const int m_bufSize = 512;
private const int m_tcpPort = 502;
private const int m_tcpOffset = 7;
private object m_socketLock = new object();
private Socket m_socket = null;
private IPAddress m_address = null;
#endregion
#region Public Methods
public override async Task WriteCB(string uid, string address, string value)
{
byte[] writeRequest = new byte[m_bufSize];
byte[] writeResponse;
int reqLen = m_reqSize;
EncodeWrite(writeRequest, uid, address, value);
writeResponse = await SendRequest(writeRequest, reqLen);
}
public override async Task<List<ModbusOutMessage>> ProcessOperations()
{
List<ModbusOutMessage> result = new List<ModbusOutMessage>();
foreach (var op_pair in config.Operations)
{
ReadOperation x = op_pair.Value;
x.Response = await SendRequest(x.Request, x.RequestLen);
if (x.Response != null)
{
if (x.Request[m_tcpOffset] == x.Response[m_tcpOffset])
{
var msg = ProcessResponse(config, x);
result.AddRange(msg);
}
else if (x.Request[m_tcpOffset] + 0x80 == x.Response[m_tcpOffset])
{
Console.WriteLine($"Modbus exception code: {x.Response[m_tcpOffset + 1]}");
}
}
x.Response = null;
}
return result;
}
public override async Task InitSession()
public abstract void ReleaseSession();
public delegate void HandleResultDelegate(ModbusOutMessage message);
public async Task InitSession()
{
await ConnectSlave();
@ -169,36 +136,102 @@ namespace Modbus.Slaves
EncodeRead(x);
}
}
public async Task WriteCB(string uid, string address, string value)
{
byte[] writeRequest = new byte[m_bufSize];
byte[] writeResponse = null;
int reqLen = m_reqSize;
public override void ReleaseSession()
{
if (m_socket != null)
{
m_socket.Disconnect(false);
m_socket.Dispose();
m_socket = null;
}
EncodeWrite(writeRequest, uid, address, value);
writeResponse = await SendRequest(writeRequest, reqLen);
}
public void ProcessOperations()
{
m_run = true;
foreach (var op_pair in config.Operations)
{
ReadOperation x = op_pair.Value;
Task t = Task.Run(async () => await SingleOperation(x));
m_taskList.Add(t);
}
}
#endregion
#region Private Methods
private async Task ConnectSlave()
{
if (IPAddress.TryParse(config.SlaveConnection, out m_address))
{
try
{
m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await m_socket.ConnectAsync(m_address, m_tcpPort);
}
catch (Exception e)
{
Console.WriteLine("Connect Slave failed");
Console.WriteLine(e.Message);
m_socket = null;
}
}
#region Protected Methods
protected abstract void EncodeWrite(byte[] request, string uid, string address, string value);
protected abstract Task<byte[]> SendRequest(byte[] request, int reqLen);
protected abstract Task ConnectSlave();
protected abstract void EncodeRead(ReadOperation operation);
protected async Task SingleOperation(ReadOperation x)
{
while (m_run)
{
x.Response = null;
x.Response = await SendRequest(x.Request, x.RequestLen);
if (x.Response != null)
{
if (x.Request[m_dataBodyOffset] == x.Response[m_dataBodyOffset])
{
ProcessResponse(config, x);
}
else if (x.Request[m_dataBodyOffset] + 0x80 == x.Response[m_dataBodyOffset])
{
Console.WriteLine($"Modbus exception code: {x.Response[m_dataBodyOffset + 1]}");
}
}
await Task.Delay(x.PollingInterval);
}
}
private bool ParseEntity(string startAddress, bool isRead, out ushort outAddress, out byte functionCode, out byte entityType)
protected void ProcessResponse(ModbusSlaveConfig config, ReadOperation x)
{
int count = 0;
int step_size = 0;
int start_digit = 0;
switch (x.Response[m_dataBodyOffset])//function code
{
case (byte)ModbusConstants.FunctionCodeType.ReadCoils:
case (byte)ModbusConstants.FunctionCodeType.ReadInputs:
{
count = x.Response[m_dataBodyOffset + 1] * 8;
count = (count > x.Count) ? x.Count : count;
step_size = 1;
start_digit = x.Response[m_dataBodyOffset] - 1;
break;
}
case (byte)ModbusConstants.FunctionCodeType.ReadHoldingRegisters:
case (byte)ModbusConstants.FunctionCodeType.ReadInputRegisters:
{
count = x.Response[m_dataBodyOffset + 1];
step_size = 2;
start_digit = (x.Response[m_dataBodyOffset] == 3) ? 4 : 3;
break;
}
}
for (int i = 0; i < count; i += step_size)
{
string res = "";
string cell = "";
string val = "";
if (step_size == 1)
{
cell = string.Format(x.OutFormat, (char)x.EntityType, x.Address + i + 1);
val = string.Format("{0}", (x.Response[m_dataBodyOffset + 2 + (i / 8)] >> (i % 8)) & 0b1);
}
else if (step_size == 2)
{
cell = string.Format(x.OutFormat, (char)x.EntityType, x.Address + (i / 2) + 1);
val = string.Format("{0,00000}", ((x.Response[m_dataBodyOffset + 2 + i]) * 0x100 + x.Response[m_dataBodyOffset + 3 + i]));
}
res = cell + ": " + val + "\n";
Console.WriteLine(res);
ModbusOutMessage message = new ModbusOutMessage()
{ HwId = config.HwId, DisplayName = x.DisplayName, Address = cell, Value = val, SourceTimestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") };
messageDelegate(message);
}
}
protected bool ParseEntity(string startAddress, bool isRead, out ushort outAddress, out byte functionCode, out byte entityType)
{
outAddress = 0;
functionCode = 0;
@ -245,8 +278,72 @@ namespace Modbus.Slaves
//address
outAddress = (UInt16)(address_int - 1);
return true;
}
protected void ReleaseOperations()
{
m_run = false;
Task.WaitAll(m_taskList.ToArray());
m_taskList.Clear();
}
#endregion
}
/// <summary>
/// This class is Modbus TCP session.
/// </summary>
class ModbusTCPSlaveSession : ModbusSlaveSession
{
#region Constructors
public ModbusTCPSlaveSession(ModbusSlaveConfig conf, HandleResultDelegate resultHandler)
: base(conf, resultHandler)
{
}
#endregion
#region Protected Properties
protected override int m_reqSize { get { return 12; } }
protected override int m_dataBodyOffset { get { return 7; } }
#endregion
#region Private Fields
private const int m_tcpPort = 502;
private object m_socketLock = new object();
private Socket m_socket = null;
private IPAddress m_address = null;
#endregion
#region Public Methods
public override void ReleaseSession()
{
ReleaseOperations();
if (m_socket != null)
{
m_socket.Disconnect(false);
m_socket.Dispose();
m_socket = null;
}
}
private void EncodeRead(ReadOperation operation)
#endregion
#region Private Methods
protected override async Task ConnectSlave()
{
if (IPAddress.TryParse(config.SlaveConnection, out m_address))
{
try
{
m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await m_socket.ConnectAsync(m_address, m_tcpPort);
}
catch (Exception e)
{
Console.WriteLine("Connect Slave failed");
Console.WriteLine(e.Message);
m_socket = null;
}
}
}
protected override void EncodeRead(ReadOperation operation)
{
//MBAP
//transaction id 2 bytes
@ -264,17 +361,17 @@ namespace Modbus.Slaves
//Body
//function code
operation.Request[m_tcpOffset] = operation.FunctionCode;
operation.Request[m_dataBodyOffset] = operation.FunctionCode;
//address
byte[] address_byte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((Int16)(operation.Address)));
operation.Request[8] = address_byte[0];
operation.Request[9] = address_byte[1];
operation.Request[m_dataBodyOffset + 1] = address_byte[0];
operation.Request[m_dataBodyOffset + 2] = address_byte[1];
//count
byte[] count_byte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((Int16)operation.Count));
operation.Request[10] = count_byte[0];
operation.Request[11] = count_byte[1];
operation.Request[m_dataBodyOffset + 3] = count_byte[0];
operation.Request[m_dataBodyOffset + 4] = count_byte[1];
}
private void EncodeWrite(byte[] request, string uid, string address, string value)
protected override void EncodeWrite(byte[] request, string uid, string address, string value)
{
//MBAP
//transaction id 2 bytes
@ -292,77 +389,27 @@ namespace Modbus.Slaves
//Body
//function code
ParseEntity(address, false, out ushort address_int16, out request[m_tcpOffset], out byte entity_type);
ParseEntity(address, false, out ushort address_int16, out request[m_dataBodyOffset], out byte entity_type);
//address
byte[] address_byte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((Int16)(address_int16)));
request[8] = address_byte[0];
request[9] = address_byte[1];
request[m_dataBodyOffset + 1] = address_byte[0];
request[m_dataBodyOffset + 2] = address_byte[1];
//value
UInt16 value_int = (UInt16)Convert.ToInt32(value);
if (entity_type == '0' && value_int == 1)
{
request[10] = 0xFF;
request[11] = 0x00;
request[m_dataBodyOffset + 3] = 0xFF;
request[m_dataBodyOffset + 4] = 0x00;
}
else
{
byte[] val_byte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((Int16)value_int));
request[10] = val_byte[0];
request[11] = val_byte[1];
request[m_dataBodyOffset + 3] = val_byte[0];
request[m_dataBodyOffset + 4] = val_byte[1];
}
}
private List<ModbusOutMessage> ProcessResponse(ModbusSlaveConfig config, ReadOperation x)
{
List<ModbusOutMessage> result = new List<ModbusOutMessage>();
int count = 0;
int step_size = 0;
int start_digit = 0;
switch (x.Response[m_tcpOffset])//function code
{
case (byte)ModbusConstants.FunctionCodeType.ReadCoils:
case (byte)ModbusConstants.FunctionCodeType.ReadInputs:
{
count = x.Response[m_tcpOffset + 1] * 8;
count = (count > x.Count) ? x.Count : count;
step_size = 1;
start_digit = x.Response[m_tcpOffset] - 1;
break;
}
case (byte)ModbusConstants.FunctionCodeType.ReadHoldingRegisters:
case (byte)ModbusConstants.FunctionCodeType.ReadInputRegisters:
{
count = x.Response[m_tcpOffset + 1];
step_size = 2;
start_digit = (x.Response[m_tcpOffset] == 3) ? 4 : 3;
break;
}
}
for (int i = 0; i < count; i += step_size)
{
string res = "";
string cell = "";
string val = "";
if (step_size == 1)
{
cell = string.Format(x.OutFormat, (char)x.EntityType, x.Address + i + 1);
val = string.Format("{0}", (x.Response[m_tcpOffset + 2 + (i / 8)] >> (i % 8)) & 0b1);
}
else if (step_size == 2)
{
cell = string.Format(x.OutFormat, (char)x.EntityType, x.Address + (i / 2) + 1);
val = string.Format("{0,00000}", ((x.Response[m_tcpOffset + 2 + i]) * 0x100 + x.Response[m_tcpOffset + 3 + i]));
}
res = cell + ": " + val + "\n";
Console.WriteLine(res);
ModbusOutMessage message = new ModbusOutMessage()
{ HwId = config.HwId, DisplayName = x.DisplayName, Address = cell, Value = val, SourceTimestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") };
result.Add(message);
}
return result;
}
private async Task<byte[]> SendRequest(byte[] request, int reqLen)
protected override async Task<byte[]> SendRequest(byte[] request, int reqLen)
{
byte[] response = new byte[m_bufSize];
if (m_socket != null && m_socket.Connected)
@ -372,9 +419,7 @@ namespace Modbus.Slaves
try
{
m_socket.Send(request, reqLen, SocketFlags.None);
m_socket.Receive(response, 0, m_tcpOffset, SocketFlags.None);
int remain = IPAddress.NetworkToHostOrder((Int16)BitConverter.ToUInt16(response, 4));
m_socket.Receive(response, m_tcpOffset, remain - 1, SocketFlags.None);
response = ReadResponse();
}
catch (Exception e)
{
@ -393,13 +438,241 @@ namespace Modbus.Slaves
await ConnectSlave();
return null;
}
}
private byte[] ReadResponse()
{
byte[] response = new byte[m_bufSize];
int header_len = 0;
int data_len = 0;
int h_l = m_socket.Receive(response, 0, m_dataBodyOffset, SocketFlags.None);
header_len += h_l;
while (header_len < m_dataBodyOffset)
{
h_l = m_socket.Receive(response, header_len, m_dataBodyOffset - header_len, SocketFlags.None);
header_len += h_l;
}
int byte_counts = IPAddress.NetworkToHostOrder((Int16)BitConverter.ToUInt16(response, 4)) - 1;
int d_l = m_socket.Receive(response, m_dataBodyOffset, byte_counts, SocketFlags.None);
data_len += d_l;
while (data_len < byte_counts)
{
d_l = m_socket.Receive(response, m_dataBodyOffset + data_len, byte_counts - data_len, SocketFlags.None);
data_len += d_l;
}
return response;
}
#endregion
}
/// <summary>
/// This class is Modbus RTU session.
/// </summary>
class ModbusRTUSlaveSession : ModbusSlaveSession
{
#region Constructors
public ModbusRTUSlaveSession(ModbusSlaveConfig conf, HandleResultDelegate resultHandler)
: base(conf, resultHandler)
{
}
#endregion
#region Protected Properties
protected override int m_reqSize { get { return 8; } }
protected override int m_dataBodyOffset { get { return 1; } }
#endregion
#region Private Fields
private const int m_numOfBits = 8;
private object m_serialPortLock = new object();
private SerialPort m_serialPort = null;
#endregion
#region Public Methods
public override void ReleaseSession()
{
ReleaseOperations();
if (m_serialPort != null)
{
m_serialPort.Close();
m_serialPort.Dispose();
m_serialPort = null;
}
}
#endregion
#region Private Methods
protected override async Task ConnectSlave()
{
if (config.SlaveConnection.Substring(0, 3) == "COM" || config.SlaveConnection.Substring(0, 4) == "ttyS")
{
try
{
m_serialPort = new SerialPort(config.SlaveConnection, (int)config.BaudRate, config.Parity, (int)config.DataBits, config.StopBits);
m_serialPort.Open();
m_serialPort.Handshake = Handshake.None;
//m_serialPort.DataReceived += new SerialDataReceivedEventHandler(sp_DataReceived);
await Task.Delay(0);
}
catch (Exception e)
{
Console.WriteLine("Connect Slave failed");
Console.WriteLine(e.Message);
m_serialPort = null;
}
}
}
protected override void EncodeRead(ReadOperation operation)
{
//uid
operation.Request[0] = operation.UnitId;
//Body
//function code
operation.Request[m_dataBodyOffset] = operation.FunctionCode;
//address
byte[] address_byte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((Int16)(operation.Address)));
operation.Request[m_dataBodyOffset + 1] = address_byte[0];
operation.Request[m_dataBodyOffset + 2] = address_byte[1];
//count
byte[] count_byte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((Int16)operation.Count));
operation.Request[m_dataBodyOffset + 3] = count_byte[0];
operation.Request[m_dataBodyOffset + 4] = count_byte[1];
if (GetCRC(operation.Request, 6, out UInt16 crc))
{
byte[] crc_byte = BitConverter.GetBytes(crc);
operation.Request[m_dataBodyOffset + 5] = crc_byte[0];
operation.Request[m_dataBodyOffset + 6] = crc_byte[1];
}
}
protected override void EncodeWrite(byte[] request, string uid, string address, string value)
{
//uid
request[0] = Convert.ToByte(uid);
//Body
//function code
ParseEntity(address, false, out ushort address_int16, out request[m_dataBodyOffset], out byte entity_type);
//address
byte[] address_byte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((Int16)(address_int16)));
request[m_dataBodyOffset + 1] = address_byte[0];
request[m_dataBodyOffset + 2] = address_byte[1];
//value
UInt16 value_int = (UInt16)Convert.ToInt32(value);
if (entity_type == '0' && value_int == 1)
{
request[m_dataBodyOffset + 3] = 0xFF;
request[m_dataBodyOffset + 4] = 0x00;
}
else
{
byte[] val_byte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((Int16)value_int));
request[m_dataBodyOffset + 3] = val_byte[0];
request[m_dataBodyOffset + 4] = val_byte[1];
}
if (GetCRC(request, 6, out UInt16 crc))
{
byte[] crc_byte = BitConverter.GetBytes(crc);
request[m_dataBodyOffset + 5] = crc_byte[0];
request[m_dataBodyOffset + 6] = crc_byte[1];
}
}
protected override async Task<byte[]> SendRequest(byte[] request, int reqLen)
{
//double slient_interval = 1000 * 5 * ((double)1 / (double)config.BaudRate);
int silent = 100;
byte[] response = null;
if (m_serialPort != null && m_serialPort.IsOpen)
{
lock (m_serialPortLock)
{
try
{
m_serialPort.DiscardInBuffer();
m_serialPort.DiscardOutBuffer();
Task.Delay(silent).Wait();
m_serialPort.Write(request, 0, reqLen);
response = ReadResponse();
}
catch (Exception e)
{
Console.WriteLine("Something wrong with the connection, disposing...");
Console.WriteLine(e.Message);
m_serialPort.Close();
m_serialPort.Dispose();
m_serialPort = null;
}
}
return response;
}
else
{
Console.WriteLine("Connection lost, reconnecting...");
await ConnectSlave();
return null;
}
}
private byte[] ReadResponse()
{
byte[] response = new byte[m_bufSize];
int header_len = 0;
int data_len = 0;
int h_l = m_serialPort.Read(response, 0, 3);
header_len += h_l;
while (header_len < 3)
{
h_l = m_serialPort.Read(response, header_len, 3 - header_len);
header_len += h_l;
}
int byte_counts = response[2] + 2;
int d_l = m_serialPort.Read(response, 3, byte_counts);
data_len += d_l;
while (data_len < byte_counts)
{
d_l = m_serialPort.Read(response, 3 + data_len, byte_counts - data_len);
data_len += d_l;
}
return response;
}
private void sp_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
string data = m_serialPort.ReadExisting();
}
private bool GetCRC(byte[] message, int length, out UInt16 res)
{
UInt16 crcFull = 0xFFFF;
res = 0;
if (message == null || length <= 0)
return false;
for (int _byte = 0; _byte < length; ++_byte)
{
crcFull = (UInt16)(crcFull ^ message[_byte]);
for (int _bit = 0; _bit < m_numOfBits; ++_bit)
{
byte crcLsb = (byte)(crcFull & 0x0001);
crcFull = (UInt16)((crcFull >> 1) & 0x7FFF);
if (crcLsb == 1)
{
crcFull = (UInt16)(crcFull ^ 0xA001);
}
}
}
res = crcFull;
return true;
}
#endregion
}
//TODO
//class ModbusRTUSlaveSession : ModbusSlaveSession
//{
//}
/// <summary>
/// This class contains the configuration for a Modbus session.
@ -408,23 +681,24 @@ namespace Modbus.Slaves
{
public string SlaveConnection { get; set; }
public string HwId { get; set; }
/*
public uint BaudRate { get; set; }
public byte StopBits { get; set; }
public StopBits StopBits { get; set; }
public byte DataBits { get; set; }
public byte Parity { get; set; }
public byte FlowControl { get; set; }
*/
public Parity Parity { get; set; }
//public byte FlowControl { get; set; }
public Dictionary<string, ReadOperation> Operations = null;
public ModbusConstants.ConnectionType GetConnectionType()
{
if (IPAddress.TryParse(SlaveConnection, out IPAddress address))
return ModbusConstants.ConnectionType.ModbusTCP;
else if (SlaveConnection.Substring(0, 3) == "COM" || SlaveConnection.Substring(0, 4) == "ttyS")
return ModbusConstants.ConnectionType.ModbusRTU;
//TODO: ModbusRTU ModbusASCII
return ModbusConstants.ConnectionType.Unknown;
}
}
/// <summary>
/// This class contains the configuration for a single Modbus read request.
/// </summary>
@ -435,6 +709,7 @@ namespace Modbus.Slaves
public int RequestLen;
public byte EntityType { get; set; }
public string OutFormat { get; set; }
public int PollingInterval { get; set; }
public byte UnitId { get; set; }
public byte FunctionCode { get; set; }
public string StartAddress { get; set; }
@ -442,6 +717,7 @@ namespace Modbus.Slaves
public UInt16 Count { get; set; }
public string DisplayName { get; set; }
}
static class ModbusConstants
{
public enum EntityType
@ -468,6 +744,7 @@ namespace Modbus.Slaves
WriteHoldingRegister = 6
};
}
class ModbusOutMessage
{
public string DisplayName { get; set; }
@ -476,6 +753,7 @@ namespace Modbus.Slaves
public string Value { get; set; }
public string SourceTimestamp { get; set; }
}
class ModbusInMessage
{
public string HwId { get; set; }
@ -483,6 +761,7 @@ namespace Modbus.Slaves
public string Address { get; set; }
public string Value { get; set; }
}
class ModuleConfig
{
public Dictionary<string, ModbusSlaveConfig> SlaveConfigs;
@ -491,12 +770,13 @@ namespace Modbus.Slaves
SlaveConfigs = slaves;
}
}
class ModbusSendInterval
class ModbusPushInterval
{
public ModbusSendInterval(int interval)
public ModbusPushInterval(int interval)
{
Interval = interval;
PublishInterval = interval;
}
public int Interval { get; set; }
public int PublishInterval { get; set; }
}
}

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

@ -1,368 +1,386 @@
#define IOT_EDGE
namespace Modbus.Containers
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Client;
using Microsoft.Azure.Devices.Client.Transport.Mqtt;
using Microsoft.Azure.Devices.Shared;
using Newtonsoft.Json;
using Modbus.Slaves;
class Program
{
const string ModbusSlaves = "SlaveConfigs";
const int DefaultPushInterval = 5000;
static int m_counter = 0;
static List<Task> m_task_list = new List<Task>();
static bool m_run = true;
static ModbusSendInterval m_interval = null;
static void Main(string[] args)
{
// Install CA certificate
InstallCert();
// Initialize Edge Module
InitEdgeModule().Wait();
// Wait until the app unloads or is cancelled
var cts = new CancellationTokenSource();
AssemblyLoadContext.Default.Unloading += (ctx) => cts.Cancel();
Console.CancelKeyPress += (sender, cpe) => cts.Cancel();
WhenCancelled(cts.Token).Wait();
}
/// <summary>
/// Handles cleanup operations when app is cancelled or unloads
/// </summary>
public static Task WhenCancelled(CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tcs);
return tcs.Task;
}
/// <summary>
/// Add certificate in local cert store for use by client for secure connection to IoT Edge runtime
/// </summary>
static void InstallCert()
{
// Suppress cert validation on Windows for now
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
string certPath = Environment.GetEnvironmentVariable("EdgeModuleCACertificateFile");
if (string.IsNullOrWhiteSpace(certPath))
{
// We cannot proceed further without a proper cert file
Console.WriteLine("Missing path to certificate collection file.");
throw new InvalidOperationException("Missing path to certificate file.");
}
else if (!File.Exists(certPath))
{
// We cannot proceed further without a proper cert file
Console.WriteLine("Missing certificate collection file.");
throw new InvalidOperationException("Missing certificate file.");
}
X509Store store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
store.Add(new X509Certificate2(X509Certificate2.CreateFromCertFile(certPath)));
Console.WriteLine("Added Cert: " + certPath);
store.Close();
}
/// <summary>
/// Initializes the Azure IoT Client for the Edge Module
/// </summary>
static async Task InitEdgeModule()
{
try
{
// Open a connection to the Edge runtime using MQTT transport and
// the connection string provided as an environment variable
string connectionString = Environment.GetEnvironmentVariable("EdgeHubConnectionString");
MqttTransportSettings mqttSettings = new MqttTransportSettings(TransportType.Mqtt_Tcp_Only);
// Suppress cert validation on Windows for now
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
mqttSettings.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
}
ITransportSettings[] settings = { mqttSettings };
DeviceClient ioTHubModuleClient = DeviceClient.CreateFromConnectionString(connectionString, settings);
await ioTHubModuleClient.OpenAsync();
Console.WriteLine("IoT Hub module client initialized.");
// Read config from Twin and Start
Twin moduleTwin = await ioTHubModuleClient.GetTwinAsync();
await UpdateStartFromTwin(moduleTwin.Properties.Desired, ioTHubModuleClient);
// Attach callback for Twin desired properties updates
await ioTHubModuleClient.SetDesiredPropertyUpdateCallbackAsync(OnDesiredPropertiesUpdate, ioTHubModuleClient);
}
catch (AggregateException ex)
{
foreach (Exception exception in ex.InnerExceptions)
{
Console.WriteLine();
Console.WriteLine("Error when initializing module: {0}", exception);
}
}
}
/// <summary>
/// This method is called whenever the module is sent a message from the EdgeHub.
/// It just pipe the messages without any change.
/// It prints all the incoming messages.
/// </summary>
static async Task<MessageResponse> PipeMessage(Message message, object userContext)
{
Console.WriteLine("Modbus Writer - Received command");
int counterValue = Interlocked.Increment(ref m_counter);
var userContextValues = userContext as Tuple<DeviceClient, Slaves.ModuleHandle>;
if (userContextValues == null)
{
throw new InvalidOperationException("UserContext doesn't contain " +
"expected values");
}
DeviceClient ioTHubModuleClient = userContextValues.Item1;
Slaves.ModuleHandle moduleHandle = userContextValues.Item2;
byte[] messageBytes = message.GetBytes();
string messageString = Encoding.UTF8.GetString(messageBytes);
Console.WriteLine($"Received message: {counterValue}, Body: [{messageString}]");
message.Properties.TryGetValue("command-type", out string cmdType);
if (cmdType == "ModbusWrite")
{
// Get message body, containing the write target and value
var messageBody = JsonConvert.DeserializeObject<ModbusInMessage>(messageString);
if (messageBody != null)
{
Console.WriteLine($"Write device {messageBody.HwId}, " +
$"address: {messageBody.Address}, value: {messageBody.Value}");
ModbusSlaveSession target = moduleHandle.GetSlaveSession(messageBody.HwId);
if (target == null)
{
Console.WriteLine($"target \"{messageBody.HwId}\" not found!");
}
else
{
await target.WriteCB(messageBody.UId, messageBody.Address, messageBody.Value);
}
}
}
return MessageResponse.Completed;
}
/// <summary> 
/// Callback to handle Twin desired properties updates 
/// </summary> 
static async Task OnDesiredPropertiesUpdate(TwinCollection desiredProperties, object userContext)
{
DeviceClient ioTHubModuleClient = userContext as DeviceClient;
try
{
#if IOT_EDGE
// stop all activities while updating configuration
await ioTHubModuleClient.SetInputMessageHandlerAsync(
"input1",
DummyCallBack,
null);
#endif
m_run = false;
await Task.WhenAll(m_task_list);
m_task_list.Clear();
m_run = true;
await UpdateStartFromTwin(desiredProperties, ioTHubModuleClient);
}
catch (AggregateException ex)
{
foreach (Exception exception in ex.InnerExceptions)
{
Console.WriteLine();
Console.WriteLine("Error when receiving desired property: {0}", exception);
}
}
catch (Exception ex)
{
Console.WriteLine();
Console.WriteLine("Error when receiving desired property: {0}", ex.Message);
}
}
/// <summary>
/// A dummy callback does nothing
/// </summary>
/// <param name="message"></param>
/// <param name="userContext"></param>
/// <returns></returns>
static async Task<MessageResponse> DummyCallBack(Message message, object userContext)
{
await Task.Delay(TimeSpan.FromSeconds(0));
return MessageResponse.Abandoned;
}
/// <summary>
/// Update Start from module Twin.
/// </summary>
static async Task UpdateStartFromTwin(TwinCollection desiredProperties, DeviceClient ioTHubModuleClient)
{
ModuleConfig config;
Slaves.ModuleHandle moduleHandle;
string jsonStr = null;
string serializedStr;
serializedStr = JsonConvert.SerializeObject(desiredProperties);
Console.WriteLine("Desired property change:");
Console.WriteLine(serializedStr);
if (desiredProperties.Contains(ModbusSlaves))
{
// get config from Twin
jsonStr = serializedStr;
}
else
{
Console.WriteLine("No configuration found in desired properties.");
if (File.Exists(@"iot-edge-modbus.json"))
{
try
{
// get config from local file
jsonStr = File.ReadAllText(@"iot-edge-modbus.json");
}
catch (Exception ex)
{
Console.WriteLine("Unable to read configuration from file. Error: " + ex.Message);
}
}
}
if (!string.IsNullOrEmpty(jsonStr))
{
config = JsonConvert.DeserializeObject<ModuleConfig>(jsonStr);
m_interval = JsonConvert.DeserializeObject<ModbusSendInterval>(jsonStr);
if (m_interval == null)
{
m_interval = new ModbusSendInterval(DefaultPushInterval);
}
moduleHandle = await Slaves.ModuleHandle.CreateHandleFromConfiguration(config);
if (moduleHandle != null)
{
var userContext = new Tuple<DeviceClient, Slaves.ModuleHandle>(ioTHubModuleClient, moduleHandle);
#if IOT_EDGE
// Register callback to be called when a message is received by the module
await ioTHubModuleClient.SetInputMessageHandlerAsync(
"input1",
PipeMessage,
userContext);
#else
m_task_list.Add(Receive(userContext));
#endif
m_task_list.Add(Start(userContext));
}
}
}
/// <summary>
/// Iterate through each Modbus session to poll data
/// </summary>
/// <param name="userContext"></param>
/// <returns></returns>
static async Task Start(object userContext)
{
var userContextValues = userContext as Tuple<DeviceClient, Slaves.ModuleHandle>;
if (userContextValues == null)
{
throw new InvalidOperationException("UserContext doesn't contain " +
"expected values");
}
DeviceClient ioTHubModuleClient = userContextValues.Item1;
Slaves.ModuleHandle moduleHandle = userContextValues.Item2;
while (m_run)
{
List<ModbusOutMessage> result = new List<ModbusOutMessage>();
foreach (ModbusSlaveSession s in moduleHandle.ModbusSessionList)
{
var msgs = await s.ProcessOperations();
result.AddRange(msgs);
}
if (result.Count > 0)
{
Message message = new Message(Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(result)));
message.Properties.Add("content-type", "application/edge-modbus-json");
#if IOT_EDGE
await ioTHubModuleClient.SendEventAsync("modbusOutput", message);
#else
await ioTHubModuleClient.SendEventAsync(message);
#endif
}
if (!m_run)
{
break;
}
await Task.Delay(m_interval.Interval);
}
moduleHandle.Release();
}
/// <summary>
/// Receive C2D message(running without iot edge)
/// </summary>
/// <param name="userContext"></param>
/// <returns></returns>
static async Task Receive(object userContext)
{
var userContextValues = userContext as Tuple<DeviceClient, Slaves.ModuleHandle>;
if (userContextValues == null)
{
throw new InvalidOperationException("UserContext doesn't contain " +
"expected values");
}
DeviceClient ioTHubModuleClient = userContextValues.Item1;
var timeout = TimeSpan.FromSeconds(3);
while (m_run)
{
try
{
Message message = await ioTHubModuleClient.ReceiveAsync(timeout);
if (message != null)
{
await PipeMessage(message, userContext);
await ioTHubModuleClient.CompleteAsync(message);
}
}
catch (Exception ex)
{
Console.WriteLine();
Console.WriteLine("Error when receiving: {0}", ex.Message);
}
}
}
}
#define IOT_EDGE
namespace Modbus.Containers
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Client;
using Microsoft.Azure.Devices.Client.Transport.Mqtt;
using Microsoft.Azure.Devices.Shared;
using Newtonsoft.Json;
using Modbus.Slaves;
class Program
{
const string ModbusSlaves = "SlaveConfigs";
const int DefaultPushInterval = 5000;
static int m_counter = 0;
static List<Task> m_task_list = new List<Task>();
static bool m_run = true;
static ModbusPushInterval m_interval = null;
static object message_lock = new object();
static List<ModbusOutMessage> result = new List<ModbusOutMessage>();
static void Main(string[] args)
{
// Install CA certificate
InstallCert();
// Initialize Edge Module
InitEdgeModule().Wait();
// Wait until the app unloads or is cancelled
var cts = new CancellationTokenSource();
AssemblyLoadContext.Default.Unloading += (ctx) => cts.Cancel();
Console.CancelKeyPress += (sender, cpe) => cts.Cancel();
WhenCancelled(cts.Token).Wait();
}
/// <summary>
/// Handles cleanup operations when app is cancelled or unloads
/// </summary>
public static Task WhenCancelled(CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tcs);
return tcs.Task;
}
/// <summary>
/// Add certificate in local cert store for use by client for secure connection to IoT Edge runtime
/// </summary>
static void InstallCert()
{
// Suppress cert validation on Windows for now
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
string certPath = Environment.GetEnvironmentVariable("EdgeModuleCACertificateFile");
if (string.IsNullOrWhiteSpace(certPath))
{
// We cannot proceed further without a proper cert file
Console.WriteLine("Missing path to certificate collection file.");
throw new InvalidOperationException("Missing path to certificate file.");
}
else if (!File.Exists(certPath))
{
// We cannot proceed further without a proper cert file
Console.WriteLine("Missing certificate collection file.");
throw new InvalidOperationException("Missing certificate file.");
}
X509Store store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
store.Add(new X509Certificate2(X509Certificate2.CreateFromCertFile(certPath)));
Console.WriteLine("Added Cert: " + certPath);
store.Close();
}
/// <summary>
/// Initializes the Azure IoT Client for the Edge Module
/// </summary>
static async Task InitEdgeModule()
{
try
{
// Open a connection to the Edge runtime using MQTT transport and
// the connection string provided as an environment variable
string connectionString = Environment.GetEnvironmentVariable("EdgeHubConnectionString");
MqttTransportSettings mqttSettings = new MqttTransportSettings(TransportType.Mqtt_Tcp_Only);
// Suppress cert validation on Windows for now
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
mqttSettings.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
}
ITransportSettings[] settings = { mqttSettings };
DeviceClient ioTHubModuleClient = DeviceClient.CreateFromConnectionString(connectionString, settings);
await ioTHubModuleClient.OpenAsync();
Console.WriteLine("IoT Hub module client initialized.");
// Read config from Twin and Start
Twin moduleTwin = await ioTHubModuleClient.GetTwinAsync();
await UpdateStartFromTwin(moduleTwin.Properties.Desired, ioTHubModuleClient);
// Attach callback for Twin desired properties updates
await ioTHubModuleClient.SetDesiredPropertyUpdateCallbackAsync(OnDesiredPropertiesUpdate, ioTHubModuleClient);
}
catch (AggregateException ex)
{
foreach (Exception exception in ex.InnerExceptions)
{
Console.WriteLine();
Console.WriteLine("Error when initializing module: {0}", exception);
}
}
}
/// <summary>
/// This method is called whenever the module is sent a message from the EdgeHub.
/// It just pipe the messages without any change.
/// It prints all the incoming messages.
/// </summary>
static async Task<MessageResponse> PipeMessage(Message message, object userContext)
{
Console.WriteLine("Modbus Writer - Received command");
int counterValue = Interlocked.Increment(ref m_counter);
var userContextValues = userContext as Tuple<DeviceClient, Slaves.ModuleHandle>;
if (userContextValues == null)
{
throw new InvalidOperationException("UserContext doesn't contain " +
"expected values");
}
DeviceClient ioTHubModuleClient = userContextValues.Item1;
Slaves.ModuleHandle moduleHandle = userContextValues.Item2;
byte[] messageBytes = message.GetBytes();
string messageString = Encoding.UTF8.GetString(messageBytes);
Console.WriteLine($"Received message: {counterValue}, Body: [{messageString}]");
message.Properties.TryGetValue("command-type", out string cmdType);
if (cmdType == "ModbusWrite")
{
// Get message body, containing the write target and value
var messageBody = JsonConvert.DeserializeObject<ModbusInMessage>(messageString);
if (messageBody != null)
{
Console.WriteLine($"Write device {messageBody.HwId}, " +
$"address: {messageBody.Address}, value: {messageBody.Value}");
ModbusSlaveSession target = moduleHandle.GetSlaveSession(messageBody.HwId);
if (target == null)
{
Console.WriteLine($"target \"{messageBody.HwId}\" not found!");
}
else
{
await target.WriteCB(messageBody.UId, messageBody.Address, messageBody.Value);
}
}
}
return MessageResponse.Completed;
}
/// <summary> 
/// Callback to handle Twin desired properties updates 
/// </summary> 
static async Task OnDesiredPropertiesUpdate(TwinCollection desiredProperties, object userContext)
{
DeviceClient ioTHubModuleClient = userContext as DeviceClient;
try
{
#if IOT_EDGE
// stop all activities while updating configuration
await ioTHubModuleClient.SetInputMessageHandlerAsync(
"input1",
DummyCallBack,
null);
#endif
m_run = false;
await Task.WhenAll(m_task_list);
m_task_list.Clear();
m_run = true;
await UpdateStartFromTwin(desiredProperties, ioTHubModuleClient);
}
catch (AggregateException ex)
{
foreach (Exception exception in ex.InnerExceptions)
{
Console.WriteLine();
Console.WriteLine("Error when receiving desired property: {0}", exception);
}
}
catch (Exception ex)
{
Console.WriteLine();
Console.WriteLine("Error when receiving desired property: {0}", ex.Message);
}
}
/// <summary>
/// A dummy callback does nothing
/// </summary>
/// <param name="message"></param>
/// <param name="userContext"></param>
/// <returns></returns>
static async Task<MessageResponse> DummyCallBack(Message message, object userContext)
{
await Task.Delay(TimeSpan.FromSeconds(0));
return MessageResponse.Abandoned;
}
/// <summary>
/// Update Start from module Twin.
/// </summary>
static async Task UpdateStartFromTwin(TwinCollection desiredProperties, DeviceClient ioTHubModuleClient)
{
ModuleConfig config;
Slaves.ModuleHandle moduleHandle;
string jsonStr = null;
string serializedStr;
serializedStr = JsonConvert.SerializeObject(desiredProperties);
Console.WriteLine("Desired property change:");
Console.WriteLine(serializedStr);
if (desiredProperties.Contains(ModbusSlaves))
{
// get config from Twin
jsonStr = serializedStr;
}
else
{
Console.WriteLine("No configuration found in desired properties.");
if (File.Exists(@"iot-edge-modbus.json"))
{
try
{
// get config from local file
jsonStr = File.ReadAllText(@"iot-edge-modbus.json");
}
catch (Exception ex)
{
Console.WriteLine("Unable to read configuration from file. Error: " + ex.Message);
}
}
}
if (!string.IsNullOrEmpty(jsonStr))
{
config = JsonConvert.DeserializeObject<ModuleConfig>(jsonStr);
m_interval = JsonConvert.DeserializeObject<ModbusPushInterval>(jsonStr);
if (m_interval == null)
{
m_interval = new ModbusPushInterval(DefaultPushInterval);
}
moduleHandle = await Slaves.ModuleHandle.CreateHandleFromConfiguration(config, UpdateMessage);
if (moduleHandle != null)
{
var userContext = new Tuple<DeviceClient, Slaves.ModuleHandle>(ioTHubModuleClient, moduleHandle);
#if IOT_EDGE
// Register callback to be called when a message is received by the module
await ioTHubModuleClient.SetInputMessageHandlerAsync(
"input1",
PipeMessage,
userContext);
#else
m_task_list.Add(Receive(userContext));
#endif
m_task_list.Add(Start(userContext));
}
}
}
/// <summary>
/// Iterate through each Modbus session to poll data
/// </summary>
/// <param name="userContext"></param>
/// <returns></returns>
static async Task Start(object userContext)
{
var userContextValues = userContext as Tuple<DeviceClient, Slaves.ModuleHandle>;
if (userContextValues == null)
{
throw new InvalidOperationException("UserContext doesn't contain " +
"expected values");
}
DeviceClient ioTHubModuleClient = userContextValues.Item1;
Slaves.ModuleHandle moduleHandle = userContextValues.Item2;
foreach (ModbusSlaveSession s in moduleHandle.ModbusSessionList)
{
s.ProcessOperations();
}
while (m_run)
{
Message message = null;
lock (message_lock)
{
if (result.Count > 0)
{
message = new Message(Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(result)));
message.Properties.Add("content-type", "application/edge-modbus-json");
}
result.Clear();
}
if (message != null)
{
#if IOT_EDGE
await ioTHubModuleClient.SendEventAsync("modbusOutput", message);
#else
await ioTHubModuleClient.SendEventAsync(message);
#endif
}
if (!m_run)
{
break;
}
await Task.Delay(m_interval.PublishInterval);
}
moduleHandle.Release();
}
/// <summary>
/// Receive C2D message(running without iot edge)
/// </summary>
/// <param name="userContext"></param>
/// <returns></returns>
static async Task Receive(object userContext)
{
var userContextValues = userContext as Tuple<DeviceClient, Slaves.ModuleHandle>;
if (userContextValues == null)
{
throw new InvalidOperationException("UserContext doesn't contain " +
"expected values");
}
DeviceClient ioTHubModuleClient = userContextValues.Item1;
var timeout = TimeSpan.FromSeconds(3);
while (m_run)
{
try
{
Message message = await ioTHubModuleClient.ReceiveAsync(timeout);
if (message != null)
{
await PipeMessage(message, userContext);
await ioTHubModuleClient.CompleteAsync(message);
}
}
catch (Exception ex)
{
Console.WriteLine();
Console.WriteLine("Error when receiving: {0}", ex.Message);
}
}
}
static void UpdateMessage(ModbusOutMessage message)
{
lock(message_lock)
{
result.Add(message);
}
}
}
}

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

@ -1,30 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<RootNamespace>Modbus.Containers</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|netcoreapp2.0|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<TreatSpecificWarningsAsErrors />
</PropertyGroup>
<ItemGroup>
<Compile Remove="V1\**" />
<EmbeddedResource Remove="V1\**" />
<None Remove="V1\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Devices.Client" Version="1.6.0-preview-001" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.0.0-preview2-final" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.0.0-preview2-final" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.0.0-preview2-final" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.0.0-preview2-final" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.0.0-preview2-final" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.0.0-preview2-final" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
</ItemGroup>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<RootNamespace>Modbus.Containers</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|netcoreapp2.0|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<TreatSpecificWarningsAsErrors />
</PropertyGroup>
<ItemGroup>
<Compile Remove="V1\**" />
<EmbeddedResource Remove="V1\**" />
<None Remove="V1\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Devices.Client" Version="1.6.0-preview-001" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.0.0-preview2-final" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.0.0-preview2-final" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.0.0-preview2-final" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.0.0-preview2-final" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.0.0-preview2-final" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.0.0-preview2-final" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
<PackageReference Include="System.IO.Ports" Version="4.4" />
</ItemGroup>
</Project>