From 60f2f6651f5a69ac8c4b32c64ff04e51e04447b9 Mon Sep 17 00:00:00 2001 From: Claude Tuxedo Date: Sun, 6 Sep 2026 22:20:17 +0200 Subject: [PATCH] Add Wireless PRO support, and wait for the erase to finish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wireless PRO transmitters answer the same delete command, unchanged, both connected directly and docked in the charging case. Tested on two of them. Two things had to change to support them. Both Wireless PRO transmitters share PID 0x0056, where the Interview PRO gives its two units distinct PIDs. hid_open(vid, pid, NULL) therefore returns whichever enumerated first and cannot reach the other one, so devices are now enumerated and opened by path. A serial can be passed to target one transmitter, and 'krode list' shows what is connected. Opening by path matters beyond that: hidraw node numbers are not stable, and were observed to swap between two transmitters across a replug. Resolving the path in the same enumeration that produced the serial keeps the two consistent. The reply is also not a single ACK. The device streams progress reports, 0x02 0x4A 0x41 , stepping to 100 — the 'status 100' in the README is the device reporting 100 % complete. Reading only the first reply announced success before the erase had finished; krode now waits for 100, and shows progress on a terminal (quietly when redirected). Documented alongside: the transmitter re-enumerates afterwards and its volume returns without a filesystem until a replug, and the serial happens to be the FAT volume UUID of that transmitter's storage, which is a practical way to tell two identical transmitters apart. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 57 ++++++++++-- krode.c | 254 +++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 252 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index d1afb2d..239cf29 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # krode -Delete onboard recordings from the **Rode Interview PRO** wireless microphone transmitter on Linux. +Delete onboard recordings from **Rode wireless microphone transmitters** on Linux — Interview PRO and Wireless PRO. Rode only officially supports this through their proprietary **Rode Central** software (Windows/macOS). This tool replicates the delete function via the device's USB HID control interface, which is accessible on Linux. @@ -46,17 +46,40 @@ sudo usermod -aG plugdev $USER ## Usage -Connect the Interview PRO TX unit via USB, then: +Connect the transmitter via USB — a Wireless PRO also works while docked in its +charging case — then: ``` -krode delete +krode list ``` -This deletes all onboard recordings. The device confirms with a status code of 100 on success. +``` +Wireless PRO TX serial 800A92D6 /dev/hidraw12 +Wireless PRO TX serial 800AF63E /dev/hidraw10 +``` + +``` +krode delete # every connected transmitter +krode delete 800AF63E # just that one +``` + +The device reports progress as it erases, and `krode` waits for 100 %. + +**Two Wireless PRO transmitters share PID `0x0056`**, so a serial is the only +way to tell them apart. Conveniently the HID serial is also the FAT volume UUID +of that transmitter's storage: mount the volume, look at the filenames, and you +know which microphone a serial belongs to. + +Afterwards the transmitter re-enumerates and its storage comes back **without a +filesystem** until the device is unplugged and replugged. Waiting does not help. + +This is irreversible and there is no confirmation prompt. Copy your recordings +off first — the storage is read-only at the device level, so this command is +the only way to clear it. ## Protocol -The Interview PRO presents as a composite USB device: +These transmitters present as composite USB devices: - **Mass Storage (SCSI)** — read-only; used to copy WAV files to the host - **HID (vendor-specific)** — used for control commands @@ -67,6 +90,11 @@ Device identifiers: |------|-----|-----| | Interview PRO TX (unit 1) | `0x19F7` | `0x0063` | | Interview PRO TX (unit 2) | `0x19F7` | `0x0068` | +| Wireless PRO TX | `0x19F7` | `0x0056` | + +Note that the two Interview PRO units have distinct PIDs, while both Wireless +PRO transmitters answer to `0x0056` — which is why devices are enumerated and +opened by path, rather than with `hid_open(vid, pid, NULL)`. ### Delete command @@ -87,8 +115,15 @@ HID Output Report, 17 bytes, sent via SET_REPORT: Response arrives on endpoint `0x81` (URB_INTERRUPT IN), 17 bytes: +The device does not answer once — it streams progress reports on endpoint +`0x81` (URB_INTERRUPT IN), 17 bytes each: + ``` -02 4A 41 64 00 00 00 00 00 00 00 00 00 00 00 00 00 +02 4A 41 00 0 % +02 4A 41 05 5 % +02 4A 41 0A 10 % + ... +02 4A 41 64 100 % ``` | Byte | Value | Meaning | @@ -96,10 +131,16 @@ Response arrives on endpoint `0x81` (URB_INTERRUPT IN), 17 bytes: | `[0]` | `0x02` | HID Report ID (input) | | `[1]` | `0x4A` | Echo of command byte | | `[2]` | `0x41` | ACK (`'A'`); NAK would be `0x4E` (`'N'`) | -| `[3]` | `0x64` | Status: 100 = success | +| `[3]` | `0x00`..`0x64` | Progress, 0 to 100 in steps of 5 | + +Byte `[3]` is a **percentage**, not a status constant: `100` means the erase +finished. Stopping at the first reply reports success before the device is done. +Erasing 6.1 GB took 1.8 s. Reverse-engineered from USB traffic captured with Wireshark + USBPcap on Windows. -Confirmed working on TX unit 1 (PID `0x0063`). PID `0x0068` is untested for delete. +Confirmed working on Interview PRO TX unit 1 (PID `0x0063`) and on both +Wireless PRO transmitters (PID `0x0056`), the latter both connected directly and +docked in the charging case. Interview PRO PID `0x0068` is untested for delete. ## Support diff --git a/krode.c b/krode.c index 7ab7f46..2ab358c 100644 --- a/krode.c +++ b/krode.c @@ -1,10 +1,12 @@ /* - * krode - delete onboard recordings from Rode Interview PRO on Linux + * krode - delete onboard recordings from Rode transmitters on Linux * - * Usage: krode delete + * Usage: krode list + * krode delete [serial] * * Reverse-engineered from USB HID traffic capture. * Delete command: Report ID 0x01, command byte 0x4A, param 0x01 + * The device answers 0x02 0x4A 0x41 , counting up to 100. * * Build: gcc -o krode krode.c -lhidapi-hidraw * Deps: libhidapi-dev (Debian/Ubuntu), hidapi-devel (Fedora) @@ -13,30 +15,110 @@ */ #include +#include #include +#include +#include +#include #include -#define RODE_VID 0x19F7 +#define RODE_VID 0x19F7 -/* Both TX units seen on Interview PRO */ -#define RODE_PID_TX1 0x0063 -#define RODE_PID_TX2 0x0068 +#define REPORT_SIZE 17 +#define CMD_DELETE 0x4A +#define DELETE_ALL 0x01 +#define ACK_OK 0x41 +#define DONE_PERCENT 100 -#define REPORT_SIZE 17 -#define CMD_DELETE 0x4A -#define DELETE_ALL 0x01 +/* Reply timeout, and how long we let the device keep reporting progress. */ +#define REPLY_MS 3000 +#define PROGRESS_MS 120000 -static int delete_recordings(hid_device *dev, unsigned short pid) +struct model { + unsigned short pid; + const char *name; +}; + +/* + * Interview PRO gives its two transmitters distinct PIDs, so VID/PID alone + * identifies them. Wireless PRO does not: both transmitters answer to 0x0056, + * which is why devices are enumerated and opened by path rather than with + * hid_open(vid, pid, NULL) - that would always return the same one. + */ +static const struct model models[] = { + { 0x0063, "Interview PRO TX (unit 1)" }, + { 0x0068, "Interview PRO TX (unit 2)" }, + { 0x0056, "Wireless PRO TX" }, +}; + +static const char *model_name(unsigned short pid) +{ + size_t i; + + for (i = 0; i < sizeof(models) / sizeof(models[0]); i++) + if (models[i].pid == pid) + return models[i].name; + return NULL; +} + +static int serial_matches(const wchar_t *serial, const char *wanted) +{ + char buf[64]; + + if (!serial) + return 0; + if (wcstombs(buf, serial, sizeof(buf)) == (size_t)-1) + return 0; + return strcasecmp(buf, wanted) == 0; +} + +static void print_serial(const wchar_t *serial) +{ + if (serial && *serial) + printf("%ls", serial); + else + printf("(none)"); +} + +/* + * On a Wireless PRO the serial is also the FAT volume UUID of that + * transmitter's storage, so mounting the volume tells you which physical + * microphone a serial belongs to. + */ +static int list_devices(void) +{ + struct hid_device_info *devs, *cur; + int found = 0; + + devs = hid_enumerate(RODE_VID, 0); + for (cur = devs; cur; cur = cur->next) { + const char *name = model_name(cur->product_id); + + if (!name) + continue; + printf("%-26s serial ", name); + print_serial(cur->serial_number); + printf(" %s\n", cur->path); + found++; + } + hid_free_enumeration(devs); + + if (!found) + printf("No supported Rode transmitter found.\n"); + return found; +} + +static int delete_recordings(hid_device *dev, const char *label) { unsigned char buf[REPORT_SIZE]; - int ret; + int ret, percent = -1; memset(buf, 0, sizeof(buf)); buf[0] = 0x01; /* Report ID */ buf[1] = CMD_DELETE; /* 0x4A - delete command */ buf[2] = DELETE_ALL; /* 0x01 - delete all */ - printf("Sending delete command to PID 0x%04X...\n", pid); + printf("Deleting recordings on %s...\n", label); ret = hid_write(dev, buf, sizeof(buf)); if (ret < 0) { @@ -44,52 +126,106 @@ static int delete_recordings(hid_device *dev, unsigned short pid) return -1; } - /* Read response - device ACKs with 0x4A 0x41 0x64 */ - memset(buf, 0, sizeof(buf)); - ret = hid_read_timeout(dev, buf, sizeof(buf), 3000); - if (ret < 0) { - fprintf(stderr, "hid_read failed: %ls\n", hid_error(dev)); - return -1; - } - if (ret == 0) { - fprintf(stderr, "Timeout waiting for device response.\n"); - return -1; + /* + * The device does not answer once: it streams progress reports, + * 0x02 0x4A 0x41 , stepping to 100. Stopping at the first + * reply reports success before the erase has finished. + */ + for (;;) { + memset(buf, 0, sizeof(buf)); + ret = hid_read_timeout(dev, buf, sizeof(buf), + percent < 0 ? REPLY_MS : PROGRESS_MS); + if (ret < 0) { + fprintf(stderr, "hid_read failed: %ls\n", + hid_error(dev)); + return -1; + } + if (ret == 0) { + fprintf(stderr, + "Timeout waiting for device response.\n"); + return -1; + } + if (buf[1] != CMD_DELETE) + continue; + if (buf[2] != ACK_OK) { + fprintf(stderr, "Device refused the command (0x%02x).\n", + buf[2]); + return -1; + } + if (buf[3] != percent) { + percent = buf[3]; + /* Overwrite in place on a terminal; stay quiet when + * redirected, so a log does not fill with percents. */ + if (isatty(STDOUT_FILENO)) { + printf("\r %3d%%", percent); + fflush(stdout); + } + } + if (percent >= DONE_PERCENT) + break; } - if (buf[1] == CMD_DELETE && buf[2] == 0x41) { - printf("Done. Recordings deleted (status: %d).\n", buf[3]); - return 0; - } - - /* Print raw response for debugging */ - printf("Response: "); - for (int i = 0; i < ret; i++) - printf("%02x ", buf[i]); - printf("\n"); - + if (isatty(STDOUT_FILENO)) + printf("\r"); + printf("Done. Recordings deleted.\n"); + printf("The transmitter re-enumerates; unplug and replug it before\n" + "expecting its storage volume back.\n"); return 0; } -static int run_on_pid(unsigned short pid) +static int delete_matching(const char *serial) { - hid_device *dev; + struct hid_device_info *devs, *cur; + int found = 0; - dev = hid_open(RODE_VID, pid, NULL); - if (!dev) - return -1; /* not found, not an error if other PID works */ + devs = hid_enumerate(RODE_VID, 0); + for (cur = devs; cur; cur = cur->next) { + const char *name = model_name(cur->product_id); + char label[128]; + hid_device *dev; - delete_recordings(dev, pid); - hid_close(dev); - return 0; + if (!name) + continue; + if (serial && !serial_matches(cur->serial_number, serial)) + continue; + + snprintf(label, sizeof(label), "%s (%ls)", name, + cur->serial_number ? cur->serial_number : L"no serial"); + + /* + * Opened by path, resolved from this same enumeration: hidraw + * node numbers are not stable, and they have been observed to + * swap between two transmitters across a replug. + */ + dev = hid_open_path(cur->path); + if (!dev) { + fprintf(stderr, "Cannot open %s: %s\n", label, + cur->path); + continue; + } + delete_recordings(dev, label); + hid_close(dev); + found++; + } + hid_free_enumeration(devs); + return found; +} + +static void usage(const char *argv0) +{ + fprintf(stderr, "Usage: %s list\n", argv0); + fprintf(stderr, " %s delete [serial]\n", argv0); + fprintf(stderr, " list - show connected transmitters\n"); + fprintf(stderr, " delete - delete all onboard recordings;\n"); + fprintf(stderr, " give a serial to target one transmitter\n"); } int main(int argc, char *argv[]) { - int found = 0; + int found; - if (argc < 2 || strcmp(argv[1], "delete") != 0) { - fprintf(stderr, "Usage: %s delete\n", argv[0]); - fprintf(stderr, " delete - delete all onboard recordings\n"); + if (argc < 2) { + usage(argv[0]); return 1; } @@ -98,14 +234,30 @@ int main(int argc, char *argv[]) return 1; } - if (run_on_pid(RODE_PID_TX1) == 0) found++; - if (run_on_pid(RODE_PID_TX2) == 0) found++; + if (strcmp(argv[1], "list") == 0) { + found = list_devices(); + hid_exit(); + return found ? 0 : 1; + } + + if (strcmp(argv[1], "delete") != 0) { + usage(argv[0]); + hid_exit(); + return 1; + } + + found = delete_matching(argc > 2 ? argv[2] : NULL); if (found == 0) { - fprintf(stderr, - "No Rode Interview PRO found. Check USB connection.\n"); - fprintf(stderr, - "You may need to run as root or add a udev rule.\n"); + if (argc > 2) + fprintf(stderr, + "No transmitter with serial %s. Try: %s list\n", + argv[2], argv[0]); + else + fprintf(stderr, + "No supported Rode transmitter found. " + "Check USB connection.\n" + "You may need to run as root or add a udev rule.\n"); hid_exit(); return 1; }