1
0

Initial commit

This commit is contained in:
2025-01-05 22:56:30 +01:00
commit 86f4edaa55
18 changed files with 3283 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
/.idea/

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationVisualStyles>true</ApplicationVisualStyles>
<ApplicationUseCompatibleTextRendering>false</ApplicationUseCompatibleTextRendering>
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
<LangVersion>default</LangVersion>
<!-- <ApplicationDefaultFont>Microsoft Sans Serif, 8.25pt</ApplicationDefaultFont> -->
<!-- <AllowUnsafeBlocks>true</AllowUnsafeBlocks> -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IO.Ports" Version="9.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup />
</Project>

25
CruiseControllerMX5.sln Normal file
View File

@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CruiseControllerMX5", "CruiseControllerMX5.csproj", "{1EB855FC-DA5B-4693-9AA9-F83B3EFCD166}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1EB855FC-DA5B-4693-9AA9-F83B3EFCD166}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1EB855FC-DA5B-4693-9AA9-F83B3EFCD166}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1EB855FC-DA5B-4693-9AA9-F83B3EFCD166}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1EB855FC-DA5B-4693-9AA9-F83B3EFCD166}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D516363E-E077-41A2-B6C6-E6E893675F31}
EndGlobalSection
EndGlobal

838
src/App.Designer.cs generated Normal file
View File

