Skip to content

Commit

Permalink
Added tests and GitHub Actions CI build
Browse files Browse the repository at this point in the history
  • Loading branch information
Aldaviva committed Jun 8, 2023
1 parent 086c22a commit 08f3355
Show file tree
Hide file tree
Showing 16 changed files with 729 additions and 55 deletions.
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "nuget"
directory: "/DryerDuty"
schedule:
interval: "weekly"
63 changes: 63 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: .NET

on:
push:
branches: [ master ]
workflow_dispatch:

jobs:
build:
env:
ProjectName: DryerDuty
TargetPlatform: linux-arm

runs-on: ubuntu-latest

steps:
- name: Clone
uses: actions/checkout@v3.5.2

- name: Initialize test reporting
uses: testspace-com/setup-testspace@v1
with:
domain: ${{ github.repository_owner }}

- name: Restore
run: dotnet restore --runtime ${{ env.TargetPlatform }} --locked-mode --verbosity normal

- name: Build
run: dotnet build --no-restore --configuration Release --runtime ${{ env.TargetPlatform }} --no-self-contained --verbosity normal

- name: Test
shell: bash
run: |
dotnet test --verbosity normal --configuration Release --collect:"XPlat Code Coverage" --settings Tests/Tests.runsettings --logger "trx;LogFileName=TestResults.xml"
echo "TEST_EXIT_CODE=$?" >> $GITHUB_ENV
cp Tests/TestResults/*/coverage.info Tests/TestResults
exit 0
- name: Upload test report
run: testspace Tests/TestResults/TestResults.xml

- name: Upload coverage
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: Tests/TestResults/coverage.info

- name: Stop if tests failed
run: exit ${{ env.TEST_EXIT_CODE }}

- name: Publish
run: dotnet publish ${{ env.ProjectName }} --no-build --configuration Release -p:PublishSingleFile=true --runtime ${{ env.TargetPlatform }} --no-self-contained

- name: Upload artifacts
uses: actions/upload-artifact@v3.1.2
with:
name: ${{ env.ProjectName }}
path: |
${{ env.ProjectName }}/bin/Release/net7.0/${{ env.TargetPlatform }}/publish/${{ env.ProjectName }}
${{ env.ProjectName }}/bin/Release/net7.0/${{ env.TargetPlatform }}/publish/appsettings.json
${{ env.ProjectName }}/bin/Release/net7.0/${{ env.TargetPlatform }}/publish/*.service
${{ env.ProjectName }}/bin/Release/net7.0/${{ env.TargetPlatform }}/publish/*.so
if-no-files-found: error
38 changes: 35 additions & 3 deletions DryerDuty.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,54 @@ VisualStudioVersion = 17.6.33723.286
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DryerDuty", "DryerDuty\DryerDuty.csproj", "{ADCF360D-42E6-47D6-84B5-E68612BA2B98}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample", "Sample\Sample.csproj", "{3B46687D-F0F7-449F-AF85-A71F5B195312}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "Sample\Sample.csproj", "{3B46687D-F0F7-449F-AF85-A71F5B195312}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{BE65E639-3971-4F7C-8D5F-4A66E4B00DBF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|ARM32 = Debug|ARM32
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|ARM32 = Release|ARM32
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{ADCF360D-42E6-47D6-84B5-E68612BA2B98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ADCF360D-42E6-47D6-84B5-E68612BA2B98}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ADCF360D-42E6-47D6-84B5-E68612BA2B98}.Debug|ARM32.ActiveCfg = Debug|ARM32
{ADCF360D-42E6-47D6-84B5-E68612BA2B98}.Debug|ARM32.Build.0 = Debug|ARM32
{ADCF360D-42E6-47D6-84B5-E68612BA2B98}.Debug|x86.ActiveCfg = Debug|x86
{ADCF360D-42E6-47D6-84B5-E68612BA2B98}.Debug|x86.Build.0 = Debug|x86
{ADCF360D-42E6-47D6-84B5-E68612BA2B98}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ADCF360D-42E6-47D6-84B5-E68612BA2B98}.Release|Any CPU.Build.0 = Release|Any CPU
{3B46687D-F0F7-449F-AF85-A71F5B195312}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B46687D-F0F7-449F-AF85-A71F5B195312}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ADCF360D-42E6-47D6-84B5-E68612BA2B98}.Release|ARM32.ActiveCfg = Release|ARM32
{ADCF360D-42E6-47D6-84B5-E68612BA2B98}.Release|ARM32.Build.0 = Release|ARM32
{ADCF360D-42E6-47D6-84B5-E68612BA2B98}.Release|x86.ActiveCfg = Release|x86
{ADCF360D-42E6-47D6-84B5-E68612BA2B98}.Release|x86.Build.0 = Release|x86
{3B46687D-F0F7-449F-AF85-A71F5B195312}.Debug|Any CPU.ActiveCfg = Debug|x86
{3B46687D-F0F7-449F-AF85-A71F5B195312}.Debug|Any CPU.Build.0 = Debug|x86
{3B46687D-F0F7-449F-AF85-A71F5B195312}.Debug|ARM32.ActiveCfg = Debug|Any CPU
{3B46687D-F0F7-449F-AF85-A71F5B195312}.Debug|x86.ActiveCfg = Debug|x86
{3B46687D-F0F7-449F-AF85-A71F5B195312}.Debug|x86.Build.0 = Debug|x86
{3B46687D-F0F7-449F-AF85-A71F5B195312}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B46687D-F0F7-449F-AF85-A71F5B195312}.Release|Any CPU.Build.0 = Release|Any CPU
{3B46687D-F0F7-449F-AF85-A71F5B195312}.Release|ARM32.ActiveCfg = Release|Any CPU
{3B46687D-F0F7-449F-AF85-A71F5B195312}.Release|x86.ActiveCfg = Release|x86
{3B46687D-F0F7-449F-AF85-A71F5B195312}.Release|x86.Build.0 = Release|x86
{BE65E639-3971-4F7C-8D5F-4A66E4B00DBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BE65E639-3971-4F7C-8D5F-4A66E4B00DBF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BE65E639-3971-4F7C-8D5F-4A66E4B00DBF}.Debug|ARM32.ActiveCfg = Debug|ARM32
{BE65E639-3971-4F7C-8D5F-4A66E4B00DBF}.Debug|ARM32.Build.0 = Debug|ARM32
{BE65E639-3971-4F7C-8D5F-4A66E4B00DBF}.Debug|x86.ActiveCfg = Debug|x86
{BE65E639-3971-4F7C-8D5F-4A66E4B00DBF}.Debug|x86.Build.0 = Debug|x86
{BE65E639-3971-4F7C-8D5F-4A66E4B00DBF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE65E639-3971-4F7C-8D5F-4A66E4B00DBF}.Release|Any CPU.Build.0 = Release|Any CPU
{BE65E639-3971-4F7C-8D5F-4A66E4B00DBF}.Release|ARM32.ActiveCfg = Release|ARM32
{BE65E639-3971-4F7C-8D5F-4A66E4B00DBF}.Release|ARM32.Build.0 = Release|ARM32
{BE65E639-3971-4F7C-8D5F-4A66E4B00DBF}.Release|x86.ActiveCfg = Release|x86
{BE65E639-3971-4F7C-8D5F-4A66E4B00DBF}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
7 changes: 6 additions & 1 deletion DryerDuty/DryerDuty.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<RuntimeIdentifier>linux-arm</RuntimeIdentifier>
<RuntimeIdentifiers>linux-arm;win-x64;linux-x64</RuntimeIdentifiers>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RollForward>major</RollForward>
Expand All @@ -15,6 +15,7 @@
<Copyright>© 2023 Ben Hutchison</Copyright>
<Authors>Ben Hutchison</Authors>
<Company>$(Authors)</Company>
<Platforms>AnyCPU;x86;ARM32</Platforms>
</PropertyGroup>

<ItemGroup>
Expand All @@ -33,5 +34,9 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Tests" />
</ItemGroup>

</Project>
59 changes: 32 additions & 27 deletions DryerDuty/DryerMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,31 @@ namespace DryerDuty;

public class DryerMonitor: IHostedService, IDisposable {

private const int SAMPLES_PER_WINDOW = 120; // 120Hz, Nyquist limit for 60Hz AC sine wave
private const int MOTOR_CHANNEL = 0;
private const int LIGHT_CHANNEL = 1;
private const int MAX_ADC_READING = (2 << 9) - 1;
private const double REFERENCE_VOLTS = 3.3;
private const double MAX_CURRENT_TRANSFORMER_OUTPUT_VOLTS = 1;
internal const int SAMPLES_PER_WINDOW = 120; // 120Hz, Nyquist limit for 60Hz AC sine wave
private const int MOTOR_CHANNEL = 0;
private const int LIGHT_CHANNEL = 1;
private const int MAX_ADC_READING = (2 << 9) - 1;
private const double REFERENCE_VOLTS = 3.3;
private const double MAX_CURRENT_TRANSFORMER_OUTPUT_VOLTS = 1;

private readonly ILogger<DryerMonitor> logger;
private readonly PagerDutyManager pagerDutyManager;
private readonly Configuration config;
private readonly SpiDevice spi = SpiDevice.Create(new SpiConnectionSettings(0, 0) { ClockFrequency = 1_000_000 });
private readonly Mcp3xxx adc;
private readonly TimeSpan samplingWindow = TimeSpan.FromSeconds(1);
private readonly Timer samplingTimer;
private readonly Timer aggregatingTimer;
private readonly int[][] samplesByChannel = new int[2][];
private readonly int[] maxCurrentTransformerAmps = { 60, 5 };

private LaundryMachineState? state;
private string? pagerDutyLaundryDoneDedupKey;
private int sampleWriteIndex;
internal LaundryMachineState? state;
internal string? pagerDutyLaundryDoneDedupKey;
private int sampleWriteIndex;

public DryerMonitor(ILogger<DryerMonitor> logger, PagerDutyManager pagerDutyManager, Configuration config) {
public DryerMonitor(ILogger<DryerMonitor> logger, PagerDutyManager pagerDutyManager, Configuration config): this(logger, pagerDutyManager, config,
new Mcp3008(SpiDevice.Create(new SpiConnectionSettings(0, 0) { ClockFrequency = 1_000_000 }))) { }

internal DryerMonitor(ILogger<DryerMonitor> logger, PagerDutyManager pagerDutyManager, Configuration config, Mcp3xxx adc) {
this.logger = logger;
this.pagerDutyManager = pagerDutyManager;
this.config = config;
Expand All @@ -39,28 +41,35 @@ public class DryerMonitor: IHostedService, IDisposable {
samplesByChannel[i] = new int[SAMPLES_PER_WINDOW];
}

adc = new Mcp3008(spi);
this.adc = adc;
aggregatingTimer = new Timer(samplingWindow) { AutoReset = true };
samplingTimer = new Timer(samplingWindow.Divide(SAMPLES_PER_WINDOW)) { AutoReset = true };

samplingTimer.Elapsed += onSample;
aggregatingTimer.Elapsed += aggregateSamplesInWindow;
aggregatingTimer.Elapsed += async (_, _) => await aggregateSamplesInWindow();
}

public async Task StartAsync(CancellationToken cancellationToken) {
samplingTimer.Start();
await Task.Delay(samplingWindow, cancellationToken);

try {
// Give the sample buffer time to fill before starting to aggregate its contents
await Task.Delay(samplingWindow, cancellationToken);
} catch (TaskCanceledException) {
return;
}

aggregatingTimer.Start();
logger.LogInformation("Timers started");
}

public Task StopAsync(CancellationToken cancellationToken) {
samplingTimer.Stop();
aggregatingTimer.Stop();
samplingTimer.Stop();
return Task.CompletedTask;
}

private void onSample(object? sender, ElapsedEventArgs e) {
internal void onSample(object? sender = null, ElapsedEventArgs? e = null) {
for (int channel = 0; channel < samplesByChannel.Length; channel++) {
samplesByChannel[channel][sampleWriteIndex] = adc.Read(channel); // sample is in the range [0, 1024)
}
Expand All @@ -75,7 +84,7 @@ public class DryerMonitor: IHostedService, IDisposable {
MAX_CURRENT_TRANSFORMER_OUTPUT_VOLTS * maxCurrentTransformerAmps[channel], 2),
sumOfSquares => Math.Sqrt(sumOfSquares / SAMPLES_PER_WINDOW));

private void aggregateSamplesInWindow(object? sender, ElapsedEventArgs e) {
internal async Task aggregateSamplesInWindow() {
double motorAmps = getRmsAmps(MOTOR_CHANNEL) * config.motorGain;
double lightAmps = getRmsAmps(LIGHT_CHANNEL) * config.lightGain;

Expand All @@ -88,13 +97,12 @@ public class DryerMonitor: IHostedService, IDisposable {

logger.LogTrace("Dryer is {state}, using {motorAmps:N3} amps for the motor and {lightAmps:N3} amps for the light", newState, motorAmps, lightAmps);

if (state != null && state != newState) {
#pragma warning disable CS4014 // don't need to wait for a PagerDuty API response before storing the new state and polling again
onStateChange(newState);
#pragma warning restore CS4014
}

bool stateChanged = state != null && state != newState;
state = newState;

if (stateChanged) {
await onStateChange(newState);
}
}

private async Task onStateChange(LaundryMachineState newState) {
Expand All @@ -115,17 +123,14 @@ public class DryerMonitor: IHostedService, IDisposable {
await pagerDutyManager.resolveIncident(pagerDutyLaundryDoneDedupKey);
pagerDutyLaundryDoneDedupKey = null;
break;

default:
break;
}
}

public void Dispose() {
aggregatingTimer.Dispose();
samplingTimer.Dispose();
adc.Dispose();
spi.Dispose();
// The Mcp3Base superclass of adc disposes of the SpiDevice instance.
GC.SuppressFinalize(this);
}

Expand Down
Loading

0 comments on commit 08f3355

Please sign in to comment.