Skip to content
Merged
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
14 changes: 14 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project>
<!--
The .NET SDK does NOT emit a combined "NET10_0_WINDOWS" preprocessor symbol for the
net10.0-windows target framework — it defines NET10_0, WINDOWS, WINDOWS7_0, etc.
This codebase gates all Windows-only code (radio/audio modes, device enumeration,
the tune meter, WPF) with #if NET10_0_WINDOWS, so without this define that code
silently compiles out of every build. Define it for the windows TFM across all
projects so the Windows-only code actually compiles into the windows build (and
stays excluded from the cross-platform net10.0 / Linux / Docker build).
-->
<PropertyGroup Condition="'$(TargetFramework)' == 'net10.0-windows'">
<DefineConstants>$(DefineConstants);NET10_0_WINDOWS</DefineConstants>
</PropertyGroup>
</Project>
6 changes: 3 additions & 3 deletions Resgrid.Audio.Core/AudioEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
using DtmfDetection.NAudio;
using NAudio.Wave;
using Resgrid.Audio.Core.Events;
using Serilog.Core;
using Serilog;

namespace Resgrid.Audio.Core
{
Expand All @@ -29,7 +29,7 @@ public interface IAudioEvaluator
public class AudioEvaluator : IAudioEvaluator
{
private static Object _lock = new Object();
private readonly Logger _logger;
private readonly ILogger _logger;

private Config _config;
private List<DtmfTone> _dtmfTone;
Expand All @@ -40,7 +40,7 @@ public class AudioEvaluator : IAudioEvaluator
public event EventHandler<EvaluatorEventArgs> EvaluatorFinished;
public event EventHandler<WatcherEventArgs> WatcherTriggered;

public AudioEvaluator(Logger logger)
public AudioEvaluator(ILogger logger)
{
_logger = logger;

Expand Down
10 changes: 9 additions & 1 deletion Resgrid.Audio.Core/Radio/RadioBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,18 @@ private void OnRadioAudio(object sender, short[] frame)
_emergency?.Process(frame);

bool open = IsSquelchOpen(frame);
SetReceiving(open);

// Only count as "receiving" when RX audio is actually forwarded to the channel —
// not merely when squelch opens, and not while AntiLoop suppresses forwarding during
// transmit (a squelch opening then is our own TX feedback, not real RX, and counting
// it would make OnChannelAudio's anti-loop gate suppress the in-progress transmit).
if (!open || (_settings.AntiLoop && _transmitting))
{
SetReceiving(false);
return;
}

SetReceiving(true);

// Copy + condition the audio before publishing.
var outgoing = (short[])frame.Clone();
Expand Down
6 changes: 3 additions & 3 deletions Resgrid.Audio.Core/WatcherAudioStorage.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using Serilog.Core;
using Serilog;

namespace Resgrid.Audio.Core
{
Expand All @@ -16,10 +16,10 @@ public interface IWatcherAudioStorage
public class WatcherAudioStorage : IWatcherAudioStorage
{
private static Object _lock = new Object();
private readonly Logger _logger;
private readonly ILogger _logger;
private static Dictionary<Guid, List<byte>> _watcherAudio;

public WatcherAudioStorage(Logger logger)
public WatcherAudioStorage(ILogger logger)
{
_logger = logger;
_watcherAudio = new Dictionary<Guid, List<byte>>();
Expand Down
11 changes: 9 additions & 2 deletions Resgrid.Audio.Relay/App.xaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
<Application x:Class="Resgrid.Audio.Relay.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources />
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ThemesDictionary Theme="Dark" />
<ui:ControlsDictionary />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
139 changes: 138 additions & 1 deletion Resgrid.Audio.Relay/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,145 @@
using System.Windows;
using System;
using System.Linq;
using System.Threading;
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Resgrid.Audio.Relay.Services;
using Resgrid.Audio.Relay.ViewModels;
using Resgrid.Audio.Relay.Views;
using Resgrid.Relay.Engine;
using Resgrid.Relay.Engine.Logging;
using Serilog;
using Wpf.Ui.Appearance;

namespace Resgrid.Audio.Relay
{
/// <summary>
/// Application entry point. Owns the single-instance guard, the DI container, theme
/// application and the start-to-tray vs start-visible decision. The engine is composed via
/// <see cref="ServiceCollectionExtensions.AddRelayEngine"/>; logging is fanned out to a
/// <see cref="UiLogBus"/> for the Logs screen.
/// </summary>
public partial class App : Application
{
private const string SingleInstanceMutexName = "Global\\ResgridRelayDesktop";

private Mutex _singleInstanceMutex;
private ServiceProvider _services;

/// <summary>Process-wide service provider, exposed for XAML-created view-models if needed.</summary>
public static IServiceProvider Services { get; private set; }

protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);

// (1) Single-instance guard — surface the existing instance instead of starting twice.
_singleInstanceMutex = new Mutex(initiallyOwned: true, SingleInstanceMutexName, out var createdNew);
if (!createdNew)
{
MessageBox.Show(
"Resgrid Relay is already running.",
"Resgrid Relay",
MessageBoxButton.OK,
MessageBoxImage.Information);
Shutdown();
return;
}

// (2) Build the DI container.
_services = BuildServiceProvider();
Services = _services;

// (3) Apply the WPF-UI theme.
ApplicationThemeManager.Apply(ApplicationTheme.Dark);

Check warning on line 54 in Resgrid.Audio.Relay/App.xaml.cs

View workflow job for this annotation

GitHub Actions / Build and test

This call site is reachable on all platforms. 'ApplicationTheme.Dark' is only supported on: 'Windows' 7.0 and later. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 54 in Resgrid.Audio.Relay/App.xaml.cs

View workflow job for this annotation

GitHub Actions / Build and test

This call site is reachable on all platforms. 'ApplicationThemeManager.Apply(ApplicationTheme, WindowBackdropType, bool)' is only supported on: 'Windows' 7.0 and later. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 54 in Resgrid.Audio.Relay/App.xaml.cs

View workflow job for this annotation

GitHub Actions / Build and test

This call site is reachable on all platforms. 'ApplicationTheme.Dark' is only supported on: 'Windows' 7.0 and later. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

// (4) Resolve the shell. Start hidden to tray when launched with --minimized.
var startMinimized = e.Args.Any(a =>
string.Equals(a, "--minimized", StringComparison.OrdinalIgnoreCase) ||
string.Equals(a, "/minimized", StringComparison.OrdinalIgnoreCase));

var shell = _services.GetRequiredService<ShellWindow>();
MainWindow = shell;

if (startMinimized)
{
// Stay hidden — the tray icon (created with the window) keeps the app alive.
shell.StartHiddenToTray();
}
else
{
shell.Show();
}
}

private static ServiceProvider BuildServiceProvider()
{
var services = new ServiceCollection();

// Engine (per-mode service factory delegate).
services.AddRelayEngine();

// UI log bus + a Serilog logger fanned out to it (plus Debug for local diagnostics).
var logBus = new UiLogBus();
services.AddSingleton(logBus);

ILogger logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.UiBus(logBus)
.CreateLogger();
Comment on lines +82 to +89

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale comment: no Debug sink is configured.

The comment claims the logger is fanned out "plus Debug for local diagnostics," but the LoggerConfiguration only wires WriteTo.UiBus(logBus). Either drop the parenthetical or add the WriteTo.Debug() sink to match intent.

📝 Option A — fix the comment
-			// UI log bus + a Serilog logger fanned out to it (plus Debug for local diagnostics).
+			// UI log bus + a Serilog logger fanned out to it.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// UI log bus + a Serilog logger fanned out to it (plus Debug for local diagnostics).
var logBus = new UiLogBus();
services.AddSingleton(logBus);
ILogger logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.UiBus(logBus)
.CreateLogger();
// UI log bus + a Serilog logger fanned out to it.
var logBus = new UiLogBus();
services.AddSingleton(logBus);
ILogger logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.UiBus(logBus)
.CreateLogger();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resgrid.Audio.Relay/App.xaml.cs` around lines 82 - 89, The logging setup in
App.xaml.cs is inconsistent with its comment because the LoggerConfiguration in
the logger initialization only uses WriteTo.UiBus(logBus) and does not include a
Debug sink. Update the implementation to match the intended behavior by either
adding the missing WriteTo.Debug() sink in the LoggerConfiguration chain or
removing the “plus Debug for local diagnostics” wording from the comment so it
accurately reflects the actual sinks configured.

services.AddSingleton(logger);
Log.Logger = logger;

// App services.
services.AddSingleton<ConfigurationService>();
services.AddSingleton<DeviceEnumerationService>();
services.AddSingleton<RelayController>();

// View-models.
services.AddSingleton<ShellViewModel>();
services.AddTransient<DashboardViewModel>();
services.AddTransient<OperationsViewModel>();
services.AddTransient<ConfigurationViewModel>();
// Singleton: owns a long-lived log pump consuming the singleton UiLogBus and must survive
// navigation (so logs aren't lost and a reused LogsView page keeps working). Disposed by
// the container at app shutdown.
services.AddSingleton<LogsViewModel>();
services.AddTransient<DevicesViewModel>();
services.AddTransient<AboutViewModel>();

// Views.
services.AddTransient<DashboardView>();
services.AddTransient<OperationsView>();
services.AddTransient<ConfigurationView>();
services.AddTransient<LogsView>();
services.AddTransient<DevicesView>();
services.AddTransient<AboutView>();

// Shell window.
services.AddSingleton<ShellWindow>();

return services.BuildServiceProvider();
}

protected override void OnExit(ExitEventArgs e)
{
try
{
// Relay modes are stopped in the explicit quit flow (ShellWindow) before
// Shutdown(); OnExit now only disposes and flushes synchronously so it always
// runs to completion (an async-void OnExit could return before cleanup ran).
_services?.Dispose();
}
catch
{
// Best-effort shutdown — never block process exit.
}
finally
{
Log.CloseAndFlush();
_singleInstanceMutex?.Dispose();
base.OnExit(e);
}
}
}
}
21 changes: 21 additions & 0 deletions Resgrid.Audio.Relay/Controls/LevelMeter.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<UserControl x:Class="Resgrid.Audio.Relay.Controls.LevelMeter"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
x:Name="Root"
d:DesignHeight="20" d:DesignWidth="200">
<Grid>
<ProgressBar x:Name="Bar"
Minimum="0" Maximum="100"
Height="14"
Foreground="#2ECC71"
Background="#22000000" />
<TextBlock x:Name="Label"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="10"
Foreground="#DDFFFFFF" />
</Grid>
</UserControl>
46 changes: 46 additions & 0 deletions Resgrid.Audio.Relay/Controls/LevelMeter.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.Windows;
using System.Windows.Controls;

namespace Resgrid.Audio.Relay.Controls
{
/// <summary>
/// A simple input-level meter that maps a dBFS value (typically -80..0) onto a 0..100
/// <see cref="ProgressBar"/> using <c>(db + 80) / 80</c>, and shows the numeric value.
/// </summary>
public partial class LevelMeter : UserControl
{
public LevelMeter()
{
InitializeComponent();
UpdateVisual(Dbfs);
}

public static readonly DependencyProperty DbfsProperty = DependencyProperty.Register(
nameof(Dbfs),
typeof(double),
typeof(LevelMeter),
new FrameworkPropertyMetadata(-80.0, OnDbfsChanged));

/// <summary>Current input level in dBFS. Clamped to the -80..0 display range.</summary>
public double Dbfs
{
get => (double)GetValue(DbfsProperty);
set => SetValue(DbfsProperty, value);
}

private static void OnDbfsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((LevelMeter)d).UpdateVisual((double)e.NewValue);
}

private void UpdateVisual(double db)
{
// Clamp to the -80..0 display range once, then map to a 0..100 percentage
// (-80 dBFS -> 0, 0 dBFS -> 100) so the bar and the label stay consistent.
var clampedDb = Math.Clamp(db, -80.0, 0.0);
Bar.Value = (clampedDb + 80.0) / 80.0 * 100.0;
Label.Text = $"{clampedDb,5:0.0} dBFS";
}
}
}
Loading
Loading