I recently dug out an old KVM switch I was playing with back in 2005–the Linkskey LKU-UA02. It’s a compact two-output switch with two USB ports (keyboard and mouse), a VGA port, a speaker jack, and a microphone jack. It works great, but I was a little saddened after I got it and realized it had no Linux or Mac OS X drivers. It was still OK because it has a physical button for changing between which computer to control, but it also had a cool little Windows-only utility for controlling it through software or locking the audio output to a specific computer–and I couldn’t use any of those features from my Mac or a Linux machine.

In 2005 (a.k.a. my college days), I had attempted to write a control utility for it in OS X. I installed USB sniffing software on my Windows PC and recorded the USB traffic as I sent various commands through the Windows control program. I gave it my best shot, even going so far as to ask for a bit of help on Apple’s USB mailing list, but I never figured out how to get past a roadblock I was running into when opening the device in Apple’s I/O Kit. In hindsight, I was probably jumping into something just a little too complicated for my knowledge at the time. With that said, the best way to learn something in the programming world is to do it! It wasn’t wasted time; I definitely learned a lot about USB in the process. I actually believe that the device has something invalid in the descriptor for the endpoint I was using (a zero value for bInterval), and older versions of Mac OS X choked on that problem, so it may not have even been my fault–I think that was the reason I eventually gave up. In fact, if I look at the latest (as of this writing) version of AppleUSBOHCI_UIM.cpp, I see that it still complains if you try to create an interrupt endpoint with a polling rate of zero (it says, “that’s illegal!”), while the analogous UHCI controller code does not have this check.

When I stumbled upon the switch again last week, I decided to take another stab at it. This time, I decided to use libusb, which is better suited for the task given its simplicity compared to I/O Kit and its cross-platform compatibility. I ended up succeeding! Not only does it work in OS X, but it also works great in Linux and Windows 7.

The vendor ID of this KVM switch is 10d5 and the product ID is 000d. The chipset appears to be made by Uni Class, so it’s possible that other KVM switches use the exact same chipset (and thus may be compatible with my control software). In particular, some Googling seems to imply that the TRENDnet TK-407 has the exact same vendor and device ID, although it’s a 4-port KVM switch–I can see how the protocol would scale in that case to switch between four outputs, so I have left room in my code to allow switching between four outputs. Anyway, if you have a device with the same device and vendor ID, you should try it out!

Here is the code (just compile it with your favorite C compiler, remembering to link against libusb-1.0):

#include <libusb-1.0/libusb.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

// USB vendor and product ID of the KVM device
#define KVM_VENDOR_ID           0x10D5
#define KVM_DEVICE_ID           0x000D

bool is_kvm(libusb_device *dev);
void print_usage(char *argv[]);

int main(int argc, char *argv[]) {
    int exit_code = 0;

    // Make sure there is a single argument passed to the app
    if (argc != 2) {
        print_usage(argv);
        exit_code = 1;
        goto exit_err_early;
    }

    // Convert the argument to a number
    char *endptr;
    unsigned long index = strtoul(argv[1], &endptr, 10);
    if ((endptr == argv[1]) || (index > 3)) {
        print_usage(argv);
        exit_code = 1;
        goto exit_err_early;
    }

    // Initialize libusb
    int result;
    result = libusb_init(NULL);
    if (result) {
        fprintf(stderr, "Unable to initialize libusb.\n");
        exit_code = 1;
        goto exit_err_early;
    }

    // Find devices
    libusb_device **list;
    libusb_device *found = NULL;
    ssize_t cnt = libusb_get_device_list(NULL, &list);
    ssize_t i = 0;
    if (cnt < 0) {
        fprintf(stderr, "Unable to get list of USB devices.\n");
        exit_code = 1;
        goto exit_err_dev_list;
    }

    // Look for a match
    for (i = 0; i < cnt; i++) {
        libusb_device *device = list[i];
        if (is_kvm(device)) {
            found = device;
            break;
        }
    }

    // Did we find the KVM?
    if (found) {
        libusb_device_handle *handle;

        // Open the device...
        result = libusb_open(found, &handle);
        if (result) {
            fprintf(stderr, "Unable to open KVM device.\n");
            exit_code = 1;
            goto exit_error_open;
        }

        // Claim interface 1...
        result = libusb_claim_interface(handle, 1);
        if (result) {
            fprintf(stderr, "Unable to claim KVM interface.\n");
            exit_code = 1;
            goto exit_error_claim_interface;
        }

        // Send the interrupt transfer
        unsigned char transfer[8] = "\x01\x00\x00\x78\x01\x3B\x00\x00";
        transfer[1] = (unsigned char)index;
        int bytes_sent = 0;
        result = libusb_interrupt_transfer(handle,
            0x02 /* EP 2 OUT */,
            transfer,
            sizeof(transfer),
            &bytes_sent,
            0 /* no timeout */
        );

        // Check result of transfer
        if (result) {
            fprintf(stderr, "Unable to send KVM switch message.\n");
            exit_code = 1;
            goto exit_error_transfer;
        }

        // Print result
        printf("Switched to KVM output %lu.\n", index);

        // Clean up
exit_error_transfer:
        result = libusb_release_interface(handle, 1);
        if (result) {
            fprintf(stderr, "Error releasing interface 1.\n");
        }

exit_error_claim_interface:
        libusb_close(handle);
    } else {
        fprintf(stderr, "Unable to find KVM USB device.\n");
    }

exit_error_open:
    libusb_free_device_list(list, 1);
exit_err_dev_list:
    libusb_exit(NULL);
exit_err_early:
    return exit_code;
}

