Files
Nonemm/bridge/main.cpp
ericek111 c8de74e24a 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
2026-08-31 21:35:45 +00:00

257 lines
8.4 KiB
C++

// Runs XMMT.ocx under Wine so that Nonemm, which is a Linux program, can drive
// MMTTY or 2Tone. Everything arrives on standard input and leaves on standard
// output, one protocol line at a time. Wine's own diagnostics go to standard
// error, which keeps them out of the protocol.
//
// The startup is N1MM's, in N1MM's order: title, PTT port and command line on
// the control, then bActive, then the host window handle to the engine.
#include <windows.h>
#include <cstdio>
#include <iostream>
#include <string>
#include <thread>
#include "Protocol.h"
#include "XmmrControl.h"
namespace {
// One protocol line, handed from the reader thread to the thread the control
// lives on. COM calls have to stay on the one thread.
constexpr UINT WM_BRIDGE_LINE = WM_APP + 1;
constexpr long MessageHostWindow = 0;
constexpr long MessageShutdown = 2;
constexpr UINT_PTR QuitTimer = 1;
constexpr UINT QuitPatience = 5000;
void killEngine();
XmmrControl* control = nullptr;
HWND bridgeWindow = nullptr;
DWORD enginePid = 0;
bool quitting = false;
// Dispatch arguments arrive last one first; `position` counts them the way the
// event declares them.
long argument(DISPPARAMS* arguments, unsigned position) {
if (arguments == nullptr || position >= arguments->cArgs) {
return 0;
}
VARIANT wanted;
VariantInit(&wanted);
if (FAILED(VariantChangeType(&wanted, &arguments->rgvarg[arguments->cArgs - 1 - position], 0,
VT_I4))) {
return 0;
}
long value = wanted.lVal;
VariantClear(&wanted);
return value;
}
// The control turns each message MMTTY sends it into an event of its own. Only
// the messages it has no event for reach OnTranslateMessage, and every message
// the logger needs has one, so that event is not listened to.
void onEvent(const std::wstring& name, DISPPARAMS* arguments) {
if (name == L"OnConnected") {
protocol::write("connected", {control->getString(L"verMMTTY")});
} else if (name == L"OnDisconnected") {
protocol::write("disconnected", argument(arguments, 0));
if (quitting) {
killEngine();
PostQuitMessage(0);
}
} else if (name == L"OnCharRcvd") {
protocol::write("rx", argument(arguments, 0));
} else if (name == L"OnPttEvent") {
protocol::write("tx", argument(arguments, 0));
} else if (name == L"OnFreqChanged") {
protocol::write("mark", argument(arguments, 0));
protocol::write("space", argument(arguments, 1));
} else if (name == L"OnSwitchChanged") {
protocol::write("switch", argument(arguments, 0));
} else if (name == L"OnViewChanged") {
protocol::write("view", argument(arguments, 0));
}
}
// The bridge started the engine, so the bridge stops it. MMTTY is asked first
// and killed only if it is still there when the wait runs out; the control's
// own shutdown call blocks forever under Wine, waiting for a message loop that
// is by then no longer pumping.
BOOL CALLBACK closeWindow(HWND window, LPARAM wanted) {
DWORD owner = 0;
GetWindowThreadProcessId(window, &owner);
if (owner == static_cast<DWORD>(wanted)) {
PostMessageW(window, WM_CLOSE, 0, 0);
}
return TRUE;
}
void stopEngine() {
control->callLong(L"PostMmttyMessage", MessageShutdown, 0);
HWND engine = reinterpret_cast<HWND>(static_cast<uintptr_t>(control->getLong(L"hWndMmtty")));
if (engine == nullptr) {
return;
}
GetWindowThreadProcessId(engine, &enginePid);
EnumWindows(closeWindow, static_cast<LPARAM>(enginePid));
}
void killEngine() {
if (enginePid == 0) {
return;
}
HANDLE engine = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, enginePid);
if (engine == nullptr) {
return;
}
if (WaitForSingleObject(engine, 0) != WAIT_OBJECT_0) {
TerminateProcess(engine, 0);
}
CloseHandle(engine);
enginePid = 0;
}
void report(const char* what, HRESULT result) {
if (FAILED(result)) {
char message[128];
std::snprintf(message, sizeof(message), "%s failed: 0x%08lx", what,
static_cast<unsigned long>(result));
protocol::write("error", {message});
}
}
void open(const protocol::Line& line) {
report("Visible", control->putBool(L"Visible", true));
report("VisibleMmtty", control->putBool(L"VisibleMmtty", true));
report("InvokeCommand", control->putString(L"InvokeCommand", line.field(2)));
report("ComName", control->putString(L"ComName", line.field(1)));
report("Title", control->putString(L"Title", line.field(0)));
report("bActive", control->putBool(L"bActive", true));
report("PostMmttyMessage",
control->callLong(L"PostMmttyMessage", MessageHostWindow, control->getLong(L"hwnd")));
}
void send(const std::string& text) {
VARIANT argument;
VariantInit(&argument);
argument.vt = VT_BSTR;
argument.bstrVal = SysAllocString(toWide(text).c_str());
control->call(L"SendString", &argument, 1);
VariantClear(&argument);
}
void setPtt(long on) {
VARIANT argument;
VariantInit(&argument);
argument.vt = VT_I4;
argument.lVal = on;
control->call(L"SetMmttyPTT", &argument, 1);
}
void act(const std::string& text) {
protocol::Line line = protocol::read(text);
if (line.verb == "open") {
open(line);
} else if (line.verb == "send") {
send(line.field(0));
} else if (line.verb == "ptt") {
setPtt(line.number(0));
} else if (line.verb == "post") {
control->callLong(L"PostMmttyMessage", line.number(0), line.number(1));
} else if (line.verb == "close") {
stopEngine();
} else if (line.verb == "quit") {
quitting = true;
stopEngine();
SetTimer(bridgeWindow, QuitTimer, QuitPatience, nullptr);
} else {
protocol::write("error", {"unknown verb: " + line.verb});
}
}
LRESULT CALLBACK onMessage(HWND window, UINT message, WPARAM first, LPARAM second) {
// The engine did not report itself gone, so stop waiting for it.
if (message == WM_TIMER && first == QuitTimer) {
killEngine();
PostQuitMessage(0);
return 0;
}
if (message == WM_BRIDGE_LINE) {
std::string* line = reinterpret_cast<std::string*>(second);
act(*line);
delete line;
return 0;
}
return DefWindowProcW(window, message, first, second);
}
HWND createWindow(bool show) {
WNDCLASSEXW description = {};
description.cbSize = sizeof(description);
description.lpfnWndProc = onMessage;
description.hInstance = GetModuleHandleW(nullptr);
description.lpszClassName = L"NonemmMmttyBridge";
RegisterClassExW(&description);
HWND window = CreateWindowExW(0, description.lpszClassName, L"Nonemm MMTTY bridge",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 320, 200,
nullptr, nullptr, description.hInstance, nullptr);
if (window != nullptr && show) {
ShowWindow(window, SW_SHOW);
}
return window;
}
void readLines(HWND window) {
std::string line;
while (std::getline(std::cin, line)) {
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
PostMessageW(window, WM_BRIDGE_LINE, 0, reinterpret_cast<LPARAM>(new std::string(line)));
}
PostMessageW(window, WM_QUIT, 0, 0);
}
} // namespace
int main(int count, char** arguments) {
bool show = count > 1 && std::string(arguments[1]) == "--show";
HRESULT started = CoInitialize(nullptr);
if (FAILED(started)) {
protocol::write("error", {"COM would not start"});
return 1;
}
OleInitialize(nullptr);
HWND window = createWindow(show);
bridgeWindow = window;
if (window == nullptr) {
protocol::write("error", {"the bridge window would not open"});
return 1;
}
XmmrControl xmmr(onEvent);
control = &xmmr;
std::string error;
if (!xmmr.create(window, &error)) {
protocol::write("error", {error});
return 1;
}
protocol::write("ready");
std::thread reader(readLines, window);
reader.detach();
MSG message;
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
TranslateMessage(&message);
DispatchMessageW(&message);
}
// The control's own teardown waits for an engine that has already been told
// to shut down, and never returns. The bridge has nothing left to do here,
// so it leaves and lets Wine reclaim what it held.
protocol::write("closed");
ExitProcess(0);
}