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
287 lines
9.6 KiB
C++
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);
|
|
}
|