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
This commit is contained in:
2026-08-31 21:35:45 +00:00
parent 76b57cee1d
commit c8de74e24a
70 changed files with 4560 additions and 27 deletions

44
bridge/ControlSite.cpp Normal file
View File

@@ -0,0 +1,44 @@
#include "ControlSite.h"
STDMETHODIMP ControlSite::QueryInterface(REFIID iid, void** out) {
if (iid == IID_IUnknown || iid == IID_IOleClientSite) {
*out = static_cast<IOleClientSite*>(this);
} else if (iid == IID_IOleWindow || iid == IID_IOleInPlaceSite) {
*out = static_cast<IOleInPlaceSite*>(this);
} else if (iid == IID_IOleInPlaceUIWindow || iid == IID_IOleInPlaceFrame) {
*out = static_cast<IOleInPlaceFrame*>(this);
} else {
*out = nullptr;
return E_NOINTERFACE;
}
AddRef();
return S_OK;
}
STDMETHODIMP_(ULONG) ControlSite::AddRef() {
return ++references_;
}
STDMETHODIMP_(ULONG) ControlSite::Release() {
ULONG left = --references_;
if (left == 0) {
delete this;
}
return left;
}
STDMETHODIMP ControlSite::GetWindowContext(IOleInPlaceFrame** frame,
IOleInPlaceUIWindow** document, LPRECT position,
LPRECT clip, LPOLEINPLACEFRAMEINFO info) {
*frame = static_cast<IOleInPlaceFrame*>(this);
AddRef();
*document = nullptr;
GetClientRect(parent_, position);
*clip = *position;
info->cb = sizeof(OLEINPLACEFRAMEINFO);
info->fMDIApp = FALSE;
info->hwndFrame = parent_;
info->haccel = nullptr;
info->cAccelEntries = 0;
return S_OK;
}

70
bridge/ControlSite.h Normal file
View File

@@ -0,0 +1,70 @@
#pragma once
#include <windows.h>
#include <ole2.h>
// The least a window has to provide for an ActiveX control to activate in it.
// XMMT.ocx draws nothing we look at, so every method that decides layout,
// menus or scrolling answers with the default.
class ControlSite : public IOleClientSite, public IOleInPlaceSite, public IOleInPlaceFrame {
public:
explicit ControlSite(HWND parent) : parent_(parent) {}
virtual ~ControlSite() = default;
STDMETHODIMP QueryInterface(REFIID iid, void** out) override;
STDMETHODIMP_(ULONG) AddRef() override;
STDMETHODIMP_(ULONG) Release() override;
// IOleClientSite
STDMETHODIMP SaveObject() override { return S_OK; }
STDMETHODIMP GetMoniker(DWORD, DWORD, IMoniker** moniker) override {
*moniker = nullptr;
return E_NOTIMPL;
}
STDMETHODIMP GetContainer(IOleContainer** container) override {
*container = nullptr;
return E_NOINTERFACE;
}
STDMETHODIMP ShowObject() override { return S_OK; }
STDMETHODIMP OnShowWindow(BOOL) override { return S_OK; }
STDMETHODIMP RequestNewObjectLayout() override { return E_NOTIMPL; }
// IOleWindow, shared by the site and the frame
STDMETHODIMP GetWindow(HWND* window) override {
*window = parent_;
return S_OK;
}
STDMETHODIMP ContextSensitiveHelp(BOOL) override { return E_NOTIMPL; }
// IOleInPlaceSite
STDMETHODIMP CanInPlaceActivate() override { return S_OK; }
STDMETHODIMP OnInPlaceActivate() override { return S_OK; }
STDMETHODIMP OnUIActivate() override { return S_OK; }
STDMETHODIMP GetWindowContext(IOleInPlaceFrame** frame, IOleInPlaceUIWindow** document,
LPRECT position, LPRECT clip,
LPOLEINPLACEFRAMEINFO info) override;
STDMETHODIMP Scroll(SIZE) override { return E_NOTIMPL; }
STDMETHODIMP OnUIDeactivate(BOOL) override { return S_OK; }
STDMETHODIMP OnInPlaceDeactivate() override { return S_OK; }
STDMETHODIMP DiscardUndoState() override { return E_NOTIMPL; }
STDMETHODIMP DeactivateAndUndo() override { return E_NOTIMPL; }
STDMETHODIMP OnPosRectChange(LPCRECT) override { return S_OK; }
// IOleInPlaceUIWindow
STDMETHODIMP GetBorder(LPRECT) override { return E_NOTIMPL; }
STDMETHODIMP RequestBorderSpace(LPCBORDERWIDTHS) override { return E_NOTIMPL; }
STDMETHODIMP SetBorderSpace(LPCBORDERWIDTHS) override { return E_NOTIMPL; }
STDMETHODIMP SetActiveObject(IOleInPlaceActiveObject*, LPCOLESTR) override { return S_OK; }
// IOleInPlaceFrame
STDMETHODIMP InsertMenus(HMENU, LPOLEMENUGROUPWIDTHS) override { return E_NOTIMPL; }
STDMETHODIMP SetMenu(HMENU, HOLEMENU, HWND) override { return S_OK; }
STDMETHODIMP RemoveMenus(HMENU) override { return E_NOTIMPL; }
STDMETHODIMP SetStatusText(LPCOLESTR) override { return S_OK; }
STDMETHODIMP EnableModeless(BOOL) override { return S_OK; }
STDMETHODIMP TranslateAccelerator(LPMSG, WORD) override { return S_FALSE; }
private:
HWND parent_;
ULONG references_ = 1;
};