@@ -0,0 +1,838 @@
using System.ComponentModel;
namespace CruiseControllerMX5;
partial class App
{
/// <summary>
/// Required designer variable.
/// </summary>
private IContainer components = null;
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
leftPanel = new System.Windows.Forms.Panel();
leftFillPanel = new System.Windows.Forms.Panel();
speedGaugeCanvas = new System.Windows.Forms.PictureBox();
leftFillTopPanel = new System.Windows.Forms.Panel();
leftBottomPanel = new System.Windows.Forms.Panel();
kpidPanel = new System.Windows.Forms.TableLayoutPanel();
kdCurrentField = new System.Windows.Forms.TextBox();
kdField = new System.Windows.Forms.TextBox();
kpidNewValueLabel = new System.Windows.Forms.Label();
kpidCurrentValueLabel = new System.Windows.Forms.Label();
kpLabel = new System.Windows.Forms.Label();
kpField = new System.Windows.Forms.TextBox();
kpCurrentField = new System.Windows.Forms.TextBox();
kiLabel = new System.Windows.Forms.Label();
kiField = new System.Windows.Forms.TextBox();
kiCurrentField = new System.Windows.Forms.TextBox();
kdLabel = new System.Windows.Forms.Label();
rightPanel = new System.Windows.Forms.Panel();
graphPanel = new System.Windows.Forms.Panel();
graphCanvas = new System.Windows.Forms.PictureBox();
graphControlsPanel = new System.Windows.Forms.Panel();
servoToggle = new System.Windows.Forms.CheckBox();
accelerationToggle = new System.Windows.Forms.CheckBox();
speedToggle = new System.Windows.Forms.CheckBox();
derivativeToggle = new System.Windows.Forms.CheckBox();
integralToggle = new System.Windows.Forms.CheckBox();
errorToggle = new System.Windows.Forms.CheckBox();
valuesPanel = new System.Windows.Forms.Panel();
tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
errorField = new System.Windows.Forms.TextBox();
errorLabel = new System.Windows.Forms.Label();
accelerationLabel = new System.Windows.Forms.Label();
speedLabel = new System.Windows.Forms.Label();
speedField = new System.Windows.Forms.TextBox();
accelerationField = new System.Windows.Forms.TextBox();
derivativeField = new System.Windows.Forms.TextBox();
integralField = new System.Windows.Forms.TextBox();
proportionalField = new System.Windows.Forms.TextBox();
derivativeLabel = new System.Windows.Forms.Label();
integralLabel = new System.Windows.Forms.Label();
proportionalLabel = new System.Windows.Forms.Label();
servoLabel = new System.Windows.Forms.Label();
servoMinLabel = new System.Windows.Forms.Label();
servoMaxLabel = new System.Windows.Forms.Label();
servoNeutralLabel = new System.Windows.Forms.Label();
servoField = new System.Windows.Forms.TextBox();
servoMinField = new System.Windows.Forms.TextBox();
servoMaxField = new System.Windows.Forms.TextBox();
servoNeutralField = new System.Windows.Forms.TextBox();
statusPanel = new System.Windows.Forms.TableLayoutPanel();
stateLabel = new System.Windows.Forms.Label();
brakeLabel = new System.Windows.Forms.Label();
menuStrip1 = new System.Windows.Forms.MenuStrip();
leftPanel.SuspendLayout();
leftFillPanel.SuspendLayout();
((System.ComponentModel.ISupportInitialize)speedGaugeCanvas).BeginInit();
leftBottomPanel.SuspendLayout();
kpidPanel.SuspendLayout();
rightPanel.SuspendLayout();
graphPanel.SuspendLayout();
((System.ComponentModel.ISupportInitialize)graphCanvas).BeginInit();
graphControlsPanel.SuspendLayout();
valuesPanel.SuspendLayout();
tableLayoutPanel1.SuspendLayout();
statusPanel.SuspendLayout();
SuspendLayout();
//
// leftPanel
//
leftPanel.Controls.Add(leftFillPanel);
leftPanel.Controls.Add(leftBottomPanel);
leftPanel.Dock = System.Windows.Forms.DockStyle.Left;
leftPanel.Location = new System.Drawing.Point(0, 24);
leftPanel.Name = "leftPanel";
leftPanel.Size = new System.Drawing.Size(300, 480);
leftPanel.TabIndex = 1;
//
// leftFillPanel
//
leftFillPanel.Controls.Add(speedGaugeCanvas);
leftFillPanel.Controls.Add(leftFillTopPanel);
leftFillPanel.Dock = System.Windows.Forms.DockStyle.Fill;
leftFillPanel.Location = new System.Drawing.Point(0, 0);
leftFillPanel.Name = "leftFillPanel";
leftFillPanel.Size = new System.Drawing.Size(300, 300);
leftFillPanel.TabIndex = 4;
//
// speedGaugeCanvas
//
speedGaugeCanvas.Dock = System.Windows.Forms.DockStyle.Fill;
speedGaugeCanvas.Location = new System.Drawing.Point(0, 40);
speedGaugeCanvas.Name = "speedGaugeCanvas";
speedGaugeCanvas.Size = new System.Drawing.Size(300, 260);
speedGaugeCanvas.TabIndex = 2;
speedGaugeCanvas.TabStop = false;
speedGaugeCanvas.Paint += speedGaugePanel_Paint;
//
// leftFillTopPanel
//
leftFillTopPanel.Dock = System.Windows.Forms.DockStyle.Top;
leftFillTopPanel.Location = new System.Drawing.Point(0, 0);
leftFillTopPanel.Name = "leftFillTopPanel";
leftFillTopPanel.Size = new System.Drawing.Size(300, 40);
leftFillTopPanel.TabIndex = 3;
//
// leftBottomPanel
//
leftBottomPanel.Controls.Add(kpidPanel);
leftBottomPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
leftBottomPanel.Location = new System.Drawing.Point(0, 300);
leftBottomPanel.Name = "leftBottomPanel";
leftBottomPanel.Size = new System.Drawing.Size(300, 180);
leftBottomPanel.TabIndex = 3;
//
// kpidPanel
//
kpidPanel.AutoSize = true;
kpidPanel.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
kpidPanel.ColumnCount = 3;
kpidPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
kpidPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
kpidPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
kpidPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
kpidPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
kpidPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
kpidPanel.Controls.Add(kdCurrentField, 0, 3);
kpidPanel.Controls.Add(kdField, 0, 3);
kpidPanel.Controls.Add(kpidNewValueLabel, 1, 0);
kpidPanel.Controls.Add(kpidCurrentValueLabel, 2, 0);
kpidPanel.Controls.Add(kpLabel, 0, 1);
kpidPanel.Controls.Add(kpField, 1, 1);
kpidPanel.Controls.Add(kpCurrentField, 2, 1);
kpidPanel.Controls.Add(kiLabel, 0, 2);
kpidPanel.Controls.Add(kiField, 1, 2);
kpidPanel.Controls.Add(kiCurrentField, 2, 2);
kpidPanel.Controls.Add(kdLabel, 0, 3);
kpidPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
kpidPanel.Location = new System.Drawing.Point(0, 29);
kpidPanel.Name = "kpidPanel";
kpidPanel.Padding = new System.Windows.Forms.Padding(16);
kpidPanel.RowCount = 4;
kpidPanel.RowStyles.Add(new System.Windows.Forms.RowStyle());
kpidPanel.RowStyles.Add(new System.Windows.Forms.RowStyle());
kpidPanel.RowStyles.Add(new System.Windows.Forms.RowStyle());
kpidPanel.RowStyles.Add(new System.Windows.Forms.RowStyle());
kpidPanel.RowStyles.Add(new System.Windows.Forms.RowStyle());
kpidPanel.RowStyles.Add(new System.Windows.Forms.RowStyle());
kpidPanel.RowStyles.Add(new System.Windows.Forms.RowStyle());
kpidPanel.RowStyles.Add(new System.Windows.Forms.RowStyle());
kpidPanel.Size = new System.Drawing.Size(300, 151);
kpidPanel.TabIndex = 1;
//
// kdCurrentField
//
kdCurrentField.Dock = System.Windows.Forms.DockStyle.Fill;
kdCurrentField.Location = new System.Drawing.Point(165, 105);
kdCurrentField.Name = "kdCurrentField";
kdCurrentField.ReadOnly = true;
kdCurrentField.Size = new System.Drawing.Size(116, 27);
kdCurrentField.TabIndex = 10;
kdCurrentField.TabStop = false;
//
// kdField
//
kdField.Dock = System.Windows.Forms.DockStyle.Fill;
kdField.Location = new System.Drawing.Point(44, 105);
kdField.Name = "kdField";
kdField.Size = new System.Drawing.Size(115, 27);
kdField.TabIndex = 9;
//
// kpidNewValueLabel
//
kpidNewValueLabel.AutoSize = true;
kpidNewValueLabel.Dock = System.Windows.Forms.DockStyle.Fill;
kpidNewValueLabel.Location = new System.Drawing.Point(41, 16);
kpidNewValueLabel.Margin = new System.Windows.Forms.Padding(0);
kpidNewValueLabel.Name = "kpidNewValueLabel";
kpidNewValueLabel.Size = new System.Drawing.Size(121, 20);
kpidNewValueLabel.TabIndex = 0;
kpidNewValueLabel.Text = "Réglage";
kpidNewValueLabel.TextAlign = System.Drawing.ContentAlignment.BottomCenter;
//
// kpidCurrentValueLabel
//
kpidCurrentValueLabel.AutoSize = true;
kpidCurrentValueLabel.Dock = System.Windows.Forms.DockStyle.Fill;
kpidCurrentValueLabel.Location = new System.Drawing.Point(162, 16);
kpidCurrentValueLabel.Margin = new System.Windows.Forms.Padding(0);
kpidCurrentValueLabel.Name = "kpidCurrentValueLabel";
kpidCurrentValueLabel.Size = new System.Drawing.Size(122, 20);
kpidCurrentValueLabel.TabIndex = 1;
kpidCurrentValueLabel.Text = "Actuel";
kpidCurrentValueLabel.TextAlign = System.Drawing.ContentAlignment.BottomCenter;
//
// kpLabel
//
kpLabel.AutoSize = true;
kpLabel.Dock = System.Windows.Forms.DockStyle.Fill;
kpLabel.Location = new System.Drawing.Point(16, 36);
kpLabel.Margin = new System.Windows.Forms.Padding(0);
kpLabel.Name = "kpLabel";
kpLabel.Size = new System.Drawing.Size(25, 33);
kpLabel.TabIndex = 2;
kpLabel.Text = "kp";
kpLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// kpField
//
kpField.Dock = System.Windows.Forms.DockStyle.Fill;
kpField.Location = new System.Drawing.Point(44, 39);
kpField.Name = "kpField";
kpField.Size = new System.Drawing.Size(115, 27);
kpField.TabIndex = 3;
//
// kpCurrentField
//
kpCurrentField.Dock = System.Windows.Forms.DockStyle.Fill;
kpCurrentField.Location = new System.Drawing.Point(165, 39);
kpCurrentField.Name = "kpCurrentField";
kpCurrentField.ReadOnly = true;
kpCurrentField.Size = new System.Drawing.Size(116, 27);
kpCurrentField.TabIndex = 4;
kpCurrentField.TabStop = false;
//
// kiLabel
//
kiLabel.AutoSize = true;
kiLabel.Dock = System.Windows.Forms.DockStyle.Fill;
kiLabel.Location = new System.Drawing.Point(16, 69);
kiLabel.Margin = new System.Windows.Forms.Padding(0);
kiLabel.Name = "kiLabel";
kiLabel.Size = new System.Drawing.Size(25, 33);
kiLabel.TabIndex = 5;
kiLabel.Text = "ki";
kiLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// kiField
//
kiField.Dock = System.Windows.Forms.DockStyle.Fill;
kiField.Location = new System.Drawing.Point(44, 72);
kiField.Name = "kiField";
kiField.Size = new System.Drawing.Size(115, 27);
kiField.TabIndex = 6;
//
// kiCurrentField
//
kiCurrentField.Dock = System.Windows.Forms.DockStyle.Fill;
kiCurrentField.Location = new System.Drawing.Point(165, 72);
kiCurrentField.Name = "kiCurrentField";
kiCurrentField.ReadOnly = true;
kiCurrentField.Size = new System.Drawing.Size(116, 27);
kiCurrentField.TabIndex = 7;
kiCurrentField.TabStop = false;
//
// kdLabel
//
kdLabel.AutoSize = true;
kdLabel.Dock = System.Windows.Forms.DockStyle.Fill;
kdLabel.Location = new System.Drawing.Point(16, 102);
kdLabel.Margin = new System.Windows.Forms.Padding(0);
kdLabel.Name = "kdLabel";
kdLabel.Size = new System.Drawing.Size(25, 33);
kdLabel.TabIndex = 8;
kdLabel.Text = "kd";
kdLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// rightPanel
//
rightPanel.Controls.Add(graphPanel);
rightPanel.Controls.Add(valuesPanel);
rightPanel.Dock = System.Windows.Forms.DockStyle.Fill;
rightPanel.Location = new System.Drawing.Point(300, 24);
rightPanel.Name = "rightPanel";
rightPanel.Size = new System.Drawing.Size(1300, 480);
rightPanel.TabIndex = 2;
//
// graphPanel
//
graphPanel.Controls.Add(graphCanvas);
graphPanel.Controls.Add(graphControlsPanel);
graphPanel.Dock = System.Windows.Forms.DockStyle.Fill;
graphPanel.Location = new System.Drawing.Point(0, 0);
graphPanel.Name = "graphPanel";
graphPanel.Size = new System.Drawing.Size(1300, 300);
graphPanel.TabIndex = 3;
//
// graphCanvas
//
graphCanvas.Dock = System.Windows.Forms.DockStyle.Fill;
graphCanvas.Location = new System.Drawing.Point(0, 40);
graphCanvas.Name = "graphCanvas";
graphCanvas.Size = new System.Drawing.Size(1300, 260);
graphCanvas.TabIndex = 2;
graphCanvas.TabStop = false;
graphCanvas.Paint += graphCanvas_Paint;
//
// graphControlsPanel
//
graphControlsPanel.Controls.Add(servoToggle);
graphControlsPanel.Controls.Add(accelerationToggle);
graphControlsPanel.Controls.Add(speedToggle);
graphControlsPanel.Controls.Add(derivativeToggle);
graphControlsPanel.Controls.Add(integralToggle);
graphControlsPanel.Controls.Add(errorToggle);
graphControlsPanel.Dock = System.Windows.Forms.DockStyle.Top;
graphControlsPanel.Location = new System.Drawing.Point(0, 0);
graphControlsPanel.Name = "graphControlsPanel";
graphControlsPanel.Size = new System.Drawing.Size(1300, 40);
graphControlsPanel.TabIndex = 3;
//
// servoToggle
//
servoToggle.AutoSize = true;
servoToggle.Location = new System.Drawing.Point(452, 10);
servoToggle.Name = "servoToggle";
servoToggle.Size = new System.Drawing.Size(65, 24);
servoToggle.TabIndex = 5;
servoToggle.Text = "Servo";
servoToggle.UseVisualStyleBackColor = true;
//
// accelerationToggle
//
accelerationToggle.AutoSize = true;
accelerationToggle.Location = new System.Drawing.Point(335, 10);
accelerationToggle.Name = "accelerationToggle";
accelerationToggle.Size = new System.Drawing.Size(111, 24);
accelerationToggle.TabIndex = 4;
accelerationToggle.Text = "Accélération";
accelerationToggle.UseVisualStyleBackColor = true;
//
// speedToggle
//
speedToggle.AutoSize = true;
speedToggle.Location = new System.Drawing.Point(255, 10);
speedToggle.Name = "speedToggle";
speedToggle.Size = new System.Drawing.Size(74, 24);
speedToggle.TabIndex = 3;
speedToggle.Text = "Vitesse";
speedToggle.UseVisualStyleBackColor = true;
//
// derivativeToggle
//
derivativeToggle.AutoSize = true;
derivativeToggle.Location = new System.Drawing.Point(164, 10);
derivativeToggle.Name = "derivativeToggle";
derivativeToggle.Size = new System.Drawing.Size(85, 24);
derivativeToggle.TabIndex = 2;
derivativeToggle.Text = "Dérivatif";
derivativeToggle.UseVisualStyleBackColor = true;
//
// integralToggle
//
integralToggle.AutoSize = true;
integralToggle.Location = new System.Drawing.Point(79, 10);
integralToggle.Name = "integralToggle";
integralToggle.Size = new System.Drawing.Size(79, 24);
integralToggle.TabIndex = 1;
integralToggle.Text = "Intégral";
integralToggle.UseVisualStyleBackColor = true;
//
// errorToggle
//
errorToggle.AutoSize = true;
errorToggle.Location = new System.Drawing.Point(6, 10);
errorToggle.Name = "errorToggle";
errorToggle.Size = new System.Drawing.Size(67, 24);
errorToggle.TabIndex = 0;
errorToggle.Text = "Erreur";
errorToggle.UseVisualStyleBackColor = true;
//
// valuesPanel
//
valuesPanel.Controls.Add(tableLayoutPanel1);
valuesPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
valuesPanel.Location = new System.Drawing.Point(0, 300);
valuesPanel.Name = "valuesPanel";
valuesPanel.Size = new System.Drawing.Size(1300, 180);
valuesPanel.TabIndex = 1;
//
// tableLayoutPanel1
//
tableLayoutPanel1.AutoSize = true;
tableLayoutPanel1.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
tableLayoutPanel1.ColumnCount = 8;
tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 200F));
tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 16F));
tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 100F));
tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 16F));
tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 50F));
tableLayoutPanel1.Controls.Add(errorField, 1, 0);
tableLayoutPanel1.Controls.Add(errorLabel, 0, 0);
tableLayoutPanel1.Controls.Add(accelerationLabel, 3, 1);
tableLayoutPanel1.Controls.Add(speedLabel, 3, 0);
tableLayoutPanel1.Controls.Add(speedField, 4, 0);
tableLayoutPanel1.Controls.Add(accelerationField, 4, 1);
tableLayoutPanel1.Controls.Add(derivativeField, 1, 3);
tableLayoutPanel1.Controls.Add(integralField, 1, 2);
tableLayoutPanel1.Controls.Add(proportionalField, 1, 1);
tableLayoutPanel1.Controls.Add(derivativeLabel, 0, 3);
tableLayoutPanel1.Controls.Add(integralLabel, 0, 2);
tableLayoutPanel1.Controls.Add(proportionalLabel, 0, 1);
tableLayoutPanel1.Controls.Add(servoLabel, 6, 0);
tableLayoutPanel1.Controls.Add(servoMinLabel, 6, 1);
tableLayoutPanel1.Controls.Add(servoMaxLabel, 6, 2);
tableLayoutPanel1.Controls.Add(servoNeutralLabel, 6, 3);
tableLayoutPanel1.Controls.Add(servoField, 7, 0);
tableLayoutPanel1.Controls.Add(servoMinField, 7, 1);
tableLayoutPanel1.Controls.Add(servoMaxField, 7, 2);
tableLayoutPanel1.Controls.Add(servoNeutralField, 7, 3);
tableLayoutPanel1.Location = new System.Drawing.Point(16, 32);
tableLayoutPanel1.Name = "tableLayoutPanel1";
tableLayoutPanel1.RowCount = 4;
tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle());
tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle());
tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle());
tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle());
tableLayoutPanel1.Size = new System.Drawing.Size(699, 132);
tableLayoutPanel1.TabIndex = 11;
//
// errorField
//
errorField.Dock = System.Windows.Forms.DockStyle.Fill;
errorField.Location = new System.Drawing.Point(103, 3);
errorField.Name = "errorField";
errorField.ReadOnly = true;
errorField.Size = new System.Drawing.Size(194, 27);
errorField.TabIndex = 1;
errorField.TabStop = false;
//
// errorLabel
//
errorLabel.AutoSize = true;
errorLabel.Dock = System.Windows.Forms.DockStyle.Fill;
errorLabel.Location = new System.Drawing.Point(0, 0);
errorLabel.Margin = new System.Windows.Forms.Padding(0);
errorLabel.Name = "errorLabel";
errorLabel.Size = new System.Drawing.Size(100, 33);
errorLabel.TabIndex = 0;
errorLabel.Text = "Erreur";
errorLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// accelerationLabel
//
accelerationLabel.AutoSize = true;
accelerationLabel.Dock = System.Windows.Forms.DockStyle.Fill;
accelerationLabel.Location = new System.Drawing.Point(316, 33);
accelerationLabel.Margin = new System.Windows.Forms.Padding(0);
accelerationLabel.Name = "accelerationLabel";
accelerationLabel.Size = new System.Drawing.Size(92, 33);
accelerationLabel.TabIndex = 10;
accelerationLabel.Text = "Accélération";
accelerationLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// speedLabel
//
speedLabel.AutoSize = true;
speedLabel.Dock = System.Windows.Forms.DockStyle.Fill;
speedLabel.Location = new System.Drawing.Point(316, 0);
speedLabel.Margin = new System.Windows.Forms.Padding(0);
speedLabel.Name = "speedLabel";
speedLabel.Size = new System.Drawing.Size(92, 33);
speedLabel.TabIndex = 8;
speedLabel.Text = "Vitesse";
speedLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// speedField
//
speedField.Dock = System.Windows.Forms.DockStyle.Fill;
speedField.Location = new System.Drawing.Point(411, 3);
speedField.Name = "speedField";
speedField.ReadOnly = true;
speedField.Size = new System.Drawing.Size(94, 27);
speedField.TabIndex = 9;
speedField.TabStop = false;
//
// accelerationField
//
accelerationField.Dock = System.Windows.Forms.DockStyle.Fill;
accelerationField.Location = new System.Drawing.Point(411, 36);
accelerationField.Name = "accelerationField";
accelerationField.ReadOnly = true;
accelerationField.Size = new System.Drawing.Size(94, 27);
accelerationField.TabIndex = 11;
accelerationField.TabStop = false;
//
// derivativeField
//
derivativeField.Dock = System.Windows.Forms.DockStyle.Fill;
derivativeField.Location = new System.Drawing.Point(103, 102);
derivativeField.Name = "derivativeField";
derivativeField.ReadOnly = true;
derivativeField.Size = new System.Drawing.Size(194, 27);
derivativeField.TabIndex = 7;
derivativeField.TabStop = false;
//
// integralField
//
integralField.Dock = System.Windows.Forms.DockStyle.Fill;
integralField.Location = new System.Drawing.Point(103, 69);
integralField.Name = "integralField";
integralField.ReadOnly = true;
integralField.Size = new System.Drawing.Size(194, 27);
integralField.TabIndex = 5;
integralField.TabStop = false;
//
// proportionalField
//
proportionalField.Dock = System.Windows.Forms.DockStyle.Fill;
proportionalField.Location = new System.Drawing.Point(103, 36);
proportionalField.Name = "proportionalField";
proportionalField.ReadOnly = true;
proportionalField.Size = new System.Drawing.Size(194, 27);
proportionalField.TabIndex = 3;
proportionalField.TabStop = false;
//
// derivativeLabel
//
derivativeLabel.AutoSize = true;
derivativeLabel.Dock = System.Windows.Forms.DockStyle.Fill;
derivativeLabel.Location = new System.Drawing.Point(0, 99);
derivativeLabel.Margin = new System.Windows.Forms.Padding(0);
derivativeLabel.Name = "derivativeLabel";
derivativeLabel.Size = new System.Drawing.Size(100, 33);
derivativeLabel.TabIndex = 6;
derivativeLabel.Text = "Dérivatif";
derivativeLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// integralLabel
//
integralLabel.AutoSize = true;
integralLabel.Dock = System.Windows.Forms.DockStyle.Fill;
integralLabel.Location = new System.Drawing.Point(0, 66);
integralLabel.Margin = new System.Windows.Forms.Padding(0);
integralLabel.Name = "integralLabel";
integralLabel.Size = new System.Drawing.Size(100, 33);
integralLabel.TabIndex = 4;
integralLabel.Text = "Intégral";
integralLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// proportionalLabel
//
proportionalLabel.AutoSize = true;
proportionalLabel.Dock = System.Windows.Forms.DockStyle.Fill;
proportionalLabel.Location = new System.Drawing.Point(0, 33);
proportionalLabel.Margin = new System.Windows.Forms.Padding(0);
proportionalLabel.Name = "proportionalLabel";
proportionalLabel.Size = new System.Drawing.Size(100, 33);
proportionalLabel.TabIndex = 2;
proportionalLabel.Text = "Proportionnel";
proportionalLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// servoLabel
//
servoLabel.AutoSize = true;
servoLabel.Dock = System.Windows.Forms.DockStyle.Fill;
servoLabel.Location = new System.Drawing.Point(524, 0);
servoLabel.Margin = new System.Windows.Forms.Padding(0);
servoLabel.Name = "servoLabel";
servoLabel.Size = new System.Drawing.Size(125, 33);
servoLabel.TabIndex = 12;
servoLabel.Text = "Commande servo";
servoLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// servoMinLabel
//
servoMinLabel.AutoSize = true;
servoMinLabel.Dock = System.Windows.Forms.DockStyle.Fill;
servoMinLabel.Location = new System.Drawing.Point(524, 33);
servoMinLabel.Margin = new System.Windows.Forms.Padding(0);
servoMinLabel.Name = "servoMinLabel";
servoMinLabel.Size = new System.Drawing.Size(125, 33);
servoMinLabel.TabIndex = 14;
servoMinLabel.Text = "Servo min";
servoMinLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// servoMaxLabel
//
servoMaxLabel.AutoSize = true;
servoMaxLabel.Dock = System.Windows.Forms.DockStyle.Fill;
servoMaxLabel.Location = new System.Drawing.Point(524, 66);
servoMaxLabel.Margin = new System.Windows.Forms.Padding(0);
servoMaxLabel.Name = "servoMaxLabel";
servoMaxLabel.Size = new System.Drawing.Size(125, 33);
servoMaxLabel.TabIndex = 16;
servoMaxLabel.Text = "Servo max";
servoMaxLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// servoNeutralLabel
//
servoNeutralLabel.AutoSize = true;
servoNeutralLabel.Dock = System.Windows.Forms.DockStyle.Fill;
servoNeutralLabel.Location = new System.Drawing.Point(524, 99);
servoNeutralLabel.Margin = new System.Windows.Forms.Padding(0);
servoNeutralLabel.Name = "servoNeutralLabel";
servoNeutralLabel.Size = new System.Drawing.Size(125, 33);
servoNeutralLabel.TabIndex = 18;
servoNeutralLabel.Text = "Servo neutre";
servoNeutralLabel.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// servoField
//
servoField.Dock = System.Windows.Forms.DockStyle.Fill;
servoField.Location = new System.Drawing.Point(652, 3);
servoField.Name = "servoField";
servoField.ReadOnly = true;
servoField.Size = new System.Drawing.Size(44, 27);
servoField.TabIndex = 13;
servoField.TabStop = false;
//
// servoMinField
//
servoMinField.Dock = System.Windows.Forms.DockStyle.Fill;
servoMinField.Location = new System.Drawing.Point(652, 36);
servoMinField.Name = "servoMinField";
servoMinField.ReadOnly = true;
servoMinField.Size = new System.Drawing.Size(44, 27);
servoMinField.TabIndex = 15;
servoMinField.TabStop = false;
//
// servoMaxField
//
servoMaxField.Dock = System.Windows.Forms.DockStyle.Fill;
servoMaxField.Location = new System.Drawing.Point(652, 69);
servoMaxField.Name = "servoMaxField";
servoMaxField.ReadOnly = true;
servoMaxField.Size = new System.Drawing.Size(44, 27);
servoMaxField.TabIndex = 17;
servoMaxField.TabStop = false;
//
// servoNeutralField
//
servoNeutralField.Dock = System.Windows.Forms.DockStyle.Fill;
servoNeutralField.Location = new System.Drawing.Point(652, 102);
servoNeutralField.Name = "servoNeutralField";
servoNeutralField.ReadOnly = true;
servoNeutralField.Size = new System.Drawing.Size(44, 27);
servoNeutralField.TabIndex = 19;
servoNeutralField.TabStop = false;
//
// statusPanel
//
statusPanel.ColumnCount = 3;
statusPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.333332F));
statusPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.333332F));
statusPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.333332F));
statusPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.333332F));
statusPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.333332F));
statusPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.333332F));
statusPanel.Controls.Add(stateLabel, 0, 0);
statusPanel.Controls.Add(brakeLabel, 1, 0);
statusPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
statusPanel.Location = new System.Drawing.Point(0, 504);
statusPanel.Name = "statusPanel";
statusPanel.RowCount = 1;
statusPanel.RowStyles.Add(new System.Windows.Forms.RowStyle());
statusPanel.RowStyles.Add(new System.Windows.Forms.RowStyle());
statusPanel.Size = new System.Drawing.Size(1600, 32);
statusPanel.TabIndex = 3;
//
// stateLabel
//
stateLabel.BackColor = System.Drawing.Color.FromArgb(((int)((byte)70)), ((int)((byte)70)), ((int)((byte)70)));
stateLabel.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
stateLabel.Dock = System.Windows.Forms.DockStyle.Fill;
stateLabel.Font = new System.Drawing.Font("Segoe UI", 14F, System.Drawing.FontStyle.Bold);
stateLabel.Location = new System.Drawing.Point(0, 0);
stateLabel.Margin = new System.Windows.Forms.Padding(0);
stateLabel.Name = "stateLabel";
stateLabel.Size = new System.Drawing.Size(533, 32);
stateLabel.TabIndex = 0;
stateLabel.Text = "Déconnecté / OFF";
stateLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
//
// brakeLabel
//
brakeLabel.BackColor = System.Drawing.Color.FromArgb(((int)((byte)70)), ((int)((byte)70)), ((int)((byte)70)));
brakeLabel.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
brakeLabel.Dock = System.Windows.Forms.DockStyle.Fill;
brakeLabel.Font = new System.Drawing.Font("Segoe UI", 14F, System.Drawing.FontStyle.Bold);
brakeLabel.Location = new System.Drawing.Point(533, 0);
brakeLabel.Margin = new System.Windows.Forms.Padding(0);
brakeLabel.Name = "brakeLabel";
brakeLabel.Size = new System.Drawing.Size(533, 32);
brakeLabel.TabIndex = 1;
brakeLabel.Text = "Frein";
brakeLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
//
// menuStrip1
//
menuStrip1.Location = new System.Drawing.Point(0, 0);
menuStrip1.Name = "menuStrip1";
menuStrip1.Size = new System.Drawing.Size(1600, 24);
menuStrip1.TabIndex = 4;
menuStrip1.Text = "menuStrip1";
//
// App
//
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
BackColor = System.Drawing.Color.FromArgb(((int)((byte)30)), ((int)((byte)30)), ((int)((byte)30)));
ClientSize = new System.Drawing.Size(1600, 536);
Controls.Add(rightPanel);
Controls.Add(leftPanel);
Controls.Add(statusPanel);
Controls.Add(menuStrip1);
Font = new System.Drawing.Font("Segoe UI", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)0));
ForeColor = System.Drawing.Color.White;
Location = new System.Drawing.Point(15, 15);
MainMenuStrip = menuStrip1;
MinimumSize = new System.Drawing.Size(1616, 551);
Text = "Cruise Controller MX5";
Load += App_Load;
leftPanel.ResumeLayout(false);
leftFillPanel.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)speedGaugeCanvas).EndInit();
leftBottomPanel.ResumeLayout(false);
leftBottomPanel.PerformLayout();
kpidPanel.ResumeLayout(false);
kpidPanel.PerformLayout();
rightPanel.ResumeLayout(false);
graphPanel.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)graphCanvas).EndInit();
graphControlsPanel.ResumeLayout(false);
graphControlsPanel.PerformLayout();
valuesPanel.ResumeLayout(false);
valuesPanel.PerformLayout();
tableLayoutPanel1.ResumeLayout(false);
tableLayoutPanel1.PerformLayout();
statusPanel.ResumeLayout(false);
ResumeLayout(false);
PerformLayout();
}
private System.Windows.Forms.MenuStrip menuStrip1;
private System.Windows.Forms.CheckBox speedToggle;
private System.Windows.Forms.CheckBox accelerationToggle;
private System.Windows.Forms.CheckBox servoToggle;
private System.Windows.Forms.CheckBox integralToggle;
private System.Windows.Forms.CheckBox derivativeToggle;
private System.Windows.Forms.CheckBox errorToggle;
private System.Windows.Forms.Label servoMinLabel;
private System.Windows.Forms.Label servoMaxLabel;
private System.Windows.Forms.Label servoNeutralLabel;
private System.Windows.Forms.TextBox servoField;
private System.Windows.Forms.TextBox servoMinField;
private System.Windows.Forms.TextBox servoMaxField;
private System.Windows.Forms.TextBox servoNeutralField;
private System.Windows.Forms.Label servoLabel;
private System.Windows.Forms.Panel leftFillTopPanel;
private System.Windows.Forms.Panel leftFillPanel;
private System.Windows.Forms.Panel leftBottomPanel;
private System.Windows.Forms.TextBox speedField;
private System.Windows.Forms.TextBox accelerationField;
private System.Windows.Forms.Label speedLabel;
private System.Windows.Forms.Label accelerationLabel;
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
private System.Windows.Forms.TextBox errorField;
private System.Windows.Forms.TextBox integralField;
private System.Windows.Forms.TextBox derivativeField;
private System.Windows.Forms.Label derivativeLabel;
private System.Windows.Forms.Label errorLabel;
private System.Windows.Forms.TextBox proportionalField;
private System.Windows.Forms.Label integralLabel;
private System.Windows.Forms.Label proportionalLabel;
private System.Windows.Forms.Label brakeLabel;
private System.Windows.Forms.Label stateLabel;
private System.Windows.Forms.PictureBox graphCanvas;
private System.Windows.Forms.Panel graphControlsPanel;
private System.Windows.Forms.Panel graphPanel;
private System.Windows.Forms.PictureBox speedGaugeCanvas;
private System.Windows.Forms.TableLayoutPanel statusPanel;
private System.Windows.Forms.Panel valuesPanel;
private System.Windows.Forms.Label kpidNewValueLabel;
private System.Windows.Forms.Label kpidCurrentValueLabel;
private System.Windows.Forms.Label kpLabel;
private System.Windows.Forms.TextBox kpField;
private System.Windows.Forms.TextBox kpCurrentField;
private System.Windows.Forms.Label kiLabel;
private System.Windows.Forms.TextBox kiField;
private System.Windows.Forms.TextBox kiCurrentField;
private System.Windows.Forms.Label kdLabel;
private System.Windows.Forms.TextBox kdField;
private System.Windows.Forms.TextBox kdCurrentField;
private System.Windows.Forms.TableLayoutPanel kpidPanel;
private System.Windows.Forms.Panel rightPanel;
private System.Windows.Forms.Panel leftPanel;
#endregion
}

