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

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
/.vs
/.vscode
bin/
obj/
publish/
*.user

View File

@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>$(SolutionName.Replace(" ", "_")).$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Remove="publish\**" />
<EmbeddedResource Remove="publish\**" />
<None Remove="publish\**" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageReference Include="StackExchange.Redis" Version="2.6.70" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DataModels\DataModels.csproj" />
<ProjectReference Include="..\Utils\Utils.csproj" />
</ItemGroup>
</Project>

167
ConsoleLog/Program.cs Normal file
View File

@@ -0,0 +1,167 @@
using System.Globalization;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using MoniteurBaie.DataModels;
using MoniteurBaie.Utils;
using StackExchange.Redis;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
var redisConfig = config.GetSection("MoniteurBaie:Redis");
var commandsChannel = redisConfig.GetValue<string>("Channels:Commands")!;
using var redis = ConnectionMultiplexer.Connect(redisConfig.GetValue<string>("Endpoint")!, opts =>
{
opts.ClientName = redisConfig.GetValue<string>("ClientName");
});
var packetChannel = redis.GetSubscriber().Subscribe(redisConfig.GetValue<string>("Channels:Packets")!);
packetChannel.OnMessage(OnPacketMessage);
redis.GetSubscriber().Subscribe(commandsChannel).OnMessage(OnCommandMessage);
var inputThread = new Thread(DoInput)
{
IsBackground = true
};
inputThread.Start();
using var cancel = new AutoResetEvent(false);
void ctrlC(object? sender, ConsoleCancelEventArgs e)
{
e.Cancel = true;
Console.CancelKeyPress -= ctrlC;
cancel.Set();
}
Console.CancelKeyPress += ctrlC;
cancel.WaitOne();
static void OnPacketMessage(ChannelMessage channelMessage)
{
var message = channelMessage.Message.ToString();
Console.WriteLine("<< " + message);
PrintPacket(message);
}
static void OnCommandMessage(ChannelMessage channelMessage)
{
Console.WriteLine("<< " + channelMessage.Message);
}
static void PrintPacket(string serializedPacket)
{
var sb = new StringBuilder();
BatteryControllerPacket? batCtrlPacket;
try
{
batCtrlPacket = JsonSerializer.Deserialize(serializedPacket, BatteryControllerPacketContext.Default.BatteryControllerPacket);
}
catch (Exception ex)
{
Console.Error.WriteLine("Invalid packet.");
Console.Error.WriteLine(ex);
return;
}
if (batCtrlPacket is null)
{
Console.Error.WriteLine("Null packet.");
return;
}
SerialDataPacket packet;
try
{
packet = PacketParser.ParseSerialDataPacket(batCtrlPacket.SerialData);
}
catch (Exception ex)
{
Console.Error.WriteLine("Invalid data packet.");
Console.Error.WriteLine(ex);
throw;
}
sb.AppendLine($"==== [{batCtrlPacket.Timestamp:yyyy/MM/dd HH:mm:ss)}] ====");
sb.AppendLine();
sb.AppendLine("-- Tensions --");
sb.AppendLine();
sb.AppendLine($"{packet.VBat:F2} // {packet.VB1:F2} / {packet.VB2:F2} / {packet.VB3:F2} / {packet.VB4:F2} / {packet.VB5:F2} / {packet.VB6:F2}");
sb.AppendLine($"Min : {packet.VBmin:F2}");
sb.AppendLine($"Max : {packet.VBmax:F2}");
sb.AppendLine($"Diff : {packet.VBmax - packet.VBmin:F2}");
sb.AppendLine();
sb.AppendLine("-- Puissance --");
sb.AppendLine();
sb.AppendLine($"Courant : {packet.Curr:F2}");
sb.AppendLine($"Puissance : {packet.Power:F2}");
sb.AppendLine($"Energie : {packet.Energy:F2}");
sb.AppendLine();
sb.AppendLine("-- Températures --");
sb.AppendLine();
sb.AppendLine($"Alimentation : {packet.Temp_alim:F2}");
sb.AppendLine($"Chargeur : {packet.Temp_cha:F2}");
sb.AppendLine($"Batterie : {packet.Temp_bat:F2}");
sb.AppendLine();
sb.AppendLine("-- Relais --");
sb.AppendLine();
sb.AppendLine($"1000W : {packet.S_PowRelay.ToAnsi()}");
sb.AppendLine($"120W : {packet.S_ChaRelay.ToAnsi()}");
sb.AppendLine($"Batterie-carte : {packet.S_BatRelay1.ToAnsi()}");
sb.AppendLine($"Batterie-puissance : {packet.S_BatRelay_State.ToAnsi()}");
sb.AppendLine();
sb.AppendLine("-- Défauts --");
sb.AppendLine();
sb.AppendLine($"Défaut général : {packet.DF_GENERAL.ToAnsi()}");
sb.AppendLine();
sb.AppendLine($"Température alim : {packet.DF_TEMP_ALIM.ToAnsi()} / {packet.MEM_DF_TEMP_ALIM.ToAnsi()}");
sb.AppendLine($"Température batterie : {packet.DF_TEMP_BAT.ToAnsi()} / {packet.MEM_DF_TEMP_BAT.ToAnsi()}");
sb.AppendLine($"Température chargeur : {packet.DF_TEMP_CHA.ToAnsi()} / {packet.MEM_DF_TEMP_CHA.ToAnsi()}");
sb.AppendLine($"Mesure tension incohérente : {packet.DF_V_INCOHERENT.ToAnsi()}");
sb.AppendLine($"Mauvaise cellule : {packet.DF_BAD_CELL.ToAnsi()}");
sb.AppendLine($"Surintensité : {packet.DF_OVERCURRENT.ToAnsi()}");
sb.AppendLine($"Surintensité critique : {packet.DF_OVERCURRENT_STOP.ToAnsi()}");
sb.AppendLine($"Stop général : {packet.DF_STOP_GENERAL.ToAnsi()}");
sb.AppendLine($"Surtension cellule : {packet.DF_CELL_OVERVOLTAGE.ToAnsi()} / {packet.MEM_DF_CELL_OVERVOLTAGE.ToAnsi()}");
sb.AppendLine($"Equilibrage : {packet.DF_UNBALANCE.ToAnsi()} / {packet.MEM_DF_UNBALANCE.ToAnsi()}");
sb.AppendLine();
sb.AppendLine("-- Informations --");
sb.AppendLine();
sb.AppendLine($"Décharge : {packet.DECHARGE.ToAnsi()} / {packet.MEM_DECHARGE.ToAnsi()}");
sb.AppendLine($"Décharge forcée : {packet.Flag_decharge.ToAnsi()}");
sb.AppendLine();
sb.AppendLine($"Buzzer stop : {packet.Buzzer_stop.ToAnsi()}");
sb.AppendLine($"Compteur demande coupure batterie : {packet.Compteur_demande_coupure_batterie}");
sb.AppendLine($"Compteur demande coupure totale : {packet.Compteur_demande_coupure_totale}");
Console.WriteLine(sb.ToString());
}
void DoInput()
{
while (true)
{
var line = Console.ReadLine();
if (string.IsNullOrEmpty(line))
{
continue;
}
Console.WriteLine(">> " + line);
redis.GetSubscriber().Publish(commandsChannel, line);
}
}

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,12 @@
{
"MoniteurBaie": {
"Redis": {
"Endpoint": "mercedes.hbsha.re:6379",
"ClientName": "Console",
"Channels": {
"Packets": "batCtrlPackets",
"Commands": "batCtrlCommands"
}
}
}
}

View File

@@ -0,0 +1,19 @@
using MySqlConnector;
namespace MoniteurBaie.DataApi.Common.Repositories;
public class BaseMariaDbRepository
{
private readonly string _connectionString;
protected BaseMariaDbRepository(IConfiguration config, string connectionStringName) : this(config.GetConnectionString(connectionStringName)!) { }
protected BaseMariaDbRepository(string connectionString) => _connectionString = connectionString;
protected async Task<MySqlConnection> NewConnectionAsync()
{
var connection = new MySqlConnection(_connectionString);
await connection.OpenAsync();
return connection;
}
}

View File

@@ -0,0 +1,59 @@
using Microsoft.AspNetCore.Mvc;
using MoniteurBaie.DataApi.Interfaces.Repositories;
using MoniteurBaie.DataModels;
namespace MoniteurBaie.DataApi.Controllers;
[ApiController]
[Route("packets")]
public class DataPacketsController : ControllerBase
{
private readonly ILogger<DataPacketsController> _logger;
private readonly IDataRepository _dataRepository;
public DataPacketsController(ILogger<DataPacketsController> logger, IDataRepository dataRepository)
{
_logger = logger;
_dataRepository = dataRepository;
}
[HttpGet("{id}", Name = "GetDataPacket")]
public async Task<IActionResult> GetAsync(uint id)
{
var packet = await _dataRepository.GetAsync(id);
return packet is null ? NotFound() : Ok(packet);
}
[HttpPost(Name = "CreateDataPacket")]
public async Task<IActionResult> CreateAsync(DataPacket packet)
{
var packetId = await _dataRepository.CreateAsync(packet);
return CreatedAtRoute("GetDataPacket", new { id = packetId }, packet with { Id = packetId });
}
[HttpDelete("{id}", Name = "DeleteDataPacket")]
public async Task<IActionResult> DeleteAsync(uint id)
{
var success = await _dataRepository.DeleteAsync(id);
return success ? NoContent() : NotFound();
}
[HttpGet("last", Name = "GetLastDataPackets")]
public IActionResult GetLastAsync(int count)
{
return Ok(_dataRepository.GetLastAsync(count));
}
[HttpGet("range", Name = "GetRangeDataPackets")]
public IActionResult GetRangeAsync(DateTime fromInstant, DateTime toInstant)
{
return Ok(_dataRepository.GetRangeAsync(fromInstant, toInstant));
}
[HttpPost("bulk", Name = "BulkCreateDataPackets")]
public async Task<IActionResult> BulkCreateAsync(IEnumerable<DataPacket> packets)
{
await _dataRepository.BulkCreateAsync(packets);
return Ok();
}
}

28
DataApi/DataApi.csproj Normal file
View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>$(SolutionName.Replace(" ", "_")).$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Remove="publish\**" />
<Content Remove="publish\**" />
<EmbeddedResource Remove="publish\**" />
<None Remove="publish\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="7.0.0" />
<PackageReference Include="MySqlConnector" Version="2.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DataModels\DataModels.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,18 @@
using MoniteurBaie.DataModels;
namespace MoniteurBaie.DataApi.Interfaces.Repositories;
public interface IDataRepository
{
Task<DataPacket?> GetAsync(uint id);
Task<uint> CreateAsync(DataPacket packet);
IAsyncEnumerable<DataPacket> GetRangeAsync(DateTime fromInstant, DateTime toInstant);
IAsyncEnumerable<DataPacket> GetLastAsync(int count);
Task<bool> DeleteAsync(uint id);
Task BulkCreateAsync(IEnumerable<DataPacket> packets);
}

30
DataApi/Program.cs Normal file
View File

