Category Archives: Hardware

Uniquely Identifying RawInput Devices in AutoHotKey

80
Filed under AutoHotKey, Hardware, MP3s

NOTE: My take on this has changed slightly, as a result of some feedback. See the end of this post and the "Better Alternate Solution” for details.

I recently completed restoration of an old 1930’s era console radio into a fully modern touch-screen mp3-playing networked jukebox, and have been putting the finishing touches on the loaded software configuration for a few weeks.

Jukebox Original CL Photo

WP_000576

Radio as we found it, stuffed at the back of a barn/storage area

After the conversion, that touchscreen you see automatically folds and the top lowers back down to it’s original position when not in use

There were 3 knobs on the original unit. The actual knobs were long gone, but the holes were still there and I wanted some old-school tactile controls for this thing, as well as the touchscreen.

A few ebay searches and junk drawers scavanges later, and I had 3 very serviceable knobs for the front. You can see them in the photo, the two large dial knobs and the one small brass knob on the center plate.

WP_000498

Now that that was taken care of, I had to determine

  1. What I wanted them to do
  2. How to make them actually do that

Believe it or not, the “what” has actually turned out to be harder than the “how”

All About the “How”

Realistically, the what doesn’t much matter from a technical standpoint. Controlling volume, track playback etc is pretty trivial stuff with most software these days. So I’ll focus on the “how”.

I’d used a macro application called AutoHotKey on several occasions, and, while it’s programming language is quirky to put it mildly, it does get the job done, particularly when the job consists of converting one type of computer input into another.

In my case, each of those knobs it connected, via a typical encoder wheel, to the inputs of a standard 3 button optical mouse with middle wheel. Yes, you heard that right, there are three mice lurking in that cabinet (a forth if you count the real mouse that I use when working on it).

The Three Blind Mice

If you’ve ever hooked up 2 or more mice to a Windows PC, you know that they all control the single cursor. There’s no clear way to tell one mouse’s input from the other. Windows does that intentionally because it generally makes sense.

However, underneath the covers, there’s an API that exposes all input devices uniquely, meaning you CAN actually tell whether one mouse or another is being moved, clicked, scrolled or what not. It’s called the “RawInput” API, and it’s been there ever since Windows XP (and possibly even earlier, though I’m not sure of that).

Unfortunately, the RawInput functions are not the easiest functions to deal with in the world. Lots of C-style structure parsing and pointer juggling.

AHKHID to the Rescue

Fortunately though, for AutoHotKey users, a user named “theGood” posted AHKHID.ahk, an AutoHotKey script of functions to make working with HID devices (Human Interface Devices) at the RawInput level relatively straightforward.

I’d been able to craft up a script to detect which of my mice were been wheeled and clicked and react accordingly, so things were good.

Until I unplugged the mouse I used for working on the jukebox, and the keyboard.

When I booted it back up, boom, nothing worked right.

An ID isn’t an ID

I had been using the function AHKHID_GetDevIndex, as in:

id := AHKHID_GetDevIndex(h)

in order to retrieve a device ID and I’d assumed that ID would consistently identify a device. And I’d assumed wrong. Unplugging my actual mouse and keyboard had caused the devices to get renumbered on reboot, a common issue with Windows applications.

A Nasty Solution

The AHKHID script includes a function called AHKHID_GetDevName() that retrieves a unique “device name”, but that “name” is huge, consisting of a number of segments of hex number, plus an additional entire GUID, something along these lines:

USB\VID_045E&PID_00DD&REV_0173&MI_00{745a17a0-74d3-11d0-b6fe-00a0c90f57da}

Obviously, this could be used for a key, but it’s a bit unwieldy.

So, to shorten the key name a bit and make it easier to work with, I came up with AHKHID_GetDevKey()

AHKHID_GetDevKey(i, IsHandle = false) {
    ;generate a unique name from the DevName (which is huge, and usually includes a useless GUID)
    devname := AHKHID_GetDevName(i, IsHandle)
    StringReplace, devname, devname, Hid#Vid_, , All
    StringReplace, devname, devname, USBVid_, , All
    StringReplace, devname, devname, KODAK, , All
    StringReplace, devname, devname, &Pid_, , All
    StringReplace, devname, devname, &MI_, , All
    StringReplace, devname, devname, #7&, , All
    StringReplace, devname, devname, #, , All
    StringReplace, devname, devname, &, , All
    StringReplace, devname, devname, \, , All
    StringReplace, devname, devname, ?, , All
    p := Instr(devname, "{")
    if p > 0
    {
        p := p - 1
        StringLeft, devname, devname, p
    }
    ;remove last 5 0's if that's what's there
    if SubStr(devname, -4) = "00000"
    {
        p := StrLen(devname) - 5
        StringLeft, devname, devname, p
    }
    StringUpper, devname, devname
    Return devname
}