1196
src/App.cs Normal file

File diff suppressed because it is too large Load Diff

64
src/App.resx Normal file
View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="menuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing.Primitives, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root>

502
src/ArduinoCom.cs Normal file
View File

@@ -0,0 +1,502 @@
using System.Collections.Concurrent;
using System.Globalization;
using System.IO.Ports;
namespace CruiseControllerMX5;
public interface IArduinoCom : IDisposable, IObservable<Sample>, IObserver<string>
{
void Open();
void Close();
IDisposable AddSerialObserver(IObserver<Sample> observer);
void SendCommand(string command);
}
public sealed class ArduinoCom : IArduinoCom
{
private readonly List<IObserver<Sample>> _sampleObservers = [];
private readonly BlockingCollection<string> _commands = [];
private readonly SerialPort _serialPort;
private readonly bool _keepOpen;
private bool _hasIgnoredFirstPacket, _hasReadSecondPacket;
private Sample? _previousSample;
public ArduinoCom(SerialPort serialPort, bool keepOpen = false)
{
_serialPort = serialPort;
_keepOpen = keepOpen;
}
public ArduinoCom(Action<SerialPort> configure)
{
var serialPort = new SerialPort();
configure(serialPort);
_serialPort = serialPort;
_keepOpen = false;
}
public long ZeroTimestamp { get; private set; }
public void Open()
{
_serialPort.Open();
var readThread = new Thread(DoRead)
{
IsBackground = true
};
readThread.Start();
var writeThread = new Thread(DoWrite)
{
IsBackground = true
};
writeThread.Start();
}
public void Close()
{
_serialPort.Close();
_hasIgnoredFirstPacket = false;
_hasReadSecondPacket = false;
ZeroTimestamp = 0;
_previousSample = null;
}
public IDisposable AddSerialObserver(IObserver<Sample> observer)
{
_sampleObservers.Add(observer);
return new Disposer(() => _sampleObservers.Remove(observer));
}
public void SendCommand(string command)
{
_commands.Add(command);
}
private Sample HandlePacket(string packet)
{
var timestamp = TimeProvider.System.GetTimestamp();
if (!_hasReadSecondPacket)
{
ZeroTimestamp = timestamp;
_hasReadSecondPacket = true;
}
Console.WriteLine(packet);
var millis = (uint)TimeProvider.System.GetElapsedTime(ZeroTimestamp, timestamp).TotalMilliseconds;
var sample = new Sample(millis,
_previousSample == null || _previousSample.Timestamp / 1000 != timestamp / 1000);
var variables = packet.Split(';');
for (int i = 0, max = variables.Length - 1; i < max; i++)
{
var variable = variables[i];
var split = variable.Split(':', 2);
var name = split[0];
var value = split[1];
UpdateSample(sample, name, value);
}
return sample;
}
private static void UpdateSample(Sample sample, string name, string value)
{
switch (name)
{
case "KP":
sample.Kp = float.Parse(value, CultureInfo.InvariantCulture);
break;
case "KI":
sample.Ki = float.Parse(value, CultureInfo.InvariantCulture);
break;
case "KD":
sample.Kd = float.Parse(value, CultureInfo.InvariantCulture);
break;
case "VITESSEMIN":
sample.SpeedMin = int.Parse(value, CultureInfo.InvariantCulture);
break;
case "VITESSEMAX":
sample.SpeedMax = int.Parse(value, CultureInfo.InvariantCulture);
break;
case "ETAT":
sample.Active = Utils.IntToBool(value);
break;
case "CONSIGNE":
sample.Consigne = float.Parse(value, CultureInfo.InvariantCulture);
break;
case "VITESSE":
sample.Speed = float.Parse(value, CultureInfo.InvariantCulture);
break;
case "ACCEL":
sample.Acceleration = float.Parse(value, CultureInfo.InvariantCulture);
break;
case "ERREUR":
sample.Error = float.Parse(value, CultureInfo.InvariantCulture);
break;
case "PROPORTIONNEL":
sample.Proportional = float.Parse(value, CultureInfo.InvariantCulture);
break;
case "INTEGRAL":
sample.Integral = float.Parse(value, CultureInfo.InvariantCulture);
break;
case "DERIVATIF":
sample.Derivative = float.Parse(value, CultureInfo.InvariantCulture);
break;
case "SERVO":
sample.Servo = int.Parse(value, CultureInfo.InvariantCulture);
break;
case "SERVOMIN":
sample.ServoMin = int.Parse(value, CultureInfo.InvariantCulture);
break;
case "SERVOMAX":
sample.ServoMax = int.Parse(value, CultureInfo.InvariantCulture);
break;
case "SERVONEUTRE":
sample.ServoNeutre = int.Parse(value, CultureInfo.InvariantCulture);
break;
case "FREIN":
sample.BrakeFlag = Utils.IntToBool(value);
break;
}
}
private void DoRead()
{
try
{
while (_serialPort.IsOpen)
{
string line;
try
{
line = _serialPort.ReadLine();
}
catch (ObjectDisposedException)
{
return;
}
catch (OperationCanceledException)
{
return;
}
catch (Exception ex)
{
Console.Error.WriteLine("Failed to read line from serial connection");
Console.Error.WriteLine(ex.Message);
return;
}
if (string.IsNullOrEmpty(line))
{
continue;
}
if (!_hasIgnoredFirstPacket)
{
_hasIgnoredFirstPacket = true;
continue;
}
var sample = HandlePacket(line);
_previousSample = sample;
foreach (var observer in _sampleObservers)
{
try
{
observer.OnNext(sample);
}
catch (Exception ex)
{
Console.Error.WriteLine("An exception occurred in a packet handler");
Console.Error.WriteLine(ex.Message);
}
}
}
}
catch (Exception ex)
{
Console.Error.WriteLine("An exception occurred in the serial read loop");
Console.Error.WriteLine(ex.Message);
throw;
}
}
private void DoWrite()
{
try
{
while (_serialPort.IsOpen)
{
string line;
try
{
line = _commands.Take();
}
catch (ObjectDisposedException)
{
return;
}
catch (OperationCanceledException)
{
return;
}
if (string.IsNullOrEmpty(line))
{
continue;
}
try
{
_serialPort.WriteLine(line);
}
catch (ObjectDisposedException)
{
return;
}
catch (OperationCanceledException)
{
return;
}
catch (Exception ex)
{
Console.Error.WriteLine("Failed to send command");
Console.Error.WriteLine(ex.Message);
}
}
}
catch (Exception ex)
{
Console.Error.WriteLine("An exception occurred in the serial write loop");
Console.Error.WriteLine(ex.Message);
throw;
}
}
#region IObservable
IDisposable IObservable<Sample>.Subscribe(IObserver<Sample> 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);
}
#endregion
#region IDisposable
private bool _disposedValue;
public void Dispose()
{
if (_disposedValue)
{
return;
}
if (!_keepOpen)
{
_serialPort.Close();
}
_commands.Dispose();
_sampleObservers.Clear();
_disposedValue = true;
}
#endregion
}
public sealed class TestArduinoCom : IArduinoCom
{
private readonly List<IObserver<Sample>> _sampleObservers = [];
private long _zeroTimestamp;
private volatile Sample? _previousSample;
private volatile bool _keepGoing;
public void Open()
{
_keepGoing = true;
var readThread = new Thread(DoRead)
{
IsBackground = true
};
readThread.Start();
}
public void Close()
{
_keepGoing = false;
_previousSample = null;
}
public IDisposable AddSerialObserver(IObserver<Sample> observer)
{
_sampleObservers.Add(observer);
return new Disposer(() => _sampleObservers.Remove(observer));
}
public void SendCommand(string command)
{
}
private void DoRead()
{
try
{
Thread.Sleep(2000);
while (_keepGoing)
{
var timestamp = TimeProvider.System.GetTimestamp();
if (_zeroTimestamp == 0L)
{
_zeroTimestamp = timestamp;
}
var currentSecs = (int)TimeProvider.System.GetElapsedTime(_zeroTimestamp, timestamp).TotalSeconds;
Sample sample;
if (_previousSample == null)
{
sample = new Sample(timestamp, true)
{
Speed = 0,
SpeedMin = 30,
SpeedMax = 150,
Active = true,
Consigne = 30,
ServoMin = 1160,
ServoMax = 1600,
ServoNeutre = 1200,
};
}
else
{
var previousSecs = (int)TimeProvider.System
.GetElapsedTime(_zeroTimestamp, _previousSample.Timestamp).TotalSeconds;
sample = _previousSample.Copy(timestamp, previousSecs != currentSecs);
sample.Speed += 1f;
if (sample.Speed > 240)
{
sample.Speed = 0;
}
sample.Consigne += 0.1f;
if (sample.Consigne > sample.SpeedMax)
{
sample.Consigne = sample.SpeedMin;
}
}
sample.Active = currentSecs / 3 % 3 == 0;
sample.BrakeFlag = currentSecs % 2 == 0;
_previousSample = sample;
foreach (var observer in _sampleObservers)
{
try
{
observer.OnNext(sample);
}
catch (Exception ex)
{
Console.Error.WriteLine("An exception occurred in a packet handler");
Console.Error.WriteLine(ex.Message);
}
}
Thread.Sleep(100);
}
}
catch (Exception ex)
{
Console.Error.WriteLine("An exception occurred in the serial read loop");
Console.Error.WriteLine(ex.Message);
throw;
}
}
#region IObservable
IDisposable IObservable<Sample>.Subscribe(IObserver<Sample> observer)
{
return AddSerialObserver(observer);
}
#endregion
#region IObserver
void IObserver<string>.OnCompleted()
{
}
void IObserver<string>.OnError(Exception error)
{
}
void IObserver<string>.OnNext(string value)
{
}
#endregion
#region IDisposable
private bool _disposedValue;
public void Dispose()
{
if (_disposedValue)
{
return;
}
Close();
_sampleObservers.Clear();
_disposedValue = true;
}
#endregion
}

