From d239c214b48889e1b621422990942bf5fa3a2d67 Mon Sep 17 00:00:00 2001 From: Justin Reiners Date: Wed, 7 Oct 2026 22:45:59 -0500 Subject: [PATCH] DGM20 control for Linux: protocol library, CLI and local web UI Reverse-engineered from Maono Link 4.0.88 (DGM20_OLD.dll) and verified against a DGM20 (352f:0105, firmware 2.1.2). Co-Authored-By: Claude Opus 5.5 --- .gitignore | 5 + LICENSE | 21 +++ PROTOCOL.md | 74 ++++++++ README.md | 78 +++++++++ contrib/dgm20-web.service | 9 + docs/screenshot.png | Bin 0 -> 81226 bytes pyproject.toml | 23 +++ src/dgm20/__init__.py | 1 + src/dgm20/cli.py | 87 ++++++++++ src/dgm20/device.py | 220 +++++++++++++++++++++++ src/dgm20/static/index.html | 337 ++++++++++++++++++++++++++++++++++++ src/dgm20/web.py | 175 +++++++++++++++++++ tests/test_protocol.py | 64 +++++++ udev/70-maono-dgm20.rules | 3 + 14 files changed, 1097 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 PROTOCOL.md create mode 100644 README.md create mode 100644 contrib/dgm20-web.service create mode 100644 docs/screenshot.png create mode 100644 pyproject.toml create mode 100644 src/dgm20/__init__.py create mode 100644 src/dgm20/cli.py create mode 100644 src/dgm20/device.py create mode 100644 src/dgm20/static/index.html create mode 100644 src/dgm20/web.py create mode 100644 tests/test_protocol.py create mode 100644 udev/70-maono-dgm20.rules diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47584ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +*.egg-info/ +build/ +dist/ +.venv/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a978c00 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Justin Reiners + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PROTOCOL.md b/PROTOCOL.md new file mode 100644 index 0000000..587bf0b --- /dev/null +++ b/PROTOCOL.md @@ -0,0 +1,74 @@ +# DGM20 control protocol + +These notes come from Maono Link 4.0.88 for Windows. The app loads a per-model plugin, and the +plugin for USB `352f:0105` is `DGM20_OLD.dll`. Every value below was checked against a real mic +(firmware 2.1.2) unless it is marked otherwise. + +## Transport + +The mic has USB audio interfaces 0-2 and a HID interface 3. Interface 3 carries a vendor usage page +(`0xFF01`) with four reports: + +| report | direction | size | +|--------|-----------|------| +| 0x50 | out | 15 | +| 0x51 | out | 63 | +| 0x10 | in | 15 | +| 0x11 | in | 63 | + +The app uses only 0x51 (requests) and 0x11 (replies). On Linux the interface is a hidraw node: a +65-byte `write()` (the report id plus 64 data bytes) sends a request, and `read()` returns replies. + +## Packet + +``` +offset size field +0 1 report id: 0x51 request, 0x11 reply +1 1 0xC4 +2 1 length of bytes 1..end, including the checksum +3 2 0x0000 +5 1 type +6 n payload, u16 little-endian words +6+n 2 checksum, u16 LE = -(sum of bytes 1 .. 5+n) +``` + +The rest of the 64 bytes is zero-padded. + +| type | request payload | reply | +|------|-----------------|-------| +| 2 | (reply only) | write ack: `status` (0 = ok) | +| 3 | `addr, value` pairs | type-2 ack. The mic also sends type-3 packets on its own when a setting changes on the hardware, such as the touch pad. | +| 4 | `addr` | `addr, value` | +| 5 | `addr, count, data...` | bulk write. The app uses it only to write the serial number; don't. | +| 6 | `addr, count` | `addr, count, values...` (range read) | + +## Registers + +| reg | meaning | values | +|--------|---------|--------| +| 0x0003 | model name, 8 words, ASCII | `"DGM20"` | +| 0x000B | firmware version, 3 words | `2, 1, 2` | +| 0x001E | serial number, up to 20 words | | +| 0x1008 | DFU mode (read) | 0 = normal | +| 0x100A | factory reset | write 1 | +| 0x2022 | mute | 0 / 1 | +| 0x2026 | headphone monitoring | 2000 = on, 3000 = off | +| 0x2027 | noise cancellation | 0 / 1 | +| 0x2028 | noise cancellation strength | 0-100 | +| 0x202A | RGB ring brightness | 0-100 (the app does not show it for this model) | +| 0x202C | vocal enhancement | 0 / 1 | +| 0x204B | level-meter enable | not answered by firmware 2.1.2 | + +Maono Link reads settings once, in this order: 0x204B, then 0x2022, then a range read of 0x2027 +(count 2), then 0x2026, then 0x202C. After that it relies on type-3 packets the mic sends on its +own. + +## Present in the plugin but not used for this model + +The DLL shares widgets with the PD200X plugin, so it contains EQ and RGB-colour code. That code +is never instantiated for the DGM20, and the mic does not answer reads of these registers. They +are listed here for anyone exploring. + +- **EQ:** 5 parametric bands, each 5 words, starting at 0x204D + 5·i. The HPF is at 0x2066 and the + LPF at 0x206B. Each band is `[enable, type, freq Hz, gain dB×10+2000, Q×100]`. +- **Light on/off:** 0x2036. **Colour index:** 0x2038. diff --git a/README.md b/README.md new file mode 100644 index 0000000..431195b --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# dgm20-linux + +Control the **Maono DGM20** USB microphone on Linux. Maono Link only runs on Windows and macOS; this +covers the same settings with a local web page and a command-line tool. + +![The control page](docs/screenshot.png) + +- Noise cancellation on/off and strength +- Vocal enhancement +- Hear yourself (headphone monitoring) +- Mute, kept in sync with the mic's touch pad +- RGB ring brightness +- Factory reset + +Pure Python 3.9+, no dependencies. Nothing leaves your machine: the page is served from localhost +and loads no external fonts or scripts. + +> Unofficial. Not affiliated with or endorsed by Maono. Tested on a DGM20 with USB ID +> `352f:0105`, firmware 2.1.2. Newer DGM20 revisions (`352f:0112`) and the DGM20G/DGM20W +> use different firmware and are not supported yet. + +## Install + +```sh +pipx install git+https://git.reiners.dev/ReinersCloudConsulting/dgm20-linux.git +# or, from a checkout +pipx install . +``` + +Let your user reach the mic without root, then unplug and replug it: + +```sh +sudo cp udev/70-maono-dgm20.rules /etc/udev/rules.d/ +sudo udevadm control --reload && sudo udevadm trigger +``` + +The rule grants access to the logged-in desktop user (`uaccess`) and to the `wheel` group, which +covers ssh sessions. Change the group if your distro uses a different one. + +## Use + +```sh +dgm20-web --open # control page at http://localhost:8720 +dgm20ctl # show settings +dgm20ctl set nc on nc-level 80 enhance off +dgm20ctl watch # print changes made on the mic itself +dgm20ctl factory-reset --yes +``` + +To keep the page running in the background: + +```sh +cp contrib/dgm20-web.service ~/.config/systemd/user/ +systemctl --user enable --now dgm20-web +``` + +`dgm20-web` only listens on localhost. `--host 0.0.0.0` makes it reachable from your network, and +anyone on that network can then change your mic settings, because there is no login. + +Input gain and headphone volume are standard USB audio controls. Set them in your desktop's sound +settings, `pavucontrol` or `alsamixer`. The mic does not expose them on its control channel. + +## How it works + +The settings were found by reading Maono Link's DGM20 plugin. See [PROTOCOL.md](PROTOCOL.md) for +the packet format and register map. `dgm20ctl raw` gives direct register access if you want to +explore. + +## Development + +```sh +PYTHONPATH=src python3 -m unittest discover -s tests +PYTHONPATH=src python3 -m dgm20.web +``` + +## License + +MIT diff --git a/contrib/dgm20-web.service b/contrib/dgm20-web.service new file mode 100644 index 0000000..9aca271 --- /dev/null +++ b/contrib/dgm20-web.service @@ -0,0 +1,9 @@ +[Unit] +Description=Maono DGM20 web control panel (http://localhost:8720) + +[Service] +ExecStart=%h/.local/bin/dgm20-web +Restart=on-failure + +[Install] +WantedBy=default.target diff --git a/docs/screenshot.png b/docs/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..458eed03ebc32c6cae65e0dbea931dfbb420085d GIT binary patch literal 81226 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"] +build-backend = "setuptools.build_meta" + +[project] +name = "dgm20-linux" +version = "0.1.0" +description = "Control the Maono DGM20 USB microphone on Linux: noise cancellation, monitoring, vocal enhancement, mute, RGB brightness" +readme = "README.md" +license = "MIT" +requires-python = ">=3.9" +dependencies = [] +classifiers = ["Operating System :: POSIX :: Linux", "Topic :: Multimedia :: Sound/Audio"] + +[project.scripts] +dgm20ctl = "dgm20.cli:main" +dgm20-web = "dgm20.web:main" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +dgm20 = ["static/*.html"] diff --git a/src/dgm20/__init__.py b/src/dgm20/__init__.py new file mode 100644 index 0000000..6919f31 --- /dev/null +++ b/src/dgm20/__init__.py @@ -0,0 +1 @@ +"""Linux control for the Maono DGM20 USB microphone.""" diff --git a/src/dgm20/cli.py b/src/dgm20/cli.py new file mode 100644 index 0000000..7cdb30f --- /dev/null +++ b/src/dgm20/cli.py @@ -0,0 +1,87 @@ +"""dgm20ctl: command-line control for the Maono DGM20.""" +import argparse +import sys + +from .device import SETTINGS, DeviceError, Mic, describe + + +def fmt(value): + if value is None: + return "?" + if isinstance(value, bool): + return "on" if value else "off" + return str(value) + + +def main(argv=None): + ap = argparse.ArgumentParser( + prog="dgm20ctl", + description="Control Maono DGM20 microphone settings.", + epilog="settings: " + ", ".join(f"{n} ({describe(e)})" for n, (_, e, _) in SETTINGS.items())) + ap.add_argument("--dev", help="hidraw node (default: autodetect)") + sub = ap.add_subparsers(dest="cmd") + sub.add_parser("status", help="show current settings (default)") + sub.add_parser("info", help="model and firmware version") + sub.add_parser("watch", help="print changes made on the mic itself") + s = sub.add_parser("set", help="change settings, e.g. set nc on nc-level 80") + s.add_argument("pairs", nargs="+", metavar="NAME VALUE") + r = sub.add_parser("factory-reset", help="restore factory defaults") + r.add_argument("--yes", action="store_true", help="confirm the reset") + raw = sub.add_parser("raw", help="raw register access, for exploring") + raw.add_argument("op", choices=["get", "set", "dump"]) + raw.add_argument("addr", type=lambda v: int(v, 0)) + raw.add_argument("value", type=lambda v: int(v, 0), nargs="?", help="value (set) or count (dump)") + a = ap.parse_args(argv) + + if a.cmd == "set": + if len(a.pairs) % 2: + ap.error("set takes NAME VALUE pairs") + for name in a.pairs[0::2]: + if name not in SETTINGS: + ap.error(f"unknown setting {name!r}; known: {', '.join(SETTINGS)}") + if a.cmd == "factory-reset" and not a.yes: + ap.error("factory-reset resets every setting on the mic; rerun with --yes") + + try: + with Mic(a.dev) as mic: + run(mic, a) + except (DeviceError, ValueError) as e: + sys.exit(f"dgm20ctl: {e}") + except KeyboardInterrupt: + pass + + +def run(mic, a): + if a.cmd in (None, "status"): + for name, value in mic.state().items(): + print(f"{name:11} {fmt(value):>5} {SETTINGS[name][2]}") + elif a.cmd == "info": + for k, v in mic.info().items(): + print(f"{k + ':':9} {v or '?'}") + elif a.cmd == "watch": + print("watching for changes made on the mic, ctrl-c to stop") + for name, value in mic.events(): + print(f"{name} = {fmt(value)}", flush=True) + elif a.cmd == "set": + for name, value in zip(a.pairs[0::2], a.pairs[1::2]): + print(f"{name} = {fmt(mic.set(name, value))}") + elif a.cmd == "factory-reset": + print("settings restored to factory defaults" if mic.factory_reset() + else "the mic rejected the reset") + elif a.op == "get": + v = mic.read(a.addr) + print("no reply" if v is None else v) + elif a.op == "set": + if a.value is None: + raise ValueError("raw set needs a value") + print(f"{a.addr:#06x} = {mic.read(a.addr)}" if mic.write(a.addr, a.value) else "rejected") + else: + vals = mic.read_range(a.addr, a.value or 1) + if vals is None: + raise DeviceError("no reply") + for i, v in enumerate(vals): + print(f"{a.addr + i:#06x} {v:5} {v:#06x}") + + +if __name__ == "__main__": + main() diff --git a/src/dgm20/device.py b/src/dgm20/device.py new file mode 100644 index 0000000..e2e7c17 --- /dev/null +++ b/src/dgm20/device.py @@ -0,0 +1,220 @@ +"""Maono DGM20 vendor HID protocol. + +The mic exposes a vendor HID channel next to its USB audio interfaces. The +host writes output report 0x51 and the mic answers with input report 0x11. +Both share one framing: + + [0] report id (0x51 out / 0x11 in) + [1] 0xC4 magic + [2] length of bytes [1..] including the checksum + [3-4] 00 00 + [5] type: 2 = write ack (payload u16 status, 0 = ok) + 3 = write (addr,val) pairs; the mic also sends these when + a setting changes on the hardware + 4 = read one register + 5 = bulk write + 6 = read a range (addr, count) + [6..] payload, u16 little-endian + [-2:] checksum u16 LE = -(sum of bytes [1..end-2]) + +Settings are 16-bit registers. Input gain and headphone volume are standard +USB audio controls and are not part of this channel. +""" +import glob +import os +import select +import threading + +VID, PID = 0x352F, 0x0105 +REPORT_OUT, REPORT_IN, MAGIC = 0x51, 0x11, 0xC4 +T_ACK, T_WRITE, T_READ, T_BULK, T_RANGE = 2, 3, 4, 5, 6 + +# name: (register, encoding, description) +# encodings: "bool" = 0/1, (lo, hi) = integer range, dict = word -> raw value +SETTINGS = { + "mute": (0x2022, "bool", "Mute the mic (same as the touch pad)"), + "nc": (0x2027, "bool", "Noise cancellation"), + "nc-level": (0x2028, (0, 100), "Noise cancellation strength"), + "enhance": (0x202C, "bool", "Vocal enhancement"), + "monitor": (0x2026, {"on": 2000, "off": 3000}, "Hear yourself in headphones"), + "brightness": (0x202A, (0, 100), "RGB ring brightness"), +} +REG_FACTORY_RESET = 0x100A +REG_DFU = 0x1008 +REG_MODEL, REG_VERSION = 0x0003, 0x000B + + +class DeviceError(Exception): + pass + + +def find_hidraw(): + for node in sorted(glob.glob("/sys/class/hidraw/hidraw*")): + try: + with open(os.path.join(node, "device/uevent")) as f: + uevent = f.read() + except OSError: + continue + if f"HID_ID=0003:{VID:08X}:{PID:08X}" in uevent: + return "/dev/" + os.path.basename(node) + return None + + +def frame(typ, words): + body = bytearray([MAGIC, 0, 0, 0, typ]) + for w in words: + body += bytes([w & 0xFF, (w >> 8) & 0xFF]) + body[1] = len(body) + 2 + ck = -sum(body) & 0xFFFF + body += bytes([ck & 0xFF, ck >> 8]) + return bytes([REPORT_OUT]) + bytes(body) + bytes(64 - len(body)) + + +def parse(r): + """Return (type, [u16 words]) for a well-formed reply, else None.""" + if len(r) < 8 or r[0] != REPORT_IN or r[1] != MAGIC: + return None + n = r[2] + if n < 7 or n + 1 > len(r) or (sum(r[1:n - 1]) + (r[n - 1] | r[n] << 8)) & 0xFFFF: + return None + payload = r[6:n - 1] + return r[5], [payload[i] | payload[i + 1] << 8 for i in range(0, len(payload) - 1, 2)] + + +def decode(name, raw): + """Raw register value -> friendly value (bool, int or str).""" + enc = SETTINGS[name][1] + if raw is None: + return None + if enc == "bool": + return bool(raw) + if isinstance(enc, dict): + return next((k for k, v in enc.items() if v == raw), raw) + return raw + + +def encode(name, value): + """Friendly value (bool, int, or 'on'/'off'/number string) -> raw register value.""" + if name not in SETTINGS: + raise ValueError(f"unknown setting {name!r}; known: {', '.join(SETTINGS)}") + enc = SETTINGS[name][1] + if isinstance(enc, tuple): + if isinstance(value, str) and value.strip().isdigit(): + value = int(value) + if isinstance(value, int) and not isinstance(value, bool) and enc[0] <= value <= enc[1]: + return value + else: + if isinstance(value, str): + value = {"on": True, "true": True, "1": True, + "off": False, "false": False, "0": False}.get(value.strip().lower(), value) + if isinstance(value, bool): + return enc["on" if value else "off"] if isinstance(enc, dict) else int(value) + raise ValueError(f"{name}: expected {describe(enc)}, got {value!r}") + + +def describe(enc): + if isinstance(enc, tuple): + return f"{enc[0]}-{enc[1]}" + return "on|off" + + +def words_to_ascii(words): + return bytes(b for w in words for b in (w & 0xFF, w >> 8)).split(b"\0")[0].decode(errors="replace") + + +class Mic: + """One open handle to the mic. Thread-safe: each transaction holds a lock.""" + + def __init__(self, path=None): + self.path = path or find_hidraw() + if not self.path: + raise DeviceError("DGM20 (352f:0105) not found") + try: + self.fd = os.open(self.path, os.O_RDWR) + except PermissionError: + raise DeviceError(f"no permission to open {self.path}; install the udev rule") from None + except OSError as e: + raise DeviceError(f"cannot open {self.path}: {e.strerror}") from None + self.lock = threading.Lock() + self._drain() + + def close(self): + os.close(self.fd) + + def __enter__(self): + return self + + def __exit__(self, *exc): + self.close() + + def _drain(self): + while select.select([self.fd], [], [], 0)[0]: + os.read(self.fd, 64) + + def _io(self, typ, words, accept, timeout=0.5): + with self.lock: + try: + self._drain() + os.write(self.fd, frame(typ, words)) + while select.select([self.fd], [], [], timeout)[0]: + p = parse(os.read(self.fd, 64)) + if p and accept(p): + return p[1] + except OSError as e: + raise DeviceError(f"mic disconnected ({e.strerror})") from None + return None + + def read(self, addr): + w = self._io(T_READ, [addr], lambda p: p[0] == T_READ and p[1][:1] == [addr]) + return None if w is None else w[1] + + def read_range(self, addr, count): + w = self._io(T_RANGE, [addr, count], lambda p: p[0] == T_RANGE and p[1][:1] == [addr]) + return None if w is None else w[2:2 + count] + + def write(self, addr, value): + w = self._io(T_WRITE, [addr, value], lambda p: p[0] == T_ACK) + return w == [0] + + # friendly layer + + def get(self, name): + return decode(name, self.read(SETTINGS[name][0])) + + def set(self, name, value): + raw = encode(name, value) + if not self.write(SETTINGS[name][0], raw): + raise DeviceError(f"mic rejected {name} = {value}") + return self.get(name) + + def state(self): + return {name: self.get(name) for name in SETTINGS} + + def info(self): + model = self.read_range(REG_MODEL, 8) + ver = self.read_range(REG_VERSION, 3) + return { + "model": words_to_ascii(model) if model else None, + "firmware": ".".join(map(str, ver)) if ver else None, + "path": self.path, + } + + def factory_reset(self): + return self.write(REG_FACTORY_RESET, 1) + + def events(self): + """Yield (name_or_addr, value) for changes the mic reports on its own.""" + by_addr = {a: n for n, (a, _, _) in SETTINGS.items()} + while True: + select.select([self.fd], [], []) + with self.lock: + try: + p = parse(os.read(self.fd, 64)) + except OSError as e: + raise DeviceError(f"mic disconnected ({e.strerror})") from None + if not p or p[0] != T_WRITE: + continue + w = p[1] + for addr, val in zip(w[0::2], w[1::2]): + name = by_addr.get(addr) + yield (name, decode(name, val)) if name else (f"{addr:#06x}", val) diff --git a/src/dgm20/static/index.html b/src/dgm20/static/index.html new file mode 100644 index 0000000..a007c6b --- /dev/null +++ b/src/dgm20/static/index.html @@ -0,0 +1,337 @@ + + + + + +DGM20 Control + + + + +

