make AMPT use updated Modbus Lib

This commit is contained in:
ig 2023-05-06 15:41:20 +02:00
parent 0ac7773ac0
commit a2c0674ecb
9 changed files with 648 additions and 130 deletions

View File

@ -1,9 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configurations>Debug;Release;Release-Server</Configurations>
<Platforms>AnyCPU;linux-arm</Platforms>
</PropertyGroup>
<Import Project="../../InnovEnergy.Lib.props" />
<!-- <Import Project="../../InnovEnergy.Lib.props"/>-->
<Import Project="../../../App/InnovEnergy.App.props" />
<ItemGroup>
<ProjectReference Include="../../Protocols/Modbus/Modbus.csproj" />

View File

@ -1,118 +1,90 @@
using InnovEnergy.Lib.Protocols.Modbus.Clients;
using InnovEnergy.Lib.Protocols.Modbus.Connections;
using InnovEnergy.Lib.Units.Composite;
using static DecimalMath.DecimalEx;
namespace InnovEnergy.Lib.Devices.AMPT;
public class AmptCommunicationUnit
{
private ModbusTcpClient? Modbus { get; set; }
private const UInt16 RegistersPerDevice = 16;
private const UInt16 FirstDeviceOffset = 85;
public String Hostname { get; }
public UInt16 Port { get; }
public Byte SlaveAddress { get; }
public AmptCommunicationUnit(String hostname, UInt16 port = 502, Byte slaveAddress = 1)
{
Hostname = hostname;
Port = port;
SlaveAddress = slaveAddress;
}
public AmptCommunicationUnitStatus? ReadStatus()
{
try
{
OpenConnection();
return TryReadStatus();
}
catch
{
CloseConnection();
return null;
}
}
private void CloseConnection()
{
try
{
Modbus?.CloseConnection();
}
catch
{
// ignored
}
Modbus = null;
}
private void OpenConnection()
{
if (Modbus is null)
{
var connection = new ModbusTcpConnection(Hostname, Port);
Modbus = new ModbusTcpClient(connection, SlaveAddress);
}
}
private AmptCommunicationUnitStatus TryReadStatus()
{
var r = Modbus!.ReadHoldingRegisters(1, 116);
var currentFactor = Pow(10.0m, r.GetInt16(73));
var voltageFactor = Pow(10.0m, r.GetInt16(74));
var energyFactor = Pow(10.0m, r.GetInt16(76) + 3); // +3 => converted from Wh to kWh
var nbrOfDevices = r.GetUInt16(78);
var devices = Enumerable
.Range(0, nbrOfDevices)
.Select(ReadDeviceStatus)
.ToList();
return new AmptCommunicationUnitStatus
{
Sid = r.GetUInt32(1),
IdSunSpec = r.GetUInt16(3),
Manufacturer = r.GetString(5, 16),
Model = r.GetString(21, 16),
Version = r.GetString(45, 8),
SerialNumber = r.GetString(53, 16),
DeviceAddress = r.GetInt16(69),
IdVendor = r.GetUInt16(71),
Devices = devices
};
AmptStatus ReadDeviceStatus(Int32 deviceNumber)
{
var baseAddress = (UInt16)(FirstDeviceOffset + deviceNumber * RegistersPerDevice); // base address
return new AmptStatus
{
Dc = new DcBus
{
Voltage = r.GetUInt32((UInt16)(baseAddress + 6)) * voltageFactor,
Current = r.GetUInt16((UInt16)(baseAddress + 5)) * currentFactor
},
Strings = new DcBus[]
{
new()
{
Voltage = r.GetUInt32((UInt16)(baseAddress + 8)) * voltageFactor,
Current = r.GetUInt16((UInt16)(baseAddress + 14)) * currentFactor
},
new()
{
Voltage = r.GetUInt32((UInt16)(baseAddress + 9)) * voltageFactor,
Current = r.GetUInt16((UInt16)(baseAddress + 15)) * currentFactor
}
},
ProductionToday = r.GetUInt32((UInt16)(baseAddress + 12)) * energyFactor,
};
}
}
}
// using InnovEnergy.Lib.Protocols.Modbus.Channels;
// using InnovEnergy.Lib.Protocols.Modbus.Clients;
// using InnovEnergy.Lib.Protocols.Modbus.Conversions;
// using InnovEnergy.Lib.Protocols.Modbus.Slaves;
// using InnovEnergy.Lib.Units.Composite;
//
// namespace InnovEnergy.Lib.Devices.AMPT;
//
// public class AmptCommunicationUnit : ModbusDevice<CommunicationUnitRegisters>
// {
//
// private const UInt16 RegistersPerDevice = 16;
// private const UInt16 FirstDeviceOffset = 85;
//
//
// public AmptCommunicationUnit(String hostname, UInt16 port = 502, Byte slaveAddress = 1) : this
// (
// channel: new TcpChannel(hostname, port),
// slaveAddress
// )
// {}
//
//
// public AmptCommunicationUnit(Channel channel, Byte slaveAddress) : this
// (
// client: new ModbusTcpClient(channel, slaveAddress)
// )
// {}
//
// public AmptCommunicationUnit(ModbusClient client) : base(client)
// {
// }
//
//
// private AmptCommunicationUnitStatus TryReadStatus()
// {
// var r = new ModbusRegisters(116, Modbus.ReadHoldingRegisters(1, 116).ToArray()) ; // TODO
//
// var currentFactor = Pow(10.0m, r.GetInt16(73));
// var voltageFactor = Pow(10.0m, r.GetInt16(74));
// var energyFactor = Pow(10.0m, r.GetInt16(76) + 3); // +3 => converted from Wh to kWh
// var nbrOfDevices = r.GetUInt16(78);
//
// var devices = Enumerable
// .Range(0, nbrOfDevices)
// .Select(ReadDeviceStatus)
// .ToList();
//
// return new AmptCommunicationUnitStatus
// {
// Sid = r.GetUInt32(1),
// IdSunSpec = r.GetUInt16(3),
// Manufacturer = r.GetString(5, 16),
// Model = r.GetString(21, 16),
// Version = r.GetString(45, 8),
// SerialNumber = r.GetString(53, 16),
// DeviceAddress = r.GetInt16(69),
// IdVendor = r.GetUInt16(71),
// Devices = devices
// };
//
// AmptStatus ReadDeviceStatus(Int32 deviceNumber)
// {
// var baseAddress = (UInt16)(FirstDeviceOffset + deviceNumber * RegistersPerDevice); // base address
//
// return new AmptStatus
// {
// Dc = new DcBus
// {
// Voltage = r.GetUInt32((UInt16)(baseAddress + 6)) * voltageFactor,
// Current = r.GetUInt16((UInt16)(baseAddress + 5)) * currentFactor
// },
// Strings = new DcBus[]
// {
// new()
// {
// Voltage = r.GetUInt32((UInt16)(baseAddress + 8)) * voltageFactor,
// Current = r.GetUInt16((UInt16)(baseAddress + 14)) * currentFactor
// },
// new()
// {
// Voltage = r.GetUInt32((UInt16)(baseAddress + 9)) * voltageFactor,
// Current = r.GetUInt16((UInt16)(baseAddress + 15)) * currentFactor
// }
// },
// ProductionToday = r.GetUInt32((UInt16)(baseAddress + 12)) * energyFactor,
// };
// }
// }
// }