73
src/Buffer.cs Normal file
View File

@@ -0,0 +1,73 @@
using System.Collections;
namespace CruiseControllerMX5;
public static class Buffer
{
public static IBuffer<T> CreateBounded<T>(int capacity)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity);
return new BoundedBuffer<T>(capacity);
}
}
public interface IBuffer<T> : IEnumerable<T>
{
int Capacity { get; }
int Size { get; }
T? Get();
T? Get(int index);
void Add(T value);
void Clear();
}
file class BoundedBuffer<T>(int capacity) : IBuffer<T>
{
private readonly LinkedList<T> _buffer = [];
public int Capacity { get; } = capacity;
public int Size => _buffer.Count;
public T? Get()
{
var head = _buffer.First;
return head is null ? default : head.Value;
}
public T? Get(int index)
{
var node = _buffer.First;
if (node is null) return default;
for (var i = 0; i < index; i++)
{
node = node.Next;
if (node is null) return default;
}
return node.Value;
}
public void Add(T value)
{
_buffer.AddFirst(value);
if (Size > Capacity)
{
_buffer.RemoveLast();
}
}
public void Clear() => _buffer.Clear();
public IEnumerator<T> GetEnumerator() => _buffer.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_buffer).GetEnumerator();
}

6
src/Disposer.cs Normal file
View File