The idea here is to essentially strip out all the characters that wouldn’t help to make the string any more unique anyway. It’s a tad brute-force, but it’s easy to understand and it works.

So that insane name could end up a relatively benign:

045E00DD017300

Which is a perfectly reasonable unique key in my book.

An Alternate Solution

After I’d put this together and gotten it in use, I realized a potentially even better solution: just calculate a CRC-32 for the full Device Name. It turns out another user has already built a CRC-32 algorithm in AHK script, so doing so would be trivial. Granted, you might still get collisions with a CRC, but it’s reasonably unlikely.

A Better Alternate Solution

Well, after posting this, I got a comment on the AutoHotKey forums that, essentially, was pointing out that the “VID_” and “PID_” strings that my routine removes, actually can have an impact on the uniqueness of the name. The poster also indicated that the GUID at the end of the name also aided in the uniqueness.

While neither of those two claims held true in my specific case (on the 3 machines I tested on), I don’t doubt them in the list.

Which got me to thinking if there’s a better solution.

And there is. Our old friend, the hash function.

In reality, the CRC-32 approach that I suggest above can certainly be considered a form of hash function, and a pretty good one at that.

But, if you’re really gunning for just about ironclad uniqueness, at 64 or 128 bit hash is what you need, and there’s a couple of implementations that already exist for AutoHotKey.

Grab one of those, run it against the GetDevName value, and convert to a string value and you have a relatively short and manageable, unique and consistent HID device ID.

Wrap Up

I love Visual Basic, and VB.net in particular, but AutoHotKey makes many tasks concerned with translating one type of input into another (converted joystick input to mouse input, or keyboard keystrokes, for instance) so easy, it just often doesn’t make sense to build a dedicated app for the purpose.

It’s definitely an application to key handy in your toolbox.

HP G85 and JetDirect for the Win!

2
Filed under Hardware, Rants

image I’ve always been a bit of an HP fan when it comes to their printers. Ages ago, my dad bought a LaserJet that worked for what seemed like forever.

So, back when I first started telecommuting, when I needed a printer, I naturally thought of HP and ended up with a G85 Inkjet/Copier/Scanner.

I also picked up a JetDirect box to let me hook it up directly to an ethernet network. This was all circa 2000. Yep, seriously olden days.

But, that printer is still going strong. Absolutely no problems with it. I’ve dropped it. Moved twice. And most recently, the house got struck by lightning. I lost computers, a SAN drive, network switches, hubs, a TIVO, even UPS’s and my AC units.

But that printer and the Jet Direct box survived unscathed.

So what?

Here’s the thing. The G85 is a great scanner, BUT, the software is hopelessly outdated and HP hasn’t updated it since the XP days. So running it under Vista or Win7 has been a pain.

After the lightning, I was re-setting up new machines, when I happened to try browsing over to the printer’s IP address, basically on a whim.

Guess what I found?

image

That JetDirect box has a webserver built into this WHOLE TIME!

Not just that, but check that scan button…

Click it, and it leads you to this page…

image

Yep, that’s right, you can initiate a scan right here, from the browser, then download the resulting image. No drivers to load, no compatibility issues. Nothing but good ol’ fashioned sweetness.

I’ve had this thing literally ten years and never knew that was there. Certainly never came across it in the docs or any online info.

Turns out you can teach an old dog new tricks (or get him to do tricks you’ve never seen before <g>)

The Antec Sonata Case

75
Filed under Hardware

image I’ve always been impressed by Antec cases, and the Sonata I just picked up for my NAS box is definitely no exception.

First, it’s packed well.

It came with a bag STUFFED to the gills with screws, knobs etc. way more than you’d ever actually need.

imageI needed some drive rails to mount a CDRom. Hmm. Didn’t have any right off hand, but lookee here, right behind the 5.25 drive bay cover…

Unbelievable. Rails clipped in behind it!

