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; }