diff --git a/src/Log4YM.Contracts/Events/LogEvents.cs b/src/Log4YM.Contracts/Events/LogEvents.cs
index cad375d..5b69b0c 100644
--- a/src/Log4YM.Contracts/Events/LogEvents.cs
+++ b/src/Log4YM.Contracts/Events/LogEvents.cs
@@ -899,3 +899,78 @@ public record ActivateChannelTunerGeniusCommand(
string DeviceSerial,
int Channel // 1 or 2
);
+
+// ===== UDP Provider Events =====
+
+///
+/// UDP provider status changed
+///
+public record UdpProviderStatusChangedEvent(
+ string ProviderId,
+ string ProviderName,
+ bool IsRunning,
+ bool IsListening,
+ int? ListeningPort,
+ bool? IsMulticastEnabled,
+ string? ErrorMessage = null
+);
+
+///
+/// WSJT-X status update received
+///
+public record WsjtxStatusReceivedEvent(
+ string Id,
+ long DialFrequency,
+ string Mode,
+ string DxCall,
+ string DxGrid,
+ string DeCall,
+ string DeGrid,
+ bool TxEnabled,
+ bool Transmitting,
+ bool Decoding,
+ int RxDf,
+ int TxDf,
+ string SubMode,
+ int FreqTolerance,
+ double TrPeriod,
+ string Report,
+ string TxMessage
+);
+
+///
+/// WSJT-X decode message received
+///
+public record WsjtxDecodeReceivedEvent(
+ string Id,
+ bool IsNew,
+ DateTime Time,
+ int Snr,
+ double DeltaTime,
+ int DeltaFrequency,
+ string Mode,
+ string Message,
+ bool LowConfidence,
+ bool OffAir
+);
+
+///
+/// WSJT-X QSO logged event
+///
+public record WsjtxQsoLoggedEvent(
+ string Id,
+ string DxCall,
+ string DxGrid,
+ long TxFrequency,
+ string Mode,
+ string ReportSent,
+ string ReportReceived,
+ string TxPower,
+ DateTime TimeOn,
+ DateTime TimeOff,
+ string MyCall,
+ string MyGrid,
+ string? Comments = null,
+ string? ExchangeSent = null,
+ string? ExchangeReceived = null
+);
diff --git a/src/Log4YM.Contracts/Models/Settings.cs b/src/Log4YM.Contracts/Models/Settings.cs
index 0198ceb..317fca4 100644
--- a/src/Log4YM.Contracts/Models/Settings.cs
+++ b/src/Log4YM.Contracts/Models/Settings.cs
@@ -36,6 +36,9 @@ public class UserSettings
[BsonElement("ai")]
public AiSettings Ai { get; set; } = new();
+ [BsonElement("udpProviders")]
+ public UdpProviderSettings UdpProviders { get; set; } = new();
+
[BsonElement("layoutJson")]
public string? LayoutJson { get; set; }
diff --git a/src/Log4YM.Contracts/Models/UdpProviderSettings.cs b/src/Log4YM.Contracts/Models/UdpProviderSettings.cs
new file mode 100644
index 0000000..3de4c71
--- /dev/null
+++ b/src/Log4YM.Contracts/Models/UdpProviderSettings.cs
@@ -0,0 +1,71 @@
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace Log4YM.Contracts.Models;
+
+///
+/// Settings for UDP-based integrations (WSJT-X, JTDX, MSHV, GridTracker, Hamlib, etc.)
+///
+public class UdpProviderSettings
+{
+ [BsonElement("providers")]
+ public List Providers { get; set; } = new()
+ {
+ // WSJT-X/JTDX/MSHV (compatible protocol)
+ new UdpProviderConfig
+ {
+ Id = "wsjtx",
+ Name = "WSJT-X / JTDX / MSHV",
+ Enabled = false,
+ Port = 2237,
+ SupportsMulticast = true,
+ MulticastEnabled = false,
+ MulticastAddress = "224.0.0.73",
+ MulticastTtl = 1,
+ ForwardingEnabled = false,
+ ForwardingAddresses = new List(),
+ Description = "Digital mode applications (FT8, FT4, JT65, etc.)"
+ }
+ };
+}
+
+///
+/// Configuration for a single UDP provider
+///
+public class UdpProviderConfig
+{
+ [BsonElement("id")]
+ public string Id { get; set; } = string.Empty;
+
+ [BsonElement("name")]
+ public string Name { get; set; } = string.Empty;
+
+ [BsonElement("enabled")]
+ public bool Enabled { get; set; }
+
+ [BsonElement("port")]
+ public int Port { get; set; }
+
+ [BsonElement("supportsMulticast")]
+ public bool SupportsMulticast { get; set; }
+
+ [BsonElement("multicastEnabled")]
+ public bool MulticastEnabled { get; set; }
+
+ [BsonElement("multicastAddress")]
+ public string MulticastAddress { get; set; } = "224.0.0.73";
+
+ [BsonElement("multicastTtl")]
+ public int MulticastTtl { get; set; } = 1;
+
+ [BsonElement("forwardingEnabled")]
+ public bool ForwardingEnabled { get; set; }
+
+ [BsonElement("forwardingAddresses")]
+ public List ForwardingAddresses { get; set; } = new();
+
+ [BsonElement("description")]
+ public string Description { get; set; } = string.Empty;
+
+ [BsonElement("customSettings")]
+ public Dictionary CustomSettings { get; set; } = new();
+}
diff --git a/src/Log4YM.Server/Hubs/LogHub.cs b/src/Log4YM.Server/Hubs/LogHub.cs
index 79bfd73..d78ec86 100644
--- a/src/Log4YM.Server/Hubs/LogHub.cs
+++ b/src/Log4YM.Server/Hubs/LogHub.cs
@@ -69,6 +69,12 @@ public interface ILogHubClient
// RBN events
Task OnRbnSpot(RbnSpot spot);
+
+ // UDP Provider events
+ Task OnUdpProviderStatusChanged(UdpProviderStatusChangedEvent evt);
+ Task OnWsjtxStatusReceived(WsjtxStatusReceivedEvent evt);
+ Task OnWsjtxDecodeReceived(WsjtxDecodeReceivedEvent evt);
+ Task OnWsjtxQsoLogged(WsjtxQsoLoggedEvent evt);
}
public class LogHub : Hub
diff --git a/src/Log4YM.Server/Program.cs b/src/Log4YM.Server/Program.cs
index 140e7e6..1974c0a 100644
--- a/src/Log4YM.Server/Program.cs
+++ b/src/Log4YM.Server/Program.cs
@@ -206,6 +206,10 @@
// Register CW Keyer service
builder.Services.AddSingleton();
+// Register UDP Provider services
+builder.Services.AddSingleton();
+builder.Services.AddHostedService(sp => sp.GetRequiredService());
+
var app = builder.Build();
// Configure middleware
diff --git a/src/Log4YM.Server/Services/UdpProviders/WsjtxUdpService.cs b/src/Log4YM.Server/Services/UdpProviders/WsjtxUdpService.cs
new file mode 100644
index 0000000..772ec1a
--- /dev/null
+++ b/src/Log4YM.Server/Services/UdpProviders/WsjtxUdpService.cs
@@ -0,0 +1,508 @@
+using System.Collections.Concurrent;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using Microsoft.AspNetCore.SignalR;
+using Log4YM.Contracts.Events;
+using Log4YM.Contracts.Models;
+using Log4YM.Server.Core.Events;
+using Log4YM.Server.Hubs;
+
+namespace Log4YM.Server.Services.UdpProviders;
+
+///
+/// WSJT-X/JTDX/MSHV UDP Protocol Service
+/// Supports receiving status, decode, and QSO logged messages from digital mode applications
+/// Compatible with WSJT-X protocol specification (magic: 0xadbccbda)
+///
+public class WsjtxUdpService : BackgroundService
+{
+ private readonly ILogger _logger;
+ private readonly IHubContext _hubContext;
+ private readonly EventBus _eventBus;
+ private readonly SettingsService _settingsService;
+
+ private UdpClient? _udpClient;
+ private UdpProviderConfig? _config;
+ private CancellationTokenSource? _listenerCts;
+
+ private const uint MagicNumber = 0xadbccbda;
+ private const int SchemaVersion = 3; // Using schema version 3 (Qt 5.4)
+
+ // Message type constants
+ private const uint MessageTypeHeartbeat = 0;
+ private const uint MessageTypeStatus = 1;
+ private const uint MessageTypeDecode = 2;
+ private const uint MessageTypeClear = 3;
+ private const uint MessageTypeReply = 4;
+ private const uint MessageTypeQsoLogged = 5;
+ private const uint MessageTypeClose = 6;
+ private const uint MessageTypeReplay = 7;
+ private const uint MessageTypeHaltTx = 8;
+ private const uint MessageTypeFreeText = 9;
+ private const uint MessageTypeWsprDecode = 10;
+ private const uint MessageTypeLocation = 11;
+ private const uint MessageTypeLoggedAdif = 12;
+ private const uint MessageTypeHighlightCallsign = 13;
+
+ public WsjtxUdpService(
+ ILogger logger,
+ IHubContext hubContext,
+ EventBus eventBus,
+ SettingsService settingsService)
+ {
+ _logger = logger;
+ _hubContext = hubContext;
+ _eventBus = eventBus;
+ _settingsService = settingsService;
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ _logger.LogInformation("WSJT-X UDP service starting...");
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ try
+ {
+ await LoadConfigurationAsync();
+
+ if (_config?.Enabled == true)
+ {
+ await StartListenerAsync(stoppingToken);
+ }
+ else
+ {
+ await StopListenerAsync();
+ }
+
+ // Check for configuration changes every 5 seconds
+ await Task.Delay(5000, stoppingToken);
+ }
+ catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
+ {
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error in WSJT-X UDP service");
+ await Task.Delay(5000, stoppingToken);
+ }
+ }
+
+ await StopListenerAsync();
+ _logger.LogInformation("WSJT-X UDP service stopped");
+ }
+
+ private async Task LoadConfigurationAsync()
+ {
+ var settings = await _settingsService.GetSettingsAsync();
+ var newConfig = settings.UdpProviders.Providers.FirstOrDefault(p => p.Id == "wsjtx");
+
+ // Check if configuration changed
+ if (newConfig != null && !ConfigEquals(_config, newConfig))
+ {
+ _logger.LogInformation("WSJT-X configuration changed, reloading...");
+ _config = newConfig;
+
+ // Restart listener if already running
+ if (_udpClient != null)
+ {
+ await StopListenerAsync();
+ }
+ }
+ }
+
+ private bool ConfigEquals(UdpProviderConfig? a, UdpProviderConfig? b)
+ {
+ if (a == null || b == null) return a == b;
+ return a.Enabled == b.Enabled &&
+ a.Port == b.Port &&
+ a.MulticastEnabled == b.MulticastEnabled &&
+ a.MulticastAddress == b.MulticastAddress;
+ }
+
+ private async Task StartListenerAsync(CancellationToken ct)
+ {
+ if (_udpClient != null || _config == null) return;
+
+ try
+ {
+ _listenerCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
+ _udpClient = new UdpClient();
+ _udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
+
+ if (_config.MulticastEnabled && !string.IsNullOrEmpty(_config.MulticastAddress))
+ {
+ // Multicast mode
+ _udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, _config.Port));
+ var multicastAddress = IPAddress.Parse(_config.MulticastAddress);
+ _udpClient.JoinMulticastGroup(multicastAddress, _config.MulticastTtl);
+
+ _logger.LogInformation(
+ "WSJT-X listening on multicast {Address}:{Port} (TTL: {Ttl})",
+ _config.MulticastAddress, _config.Port, _config.MulticastTtl);
+ }
+ else
+ {
+ // Unicast mode
+ _udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, _config.Port));
+ _logger.LogInformation("WSJT-X listening on UDP port {Port}", _config.Port);
+ }
+
+ await PublishStatusEvent(true, null);
+
+ // Start listener task
+ _ = Task.Run(async () => await RunListenerAsync(_listenerCts.Token), _listenerCts.Token);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to start WSJT-X UDP listener");
+ await PublishStatusEvent(false, ex.Message);
+ await StopListenerAsync();
+ }
+ }
+
+ private async Task StopListenerAsync()
+ {
+ if (_udpClient == null) return;
+
+ try
+ {
+ _listenerCts?.Cancel();
+ _udpClient?.Close();
+ _udpClient?.Dispose();
+ _udpClient = null;
+ _listenerCts?.Dispose();
+ _listenerCts = null;
+
+ await PublishStatusEvent(false, null);
+ _logger.LogInformation("WSJT-X UDP listener stopped");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error stopping WSJT-X UDP listener");
+ }
+ }
+
+ private async Task RunListenerAsync(CancellationToken ct)
+ {
+ while (!ct.IsCancellationRequested && _udpClient != null)
+ {
+ try
+ {
+ var result = await _udpClient.ReceiveAsync(ct);
+ await ProcessDatagramAsync(result.Buffer, result.RemoteEndPoint);
+
+ // Forward datagram if enabled
+ if (_config?.ForwardingEnabled == true && _config.ForwardingAddresses.Any())
+ {
+ await ForwardDatagramAsync(result.Buffer);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error receiving WSJT-X UDP datagram");
+ await Task.Delay(1000, ct);
+ }
+ }
+ }
+
+ private async Task ProcessDatagramAsync(byte[] data, IPEndPoint sender)
+ {
+ if (data.Length < 12) return; // Minimum header size
+
+ using var stream = new MemoryStream(data);
+ using var reader = new BinaryReader(stream);
+
+ // Read header
+ var magic = ReadUInt32BigEndian(reader);
+ if (magic != MagicNumber)
+ {
+ _logger.LogDebug("Invalid WSJT-X magic number: 0x{Magic:X8}", magic);
+ return;
+ }
+
+ var schema = ReadUInt32BigEndian(reader);
+ var messageType = ReadUInt32BigEndian(reader);
+
+ _logger.LogDebug("WSJT-X message: type={Type}, schema={Schema}", messageType, schema);
+
+ try
+ {
+ switch (messageType)
+ {
+ case MessageTypeStatus:
+ await HandleStatusMessage(reader);
+ break;
+
+ case MessageTypeDecode:
+ await HandleDecodeMessage(reader);
+ break;
+
+ case MessageTypeQsoLogged:
+ await HandleQsoLoggedMessage(reader);
+ break;
+
+ case MessageTypeLoggedAdif:
+ await HandleLoggedAdifMessage(reader);
+ break;
+
+ default:
+ _logger.LogDebug("Unhandled WSJT-X message type: {Type}", messageType);
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error processing WSJT-X message type {Type}", messageType);
+ }
+ }
+
+ private async Task HandleStatusMessage(BinaryReader reader)
+ {
+ var id = ReadQString(reader);
+ var dialFrequency = ReadUInt64BigEndian(reader);
+ var mode = ReadQString(reader);
+ var dxCall = ReadQString(reader);
+ var report = ReadQString(reader);
+ var txMode = ReadQString(reader);
+ var txEnabled = reader.ReadBoolean();
+ var transmitting = reader.ReadBoolean();
+ var decoding = reader.ReadBoolean();
+ var rxDf = ReadUInt32BigEndian(reader);
+ var txDf = ReadUInt32BigEndian(reader);
+ var deCall = ReadQString(reader);
+ var deGrid = ReadQString(reader);
+ var dxGrid = ReadQString(reader);
+ var txWatchdog = reader.ReadBoolean();
+ var subMode = ReadQString(reader);
+ var fastMode = reader.ReadBoolean();
+ var specialOpMode = reader.ReadByte();
+ var freqTolerance = ReadUInt32BigEndian(reader);
+ var trPeriod = ReadUInt32BigEndian(reader);
+ var confName = ReadQString(reader);
+ var txMessage = ReadQString(reader);
+
+ var evt = new WsjtxStatusReceivedEvent(
+ Id: id,
+ DialFrequency: (long)dialFrequency,
+ Mode: mode,
+ DxCall: dxCall,
+ DxGrid: dxGrid,
+ DeCall: deCall,
+ DeGrid: deGrid,
+ TxEnabled: txEnabled,
+ Transmitting: transmitting,
+ Decoding: decoding,
+ RxDf: (int)rxDf,
+ TxDf: (int)txDf,
+ SubMode: subMode,
+ FreqTolerance: (int)freqTolerance,
+ TrPeriod: trPeriod,
+ Report: report,
+ TxMessage: txMessage
+ );
+
+ _eventBus.Publish(evt);
+ await _hubContext.Clients.All.OnWsjtxStatusReceived(evt);
+ }
+
+ private async Task HandleDecodeMessage(BinaryReader reader)
+ {
+ var id = ReadQString(reader);
+ var isNew = reader.ReadBoolean();
+ var time = ReadQTime(reader);
+ var snr = ReadInt32BigEndian(reader);
+ var deltaTime = ReadDoubleBigEndian(reader);
+ var deltaFrequency = ReadUInt32BigEndian(reader);
+ var mode = ReadQString(reader);
+ var message = ReadQString(reader);
+ var lowConfidence = reader.ReadBoolean();
+ var offAir = reader.ReadBoolean();
+
+ var evt = new WsjtxDecodeReceivedEvent(
+ Id: id,
+ IsNew: isNew,
+ Time: time,
+ Snr: snr,
+ DeltaTime: deltaTime,
+ DeltaFrequency: (int)deltaFrequency,
+ Mode: mode,
+ Message: message,
+ LowConfidence: lowConfidence,
+ OffAir: offAir
+ );
+
+ _eventBus.Publish(evt);
+ await _hubContext.Clients.All.OnWsjtxDecodeReceived(evt);
+ }
+
+ private async Task HandleQsoLoggedMessage(BinaryReader reader)
+ {
+ var id = ReadQString(reader);
+ var timeOff = ReadQDateTime(reader);
+ var dxCall = ReadQString(reader);
+ var dxGrid = ReadQString(reader);
+ var txFrequency = ReadUInt64BigEndian(reader);
+ var mode = ReadQString(reader);
+ var reportSent = ReadQString(reader);
+ var reportReceived = ReadQString(reader);
+ var txPower = ReadQString(reader);
+ var comments = ReadQString(reader);
+ var name = ReadQString(reader);
+ var timeOn = ReadQDateTime(reader);
+ var operatorCall = ReadQString(reader);
+ var myCall = ReadQString(reader);
+ var myGrid = ReadQString(reader);
+ var exchangeSent = ReadQString(reader);
+ var exchangeReceived = ReadQString(reader);
+ var propMode = ReadQString(reader);
+
+ var evt = new WsjtxQsoLoggedEvent(
+ Id: id,
+ DxCall: dxCall,
+ DxGrid: dxGrid,
+ TxFrequency: (long)txFrequency,
+ Mode: mode,
+ ReportSent: reportSent,
+ ReportReceived: reportReceived,
+ TxPower: txPower,
+ TimeOn: timeOn,
+ TimeOff: timeOff,
+ MyCall: myCall,
+ MyGrid: myGrid,
+ Comments: string.IsNullOrEmpty(comments) ? null : comments,
+ ExchangeSent: string.IsNullOrEmpty(exchangeSent) ? null : exchangeSent,
+ ExchangeReceived: string.IsNullOrEmpty(exchangeReceived) ? null : exchangeReceived
+ );
+
+ _eventBus.Publish(evt);
+ await _hubContext.Clients.All.OnWsjtxQsoLogged(evt);
+ }
+
+ private async Task HandleLoggedAdifMessage(BinaryReader reader)
+ {
+ var id = ReadQString(reader);
+ var adifText = ReadQString(reader);
+
+ _logger.LogInformation("WSJT-X ADIF QSO logged from {Id}: {Length} bytes", id, adifText.Length);
+
+ // TODO: Parse ADIF and emit QSO event
+ // For now, just log it
+ }
+
+ private async Task ForwardDatagramAsync(byte[] data)
+ {
+ if (_config?.ForwardingAddresses == null) return;
+
+ foreach (var addressStr in _config.ForwardingAddresses)
+ {
+ try
+ {
+ var parts = addressStr.Split(':', 2);
+ if (parts.Length != 2) continue;
+
+ var host = parts[0];
+ var port = int.Parse(parts[1]);
+
+ using var forwardClient = new UdpClient();
+ await forwardClient.SendAsync(data, data.Length, host, port);
+
+ _logger.LogDebug("Forwarded WSJT-X datagram to {Address}", addressStr);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error forwarding WSJT-X datagram to {Address}", addressStr);
+ }
+ }
+ }
+
+ private async Task PublishStatusEvent(bool isRunning, string? errorMessage)
+ {
+ var evt = new UdpProviderStatusChangedEvent(
+ ProviderId: "wsjtx",
+ ProviderName: "WSJT-X / JTDX / MSHV",
+ IsRunning: isRunning,
+ IsListening: isRunning,
+ ListeningPort: _config?.Port,
+ IsMulticastEnabled: _config?.MulticastEnabled,
+ ErrorMessage: errorMessage
+ );
+
+ _eventBus.Publish(evt);
+ await _hubContext.Clients.All.OnUdpProviderStatusChanged(evt);
+ }
+
+ // Binary reading helpers (WSJT-X uses big-endian)
+
+ private static uint ReadUInt32BigEndian(BinaryReader reader)
+ {
+ var bytes = reader.ReadBytes(4);
+ if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
+ return BitConverter.ToUInt32(bytes, 0);
+ }
+
+ private static ulong ReadUInt64BigEndian(BinaryReader reader)
+ {
+ var bytes = reader.ReadBytes(8);
+ if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
+ return BitConverter.ToUInt64(bytes, 0);
+ }
+
+ private static int ReadInt32BigEndian(BinaryReader reader)
+ {
+ var bytes = reader.ReadBytes(4);
+ if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
+ return BitConverter.ToInt32(bytes, 0);
+ }
+
+ private static double ReadDoubleBigEndian(BinaryReader reader)
+ {
+ var bytes = reader.ReadBytes(8);
+ if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
+ return BitConverter.ToDouble(bytes, 0);
+ }
+
+ private static string ReadQString(BinaryReader reader)
+ {
+ var length = ReadUInt32BigEndian(reader);
+ if (length == 0xFFFFFFFF) return string.Empty; // Null string in Qt
+ if (length == 0) return string.Empty;
+
+ var bytes = reader.ReadBytes((int)length);
+ return Encoding.UTF8.GetString(bytes);
+ }
+
+ private static DateTime ReadQDateTime(BinaryReader reader)
+ {
+ var julianDay = ReadUInt64BigEndian(reader);
+ var msecsSinceMidnight = ReadUInt32BigEndian(reader);
+ var timeSpec = reader.ReadByte();
+
+ // Qt Julian Day starts from November 24, 4714 BCE
+ // Convert to .NET DateTime
+ var baseDate = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+ var days = (long)julianDay - 2440588; // Convert Qt Julian Day to Unix epoch days
+ var dt = baseDate.AddDays(days).AddMilliseconds(msecsSinceMidnight);
+
+ return dt;
+ }
+
+ private static DateTime ReadQTime(BinaryReader reader)
+ {
+ var msecsSinceMidnight = ReadUInt32BigEndian(reader);
+ var today = DateTime.UtcNow.Date;
+ return today.AddMilliseconds(msecsSinceMidnight);
+ }
+
+ public override void Dispose()
+ {
+ StopListenerAsync().GetAwaiter().GetResult();
+ base.Dispose();
+ }
+}
diff --git a/src/Log4YM.Web/src/components/SettingsPanel.tsx b/src/Log4YM.Web/src/components/SettingsPanel.tsx
index f5ef5de..273e525 100644
--- a/src/Log4YM.Web/src/components/SettingsPanel.tsx
+++ b/src/Log4YM.Web/src/components/SettingsPanel.tsx
@@ -91,6 +91,12 @@ const SETTINGS_SECTIONS: { id: SettingsSection; name: string; icon: React.ReactN
icon: ,
description: 'LLM API settings for talk points',
},
+ {
+ id: 'udp',
+ name: 'UDP Integrations',
+ icon: ,
+ description: 'WSJT-X, JTDX, MSHV, and other UDP apps',
+ },
{
id: 'about',
name: 'About',
diff --git a/src/Log4YM.Web/src/store/settingsStore.ts b/src/Log4YM.Web/src/store/settingsStore.ts
index b8b3300..b42a9c3 100644
--- a/src/Log4YM.Web/src/store/settingsStore.ts
+++ b/src/Log4YM.Web/src/store/settingsStore.ts
@@ -122,6 +122,25 @@ export interface AiSettings {
includeSpotComments: boolean;
}
+export interface UdpProviderConfig {
+ id: string;
+ name: string;
+ enabled: boolean;
+ port: number;
+ supportsMulticast: boolean;
+ multicastEnabled: boolean;
+ multicastAddress: string;
+ multicastTtl: number;
+ forwardingEnabled: boolean;
+ forwardingAddresses: string[];
+ description: string;
+ customSettings: Record;
+}
+
+export interface UdpProviderSettings {
+ providers: UdpProviderConfig[];
+}
+
export interface Settings {
station: StationSettings;
qrz: QrzSettings;
@@ -132,9 +151,10 @@ export interface Settings {
cluster: ClusterSettings;
header: HeaderSettings;
ai: AiSettings;
+ udpProviders: UdpProviderSettings;
}
-export type SettingsSection = 'station' | 'qrz' | 'rotator' | 'database' | 'appearance' | 'map' | 'header' | 'ai' | 'about';
+export type SettingsSection = 'station' | 'qrz' | 'rotator' | 'database' | 'appearance' | 'map' | 'header' | 'ai' | 'udp' | 'about';
interface SettingsState {
// Settings data
@@ -166,6 +186,8 @@ interface SettingsState {
updateClusterConnection: (connectionId: string, connection: Partial) => void;
updateHeaderSettings: (header: Partial) => void;
updateAiSettings: (ai: Partial) => void;
+ updateUdpProviderSettings: (udpProviders: Partial) => void;
+ updateUdpProvider: (providerId: string, provider: Partial) => void;
addClusterConnection: () => void;
removeClusterConnection: (connectionId: string) => void;
@@ -267,6 +289,24 @@ const defaultSettings: Settings = {
includeQsoHistory: true,
includeSpotComments: false,
},
+ udpProviders: {
+ providers: [
+ {
+ id: 'wsjtx',
+ name: 'WSJT-X / JTDX / MSHV',
+ enabled: false,
+ port: 2237,
+ supportsMulticast: true,
+ multicastEnabled: false,
+ multicastAddress: '224.0.0.73',
+ multicastTtl: 1,
+ forwardingEnabled: false,
+ forwardingAddresses: [],
+ description: 'Digital mode applications (FT8, FT4, JT65, etc.)',
+ customSettings: {},
+ },
+ ],
+ },
};
// Settings are stored in MongoDB only - no localStorage persistence
@@ -380,6 +420,30 @@ export const useSettingsStore = create()((set, get) => ({
isDirty: true,
})),
+ // UDP Provider settings
+ updateUdpProviderSettings: (udpProviders) =>
+ set((state) => ({
+ settings: {
+ ...state.settings,
+ udpProviders: { ...state.settings.udpProviders, ...udpProviders },
+ },
+ isDirty: true,
+ })),
+
+ updateUdpProvider: (providerId, provider) =>
+ set((state) => {
+ const updatedProviders = state.settings.udpProviders.providers.map((p) =>
+ p.id === providerId ? { ...p, ...provider } : p
+ );
+ return {
+ settings: {
+ ...state.settings,
+ udpProviders: { ...state.settings.udpProviders, providers: updatedProviders },
+ },
+ isDirty: true,
+ };
+ }),
+
// Cluster settings
updateClusterSettings: (cluster) =>
set((state) => ({
@@ -515,6 +579,7 @@ export const useSettingsStore = create()((set, get) => ({
cluster: { ...defaultSettings.cluster, ...settings.cluster },
header: { ...defaultSettings.header, ...settings.header },
ai: { ...defaultSettings.ai, ...settings.ai },
+ udpProviders: { ...defaultSettings.udpProviders, ...settings.udpProviders },
};
set({ settings: mergedSettings, isDirty: false, isLoaded: true });
} else {