From 957670ce424733e595e1bd8381662b8ca62a8cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20BECHER?= Date: Wed, 21 Jun 2023 13:51:38 +0200 Subject: [PATCH] Initial commit --- .gitignore | 7 + ConsoleLog/ConsoleLog.csproj | 29 +++ ConsoleLog/Program.cs | 167 ++++++++++++ .../PublishProfiles/RaspiPublish.pubxml | 16 ++ ConsoleLog/appsettings.json | 12 + .../Repositories/BaseMariaDbRepository.cs | 19 ++ DataApi/Controllers/DataPacketsController.cs | 59 +++++ DataApi/DataApi.csproj | 28 ++ .../Repositories/IDataRepository.cs | 18 ++ DataApi/Program.cs | 30 +++ .../PublishProfiles/FolderPublish.pubxml | 22 ++ DataApi/Properties/launchSettings.json | 31 +++ DataApi/Repositories/DataRepository.cs | 246 ++++++++++++++++++ DataApi/appsettings.Development.json | 8 + DataApi/appsettings.json | 12 + DataModels/BatteryControllerPacket.cs | 24 ++ DataModels/DataModels.csproj | 14 + DataModels/DataPacket.cs | 56 ++++ DataModels/Extensions.cs | 6 + DataModels/PacketParser.cs | 63 +++++ DataModels/SerialDataPacket.cs | 47 ++++ DiscordApp/DiscordApp.csproj | 16 ++ DiscordApp/Program.cs | 9 + MoniteurBaie.sln | 61 +++++ Properties/PublishProfiles/RaspiDeploy.pubxml | 17 ++ SerialCom/BatteryController.cs | 221 ++++++++++++++++ SerialCom/DataApi.cs | 71 +++++ SerialCom/IBatteryController.cs | 12 + SerialCom/MockBatteryController.cs | 129 +++++++++ SerialCom/Program.cs | 11 + .../PublishProfiles/RaspiPublish.pubxml | 16 ++ SerialCom/Properties/launchSettings.json | 11 + SerialCom/SerialCom.csproj | 25 ++ SerialCom/Worker.cs | 63 +++++ SerialCom/appsettings.Development.json | 11 + SerialCom/appsettings.json | 32 +++ Testation/Program.cs | 67 +++++ .../PublishProfiles/RaspiPublish.pubxml | 17 ++ Testation/Testation.csproj | 25 ++ Utils/BlockingListener.cs | 58 +++++ Utils/Disposer.cs | 19 ++ Utils/Extensions.cs | 10 + Utils/Listener.cs | 31 +++ Utils/Splitter.cs | 38 +++ Utils/Utils.csproj | 10 + 45 files changed, 1894 insertions(+) create mode 100644 .gitignore create mode 100644 ConsoleLog/ConsoleLog.csproj create mode 100644 ConsoleLog/Program.cs create mode 100644 ConsoleLog/Properties/PublishProfiles/RaspiPublish.pubxml create mode 100644 ConsoleLog/appsettings.json create mode 100644 DataApi/Common/Repositories/BaseMariaDbRepository.cs create mode 100644 DataApi/Controllers/DataPacketsController.cs create mode 100644 DataApi/DataApi.csproj create mode 100644 DataApi/Interfaces/Repositories/IDataRepository.cs create mode 100644 DataApi/Program.cs create mode 100644 DataApi/Properties/PublishProfiles/FolderPublish.pubxml create mode 100644 DataApi/Properties/launchSettings.json create mode 100644 DataApi/Repositories/DataRepository.cs create mode 100644 DataApi/appsettings.Development.json create mode 100644 DataApi/appsettings.json create mode 100644 DataModels/BatteryControllerPacket.cs create mode 100644 DataModels/DataModels.csproj create mode 100644 DataModels/DataPacket.cs create mode 100644 DataModels/Extensions.cs create mode 100644 DataModels/PacketParser.cs create mode 100644 DataModels/SerialDataPacket.cs create mode 100644 DiscordApp/DiscordApp.csproj create mode 100644 DiscordApp/Program.cs create mode 100644 MoniteurBaie.sln create mode 100644 Properties/PublishProfiles/RaspiDeploy.pubxml create mode 100644 SerialCom/BatteryController.cs create mode 100644 SerialCom/DataApi.cs create mode 100644 SerialCom/IBatteryController.cs create mode 100644 SerialCom/MockBatteryController.cs create mode 100644 SerialCom/Program.cs create mode 100644 SerialCom/Properties/PublishProfiles/RaspiPublish.pubxml create mode 100644 SerialCom/Properties/launchSettings.json create mode 100644 SerialCom/SerialCom.csproj create mode 100644 SerialCom/Worker.cs create mode 100644 SerialCom/appsettings.Development.json create mode 100644 SerialCom/appsettings.json create mode 100644 Testation/Program.cs create mode 100644 Testation/Properties/PublishProfiles/RaspiPublish.pubxml create mode 100644 Testation/Testation.csproj create mode 100644 Utils/BlockingListener.cs create mode 100644 Utils/Disposer.cs create mode 100644 Utils/Extensions.cs create mode 100644 Utils/Listener.cs create mode 100644 Utils/Splitter.cs create mode 100644 Utils/Utils.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e7c9f85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.vs +/.vscode + +bin/ +obj/ +publish/ +*.user \ No newline at end of file diff --git a/ConsoleLog/ConsoleLog.csproj b/ConsoleLog/ConsoleLog.csproj new file mode 100644 index 0000000..b22827e --- /dev/null +++ b/ConsoleLog/ConsoleLog.csproj @@ -0,0 +1,29 @@ + + + Exe + net7.0 + enable + enable + $(SolutionName.Replace(" ", "_")).$(MSBuildProjectName.Replace(" ", "_")) + + + + + + + + + Always + + + + + + + + + + + + + \ No newline at end of file diff --git a/ConsoleLog/Program.cs b/ConsoleLog/Program.cs new file mode 100644 index 0000000..ed8a21c --- /dev/null +++ b/ConsoleLog/Program.cs @@ -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("Channels:Commands")!; + +using var redis = ConnectionMultiplexer.Connect(redisConfig.GetValue("Endpoint")!, opts => +{ + opts.ClientName = redisConfig.GetValue("ClientName"); +}); +var packetChannel = redis.GetSubscriber().Subscribe(redisConfig.GetValue("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); + } +} \ No newline at end of file diff --git a/ConsoleLog/Properties/PublishProfiles/RaspiPublish.pubxml b/ConsoleLog/Properties/PublishProfiles/RaspiPublish.pubxml new file mode 100644 index 0000000..a2a1cb7 --- /dev/null +++ b/ConsoleLog/Properties/PublishProfiles/RaspiPublish.pubxml @@ -0,0 +1,16 @@ + + + + + Release + Any CPU + publish\ + FileSystem + net7.0 + linux-arm64 + false + true + + \ No newline at end of file diff --git a/ConsoleLog/appsettings.json b/ConsoleLog/appsettings.json new file mode 100644 index 0000000..239aa30 --- /dev/null +++ b/ConsoleLog/appsettings.json @@ -0,0 +1,12 @@ +{ + "MoniteurBaie": { + "Redis": { + "Endpoint": "mercedes.hbsha.re:6379", + "ClientName": "Console", + "Channels": { + "Packets": "batCtrlPackets", + "Commands": "batCtrlCommands" + } + } + } +} \ No newline at end of file diff --git a/DataApi/Common/Repositories/BaseMariaDbRepository.cs b/DataApi/Common/Repositories/BaseMariaDbRepository.cs new file mode 100644 index 0000000..af5db29 --- /dev/null +++ b/DataApi/Common/Repositories/BaseMariaDbRepository.cs @@ -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 NewConnectionAsync() + { + var connection = new MySqlConnection(_connectionString); + await connection.OpenAsync(); + return connection; + } +} diff --git a/DataApi/Controllers/DataPacketsController.cs b/DataApi/Controllers/DataPacketsController.cs new file mode 100644 index 0000000..d9fc74d --- /dev/null +++ b/DataApi/Controllers/DataPacketsController.cs @@ -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 _logger; + private readonly IDataRepository _dataRepository; + + public DataPacketsController(ILogger logger, IDataRepository dataRepository) + { + _logger = logger; + _dataRepository = dataRepository; + } + + [HttpGet("{id}", Name = "GetDataPacket")] + public async Task GetAsync(uint id) + { + var packet = await _dataRepository.GetAsync(id); + return packet is null ? NotFound() : Ok(packet); + } + + [HttpPost(Name = "CreateDataPacket")] + public async Task 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 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 BulkCreateAsync(IEnumerable packets) + { + await _dataRepository.BulkCreateAsync(packets); + return Ok(); + } +} diff --git a/DataApi/DataApi.csproj b/DataApi/DataApi.csproj new file mode 100644 index 0000000..d80d49a --- /dev/null +++ b/DataApi/DataApi.csproj @@ -0,0 +1,28 @@ + + + + net7.0 + enable + enable + $(SolutionName.Replace(" ", "_")).$(MSBuildProjectName.Replace(" ", "_")) + + + + + + + + + + + + + + + + + + + + + diff --git a/DataApi/Interfaces/Repositories/IDataRepository.cs b/DataApi/Interfaces/Repositories/IDataRepository.cs new file mode 100644 index 0000000..0da2901 --- /dev/null +++ b/DataApi/Interfaces/Repositories/IDataRepository.cs @@ -0,0 +1,18 @@ +using MoniteurBaie.DataModels; + +namespace MoniteurBaie.DataApi.Interfaces.Repositories; + +public interface IDataRepository +{ + Task GetAsync(uint id); + + Task CreateAsync(DataPacket packet); + + IAsyncEnumerable GetRangeAsync(DateTime fromInstant, DateTime toInstant); + + IAsyncEnumerable GetLastAsync(int count); + + Task DeleteAsync(uint id); + + Task BulkCreateAsync(IEnumerable packets); +} diff --git a/DataApi/Program.cs b/DataApi/Program.cs new file mode 100644 index 0000000..b804d4f --- /dev/null +++ b/DataApi/Program.cs @@ -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(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/DataApi/Properties/PublishProfiles/FolderPublish.pubxml b/DataApi/Properties/PublishProfiles/FolderPublish.pubxml new file mode 100644 index 0000000..c96ae90 --- /dev/null +++ b/DataApi/Properties/PublishProfiles/FolderPublish.pubxml @@ -0,0 +1,22 @@ + + + + + true + false + true + Release + Any CPU + FileSystem + publish\ + FileSystem + + net6.0 + linux-x64 + true + 12a93e6e-00cb-4447-b431-64ebfe8ee566 + false + + \ No newline at end of file diff --git a/DataApi/Properties/launchSettings.json b/DataApi/Properties/launchSettings.json new file mode 100644 index 0000000..c406805 --- /dev/null +++ b/DataApi/Properties/launchSettings.json @@ -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" + } + } + } +} diff --git a/DataApi/Repositories/DataRepository.cs b/DataApi/Repositories/DataRepository.cs new file mode 100644 index 0000000..fc7d4f5 --- /dev/null +++ b/DataApi/Repositories/DataRepository.cs @@ -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 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 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 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 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 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 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(); + } +} diff --git a/DataApi/appsettings.Development.json b/DataApi/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/DataApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/DataApi/appsettings.json b/DataApi/appsettings.json new file mode 100644 index 0000000..645cf1b --- /dev/null +++ b/DataApi/appsettings.json @@ -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" + } +} diff --git a/DataModels/BatteryControllerPacket.cs b/DataModels/BatteryControllerPacket.cs new file mode 100644 index 0000000..2c15da3 --- /dev/null +++ b/DataModels/BatteryControllerPacket.cs @@ -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 +{ +} \ No newline at end of file diff --git a/DataModels/DataModels.csproj b/DataModels/DataModels.csproj new file mode 100644 index 0000000..deea6bb --- /dev/null +++ b/DataModels/DataModels.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + enable + enable + $(SolutionName.Replace(" ", "_")).$(MSBuildProjectName.Replace(" ", "_")) + + + + + + + diff --git a/DataModels/DataPacket.cs b/DataModels/DataPacket.cs new file mode 100644 index 0000000..cf62f1b --- /dev/null +++ b/DataModels/DataPacket.cs @@ -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 +{ +} \ No newline at end of file diff --git a/DataModels/Extensions.cs b/DataModels/Extensions.cs new file mode 100644 index 0000000..aa8e8dd --- /dev/null +++ b/DataModels/Extensions.cs @@ -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); +} diff --git a/DataModels/PacketParser.cs b/DataModels/PacketParser.cs new file mode 100644 index 0000000..76a4c9c --- /dev/null +++ b/DataModels/PacketParser.cs @@ -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); + } +} diff --git a/DataModels/SerialDataPacket.cs b/DataModels/SerialDataPacket.cs new file mode 100644 index 0000000..58b97d8 --- /dev/null +++ b/DataModels/SerialDataPacket.cs @@ -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) +{ + +} diff --git a/DiscordApp/DiscordApp.csproj b/DiscordApp/DiscordApp.csproj new file mode 100644 index 0000000..72f9d76 --- /dev/null +++ b/DiscordApp/DiscordApp.csproj @@ -0,0 +1,16 @@ + + + + Exe + net7.0 + enable + enable + $(SolutionName.Replace(" ", "_")).$(MSBuildProjectName.Replace(" ", "_")) + + + + + + + + diff --git a/DiscordApp/Program.cs b/DiscordApp/Program.cs new file mode 100644 index 0000000..26f40bd --- /dev/null +++ b/DiscordApp/Program.cs @@ -0,0 +1,9 @@ +namespace MoniteurBaie.DiscordApp; + +public static class Program +{ + public static void Main(string[] args) + { + + } +} diff --git a/MoniteurBaie.sln b/MoniteurBaie.sln new file mode 100644 index 0000000..23b069b --- /dev/null +++ b/MoniteurBaie.sln @@ -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 diff --git a/Properties/PublishProfiles/RaspiDeploy.pubxml b/Properties/PublishProfiles/RaspiDeploy.pubxml new file mode 100644 index 0000000..31fb124 --- /dev/null +++ b/Properties/PublishProfiles/RaspiDeploy.pubxml @@ -0,0 +1,17 @@ + + + + + Release + Any CPU + bin\publish\ + FileSystem + net6.0 + false + linux-arm64 + true + false + + \ No newline at end of file diff --git a/SerialCom/BatteryController.cs b/SerialCom/BatteryController.cs new file mode 100644 index 0000000..e3a934a --- /dev/null +++ b/SerialCom/BatteryController.cs @@ -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> _packetObservers = new(); + private readonly BlockingListener _commandListener = new(); + + public BatteryController(ILogger logger, Action 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 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.Subscribe(IObserver observer) + { + return AddSerialObserver(observer); + } + + #endregion + + #region IObserver + + void IObserver.OnCompleted() + { + + } + + void IObserver.OnError(Exception error) + { + + } + + void IObserver.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 +} diff --git a/SerialCom/DataApi.cs b/SerialCom/DataApi.cs new file mode 100644 index 0000000..aa6a575 --- /dev/null +++ b/SerialCom/DataApi.cs @@ -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("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 +} diff --git a/SerialCom/IBatteryController.cs b/SerialCom/IBatteryController.cs new file mode 100644 index 0000000..c98ad81 --- /dev/null +++ b/SerialCom/IBatteryController.cs @@ -0,0 +1,12 @@ +using MoniteurBaie.DataModels; + +namespace MoniteurBaie.SerialCom; + +public interface IBatteryController : IObservable, IObserver, IDisposable +{ + Task Open(CancellationToken cancellationToken); + + IDisposable AddSerialObserver(IObserver observer); + + Task SendCommand(string command, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/SerialCom/MockBatteryController.cs b/SerialCom/MockBatteryController.cs new file mode 100644 index 0000000..148b2c2 --- /dev/null +++ b/SerialCom/MockBatteryController.cs @@ -0,0 +1,129 @@ +using MoniteurBaie.DataModels; +using MoniteurBaie.Utils; + +namespace MoniteurBaie.SerialCom; + +public sealed class MockBatteryController : IBatteryController +{ + private readonly List> _serialObservers = new(); + private readonly BlockingListener _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 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.Subscribe(IObserver observer) + { + return AddSerialObserver(observer); + } + + #endregion + + #region IObserver + + void IObserver.OnCompleted() + { + + } + + void IObserver.OnError(Exception error) + { + + } + + void IObserver.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 +} diff --git a/SerialCom/Program.cs b/SerialCom/Program.cs new file mode 100644 index 0000000..b3ab97a --- /dev/null +++ b/SerialCom/Program.cs @@ -0,0 +1,11 @@ +using MoniteurBaie.SerialCom; + +var host = Host.CreateDefaultBuilder(args) + .UseSystemd() + .ConfigureServices(services => + { + services.AddHostedService(); + }) + .Build(); + +await host.RunAsync(); diff --git a/SerialCom/Properties/PublishProfiles/RaspiPublish.pubxml b/SerialCom/Properties/PublishProfiles/RaspiPublish.pubxml new file mode 100644 index 0000000..a2a1cb7 --- /dev/null +++ b/SerialCom/Properties/PublishProfiles/RaspiPublish.pubxml @@ -0,0 +1,16 @@ + + + + + Release + Any CPU + publish\ + FileSystem + net7.0 + linux-arm64 + false + true + + \ No newline at end of file diff --git a/SerialCom/Properties/launchSettings.json b/SerialCom/Properties/launchSettings.json new file mode 100644 index 0000000..7b13948 --- /dev/null +++ b/SerialCom/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "SerialCom": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/SerialCom/SerialCom.csproj b/SerialCom/SerialCom.csproj new file mode 100644 index 0000000..b637cb9 --- /dev/null +++ b/SerialCom/SerialCom.csproj @@ -0,0 +1,25 @@ + + + net7.0 + enable + enable + dotnet-SerialCom-7208DE51-06AE-44FC-809D-543080AAAFF0 + $(SolutionName.Replace(" ", "_")).$(MSBuildProjectName.Replace(" ", "_")) + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SerialCom/Worker.cs b/SerialCom/Worker.cs new file mode 100644 index 0000000..ce9ae0c --- /dev/null +++ b/SerialCom/Worker.cs @@ -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 _logger; + private readonly IConfiguration _configuration; + + public Worker(ILogger 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("Mock") + ? new MockBatteryController() + : new BatteryController(_logger, serialPort => + { + serialPort.PortName = serialConfig.GetValue("Path")!; + serialPort.BaudRate = serialConfig.GetValue("BaudRate"); + serialPort.Parity = serialConfig.GetValue("Parity"); + serialPort.DataBits = serialConfig.GetValue("DataBits"); + serialPort.StopBits = serialConfig.GetValue("StopBits"); + serialPort.Handshake = serialConfig.GetValue("Handshake"); + serialPort.Encoding = Encoding.GetEncoding(serialConfig.GetValue("Encoding")!); + serialPort.NewLine = serialConfig.GetValue("NewLine")!; + }); + + await batteryController.Open(stoppingToken); + + using var redis = await ConnectionMultiplexer.ConnectAsync(redisConfig.GetValue("Endpoint")!, opts => + { + opts.ClientName = redisConfig.GetValue("ClientName"); + }); + + using var dataApi = new DataApi(_logger, config.GetSection("Api")); + + var redisChannel = redisConfig.GetValue("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("Channels:Commands")!); + mq.OnMessage(channelMessage => batteryController.SendCommand(channelMessage.Message.ToString(), stoppingToken)); + + await Task.Delay(Timeout.Infinite, stoppingToken); + } +} diff --git a/SerialCom/appsettings.Development.json b/SerialCom/appsettings.Development.json new file mode 100644 index 0000000..d7e0df4 --- /dev/null +++ b/SerialCom/appsettings.Development.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "MoniteurBaie": { + "Mock": false + } +} \ No newline at end of file diff --git a/SerialCom/appsettings.json b/SerialCom/appsettings.json new file mode 100644 index 0000000..0299601 --- /dev/null +++ b/SerialCom/appsettings.json @@ -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 + } +} \ No newline at end of file diff --git a/Testation/Program.cs b/Testation/Program.cs new file mode 100644 index 0000000..be7895c --- /dev/null +++ b/Testation/Program.cs @@ -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}"); + } +} \ No newline at end of file diff --git a/Testation/Properties/PublishProfiles/RaspiPublish.pubxml b/Testation/Properties/PublishProfiles/RaspiPublish.pubxml new file mode 100644 index 0000000..1b8cf68 --- /dev/null +++ b/Testation/Properties/PublishProfiles/RaspiPublish.pubxml @@ -0,0 +1,17 @@ + + + + + Release + Release + Any CPU + publish\ + FileSystem + net6.0 + linux-arm64 + false + true + + \ No newline at end of file diff --git a/Testation/Testation.csproj b/Testation/Testation.csproj new file mode 100644 index 0000000..4fd4fdc --- /dev/null +++ b/Testation/Testation.csproj @@ -0,0 +1,25 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/Utils/BlockingListener.cs b/Utils/BlockingListener.cs new file mode 100644 index 0000000..48b4c64 --- /dev/null +++ b/Utils/BlockingListener.cs @@ -0,0 +1,58 @@ +namespace MoniteurBaie.Utils; + +public sealed class BlockingListener : IObserver, 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.OnCompleted() + { + + } + + void IObserver.OnError(Exception error) + { + + } + + void IObserver.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 +} diff --git a/Utils/Disposer.cs b/Utils/Disposer.cs new file mode 100644 index 0000000..4408735 --- /dev/null +++ b/Utils/Disposer.cs @@ -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); + } +} diff --git a/Utils/Extensions.cs b/Utils/Extensions.cs new file mode 100644 index 0000000..23e16d4 --- /dev/null +++ b/Utils/Extensions.cs @@ -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"; +} diff --git a/Utils/Listener.cs b/Utils/Listener.cs new file mode 100644 index 0000000..0cc16d0 --- /dev/null +++ b/Utils/Listener.cs @@ -0,0 +1,31 @@ +namespace MoniteurBaie.Utils; + +public static class Listener +{ + public static Listener Create(Action callback) => new(callback); +} + +public class Listener : IObserver +{ + private readonly Action _callback; + + public Listener(Action callback) + { + _callback = callback; + } + + public void OnCompleted() + { + + } + + public void OnError(Exception error) + { + + } + + public void OnNext(T value) + { + _callback(value); + } +} diff --git a/Utils/Splitter.cs b/Utils/Splitter.cs new file mode 100644 index 0000000..36d6c3b --- /dev/null +++ b/Utils/Splitter.cs @@ -0,0 +1,38 @@ +using System.Globalization; + +namespace MoniteurBaie.Utils; + +public class Splitter +{ + const char DELIMITER = ','; + + public delegate T ValueReader(ReadOnlySpan 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(ValueReader 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)); +} diff --git a/Utils/Utils.csproj b/Utils/Utils.csproj new file mode 100644 index 0000000..834cd4d --- /dev/null +++ b/Utils/Utils.csproj @@ -0,0 +1,10 @@ + + + + net7.0 + enable + enable + $(SolutionName.Replace(" ", "_")).$(MSBuildProjectName.Replace(" ", "_")) + + +