-
Notifications
You must be signed in to change notification settings - Fork 10
RR1-T102 More work to get ready for windows app #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| using Microsoft.Extensions.Configuration; | ||
| using Resgrid.Relay.Engine; | ||
| using Resgrid.Relay.Engine.Configuration; | ||
| using Resgrid.Relay.Engine.Smtp; | ||
| using Resgrid.Relay.Engine.Services; | ||
| using Resgrid.Providers.ApiClient.V4; | ||
| using Serilog; | ||
| using Serilog.Core; | ||
|
|
@@ -68,7 +68,6 @@ public static async Task<int> Main(string[] args) | |
| private static async Task<int> RunAsync() | ||
| { | ||
| var hostOptions = LoadHostOptions(); | ||
| var mode = (hostOptions.Mode ?? "smtp").Trim().ToLowerInvariant(); | ||
|
|
||
| // Validate required settings before starting (fail fast with clear messages). | ||
| if (!ValidateOptions(hostOptions)) | ||
|
|
@@ -88,28 +87,24 @@ private static async Task<int> RunAsync() | |
|
|
||
| try | ||
| { | ||
| switch (mode) | ||
| // The engine owns all mode wiring; the console just builds the service and runs it. | ||
| IRelayService service; | ||
| try | ||
| { | ||
| case "smtp": | ||
| { | ||
| var smtpLogger = CreateLogger(debug: false); | ||
| await using var telemetry = SmtpTelemetry.Create(hostOptions, smtpLogger); | ||
| using var apiClient = new ResgridV4ApiClient(hostOptions.Resgrid); | ||
| await SmtpRelayRunner.RunAsync(hostOptions.Smtp, telemetry, apiClient, cancellationTokenSource.Token).ConfigureAwait(false); | ||
| return 0; | ||
| } | ||
| case "audio": | ||
| return await RunAudioModeAsync(hostOptions, cancellationTokenSource.Token).ConfigureAwait(false); | ||
| case "radio": | ||
| return await RunRadioModeAsync(hostOptions, cancellationTokenSource.Token).ConfigureAwait(false); | ||
| case "record": | ||
| return await EngineVoice.RecordMode.RunAsync(hostOptions, CreateLogger(debug: false), cancellationTokenSource.Token).ConfigureAwait(false); | ||
| case "dispatch": | ||
| return await EngineVoice.DispatchVoiceMode.RunAsync(hostOptions, CreateLogger(debug: false), cancellationTokenSource.Token).ConfigureAwait(false); | ||
| default: | ||
| Cli.Error.WriteLine($"Unsupported relay mode '{hostOptions.Mode}'. Supported modes are 'smtp', 'audio', 'radio', 'record' and 'dispatch'."); | ||
| return 1; | ||
| service = RelayServiceFactory.Create(hostOptions, CreateLogger(debug: false)); | ||
| } | ||
| catch (ArgumentException ex) | ||
| { | ||
| Cli.Error.WriteLine(ex.Message); | ||
| return 1; | ||
| } | ||
|
|
||
| await using (service) | ||
| { | ||
| await service.StartAsync(cancellationTokenSource.Token).ConfigureAwait(false); | ||
| } | ||
|
|
||
| return service.State == RelayServiceState.Faulted ? 1 : 0; | ||
|
Comment on lines
+102
to
+107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Inspect relay service wrappers for non-zero RunAsync result handling.
# Expected: Each ExecuteAsync that awaits a mode RunAsync(...) stores/checks the int result
# and throws (or otherwise causes RelayServiceBase to transition to Faulted) when result != 0.
set -euo pipefail
fd -i '.*(RelayService|ImportService)\.cs$' . \
| xargs rg -n -C 6 'ExecuteAsync|RunAsync\(|returnCode|exitCode|!=\s*0|throw new'Repository: Resgrid/Relay Length of output: 7645 🏁 Script executed: find . -name "*.cs" -type f \( -name "AudioImportMode.cs" -o -name "SmtpRelayRunner.cs" -o -name "RecordMode.cs" -o -name "RadioMode.cs" -o -name "DispatchVoiceMode.cs" \) | xargs ast-grep outlineRepository: Resgrid/Relay Length of output: 393 🏁 Script executed: find . -name "*.cs" -type f | xargs rg -l "class.*RunAsync"Repository: Resgrid/Relay Length of output: 155 🏁 Script executed: rg -n "public static.*Task.*RunAsync" --type cs --files-with-matchesRepository: Resgrid/Relay Length of output: 151 🏁 Script executed: rg -l "class.*Mode" --type cs --max-count 10Repository: Resgrid/Relay Length of output: 151 🏁 Script executed: find . -name "*.cs" -type f -exec grep -l "RunAsync" {} \; | head -20Repository: Resgrid/Relay Length of output: 699 🏁 Script executed: cat -n Resgrid.Relay.Engine/Voice/AudioImportMode.cs | head -60Repository: Resgrid/Relay Length of output: 2812 🏁 Script executed: cat -n Resgrid.Relay.Engine/Voice/AudioImportMode.cs | tail -50Repository: Resgrid/Relay Length of output: 2429 🏁 Script executed: cat -n Resgrid.Relay.Engine/Voice/RecordMode.csRepository: Resgrid/Relay Length of output: 5671 🏁 Script executed: cat -n Resgrid.Relay.Engine/Voice/RadioMode.csRepository: Resgrid/Relay Length of output: 12185 🏁 Script executed: cat -n Resgrid.Relay.Engine/Voice/DispatchVoiceMode.csRepository: Resgrid/Relay Length of output: 5744 🏁 Script executed: cat -n Resgrid.Relay.Engine/Smtp/SmtpRelayRunner.csRepository: Resgrid/Relay Length of output: 228 🏁 Script executed: find . -name "SmtpRelayRunner.cs" -type fRepository: Resgrid/Relay Length of output: 151 🏁 Script executed: find . -name "*Smtp*.cs" -type fRepository: Resgrid/Relay Length of output: 525 🏁 Script executed: cat -n Resgrid.Relay.Engine/Services/SmtpRelayService.csRepository: Resgrid/Relay Length of output: 2331 🏁 Script executed: rg -l "class.*SmtpRelayRunner" --type csRepository: Resgrid/Relay Length of output: 151 🏁 Script executed: rg "SmtpRelayRunner" -t cs --type-limit 1 --files-with-matchesRepository: Resgrid/Relay Length of output: 593 Return values from mode runners are ignored, masking failures The relay service wrappers ( While Ensure wrappers explicitly check the returned status and throw if it is non-zero. 🤖 Prompt for AI Agents |
||
| } | ||
| finally | ||
| { | ||
|
|
@@ -261,68 +256,6 @@ private static void ShowHelp() | |
| } | ||
|
|
||
| #if NET10_0_WINDOWS | ||
| private static async Task<int> RunAudioModeAsync(RelayHostOptions hostOptions, CancellationToken cancellationToken) | ||
| { | ||
| var config = LoadAudioConfig(hostOptions.AudioConfigPath); | ||
| var apiOptions = ResolveResgridOptions(hostOptions, config); | ||
| using var apiClient = new ResgridV4ApiClient(apiOptions); | ||
| var healthApi = new HealthApi(apiClient); | ||
| var callsApi = new CallsApi(apiClient); | ||
|
|
||
| Logger logger = CreateLogger(config.Debug); | ||
| var audioStorage = new WatcherAudioStorage(logger); | ||
| var evaluator = new AudioEvaluator(logger); | ||
| var recorder = new AudioRecorder(evaluator, audioStorage); | ||
| var processor = new AudioProcessor(recorder, evaluator, audioStorage); | ||
| var comService = new ComService(logger, processor, apiClient, healthApi, callsApi); | ||
|
|
||
| evaluator.WatcherTriggered += (_, eventArgs) => | ||
| Cli.WriteLine($"{DateTime.Now:G}: WATCHER TRIGGERED: {eventArgs.Watcher.Name}"); | ||
|
|
||
| comService.CallCreatedEvent += (_, eventArgs) => | ||
| Cli.WriteLine($"{eventArgs.Timestamp:G}: CALL CREATED: {eventArgs.CallId} ({eventArgs.CallNumber})"); | ||
|
|
||
| comService.Init(config); | ||
| if (!comService.IsConnectionValid()) | ||
| { | ||
| Cli.Error.WriteLine("Unable to reach the Resgrid v4 API with the configured OpenID Connect settings."); | ||
| return 1; | ||
| } | ||
|
|
||
| Cli.WriteLine($"Listening for dispatches on device {config.InputDevice}"); | ||
| processor.Init(config); | ||
| processor.Start(); | ||
|
|
||
| using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); | ||
| ConsoleCancelEventHandler stopHandler = (_, eventArgs) => | ||
| { | ||
| eventArgs.Cancel = true; | ||
| recorder.Stop(); | ||
| linkedCts.Cancel(); | ||
| }; | ||
| Cli.CancelKeyPress += stopHandler; | ||
|
|
||
| try | ||
| { | ||
| while (!linkedCts.IsCancellationRequested && | ||
| (recorder.RecordingState == RecordingState.Monitoring || | ||
| recorder.RecordingState == RecordingState.Recording || | ||
| recorder.RecordingState == RecordingState.RequestedStop)) | ||
| { | ||
| await Task.Delay(250, linkedCts.Token).ConfigureAwait(false); | ||
| } | ||
| } | ||
| catch (TaskCanceledException) | ||
| { | ||
| } | ||
| finally | ||
| { | ||
| Cli.CancelKeyPress -= stopHandler; | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| private static async Task<int> SetupAsync() | ||
| { | ||
| Cli.WriteLine("Resgrid Relay Audio Setup"); | ||
|
|
@@ -432,15 +365,23 @@ private static string SafeName(HidDevice device) | |
| catch { return "(unnamed)"; } | ||
| } | ||
|
|
||
| private static Task<int> RunRadioModeAsync(RelayHostOptions hostOptions, CancellationToken cancellationToken) | ||
| => EngineVoice.RadioMode.RunAsync(hostOptions, CreateLogger(debug: false), cancellationToken); | ||
|
|
||
| private static async Task<int> TuneAsync(string[] args) | ||
| { | ||
| var hostOptions = LoadHostOptions(); | ||
| if (args.Length > 0 && Int32.TryParse(args[0], out var device)) | ||
| hostOptions.Radio.InputDevice = device; | ||
|
|
||
| Cli.WriteLine($"Tuning input device {hostOptions.Radio.InputDevice}. Open={hostOptions.Radio.Squelch.OpenDbfs} dBFS, Close={hostOptions.Radio.Squelch.CloseDbfs} dBFS."); | ||
| Cli.WriteLine("Key up the radio (or feed static) and adjust OpenDbfs just above the static floor. Ctrl+C to stop."); | ||
|
|
||
| // Render the live meter in-place from the engine's tune samples. | ||
| var progress = new Progress<TuneSample>(sample => | ||
| { | ||
| int bars = Math.Clamp((int)((sample.Dbfs + 80) / 80 * 30), 0, 30); | ||
| var meter = new string('#', bars).PadRight(30, '·'); | ||
| Cli.Write($"\r[{meter}] {sample.Dbfs,6:0.0} dBFS {(sample.SquelchOpen ? "OPEN " : "closed")}"); | ||
| }); | ||
|
|
||
| using var cancellationTokenSource = new CancellationTokenSource(); | ||
| ConsoleCancelEventHandler cancelHandler = (_, eventArgs) => | ||
| { | ||
|
|
@@ -450,11 +391,12 @@ private static async Task<int> TuneAsync(string[] args) | |
| Cli.CancelKeyPress += cancelHandler; | ||
| try | ||
| { | ||
| return await EngineVoice.RadioMode.RunTuneAsync(hostOptions, cancellationTokenSource.Token).ConfigureAwait(false); | ||
| return await EngineVoice.RadioMode.RunTuneAsync(hostOptions, Logger.None, cancellationTokenSource.Token, progress).ConfigureAwait(false); | ||
| } | ||
| finally | ||
| { | ||
| Cli.CancelKeyPress -= cancelHandler; | ||
| Cli.WriteLine(); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -528,33 +470,13 @@ private static string DefaultAudioConfigPath() | |
| return Path.Combine(AppContext.BaseDirectory, "settings.json"); | ||
| } | ||
|
|
||
| private static ResgridApiClientOptions ResolveResgridOptions(RelayHostOptions hostOptions, Config config) | ||
| { | ||
| return new ResgridApiClientOptions | ||
| { | ||
| BaseUrl = FirstNonEmpty(hostOptions.Resgrid.BaseUrl, config.Resgrid?.BaseUrl, config.ApiUrl, "https://api.resgrid.com"), | ||
| ApiVersion = FirstNonEmpty(hostOptions.Resgrid.ApiVersion, config.Resgrid?.ApiVersion, "4"), | ||
| ClientId = FirstNonEmpty(hostOptions.Resgrid.ClientId, config.Resgrid?.ClientId), | ||
| ClientSecret = FirstNonEmpty(hostOptions.Resgrid.ClientSecret, config.Resgrid?.ClientSecret), | ||
| RefreshToken = FirstNonEmpty(hostOptions.Resgrid.RefreshToken, config.Resgrid?.RefreshToken), | ||
| Scope = FirstNonEmpty(hostOptions.Resgrid.Scope, config.Resgrid?.Scope, "openid profile email offline_access mobile"), | ||
| TokenCachePath = FirstNonEmpty(hostOptions.Resgrid.TokenCachePath, config.Resgrid?.TokenCachePath, Path.Combine(AppContext.BaseDirectory, "data", "resgrid-token.json")) | ||
| }; | ||
| } | ||
|
|
||
| private static string Prompt(string text, string defaultValue) | ||
| { | ||
| Cli.Write($"{text}{(String.IsNullOrWhiteSpace(defaultValue) ? String.Empty : $" [{defaultValue}]")}: "); | ||
| var input = Cli.ReadLine(); | ||
| return String.IsNullOrWhiteSpace(input) ? defaultValue : input.Trim(); | ||
| } | ||
| #else | ||
| private static Task<int> RunAudioModeAsync(RelayHostOptions hostOptions, CancellationToken cancellationToken) | ||
| { | ||
| Cli.Error.WriteLine("Audio mode is only available in the net10.0-windows build."); | ||
| return Task.FromResult(1); | ||
| } | ||
|
|
||
| private static Task<int> SetupAsync() | ||
| { | ||
| Cli.Error.WriteLine("Audio setup is only available in the net10.0-windows build."); | ||
|
|
@@ -573,12 +495,6 @@ private static Task<int> MonitorAsync(string[] args) | |
| return Task.FromResult(1); | ||
| } | ||
|
|
||
| private static Task<int> RunRadioModeAsync(RelayHostOptions hostOptions, CancellationToken cancellationToken) | ||
| { | ||
| Cli.Error.WriteLine("Radio bridge mode is only available in the net10.0-windows build (it needs physical audio devices)."); | ||
| return Task.FromResult(1); | ||
| } | ||
|
|
||
| private static Task<int> TuneAsync(string[] args) | ||
| { | ||
| Cli.Error.WriteLine("Tune is only available in the net10.0-windows build."); | ||
|
|
@@ -593,10 +509,5 @@ private static Logger CreateLogger(bool debug) | |
| .WriteTo.Console() | ||
| .CreateLogger(); | ||
| } | ||
|
|
||
| private static string FirstNonEmpty(params string[] values) | ||
| { | ||
| return values.FirstOrDefault(x => !String.IsNullOrWhiteSpace(x)); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| using System.Collections.Generic; | ||
| using System.ComponentModel; | ||
| using System.Runtime.CompilerServices; | ||
| using System.Threading; | ||
|
|
||
| namespace Resgrid.Relay.Engine | ||
| { | ||
| /// <summary> | ||
| /// Mutable <see cref="IRelayStatus"/> that a running relay service updates as it | ||
| /// connects and passes traffic. Connection/level/flag setters raise | ||
| /// <see cref="INotifyPropertyChanged"/>; the counters are incremented atomically. | ||
| /// A service sets only the connections it actually uses — the rest stay | ||
| /// <see cref="ConnectionState.NotApplicable"/> so the UI can grey them out. | ||
| /// </summary> | ||
| public sealed class RelayStatus : IRelayStatus | ||
| { | ||
| public event PropertyChangedEventHandler PropertyChanged; | ||
|
|
||
| private ConnectionState _resgridApi = ConnectionState.NotApplicable; | ||
| public ConnectionState ResgridApi { get => _resgridApi; set => SetField(ref _resgridApi, value); } | ||
|
|
||
| private ConnectionState _liveKit = ConnectionState.NotApplicable; | ||
| public ConnectionState LiveKit { get => _liveKit; set => SetField(ref _liveKit, value); } | ||
|
|
||
| private ConnectionState _redis = ConnectionState.NotApplicable; | ||
| public ConnectionState Redis { get => _redis; set => SetField(ref _redis, value); } | ||
|
|
||
| private ConnectionState _smtp = ConnectionState.NotApplicable; | ||
| public ConnectionState Smtp { get => _smtp; set => SetField(ref _smtp, value); } | ||
|
|
||
| private ConnectionState _tts = ConnectionState.NotApplicable; | ||
| public ConnectionState Tts { get => _tts; set => SetField(ref _tts, value); } | ||
|
|
||
| private double _inputDbfs; | ||
| public double InputDbfs { get => _inputDbfs; set => SetField(ref _inputDbfs, value); } | ||
|
|
||
| private bool _squelchOpen; | ||
| public bool SquelchOpen { get => _squelchOpen; set => SetField(ref _squelchOpen, value); } | ||
|
|
||
| private bool _transmitting; | ||
| public bool Transmitting { get => _transmitting; set => SetField(ref _transmitting, value); } | ||
|
|
||
| private bool _receiving; | ||
| public bool Receiving { get => _receiving; set => SetField(ref _receiving, value); } | ||
|
|
||
| private long _callsCreated; | ||
| public long CallsCreated => Interlocked.Read(ref _callsCreated); | ||
|
|
||
| private long _messagesProcessed; | ||
| public long MessagesProcessed => Interlocked.Read(ref _messagesProcessed); | ||
|
|
||
| private long _transmissionsRecorded; | ||
| public long TransmissionsRecorded => Interlocked.Read(ref _transmissionsRecorded); | ||
|
|
||
| public void IncrementCallsCreated() | ||
| { | ||
| Interlocked.Increment(ref _callsCreated); | ||
| OnPropertyChanged(nameof(CallsCreated)); | ||
| } | ||
|
|
||
| public void IncrementMessagesProcessed() | ||
| { | ||
| Interlocked.Increment(ref _messagesProcessed); | ||
| OnPropertyChanged(nameof(MessagesProcessed)); | ||
| } | ||
|
|
||
| public void IncrementTransmissionsRecorded() | ||
| { | ||
| Interlocked.Increment(ref _transmissionsRecorded); | ||
| OnPropertyChanged(nameof(TransmissionsRecorded)); | ||
| } | ||
|
|
||
| private void SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null) | ||
| { | ||
| if (EqualityComparer<T>.Default.Equals(field, value)) | ||
| return; | ||
| field = value; | ||
| OnPropertyChanged(propertyName); | ||
| } | ||
|
|
||
| private void OnPropertyChanged(string propertyName) => | ||
| PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Only mark
Receivingwhen RX audio is actually forwarded.Line 139 uses raw squelch-open state, but Line 141 can still drop the frame because anti-loop is suppressing RX while TX is active. The new status surface then reports
Receiving = trueeven though nothing is being published to the channel.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents