Files
Nonemm/bridge/main.cpp
ericek111 54b9181c06 Let MMTTY hold the text waiting to go out, behind a setting
MMTTY has a type-ahead buffer of its own: characters go into it, a backspace
takes back one it has not transmitted, and TxBufLen says how many are left. Used
that way it paces itself, so there is no gap between characters to tune and no
baud rate to keep in step with the engine.

EngineTypeAhead does that, and Config > Digital picks between it and the pump
that is there now. Off is still the default: three things it rests on have never
been seen with a real engine.

tools/Nonemm.EngineProbe asks the engine those three questions and writes the
answers to a file. It starts MMTTY through the bridge, pushes a message, polls
TxBufLen while it goes out, backspaces over text that has and has not been
transmitted, and logs what came back on the receive side and when.

The bridge learns one verb for it: `buffer` reads TxBufLen and answers with the
count, or -1 when the control will not say. A property the control does not know
is a log line rather than an error, since it stops nothing.

TypeAhead and EngineTypeAhead share the TransmitBuffer interface, which is what
the digital window now works through, so the window does not know which one it
has.

docs/unfinished.md states what each buffer assumes and how to run the probe.

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

277 lines
9.3 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);
}
// 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 == "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);
}