@@ -0,0 +1,6 @@
namespace CruiseControllerMX5;
public sealed class Disposer(Action callback) : IDisposable
{
public void Dispose() => callback();
}

296
src/DrawContext.cs Normal file
View File

@@ -0,0 +1,296 @@
using System.Drawing.Drawing2D;
namespace CruiseControllerMX5;
public sealed class DrawContext(Graphics g) : IDisposable
{
private readonly Stack<Context> _contexts = new();
// private Font font = (Font)SystemFonts.DefaultFont.Clone();
private EllipseMode _ellipseMode = EllipseMode.Default;
private RectMode _rectMode = RectMode.Default;
private bool _noStroke, _noFill;
public Graphics Graphics { get; } = g;
public Pen DrawPen { get; private set; } = new(Color.White)
{
StartCap = LineCap.Round,
EndCap = LineCap.Round,
DashStyle = DashStyle.Solid,
DashCap = DashCap.Round
};
public Brush FillBrush { get; private set; } = new SolidBrush(Color.White);
public StringFormat TextAlignment { get; private set; } = new();
public void Push()
{
_contexts.Push(new Context(Graphics.Save(), (Pen)DrawPen.Clone(), (Brush)FillBrush.Clone(),
(StringFormat)TextAlignment.Clone(), _ellipseMode, _rectMode, _noStroke, _noFill));
}
public void Pop()
{
if (_contexts.Count > 0)
{
var ctx = _contexts.Pop();
Graphics.Restore(ctx.GraphicsState);
DrawPen.Dispose();
DrawPen = ctx.DrawPen;
FillBrush.Dispose();
FillBrush = ctx.FillBrush;
TextAlignment.Dispose();
TextAlignment = ctx.StringFormat;
_noStroke = ctx.NoStroke;
_noFill = ctx.NoFill;
}
}
public void Stroke(int gray) => Stroke(gray, gray, gray);
public void Stroke(int alpha, int gray) => Stroke(alpha, gray, gray, gray);
public void Stroke(int red, int green, int blue) => Stroke(Color.FromArgb(red, green, blue));
public void Stroke(int alpha, int red, int green, int blue) => Stroke(Color.FromArgb(alpha, red, green, blue));
public void Stroke(Color color)
{
DrawPen.Color = color;
_noStroke = false;
}
public void StrokeWeight(float strokeWeight)
{
DrawPen.Width = strokeWeight;
}
public void StrokeCap(LineCap cap)
{
DrawPen.StartCap = cap;
DrawPen.EndCap = cap;
}
public void Fill(int gray) => Fill(gray, gray, gray);
public void Fill(int alpha, int gray) => Fill(alpha, gray, gray, gray);
public void Fill(int red, int green, int blue) => Fill(Color.FromArgb(red, green, blue));
public void Fill(int alpha, int red, int green, int blue) => Fill(Color.FromArgb(alpha, red, green, blue));
public void Fill(Color color)
{
FillBrush.Dispose();
FillBrush = new SolidBrush(color);
_noFill = false;
}
public void NoStroke()
{
_noStroke = true;
}
public void NoFill()
{
_noFill = true;
}
public void ResetDashes()
{
DrawPen.DashStyle = DashStyle.Solid;
}
public void SetDashes(float filled, float empty)
{
DrawPen.DashStyle = DashStyle.Custom;
DrawPen.DashPattern = [filled, empty];
}
public void ResetTextAlign() => SetTextAlign(TextHorizontalAlignment.Default, TextVerticalAlignment.Default);
public void SetTextAlign(TextHorizontalAlignment hAlign = TextHorizontalAlignment.Unspecified,
TextVerticalAlignment vAlign = TextVerticalAlignment.Unspecified)
{
if (hAlign is not TextHorizontalAlignment.Unspecified)
{
TextAlignment.Alignment = hAlign switch
{
TextHorizontalAlignment.Center => StringAlignment.Center,
TextHorizontalAlignment.Left => StringAlignment.Near,
TextHorizontalAlignment.Right => StringAlignment.Far,
_ => throw new ArgumentException("Invalid horizontal alignment", nameof(hAlign))
};
}
if (vAlign is not TextVerticalAlignment.Unspecified)
{
TextAlignment.LineAlignment = vAlign switch
{
TextVerticalAlignment.Center => StringAlignment.Center,
TextVerticalAlignment.Top => StringAlignment.Near,
TextVerticalAlignment.Bottom => StringAlignment.Far,
_ => throw new ArgumentException("Invalid vertical alignment", nameof(vAlign))
};
}
}
public void SetEllipseMode(EllipseMode mode)
{
_ellipseMode = mode;
}
public void SetRectMode(RectMode mode)
{
_rectMode = mode;
}
// public void Arc(float x, float y, float width, float height, float startAngle, float sweepAngle)
// {
// g.DrawArc(drawPen, RectangleExtensions.FromCenter((int)speedGaugeX, (int)speedGaugeY, speedGaugeRect.Width - 16, speedGaugeRect.Height - 16), speedGaugeStartDeg, speedGaugeEndDeg);
// }
// public void Rect(int x, int y, int width, int height)
// {
// if (!noFill)
// {
// g.FillRectangle(fillBrush, x, y, width, height);
// }
// if (!noStroke)
// {
// g.DrawRectangle(DrawPen, x, y, width, height);
// }
// }
// public void Rect(float x, float y, float width, float height)
// {
// if (!noFill)
// {
// g.FillRectangle(fillBrush, x, y, width, height);
// }
// if (!noStroke)
// {
// g.DrawRectangle(DrawPen, x, y, width, height);
// }
// }
// public void Circle(int x, int y, int width, int height)
// {
// if (!noFill)
// {
// g.FillEllipse(fillBrush, x, y, width, height);
// }
// if (!noStroke)
// {
// g.DrawEllipse(DrawPen, x, y, width, height);
// }
// }
// public void Circle(float x, float y, float width, float height)
// {
// if (!noFill)
// {
// g.FillRectangle(fillBrush, x, y, width, height);
// }
// if (!noStroke)
// {
// g.DrawRectangle(DrawPen, x, y, width, height);
// }
// }
#region IDisposable
private bool _disposedValue;
public void Dispose()
{
if (_disposedValue) return;
DrawPen.Dispose();
FillBrush.Dispose();
TextAlignment.Dispose();
_disposedValue = true;
}
#endregion
public enum TextHorizontalAlignment
{
Unspecified,
Center,
Left,
Right,
Default = Left
}
public enum TextVerticalAlignment
{
Unspecified,
Center,
Top,
Bottom,
Default = Center
}
public enum EllipseMode
{
Center,
Radius,
Corner,
Corners,
Default = Center
}
public enum RectMode
{
Center,
Radius,
Corner,
Corners,
Default = Corner
}
private class Context(
GraphicsState graphicsState,
Pen drawPen,
Brush fillBrush,
StringFormat sf,
EllipseMode ellipseMode,
RectMode rectMode,
bool noStroke,
bool noFill) : IDisposable
{
public GraphicsState GraphicsState { get; } = graphicsState;
public Pen DrawPen { get; } = drawPen;
public Brush FillBrush { get; } = fillBrush;
public StringFormat StringFormat { get; } = sf;
public bool NoStroke { get; } = noStroke;
public bool NoFill { get; } = noFill;
public EllipseMode EllipseMode { get; } = ellipseMode;
public RectMode RectMode { get; } = rectMode;
private bool _disposedValue;
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
DrawPen.Dispose();
FillBrush.Dispose();
StringFormat.Dispose();
}
_disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

15
src/Listener.cs Normal file
View File

@@ -0,0 +1,15 @@
namespace CruiseControllerMX5;
public static class Listener
{
public static Listener<T> Create<T>(Action<T> callback) => new(callback);
}
public sealed class Listener<T>(Action<T> callback) : IObserver<T>
{
public void OnCompleted() { }
public void OnError(Exception error) { }
public void OnNext(T value) => callback(value);
}

16
src/Program.cs Normal file
View File

@@ -0,0 +1,16 @@
namespace CruiseControllerMX5;
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
Application.Run(new App());
}
}

