Files
Nonemm/bridge/main.cpp
ericek111 917a05b898 Key MMTTY the way N1MM does, with the control's PTT property
Nothing was ever transmitted. SetMmttyPTT does not start a transmission: N1MM
calls it with 1 to stop once the buffer is empty, which is its XmitOff, and with
0 to stop now, which is its AbortXmit. A transmission starts by setting the
control's PTT property, in XmitOn.

So the bridge learns `key <0|1>` for that property, and MmttyEngine now keys
with it, ends a message with SetMmttyPTT(1) so the buffer still goes out, and
aborts with SetMmttyPTT(0). This is the digital {TX} in the entry window and the
digital window as well as the probe: none of them could key the engine before.

The probe checks that the engine keys before it measures anything, and stops
with a plain statement if it does not, rather than reporting numbers from an
engine sitting still. It also asks a new question: whether the engine holds a
word until the space after it, which is MMTTY's Way to send. Received characters
are marked as noise while the engine is not transmitting, since a machine with a
sound card decodes the band all the way through the run.

The first run on a real engine says MMTTY 1.70 connects, the control answers
TxBufLen and refuses NotAProperty with DISP_E_UNKNOWNNAME. What TxBufLen counts
is still open: read while nothing was transmitting it rose over time and rose by
four after four backspaces, which is not what characters-left would do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtspmWmS7f8kUvcyaHpRWZ
2026-09-01 22:51:43 +00:00

287 lines
9.6 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);
}
// N1MM's {TX}: the control's own PTT property, which is what starts a
// transmission. SetMmttyPTT is the other direction, and only that.
void key(bool on) {
report("PTT", control->putBool(L"PTT", on));
}
// 1 stops when the buffer is empty, which is N1MM's XmitOff; 0 stops now,
// which is its AbortXmit.
void setPtt(long on) {
VARIANT argument;
VariantInit(&argument);
argument.vt = VT_I4;
argument.lVal = on;
control->call(L"SetMmttyPTT", &argument, 1);
}
// How many characters MMTTY still has to transmit. It is asked for rather than
// reported: the control has no event for it.
void askBuffer(const std::string& name) {
const std::wstring wanted = toWide(name.empty() ? "TxBufLen" : name);
long left = 0;
HRESULT result = control->tryGetLong(wanted.c_str(), &left);
if (FAILED(result)) {
char message[128];
std::snprintf(message, sizeof(message), "%s failed: 0x%08lx", wanted.empty() ? "" : name.c_str(),
static_cast<unsigned long>(result));
// not an "error": a property the control will not answer stops nothing
protocol::write("log", {message});
protocol::write("buffer", -1);
return;
}
protocol::write("buffer", left);
}
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 == "key") {
key(line.number(0) != 0);
} else if (line.verb == "ptt") {
setPtt(line.number(0));
} else if (line.verb == "buffer") {
askBuffer(line.field(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);
}