+ + + + diff --git a/src/dgm20/web.py b/src/dgm20/web.py new file mode 100644 index 0000000..7977f0f --- /dev/null +++ b/src/dgm20/web.py @@ -0,0 +1,175 @@ +"""dgm20-web: a small local web UI for the Maono DGM20. + +Serves one page plus a JSON API. Binds to 127.0.0.1 unless told otherwise. +Requests from other sites are refused: the Host header must match, and API +writes must be application/json, which browsers won't send cross-origin +without a CORS preflight that this server never approves. +""" +import argparse +import json +import threading +import webbrowser +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from importlib.resources import files + +from .device import SETTINGS, DeviceError, Mic, describe + +INDEX = files("dgm20").joinpath("static/index.html").read_bytes() + + +class MicHolder: + """Keeps one Mic open and reopens it after an unplug.""" + + def __init__(self, path=None): + self.path, self.mic, self.info, self.lock = path, None, None, threading.Lock() + + def call(self, fn): + with self.lock: + for attempt in (1, 2): + try: + if self.mic is None: + self.mic = Mic(self.path) + self.info = self.mic.info() + return fn(self.mic) + except DeviceError: + if self.mic is not None: + try: + self.mic.close() + except OSError: + pass + self.mic = None + if attempt == 2: + raise + + def snapshot(self): + try: + state = self.call(lambda m: m.state()) + return {"connected": True, "info": self.info, "state": state} + except DeviceError as e: + return {"connected": False, "error": str(e)} + + +def schema(): + out = {} + for name, (_, enc, desc) in SETTINGS.items(): + kind = {"type": "range", "min": enc[0], "max": enc[1]} if isinstance(enc, tuple) \ + else {"type": "toggle"} + out[name] = {**kind, "label": desc, "accepts": describe(enc)} + return out + + +def make_handler(holder, allowed_hosts): + class Handler(BaseHTTPRequestHandler): + server_version = "dgm20-web" + + def log_message(self, format, *args): # noqa: A002 (stdlib signature) + pass + + def send(self, code, body, ctype="application/json"): + if not isinstance(body, bytes): + body = json.dumps(body).encode() + self.send_response(code) + self.send_header("Content-Type", ctype) + self.send_header("Content-Length", str(len(body))) + self.send_header("Cache-Control", "no-store") + self.send_header("X-Content-Type-Options", "nosniff") + self.send_header("Content-Security-Policy", + "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; " + "script-src 'self' 'unsafe-inline'; frame-ancestors 'none'") + self.end_headers() + self.wfile.write(body) + + def host_ok(self): + host = (self.headers.get("Host") or "").rsplit(":", 1)[0].strip("[]") + if host in allowed_hosts: + return True + self.send(403, {"error": "unexpected Host header"}) + return False + + def do_GET(self): + if not self.host_ok(): + return + if self.path in ("/", "/index.html"): + self.send(200, INDEX, "text/html; charset=utf-8") + elif self.path == "/api/state": + self.send(200, holder.snapshot()) + elif self.path == "/api/schema": + self.send(200, schema()) + else: + self.send(404, {"error": "not found"}) + + def do_POST(self): + if not self.host_ok(): + return + if self.headers.get("Content-Type", "").split(";")[0] != "application/json": + return self.send(415, {"error": "send application/json"}) + try: + n = int(self.headers.get("Content-Length") or 0) + body = json.loads(self.rfile.read(min(n, 4096)) or b"{}") + except (ValueError, json.JSONDecodeError): + return self.send(400, {"error": "invalid JSON"}) + try: + if self.path == "/api/set": + name, value = body.get("name"), body.get("value") + if name not in SETTINGS: + return self.send(400, {"error": f"unknown setting {name!r}"}) + holder.call(lambda m: m.set(name, value)) + self.send(200, holder.snapshot()) + elif self.path == "/api/factory-reset": + if body.get("confirm") is not True: + return self.send(400, {"error": "confirm required"}) + ok = holder.call(lambda m: m.factory_reset()) + self.send(200 if ok else 502, {"ok": ok}) + else: + self.send(404, {"error": "not found"}) + except ValueError as e: + self.send(400, {"error": str(e)}) + except DeviceError as e: + self.send(503, {"error": str(e)}) + + return Handler + + +def main(argv=None): + ap = argparse.ArgumentParser(prog="dgm20-web", description="Local web UI for the Maono DGM20.") + ap.add_argument("--host", default="127.0.0.1", + help="address to listen on (default 127.0.0.1; use 0.0.0.0 for your LAN, " + "there is no login)") + ap.add_argument("--port", type=int, default=8720) + ap.add_argument("--dev", help="hidraw node (default: autodetect)") + ap.add_argument("--open", action="store_true", help="open the page in your browser") + a = ap.parse_args(argv) + + allowed = {"localhost", "127.0.0.1", "::1"} + if a.host not in ("127.0.0.1", "localhost", "::1"): + import socket + allowed |= {a.host, socket.gethostname(), socket.getfqdn()} + if a.host in ("0.0.0.0", "::"): + allowed |= {ip for ip in socket.gethostbyname_ex(socket.gethostname())[2]} + allowed |= set(_local_ips()) + try: + srv = ThreadingHTTPServer((a.host, a.port), make_handler(MicHolder(a.dev), allowed)) + except OSError as e: + raise SystemExit(f"dgm20-web: cannot listen on {a.host}:{a.port}: {e.strerror}" + " (already running? try --port)") from None + url = f"http://{'localhost' if a.host in ('127.0.0.1', '0.0.0.0', '::') else a.host}:{a.port}/" + print(f"dgm20-web listening on {url} (ctrl-c to stop)") + if a.open: + webbrowser.open(url) + try: + srv.serve_forever() + except KeyboardInterrupt: + pass + + +def _local_ips(): + import subprocess + try: + out = subprocess.run(["hostname", "-I"], capture_output=True, text=True, timeout=2).stdout + return out.split() + except (OSError, subprocess.SubprocessError): + return [] + + +if __name__ == "__main__": + main() diff --git a/tests/test_protocol.py b/tests/test_protocol.py new file mode 100644 index 0000000..3a19c23 --- /dev/null +++ b/tests/test_protocol.py @@ -0,0 +1,64 @@ +"""Framing tests against packets captured from a real DGM20 (firmware 2.1.2).""" +import unittest + +from dgm20.device import decode, encode, frame, parse, words_to_ascii + + +def h(s): + return bytes.fromhex(s) + bytes(64 - len(bytes.fromhex(s))) + + +class Framing(unittest.TestCase): + def test_read_request_matches_app(self): + # checksum = -(0xC4+0x09+0x04+0x28+0x20) = -0x119 = 0xFEE7 + p = frame(4, [0x2028]) + self.assertEqual(p[:10].hex(" "), "51 c4 09 00 00 04 28 20 e7 fe") + self.assertEqual(len(p), 65) + + def test_write_request_checksum(self): + p = frame(3, [0x2028, 64]) + self.assertEqual(p[1:3], bytes([0xC4, 0x0B])) + self.assertEqual((sum(p[1:10]) + (p[10] | p[11] << 8)) & 0xFFFF, 0) + + def test_parse_read_reply(self): + self.assertEqual(parse(h("11 c4 0b 00 00 04 28 20 40 00 a5 fe")), (4, [0x2028, 64])) + + def test_parse_range_reply(self): + self.assertEqual(parse(h("11 c4 0f 00 00 06 27 20 02 00 01 00 40 00 9d fe")), + (6, [0x2027, 2, 1, 64])) + + def test_parse_ack(self): + self.assertEqual(parse(h("11 c4 09 00 00 02 00 00 31 ff")), (2, [0])) + + def test_parse_rejects_bad_checksum(self): + self.assertIsNone(parse(h("11 c4 0b 00 00 04 28 20 40 00 a5 ff"))) + + def test_model_name(self): + self.assertEqual(words_to_ascii([0x4744, 0x324D, 0x0030, 0]), "DGM20") + + +class Encoding(unittest.TestCase): + def test_bool(self): + self.assertEqual(encode("nc", "on"), 1) + self.assertEqual(encode("mute", False), 0) + self.assertIs(decode("nc", 1), True) + + def test_monitor_uses_2000_3000(self): + self.assertEqual(encode("monitor", True), 2000) + self.assertEqual(encode("monitor", "off"), 3000) + self.assertEqual(decode("monitor", 2000), "on") + + def test_range(self): + self.assertEqual(encode("nc-level", "0"), 0) + self.assertEqual(encode("nc-level", 100), 100) + for bad in (101, -1, True, "loud"): + with self.assertRaises(ValueError): + encode("nc-level", bad) + + def test_unknown(self): + with self.assertRaises(ValueError): + encode("volume", 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/udev/70-maono-dgm20.rules b/udev/70-maono-dgm20.rules new file mode 100644 index 0000000..0f90c7c --- /dev/null +++ b/udev/70-maono-dgm20.rules @@ -0,0 +1,3 @@ +# Maono DGM20 USB microphone: vendor HID channel for dgm20ctl. +# uaccess covers local desktop logins; the group covers ssh/tmux sessions. +SUBSYSTEM=="hidraw", ATTRS{idVendor}=="352f", ATTRS{idProduct}=="0105", MODE="0660", GROUP="wheel", TAG+="uaccess"