From bba851ae16508f09e16909b2df06dfce8f340902 Mon Sep 17 00:00:00 2001 From: Denis Denisov Date: Sat, 10 Apr 2021 14:41:43 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B2=D0=B5=D1=80=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=81=D0=B1=D0=BE=D1=80=D0=B0=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D1=85=20=D1=81=20=D1=82=D0=B5=D1=80=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0=D1=82=D1=87=D0=B8=D0=BA=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D1=85=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + esp_server/Logging.cs | 16 ++ esp_server/ModbusServer.cs | 153 ++++++++++++++++ esp_server/Program.cs | 76 ++++++++ .../PublishProfiles/FolderProfile.pubxml | 16 ++ .../PublishProfiles/FolderProfile.pubxml.user | 6 + esp_server/RegistersContainer.cs | 41 +++++ esp_server/UnitDataReceiver.cs | 113 ++++++++++++ esp_server/UnitsDatabase.cs | 169 ++++++++++++++++++ esp_server/esp_server.csproj | 13 ++ esp_server/esp_server.csproj.user | 6 + esp_server/esp_server.json.sample | 16 ++ esp_server/esp_server.sln | 25 +++ 13 files changed, 653 insertions(+) create mode 100644 esp_server/Logging.cs create mode 100644 esp_server/ModbusServer.cs create mode 100644 esp_server/Program.cs create mode 100644 esp_server/Properties/PublishProfiles/FolderProfile.pubxml create mode 100644 esp_server/Properties/PublishProfiles/FolderProfile.pubxml.user create mode 100644 esp_server/RegistersContainer.cs create mode 100644 esp_server/UnitDataReceiver.cs create mode 100644 esp_server/UnitsDatabase.cs create mode 100644 esp_server/esp_server.csproj create mode 100644 esp_server/esp_server.csproj.user create mode 100644 esp_server/esp_server.json.sample create mode 100644 esp_server/esp_server.sln diff --git a/.gitignore b/.gitignore index 61f014a..391c872 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ thermo_sensor/config.h +esp_server/.vs/ +esp_server/bin/ +esp_server/obj/ diff --git a/esp_server/Logging.cs b/esp_server/Logging.cs new file mode 100644 index 0000000..92cfb2a --- /dev/null +++ b/esp_server/Logging.cs @@ -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; + } + } +} diff --git a/esp_server/ModbusServer.cs b/esp_server/ModbusServer.cs new file mode 100644 index 0000000..9c5a963 --- /dev/null +++ b/esp_server/ModbusServer.cs @@ -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 received; + ManualResetEvent readCompleted; + int currentReceived; + NetworkStream stream; + byte[] buffer = new byte[256]; + + public void Run() + { + logger.Debug("Working with client"); + received = new List(); + 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 response = new List(); + 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 fullResponse = new List(); + 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; + } + } + } +} diff --git a/esp_server/Program.cs b/esp_server/Program.cs new file mode 100644 index 0000000..c1bea27 --- /dev/null +++ b/esp_server/Program.cs @@ -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(RunService); + rootCommand.AddCommand(runCommand); + var openhabConfigCommand = new Command("openhab-config") + { + }; + openhabConfigCommand.Handler = CommandHandler.Create(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); + } + } +} diff --git a/esp_server/Properties/PublishProfiles/FolderProfile.pubxml b/esp_server/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..83ef5b4 --- /dev/null +++ b/esp_server/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,16 @@ + + + + + Release + Any CPU + bin\Release\netcoreapp3.1\publish\ + FileSystem + netcoreapp3.1 + linux-x64 + false + False + + \ No newline at end of file diff --git a/esp_server/Properties/PublishProfiles/FolderProfile.pubxml.user b/esp_server/Properties/PublishProfiles/FolderProfile.pubxml.user new file mode 100644 index 0000000..312c6e3 --- /dev/null +++ b/esp_server/Properties/PublishProfiles/FolderProfile.pubxml.user @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/esp_server/RegistersContainer.cs b/esp_server/RegistersContainer.cs new file mode 100644 index 0000000..e19df38 --- /dev/null +++ b/esp_server/RegistersContainer.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; + +namespace ESPServer +{ + class RegistersContainer + { + private Dictionary registers; + + public RegistersContainer() + { + registers = new Dictionary(); + } + + 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); + } + } +} diff --git a/esp_server/UnitDataReceiver.cs b/esp_server/UnitDataReceiver.cs new file mode 100644 index 0000000..92bd4d6 --- /dev/null +++ b/esp_server/UnitDataReceiver.cs @@ -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); + } + } + } +} diff --git a/esp_server/UnitsDatabase.cs b/esp_server/UnitsDatabase.cs new file mode 100644 index 0000000..828485b --- /dev/null +++ b/esp_server/UnitsDatabase.cs @@ -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 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 units; + + public UnitsDatabase(JsonElement config) + { + units = new List(); + 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(); + 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(); + 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 + } +} diff --git a/esp_server/esp_server.csproj b/esp_server/esp_server.csproj new file mode 100644 index 0000000..851103b --- /dev/null +++ b/esp_server/esp_server.csproj @@ -0,0 +1,13 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + diff --git a/esp_server/esp_server.csproj.user b/esp_server/esp_server.csproj.user new file mode 100644 index 0000000..d8f09ee --- /dev/null +++ b/esp_server/esp_server.csproj.user @@ -0,0 +1,6 @@ + + + + <_LastSelectedProfileId>C:\Work\heat_control\esp_server\Properties\PublishProfiles\FolderProfile.pubxml + + \ No newline at end of file diff --git a/esp_server/esp_server.json.sample b/esp_server/esp_server.json.sample new file mode 100644 index 0000000..954456e --- /dev/null +++ b/esp_server/esp_server.json.sample @@ -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 } + ] + } + ] +} \ No newline at end of file diff --git a/esp_server/esp_server.sln b/esp_server/esp_server.sln new file mode 100644 index 0000000..8c3625a --- /dev/null +++ b/esp_server/esp_server.sln @@ -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