Сервер для сбора данных с термодатчиковых модулей

This commit is contained in:
ftc
2021-04-10 14:41:43 +03:00
parent 087db98d98
commit bba851ae16
13 changed files with 653 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
using NLog;
namespace ESPServer
{
public static class Logging
{
public static void Setup()
{
var config = new NLog.Config.LoggingConfiguration();
var logConsole = new NLog.Targets.ConsoleTarget("logconsole");
config.AddRuleForAllLevels(logConsole);
LogManager.Configuration = config;
}
}
}
+153
View File
@@ -0,0 +1,153 @@
using NLog;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace ESPServer
{
class ModbusServer
{
private RegistersContainer registers;
public int Port { get; set; }
public ModbusServer(RegistersContainer registers)
{
this.registers = registers;
}
internal void Run()
{
var logger = LogManager.GetCurrentClassLogger();
logger.Info("Accepting connections on port " + Port);
var listener = new TcpListener(IPAddress.Any, Port);
listener.Start();
while (true)
{
var client = listener.AcceptTcpClient();
logger.Info("Accepted connection from " + client.Client.RemoteEndPoint);
var worker = new ModbusServerWorker(registers, client);
Thread clientThread = new Thread(new ThreadStart(worker.Run));
clientThread.Start();
}
}
}
class ModbusServerWorker
{
const int RECEIVE_TIMEOUT = 10000; // 10 seconds
private RegistersContainer registers;
private TcpClient client;
private static Logger logger = LogManager.GetCurrentClassLogger();
public ModbusServerWorker(RegistersContainer registers, TcpClient client)
{
this.registers = registers;
this.client = client;
}
List<byte> received;
ManualResetEvent readCompleted;
int currentReceived;
NetworkStream stream;
byte[] buffer = new byte[256];
public void Run()
{
logger.Debug("Working with client");
received = new List<byte>();
try
{
client.ReceiveTimeout = RECEIVE_TIMEOUT;
stream = client.GetStream();
readCompleted = new ManualResetEvent(false);
while (true)
{
readCompleted.Reset();
stream.BeginRead(buffer, 0, buffer.Length, new AsyncCallback(this.OnReadCompleted), this);
readCompleted.WaitOne();
if (currentReceived <= 0)
{
client.Close();
return;
}
ProcessReceivedData();
}
}
catch (IOException)
{
client.Close();
}
finally
{
if (stream != null) stream.Dispose();
client.Dispose();
}
}
private void ProcessReceivedData()
{
lock (received)
{
if (received.Count < 6) return;
int len = (((int)received[4] << 8) | (int)received[5]);
if (received.Count < 6 + len) return;
if (len < 2) throw new Exception("Wrong length");
int transactionId = (((int)received[0] << 8) | (int)received[1]);
byte unitId = received[6];
byte function = received[7];
List<byte> response = new List<byte>();
if (function == 0x03) // read holding registers
{
int addr = (((int)received[8] << 8) | (int)received[9]);
int amount = (((int)received[10] << 8) | (int)received[11]);
for (int a = addr; a < addr + amount; a++)
{
int value = registers[a];
response.Add((byte)((value >> 8) & 0xFF));
response.Add((byte)(value & 0xFF));
}
}
else
{
throw new Exception("Unsupported function: " + function);
}
List<byte> fullResponse = new List<byte>();
fullResponse.Add(received[0]);
fullResponse.Add(received[1]); // transaction id
fullResponse.Add(received[2]);
fullResponse.Add(received[3]); // protocol id
int responseLength = 3 + response.Count;
fullResponse.Add((byte)((responseLength >> 8) & 0xFF));
fullResponse.Add((byte)((responseLength) & 0xFF));
fullResponse.Add(unitId);
fullResponse.Add(function);
fullResponse.Add((byte)response.Count);
fullResponse.AddRange(response);
received.RemoveRange(0, 6 + len);
stream.Write(fullResponse.ToArray(), 0, fullResponse.Count);
}
}
private void OnReadCompleted(IAsyncResult ar)
{
try
{
currentReceived = stream.EndRead(ar);
lock (received)
{
for (int i = 0; i < currentReceived; i++) received.Add(buffer[i]);
}
readCompleted.Set();
}
catch (IOException e)
{
logger.Info("Error receiving " + e);
currentReceived = -1;
}
}
}
}
+76
View File
@@ -0,0 +1,76 @@
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading;
namespace ESPServer
{
class Program
{
static JsonDocument config;
static UnitsDatabase db;
static RegistersContainer registers;
static void Main(string[] args)
{
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
config = LoadConfig();
Logging.Setup();
db = new UnitsDatabase(config.RootElement.GetProperty("units"));
registers = new RegistersContainer();
var rootCommand = new RootCommand();
var runCommand = new Command("run")
{
};
runCommand.Handler = CommandHandler.Create<string>(RunService);
rootCommand.AddCommand(runCommand);
var openhabConfigCommand = new Command("openhab-config")
{
};
openhabConfigCommand.Handler = CommandHandler.Create<string>(GenerateOpenhabConfig);
rootCommand.AddCommand(openhabConfigCommand);
rootCommand.InvokeAsync(args).Wait();
}
private static void GenerateOpenhabConfig(string obj)
{
var root = config.RootElement;
StringBuilder thingsConfig = new StringBuilder();
StringBuilder itemsConfig = new StringBuilder();
thingsConfig.AppendFormat("Bridge modbus:tcp:{0} [ host=\"{1}\", port={2}, id=1 ] {{\n",
"esp_server",
root.GetProperty("IP").GetString(),
root.GetProperty("modbusPort").GetInt32());
//
db.GenerateThingsConfig(thingsConfig);
db.GenerateItemsConfig(itemsConfig);
//
thingsConfig.Append("}\n");
File.WriteAllText("esp_server.things", thingsConfig.ToString());
File.WriteAllText("esp_server.items", itemsConfig.ToString());
}
private static void RunService(string obj)
{
var unitReceiver = new UnitDataReceiver(config.RootElement.GetProperty("port").GetInt32(), db, registers);
unitReceiver.Start();
var modbusServer = new ModbusServer(registers);
modbusServer.Port = config.RootElement.GetProperty("modbusPort").GetInt32();
modbusServer.Run();
}
private static JsonDocument LoadConfig()
{
string assemblyFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var config = Path.Combine(assemblyFolder, "esp_server.json");
string configStr = File.ReadAllText(config);
return JsonDocument.Parse(configStr);
}
}
}
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\netcoreapp3.1\publish\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<SelfContained>false</SelfContained>
<PublishSingleFile>False</PublishSingleFile>
</PropertyGroup>
</Project>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
</Project>
+41
View File
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
namespace ESPServer
{
class RegistersContainer
{
private Dictionary<int, int> registers;
public RegistersContainer()
{
registers = new Dictionary<int, int>();
}
public int this[int address]
{
get
{
lock(registers)
{
if (registers.ContainsKey(address)) return registers[address];
return 0;
}
}
set
{
lock (registers)
{
registers[address] = value;
}
}
}
public void SetFloat(int address, float value)
{
byte[] bytes = BitConverter.GetBytes(value);
registers[address + 1] = (int)bytes[0] + (((int)bytes[1]) << 8);
registers[address] = (int)bytes[2] + (((int)bytes[3]) << 8);
}
}
}
+113
View File
@@ -0,0 +1,113 @@
using NLog;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using System.Threading;
namespace ESPServer
{
class UnitDataReceiver
{
private readonly Logger logger = LogManager.GetCurrentClassLogger();
UnitsDatabase db;
RegistersContainer registers;
int port;
public UnitDataReceiver(int port, UnitsDatabase db, RegistersContainer registers)
{
this.db = db;
this.port = port;
this.registers = registers;
}
public void Start()
{
var t = new Thread(new ThreadStart(this.Run));
t.Start();
}
private void Run()
{
UdpClient listener = new UdpClient(port);
IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, port);
try
{
logger.Info("Listening for UDP datagrams on port " + port);
while (true)
{
byte[] bytes = listener.Receive(ref groupEP);
string t = Encoding.ASCII.GetString(bytes, 0, bytes.Length);
logger.Debug("Received: " + t);
try
{
var received = JsonDocument.Parse(t);
var id = received.RootElement.GetProperty("id").GetString();
var unit = db.GetUnit(id);
if (unit == null)
{
logger.Info("Packet from unknown unit " + id);
unit = db.AddUnit(id);
}
ProcessUnit(unit, received.RootElement);
}
catch (Exception e)
{
logger.Error(e.Message);
}
}
}
catch (SocketException e)
{
Console.WriteLine(e);
}
finally
{
listener.Close();
}
}
private void ProcessUnit(Unit unit, JsonElement data)
{
var ip = data.GetProperty("ip").GetString();
lock (unit)
{
unit.ip = ip;
foreach (var v in data.EnumerateObject())
{
if (v.Name == "id" || v.Name == "ip") continue;
var uv = unit.GetValue(v.Name);
if (uv == null)
{
logger.Info("Unit " + unit.id + " unknown value " + v.Name);
}
else
{
if (v.Value.ValueKind == JsonValueKind.Null) uv.value = float.NaN;
if (v.Value.ValueKind == JsonValueKind.Number) uv.value = v.Value.GetSingle();
else
{
if (!float.TryParse(v.Value.GetString(), out uv.value))
{
uv.value = float.NaN;
}
}
}
}
FillRegisters(unit);
}
}
private void FillRegisters(Unit unit)
{
foreach (var uv in unit.values)
{
registers.SetFloat(unit.baseAddress + uv.Value.addressOffset, uv.Value.value);
}
}
}
}
+169
View File
@@ -0,0 +1,169 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
namespace ESPServer
{
/*
* Описание параметра, который может придти от юнита
*/
class UnitValue
{
// Название параметра
public string name;
// Текущее значение
public float value;
// Сдвиг адреса, по которому это значение будет храниться в modbus
public int addressOffset;
}
/*
* Описание конкретного модуля (релейный, датчик, whatever)
*/
class Unit
{
// Айдишник юнита (мак-адрес)
public string id;
// Название (для генерации конфигов например)
public string name;
// Базовый адрес в модбасе
public int baseAddress;
// Значения которые есть в юните
public Dictionary<string, UnitValue> values;
// текущий IP юнита
public string ip;
public UnitValue GetValue(string name)
{
if (!values.ContainsKey(name))
{
return null;
}
return values[name];
}
public int MinOffset
{
get
{
int min = int.MaxValue;
foreach (var u in values) if (u.Value.addressOffset < min) min = u.Value.addressOffset;
return min;
}
}
public int MaxOffset
{
get
{
int max = int.MinValue;
foreach (var u in values) if (u.Value.addressOffset > max) max = u.Value.addressOffset;
return max;
}
}
}
class UnitsDatabase
{
List<Unit> units;
public UnitsDatabase(JsonElement config)
{
units = new List<Unit>();
foreach (var unitConfig in config.EnumerateArray())
{
var u = new Unit();
u.id = unitConfig.GetProperty("id").GetString();
u.name = unitConfig.GetProperty("name").GetString();
u.baseAddress = unitConfig.GetProperty("address").GetInt32();
u.values = new Dictionary<string, UnitValue>();
foreach (var knownValue in unitConfig.GetProperty("values").EnumerateArray())
{
var uv = new UnitValue();
uv.name = knownValue.GetProperty("name").GetString();
uv.addressOffset = knownValue.GetProperty("offset").GetInt32();
u.values[uv.name] = uv;
}
units.Add(u);
}
}
internal Unit AddUnit(string id)
{
lock (units)
{
var u = new Unit();
u.id = id;
u.name = id;
u.baseAddress = -1;
u.values = new Dictionary<string, UnitValue>();
units.Add(u);
return u;
}
}
public Unit GetUnit(string id)
{
lock (units)
{
foreach (var u in units)
{
if (u.id == id) return u;
}
}
return null;
}
#region OpenHAB config generation
internal void GenerateItemsConfig(StringBuilder config)
{
foreach (var u in units)
{
foreach (var uv in u.values)
{
var v = uv.Value;
config.Append(string.Format("Number {0}_{1} \"{0} {1}\"", u.name, v.name));
config.Append(" {channel=\"");
config.AppendFormat("modbus:data:esp_server:esp_server:");
config.AppendFormat("{0}_{1}:number", u.name, v.name);
config.Append("\"}\n");
}
}
}
internal void GenerateThingsConfig(StringBuilder config)
{
int minAddress = int.MaxValue;
int maxAddress = int.MinValue;
foreach (var u in units)
{
if (u.baseAddress + u.MinOffset < minAddress)
{
minAddress = u.baseAddress + u.MinOffset;
}
if (u.baseAddress + u.MaxOffset > maxAddress)
{
maxAddress = u.baseAddress + u.MaxOffset;
}
}
// поскольку читаем флоаты, нужен ещё регистр запаса
maxAddress++;
config.Append("\tBridge poller ");
config.Append("esp_server");
config.AppendFormat("[start = {0}, length = {1}, type = \"holding\", refresh = 500] {{\n", minAddress, maxAddress - minAddress + 1);
foreach (var u in units)
{
foreach (var uv in u.values)
{
var v = uv.Value;
config.Append("\t\tThing data ");
config.Append(u.name + "_" + v.name);
config.AppendFormat("[readStart = \"{0}\", readValueType = \"float32\"]\n", u.baseAddress + v.addressOffset);
}
}
config.Append("\t}\n");
}
#endregion
}
}
+13
View File
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NLog" Version="4.7.9" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20574.7" />
</ItemGroup>
</Project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<_LastSelectedProfileId>C:\Work\heat_control\esp_server\Properties\PublishProfiles\FolderProfile.pubxml</_LastSelectedProfileId>
</PropertyGroup>
</Project>
+16
View File
@@ -0,0 +1,16 @@
{
"IP": "192.168.0.10",
"port": 7000,
"modbusPort": 7001,
"units": [
{
"name": "unit1",
"id": "8c:aa:b5:7d:6e:6c",
"address": 10,
"values": [
{ "name": "t", "offset": 0 },
{ "name": "h", "offset": 2 }
]
}
]
}
+25
View File
@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30804.86
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "esp_server", "esp_server.csproj", "{A9FC0FC1-AB9D-4395-A3E2-721EF901226B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A9FC0FC1-AB9D-4395-A3E2-721EF901226B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A9FC0FC1-AB9D-4395-A3E2-721EF901226B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9FC0FC1-AB9D-4395-A3E2-721EF901226B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A9FC0FC1-AB9D-4395-A3E2-721EF901226B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {75B90B6A-0F87-466B-B503-069E4A09A77C}
EndGlobalSection
EndGlobal