// Checks a libusb_device to see if it matches
bool is_kvm(libusb_device *dev) {
    // Grab the descriptor...
    struct libusb_device_descriptor desc;
    if (libusb_get_device_descriptor(dev, &desc) != 0) {
        fprintf(stderr, "Warning: Unable to get device descriptor.\n");
        return false;
    }

    // And see if the vendor/product ID matches
    if ((desc.idVendor == KVM_VENDOR_ID) &&
        (desc.idProduct == KVM_DEVICE_ID)) {
        return true;
    }

    // No match, so false
    return false;
}

// Prints usage info about the program
void print_usage(char *argv[]) {
    fprintf(stderr, "Usage: %s <index of KVM output (0-3)>\n", argv[0]);
}

I have noticed occasional error messages saying “Unable to send KVM switch message”, but despite the error, it still switches correctly. My guess is that the USB device disappears before I get a chance to completely close it. There may be a few small improvements needed in that regard, even if the improvement is just to always assume success at that point and not print an error message.

I plan on making a graphical interface (probably with Qt) for easy control in the future, but for now this command line utility works great! There are some extra features available in the KVM switch’s protocol such as automatically cycling between the outputs, fixing the audio port to a specific output, and determining which output is the currently active output. I haven’t implemented any of that extra protocol yet, but it shouldn’t be too difficult to do.

I do want to figure out how to do the equivalent task in I/O Kit at some point just for my own sanity and for some closure on the problem I had back in 2005, but it’s not high on my priority list. After my research into Apple’s open source code described above, I’m fairly sure that I still won’t be able to make it work on an older PowerPC computer at all. It will probably work OK with newer Intel computers, though. In fact, it would be interesting to test the libusb solution on a PowerPC computer to see if it works at all (my guess is it won’t work). So much to try and so little time!

As you may recall from the past, I’ve taken a Mac IIci and hacked a new startup sound into it. Then, later, I actually had a programmable ROM SIMM PCB manufactured that works with many Macs from the late 1980s and early 1990s. I have yet to talk about it on this blog, but I have also created a USB programmer for the ROM SIMMs. Maybe I’ll create a post about that later. My post today is about another startup sound hack I’ve just completed. I have a video below, so check it out!

I’ve moved on to the next stage of my Mac ROM startup chime hacking, toward the late 90s to mid 2000s machines — the New World PowerPC machines. These computers are powered by the PowerPC G3, G4, and G5, starting with the original iMac. They call them “New World” because they use a totally different style of ROM from previous Macs. They have a 1 MB boot ROM on the logic board which does the initial setup of the machine. The boot ROM also provides Open Firmware which gives them a lot of flexibility for doing things before the operating system has loaded. Open Firmware is capable of loading a file from the hard drive (typically called Mac OS ROM) which contains the rest of the ROM that older machines had on a mask ROM chip on the logic board.

I discovered that the startup chime on these Macs is also stored in the 1 MB boot ROM. I have several New World Macs, and they all have the exact same startup chime. I played around with the ROM from my G3 Blue and White by opening a dump of it as raw sound data in Audacity and found the sound, but I could tell it was compressed. After some detective work combined with lucky guesses, I figured out that the sound is a chunk of 58,548 bytes compressed using Apple’s version (IMA 4:1) of the IMA ADPCM compression format. The uncompressed sound is a 44.1 kHz, 16-bit mono sound that is just under 2.5 seconds long–so anything that is 2.5 seconds or shorter should work fine as a replacement. I’ve looked at the ROM from all of my other New World Macs, and the sound is stored in the exact same format in all of their ROMs as well. I would venture a guess that it’s stored in that exact same format in all New World Macs.

