From 86f4edaa558ff4db45330d63088c93c6679102d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20BECHER?= Date: Sun, 5 Jan 2025 22:56:30 +0100 Subject: [PATCH] Initial commit --- .gitignore | 6 + CruiseControllerMX5.csproj | 21 + CruiseControllerMX5.csproj.user | 4 + CruiseControllerMX5.sln | 25 + src/App.Designer.cs | 838 ++++++++++++++++++++++ src/App.cs | 1196 +++++++++++++++++++++++++++++++ src/App.resx | 64 ++ src/ArduinoCom.cs | 502 +++++++++++++ src/Buffer.cs | 73 ++ src/Disposer.cs | 6 + src/DrawContext.cs | 296 ++++++++ src/Listener.cs | 15 + src/Program.cs | 16 + src/RectangleExtensions.cs | 60 ++ src/RectangleFExtensions.cs | 71 ++ src/Sample.cs | 33 + src/Splitter.cs | 38 + src/Utils.cs | 19 + 18 files changed, 3283 insertions(+) create mode 100644 .gitignore create mode 100644 CruiseControllerMX5.csproj create mode 100644 CruiseControllerMX5.csproj.user create mode 100644 CruiseControllerMX5.sln create mode 100644 src/App.Designer.cs create mode 100644 src/App.cs create mode 100644 src/App.resx create mode 100644 src/ArduinoCom.cs create mode 100644 src/Buffer.cs create mode 100644 src/Disposer.cs create mode 100644 src/DrawContext.cs create mode 100644 src/Listener.cs create mode 100644 src/Program.cs create mode 100644 src/RectangleExtensions.cs create mode 100644 src/RectangleFExtensions.cs create mode 100644 src/Sample.cs create mode 100644 src/Splitter.cs create mode 100644 src/Utils.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aad36c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ +/.idea/ \ No newline at end of file diff --git a/CruiseControllerMX5.csproj b/CruiseControllerMX5.csproj new file mode 100644 index 0000000..9de3152 --- /dev/null +++ b/CruiseControllerMX5.csproj @@ -0,0 +1,21 @@ + + + + WinExe + net9.0-windows + enable + true + enable + true + false + PerMonitorV2 + default + + + + + + + + + \ No newline at end of file diff --git a/CruiseControllerMX5.csproj.user b/CruiseControllerMX5.csproj.user new file mode 100644 index 0000000..0295687 --- /dev/null +++ b/CruiseControllerMX5.csproj.user @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/CruiseControllerMX5.sln b/CruiseControllerMX5.sln new file mode 100644 index 0000000..e0701c3 --- /dev/null +++ b/CruiseControllerMX5.sln @@ -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 diff --git a/src/App.Designer.cs b/src/App.Designer.cs new file mode 100644 index 0000000..44d2618 --- /dev/null +++ b/src/App.Designer.cs @@ -0,0 +1,838 @@ +using System.ComponentModel; + +namespace CruiseControllerMX5; + +partial class App +{ + /// + /// Required designer variable. + /// + private IContainer components = null; + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + 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 +} \ No newline at end of file diff --git a/src/App.cs b/src/App.cs new file mode 100644 index 0000000..146681e --- /dev/null +++ b/src/App.cs @@ -0,0 +1,1196 @@ +using System.Drawing.Drawing2D; +using System.Globalization; +using System.IO.Ports; +using System.Runtime.InteropServices; + +namespace CruiseControllerMX5; + +public partial class App : Form +{ + private const int SpeedStart = 0, SpeedEnd = 240; + private const int MinSampleSize = 50, MaxSampleSize = 600; + private const float MinTimeSpan = 5F, MaxTimeSpan = 60F; + + private const double RefreshRateMs = 1000.0 / 60.0; + + private static readonly Color StatusGrayColor = Color.FromArgb(70, 70, 70), GreenColor = Color.FromArgb(0, 255, 0); + + private readonly IBuffer _samples = Buffer.CreateBounded(MaxSampleSize); + private readonly Queue _sampleQueue = new(); + private readonly long _timestampStart = TimeProvider.System.GetTimestamp(); + private IArduinoCom? _arduinoCom; + private Sample? _lastDrawnSample; + private long _frameTimeStart; + private const int _sampleSize = 50; + private readonly TimeSpan _graphTimeSpan = TimeSpan.FromSeconds(10); + + private bool _autoScaleError = false, + _autoScaleIntegral = true, + _autoScaleDerivative = true, + _autoScaleSpeed = false, + _autoScaleAcceleration = true, + _autoScaleServo = true; + + private float? _errorRangeMin = -3F, + _errorRangeMax = 3F, + _integralRangeMin = null, + _integralRangeMax = null, + _derivativeRangeMin = null, + _derivativeRangeMax = null, + _speedRangeMin = 0F, + _speedRangeMax = 150F, + _accelerationRangeMin = null, + _accelerationRangeMax = null; + + private int? _servoRangeMin = 1170, _servoRangeMax = 1970; + private bool _sampleOrTimeGraph = false; + private bool _drawSmoothCurves = false; + + private readonly Font _font12, _font14, _font16; + private readonly Font _font14Bold; + + private readonly System.Timers.Timer _timer; + + public App() + { + _font12 = new Font("Consolas", 12); + _font14 = new Font(_font12.FontFamily, 14); + _font16 = new Font(_font12.FontFamily, 16); + _font14Bold = new Font(_font14, FontStyle.Bold); + + InitializeComponent(); + + var timer = new System.Timers.Timer(RefreshRateMs); + timer.SynchronizingObject = this; + timer.Elapsed += delegate { OnDrawTimer(); }; + _timer = timer; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + components?.Dispose(); + _timer.Stop(); + _arduinoCom?.Dispose(); + _font12.Dispose(); + _font14.Dispose(); + _font16.Dispose(); + _font14Bold.Dispose(); + } + + base.Dispose(disposing); + } + + // double buffering low level property + // protected override CreateParams CreateParams + // { + // get + // { + // var cp = base.CreateParams; + // cp.ExStyle |= 0x02000000; // double buffering shit + // return cp; + // } + // } + + private void App_Load(object sender, EventArgs e) + { + _timer.Start(); + + _arduinoCom = new TestArduinoCom(); + _arduinoCom.Open(); + _arduinoCom.AddSerialObserver(Listener.Create(_sampleQueue.Enqueue)); + } + + private void InitializeArduino() + { + var arduinoCom = new ArduinoCom(new SerialPort("COM4", 921600) + { + NewLine = "\n" + }); + + try + { + arduinoCom.Open(); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + + _arduinoCom = arduinoCom; + arduinoCom.AddSerialObserver(Listener.Create(sample => _sampleQueue.Enqueue(sample))); + } + + + private PointF CalculateGraphPosition(Rectangle graphRect, float graphMargin, int i, int sampleSize, float value, + float minValue, float maxValue, long timestamp, long minTimestamp, long maxTimestamp) => + CalculateGraphPosition(new RectangleF(graphRect.X, graphRect.Y, graphRect.Width, graphRect.Height), graphMargin, + i, sampleSize, value, minValue, maxValue, timestamp, minTimestamp, maxTimestamp); + + private PointF CalculateGraphPosition(RectangleF graphRect, float graphMargin, int i, int sampleSize, float value, + float minValue, float maxValue, long timestamp, long minTimestamp, long maxTimestamp) + { + var x = _sampleOrTimeGraph + ? Utils.Map(i, 0, sampleSize - 1, graphRect.Width, 0F) + : Utils.Map(timestamp, minTimestamp, maxTimestamp, 0F, graphRect.Width); + var y = minValue == maxValue + ? graphRect.Height / 2F + : Utils.Map(value, minValue, maxValue, graphRect.Height - graphMargin, graphMargin); + + return new PointF(x, y); + } + + private void OnDrawTimer() + { + while (_sampleQueue.Count > 0) + { + _samples.Add(_sampleQueue.Dequeue()); + } + + _frameTimeStart = TimeProvider.System.GetTimestamp(); + + speedGaugeCanvas.Refresh(); + graphPanel.Refresh(); + + var sample = _samples.Get(); + + if (sample == null) + { + if (_lastDrawnSample != null) + { + kpCurrentField.ResetText(); + kiCurrentField.ResetText(); + kdCurrentField.ResetText(); + errorField.ResetText(); + proportionalField.ResetText(); + integralField.ResetText(); + derivativeField.ResetText(); + speedField.ResetText(); + accelerationField.ResetText(); + servoField.ResetText(); + servoMinField.ResetText(); + servoMaxField.ResetText(); + servoNeutralField.ResetText(); + } + } + else + { + if (sample.Active) + { + if (_lastDrawnSample is { Active: true }) + { + if (sample.Proportional != _lastDrawnSample.Proportional) + proportionalField.Text = sample.Proportional.ToString(CultureInfo.InvariantCulture); + if (sample.Integral != _lastDrawnSample.Integral) + integralField.Text = sample.Integral.ToString(CultureInfo.InvariantCulture); + if (sample.Derivative != _lastDrawnSample.Derivative) + derivativeField.Text = sample.Derivative.ToString(CultureInfo.InvariantCulture); + } + else + { + proportionalField.Text = sample.Proportional.ToString(CultureInfo.InvariantCulture); + integralField.Text = sample.Integral.ToString(CultureInfo.InvariantCulture); + derivativeField.Text = sample.Derivative.ToString(CultureInfo.InvariantCulture); + } + } + else + { + proportionalField.ResetText(); + integralField.ResetText(); + derivativeField.ResetText(); + } + + if (_lastDrawnSample != null) + { + if (sample.Kp != _lastDrawnSample.Kp) + kpCurrentField.Text = sample.Kp.ToString(CultureInfo.InvariantCulture); + if (sample.Ki != _lastDrawnSample.Ki) + kiCurrentField.Text = sample.Ki.ToString(CultureInfo.InvariantCulture); + if (sample.Kd != _lastDrawnSample.Kd) + kdCurrentField.Text = sample.Kd.ToString(CultureInfo.InvariantCulture); + if (sample.Error != _lastDrawnSample.Error) + errorField.Text = sample.Error.ToString(CultureInfo.InvariantCulture); + if (sample.Speed != _lastDrawnSample.Speed) + speedField.Text = sample.Speed.ToString(CultureInfo.InvariantCulture); + if (sample.Acceleration != _lastDrawnSample.Acceleration) + accelerationField.Text = sample.Acceleration.ToString(CultureInfo.InvariantCulture); + if (sample.Servo != _lastDrawnSample.Servo) + servoField.Text = sample.Servo.ToString(CultureInfo.InvariantCulture); + if (sample.ServoMin != _lastDrawnSample.ServoMin) + servoMinField.Text = sample.ServoMin.ToString(CultureInfo.InvariantCulture); + if (sample.ServoMax != _lastDrawnSample.ServoMax) + servoMaxField.Text = sample.ServoMax.ToString(CultureInfo.InvariantCulture); + if (sample.ServoNeutre != _lastDrawnSample.ServoNeutre) + servoNeutralField.Text = sample.ServoNeutre.ToString(CultureInfo.InvariantCulture); + } + else + { + kpCurrentField.Text = sample.Kp.ToString(CultureInfo.InvariantCulture); + kiCurrentField.Text = sample.Ki.ToString(CultureInfo.InvariantCulture); + kdCurrentField.Text = sample.Kd.ToString(CultureInfo.InvariantCulture); + errorField.Text = sample.Error.ToString(CultureInfo.InvariantCulture); + speedField.Text = sample.Speed.ToString(CultureInfo.InvariantCulture); + accelerationField.Text = sample.Acceleration.ToString(CultureInfo.InvariantCulture); + servoField.Text = sample.Servo.ToString(CultureInfo.InvariantCulture); + servoMinField.Text = sample.ServoMin.ToString(CultureInfo.InvariantCulture); + servoMaxField.Text = sample.ServoMax.ToString(CultureInfo.InvariantCulture); + servoNeutralField.Text = sample.ServoNeutre.ToString(CultureInfo.InvariantCulture); + } + } + + (stateLabel.Text, stateLabel.BackColor) = sample != null + ? sample.Active ? ("Régulation", Color.Green) : ("Standby", Color.DarkGoldenrod) + : ("Déconnecté / OFF", StatusGrayColor); + + brakeLabel.BackColor = sample is { BrakeFlag: true } ? Color.DarkRed : StatusGrayColor; + + _lastDrawnSample = sample; + } + + private void speedGaugePanel_Paint(object sender, PaintEventArgs e) + { + var g = e.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + g.PixelOffsetMode = PixelOffsetMode.HighSpeed; + g.PageScale = DeviceDpi / 96F; + g.PageUnit = GraphicsUnit.Pixel; + + using var ctx = new DrawContext(g); + + DrawSpeedGauge(ctx); + } + + private void graphCanvas_Paint(object sender, PaintEventArgs e) + { + var g = e.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + g.PixelOffsetMode = PixelOffsetMode.HighSpeed; + g.PageScale = DeviceDpi / 96F; + g.PageUnit = GraphicsUnit.Pixel; + + using var ctx = new DrawContext(g); + + DrawGraph(ctx); + } + + private void DrawSpeedGauge(DrawContext ctx) + { + var dpiScale = DeviceDpi / 96F; + var speedGaugeRect = speedGaugeCanvas.ClientRectangle; + speedGaugeRect.Width = (int)(speedGaugeRect.Width / dpiScale); + speedGaugeRect.Height = (int)(speedGaugeRect.Height / dpiScale); + speedGaugeRect.Inflate(-16, -16); + + // squareify + if (speedGaugeRect.Width != speedGaugeRect.Height) + { + if (speedGaugeRect.Width > speedGaugeRect.Height) + { + speedGaugeRect.X += (speedGaugeRect.Width - speedGaugeRect.Height) / 2; + speedGaugeRect.Width = speedGaugeRect.Height; + } + else + { + speedGaugeRect.Y += (speedGaugeRect.Height - speedGaugeRect.Width) / 2; + speedGaugeRect.Height = speedGaugeRect.Width; + } + } + + var diameter = speedGaugeRect.Width; + var radius = diameter / 2F; + var center = speedGaugeRect.GetCenter(); + var centerX = center.X; + var centerY = center.Y; + const float startAngleRad = MathF.PI * 0.75F; + const float endAngleRad = MathF.PI * 2.25F; + const float startAngleDeg = 135F; + const float sweepAngleDeg = 270F; + float speed = SpeedStart; + + var g = ctx.Graphics; + + var sample = _samples.Get(); + + if (sample != null) + { + speed = float.Clamp(sample.Speed, SpeedStart, SpeedEnd); + + ctx.Stroke(159, 0, 0); + ctx.StrokeWeight(8); + ctx.StrokeCap(LineCap.Flat); + + if (sample.SpeedMin > SpeedStart) + { + g.DrawArc(ctx.DrawPen, RectangleFExtensions.Centered(centerX, centerY, diameter - 8F, diameter - 8F), + startAngleDeg, + Utils.Map(sample.SpeedMin, SpeedStart, SpeedEnd, 0F, sweepAngleDeg)); + } + + if (sample.SpeedMax < SpeedEnd) + { + g.DrawArc(ctx.DrawPen, RectangleFExtensions.Centered(centerX, centerY, diameter - 8F, diameter - 8F), + 45, Utils.Map(sample.SpeedMax, SpeedStart, SpeedEnd, -sweepAngleDeg, 0F)); + } + + ctx.StrokeCap(LineCap.Round); + } + + ctx.Stroke(127, 180, 127); + ctx.StrokeWeight(1); + + g.DrawArc(ctx.DrawPen, + RectangleFExtensions.Centered(centerX, centerY, speedGaugeRect.Width - 16, speedGaugeRect.Height - 16), + startAngleDeg, sweepAngleDeg); + + for (int i = SpeedStart, j = 0; i <= SpeedEnd; i += 10, j++) + { + var a = Utils.Map(i, SpeedStart, SpeedEnd, startAngleRad, endAngleRad); + var bigTick = j % 3 == 0; + var cosA = MathF.Cos(a); + var sinA = MathF.Sin(a); + + var tickX = centerX + radius * cosA; + var tickY = centerY + radius * sinA; + var tickX2 = centerX + (radius - (bigTick ? 15 : 8)) * cosA; + var tickY2 = centerY + (radius - (bigTick ? 15 : 8)) * sinA; + + if (bigTick) + { + ctx.Stroke(255, 127, 0); + ctx.StrokeWeight(3); + } + else + { + ctx.Stroke(127, 180, 127); + ctx.StrokeWeight(1); + } + + g.DrawLine(ctx.DrawPen, tickX, tickY, tickX2, tickY2); + + if (bigTick) + { + ctx.SetTextAlign(DrawContext.TextHorizontalAlignment.Center, DrawContext.TextVerticalAlignment.Center); + + g.DrawString(i.ToString(), _font12, Brushes.White, centerX + (radius - 34) * cosA, + centerY + (radius - 34) * sinA, ctx.TextAlignment); + } + } + + ctx.Stroke(127, 180, 127); + ctx.StrokeWeight(3); + g.DrawArc(ctx.DrawPen, RectangleFExtensions.Centered(center, speedGaugeRect.Width, speedGaugeRect.Height), + startAngleDeg, sweepAngleDeg); + + g.DrawString("KPH", _font16, Brushes.White, centerX, centerY - 40, ctx.TextAlignment); + + ctx.Fill(31); + g.FillRectangle(ctx.FillBrush, RectangleFExtensions.Centered(centerX, centerY + 60, 66, 26)); + ctx.Stroke(200); + ctx.StrokeWeight(1); + g.DrawRectangle(ctx.DrawPen, RectangleFExtensions.Centered(centerX, centerY + 60, 66, 26)); + + g.DrawString(speed.ToString("N2", CultureInfo.InvariantCulture), _font12, Brushes.White, centerX, centerY + 60, + ctx.TextAlignment); + + var speedAngle = Utils.Map(speed, SpeedStart, SpeedEnd, startAngleRad, endAngleRad); + ctx.Stroke(255, 127, 0); + ctx.StrokeWeight(4); + g.DrawLine(ctx.DrawPen, centerX - 20 * MathF.Cos(speedAngle), centerY - 20 * MathF.Sin(speedAngle), + centerX + (radius - 20) * MathF.Cos(speedAngle), centerY + (radius - 20) * MathF.Sin(speedAngle)); + + ctx.Fill(127); + g.FillEllipse(ctx.FillBrush, RectangleFExtensions.Centered(centerX, centerY, 20, 20)); + ctx.Stroke(BackColor); + ctx.StrokeWeight(4); + g.DrawEllipse(ctx.DrawPen, RectangleFExtensions.Centered(centerX, centerY, 20, 20)); + + if (sample == null) + { + return; + } + + var consigne = sample.Consigne; + + if (consigne == 0) + { + return; + } + + var consigneAngle = Utils.Map(float.Clamp(consigne, SpeedStart, SpeedEnd), SpeedStart, + SpeedEnd, startAngleDeg, + startAngleDeg + sweepAngleDeg); + var consigneColor = sample.Active ? GreenColor : Color.Cyan; + + ctx.Push(); + g.TranslateTransform(centerX, centerY); + g.RotateTransform(consigneAngle); + g.TranslateTransform(radius, 0); + ctx.Fill(consigneColor); + g.FillPolygon(ctx.FillBrush, new Point(4, 0), new Point(12, 8), new Point(12, -8)); + ctx.Stroke(consigneColor); + ctx.StrokeWeight(3); + ctx.StrokeCap(LineCap.Flat); + g.DrawLine(ctx.DrawPen, 8, 0, -8, 0); + ctx.StrokeCap(LineCap.Round); + ctx.Pop(); + + ctx.Stroke(consigneColor); + ctx.StrokeWeight(1); + ctx.Fill(31); + g.FillRectangle(ctx.FillBrush, RectangleFExtensions.Centered(centerX, centerY + 95, 66, 26)); + g.DrawRectangle(ctx.DrawPen, RectangleFExtensions.Centered(centerX, centerY + 95, 66, 26)); + + ctx.Fill(consigneColor); + g.DrawString(consigne.ToString("N2", CultureInfo.InvariantCulture), _font12, ctx.FillBrush, centerX, + centerY + 95, ctx.TextAlignment); + } + + private void DrawGraph(DrawContext ctx) + { + var dpiScale = DeviceDpi / 96F; + var graphRect = graphCanvas.ClientRectangle; + graphRect.Width = (int)(graphRect.Width / dpiScale); + graphRect.Height = (int)(graphRect.Height / dpiScale); + graphRect.Inflate(-8, -8); + graphRect.Width -= 16; + + var g = ctx.Graphics; + + ctx.Push(); + + var maxTimestamp = _frameTimeStart; + var minTimestamp = maxTimestamp - _graphTimeSpan.Ticks; + + int sampleSize; + if (_sampleOrTimeGraph) + { + sampleSize = _sampleSize; + } + else + { + var i = 0; + + foreach (var sample in _samples) + { + i++; + + if (sample.Timestamp < minTimestamp) + { + break; + } + } + + sampleSize = i; + } + + g.TranslateTransform(graphRect.Left, graphRect.Top); + + const int graphMargin = 24; + + ctx.Stroke(100); + ctx.StrokeWeight(1); + ctx.SetDashes(5, 5); + g.DrawLine(ctx.DrawPen, 0, graphMargin - 1, graphRect.Width, graphMargin - 1); + g.DrawLine(ctx.DrawPen, 0, graphRect.Height - graphMargin + 1, graphRect.Width, + graphRect.Height - graphMargin + 1); + + ctx.SetDashes(3, 7); + + if (_sampleOrTimeGraph) + { + var i = 0; + + foreach (var sample in _samples.Take(sampleSize)) + { + i++; + + if (sample.DrawTimeTick) + { + var tickX = Utils.Map(i, 0, sampleSize, graphRect.Width, 0); + + g.DrawLine(ctx.DrawPen, tickX, graphMargin, tickX, graphRect.Height - graphMargin); + } + } + } + else + { + var prevT = TimeProvider.System.GetElapsedTime(_timestampStart, + Utils.Map(-1, 0, graphRect.Width - 1, minTimestamp, maxTimestamp)); + + for (var i = 0; i < graphRect.Width; i++) + { + var t = TimeProvider.System.GetElapsedTime(_timestampStart, + Utils.Map(i, 0, graphRect.Width - 1, minTimestamp, maxTimestamp)); + + if ((int)Math.Floor(t.TotalSeconds) != (int)Math.Floor(prevT.TotalSeconds)) + { + g.DrawLine(ctx.DrawPen, i, graphMargin, i, graphRect.Height - graphMargin); + } + + prevT = t; + } + } + + ctx.ResetDashes(); + + var points = new List(); + + // ERREUR + + if (errorToggle.Checked) + { + var autoScale = _autoScaleError || _errorRangeMin == _errorRangeMax; + var autoScaleMin = autoScale || !_errorRangeMin.HasValue; + var autoScaleMax = autoScale || !_errorRangeMax.HasValue; + + var minValue = autoScaleMin ? float.PositiveInfinity : _errorRangeMin!.Value; + var maxValue = autoScaleMax ? float.NegativeInfinity : _errorRangeMax!.Value; + var sampleCount = 0; + + foreach (var sample in _samples.Take(sampleSize)) + { + if (!sample.Active) + { + continue; + } + + sampleCount++; + + var value = sample.Error; + + if (autoScaleMin && value < minValue) + { + minValue = value; + } + + if (autoScaleMax && value > maxValue) + { + maxValue = value; + } + } + + if (sampleCount > 0) + { + g.SetClip(new RectangleF(0, graphMargin, graphRect.Width, graphRect.Height - 2 * graphMargin + 1)); + + if (minValue <= 0 && maxValue >= 0) + { + var zero = minValue == maxValue + ? graphRect.Height / 2F + : Utils.Map(0, minValue, maxValue, graphRect.Height - graphMargin, + graphMargin); + + ctx.Stroke(63, 63, 127); + ctx.StrokeWeight(1); + ctx.SetDashes(3, 3); + g.DrawLine(ctx.DrawPen, 0, zero, graphRect.Width, zero); + ctx.ResetDashes(); + } + + ctx.Stroke(127, 127, 255); + ctx.StrokeWeight(1); + + var i = 0; + + foreach (var sample in _samples.Take(sampleSize)) + { + i++; + + if (!sample.Active) + { + if (points.Count > 0) + { + if (points.Count > 1) + { + if (_drawSmoothCurves) + { + g.DrawCurve(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + else + { + g.DrawLines(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + } + + points.Clear(); + } + + continue; + } + + var p = CalculateGraphPosition(graphRect, graphMargin, i, sampleSize, sample.Error, minValue, + maxValue, sample.Timestamp, minTimestamp, maxTimestamp); + + points.Add(p); + } + + if (points.Count > 0) + { + if (points.Count > 1) + { + if (_drawSmoothCurves) + { + g.DrawCurve(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + else + { + g.DrawLines(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + } + + points.Clear(); + } + + g.ResetClip(); + + ctx.SetTextAlign(DrawContext.TextHorizontalAlignment.Left, DrawContext.TextVerticalAlignment.Center); + ctx.Fill(127, 127, 255); + g.DrawString(maxValue.ToString("N3", CultureInfo.InvariantCulture), _font12, ctx.FillBrush, 4, + graphMargin / 2f, ctx.TextAlignment); + g.DrawString(minValue.ToString("N3", CultureInfo.InvariantCulture), _font12, ctx.FillBrush, 4, + graphRect.Height - graphMargin / 2f, ctx.TextAlignment); + ctx.ResetTextAlign(); + } + } + + // INTEGRAL + + if (integralToggle.Checked) + { + var autoScale = _autoScaleIntegral || _integralRangeMin == _integralRangeMax; + var autoScaleMin = autoScale || !_integralRangeMin.HasValue; + var autoScaleMax = autoScale || !_integralRangeMax.HasValue; + + var minValue = autoScaleMin ? float.PositiveInfinity : _integralRangeMin!.Value; + var maxValue = autoScaleMax ? float.NegativeInfinity : _integralRangeMax!.Value; + var sampleCount = 0; + + foreach (var sample in _samples.Take(sampleSize)) + { + if (!sample.Active) + { + continue; + } + + sampleCount++; + + var value = sample.Integral; + + if (autoScaleMin && value < minValue) + { + minValue = value; + } + + if (autoScaleMax && value > maxValue) + { + maxValue = value; + } + } + + if (sampleCount > 0) + { + g.SetClip(new RectangleF(0, graphMargin, graphRect.Width, graphRect.Height - 2 * graphMargin + 1)); + + ctx.Stroke(255, 255, 0); + ctx.StrokeWeight(1); + + var i = 0; + + foreach (var sample in _samples.Take(sampleSize)) + { + i++; + + if (!sample.Active) + { + if (points.Count > 0) + { + if (points.Count > 1) + { + if (_drawSmoothCurves) + { + g.DrawCurve(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + else + { + g.DrawLines(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + } + + points.Clear(); + } + + continue; + } + + var p = CalculateGraphPosition(graphRect, graphMargin, i, sampleSize, sample.Integral, minValue, + maxValue, + sample.Timestamp, minTimestamp, maxTimestamp); + + points.Add(p); + } + + if (points.Count > 0) + { + if (points.Count > 1) + { + if (_drawSmoothCurves) + { + g.DrawCurve(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + else + { + g.DrawLines(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + } + + points.Clear(); + } + + g.ResetClip(); + + ctx.SetTextAlign(DrawContext.TextHorizontalAlignment.Left, DrawContext.TextVerticalAlignment.Center); + ctx.Fill(255, 255, 0); + g.DrawString(maxValue.ToString("N3", CultureInfo.InvariantCulture), _font12, ctx.FillBrush, 90, + graphMargin / 2f, ctx.TextAlignment); + g.DrawString(minValue.ToString("N3", CultureInfo.InvariantCulture), _font12, ctx.FillBrush, 90, + graphRect.Height - graphMargin / 2f, ctx.TextAlignment); + ctx.ResetTextAlign(); + } + } + + // DERIVATIF + + if (derivativeToggle.Checked) + { + var autoScale = _autoScaleDerivative || _derivativeRangeMin == _derivativeRangeMax; + var autoScaleMin = autoScale || !_derivativeRangeMin.HasValue; + var autoScaleMax = autoScale || !_derivativeRangeMax.HasValue; + + var minValue = autoScaleMin ? float.PositiveInfinity : _derivativeRangeMin!.Value; + var maxValue = autoScaleMax ? float.NegativeInfinity : _derivativeRangeMax!.Value; + var sampleCount = 0; + + foreach (var sample in _samples.Take(sampleSize)) + { + if (!sample.Active) + { + continue; + } + + sampleCount++; + + var value = sample.Derivative; + + if (autoScaleMin && value < minValue) + { + minValue = value; + } + + if (autoScaleMax && value > maxValue) + { + maxValue = value; + } + } + + if (sampleCount > 0) + { + g.SetClip(new RectangleF(0, graphMargin, graphRect.Width, graphRect.Height - 2 * graphMargin + 1)); + + ctx.Stroke(255, 63, 63); + ctx.StrokeWeight(1); + + var i = 0; + + foreach (var sample in _samples.Take(sampleSize)) + { + i++; + + if (!sample.Active) + { + if (points.Count > 0) + { + if (points.Count > 1) + { + if (_drawSmoothCurves) + { + g.DrawCurve(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + else + { + g.DrawLines(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + } + + points.Clear(); + } + + continue; + } + + var p = CalculateGraphPosition(graphRect, graphMargin, i, sampleSize, sample.Derivative, minValue, + maxValue, + sample.Timestamp, minTimestamp, maxTimestamp); + + points.Add(p); + } + + if (points.Count > 0) + { + if (points.Count > 1) + { + if (_drawSmoothCurves) + { + g.DrawCurve(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + else + { + g.DrawLines(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + } + + points.Clear(); + } + + g.ResetClip(); + + ctx.SetTextAlign(DrawContext.TextHorizontalAlignment.Left, DrawContext.TextVerticalAlignment.Center); + ctx.Fill(255, 63, 63); + g.DrawString(maxValue.ToString("N3", CultureInfo.InvariantCulture), _font12, ctx.FillBrush, 176, + graphMargin / 2f, ctx.TextAlignment); + g.DrawString(minValue.ToString("N3", CultureInfo.InvariantCulture), _font12, ctx.FillBrush, 176, + graphRect.Height - graphMargin / 2f, ctx.TextAlignment); + ctx.ResetTextAlign(); + } + } + + // VITESSE + + if (speedToggle.Checked) + { + var autoScale = _autoScaleSpeed || _speedRangeMin == _speedRangeMax; + var autoScaleMin = autoScale || !_speedRangeMin.HasValue; + var autoScaleMax = autoScale || !_speedRangeMax.HasValue; + + var minValue = autoScaleMin ? float.PositiveInfinity : _speedRangeMin!.Value; + var maxValue = autoScaleMax ? float.NegativeInfinity : _speedRangeMax!.Value; + var sampleCount = 0; + + foreach (var sample in _samples.Take(sampleSize)) + { + sampleCount++; + + var value = sample.Speed; + + if (autoScaleMin && value < minValue) + { + minValue = value; + } + + if (autoScaleMax && value > maxValue) + { + maxValue = value; + } + } + + if (sampleCount > 0) + { + g.SetClip(new RectangleF(0, graphMargin, graphRect.Width, graphRect.Height - 2 * graphMargin + 1)); + + // CONSIGNE + + ctx.Stroke(63, 127, 127); + ctx.StrokeWeight(1); + ctx.SetDashes(3, 3); + + var i = 0; + + foreach (var sample in _samples.Take(sampleSize)) + { + i++; + + // if (!sample.Active) + // { + // if (points.Count > 0) + // { + // if (points.Count > 1) + // { + // g.DrawLines(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + // } + // + // points.Clear(); + // } + // + // continue; + // } + + var p = CalculateGraphPosition(graphRect, graphMargin, i, sampleSize, sample.Consigne, minValue, + maxValue, + sample.Timestamp, minTimestamp, maxTimestamp); + + points.Add(p); + } + + if (points.Count > 0) + { + if (points.Count > 1) + { + g.DrawLines(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + + points.Clear(); + } + + ctx.ResetDashes(); + + // VITESSE + + ctx.Stroke(127, 255, 255); + ctx.StrokeWeight(1); + + i = 0; + + foreach (var sample in _samples.Take(sampleSize)) + { + i++; + + var p = CalculateGraphPosition(graphRect, graphMargin, i, sampleSize, sample.Speed, minValue, + maxValue, + sample.Timestamp, minTimestamp, maxTimestamp); + + points.Add(p); + } + + if (points.Count > 0) + { + if (points.Count > 1) + { + if (_drawSmoothCurves) + { + g.DrawCurve(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + else + { + g.DrawLines(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + } + + points.Clear(); + } + + g.ResetClip(); + + ctx.SetTextAlign(DrawContext.TextHorizontalAlignment.Left, DrawContext.TextVerticalAlignment.Center); + ctx.Fill(127, 255, 255); + g.DrawString(maxValue.ToString("N3", CultureInfo.InvariantCulture), _font12, ctx.FillBrush, 262, + graphMargin / 2f, ctx.TextAlignment); + g.DrawString(minValue.ToString("N3", CultureInfo.InvariantCulture), _font12, ctx.FillBrush, 262, + graphRect.Height - graphMargin / 2f, ctx.TextAlignment); + ctx.ResetTextAlign(); + } + } + + // ACCELERATION + + if (accelerationToggle.Checked) + { + var autoScale = _autoScaleAcceleration || _accelerationRangeMin == _accelerationRangeMax; + var autoScaleMin = autoScale || !_accelerationRangeMin.HasValue; + var autoScaleMax = autoScale || !_accelerationRangeMax.HasValue; + + var minValue = autoScaleMin ? float.PositiveInfinity : _accelerationRangeMin!.Value; + var maxValue = autoScaleMax ? float.NegativeInfinity : _accelerationRangeMax!.Value; + var sampleCount = 0; + + foreach (var sample in _samples.Take(sampleSize)) + { + if (!sample.Active) + { + continue; + } + + sampleCount++; + + var value = sample.Acceleration; + + if (autoScaleMin && value < minValue) + { + minValue = value; + } + + if (autoScaleMax && value > maxValue) + { + maxValue = value; + } + } + + if (sampleCount > 0) + { + g.SetClip(new RectangleF(0, graphMargin, graphRect.Width, graphRect.Height - 2 * graphMargin + 1)); + + ctx.Stroke(127, 255, 127); + ctx.StrokeWeight(1); + + var i = 0; + + foreach (var sample in _samples.Take(sampleSize)) + { + i++; + + if (!sample.Active) + { + if (points.Count > 0) + { + if (points.Count > 1) + { + if (_drawSmoothCurves) + { + g.DrawCurve(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + else + { + g.DrawLines(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + } + + points.Clear(); + } + + continue; + } + + var p = CalculateGraphPosition(graphRect, graphMargin, i, sampleSize, sample.Acceleration, minValue, + maxValue, + sample.Timestamp, minTimestamp, maxTimestamp); + + points.Add(p); + } + + if (points.Count > 0) + { + if (points.Count > 1) + { + if (_drawSmoothCurves) + { + g.DrawCurve(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + else + { + g.DrawLines(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + } + + points.Clear(); + } + + g.ResetClip(); + + ctx.SetTextAlign(DrawContext.TextHorizontalAlignment.Left, DrawContext.TextVerticalAlignment.Center); + ctx.Fill(127, 255, 127); + g.DrawString(maxValue.ToString("N3", CultureInfo.InvariantCulture), _font12, ctx.FillBrush, 348, + graphMargin / 2f, ctx.TextAlignment); + g.DrawString(minValue.ToString("N3", CultureInfo.InvariantCulture), _font12, ctx.FillBrush, 348, + graphRect.Height - graphMargin / 2f, ctx.TextAlignment); + ctx.ResetTextAlign(); + } + } + + // SERVO + + if (servoToggle.Checked) + { + var autoScale = _autoScaleServo || _servoRangeMin == _servoRangeMax; + var autoScaleMin = autoScale || !_servoRangeMin.HasValue; + var autoScaleMax = autoScale || !_servoRangeMax.HasValue; + + var minValue = autoScaleMin ? int.MaxValue : _servoRangeMin!.Value; + var maxValue = autoScaleMax ? int.MinValue : _servoRangeMax!.Value; + var sampleCount = 0; + + foreach (var sample in _samples.Take(sampleSize)) + { + if (!sample.Active) + { + continue; + } + + sampleCount++; + + var value = sample.Servo; + + if (autoScaleMin && value < minValue) + { + minValue = value; + } + + if (autoScaleMax && value > maxValue) + { + maxValue = value; + } + } + + if (sampleCount > 0) + { + g.SetClip(new RectangleF(0, graphMargin, graphRect.Width, graphRect.Height - 2 * graphMargin + 1)); + + ctx.Stroke(255, 127, 255); + ctx.StrokeWeight(1); + + var i = 0; + + foreach (var sample in _samples.Take(sampleSize)) + { + i++; + + if (!sample.Active) + { + if (points.Count > 0) + { + if (points.Count > 1) + { + g.DrawLines(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + + points.Clear(); + } + + continue; + } + + var p = CalculateGraphPosition(graphRect, graphMargin, i, sampleSize, sample.Servo, minValue, + maxValue, + sample.Timestamp, minTimestamp, maxTimestamp); + + points.Add(p); + } + + if (points.Count > 0) + { + if (points.Count > 1) + { + g.DrawLines(ctx.DrawPen, CollectionsMarshal.AsSpan(points)); + } + + points.Clear(); + } + + g.ResetClip(); + + ctx.SetTextAlign(DrawContext.TextHorizontalAlignment.Left, DrawContext.TextVerticalAlignment.Center); + ctx.Fill(255, 127, 255); + g.DrawString(maxValue.ToString(CultureInfo.InvariantCulture), _font12, ctx.FillBrush, 434, + graphMargin / 2f, ctx.TextAlignment); + g.DrawString(minValue.ToString(CultureInfo.InvariantCulture), _font12, ctx.FillBrush, 434, + graphRect.Height - graphMargin / 2f, ctx.TextAlignment); + ctx.ResetTextAlign(); + } + } + + ctx.Stroke(100); + ctx.StrokeWeight(2); + g.DrawRectangle(ctx.DrawPen, 0, 0, graphRect.Width, graphRect.Height); + + ctx.Pop(); + } +} \ No newline at end of file diff --git a/src/App.resx b/src/App.resx new file mode 100644 index 0000000..840f971 --- /dev/null +++ b/src/App.resx @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/src/ArduinoCom.cs b/src/ArduinoCom.cs new file mode 100644 index 0000000..5bc7fb6 --- /dev/null +++ b/src/ArduinoCom.cs @@ -0,0 +1,502 @@ +using System.Collections.Concurrent; +using System.Globalization; +using System.IO.Ports; + +namespace CruiseControllerMX5; + +public interface IArduinoCom : IDisposable, IObservable, IObserver +{ + void Open(); + void Close(); + IDisposable AddSerialObserver(IObserver observer); + void SendCommand(string command); +} + +public sealed class ArduinoCom : IArduinoCom +{ + private readonly List> _sampleObservers = []; + private readonly BlockingCollection _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 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 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.Subscribe(IObserver observer) + { + return AddSerialObserver(observer); + } + + #endregion + + #region IObserver + + void IObserver.OnCompleted() + { + } + + void IObserver.OnError(Exception error) + { + } + + void IObserver.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> _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 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.Subscribe(IObserver observer) + { + return AddSerialObserver(observer); + } + + #endregion + + #region IObserver + + void IObserver.OnCompleted() + { + } + + void IObserver.OnError(Exception error) + { + } + + void IObserver.OnNext(string value) + { + } + + #endregion + + #region IDisposable + + private bool _disposedValue; + + public void Dispose() + { + if (_disposedValue) + { + return; + } + + Close(); + _sampleObservers.Clear(); + + _disposedValue = true; + } + + #endregion +} \ No newline at end of file diff --git a/src/Buffer.cs b/src/Buffer.cs new file mode 100644 index 0000000..5c5d032 --- /dev/null +++ b/src/Buffer.cs @@ -0,0 +1,73 @@ +using System.Collections; + +namespace CruiseControllerMX5; + +public static class Buffer +{ + public static IBuffer CreateBounded(int capacity) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacity); + + return new BoundedBuffer(capacity); + } +} + +public interface IBuffer : IEnumerable +{ + int Capacity { get; } + + int Size { get; } + + T? Get(); + + T? Get(int index); + + void Add(T value); + + void Clear(); +} + +file class BoundedBuffer(int capacity) : IBuffer +{ + private readonly LinkedList _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 GetEnumerator() => _buffer.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_buffer).GetEnumerator(); +} \ No newline at end of file diff --git a/src/Disposer.cs b/src/Disposer.cs new file mode 100644 index 0000000..f631c3b --- /dev/null +++ b/src/Disposer.cs @@ -0,0 +1,6 @@ +namespace CruiseControllerMX5; + +public sealed class Disposer(Action callback) : IDisposable +{ + public void Dispose() => callback(); +} diff --git a/src/DrawContext.cs b/src/DrawContext.cs new file mode 100644 index 0000000..b78e5bd --- /dev/null +++ b/src/DrawContext.cs @@ -0,0 +1,296 @@ +using System.Drawing.Drawing2D; + +namespace CruiseControllerMX5; + +public sealed class DrawContext(Graphics g) : IDisposable +{ + private readonly Stack _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); + } + } +} \ No newline at end of file diff --git a/src/Listener.cs b/src/Listener.cs new file mode 100644 index 0000000..89ede2f --- /dev/null +++ b/src/Listener.cs @@ -0,0 +1,15 @@ +namespace CruiseControllerMX5; + +public static class Listener +{ + public static Listener Create(Action callback) => new(callback); +} + +public sealed class Listener(Action callback) : IObserver +{ + public void OnCompleted() { } + + public void OnError(Exception error) { } + + public void OnNext(T value) => callback(value); +} diff --git a/src/Program.cs b/src/Program.cs new file mode 100644 index 0000000..0d91359 --- /dev/null +++ b/src/Program.cs @@ -0,0 +1,16 @@ +namespace CruiseControllerMX5; + +static class Program +{ + /// + /// The main entry point for the application. + /// + [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()); + } +} \ No newline at end of file diff --git a/src/RectangleExtensions.cs b/src/RectangleExtensions.cs new file mode 100644 index 0000000..9ce8571 --- /dev/null +++ b/src/RectangleExtensions.cs @@ -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 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 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); +} \ No newline at end of file diff --git a/src/RectangleFExtensions.cs b/src/RectangleFExtensions.cs new file mode 100644 index 0000000..a433122 --- /dev/null +++ b/src/RectangleFExtensions.cs @@ -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 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 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); +} \ No newline at end of file diff --git a/src/Sample.cs b/src/Sample.cs new file mode 100644 index 0000000..da128e7 --- /dev/null +++ b/src/Sample.cs @@ -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; + } +} \ No newline at end of file diff --git a/src/Splitter.cs b/src/Splitter.cs new file mode 100644 index 0000000..88da848 --- /dev/null +++ b/src/Splitter.cs @@ -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(ReadOnlySpan 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(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)); + + public TParsable Read() where TParsable: ISpanParsable => Read(static s => TParsable.Parse(s, provider: CultureInfo.InvariantCulture)); +} diff --git a/src/Utils.cs b/src/Utils.cs new file mode 100644 index 0000000..fdfe155 --- /dev/null +++ b/src/Utils.cs @@ -0,0 +1,19 @@ +using System.Numerics; + +namespace CruiseControllerMX5; + +public static class Utils +{ + public static TTo Map(TFrom value, TFrom fromMin, TFrom fromMax, TTo toMin, TTo toMax) + where TFrom : INumber where TTo : INumber => + Map(value, fromMin, fromMax, toMin, toMax); + + public static TTo Map(TFrom value, TFrom fromMin, TFrom fromMax, TTo toMin, TTo toMax) + where TFrom : INumber where TTo : INumber where TInt : INumber => 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; +} \ No newline at end of file