View File

@@ -0,0 +1,60 @@
namespace CruiseControllerMX5;
public static class RectangleExtensions
{
public static (Rectangle left, Rectangle right) SplitVerticallyAt(this Rectangle rect, int x) => (
rect with { Width = x - rect.Left }, rect with { X = x, Width = rect.Right - x });
public static (Rectangle left, Rectangle right) SplitHorizontallyAt(this Rectangle rect, int y) => (
rect with { Height = y - rect.Bottom }, rect with { Y = y, Height = rect.Bottom - y });
public static (Rectangle left, Rectangle right) SplitVertically(this Rectangle rect, int w) => (
rect with { Width = w }, rect with { X = rect.X + w, Width = rect.Width - w });
public static (Rectangle left, Rectangle right) SplitHorizontally(this Rectangle rect, int h) => (
rect with { Height = h }, rect with { Y = rect.Y + h, Height = rect.Height - h });
public static Rectangle[] SplitVerticallyInto(this Rectangle rect, int n)
{
var result = new Rectangle[n];
SplitVerticallyInto(rect, result);
return result;
}
public static void SplitVerticallyInto(this Rectangle rect, Span<Rectangle> rects)
{
var n = rects.Length;
var w = (float)rect.Width / n;
for (var i = 0; i < n; i++)
{
rects[i] = rect with { X = (int)(i * w), Width = (int)w };
}
}
public static Rectangle[] SplitHorizontallyInto(this Rectangle rect, int n)
{
var result = new Rectangle[n];
SplitHorizontallyInto(rect, result);
return result;
}
public static void SplitHorizontallyInto(this Rectangle rect, Span<Rectangle> rects)
{
var n = rects.Length;
var h = (float)rect.Height / n;
for (var i = 0; i < n; i++)
{
rects[i] = rect with { Y = (int)(i * h), Height = (int)h };
}
}
public static PointF GetCenter(this Rectangle rect) => new(rect.X + rect.Width / 2F, rect.Y + rect.Height / 2F);
public static Rectangle Centered(Point center, int width, int height) =>
Centered(center.X, center.Y, width, height);
public static Rectangle Centered(int centerX, int centerY, int width, int height) =>
new(centerX - width / 2, centerY - height / 2, width, height);
}