Plus, Antec uses silicone vibration isolation washers for their harddrive mounts, and special screws for that. Good stuff.

The Sonata is not quite as nice as the P182image

but it’s cheaper and smaller, so it fits the bill better for a NAS box with 2 drives in it and not a lot else.

NAS’s, The Gigabyte M68M-S2P Motherboard and gigabit Lan Ethernet speed

4
Filed under Hardware, Networking, Windows 7

One small element of replacing all my equipment damaged by a lightning strike was to figure out what to do about my NAS.

image I’d picked up a sweet little Iomega StorCenter 1TB NAS server about 4 years back and it had served me well (despite having Seagate drives in it, ack!). But, after the strike, it was toast. What to do?

My first inclination was to just replace it. The Iomega units tend to be a little on the pricy side and Fry’s had a sale on Western Digital NAS boxes, so what the heck. I picked up a My World Book II, 2TB (Dual 1TB drives) box and came on home.image

Brought it up, restored my backup and all was well. Till a few hours later. The NAS just dropped off the network. I could still ping it, but couldn’t browse to it, connect via mapped driver letter, or anything else.

I powered the box down, and brought it back up, and all was well, for a few more hours. Then, same problem.

After far too long on the phone with WD’s tech support, I wrapped it up and took it back. After a few more false starts, I eventually decided that, heck, for the cost of 2 2TB drives, an Antec Sonata case, some ram, a Motherboard and CPU, and an OS, I could just BUILD a NAS box for about the same money.

imageSo off I went. Settled for the Gigabyte M68M-S2P motherboard with an AMD Athlon 64 X3 tricore. Pretty smooth sailing. Got the box running, hooked it up to the network, and then started copying files over to it.

SSSSSSLLLLLOOOOOOWWWWWW……

What the heck!?

Checked the network adapter and it was auto negotiating at 10mbps! This is the onboard gigabit Ethernet port on the Gigabyte board. Googling came up with a number of people having similar problems but no solutions.

I tried downloading new drivers, installing WinXP 64, and Win7 64, you name it. I could never get the drivers to negotiate at 1gbps.

Finally, I happened to be in the Device Manager, and right clicked on the NVidia nForce Ethernet controller (again, I’d done this plenty of times!)

image

I right clicked it, selected UPDATE DRIVER SOFTWARE, then clicked BROWSE MY COMPUTER FOR SOFTWARE.

Then, clicked LET ME PICK FROM A LIST OF DEVICE DRIVERS ON MY COMPUTER.

Lo and behold, I get this:

image

The NVidia drivers were selected, since I’d installed them off the CD that came with the motherboard, BUT there was that “Microsoft” set of drivers. I selected it, let it install and rebooted.

Presto, the magic 1gbps connection speed!

image

OpenGL 2.1 on an ATI Radeon x1900XT

2
Filed under Arcade, Hardware

I converted my system over to Win7 64 about 5 months ago, but hadn’t really had much time to do any gaming.

Eventually, my daughter asked about playing a Disney Princess CD that she’d played for months under Vista 32.

So I pulled out the CD, ran through the install and started it up. Chug… Chug… Chug… Crash!

Ugh. This isn’t good. After some digging around, turns out that Disney Princesses is an OpenGL game that requires 1.1+. Ok fair enough. This is Win7 after all.

I found an app called OpenGL Extensions which shows detailed info about the OpenGL stack loaded and turns out, I was using the generic MS video driver and OpenGL 1.0. Yikes!.

So I tried installing the latest ATI drivers, which, as of April 2010, is 10.3. No dice. The x1900XT is no longer supported.

Double Yikes!

And this card is not  that old.

Long story short. I grabbed the 9.11 version of the Catalyst drivers off the ATI website here. They appear to be the last version (for Vista 64) that did support the x1900XT, and they DO install under Win7 64 (albeit the manager pops an error about some MOM file being missing, but that doesn’t seem to affect the drivers).

Now, OpenGL extensions reports OpenGL 2.1 and that game works flawlessly.

And, I suppose, I’ll be in the market for a newer video card at some point in the nearish future.

Calling C Object Methods from VB.Net

0
Filed under .NET, amBX, Games, Hardware, VB Feng Shui

image I’ve been playing recently with the amBX ambient effects library and devices.

