// 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 #include #include #include #include #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(wanted)) { PostMessageW(window, WM_CLOSE, 0, 0); } return TRUE; } void stopEngine() { control->callLong(L"PostMmttyMessage", MessageShutdown, 0); HWND engine = reinterpret_cast(static_cast(control->getLong(L"hWndMmtty"))); if (engine == nullptr) { return; } GetWindowThreadProcessId(engine, &enginePid); EnumWindows(closeWindow, static_cast(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(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(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(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(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); }