123
bridge/EventSink.cpp Normal file
View File

@@ -0,0 +1,123 @@
#include "EventSink.h"
namespace {
// Turns a dispid into the event name the type library gives it, so the caller
// works in names rather than in numbers the OCX is free to renumber.
class EventSink : public IDispatch {
public:
EventSink(ITypeInfo* events, SinkHandler handler)
: events_(events), handler_(std::move(handler)) {
if (events_ != nullptr) {
events_->AddRef();
}
}
virtual ~EventSink() {
if (events_ != nullptr) {
events_->Release();
}
}
STDMETHODIMP QueryInterface(REFIID iid, void** out) override {
if (iid == IID_IUnknown || iid == IID_IDispatch || iid == source_) {
*out = static_cast<IDispatch*>(this);
AddRef();
return S_OK;
}
*out = nullptr;
return E_NOINTERFACE;
}
STDMETHODIMP_(ULONG) AddRef() override { return ++references_; }
STDMETHODIMP_(ULONG) Release() override {
ULONG left = --references_;
if (left == 0) {
delete this;
}
return left;
}
STDMETHODIMP GetTypeInfoCount(UINT* count) override {
*count = 0;
return S_OK;
}
STDMETHODIMP GetTypeInfo(UINT, LCID, ITypeInfo**) override { return E_NOTIMPL; }
STDMETHODIMP GetIDsOfNames(REFIID, LPOLESTR*, UINT, LCID, DISPID*) override {
return E_NOTIMPL;
}
STDMETHODIMP Invoke(DISPID dispid, REFIID, LCID, WORD, DISPPARAMS* arguments, VARIANT*,
EXCEPINFO*, UINT*) override {
handler_(nameOf(dispid), arguments);
return S_OK;
}
void setSource(REFIID source) { source_ = source; }
private:
std::wstring nameOf(DISPID dispid) {
BSTR name = nullptr;
if (events_ == nullptr ||
FAILED(events_->GetDocumentation(dispid, &name, nullptr, nullptr, nullptr))) {
return L"";
}
std::wstring found(name, SysStringLen(name));
SysFreeString(name);
return found;
}
ITypeInfo* events_;
SinkHandler handler_;
IID source_ = IID_NULL;
ULONG references_ = 1;
};
} // namespace
ITypeInfo* sourceTypeInfo(IDispatch* control, IID* source) {
IProvideClassInfo2* provider = nullptr;
if (FAILED(control->QueryInterface(IID_IProvideClassInfo2,
reinterpret_cast<void**>(&provider)))) {
return nullptr;
}
ITypeInfo* events = nullptr;
if (SUCCEEDED(provider->GetGUID(GUIDKIND_DEFAULT_SOURCE_DISP_IID, source))) {
ITypeInfo* coclass = nullptr;
if (SUCCEEDED(provider->GetClassInfo(&coclass))) {
TYPEATTR* attributes = nullptr;
if (SUCCEEDED(coclass->GetTypeAttr(&attributes))) {
for (UINT i = 0; i < attributes->cImplTypes && events == nullptr; i++) {
HREFTYPE reference = 0;
ITypeInfo* implemented = nullptr;
if (FAILED(coclass->GetRefTypeOfImplType(i, &reference)) ||
FAILED(coclass->GetRefTypeInfo(reference, &implemented))) {
continue;
}
TYPEATTR* theirs = nullptr;
if (SUCCEEDED(implemented->GetTypeAttr(&theirs))) {
if (theirs->guid == *source) {
events = implemented;
implemented->AddRef();
}
implemented->ReleaseTypeAttr(theirs);
}
implemented->Release();
}
coclass->ReleaseTypeAttr(attributes);
}
coclass->Release();
}
}
provider->Release();
return events;
}
IDispatch* createEventSink(ITypeInfo* events, REFIID source, SinkHandler handler) {
EventSink* sink = new EventSink(events, std::move(handler));
sink->setSource(source);
return sink;
}