Essentially, it’s an effects platform that allows you to easily create light, wind and rumble effects to coordinate with what’s going on on-screen, in a game, in media players, or what-have-you. The lighting effects are nothing short of fantastic. The rumble and fan effects…. meh. They’re interesting, but I’m not sure where that’ll go.

Regardless, the API for amBX is all C style objects, which means essentially an array of function pointers to the methods of the object; not really an API that VB.net likes to consume. Be sure to grab the amBX developer kit here. You’ll also need to core amBX software available from the main website. Finally, play around with the “Virtual amBX Test Tool”. It’ll allow you to experiment with all the amBX features and functions without having to buy anything at all. Of course, eventually you’ll want to get at least the starter kit, with the lights. But it’s not necessary to begin experimenting.

Hats off to Philips for making this possible!

image

Here’s an example of the structure definition from the amBX.h header file that comes with the API (I’ve clipped out comments and other bits):

struct IamBX {
    amBX_RESULT (*release) (struct IamBX * pThis);
    amBX_RESULT (*createLight) (struct IamBX * pThis,
                                amBX_u32 loc,
                                amBX_u32 height,
                                struct IamBX_Light** ppLight);
....

As you can see, each element in the structure is made up of a pointer to a function. The various functions all take, as their first argument, a pointer back to this structure. Under the covers, this is how virtually all object oriented languages function, it’s just the the compiler usually hides all this nasty plumbing so you don’t have to deal with it regularly.

But this presents a problem. VB.net is “managed” code, and as such, it likes things to be wrapped up in nice “managed” bits. There are plenty of good reasons for this, but these function pointers are decidedly not nice and tidy managed bits! So, what to do?

One solution is the approach Robert Grazz took here. It’s a solid solution, no doubt, and I learned a lot from looking at his approach, but, this being VBFengShui, I wanted that same functionality in native VB.net with no additional dll files hanging around!

Delegates to the Rescue!

A delegate in .net is essentially a managed function pointer. In VB.net, they’re most commonly used to handle events, but you wouldn’t really know it, because VB’s compiler does a good job of hiding all that plumbing from you. However, unlike VB6 and earlier, in VB.net, all the plumbing can, if you’re willing, be “brought into the daylight” and used however you want.

There are many, many discussions about delegates for VB.net out on the web. A really good Introduction to the concepts by Malli_S is on codeproject, so I won’t rehash the basics here.

The problem is, delegates in VB.net tend to be generated by using the AddressOf operator, and I already had the function addresses (they’re in that C structure). I just needed to get a delegate that would allow me to call it.

Some Googling turned up several good posts that provided pointers, including this one on StackOverflow. But it wasn’t exactly all the steps necessary.

Getting the C Structure

The first step toward getting something working with amBX is to retrieve the main amBX object. You do this through a standard Windows DLL call.

<DllImport("ambxrt.dll", ExactSpelling:=True, CharSet:=CharSet.Auto)> _
        Public Shared Function amBXCreateInterface(ByRef IamBXPtr As IntPtr, ByVal Major As UInt32, ByVal Minor As UInt32, ByVal AppName As String, ByVal AppVer As String, ByVal Memptr As Integer, ByVal UsingThreads As Boolean) As Integer
        End Function

Assuming you have the ambxrt.dll file somewhere on your path or in the current dir, the call will succeed, but what exactly does that mean?

Here’s an example of a call to it in C:

    if (amBXCreateInterface(
            &pEngineHandle, 
            majorVersion, minorVersion, 
            (amBX_char*)cAppName.ToPointer(), (amBX_char*)cAppName.ToPointer(),
            nullptr, false)
        != amBX_OK)

Essentially, you pass in the Major and minor version numbers of the Version of amBX you need, and two strings indicating the name of your app and the version of your app. No problems with any of that. But that &EngineHandle is the trick.

It means that this call will return a 4 byte pointer to a block of memory that contains the amBX object structure, which is, itself, an array of 4 byte function pointers to the various methods of the amBX object. Therefore, in VB.net, that argument is declared ByRef IamBXPtr as IntPtr.

Now, we need a place to store that “structure of pointers.” Going through the ambx.h file, I ended up with a structure that looks like this:

