Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,6 @@ _UpgradeReport_Files/

Thumbs.db
Desktop.ini
.DS_Store
.DS_Store

**/runtimes/
1 change: 1 addition & 0 deletions .idea/.idea.Oma.WndwCtrl/.idea/riderPublish.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/.idea.Oma.WndwCtrl/.idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@
<ItemGroup Label="General">
<PackageVersion Include="CliWrap" Version="3.7.1"/>
<PackageVersion Include="CommandLineParser" Version="2.9.1"/>
<PackageVersion Include="InfluxDB.Client" Version="4.18.0"/>
<PackageVersion Include="Iot.Device.Bindings" Version="3.2.0"/>
<PackageVersion Include="LanguageExt.Core" Version="5.0.0-beta-45"/>
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.1"/>
<PackageVersion Include="Microsoft.Extensions.Diagnostics" Version="9.0.1"/>
<PackageVersion Include="Microsoft.Extensions.Diagnostics.ResourceMonitoring" Version="9.1.0"/>
<PackageVersion Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.1"/>
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.1"/>
<PackageVersion Include="nanoFramework.Hardware.Esp32" Version="1.6.34"/>
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.11.1"/>
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.1"/>
<PackageVersion Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.10.0-beta.1"/>
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.10.0"/>
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.10.1"/>
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.10.0"/>
<PackageVersion Include="System.Device.Gpio" Version="3.2.0"/>
</ItemGroup>
<ItemGroup Label="Host">
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1"/>
Expand Down
10 changes: 10 additions & 0 deletions Oma.AirVentShaker.Api/AirVentShakerApiService.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"Sensor": {
"Io": {
"Accelerometer": {
"BusId": 0,
"Line": 0
}
}
}
}
78 changes: 78 additions & 0 deletions Oma.AirVentShaker.Api/AirVentShakerApiService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Text.Json.Serialization.Metadata;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Oma.AirVentShaker.Api.Audio;
using Oma.AirVentShaker.Api.Interfaces;
using Oma.AirVentShaker.Api.Messaging.Consumers;
using Oma.AirVentShaker.Api.Model;
using Oma.AirVentShaker.Api.Model.Events;
using Oma.AirVentShaker.Api.Model.Settings;
using Oma.AirVentShaker.Api.Sensors;
using Oma.AirVentShaker.Api.TestRunners;
using Oma.AirVentShaker.Api.Workers;
using Oma.WndwCtrl.Configuration.Model;
using Oma.WndwCtrl.Core.Extensions;
using Oma.WndwCtrl.CoreAsp;
using Oma.WndwCtrl.Messaging.Bus;
using Oma.WndwCtrl.Messaging.Extensions;

namespace Oma.AirVentShaker.Api;

public class AirVentShakerApiService(
ComponentConfigurationAccessor configurationAccessor,
MessageBusAccessor messageBusAccessor,
IConfiguration rootConfiguration
)
: WebApplicationWrapper<AirVentShakerApiService>(messageBusAccessor, rootConfiguration)
{
private readonly MessageBusAccessor _messageBusAccessor = messageBusAccessor;

protected override Action<JsonTypeInfo> AddAdditionalModifiers =>
JsonExtensions.GetPolymorphismModifierFor<IWaveDescriptor>(
t => t.Name.Replace("WaveDescriptor", string.Empty)
);

protected override IServiceCollection ConfigureServices(IServiceCollection services)
{
base
.ConfigureServices(services)
.Configure<SensorSettings>(Configuration.GetSection(SensorSettings.SectionName))
.AddSingleton<GlobalState>()
.AddSingleton(_messageBusAccessor)
.UseMessageBus(_messageBusAccessor)
.AddMessageConsumer<TimeSeriesPersistorMessageConsumer, GForceValueBatchEvent>()
.AddMessageConsumer<AmplitudeAdjustingMessageConsumer, GForceValueBatchEvent>()
// .AddHostedService<SensorWorker>()
.AddHostedService<HighResSensorWorker>()
.AddSingleton<ITestRunner, DummyTestRunner>()
.AddSingleton<IAudioService, AudioService>();


if (Configuration.GetValue<string>("ACaaD:OS") == "windows")
{
services.AddSingleton<ISensorService, DummySensorService>();
}
else
{
services.AddSingleton<ISensorService, Adxl345SensorService>();
}

services.AddSignalR();

return services;
}

protected override WebApplication PostAppRun(
WebApplication application,
CancellationToken cancelToken = default
)
{
application.Services.StartConsumersAsync(
_messageBusAccessor.MessageBus ?? throw new InvalidOperationException("MessageBus is not populated."),
cancelToken
);

return base.PostAppRun(application, cancelToken);
}
}
136 changes: 136 additions & 0 deletions Oma.AirVentShaker.Api/Audio/AudioService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using Oma.AirVentShaker.Api.Interfaces;
using Oma.AirVentShaker.Api.Model;
using SoundFlow.Abstracts;
using SoundFlow.Backends.MiniAudio;
using SoundFlow.Components;
using SoundFlow.Enums;