OK–so at this point, I knew it could be done. My past customization experiments have required hardware modifications to change the chime. These machines are different, though–the “boot ROM” (as I have been calling it) is actually a flash chip that can be programmed in-system. Apple created firmware updates for several of the New World Macs. The firmware update just re-flashes the chip with new data. I could desolder the chip, reprogram it, and solder it back on, but why bother if there’s a way to do it directly from software? So I grabbed Apple’s G3 Firmware Update 1.1 and inspected it. It’s just an Open Firmware Forth script that contains the new ROM data to flash to the chip. It has code for programming flash chips made by several manufacturers. I wasn’t very familiar with Forth (and still don’t know it very well), but I went through some excellent tutorials to get a basic feel for the language. The data to be flashed is encoded using a variant of Ascii85 encoding and there are several Adler-32 checksums to confirm the file’s integrity.

After mcdermd from the 68k Mac Liberation Army forums was kind enough to send me a few spare logic boards for my G3 (I didn’t want to mess with my existing logic board), I went ahead and injected my own chime into the firmware update file. This required encoding a sound (I used the old startup sound used by the Quadra 700 and newer 68030 and 68040 Macs) in IMA 4:1 format, overwriting the old sound with the new raw sound data, re-encoding the ROM in Ascii85, and recalculating all of the Adler-32 checksums. I also had to patch Apple’s firmware updater program to force the firmware to always update even if it thinks the firmware is already up to date.

I did the firmware update procedure in Mac OS 9 after patching all of the programs, and sure enough, everything worked fine! Here’s a video:

This technique will probably work for all other New World Macs–everything from the original iMac up through the G5s before Apple switched to Intel processors. Some of these machines never got a firmware update from Apple, though, so it may be difficult to figure out what the Forth script should do for controlling the flash chips in them. Hopefully it stayed pretty consistent across the various models, but I just don’t know at this time. It looks like some of the newer firmware updates no longer use Ascii85 encoding, so things definitely changed in the firmware updaters as time went on. Oh well–this is a start!

Someday in the (hopefully near) future I will document everything I had to do in more detail and maybe automate it with some scripts. Because I have to mess with both the data fork and resource fork of files, I won’t be able to automate it all on a Linux or Windows computer, I think. But I can at least automate creating the data fork that will need to be injected into the firmware update file and provide instructions for using ResEdit to patch the firmware updater utility.

Update:

I am releasing the code I used to patch the chime in the G3 Firmware file bundled with the latest version (1.1) of the Power Mac G3 Blue & White firmware updater. If you want to do this for yourself, download the code and compile it on a Linux or Mac OS X machine with g++. It’s a command line utility that can patch the original G3 Firmware file with a new startup chime. The comments in the code should explain everything. Use Audacity to make a raw 44.1 kHz 16-bit big endian sound file to encode using this tool. When you’re finished, put it onto a Mac and use BBEdit Lite to copy the data fork of the newly patched file onto the original G3 Firmware file. Finally, you will need to patch the firmware updater program’s data fork in order for it to recognize the newest firmware as a valid update file. See what I said in my 68kmla forum thread. Once that’s done, you can run the updater program. After the update is done, it may complain at every boot that the firmware update was unsuccessful. If this happens, just remove the firmware updater program from your startup items, or replace the firmware updater program with the original and reboot once more time.

BEWARE: If you have a G4 chip in your G3, this firmware update will disable compatibility with the G4, rendering your system unbootable without a G3 processor installed. I haven’t yet figured out how to integrate the G4 enabler patch that G4 upgrade kits included. For now, only do this if your system has a G3 processor in it.

BEWARE #2: Doing this procedure can brick your computer if something goes wrong. Do this AT YOUR OWN RISK!

Download G3ChimePatcher.tar.gz

I hope to make the process more seamless in the future, but for now, if you know how to compile code and can mess around with data forks of files, and you’re willing to take a risk that your computer might be bricked, it’s worth a shot. I do not have the knowledge to make this work on any New World Mac. For now, it’s only limited to the blue and white G3.