18
bridge/EventSink.h Normal file
View File

@@ -0,0 +1,18 @@
#pragma once
#include <windows.h>
#include <ocidl.h>
#include <ole2.h>
#include <functional>
#include <string>
using SinkHandler = std::function<void(const std::wstring& name, DISPPARAMS* arguments)>;
// The events interface the control declares as its default source. Its type
// info is where the event names come from.
ITypeInfo* sourceTypeInfo(IDispatch* control, IID* source);
// A sink that turns a dispid into the event name before handing the call on, so
// the caller works in names rather than in numbers the OCX is free to renumber.
IDispatch* createEventSink(ITypeInfo* events, REFIID source, SinkHandler handler);

10
bridge/Makefile Normal file
View File

@@ -0,0 +1,10 @@
# The bridge is a 32-bit Windows program because XMMT.ocx is a 32-bit control.
CXX = i686-w64-mingw32-g++
CXXFLAGS = -std=c++17 -O2 -Wall -Wextra -DUNICODE -D_UNICODE
LDFLAGS = -static-libgcc -static-libstdc++ -static -lole32 -loleaut32 -luuid
nonemm-mmtty-bridge.exe: main.cpp XmmrControl.cpp EventSink.cpp ControlSite.cpp Protocol.cpp
$(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS)
clean:
rm -f nonemm-mmtty-bridge.exe

86
bridge/Protocol.cpp Normal file
View File

@@ -0,0 +1,86 @@
#include "Protocol.h"
#include <cstdio>
#include <cstdlib>
#include <mutex>
namespace protocol {
namespace {
std::mutex writing;
std::string escape(const std::string& field) {
std::string escaped;
for (char c : field) {
switch (c) {
case '\\': escaped += "\\\\"; break;
case '\t': escaped += "\\t"; break;
case '\r': escaped += "\\r"; break;
case '\n': escaped += "\\n"; break;
default: escaped += c;
}
}
return escaped;
}
std::string unescape(const std::string& field) {
std::string plain;
for (size_t i = 0; i < field.size(); i++) {
if (field[i] != '\\' || i + 1 == field.size()) {
plain += field[i];
continue;
}
switch (field[++i]) {
case 't': plain += '\t'; break;
case 'r': plain += '\r'; break;
case 'n': plain += '\n'; break;
default: plain += field[i];
}
}
return plain;
}
} // namespace
std::string Line::field(size_t index) const {
return index < fields.size() ? fields[index] : std::string();
}
long Line::number(size_t index) const {
return std::strtol(field(index).c_str(), nullptr, 10);
}
Line read(const std::string& text) {
Line line;
size_t start = 0;
while (true) {
size_t tab = text.find('\t', start);
std::string part = text.substr(start, tab == std::string::npos ? tab : tab - start);
if (start == 0) {
line.verb = part;
} else {
line.fields.push_back(unescape(part));
}
if (tab == std::string::npos) {
return line;
}
start = tab + 1;
}
}
void write(const std::string& verb, const std::vector<std::string>& fields) {
std::string line = verb;
for (const std::string& field : fields) {
line += '\t' + escape(field);
}
line += '\n';
std::lock_guard<std::mutex> held(writing);
std::fwrite(line.data(), 1, line.size(), stdout);
std::fflush(stdout);
}
void write(const std::string& verb, long value) {
write(verb, {std::to_string(value)});
}
} // namespace protocol

27
bridge/Protocol.h Normal file
View File

@@ -0,0 +1,27 @@
#pragma once
#include <string>
#include <vector>
// One line of the bridge protocol: a verb and its fields, separated by tabs.
// Nonemm.Digital/BridgeLine.cs is the other end of the same format.
namespace protocol {
struct Line {
std::string verb;
std::vector<std::string> fields;
// Empty when the field is not there, which keeps a short line from being an
// error on either side.
std::string field(size_t index) const;
long number(size_t index) const;
};
Line read(const std::string& line);
// Writes one line to standard output and flushes it. Safe to call from any
// thread.
void write(const std::string& verb, const std::vector<std::string>& fields = {});
void write(const std::string& verb, long value);
} // namespace protocol

