1
0

Initial commit

This commit is contained in:
2023-06-21 13:51:38 +02:00
commit 957670ce42
45 changed files with 1894 additions and 0 deletions

View 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
View 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
}

View 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);
}

View 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
View File

@@ -0,0 +1,11 @@
using MoniteurBaie.SerialCom;
var host = Host.CreateDefaultBuilder(args)
.UseSystemd()
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
})
.Build();
await host.RunAsync();

View 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>

View File

@@ -0,0 +1,11 @@
{
"profiles": {
"SerialCom": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}

View 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
View 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);
}
}

View File

@@ -0,0 +1,11 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"MoniteurBaie": {
"Mock": false
}
}

View 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
}
}