View File

@ -1,6 +1,6 @@
namespace InnovEnergy.Lib.Devices.AMPT;
public record AmptCommunicationUnitStatus
public class AmptCommunicationUnitStatus
{
public UInt32 Sid { get; init; } // A well-known value 0x53756e53, uniquely identifies this as a SunSpec Modbus Map
public UInt16 IdSunSpec { get; init; } // A well-known value 1, uniquely identifies this as a SunSpec Common Model

View File

@ -0,0 +1,67 @@
using InnovEnergy.Lib.Protocols.Modbus.Clients;
using InnovEnergy.Lib.Protocols.Modbus.Slaves;
using InnovEnergy.Lib.Units.Composite;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.Lib.Devices.AMPT;
public class AmptDevice
{
private readonly ModbusDevice<CommunicationUnitRegisters> _CommunicationUnit;
private readonly IEnumerable<ModbusDevice<StringOptimizerRegisters>> _StringOptimizers;
public AmptDevice(ModbusClient modbusClient)
{
_CommunicationUnit = new ModbusDevice<CommunicationUnitRegisters>(modbusClient);
_StringOptimizers = StringOptimizers(modbusClient);
}
public AmptStatus? Read()
{
var cuStatus = _CommunicationUnit.Read();
if (cuStatus.NumberOfStrings == 0)
return default;
var rs = _StringOptimizers
.Take(cuStatus.NumberOfStrings)
.Select(so => so.Read())
.ToArray(cuStatus.NumberOfStrings);
var busVoltage = rs.Average(r => r.Voltage);
var busCurrent = rs.Sum(r => r.Current);
var strings = rs.SelectMany(GetStrings).ToArray(rs.Length * 2);
return new AmptStatus
(
Dc : DcBus.FromVoltageCurrent(busVoltage, busCurrent),
Strings: strings
);
}
private static IEnumerable<DcBus> GetStrings(StringOptimizerRegisters r)
{
yield return DcBus.FromVoltageCurrent(r.String1Voltage, r.String1Current);
yield return DcBus.FromVoltageCurrent(r.String2Voltage, r.String2Current);
}
private static IEnumerable<ModbusDevice<StringOptimizerRegisters>> StringOptimizers(ModbusClient modbusClient)
{
var cache = new List<ModbusDevice<StringOptimizerRegisters>>();
ModbusDevice<StringOptimizerRegisters> GetOptimizer(Int32 i)
{
if (i < cache.Count)
return cache[i];
var modbusDevice = new ModbusDevice<StringOptimizerRegisters>(modbusClient, i * 16);
return modbusDevice.Apply(cache.Add);
}
return Enumerable
.Range(0, Byte.MaxValue)
.Select(GetOptimizer);
}
}

View File

@ -1,9 +1,6 @@
using InnovEnergy.Lib.StatusApi;
using InnovEnergy.Lib.Units;
using InnovEnergy.Lib.StatusApi.DeviceTypes;
using InnovEnergy.Lib.Units.Composite;
namespace InnovEnergy.Lib.Devices.AMPT;
public record AmptStatus : MpptStatus
{
public Energy ProductionToday { get; init; } // converted to kW in AmptCU class
}
public record AmptStatus(DcBus Dc, IReadOnlyList<DcBus> Strings) : IMppt;

View File

@ -0,0 +1,18 @@
using System.Diagnostics.CodeAnalysis;
using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
namespace InnovEnergy.Lib.Devices.AMPT;
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
[SuppressMessage("ReSharper", "UnusedMember.Global")]
[OneBasedAddressing]
public record CommunicationUnitRegisters
{
[HoldingRegister<Int16>(73)] public Int16 CurrentScaleFactor { get; private set; }
[HoldingRegister<Int16>(74)] public Int16 VoltageScaleFactor { get; private set; }
[HoldingRegister<Int16>(76)] public Int16 EnergyScaleFactor { get; private set; }
[HoldingRegister(78)] public UInt16 NumberOfStrings { get; private set; }
}

View File

@ -0,0 +1,412 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>Modbus Map</title>
<style>
* {
font-family: Arial, sans-serif;
font-size: 14px;
}
h2 {
font-size: 15px;
}
html, body {
margin: 0;
padding: 0;
}
a {
color: #565A5C;
text-decoration: none;
}
div.content {
position: relative;
text-align: center;
padding: 0;
margin-top: 0;
height: 100%;
overflow: auto;
}
div.textblock {
text-align: left;
padding: 0;
width: 40%;
position: relative;
margin: auto;
}
table.main {
border: solid black 2px;
border-spacing: 0;
margin: auto;
}
td {
padding: 6px;
border: solid black 1px;
border-spacing: 0;
}
tr.even {
background: #EFEFEF;
}
tr.odd {
background: #DDDDDD;
}
td.desc {
text-align: left;
}
</style>
</head>
<body>
<div class="content">
<br><br>
<h2>Modbus Map</h2>
<div class="textblock">
<p>These Modbus maps are for your reference. The modbus service runs on port 502.</p>
<p>Important note: The Ampt ModBus register map uses big endian values.</p>
<p><a href="smdx_64050.xml">SunSpec SMDX File</a></p>
</div>
<br>
<h2> SunSpec Registers </h2>
<table class="main" style="width:70%">
<tbody>
<tr class="odd">
<td><strong>Start Offset</strong></td>
<td><strong>Size</strong></td>
<td><strong>Name</strong></td>
<td><strong>Type</strong></td>
<td><strong>R/W</strong></td>
<td><strong>Description</strong></td>
</tr>
<tr class="even">
<td>1</td>
<td>2</td>
<td>SID</td>
<td>uint32</td>
<td>R</td>
<td class="desc">A well-known value 0x53756e53, uniquely identifies this as a SunSpec Modbus Map</td>
</tr>
<tr class="odd">
<td>3</td>
<td>1</td>
<td>ID</td>
<td>uint16</td>
<td>R</td>
<td class="desc">A well-known value 1, uniquely identifies this as a SunSpec Common Model</td>
</tr>
<tr class="even">
<td>4</td>
<td>1</td>
<td>L</td>
<td>uint16</td>
<td>R</td>
<td class="desc">Well-known # of 16-bit registers to follow: 66</td>
</tr>
<tr class="odd">
<td>5</td>
<td>16</td>
<td>Manufacturer</td>
<td>string</td>
<td>R</td>
<td class="desc">A well-known value registered with SunSpec for compliance: "Ampt"</td>
</tr>
<tr class="even">
<td>21</td>
<td>16</td>
<td>Model</td>
<td>string</td>
<td>R</td>
<td class="desc">Manufacturer specific value "Communication Unit"</td>
</tr>
<tr class="odd">
<td>37</td>
<td>8</td>
<td><em>Reserved</em></td>
<td>-</td>
<td>-</td>
<td></td>
</tr>
<tr class="even">
<td>45</td>
<td>8</td>
<td>Version</td>
<td>string</td>
<td>R</td>
<td class="desc">Software Version</td>
</tr>
<tr class="odd">
<td>53</td>
<td>16</td>
<td>Serial Number</td>
<td>string</td>
<td>R</td>
<td class="desc">Manufacturer specific value</td>
</tr>
<tr class="even">
<td>69</td>
<td>1</td>
<td>Device Address</td>
<td>int16</td>
<td>R/W</td>
<td class="desc">Modbus Device ID</td>
</tr>
<tr class="odd">
<td>70</td>
<td>1</td>
<td><em>Reserved</em></td>
<td>-</td>
<td>-</td>
<td></td>
</tr>
<tr class="even">
<td>71</td>
<td>1</td>
<td>ID</td>
<td>uint16</td>
<td>R</td>
<td class="desc">Ampt SunSpec Vendor Code 64050</td>
</tr>
<tr class="odd">
<td>72</td>
<td>1</td>
<td>L</td>
<td>uint16</td>
<td>R</td>
<td class="desc">Variable number of 16-bit registers to follow: 12 + N*16</td>
</tr>
<tr class="even">
<td>73</td>
<td>1</td>
<td>DCA_SF</td>
<td>int16</td>
<td>R</td>
<td class="desc">Current scale factor</td>
</tr>
<tr class="odd">
<td>74</td>
<td>1</td>
<td>DCV_SF</td>
<td>int16</td>
<td>R</td>
<td class="desc">Voltage scale factor</td>
</tr>
<tr class="even">
<td>75</td>
<td>1</td>
<td><em>Reserved</em></td>
<td>-</td>
<td>-</td>
<td></td>
</tr>
<tr class="odd">
<td>76</td>
<td>1</td>
<td>DCkWh_SF</td>
<td>int16</td>
<td>R</td>
<td class="desc">Energy Scale Factor</td>
</tr>
<tr class="even">
<td>77</td>
<td>1</td>
<td><em>Reserved</em></td>
<td>-</td>
<td>-</td>
<td></td>
</tr>
<tr class="odd">
<td>78</td>
<td>1</td>
<td>N</td>
<td>uint16</td>
<td>R</td>
<td class="desc">Number of strings</td>
</tr>
<tr class="even">
<td>79</td>
<td>6</td>
<td><em>Reserved</em></td>
<td>-</td>
<td>-</td>
<td></td>
</tr>
<tr>
<td colspan="6"><strong>1</strong></td>
</tr>
<tr class="even">
<td>85</td>
<td>1</td>
<td>String ID</td>
<td>int16</td>
<td>R</td>
<td class="desc">The string number</td>
</tr>
<tr class="odd">
<td>86</td>
<td>2</td>
<td><em>Reserved</em></td>
<td>-</td>
<td>-</td>
<td></td>
</tr>
<tr class="even">
<td>88</td>
<td>2</td>
<td>String data timestamp</td>
<td>uint32</td>
<td>R</td>
<td class="desc">The UTC timestamp of the measurements</td>
</tr>
<tr class="odd">
<td>90</td>
<td>1</td>
<td>OutDCA</td>
<td>int16</td>
<td>R</td>
<td class="desc">String output current in mA</td>
</tr>
<tr class="even">
<td>91</td>
<td>2</td>
<td>OutDCV</td>
<td>uint32</td>
<td>R</td>
<td class="desc">String output voltage in mV</td>
</tr>
<tr class="odd">
<td>93</td>
<td>2</td>
<td>In1DCV</td>
<td>uint32</td>
<td>R</td>
<td class="desc">String input 1 voltage in mV</td>
</tr>
<tr class="even">
<td>95</td>
<td>2</td>
<td>In2DCV</td>
<td>uint32</td>
<td>R</td>
<td class="desc">String input 2 voltage in mV</td>
</tr>
<tr class="odd">
<td>97</td>
<td>2</td>
<td>DCWh</td>
<td>uint32</td>
<td>R</td>
<td class="desc">Daily integrated string output energy in Wh</td>
</tr>
<tr class="even">
<td>99</td>
<td>1</td>
<td>In1DCA</td>
<td>int16</td>
<td>R</td>
<td class="desc">String input 1 current in mA</td>
</tr>
<tr class="odd">
<td>100</td>
<td>1</td>
<td>In2DCA</td>
<td>int16</td>
<td>R</td>
<td class="desc">String input 2 current in mA</td>
</tr>
<tr>
<td colspan="6"><strong>2</strong></td>
</tr>
<tr class="even">
<td>101</td>
<td>1</td>
<td>String ID</td>
<td>int16</td>
<td>R</td>
<td class="desc">The string number</td>
</tr>
<tr class="odd">
<td>102</td>
<td>2</td>
<td><em>Reserved</em></td>
<td>-</td>
<td>-</td>
<td></td>
</tr>
<tr class="even">
<td>104</td>
<td>2</td>
<td>String data timestamp</td>
<td>uint32</td>
<td>R</td>
<td class="desc">The UTC timestamp of the measurements</td>
</tr>
<tr class="odd">
<td>106</td>
<td>1</td>
<td>OutDCA</td>
<td>int16</td>
<td>R</td>
<td class="desc">String output current in mA</td>
</tr>
<tr class="even">
<td>107</td>
<td>2</td>
<td>OutDCV</td>
<td>uint32</td>
<td>R</td>
<td class="desc">String output voltage in mV</td>
</tr>
<tr class="odd">
<td>109</td>
<td>2</td>
<td>In1DCV</td>
<td>uint32</td>
<td>R</td>
<td class="desc">String input 1 voltage in mV</td>
</tr>
<tr class="even">
<td>111</td>
<td>2</td>
<td>In2DCV</td>
<td>uint32</td>
<td>R</td>
<td class="desc">String input 2 voltage in mV</td>
</tr>
<tr class="odd">
<td>113</td>
<td>2</td>
<td>DCWh</td>
<td>uint32</td>
<td>R</td>
<td class="desc">Daily integrated string output energy in Wh</td>
</tr>
<tr class="even">
<td>115</td>
<td>1</td>
<td>In1DCA</td>
<td>int16</td>
<td>R</td>
<td class="desc">String input 1 current in mA</td>
</tr>
<tr class="odd">
<td>116</td>
<td>1</td>
<td>In2DCA</td>
<td>int16</td>
<td>R</td>
<td class="desc">String input 2 current in mA</td>
</tr>
</tbody>
</table>
<br><br><br>
</div>
</body>
</html>

View File

@ -0,0 +1,29 @@
using System.Text.Json;
using InnovEnergy.Lib.Protocols.Modbus.Channels;
using InnovEnergy.Lib.Protocols.Modbus.Clients;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.Lib.Devices.AMPT;
public static class Program
{
public static Task Main(string[] args)
{
var ch = new TcpChannel("localhost", 5005);
var cl = new ModbusTcpClient(ch, 1);
var d = new AmptDevice(cl);
while (true)
{
var x = d.Read();
var options = new JsonSerializerOptions{WriteIndented = true};
(x, options).Apply(JsonSerializer.Serialize).WriteLine();
//Console.WriteLine(x);
}
//Console.WriteLine(x);
}
}

View File

@ -0,0 +1,25 @@
using System.Diagnostics.CodeAnalysis;
using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
namespace InnovEnergy.Lib.Devices.AMPT;
[SuppressMessage("ReSharper", "UnusedMember.Global")]
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
[OneBasedAddressing][BigEndian]
public record StringOptimizerRegisters
{
[HoldingRegister<UInt32>(88) ] public UInt32 Timestamp { get; private set; }
[HoldingRegister<Int16> (90, Scale = .001)] public Double Current { get; private set; }
[HoldingRegister<UInt32>(91, Scale = .001)] public Double Voltage { get; private set; }
[HoldingRegister<UInt32>(93, Scale = .001)] public Double String1Voltage { get; private set; }
[HoldingRegister<UInt32>(95, Scale = .001)] public Double String2Voltage { get; private set; }
[HoldingRegister<UInt32>(97)] public Double ProductionToday { get; private set; }
[HoldingRegister<Int16>(99, Scale = .001)] public Double String1Current { get; private set; }
[HoldingRegister<Int16>(100, Scale = .001)] public Double String2Current { get; private set; }
}