View File

@@ -0,0 +1,71 @@
namespace CruiseControllerMX5;
public static class RectangleFExtensions
{
public static (RectangleF left, RectangleF right) SplitVerticallyAt(this RectangleF rect, int x) => (
rect with { Width = x - rect.Left }, rect with { X = x, Width = rect.Right - x });
public static (RectangleF left, RectangleF right) SplitHorizontallyAt(this RectangleF rect, int y) => (
rect with { Height = y - rect.Bottom }, rect with { Y = y, Height = rect.Bottom - y });
public static (RectangleF left, RectangleF right) SplitVertically(this RectangleF rect, int w) => (
rect with { Width = w }, rect with { X = rect.X + w, Width = rect.Width - w });
public static (RectangleF left, RectangleF right) SplitHorizontally(this RectangleF rect, int h) => (
rect with { Height = h }, rect with { Y = rect.Y + h, Height = rect.Height - h });
public static RectangleF[] SplitVerticallyInto(this RectangleF rect, int n)
{
var result = new RectangleF[n];
SplitVerticallyInto(rect, result);
return result;
}
public static void SplitVerticallyInto(this RectangleF rect, Span<RectangleF> rects)
{
var n = rects.Length;
var w = rect.Width / n;
for (var i = 0; i < n; i++)
{
rects[i] = rect with { X = i * w, Width = w };
}
}
public static RectangleF[] SplitHorizontallyInto(this RectangleF rect, int n)
{
var result = new RectangleF[n];
SplitHorizontallyInto(rect, result);
return result;
}
public static void SplitHorizontallyInto(this RectangleF rect, Span<RectangleF> rects)
{
var n = rects.Length;
var h = rect.Height / n;
for (var i = 0; i < n; i++)
{
rects[i] = rect with { Y = i * h, Height = h };
}
}
public static float GetCenterX(this RectangleF rect) => rect.X + rect.Width / 2F;
public static float GetCenterY(this RectangleF rect) => rect.Y + rect.Height / 2F;
public static PointF GetCenter(this RectangleF rect) => new(GetCenterX(rect), GetCenterY(rect));
public static RectangleF Centered(PointF center, float width, float height) =>
Centered(center.X, center.Y, width, height);
public static RectangleF Centered(float centerX, float centerY, float width, float height) =>
new(centerX - width / 2, centerY - height / 2, width, height);
public static RectangleF Scaled(this RectangleF rect, float scale) => Scaled(rect, scale, scale);
public static RectangleF Scaled(this RectangleF rect, SizeF scale) => Scaled(rect, scale.Width, scale.Height);
public static RectangleF Scaled(this RectangleF rect, float scaleX, float scaleY) => new(rect.X * scaleX,
rect.Y * scaleY, rect.Width * scaleX, rect.Height * scaleY);
public static Rectangle Round(this RectangleF rect) =>
Rectangle.FromLTRB((int)rect.Left, (int)rect.Top, (int)rect.Right, (int)rect.Bottom);
}