205
bridge/XmmrControl.cpp Normal file
View File

@@ -0,0 +1,205 @@
#include "XmmrControl.h"
#include <ocidl.h>
#include <olectl.h>
#include "EventSink.h"
std::string toUtf8(const wchar_t* text) {
if (text == nullptr) {
return "";
}
int size = WideCharToMultiByte(CP_UTF8, 0, text, -1, nullptr, 0, nullptr, nullptr);
std::string converted(size > 0 ? size - 1 : 0, '\0');
WideCharToMultiByte(CP_UTF8, 0, text, -1, converted.data(), size, nullptr, nullptr);
return converted;
}
std::wstring toWide(const std::string& text) {
int size = MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, nullptr, 0);
std::wstring converted(size > 0 ? size - 1 : 0, L'\0');
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, converted.data(), size);
return converted;
}
bool XmmrControl::create(HWND parent, std::string* error) {
CLSID clsid;
HRESULT result = CLSIDFromProgID(L"XMMR.XMMRCtrl.1", &clsid);
if (FAILED(result)) {
*error = "XMMR.XMMRCtrl.1 is not registered in this Wine prefix";
return false;
}
result = CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER, IID_IOleObject,
reinterpret_cast<void**>(&object_));
if (FAILED(result)) {
*error = "XMMT.ocx would not load";
return false;
}
// MFC controls reject every dispatch call with E_UNEXPECTED until they are
// initialised, and this one is never loaded from a stream.
IPersistStreamInit* fresh = nullptr;
if (SUCCEEDED(object_->QueryInterface(IID_IPersistStreamInit,
reinterpret_cast<void**>(&fresh)))) {
fresh->InitNew();
fresh->Release();
}
OleSetContainedObject(object_, TRUE);
site_ = new ControlSite(parent);
object_->SetHostNames(L"Nonemm", L"Nonemm");
object_->SetClientSite(site_);
RECT area = {0, 0, 0, 0};
GetClientRect(parent, &area);
result = object_->DoVerb(OLEIVERB_INPLACEACTIVATE, nullptr, site_, 0, parent, &area);
if (FAILED(result)) {
*error = "the control would not activate";
return false;
}
if (FAILED(object_->QueryInterface(IID_IDispatch, reinterpret_cast<void**>(&dispatch_)))) {
*error = "the control has no IDispatch";
return false;
}
return listen(error);
}
bool XmmrControl::listen(std::string* error) {
IID source = IID_NULL;
ITypeInfo* events = sourceTypeInfo(dispatch_, &source);
IConnectionPointContainer* container = nullptr;
if (FAILED(dispatch_->QueryInterface(IID_IConnectionPointContainer,
reinterpret_cast<void**>(&container)))) {
*error = "the control has no connection points";
return false;
}
HRESULT found = container->FindConnectionPoint(source, &point_);
container->Release();
if (FAILED(found)) {
*error = "the control's event interface was not found";
return false;
}
sink_ = createEventSink(events, source, handler_);
if (events != nullptr) {
events->Release();
}
if (FAILED(point_->Advise(sink_, &cookie_))) {
*error = "the control refused the event sink";
return false;
}
return true;
}
void XmmrControl::destroy() {
if (point_ != nullptr && cookie_ != 0) {
point_->Unadvise(cookie_);
cookie_ = 0;
}
if (point_ != nullptr) {
point_->Release();
point_ = nullptr;
}
if (sink_ != nullptr) {
sink_->Release();
sink_ = nullptr;
}
if (dispatch_ != nullptr) {
dispatch_->Release();
dispatch_ = nullptr;
}
if (object_ != nullptr) {
object_->SetClientSite(nullptr);
object_->Release();
object_ = nullptr;
}
if (site_ != nullptr) {
site_->Release();
site_ = nullptr;
}
}
HRESULT XmmrControl::invoke(const wchar_t* name, WORD flags, DISPPARAMS* arguments,
VARIANT* result) {
if (dispatch_ == nullptr) {
return E_POINTER;
}
DISPID dispid = 0;
LPOLESTR wanted = const_cast<LPOLESTR>(name);
HRESULT found = dispatch_->GetIDsOfNames(IID_NULL, &wanted, 1, LOCALE_USER_DEFAULT, &dispid);
if (FAILED(found)) {
return found;
}
return dispatch_->Invoke(dispid, IID_NULL, LOCALE_USER_DEFAULT, flags, arguments, result,
nullptr, nullptr);
}
HRESULT XmmrControl::put(const wchar_t* name, VARIANT value) {
DISPID assigned = DISPID_PROPERTYPUT;
DISPPARAMS arguments = {&value, &assigned, 1, 1};
HRESULT result = invoke(name, DISPATCH_PROPERTYPUT, &arguments, nullptr);
VariantClear(&value);
return result;
}
HRESULT XmmrControl::putBool(const wchar_t* name, bool value) {
VARIANT wanted;
VariantInit(&wanted);
wanted.vt = VT_BOOL;
wanted.boolVal = value ? VARIANT_TRUE : VARIANT_FALSE;
return put(name, wanted);
}
HRESULT XmmrControl::putLong(const wchar_t* name, long value) {
VARIANT wanted;
VariantInit(&wanted);
wanted.vt = VT_I4;
wanted.lVal = value;
return put(name, wanted);
}
HRESULT XmmrControl::putString(const wchar_t* name, const std::string& value) {
VARIANT wanted;
VariantInit(&wanted);
wanted.vt = VT_BSTR;
wanted.bstrVal = SysAllocString(toWide(value).c_str());
return put(name, wanted);
}
HRESULT XmmrControl::call(const wchar_t* name, VARIANT* arguments, unsigned count) {
DISPPARAMS parameters = {arguments, nullptr, count, 0};
return invoke(name, DISPATCH_METHOD, &parameters, nullptr);
}
// Arguments go to a dispatch call last one first.
HRESULT XmmrControl::callLong(const wchar_t* name, long first, long second) {
VARIANT arguments[2];
VariantInit(&arguments[0]);
VariantInit(&arguments[1]);
arguments[0].vt = VT_I4;
arguments[0].lVal = second;
arguments[1].vt = VT_I4;
arguments[1].lVal = first;
return call(name, arguments, 2);
}
long XmmrControl::getLong(const wchar_t* name) {
VARIANT value;
VariantInit(&value);
DISPPARAMS none = {nullptr, nullptr, 0, 0};
if (FAILED(invoke(name, DISPATCH_PROPERTYGET, &none, &value))) {
return 0;
}
long found = SUCCEEDED(VariantChangeType(&value, &value, 0, VT_I4)) ? value.lVal : 0;
VariantClear(&value);
return found;
}
std::string XmmrControl::getString(const wchar_t* name) {
VARIANT value;
VariantInit(&value);
DISPPARAMS none = {nullptr, nullptr, 0, 0};
if (FAILED(invoke(name, DISPATCH_PROPERTYGET, &none, &value))) {
return "";
}
std::string found;
if (SUCCEEDED(VariantChangeType(&value, &value, 0, VT_BSTR))) {
found = toUtf8(value.bstrVal);
}
VariantClear(&value);
return found;
}