namespace Oma.AirVentShaker.Api.Audio;

public sealed class AudioService : IAudioService, IDisposable
{
private readonly AudioEngine _audioEngine;
private readonly SemaphoreSlim _mutex = new(initialCount: 1);
private readonly Oscillator _oscillator;

private CancellationTokenSource? _delayCancellationTokenSource;

public AudioService()
{
_audioEngine = new MiniAudioEngine(sampleRate: 44100, Capability.Playback);

_oscillator = new Oscillator()
{ Frequency = 50, Type = Oscillator.WaveformType.Sine, Amplitude = 0.2f, Enabled = false, };

Mixer.Master.AddComponent(_oscillator);
}

public async Task PlayAsync(
IWaveDescriptor waveDescriptor,
TimeSpan duration,
CancellationToken cancelToken
)
{
try
{
await _mutex.WaitAsync(cancelToken);

if (_delayCancellationTokenSource is not null)
{
await _delayCancellationTokenSource.CancelAsync();
}

_delayCancellationTokenSource = new CancellationTokenSource();

AdjustOscillator(waveDescriptor);
ScheduleStop(duration, _delayCancellationTokenSource.Token);

cancelToken.Register(() => _oscillator.Enabled = false);
}
finally
{
_mutex.Release();
}
}

public async Task StopAsync(CancellationToken cancelToken)
{
try
{
await _mutex.WaitAsync(cancelToken);

_oscillator.Enabled = false;

await (_delayCancellationTokenSource?.CancelAsync() ?? Task.CompletedTask);
_delayCancellationTokenSource?.Dispose();
_delayCancellationTokenSource = null;
}
finally
{
_mutex.Release();
}
}

public async Task UpdateAmplitudeAsync(float newVal, CancellationToken cancelToken)
{
try
{
await _mutex.WaitAsync(cancelToken);

_oscillator.Amplitude = newVal;
}
finally
{
_mutex.Release();
}
}

public void Dispose()
{
_delayCancellationTokenSource?.Dispose();
_audioEngine.Dispose();
}

private void ScheduleStop(TimeSpan duration, CancellationToken cancelToken)
{
Task.Run(
async () =>
{
try
{
await Task.Delay(duration, cancelToken);
_oscillator.Enabled = false;

_delayCancellationTokenSource?.Dispose();
_delayCancellationTokenSource = null;
}
catch (OperationCanceledException)
{
// do nothing, empty on purpose (PlayAsync was called again)
}
}
);
}

private void AdjustOscillator(IWaveDescriptor waveDescriptor)
{
Mixer.Master.RemoveComponent(_oscillator);

if (waveDescriptor is SineWaveDescriptor sineDescriptor)
{
_oscillator.Type = Oscillator.WaveformType.Sine;
_oscillator.Frequency = sineDescriptor.Frequency;
}
else
{
throw new InvalidOperationException(
$"Unknown wave descriptor {waveDescriptor.GetType().Name}. This is a programming error."
);
}

_oscillator.Amplitude = waveDescriptor.Amplitude;

_oscillator.Enabled = true;
Mixer.Master.AddComponent(_oscillator);
}
}
27 changes: 27 additions & 0 deletions Oma.AirVentShaker.Api/Controllers/AudioController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Mvc;
using Oma.AirVentShaker.Api.Interfaces;
using Oma.AirVentShaker.Api.Model;