        <StructLayout(LayoutKind.Sequential)> _
        Private Structure IamBX
            Public ReleasePtr As IntPtr
            Public CreateLightPtr As IntPtr
            Public CreateFanPtr As IntPtr
            Public CreateRumblePtr As IntPtr
            Public CreateMoviePtr As IntPtr
            Public CreateEventPtr As IntPtr
            Public SetAllEnabledPtr As IntPtr
            Public UpdatePtr As IntPtr
            Public GetVersionInfoPtr As IntPtr
            Public RunThreadPtr As IntPtr
            Public StopThreadPtr As IntPtr
        End Structure

That StructLayout attribute is particularly important. It tells the compiler that the structure should be layed out in memory JUST AS it’s declared in source code. Otherwise, the compiler could possibly rearrange elements on us. With Managed Code, that’d be no problem, but when working with unmanaged C functions, that would not be a good thing!

And finally, we need a way to retrieve that function pointer array and move it into the structure above, so that it’s easier to work with from VB.net. That’s where the System.Runtime.InteropServices.Marshal.PtrToStructure function comes in. This routine will copy a block of unmanaged memory directly into a managed structure.

Private _IamBX As IamBX
Private _IamBXPtr As IntPtr
...

amBXCreateInterface(_IamBXPtr, 1, 0, “MyAppName”, “1.0”, 0, False)

_IamBX = Marshal.PtrToStructure(_IamBXPtr, GetType(IamBX))

And presto, you should now have the _IamBX structure filled in with the function pointers of all the methods of this C “Object”.

Calling the C Function Pointer

At this point, we’ve got everything necessary to call the function pointer. Take the CreateLight function. The C prototype for this function is shown at the top of this page. As arguments, it takes a pointer back to the amBX structure, 2 32bit integers describing the location and height of the light source, and it returns are pointer to another structure, in this case, an amBX_Light structure, which contains another set of function pointers, just like the amBX structure described above.

First, we need to declare a delegate that matches the calling signature of the CreateLight C function we’ll be calling:

<UnmanagedFunctionPointer(CallingConvention.Cdecl)> _
Private Delegate Function CreateLightDelegate(
         ByVal IamBXPtr As IntPtr, _
         ByVal Location As Locations, _
         ByVal Height As Heights, _
         ByRef IamBXLightPtr As IntPtr) As amBX_RESULT

The key points here are:

  1. Make sure you have the CallingConvention attribute set right. Most C libraries will be CDECL, but some libraries are STDCALL.
  2. Make sure your parameter types match, especially in their sizes. Not doing this will lead to corrupted called or system crashes.

Now, create a variable for the delegate and use the Marshal.GetDelegateFromFunctionPointer function to create a new instance of the delegate, based on the applicable function pointer. Since the function pointer for the CreateLight function is stored in the CreateLightPtr field of the _IamBX structure, we pass that in as the pointer argument.

IMPORTANT NOTE: There are 2 overloads for the Marshal.GetDelegateFromFunctionPointer  function. Be sure to use the one what requires a second argument of the TYPE of the delegate to create. Using the other one will fail at runtime because it won’t be able to dynamically determine the type of delegate to create.

Next, call the function via the delegate, just as if it was a function itself.

Finally, use Marshal.PtrToStructure again to copy the array of function pointers returned into another structure, this one formatted to contain the function pointers of the Light object that just got created.

Dim d As CreateLightDelegate = Marshal.GetDelegateForFunctionPointer(_IamBX.CreateLightPtr, GetType(CreateLightDelegate))
Dim r = d(_IamBXPtr, Location, Height, _IamBXLightPtr)
_IamBXLight = Marshal.PtrToStructure(_IamBXLightPtr, GetType(IamBX_Light))

The amBX library is much, much larger than just this little bit I’ve shown here. Once I get the entire thing coded up, I’ll be presenting it here as well.

But in the meantime, if you have a need to interface with C style function pointer objects, don’t let anyone tell you it can’t be done in VB.net

Final Note

As you may or may not have guessed, amBX is a 32bit library and its interfaces, functions and arguments all live in 32bit land. If you’re working with VS2008, BE SURE to set your project to the x86 platform, and NOT “Any CPU” or “x64”! The default, for whatever reason is “Any CPU”, which means things will work properly when run under 32bit windows, but things won’t work well at all if run under a 64bit OS.

507 Mechanical Movements

0
Filed under Hardware, Misc

