Initial commit
This commit is contained in:
221
SerialCom/BatteryController.cs
Normal file
221
SerialCom/BatteryController.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
using System.IO.Ports;
|
||||
using MoniteurBaie.DataModels;
|
||||
using MoniteurBaie.Utils;
|
||||
|
||||
namespace MoniteurBaie.SerialCom;
|
||||
|
||||
public sealed class BatteryController : IBatteryController
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly SerialPort _serialPort;
|
||||
private readonly bool _closePort;
|
||||
|
||||
private readonly List<IObserver<BatteryControllerPacket>> _packetObservers = new();
|
||||
private readonly BlockingListener<string> _commandListener = new();
|
||||
|
||||
public BatteryController(ILogger logger, Action<SerialPort> configure)
|
||||
{
|
||||
_logger = logger;
|
||||
_serialPort = new SerialPort();
|
||||
configure(_serialPort);
|
||||
_closePort = true;
|
||||
}
|
||||
|
||||
public BatteryController(ILogger logger, SerialPort serialPort, bool closePort = false)
|
||||
{
|
||||
_logger = logger;
|
||||
_serialPort = serialPort;
|
||||
_closePort = closePort;
|
||||
}
|
||||
|
||||
public Task Open(CancellationToken cancellationToken)
|
||||
{
|
||||
_serialPort.Open();
|
||||
|
||||
var readThread = new Thread(Test)
|
||||
{
|
||||
IsBackground = true
|
||||
};
|
||||
readThread.Start();
|
||||
|
||||
// var writeThread = new Thread(DoWrite)
|
||||
// {
|
||||
// IsBackground = true
|
||||
// };
|
||||
// writeThread.Start();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public IDisposable AddSerialObserver(IObserver<BatteryControllerPacket> observer)
|
||||
{
|
||||
_packetObservers.Add(observer);
|
||||
return new Disposer(() => _packetObservers.Remove(observer));
|
||||
}
|
||||
|
||||
public Task SendCommand(string command, CancellationToken cancellationToken)
|
||||
{
|
||||
_commandListener.Push(command);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void Test()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (_serialPort.IsOpen)
|
||||
{
|
||||
var b = _serialPort.ReadByte();
|
||||
Console.WriteLine(b);
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException ex)
|
||||
{
|
||||
_logger.LogError(ex, "The serial connection has been disposed.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "An exception occurred in the serial read loop.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void DoRead()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (_serialPort.IsOpen)
|
||||
{
|
||||
string line;
|
||||
try
|
||||
{
|
||||
line = _serialPort.ReadLine();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to read line from serial connection.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var packet = new BatteryControllerPacket(line);
|
||||
|
||||
foreach (var observer in _packetObservers)
|
||||
{
|
||||
try
|
||||
{
|
||||
observer.OnNext(packet);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "An exception occurred in a packet handler.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException ex)
|
||||
{
|
||||
_logger.LogError(ex, "The serial connection has been disposed.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "An exception occurred in the serial read loop.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void DoWrite()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (_serialPort.IsOpen)
|
||||
{
|
||||
var line = _commandListener.Next();
|
||||
|
||||
if (string.IsNullOrEmpty(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_serialPort.Write(line + "\n");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to send command.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException ex)
|
||||
{
|
||||
_logger.LogError(ex, "The serial connection has been disposed.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "An exception occurred in the serial write loop.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#region IObservable
|
||||
|
||||
IDisposable IObservable<BatteryControllerPacket>.Subscribe(IObserver<BatteryControllerPacket> observer)
|
||||
{
|
||||
return AddSerialObserver(observer);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IObserver
|
||||
|
||||
void IObserver<string>.OnCompleted()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void IObserver<string>.OnError(Exception error)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void IObserver<string>.OnNext(string value)
|
||||
{
|
||||
SendCommand(value, default);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
private bool disposedValue;
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (_closePort)
|
||||
{
|
||||
_serialPort.Close();
|
||||
}
|
||||
}
|
||||
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
71
SerialCom/DataApi.cs
Normal file
71
SerialCom/DataApi.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.Net.Http.Json;
|
||||
using MoniteurBaie.DataModels;
|
||||
|
||||
namespace MoniteurBaie.SerialCom;
|
||||
|
||||
sealed class DataApi : IDisposable
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public DataApi(ILogger logger, IConfiguration config)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(config.GetValue<string>("BaseUrl")!),
|
||||
Timeout = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
}
|
||||
|
||||
public void Send(BatteryControllerPacket packet) => Task.Run(() => DoSend(packet));
|
||||
|
||||
private async Task DoSend(BatteryControllerPacket packet)
|
||||
{
|
||||
SerialDataPacket serialPacket;
|
||||
|
||||
try
|
||||
{
|
||||
serialPacket = PacketParser.ParseSerialDataPacket(packet.SerialData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse packet.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _httpClient.PostAsJsonAsync("/packets", serialPacket.ToDataPacket(packet.Timestamp), DataPacketContext.Default.DataPacket);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to send packet.");
|
||||
}
|
||||
}
|
||||
|
||||
#region IDisposable
|
||||
|
||||
private bool _disposedValue;
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
12
SerialCom/IBatteryController.cs
Normal file
12
SerialCom/IBatteryController.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using MoniteurBaie.DataModels;
|
||||
|
||||
namespace MoniteurBaie.SerialCom;
|
||||
|
||||
public interface IBatteryController : IObservable<BatteryControllerPacket>, IObserver<string>, IDisposable
|
||||
{
|
||||
Task Open(CancellationToken cancellationToken);
|
||||
|
||||
IDisposable AddSerialObserver(IObserver<BatteryControllerPacket> observer);
|
||||
|
||||
Task SendCommand(string command, CancellationToken cancellationToken);
|
||||
}
|
||||
129
SerialCom/MockBatteryController.cs
Normal file
129
SerialCom/MockBatteryController.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using MoniteurBaie.DataModels;
|
||||
using MoniteurBaie.Utils;
|
||||
|
||||
namespace MoniteurBaie.SerialCom;
|
||||
|
||||
public sealed class MockBatteryController : IBatteryController
|
||||
{
|
||||
private readonly List<IObserver<BatteryControllerPacket>> _serialObservers = new();
|
||||
private readonly BlockingListener<string> _commandListener = new();
|
||||
|
||||
public Task Open(CancellationToken cancellationToken)
|
||||
{
|
||||
var readThread = new Thread(DoRead)
|
||||
{
|
||||
IsBackground = true
|
||||
};
|
||||
readThread.Start();
|
||||
|
||||
var writeThread = new Thread(DoWrite)
|
||||
{
|
||||
IsBackground = true
|
||||
};
|
||||
writeThread.Start();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public IDisposable AddSerialObserver(IObserver<BatteryControllerPacket> observer)
|
||||
{
|
||||
_serialObservers.Add(observer);
|
||||
return new Disposer(() => _serialObservers.Remove(observer));
|
||||
}
|
||||
|
||||
public Task SendCommand(string command, CancellationToken cancellationToken)
|
||||
{
|
||||
_commandListener.Push(command);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void DoRead()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var packet = CreateFakePacket();
|
||||
|
||||
foreach (var observer in _serialObservers)
|
||||
{
|
||||
observer.OnNext(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BatteryControllerPacket CreateFakePacket()
|
||||
{
|
||||
Thread.Sleep(Random.Shared.Next(1000 - 50, 1000 + 50));
|
||||
|
||||
return new(
|
||||
DateTime.Now,
|
||||
$"COM,{Random.Shared.NextSingle()},{Random.Shared.NextSingle()},{Random.Shared.NextSingle()},{Random.Shared.NextSingle()},{Random.Shared.NextSingle()},{Random.Shared.NextSingle()},{Random.Shared.NextSingle()},{Random.Shared.NextSingle()},{Random.Shared.NextSingle()},{Random.Shared.NextSingle()},{Random.Shared.NextSingle()},{Random.Shared.NextSingle()},{Random.Shared.NextSingle()},{Random.Shared.NextSingle()},{Random.Shared.NextSingle()},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(100)},{Random.Shared.Next(100)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)},{Random.Shared.Next(2)}");
|
||||
}
|
||||
|
||||
private void DoWrite()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
_commandListener.Next();
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
#region IObservable
|
||||
|
||||
IDisposable IObservable<BatteryControllerPacket>.Subscribe(IObserver<BatteryControllerPacket> observer)
|
||||
{
|
||||
return AddSerialObserver(observer);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IObserver
|
||||
|
||||
void IObserver<string>.OnCompleted()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void IObserver<string>.OnError(Exception error)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void IObserver<string>.OnNext(string value)
|
||||
{
|
||||
SendCommand(value, default);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
private bool _disposedValue;
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_commandListener.Dispose();
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
11
SerialCom/Program.cs
Normal file
11
SerialCom/Program.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using MoniteurBaie.SerialCom;
|
||||
|
||||
var host = Host.CreateDefaultBuilder(args)
|
||||
.UseSystemd()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddHostedService<Worker>();
|
||||
})
|
||||
.Build();
|
||||
|
||||
await host.RunAsync();
|
||||
16
SerialCom/Properties/PublishProfiles/RaspiPublish.pubxml
Normal file
16
SerialCom/Properties/PublishProfiles/RaspiPublish.pubxml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>publish\</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<RuntimeIdentifier>linux-arm64</RuntimeIdentifier>
|
||||
<SelfContained>false</SelfContained>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
11
SerialCom/Properties/launchSettings.json
Normal file
11
SerialCom/Properties/launchSettings.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"profiles": {
|
||||
"SerialCom": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
SerialCom/SerialCom.csproj
Normal file
25
SerialCom/SerialCom.csproj
Normal file
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>dotnet-SerialCom-7208DE51-06AE-44FC-809D-543080AAAFF0</UserSecretsId>
|
||||
<RootNamespace>$(SolutionName.Replace(" ", "_")).$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="publish\**" />
|
||||
<Content Remove="publish\**" />
|
||||
<EmbeddedResource Remove="publish\**" />
|
||||
<None Remove="publish\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="7.0.0" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.6.104" />
|
||||
<PackageReference Include="System.IO.Ports" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DataModels\DataModels.csproj" />
|
||||
<ProjectReference Include="..\Utils\Utils.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
63
SerialCom/Worker.cs
Normal file
63
SerialCom/Worker.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.IO.Ports;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using MoniteurBaie.DataModels;
|
||||
using MoniteurBaie.Utils;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace MoniteurBaie.SerialCom;
|
||||
|
||||
public class Worker : BackgroundService
|
||||
{
|
||||
private readonly ILogger<Worker> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public Worker(ILogger<Worker> logger, IConfiguration configuration)
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var config = _configuration.GetSection("MoniteurBaie");
|
||||
var serialConfig = config.GetSection("Serial");
|
||||
var redisConfig = config.GetSection("Redis");
|
||||
|
||||
using IBatteryController batteryController = config.GetValue<bool>("Mock")
|
||||
? new MockBatteryController()
|
||||
: new BatteryController(_logger, serialPort =>
|
||||
{
|
||||
serialPort.PortName = serialConfig.GetValue<string>("Path")!;
|
||||
serialPort.BaudRate = serialConfig.GetValue<int>("BaudRate");
|
||||
serialPort.Parity = serialConfig.GetValue<Parity>("Parity");
|
||||
serialPort.DataBits = serialConfig.GetValue<int>("DataBits");
|
||||
serialPort.StopBits = serialConfig.GetValue<StopBits>("StopBits");
|
||||
serialPort.Handshake = serialConfig.GetValue<Handshake>("Handshake");
|
||||
serialPort.Encoding = Encoding.GetEncoding(serialConfig.GetValue<string>("Encoding")!);
|
||||
serialPort.NewLine = serialConfig.GetValue<string>("NewLine")!;
|
||||
});
|
||||
|
||||
await batteryController.Open(stoppingToken);
|
||||
|
||||
using var redis = await ConnectionMultiplexer.ConnectAsync(redisConfig.GetValue<string>("Endpoint")!, opts =>
|
||||
{
|
||||
opts.ClientName = redisConfig.GetValue<string>("ClientName");
|
||||
});
|
||||
|
||||
using var dataApi = new DataApi(_logger, config.GetSection("Api"));
|
||||
|
||||
var redisChannel = redisConfig.GetValue<string>("Channels:Packets")!;
|
||||
batteryController.AddSerialObserver(Listener.Create((BatteryControllerPacket packet) =>
|
||||
{
|
||||
var dataPacket = JsonSerializer.Serialize(packet, BatteryControllerPacketContext.Default.BatteryControllerPacket);
|
||||
redis.GetSubscriber().Publish(redisChannel, dataPacket, CommandFlags.FireAndForget);
|
||||
dataApi.Send(packet);
|
||||
}));
|
||||
|
||||
var mq = await redis.GetSubscriber().SubscribeAsync(redisConfig.GetValue<string>("Channels:Commands")!);
|
||||
mq.OnMessage(channelMessage => batteryController.SendCommand(channelMessage.Message.ToString(), stoppingToken));
|
||||
|
||||
await Task.Delay(Timeout.Infinite, stoppingToken);
|
||||
}
|
||||
}
|
||||
11
SerialCom/appsettings.Development.json
Normal file
11
SerialCom/appsettings.Development.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"MoniteurBaie": {
|
||||
"Mock": false
|
||||
}
|
||||
}
|
||||
32
SerialCom/appsettings.json
Normal file
32
SerialCom/appsettings.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"MoniteurBaie": {
|
||||
"Serial": {
|
||||
"Path": "/dev/ttyAMA0",
|
||||
"BaudRate": 115200,
|
||||
"Parity": "None",
|
||||
"DataBits": 8,
|
||||
"StopBits": "One",
|
||||
"Handshake": "None",
|
||||
"Encoding": "UTF-8",
|
||||
"NewLine": "\r\n"
|
||||
},
|
||||
"Redis": {
|
||||
"Endpoint": "mercedes.hbsha.re:6379",
|
||||
"ClientName": "Serial",
|
||||
"Channels": {
|
||||
"Packets": "batCtrlPackets",
|
||||
"Commands": "batCtrlCommands"
|
||||
}
|
||||
},
|
||||
"Api": {
|
||||
"BaseUrl": "http://mercedes.hbsha.re:5000"
|
||||
},
|
||||
"Mock": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user