Add the digital interface, running MMTTY under Wine
MMTTY and 2Tone have no socket or pipe interface: N1MM hosts XMMT.ocx and exchanges window messages with the engine. A Linux process cannot load that control, so bridge/nonemm-mmtty-bridge.exe hosts it under Wine and passes lines over its standard input and output. docs/digital-bridge.md states the protocol and what the control needs. The window is N1MM's: receive pane with coloured callsigns, grab list, call stacking, twenty-four macro buttons and the engine controls. One left click copies what is under it — a callsign to the callsign box, anything else to the exchange box the contest keeps for that kind of value. Config > Digital registers XMMT.ocx in the Wine prefix on its own, making the prefix first if it is not there. The engine, the bridge and the control start at the copies shipped beside the program. The bridge reads the control's own events. OnTranslateMessage carries only the messages the control has no event for, so nothing was ever decoded through it. hamlib's data mode names read as digital modes now, and a mode typed into the callsign box changes mode the way a frequency changes band, so a station with no radio can reach RTTY at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Nonemm.App.Dialogs.DigitalDialog"
|
||||
Title="Digital engine" Width="620" SizeToContent="Height"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<Window.Styles>
|
||||
<Style Selector="TextBlock.label">
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="Opacity" Value="0.7" />
|
||||
<Setter Property="Margin" Value="0,8,0,1" />
|
||||
</Style>
|
||||
</Window.Styles>
|
||||
<DockPanel Margin="14">
|
||||
<StackPanel DockPanel.Dock="Top">
|
||||
<TextBlock TextWrapping="Wrap" FontSize="11" Opacity="0.75"
|
||||
Text="MMTTY and 2Tone are Windows programs. They run under Wine, driven through the same XMMT.ocx control N1MM uses, with a small bridge program in between. See docs/digital-bridge.md for setting the Wine prefix up." />
|
||||
<TextBlock Classes="label" Text="Engine program (mmtty.exe or 2Tone.exe)" />
|
||||
<Grid ColumnDefinitions="*,6,Auto">
|
||||
<TextBox Name="EngineBox" />
|
||||
<Button Grid.Column="2" Content="Browse…" Click="OnBrowseEngine" />
|
||||
</Grid>
|
||||
<TextBlock Classes="label" Text="Bridge program (nonemm-mmtty-bridge.exe)" />
|
||||
<Grid ColumnDefinitions="*,6,Auto">
|
||||
<TextBox Name="BridgeBox" />
|
||||
<Button Grid.Column="2" Content="Browse…" Click="OnBrowseBridge" />
|
||||
</Grid>
|
||||
<Grid ColumnDefinitions="*,8,160" RowDefinitions="Auto,Auto">
|
||||
<TextBlock Classes="label" Text="Wine prefix (empty for Wine's own)" />
|
||||
<TextBox Name="PrefixBox" Grid.Row="1" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="Wine command" />
|
||||
<TextBox Name="WineBox" Grid.Row="1" Grid.Column="2" />
|
||||
</Grid>
|
||||
<TextBlock Classes="label" Text="Control (XMMT.ocx), registered in the prefix above" />
|
||||
<Grid ColumnDefinitions="*,6,Auto,6,Auto">
|
||||
<TextBox Name="ControlBox" />
|
||||
<Button Grid.Column="2" Content="Browse…" Click="OnBrowseControl" />
|
||||
<Button Grid.Column="4" Name="RegisterButton" Content="Register" Click="OnRegister" />
|
||||
</Grid>
|
||||
<TextBlock Name="RegisterText" TextWrapping="Wrap" FontSize="11" Opacity="0.75"
|
||||
Margin="0,4,0,0" IsVisible="False" />
|
||||
<Grid ColumnDefinitions="160,8,160,8,*" RowDefinitions="Auto,Auto">
|
||||
<TextBlock Classes="label" Text="Engine window" />
|
||||
<ComboBox Name="WindowBox" Grid.Row="1" HorizontalAlignment="Stretch" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="PTT port (empty reads Mmtty.INI)" />
|
||||
<TextBox Name="PttBox" Grid.Row="1" Grid.Column="2" />
|
||||
<TextBlock Grid.Column="4" Classes="label" Text="Mark tone (Hz)" />
|
||||
<TextBox Name="MarkBox" Grid.Row="1" Grid.Column="4" />
|
||||
</Grid>
|
||||
<CheckBox Name="OnTopBox" Content="Keep the engine window on top" Margin="0,10,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right"
|
||||
Spacing="6" Margin="0,12,0,0">
|
||||
<Button Content="Cancel" Click="OnCancel" />
|
||||
<Button Content="Save" Click="OnSave" IsDefault="True" />
|
||||
</StackPanel>
|
||||
<Panel />
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,105 @@
|
||||
using System.Globalization;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Nonemm.App.Configuration;
|
||||
using Nonemm.Digital;
|
||||
|
||||
namespace Nonemm.App.Dialogs;
|
||||
|
||||
/// Where the digital engine is and how it is started. It closes with the
|
||||
/// settings, or with null when nothing is to be changed.
|
||||
public sealed partial class DigitalDialog : Window
|
||||
{
|
||||
private static readonly string[] WindowSizes = ["Normal", "Small", "Medium1", "Medium2"];
|
||||
|
||||
private readonly Settings settings;
|
||||
|
||||
public DigitalDialog(Settings settings)
|
||||
{
|
||||
this.settings = settings;
|
||||
InitializeComponent();
|
||||
EngineBox.Text = settings.DigitalEnginePath;
|
||||
BridgeBox.Text = settings.DigitalBridgePath;
|
||||
ControlBox.Text = settings.DigitalControlPath;
|
||||
PrefixBox.Text = settings.DigitalWinePrefix;
|
||||
WineBox.Text = settings.DigitalWineCommand;
|
||||
PttBox.Text = settings.DigitalPttPort;
|
||||
MarkBox.Text = settings.DigitalMarkHertz.ToString(CultureInfo.InvariantCulture);
|
||||
WindowBox.ItemsSource = WindowSizes;
|
||||
WindowBox.SelectedItem =
|
||||
WindowSizes.FirstOrDefault(s => s == settings.DigitalEngineWindow) ?? WindowSizes[0];
|
||||
OnTopBox.IsChecked = settings.DigitalEngineOnTop;
|
||||
}
|
||||
|
||||
private async void OnBrowseEngine(object? sender, RoutedEventArgs e) =>
|
||||
EngineBox.Text = await PickAsync("The MMTTY or 2Tone program") ?? EngineBox.Text;
|
||||
|
||||
private async void OnBrowseBridge(object? sender, RoutedEventArgs e) =>
|
||||
BridgeBox.Text = await PickAsync("The bridge program") ?? BridgeBox.Text;
|
||||
|
||||
private async void OnBrowseControl(object? sender, RoutedEventArgs e) =>
|
||||
ControlBox.Text = await PickAsync("The XMMT.ocx control", "ActiveX controls", "*.ocx") ?? ControlBox.Text;
|
||||
|
||||
/// Copies XMMT.ocx into the prefix and registers it. It takes a few seconds
|
||||
/// on a prefix Wine has to make first, so the button is held down until it
|
||||
/// is over and the result is written under it.
|
||||
private async void OnRegister(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
RegisterButton.IsEnabled = false;
|
||||
Say("Registering…");
|
||||
WinePrefixSetup setup = new(
|
||||
PrefixBox.Text?.Trim() is { Length: > 0 } prefix ? prefix : null,
|
||||
WineBox.Text?.Trim() is { Length: > 0 } wine ? wine : "wine");
|
||||
try
|
||||
{
|
||||
Say(await setup.RegisterAsync(ControlBox.Text?.Trim() ?? ""));
|
||||
}
|
||||
catch (Exception failure) when (failure is IOException or InvalidOperationException)
|
||||
{
|
||||
Say(failure.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
RegisterButton.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void Say(string text)
|
||||
{
|
||||
RegisterText.Text = text;
|
||||
RegisterText.IsVisible = true;
|
||||
}
|
||||
|
||||
private async Task<string?> PickAsync(
|
||||
string title,
|
||||
string kind = "Windows programs",
|
||||
string pattern = "*.exe")
|
||||
{
|
||||
IReadOnlyList<IStorageFile> picked = await StorageProvider.OpenFilePickerAsync(
|
||||
new FilePickerOpenOptions
|
||||
{
|
||||
Title = title,
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = [new FilePickerFileType(kind) { Patterns = [pattern] }],
|
||||
});
|
||||
return picked.Count > 0 ? picked[0].Path.LocalPath : null;
|
||||
}
|
||||
|
||||
private void OnCancel(object? sender, RoutedEventArgs e) => Close(null);
|
||||
|
||||
private void OnSave(object? sender, RoutedEventArgs e) =>
|
||||
Close(settings with
|
||||
{
|
||||
DigitalEnginePath = EngineBox.Text?.Trim() ?? "",
|
||||
DigitalBridgePath = BridgeBox.Text?.Trim() ?? "",
|
||||
DigitalControlPath = ControlBox.Text?.Trim() ?? "",
|
||||
DigitalWinePrefix = PrefixBox.Text?.Trim() ?? "",
|
||||
DigitalWineCommand = WineBox.Text?.Trim() is { Length: > 0 } wine ? wine : "wine",
|
||||
DigitalPttPort = PttBox.Text?.Trim() ?? "",
|
||||
DigitalMarkHertz = int.TryParse(MarkBox.Text, out int mark) ? mark : settings.DigitalMarkHertz,
|
||||
DigitalEngineWindow = WindowBox.SelectedItem as string ?? "Normal",
|
||||
DigitalEngineOnTop = OnTopBox.IsChecked == true,
|
||||
DigitalEnabled = (EngineBox.Text?.Trim().Length ?? 0) > 0,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Nonemm.App.Dialogs.DigitalMacrosDialog"
|
||||
Title="Digital macros" Width="620" Height="520"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<DockPanel Margin="14">
|
||||
<TextBlock DockPanel.Dock="Top" TextWrapping="Wrap" FontSize="11" Opacity="0.75"
|
||||
Text="One line per button: the label, a comma, then the message. Twenty-four lines fill the three rows of eight. {TX} starts the transmission and {RX} ends it; {ENTER} sends a carriage return. The text macros are the same ones the function keys use." />
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="6" Margin="0,10,0,0">
|
||||
<Button Content="Back to the defaults" Click="OnDefaults" />
|
||||
<Panel Width="180" />
|
||||
<Button Content="Cancel" Click="OnCancel" />
|
||||
<Button Content="Save" Click="OnSave" IsDefault="True" />
|
||||
</StackPanel>
|
||||
<Border BorderThickness="1" BorderBrush="#40808080" CornerRadius="3" Margin="0,8,0,0">
|
||||
<TextBox Name="TextArea" AcceptsReturn="True" FontFamily="monospace" FontSize="12"
|
||||
BorderThickness="0" />
|
||||
</Border>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,22 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace Nonemm.App.Dialogs;
|
||||
|
||||
/// The digital window's macro buttons, edited as their lines. It closes with
|
||||
/// the text, or with null when nothing is to be changed.
|
||||
public sealed partial class DigitalMacrosDialog : Window
|
||||
{
|
||||
public DigitalMacrosDialog(string text)
|
||||
{
|
||||
InitializeComponent();
|
||||
TextArea.Text = text.Trim().Length > 0 ? text : Messages.DefaultDigital;
|
||||
}
|
||||
|
||||
private void OnDefaults(object? sender, RoutedEventArgs e) =>
|
||||
TextArea.Text = Messages.DefaultDigital;
|
||||
|
||||
private void OnCancel(object? sender, RoutedEventArgs e) => Close(null);
|
||||
|
||||
private void OnSave(object? sender, RoutedEventArgs e) => Close(TextArea.Text ?? "");
|
||||
}
|
||||
Reference in New Issue
Block a user