@@ -0,0 +1,30 @@
using MoniteurBaie.DataApi.Interfaces.Repositories;
using MoniteurBaie.DataApi.Repositories;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSystemd();
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<IDataRepository, DataRepository>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<DeleteExistingFiles>true</DeleteExistingFiles>
<ExcludeApp_Data>false</ExcludeApp_Data>
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<PublishProvider>FileSystem</PublishProvider>
<PublishUrl>publish\</PublishUrl>
<WebPublishMethod>FileSystem</WebPublishMethod>
<SiteUrlToLaunchAfterPublish />
<TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<PublishSingleFile>true</PublishSingleFile>
<ProjectGuid>12a93e6e-00cb-4447-b431-64ebfe8ee566</ProjectGuid>
<SelfContained>false</SelfContained>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,31 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:56142",
"sslPort": 0
}
},
"profiles": {
"DataAccess": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5256",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,246 @@
using MoniteurBaie.DataApi.Common.Repositories;
using MoniteurBaie.DataApi.Interfaces.Repositories;
using MoniteurBaie.DataModels;
using MySqlConnector;
namespace MoniteurBaie.DataApi.Repositories;
public class DataRepository : BaseMariaDbRepository, IDataRepository
{
public DataRepository(IConfiguration config) : base(config, "MariaDb")
{
}
private static DataPacket Map(MySqlDataReader reader) => new(
reader.GetUInt32(0),
reader.GetDateTime(1),
reader.GetFloat(2),
reader.GetFloat(3),
reader.GetFloat(4),
reader.GetFloat(5),
reader.GetFloat(6),
reader.GetFloat(7),
reader.GetFloat(8),
reader.GetFloat(9),
reader.GetFloat(10),
reader.GetFloat(11),
reader.GetFloat(12),
reader.GetFloat(13),
reader.GetFloat(14),
reader.GetFloat(15),
reader.GetFloat(16),
reader.GetBoolean(17),
reader.GetBoolean(18),
reader.GetBoolean(19),
reader.GetBoolean(20),
reader.GetBoolean(21),
reader.GetBoolean(22),
reader.GetBoolean(23),
reader.GetBoolean(24),
reader.GetBoolean(25),
reader.GetBoolean(26),
reader.GetBoolean(27),
reader.GetBoolean(28),
reader.GetBoolean(29),
reader.GetBoolean(30),
reader.GetBoolean(31),
reader.GetBoolean(32),
reader.GetBoolean(33),
reader.GetBoolean(34),
reader.GetBoolean(35),
reader.GetUInt32(36),
reader.GetUInt32(37),
reader.GetBoolean(38),
reader.GetBoolean(39),
reader.GetBoolean(40),
reader.GetBoolean(41),
reader.GetBoolean(42));
public async Task<DataPacket?> GetAsync(uint id)
{
await using var connection = await NewConnectionAsync();
await using var command = connection.CreateCommand();
command.CommandText = "SELECT * FROM packets WHERE id = @id";
command.Parameters.AddWithValue("@id", id);
await using var reader = await command.ExecuteReaderAsync();
return await reader.ReadAsync() ? Map(reader) : null;
}
public async IAsyncEnumerable<DataPacket> GetRangeAsync(DateTime fromInstant, DateTime toInstant)
{
await using var connection = await NewConnectionAsync();
await using var command = connection.CreateCommand();
command.CommandText = "SELECT * FROM packets WHERE timestamp BETWEEN @from AND @to ORDER BY timestamp DESC";
command.Parameters.AddWithValue("@from", fromInstant);
command.Parameters.AddWithValue("@to", toInstant);
await using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
yield return Map(reader);
}
}
public async IAsyncEnumerable<DataPacket> GetLastAsync(int count)
{
await using var connection = await NewConnectionAsync();
await using var command = connection.CreateCommand();
command.CommandText = "SELECT * FROM packets ORDER BY timestamp DESC FETCH FIRST @count ROWS ONLY";
command.Parameters.AddWithValue("@count", count);
await using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
yield return Map(reader);
}
}
public async Task<uint> CreateAsync(DataPacket packet)
{
await using var connection = await NewConnectionAsync();
await using var command = connection.CreateCommand();
command.CommandText = "INSERT INTO packets (timestamp, vb1, vb2, vb3, vb4, vb5, vb6, vbat, vb_min, vb_max, current, power, energy, temp_alim, temp_cha, temp_bat, df_temp_alim, mem_df_temp_alim, df_temp_bat, mem_df_temp_bat, df_temp_cha, mem_df_temp_cha, df_v_incoherent, df_bad_cell, df_overcurrent, df_overcurrent_stop, df_stop_general, df_general, df_cell_overvoltage, mem_df_cell_overvoltage, df_unbalance, mem_df_unbalance, buzzer_stop, decharge, mem_decharge, compteur_demande_coupure_batterie, compteur_demande_coupure_totale, s_powrelay, s_charelay, s_batrelay1, s_batrelay_state, flag_decharge) VALUES (@timestamp, @vb1, @vb2, @vb3, @vb4, @vb5, @vb6, @vbat, @vb_min, @vb_max, @current, @power, @energy, @temp_alim, @temp_cha, @temp_bat, @df_temp_alim, @mem_df_temp_alim, @df_temp_bat, @mem_df_temp_bat, @df_temp_cha, @mem_df_temp_cha, @df_v_incoherent, @df_bad_cell, @df_overcurrent, @df_overcurrent_stop, @df_stop_general, @df_general, @df_cell_overvoltage, @mem_df_cell_overvoltage, @df_unbalance, @mem_df_unbalance, @buzzer_stop, @decharge, @mem_decharge, @compteur_demande_coupure_batterie, @compteur_demande_coupure_totale, @s_powrelay, @s_charelay, @s_batrelay1, @s_batrelay_state, @flag_decharge)";
command.Parameters.AddWithValue("@timestamp", packet.Timestamp);
command.Parameters.AddWithValue("@vb1", packet.VB1);
command.Parameters.AddWithValue("@vb2", packet.VB2);
command.Parameters.AddWithValue("@vb3", packet.VB3);
command.Parameters.AddWithValue("@vb4", packet.VB4);
command.Parameters.AddWithValue("@vb5", packet.VB5);
command.Parameters.AddWithValue("@vb6", packet.VB6);
command.Parameters.AddWithValue("@vbat", packet.VBat);
command.Parameters.AddWithValue("@vb_min", packet.VBmin);
command.Parameters.AddWithValue("@vb_max", packet.VBmax);
command.Parameters.AddWithValue("@current", packet.Curr);
command.Parameters.AddWithValue("@power", packet.Power);
command.Parameters.AddWithValue("@energy", packet.Energy);
command.Parameters.AddWithValue("@temp_alim", packet.Temp_alim);
command.Parameters.AddWithValue("@temp_cha", packet.Temp_cha);
command.Parameters.AddWithValue("@temp_bat", packet.Temp_bat);
command.Parameters.AddWithValue("@df_temp_alim", packet.DF_TEMP_ALIM);
command.Parameters.AddWithValue("@mem_df_temp_alim", packet.MEM_DF_TEMP_ALIM);
command.Parameters.AddWithValue("@df_temp_bat", packet.DF_TEMP_BAT);
command.Parameters.AddWithValue("@mem_df_temp_bat", packet.MEM_DF_TEMP_BAT);
command.Parameters.AddWithValue("@df_temp_cha", packet.DF_TEMP_CHA);
command.Parameters.AddWithValue("@mem_df_temp_cha", packet.MEM_DF_TEMP_CHA);
command.Parameters.AddWithValue("@df_v_incoherent", packet.DF_V_INCOHERENT);
command.Parameters.AddWithValue("@df_bad_cell", packet.DF_BAD_CELL);
command.Parameters.AddWithValue("@df_overcurrent", packet.DF_OVERCURRENT);
command.Parameters.AddWithValue("@df_overcurrent_stop", packet.DF_OVERCURRENT_STOP);
command.Parameters.AddWithValue("@df_stop_general", packet.DF_STOP_GENERAL);
command.Parameters.AddWithValue("@df_general", packet.DF_GENERAL);
command.Parameters.AddWithValue("@df_cell_overvoltage", packet.DF_CELL_OVERVOLTAGE);
command.Parameters.AddWithValue("@mem_df_cell_overvoltage", packet.MEM_DF_CELL_OVERVOLTAGE);
command.Parameters.AddWithValue("@df_unbalance", packet.DF_UNBALANCE);
command.Parameters.AddWithValue("@mem_df_unbalance", packet.MEM_DF_UNBALANCE);
command.Parameters.AddWithValue("@buzzer_stop", packet.Buzzer_stop);
command.Parameters.AddWithValue("@decharge", packet.DECHARGE);
command.Parameters.AddWithValue("@mem_decharge", packet.MEM_DECHARGE);
command.Parameters.AddWithValue("@compteur_demande_coupure_batterie", packet.Compteur_demande_coupure_batterie);
command.Parameters.AddWithValue("@compteur_demande_coupure_totale", packet.Compteur_demande_coupure_totale);
command.Parameters.AddWithValue("@s_powrelay", packet.S_PowRelay);
command.Parameters.AddWithValue("@s_charelay", packet.S_ChaRelay);
command.Parameters.AddWithValue("@s_batrelay1", packet.S_BatRelay1);
command.Parameters.AddWithValue("@s_batrelay_state", packet.S_BatRelay_State);
command.Parameters.AddWithValue("@flag_decharge", packet.Flag_decharge);
var rowCount = await command.ExecuteNonQueryAsync();
return rowCount > 0 ? (uint)command.LastInsertedId : 0u;
}
public async Task<bool> DeleteAsync(uint id)
{
await using var connection = await NewConnectionAsync();
await using var command = connection.CreateCommand();
command.CommandText = "DELETE FROM packets WHERE id = @id";
command.Parameters.AddWithValue("@id", id);
var result = await command.ExecuteNonQueryAsync();
return result > 0;
}
public async Task BulkCreateAsync(IEnumerable<DataPacket> packets)
{
await using var connection = await NewConnectionAsync();
await using var transaction = await connection.BeginTransactionAsync();
try
{
await using var command = connection.CreateCommand();
command.Transaction = transaction;
command.CommandText = "INSERT INTO packets (timestamp, vb1, vb2, vb3, vb4, vb5, vb6, vbat, vb_min, vb_max, current, power, energy, temp_alim, temp_cha, temp_bat, df_temp_alim, mem_df_temp_alim, df_temp_bat, mem_df_temp_bat, df_temp_cha, mem_df_temp_cha, df_v_incoherent, df_bad_cell, df_overcurrent, df_overcurrent_stop, df_stop_general, df_general, df_cell_overvoltage, mem_df_cell_overvoltage, df_unbalance, mem_df_unbalance, buzzer_stop, decharge, mem_decharge, compteur_demande_coupure_batterie, compteur_demande_coupure_totale, s_powrelay, s_charelay, s_batrelay1, s_batrelay_state, flag_decharge) VALUES (@timestamp, @vb1, @vb2, @vb3, @vb4, @vb5, @vb6, @vbat, @vb_min, @vb_max, @current, @power, @energy, @temp_alim, @temp_cha, @temp_bat, @df_temp_alim, @mem_df_temp_alim, @df_temp_bat, @mem_df_temp_bat, @df_temp_cha, @mem_df_temp_cha, @df_v_incoherent, @df_bad_cell, @df_overcurrent, @df_overcurrent_stop, @df_stop_general, @df_general, @df_cell_overvoltage, @mem_df_cell_overvoltage, @df_unbalance, @mem_df_unbalance, @buzzer_stop, @decharge, @mem_decharge, @compteur_demande_coupure_batterie, @compteur_demande_coupure_totale, @s_powrelay, @s_charelay, @s_batrelay1, @s_batrelay_state, @flag_decharge)";
foreach (var packet in packets)
{
command.Parameters.AddWithValue("@timestamp", packet.Timestamp);
command.Parameters.AddWithValue("@vb1", packet.VB1);
command.Parameters.AddWithValue("@vb2", packet.VB2);
command.Parameters.AddWithValue("@vb3", packet.VB3);
command.Parameters.AddWithValue("@vb4", packet.VB4);
command.Parameters.AddWithValue("@vb5", packet.VB5);
command.Parameters.AddWithValue("@vb6", packet.VB6);
command.Parameters.AddWithValue("@vbat", packet.VBat);
command.Parameters.AddWithValue("@vb_min", packet.VBmin);
command.Parameters.AddWithValue("@vb_max", packet.VBmax);
command.Parameters.AddWithValue("@current", packet.Curr);
command.Parameters.AddWithValue("@power", packet.Power);
command.Parameters.AddWithValue("@energy", packet.Energy);
command.Parameters.AddWithValue("@temp_alim", packet.Temp_alim);
command.Parameters.AddWithValue("@temp_cha", packet.Temp_cha);
command.Parameters.AddWithValue("@temp_bat", packet.Temp_bat);
command.Parameters.AddWithValue("@df_temp_alim", packet.DF_TEMP_ALIM);
command.Parameters.AddWithValue("@mem_df_temp_alim", packet.MEM_DF_TEMP_ALIM);
command.Parameters.AddWithValue("@df_temp_bat", packet.DF_TEMP_BAT);
command.Parameters.AddWithValue("@mem_df_temp_bat", packet.MEM_DF_TEMP_BAT);
command.Parameters.AddWithValue("@df_temp_cha", packet.DF_TEMP_CHA);
command.Parameters.AddWithValue("@mem_df_temp_cha", packet.MEM_DF_TEMP_CHA);
command.Parameters.AddWithValue("@df_v_incoherent", packet.DF_V_INCOHERENT);
command.Parameters.AddWithValue("@df_bad_cell", packet.DF_BAD_CELL);
command.Parameters.AddWithValue("@df_overcurrent", packet.DF_OVERCURRENT);
command.Parameters.AddWithValue("@df_overcurrent_stop", packet.DF_OVERCURRENT_STOP);
command.Parameters.AddWithValue("@df_stop_general", packet.DF_STOP_GENERAL);
command.Parameters.AddWithValue("@df_general", packet.DF_GENERAL);
command.Parameters.AddWithValue("@df_cell_overvoltage", packet.DF_CELL_OVERVOLTAGE);
command.Parameters.AddWithValue("@mem_df_cell_overvoltage", packet.MEM_DF_CELL_OVERVOLTAGE);
command.Parameters.AddWithValue("@df_unbalance", packet.DF_UNBALANCE);
command.Parameters.AddWithValue("@mem_df_unbalance", packet.MEM_DF_UNBALANCE);
command.Parameters.AddWithValue("@buzzer_stop", packet.Buzzer_stop);
command.Parameters.AddWithValue("@decharge", packet.DECHARGE);
command.Parameters.AddWithValue("@mem_decharge", packet.MEM_DECHARGE);
command.Parameters.AddWithValue("@compteur_demande_coupure_batterie", packet.Compteur_demande_coupure_batterie);
command.Parameters.AddWithValue("@compteur_demande_coupure_totale", packet.Compteur_demande_coupure_totale);
command.Parameters.AddWithValue("@s_powrelay", packet.S_PowRelay);
command.Parameters.AddWithValue("@s_charelay", packet.S_ChaRelay);
command.Parameters.AddWithValue("@s_batrelay1", packet.S_BatRelay1);
command.Parameters.AddWithValue("@s_batrelay_state", packet.S_BatRelay_State);
command.Parameters.AddWithValue("@flag_decharge", packet.Flag_decharge);
await command.ExecuteNonQueryAsync();
command.Parameters.Clear();
}
}
catch
{
await transaction.RollbackAsync();
throw;
}
await transaction.CommitAsync();
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

12
DataApi/appsettings.json Normal file
View File

@@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"MariaDb": "Server=mercedes.hbsha.re;Port=3306;User=moniteurbaie;Password=M0ñîT€urB@1E;Database=moniteurbaie;Pooling=True;ApplicationName=MoniteurBaie"
}
}

View File

@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace MoniteurBaie.DataModels;
public class BatteryControllerPacket
{
public BatteryControllerPacket(string serialData) : this(DateTime.Now, serialData) { }
[JsonConstructor]
public BatteryControllerPacket(DateTime timestamp, string serialData)
{
Timestamp = timestamp;
SerialData = serialData;
}
public DateTime Timestamp { get; private set; }
public string SerialData { get; private set; }
}
[JsonSerializable(typeof(BatteryControllerPacket))]
public partial class BatteryControllerPacketContext : JsonSerializerContext
{
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>$(SolutionName.Replace(" ", "_")).$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Utils\Utils.csproj" />
</ItemGroup>
</Project>

56
DataModels/DataPacket.cs Normal file
View File

@@ -0,0 +1,56 @@
using System.Text.Json.Serialization;
namespace MoniteurBaie.DataModels;
public record DataPacket(
uint Id,
DateTime Timestamp,
float VB1,
float VB2,
float VB3,
float VB4,
float VB5,
float VB6,
float VBat,
float VBmin,
float VBmax,
float Curr,
float Power,
float Energy,
float Temp_alim,
float Temp_cha,
float Temp_bat,
bool DF_TEMP_ALIM,
bool MEM_DF_TEMP_ALIM,
bool DF_TEMP_BAT,
bool MEM_DF_TEMP_BAT,
bool DF_TEMP_CHA,
bool MEM_DF_TEMP_CHA,
bool DF_V_INCOHERENT,
bool DF_BAD_CELL,
bool DF_OVERCURRENT,
bool DF_OVERCURRENT_STOP,
bool DF_STOP_GENERAL,
bool DF_GENERAL,
bool DF_CELL_OVERVOLTAGE,
bool MEM_DF_CELL_OVERVOLTAGE,
bool DF_UNBALANCE,
bool MEM_DF_UNBALANCE,
bool Buzzer_stop,
bool DECHARGE,
bool MEM_DECHARGE,
uint Compteur_demande_coupure_batterie,
uint Compteur_demande_coupure_totale,
bool S_PowRelay,
bool S_ChaRelay,
bool S_BatRelay1,
bool S_BatRelay_State,
bool Flag_decharge)
{
}
[JsonSerializable(typeof(DataPacket))]
public partial class DataPacketContext : JsonSerializerContext
{
}

6
DataModels/Extensions.cs Normal file
View File

@@ -0,0 +1,6 @@
namespace MoniteurBaie.DataModels;
public static class Extensions
{
public static DataPacket ToDataPacket(this SerialDataPacket packet, DateTime timestamp) => new(default, timestamp, packet.VB1, packet.VB2, packet.VB3, packet.VB4, packet.VB5, packet.VB6, packet.VBat, packet.VBmin, packet.VBmax, packet.Curr, packet.Power, packet.Energy, packet.Temp_alim, packet.Temp_cha, packet.Temp_bat, packet.DF_TEMP_ALIM, packet.MEM_DF_TEMP_ALIM, packet.DF_TEMP_BAT, packet.MEM_DF_TEMP_BAT, packet.DF_TEMP_CHA, packet.MEM_DF_TEMP_CHA, packet.DF_V_INCOHERENT, packet.DF_BAD_CELL, packet.DF_OVERCURRENT, packet.DF_OVERCURRENT_STOP, packet.DF_STOP_GENERAL, packet.DF_GENERAL, packet.DF_CELL_OVERVOLTAGE, packet.MEM_DF_CELL_OVERVOLTAGE, packet.DF_UNBALANCE, packet.MEM_DF_UNBALANCE, packet.Buzzer_stop, packet.DECHARGE, packet.MEM_DECHARGE, packet.Compteur_demande_coupure_batterie, packet.Compteur_demande_coupure_totale, packet.S_PowRelay, packet.S_ChaRelay, packet.S_BatRelay1, packet.S_BatRelay_State, packet.Flag_decharge);
}

View File

@@ -0,0 +1,63 @@
using MoniteurBaie.Utils;
namespace MoniteurBaie.DataModels;
public static class PacketParser
{
public static SerialDataPacket ParseSerialDataPacket(string s)
{
// "COM,{VB1},{VB2},{VB3},{VB4},{VB5},{VB6},{VBat},{VBmin},{VBmax},{Curr},{Power},{Energy},{Temp_alim},{Temp_cha},{Temp_bat},{DF_TEMP_ALIM},{MEM_DF_TEMP_ALIM},{DF_TEMP_BAT},{MEM_DF_TEMP_BAT},{DF_TEMP_CHA},{MEM_DF_TEMP_CHA},{DF_V_INCOHERENT},{DF_BAD_CELL},{DF_OVERCURRENT},{DF_OVERCURRENT_STOP},{DF_STOP_GENERAL},{DF_GENERAL},{DF_CELL_OVERVOLTAGE},{MEM_DF_CELL_OVERVOLTAGE},{DF_UNBALANCE},{MEM_DF_UNBALANCE},{Buzzer_stop},{DECHARGE},{MEM_DECHARGE},{Compteur_demande_coupure_batterie},{Compteur_demande_coupure_totale},{!digitalRead(S_PowRelay)},{digitalRead(S_ChaRelay)},{digitalRead(S_BatRelay1)},{S_BatRelay_State},{flag_decharge}";
var splitter = new Splitter(s);
var header = splitter.ReadString();
if (header is not "COM")
{
throw new Exception("Invalid packet header.");
}
var VB1 = splitter.ReadFloat();
var VB2 = splitter.ReadFloat();
var VB3 = splitter.ReadFloat();
var VB4 = splitter.ReadFloat();
var VB5 = splitter.ReadFloat();
var VB6 = splitter.ReadFloat();
var VBat = splitter.ReadFloat();
var VBmin = splitter.ReadFloat();
var VBmax = splitter.ReadFloat();
var Curr = splitter.ReadFloat();
var Power = splitter.ReadFloat();
var Energy = splitter.ReadFloat();
var Temp_alim = splitter.ReadFloat();
var Temp_cha = splitter.ReadFloat();
var Temp_bat = splitter.ReadFloat();
var DF_TEMP_ALIM = splitter.ReadBool();
var MEM_DF_TEMP_ALIM = splitter.ReadBool();
var DF_TEMP_BAT = splitter.ReadBool();
var MEM_DF_TEMP_BAT = splitter.ReadBool();
var DF_TEMP_CHA = splitter.ReadBool();
var MEM_DF_TEMP_CHA = splitter.ReadBool();
var DF_V_INCOHERENT = splitter.ReadBool();
var DF_BAD_CELL = splitter.ReadBool();
var DF_OVERCURRENT = splitter.ReadBool();
var DF_OVERCURRENT_STOP = splitter.ReadBool();
var DF_STOP_GENERAL = splitter.ReadBool();
var DF_GENERAL = splitter.ReadBool();
var DF_CELL_OVERVOLTAGE = splitter.ReadBool();
var MEM_DF_CELL_OVERVOLTAGE = splitter.ReadBool();
var DF_UNBALANCE = splitter.ReadBool();
var MEM_DF_UNBALANCE = splitter.ReadBool();
var Buzzer_stop = splitter.ReadBool();
var DECHARGE = splitter.ReadBool();
var MEM_DECHARGE = splitter.ReadBool();
var Compteur_demande_coupure_batterie = splitter.ReadUInt();
var Compteur_demande_coupure_totale = splitter.ReadUInt();
var S_PowRelay = splitter.ReadBool();
var S_ChaRelay = splitter.ReadBool();
var S_BatRelay1 = splitter.ReadBool();
var S_BatRelay_State = splitter.ReadBool();
var Flag_decharge = splitter.ReadBool();
return new(VB1, VB2, VB3, VB4, VB5, VB6, VBat, VBmin, VBmax, Curr, Power, Energy, Temp_alim, Temp_cha, Temp_bat, DF_TEMP_ALIM, MEM_DF_TEMP_ALIM, DF_TEMP_BAT, MEM_DF_TEMP_BAT, DF_TEMP_CHA, MEM_DF_TEMP_CHA, DF_V_INCOHERENT, DF_BAD_CELL, DF_OVERCURRENT, DF_OVERCURRENT_STOP, DF_STOP_GENERAL, DF_GENERAL, DF_CELL_OVERVOLTAGE, MEM_DF_CELL_OVERVOLTAGE, DF_UNBALANCE, MEM_DF_UNBALANCE, Buzzer_stop, DECHARGE, MEM_DECHARGE, Compteur_demande_coupure_batterie, Compteur_demande_coupure_totale, S_PowRelay, S_ChaRelay, S_BatRelay1, S_BatRelay_State, Flag_decharge);
}
}

View File

@@ -0,0 +1,47 @@
namespace MoniteurBaie.DataModels;
public record SerialDataPacket(
float VB1,
float VB2,
float VB3,
float VB4,
float VB5,
float VB6,
float VBat,
float VBmin,
float VBmax,
float Curr,
float Power,
float Energy,
float Temp_alim,
float Temp_cha,
float Temp_bat,
bool DF_TEMP_ALIM,
bool MEM_DF_TEMP_ALIM,
bool DF_TEMP_BAT,
bool MEM_DF_TEMP_BAT,
bool DF_TEMP_CHA,
bool MEM_DF_TEMP_CHA,
bool DF_V_INCOHERENT,
bool DF_BAD_CELL,
bool DF_OVERCURRENT,
bool DF_OVERCURRENT_STOP,
bool DF_STOP_GENERAL,
bool DF_GENERAL,
bool DF_CELL_OVERVOLTAGE,
bool MEM_DF_CELL_OVERVOLTAGE,
bool DF_UNBALANCE,
bool MEM_DF_UNBALANCE,
bool Buzzer_stop,
bool DECHARGE,
bool MEM_DECHARGE,
uint Compteur_demande_coupure_batterie,
uint Compteur_demande_coupure_totale,
bool S_PowRelay,
bool S_ChaRelay,
bool S_BatRelay1,
bool S_BatRelay_State,
bool Flag_decharge)
{
}

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>$(SolutionName.Replace(" ", "_")).$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.8.1" />
<PackageReference Include="StackExchange.Redis" Version="2.6.70" />
</ItemGroup>
</Project>

9
DiscordApp/Program.cs Normal file
View File

@@ -0,0 +1,9 @@
namespace MoniteurBaie.DiscordApp;
public static class Program
{
public static void Main(string[] args)
{
}
}

61
MoniteurBaie.sln Normal file
View File

@@ -0,0 +1,61 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32630.192
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Utils", "Utils\Utils.csproj", "{A18682B4-1D2B-4C0E-BCC7-4E499D853059}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleLog", "ConsoleLog\ConsoleLog.csproj", "{F4A92DE8-E5BF-4048-B3E2-474F0D528195}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordApp", "DiscordApp\DiscordApp.csproj", "{3469C0A6-E375-44C8-BCBA-4E07B9677C49}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataApi", "DataApi\DataApi.csproj", "{12A93E6E-00CB-4447-B431-64EBFE8EE566}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataModels", "DataModels\DataModels.csproj", "{8EFF8A80-F571-4EF4-82C3-BFC59DD3ABBD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SerialCom", "SerialCom\SerialCom.csproj", "{361CFEF0-763E-44B8-A580-0844032C0CFE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testation", "Testation\Testation.csproj", "{98E084C2-CC2E-4B61-8BEC-0BFE864C40C1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A18682B4-1D2B-4C0E-BCC7-4E499D853059}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A18682B4-1D2B-4C0E-BCC7-4E499D853059}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A18682B4-1D2B-4C0E-BCC7-4E499D853059}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A18682B4-1D2B-4C0E-BCC7-4E499D853059}.Release|Any CPU.Build.0 = Release|Any CPU
{F4A92DE8-E5BF-4048-B3E2-474F0D528195}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F4A92DE8-E5BF-4048-B3E2-474F0D528195}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4A92DE8-E5BF-4048-B3E2-474F0D528195}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4A92DE8-E5BF-4048-B3E2-474F0D528195}.Release|Any CPU.Build.0 = Release|Any CPU
{3469C0A6-E375-44C8-BCBA-4E07B9677C49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3469C0A6-E375-44C8-BCBA-4E07B9677C49}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3469C0A6-E375-44C8-BCBA-4E07B9677C49}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3469C0A6-E375-44C8-BCBA-4E07B9677C49}.Release|Any CPU.Build.0 = Release|Any CPU
{12A93E6E-00CB-4447-B431-64EBFE8EE566}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{12A93E6E-00CB-4447-B431-64EBFE8EE566}.Debug|Any CPU.Build.0 = Debug|Any CPU
{12A93E6E-00CB-4447-B431-64EBFE8EE566}.Release|Any CPU.ActiveCfg = Release|Any CPU
{12A93E6E-00CB-4447-B431-64EBFE8EE566}.Release|Any CPU.Build.0 = Release|Any CPU
{8EFF8A80-F571-4EF4-82C3-BFC59DD3ABBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8EFF8A80-F571-4EF4-82C3-BFC59DD3ABBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8EFF8A80-F571-4EF4-82C3-BFC59DD3ABBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8EFF8A80-F571-4EF4-82C3-BFC59DD3ABBD}.Release|Any CPU.Build.0 = Release|Any CPU
{361CFEF0-763E-44B8-A580-0844032C0CFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{361CFEF0-763E-44B8-A580-0844032C0CFE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{361CFEF0-763E-44B8-A580-0844032C0CFE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{361CFEF0-763E-44B8-A580-0844032C0CFE}.Release|Any CPU.Build.0 = Release|Any CPU
{98E084C2-CC2E-4B61-8BEC-0BFE864C40C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{98E084C2-CC2E-4B61-8BEC-0BFE864C40C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{98E084C2-CC2E-4B61-8BEC-0BFE864C40C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{98E084C2-CC2E-4B61-8BEC-0BFE864C40C1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {81BADD87-1FE2-4F6D-B5A2-5F294EBCE466}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\publish\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net6.0</TargetFramework>
<SelfContained>false</SelfContained>
<RuntimeIdentifier>linux-arm64</RuntimeIdentifier>
<PublishSingleFile>true</PublishSingleFile>
<PublishReadyToRun>false</PublishReadyToRun>
</PropertyGroup>
</Project>

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

67
Testation/Program.cs Normal file
View File

@@ -0,0 +1,67 @@
using System.Globalization;
using System.Text.Json;
using MoniteurBaie.DataModels;
using StackExchange.Redis;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
using var redis = ConnectionMultiplexer.Connect("mercedes.hbsha.re:6379", opts =>
{
opts.ClientName = "Testation";
});
var packetChannel = redis.GetSubscriber().Subscribe("batCtrlPackets");
packetChannel.OnMessage(OnPacketMessage);
using var cancel = new AutoResetEvent(false);
void ctrlC(object? sender, ConsoleCancelEventArgs e)
{
e.Cancel = true;
Console.CancelKeyPress -= ctrlC;
cancel.Set();
}
Console.CancelKeyPress += ctrlC;
cancel.WaitOne();
static void OnPacketMessage(ChannelMessage channelMessage)
{
BatteryControllerPacket? batCtrlPacket;
try
{
batCtrlPacket = JsonSerializer.Deserialize(channelMessage.Message.ToString(), BatteryControllerPacketContext.Default.BatteryControllerPacket);
}
catch (Exception ex)
{
Console.WriteLine("Invalid packet.");
Console.WriteLine(ex);
return;
}
if (batCtrlPacket is null)
{
Console.WriteLine("Null packet.");
return;
}
SerialDataPacket packet;
try
{
packet = PacketParser.ParseSerialDataPacket(batCtrlPacket.SerialData);
}
catch (Exception ex)
{
Console.WriteLine("Invalid data packet.");
Console.WriteLine(ex);
throw;
}
if (packet.Temp_alim < 0 || packet.Temp_bat < 0 || packet.Temp_cha < 0)
{
Console.WriteLine($"[{batCtrlPacket.Timestamp:yyyy/MM/dd HH:mm:ss}] {batCtrlPacket.SerialData}");
}
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>publish\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifier>linux-arm64</RuntimeIdentifier>
<SelfContained>false</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Remove="publish\**" />
<EmbeddedResource Remove="publish\**" />
<None Remove="publish\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="StackExchange.Redis" Version="2.6.70" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DataModels\DataModels.csproj" />
<ProjectReference Include="..\Utils\Utils.csproj" />
</ItemGroup>
</Project>

58
Utils/BlockingListener.cs Normal file
View File

@@ -0,0 +1,58 @@
namespace MoniteurBaie.Utils;
public sealed class BlockingListener<T> : IObserver<T>, IDisposable
{
private readonly AutoResetEvent _waitHandle = new(false);
private T? _data;
public T Next()
{
_waitHandle.WaitOne();
var data = _data!;
_data = default;
return data;
}
public void Push(T value)
{
_data = value;
_waitHandle.Set();
}
void IObserver<T>.OnCompleted()
{
}
void IObserver<T>.OnError(Exception error)
{
}
void IObserver<T>.OnNext(T value) => Push(value);
#region IDisposable
private bool _disposedValue;
private void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_waitHandle.Dispose();
}
_disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
#endregion
}

19
Utils/Disposer.cs Normal file
View File

@@ -0,0 +1,19 @@
namespace MoniteurBaie.Utils;
public sealed class Disposer : IDisposable
{
private Action? _callback;
public Disposer(Action callback) => _callback = callback;
public void Dispose()
{
if (_callback is not null)
{
_callback();
_callback = default;
}
GC.SuppressFinalize(this);
}
}

10
Utils/Extensions.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace MoniteurBaie.Utils;
public static class Extensions
{
public static char ToChar(this bool b) => b ? 'O' : 'N';
public static string ToEmoji(this bool b) => b ? "✔️" : "❌";
public static string ToAnsi(this bool b) => $"{(b ? "\u001b[32m" : "\u001b[31m")}{ToChar(b)}\u001b[0m";
}

31
Utils/Listener.cs Normal file
View File

@@ -0,0 +1,31 @@
namespace MoniteurBaie.Utils;
public static class Listener
{
public static Listener<T> Create<T>(Action<T> callback) => new(callback);
}
public class Listener<T> : IObserver<T>
{
private readonly Action<T> _callback;
public Listener(Action<T> callback)
{
_callback = callback;
}
public void OnCompleted()
{
}
public void OnError(Exception error)
{
}
public void OnNext(T value)
{
_callback(value);
}
}

38
Utils/Splitter.cs Normal file
View File

@@ -0,0 +1,38 @@
using System.Globalization;
namespace MoniteurBaie.Utils;
public class Splitter
{
const char DELIMITER = ',';
public delegate T ValueReader<T>(ReadOnlySpan<char> s);
private readonly string _str;
private int _pos = 0, _charCount = 0, _nextPos = 0;
public Splitter(string str) => _str = str;
public bool MoveNext()
{
_pos = _nextPos;
var i = _str.IndexOf(DELIMITER, _pos);
(_charCount, _nextPos) = i < 0 ? (_str.Length - _pos, _str.Length) : (i - _pos, i + 1);
return HasNext;
}
public bool HasNext => _pos < _str.Length;
public T Read<T>(ValueReader<T> reader) => MoveNext() ? reader(_str.AsSpan(_pos, _charCount)) : throw new InvalidOperationException();
public string? ReadString() => Read(static s => new string(s));
public bool ReadBool() => Read(static s => s.Length == 1 ? s[0] switch { '0' => false, '1' => true, _ => throw new InvalidDataException() } : throw new InvalidDataException());
public int ReadInt() => Read(static s => int.Parse(s, provider: CultureInfo.InvariantCulture));
public uint ReadUInt() => Read(static s => uint.Parse(s, provider: CultureInfo.InvariantCulture));
public float ReadFloat() => Read(static s => float.Parse(s, provider: CultureInfo.InvariantCulture));
}

10
Utils/Utils.csproj Normal file
View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>$(SolutionName.Replace(" ", "_")).$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
</PropertyGroup>
</Project>