51
bridge/XmmrControl.h Normal file
View File

@@ -0,0 +1,51 @@
#pragma once
#include <windows.h>
#include <ole2.h>
#include <ocidl.h>
#include <functional>
#include <string>
#include "ControlSite.h"
#include "EventSink.h"
// The XMMR control out of XMMT.ocx, which is what N1MM drives MMTTY and 2Tone
// with. The control starts the engine, and the engine reports back through the
// control's events.
class XmmrControl {
public:
using EventHandler = SinkHandler;
explicit XmmrControl(EventHandler handler) : handler_(std::move(handler)) {}
~XmmrControl() { destroy(); }
bool create(HWND parent, std::string* error);
void destroy();
HRESULT put(const wchar_t* name, VARIANT value);
HRESULT putBool(const wchar_t* name, bool value);
HRESULT putLong(const wchar_t* name, long value);
HRESULT putString(const wchar_t* name, const std::string& value);
HRESULT call(const wchar_t* name, VARIANT* arguments, unsigned count);
HRESULT callLong(const wchar_t* name, long first, long second);
long getLong(const wchar_t* name);
std::string getString(const wchar_t* name);
private:
HRESULT invoke(const wchar_t* name, WORD flags, DISPPARAMS* arguments, VARIANT* result);
bool listen(std::string* error);
EventHandler handler_;
ControlSite* site_ = nullptr;
IOleObject* object_ = nullptr;
IDispatch* dispatch_ = nullptr;
IConnectionPoint* point_ = nullptr;
IUnknown* sink_ = nullptr;
DWORD cookie_ = 0;
};
std::string toUtf8(const wchar_t* text);
std::wstring toWide(const std::string& text);

256
bridge/main.cpp Normal file
View File

@@ -0,0 +1,256 @@
// 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);
}