namespace Oma.AirVentShaker.Api.Controllers;

[ApiController]
[Route("vent/[controller]")]
public class AudioController(IAudioService audioService) : ControllerBase
{
[HttpPost("test/play")]
public async Task<ActionResult> GenerateTestWaveAsync(
[FromBody] IWaveDescriptor waveDescriptor,
[FromQuery(Name = "duration")] TimeSpan duration
)
{
_ = audioService.PlayAsync(waveDescriptor, duration, HttpContext.RequestAborted);
return Ok();
}

[HttpPost("test/stop")]
public async Task<ActionResult> StopAsync()
{
await audioService.StopAsync(HttpContext.RequestAborted);
return Ok();
}
}
14 changes: 14 additions & 0 deletions Oma.AirVentShaker.Api/Controllers/MeasurementController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc;
using Oma.AirVentShaker.Api.Interfaces;
using Oma.AirVentShaker.Api.Model;

namespace Oma.AirVentShaker.Api.Controllers;

[ApiController]
[Route("vent/[controller]")]
public class MeasurementController(ITestRunner testRunner) : ControllerBase
{
[HttpPost("execute")]
public async Task<ActionResult<TestSummary>> ExecuteTestAsync([FromBody] TestDefinition testDefinition) =>
Ok(await testRunner.ExecuteAsync(testDefinition, HttpContext.RequestAborted));
}
9 changes: 9 additions & 0 deletions Oma.AirVentShaker.Api/Controllers/SensorController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Mvc;

namespace Oma.AirVentShaker.Api.Controllers;

[ApiController]
[Route("vent/[controller]")]
public class SensorController : ControllerBase
{
}
19 changes: 19 additions & 0 deletions Oma.AirVentShaker.Api/Examples/IncreasingFrequency.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"steps": [
{
"frequency": 50,
"duration": "00:00:01",
"targetGravitationalForce": 0.5
},
{
"frequency": 100,
"duration": "00:00:01",
"targetGravitationalForce": 0.5
},
{
"frequency": 200,
"duration": "00:00:01",
"targetGravitationalForce": 0.5
}
]
}
12 changes: 12 additions & 0 deletions Oma.AirVentShaker.Api/Interfaces/IAudioService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Oma.AirVentShaker.Api.Model;

namespace Oma.AirVentShaker.Api.Interfaces;

public interface IAudioService
{
Task PlayAsync(IWaveDescriptor waveDescriptor, TimeSpan duration, CancellationToken cancelToken);

Task StopAsync(CancellationToken cancelToken);

Task UpdateAmplitudeAsync(float newVal, CancellationToken cancelToken);
}
23 changes: 23 additions & 0 deletions Oma.AirVentShaker.Api/Interfaces/ISensorService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Oma.AirVentShaker.Api.Model;

namespace Oma.AirVentShaker.Api.Interfaces;

public record CurrentGForces(float NetForce)
{
private const double tolerance = 0.00000001;

public TestDefinition? TestDefinition { get; init; }

public TestStep? TestStep { get; init; }

public DateTime AsOf { get; } = DateTime.UtcNow;

public virtual bool Equals(CurrentGForces? other) => Math.Abs(NetForce - other?.NetForce ?? 0) < tolerance;

public override int GetHashCode() => NetForce.GetHashCode();
}

public interface ISensorService
{
Task<CurrentGForces> ReadAsync(CancellationToken cancelToken);
}
Loading
Loading