33
src/Sample.cs Normal file
View File

@@ -0,0 +1,33 @@
namespace CruiseControllerMX5;
public class Sample(long timestamp, bool drawTimeTick)
{
public long Timestamp { get; private set; } = timestamp;
public bool DrawTimeTick { get; private set; } = drawTimeTick;
public float Proportional { get; set; }
public float Integral { get; set; }
public float Derivative { get; set; }
public float Speed { get; set; }
public float Acceleration { get; set; }
public float Consigne { get; set; }
public float Error { get; set; }
public int Servo { get; set; }
public int ServoMin { get; set; }
public int ServoMax { get; set; }
public int ServoNeutre { get; set; }
public bool Active { get; set; }
public float Kp { get; set; }
public float Ki { get; set; }
public float Kd { get; set; }
public int SpeedMin { get; set; }
public int SpeedMax { get; set; }
public bool BrakeFlag { get; set; }
public Sample Copy(long timestamp, bool drawTimeTick)
{
var copy = (Sample)MemberwiseClone();
copy.Timestamp = timestamp;
copy.DrawTimeTick = drawTimeTick;
return copy;
}
}

38
src/Splitter.cs Normal file
View File

@@ -0,0 +1,38 @@
using System.Globalization;
using System.Numerics;
namespace CruiseControllerMX5;
public class Splitter(string str)
{
private const char Delimiter = ',';
public delegate T ValueReader<out T>(ReadOnlySpan<char> s);
private int _pos = 0, _charCount = 0, _nextPos = 0;
private 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;
}
private bool HasNext => _pos < str.Length;
private 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));
public TParsable Read<TParsable>() where TParsable: ISpanParsable<TParsable> => Read(static s => TParsable.Parse(s, provider: CultureInfo.InvariantCulture));
}

19
src/Utils.cs Normal file
View File

@@ -0,0 +1,19 @@
using System.Numerics;
namespace CruiseControllerMX5;
public static class Utils
{
public static TTo Map<TFrom, TTo>(TFrom value, TFrom fromMin, TFrom fromMax, TTo toMin, TTo toMax)
where TFrom : INumber<TFrom> where TTo : INumber<TTo> =>
Map<TFrom, TTo, double>(value, fromMin, fromMax, toMin, toMax);
public static TTo Map<TFrom, TTo, TInt>(TFrom value, TFrom fromMin, TFrom fromMax, TTo toMin, TTo toMax)
where TFrom : INumber<TFrom> where TTo : INumber<TTo> where TInt : INumber<TInt> => TTo.CreateChecked(
TInt.CreateChecked(value - fromMin) / TInt.CreateChecked(fromMax - fromMin) *
TInt.CreateChecked(toMax - toMin) + TInt.CreateChecked(toMin));
public static bool IntToBool(string value) => value.Equals("1", StringComparison.Ordinal);
public static int BoolToInt(bool value) => value ? 1 : 0;
}