diff --git a/Externals/CommonControls.dll b/Externals/CommonControls.dll
index b0c5095..67018a0 100644
Binary files a/Externals/CommonControls.dll and b/Externals/CommonControls.dll differ
diff --git a/GeneratorForm.cs b/GeneratorForm.cs
index 59e99ed..d1004fa 100644
--- a/GeneratorForm.cs
+++ b/GeneratorForm.cs
@@ -204,9 +204,10 @@ private void SimulateToggle_CheckedChanged(object sender, EventArgs e)
_ = sender;
_ = e;
- // Update the simulation status and button
- m_Data.Simulated = !(m_Data.Simulated);
- m_SimulateToggle.Checked = m_Data.Simulated;
+ // Take the simulation status from the toggle rather than by inverting what is recorded. The
+ // toggle is also set from code, such as when a session is loaded, and inverting the recorded
+ // value there disagrees with the control and sets the two of them toggling each other.
+ m_Data.Simulated = m_SimulateToggle.Checked;
// Record the device interface requires initialization
m_Timer.Initialized = false;
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7e2b6f1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,152 @@
+# Random Number Generator
+
+A Windows desktop application for recording and analysing the output of a
+[TruRNGpro](https://ubld.it/truerngpro) hardware random number generator.
+
+The application samples the device, records every reading to a session file, charts the results as they
+arrive, and compares recorded sessions against one another. A built-in simulator stands in for the device,
+so the application can be run and developed without one attached.
+
+## What it does
+
+Each sample reads a block of bits from the device and records the **average of those bits**, which for an
+unbiased generator sits around 0.5. Readings are taken ten times a second. The application shows, live:
+
+- the running average, the number of data points, the deviation from the statistical mean, and the standard
+ deviation
+- a chart of the individual readings against the running average, with the axis scaling itself to the data
+- the elapsed session time
+
+Every reading is written to the session file as it is taken, so a session survives the application being
+closed or stopped unexpectedly.
+
+### Analysis
+
+A recorded session can be loaded as a **baseline** and another as a **result**, and the two are compared
+directly: mean, standard deviation, skewness and kurtosis for each, the difference between their means, and
+a histogram of the two distributions overlaid. Statistics are calculated with
+[Math.NET Numerics](https://numerics.mathdotnet.com/).
+
+### Targets
+
+A session can record a target value of `0` or `1` — the outcome the operator is attempting to influence —
+or `None`. The target is stored in the session file alongside the data.
+
+## Requirements
+
+- Windows
+- Visual Studio 2022 with the **.NET desktop** and **Desktop development with C++** workloads
+- .NET Framework 4.8
+- A TruRNGpro on a USB serial port — optional, as the simulator can be used instead
+
+## Building
+
+Build the solution rather than the individual projects: the C# application depends on the native DLL, and
+the post-build steps that place the DLL where the application and the tests can find it rely on the
+solution directory.
+
+```
+msbuild RandomNumberGenerator.sln /p:Configuration=Debug /p:Platform="Any CPU"
+```
+
+MSBuild is not usually on the path; it is under the Visual Studio installation, for example
+`C:\Program Files\Microsoft Visual Studio\18\Enterprise\MSBuild\Current\Bin\MSBuild.exe`.
+
+NuGet packages are not committed, so restore them on a fresh clone. The projects use `packages.config`, so
+`dotnet restore` will not work:
+
+```
+nuget restore RandomNumberGenerator.sln
+```
+
+The application is built to `bin\Debug\Random Number Generator.exe` and copied to `bin\`.
+
+## Running
+
+Start the application, then:
+
+1. **Choose a source.** Leave *Simulate* unchecked and pick the COM port the device is on, or check it and
+ enter a seed for the simulator. The port list updates by itself as devices are connected and removed.
+2. **Choose a file.** *Browse* selects the session file. An existing file is loaded and displayed, and the
+ session continues in it. A new file is created.
+3. **Choose a target**, if the session has one.
+4. **Start.** *Pause* suspends recording without ending the session; *Stop* ends it.
+
+To analyse previous sessions, browse for a baseline file and a result file in the comparison section.
+
+### Recovering a session
+
+A file left unfinished — by the application being stopped while recording, or by the machine losing power —
+is recovered when it is next opened. The readings it holds are loaded, the file is reported as having been
+recovered, and recording continues in it. Only complete readings are recovered; a reading that was still
+being written when the application stopped is discarded rather than being loaded as a smaller number.
+
+## Session files
+
+Sessions are XML, written as the data is recorded:
+
+```xml
+
+
+ 0.499969482421875
+ 0.500091552734375
+
+```
+
+`Simulated` records whether the readings came from the simulator. `Target` is `0`, `1`, or `-1` for a
+session with no target. `Time` is the elapsed session time when the reading was taken. Values are written
+in the invariant culture, so a file written on one machine reads back as the same numbers on another.
+
+The file is shared for reading while a session is in progress, so it can be inspected or backed up without
+stopping the recording.
+
+## Layout
+
+| Project | |
+|---|---|
+| `RandomNumberGenerator.csproj` | The application. Windows Forms, .NET Framework 4.8, AnyCPU |
+| `TruRNGpro/TruRNGpro.vcxproj` | Native DLL holding the device interface and the simulator. x64 |
+| `RandomNumberGenerator.Test/` | Unit tests. MSTest and Moq |
+
+The application talks to the native DLL through two exported functions, `Initialize` and
+`GetRandomBitAverage`, which sit behind an interface implemented by both the real device and the simulator.
+
+## Tests
+
+```
+vstest.console.exe "RandomNumberGenerator.Test\bin\Debug\Random Number Generator.Test.dll"
+```
+
+`vstest.console.exe` is under the Visual Studio installation, in
+`Common7\IDE\CommonExtensions\Microsoft\TestWindow`. A single test or class can be selected:
+
+```
+vstest.console.exe "...\Random Number Generator.Test.dll" /Tests:WriteDataPoint_ValidWriter_Success
+vstest.console.exe "...\Random Number Generator.Test.dll" /TestCaseFilter:"FullyQualifiedName~RNGXMLWriterTests"
+```
+
+Build the whole solution before running them, as the tests need the native DLL that the solution build puts
+in place.
+
+## Contributing
+
+The coding standards are written down and are followed throughout:
+
+- `CODING_GUIDELINES.md` for the application
+- `RandomNumberGenerator.Test/CODING_GUIDELINES_TESTS.md` for the tests
+- `TruRNGpro/CODING_GUIDELINES_CPP.md` for the native code
+
+`CLAUDE.md` describes the architecture and the build for anyone, or anything, new to the codebase.
+
+## Third-party code
+
+- [Math.NET Numerics](https://numerics.mathdotnet.com/) (MIT), for the statistics
+- [Moq](https://github.com/devlooped/moq) and MSTest, for the tests
+- `Externals/CommonControls.dll` and `Externals/DeviceInterfaces.dll`, committed binaries with no source
+ in this repository
+- `TruRNGpro/rng.h`, a third-party header that wraps the serial port setup and reads for the device. Its
+ licensing has not been established, which is worth settling before the code is distributed.
+
+## Copyright
+
+Copyright (C) Mike Pullen. All rights reserved. The source files are marked confidential and proprietary.
diff --git a/RandomNumberGenerator.Test/GeneratorForm.Test.cs b/RandomNumberGenerator.Test/GeneratorForm.Test.cs
index 1a88284..b686ca2 100644
--- a/RandomNumberGenerator.Test/GeneratorForm.Test.cs
+++ b/RandomNumberGenerator.Test/GeneratorForm.Test.cs
@@ -1667,6 +1667,61 @@ public void FileLoadingErrors_Integration_ProperErrorHandling()
StringAssert.Contains(generatorForm.StatusBoxText, "Result file not found", "Should show result file not found error");
}
+ ///
+ /// Tests setting the simulate toggle from code settles rather than toggling back and forth.
+ /// NOTE: Loading a session sets this toggle to match the file. Taking the new state by inverting
+ /// what is recorded rather than by reading the toggle left the two of them setting each other
+ /// without end, which overflowed the stack and took the application down as a file was opened.
+ ///
+ [TestMethod]
+ [TestCategory("Component")]
+ [Timeout(15000)]
+ public void SimulateToggle_SetFromCode_MatchesTheToggle()
+ {
+ //**************************************************************//
+ // Arrange
+ //**************************************************************//
+
+ // Mock the session timer and setup the properties
+ Mock mockSessionTimer = new Mock();
+ mockSessionTimer.SetupAllProperties();
+
+ // Mock the session data, recording it as simulated as a loaded session would
+ Mock mockSessionData = new Mock();
+ mockSessionData.Setup(mock => mock.Timer).Returns(mockSessionTimer.Object);
+ mockSessionData.SetupProperty(mock => mock.Simulated, true);
+ Mock mockDeviceTimer = new Mock();
+
+ // Create the object under test
+ GeneratorForm generatorForm = new GeneratorForm(mockSessionData.Object, mockDeviceTimer.Object);
+
+ // Reach the toggle, which is not exposed outside of the form
+ System.Reflection.FieldInfo toggleField = typeof(GeneratorForm).GetField("m_SimulateToggle",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ Assert.IsNotNull(toggleField, "The simulate toggle should be present on the form");
+ System.Windows.Forms.CheckBox simulateToggle = (System.Windows.Forms.CheckBox)toggleField.GetValue(generatorForm);
+
+ //**************************************************************//
+ // Act
+ //**************************************************************//
+
+ // Set the toggle the way loading a simulated session does, which disagrees with the toggle
+ simulateToggle.Checked = true;
+
+ //**************************************************************//
+ // Assert
+ //**************************************************************//
+
+ // Verify the toggle and what is recorded agree, rather than having driven each other
+ Assert.IsTrue(simulateToggle.Checked, "The toggle should stay set");
+ Assert.IsTrue(mockSessionData.Object.Simulated, "The recorded state should match the toggle");
+
+ // And that setting it back settles the same way
+ simulateToggle.Checked = false;
+ Assert.IsFalse(simulateToggle.Checked, "The toggle should stay clear");
+ Assert.IsFalse(mockSessionData.Object.Simulated, "The recorded state should match the toggle");
+ }
+
#endregion
}
}
\ No newline at end of file
diff --git a/RandomNumberGenerator.csproj b/RandomNumberGenerator.csproj
index e5e0a49..f327200 100644
--- a/RandomNumberGenerator.csproj
+++ b/RandomNumberGenerator.csproj
@@ -154,6 +154,10 @@
if errorlevel 1 goto end
echo Copied to "$(SolutionDir)bin\"
+xcopy /y "$(SolutionDir)bin\TruRNGpro.dll" "$(TargetDir)"
+if errorlevel 1 goto end
+echo Copied to "$(TargetDir)"
+
:end
\ No newline at end of file