   image

Just came across this excellent little book. Originally published in 1868, it’s a compendium of just about every sort of mechanical means of transferring one type of motion to another than you can think of. Lots of illustrations. Makes a great reference, and the best part, it’s available free online in a djvu version. It’s a 75mb download, because the book has just been scanned in (even the text), so it’s not really searchable.

But who cares?

If you’re into steampunk in any way at all, this is a must have reference book.

As a side note, while the DJVU format and viewer is quite nice, I preferred a PDF version.

It took some effort, because, when I tried to convert it (using the PRINT function from DJVU into a PDF printer), I kept getting errors about the Courier font not being found (which it most definitely IS installed on my machine).

After some fiddling, I ended up being able to “print” it to the “Office Document Image Writer” printer driver. I then loaded that file up into the Office Document Image Viewer, and printed to the PDF printer from there.

WinIPAC controller software isn’t quite right

7
Filed under Arcade, Hardware, Troubleshooting

image Here’s an example of the “When software doesn’t work right, it can cost loads and load of time” type bug.

I’m using an Ultimarc WinIPAC controller board for a project I’m working on. This is an awesome little controller board that looks like a USB keyboard, and supports mapping of up to 56 switch inputs to any normal keyboard key. It also has LED outputs for driving LED’s or other applicable circuits.

Anyway, the IPAC comes with a simple keyboard mapper/programmer that you use to layout your buttons, then assign which key to which button and finally to upload the mapping to the IPAC (It saves the mapping in non-volatile ram on the board, which is even more fantastic).

Anyway, I’d mapped a particular key to the ENTER key, as shown here.

image

But when I was testing all my mappings out, it didn’t work. Actually, it’d flash sometimes but not consistently. Every other key worked flawlessly.

I spent the better part of 3 hours trying to debug what the heck was happening, swapping wires, using a test meter to verify connections, switch operation, etc.

I even pulled the latest version of their website, but still no joy.

In the end, I discovered that it’s the WinIPAC software  that doesn’t quite handle ENTER keypresses properly. I opened up NOTEPAD and just started pressing buttons and all of them, including the switch I had mapped to ENTER, worked perfectly.

<sigh>

<UPDATE> I emailed with Andy at Ultimarc (these guys get back to you quickly! Very nice support). Anyway, his comment was that the WinIPAC software isn’t intended as a test application, only for programming.

I pointed out that if that’s the case, there really ought to be a warning somewhere to that effect, especially if you map keys whose actions can’t be displayed properly in the application. My rationale was, well, it DOES properly display keyboard status for virtually all the mappable keys so why wouldn’t any normal person assume it could be used as a test app? But, in the end, it’s easy enough to use notepad for those tested, IF you know enough to realize you need to!

Tiny PC

0
Filed under Arcade, Games, Hardware

image Here’s a slick little gizmo.

The VIA ArtiGo mini pc.

It’s a full PC, with space for a 2.5″ HD, onboard graphics and audio, and up to 1 GB ram. And it fits in a 5.25″ harddrive bay.

I know, I know, there are case modders out there making still smaller PC’s, but this one’s 300$ at Fry’s, no added bother, headaches, or a Dremel necessary.

The specs are decent, but I wouldn’t want to code on it. However, you could probably make a decent Mame cab out of just about anything you have lying around and I’m guessing this would be plenty powerful enough to drive it, not to mention it’d fit just about anywhere without even a second glance.

A Great Little Headset

3
Filed under Hardware

image I’ve got a Uniden TX-860 Two Line expandable 5.8 ghz phone system at the house that’s great for a home office setup. The battery life is excellent, sound is as clear as a wired set, it has distinctive rings (so office calls don’t get confused with personal calls), and a headset jack, plus it’s expandable to 8 or 10 phones.

The only downside is that it’s been very  difficult to find a headset that works well with it.

I’ve gone through literally a dozen plus headsets and all of them (except the one that came with the phone) make my voice to the other party sound like I’m yelling from across a football field.

But the one that came with the phone… Well…

image

No volume, no mute, big, clunky. It works, but that’s about all you can say for it.

I even checked out the highly-rated TheBoom and the Etymotic sets, but they weren’t any better.

I actually tried several Plantronics sets from time to time, but none of them gave a good voice level either. Then I ran into the Plantronics MX500C, one that I hadn’t tried before. It was cheap so I figured I’d give it a shot.

image

Awesome little gizmo. My voice sounds loud and clear to my callee, it’s got a volume control (mic and earpiece) and a mute, plus it’s lightweight and comfortable to boot. For 19.99 at Fry’s, it’s the best companion to my phones I’ve found yet.