In the early days of GUI interfaces, one common type of mouse was known as a “bus mouse”. This kind of mouse placed all the intelligence inside the computer or a decoder board, with little more than microswitches and rotary encoders inside the mouse itself.

Later, more and more smarts were moved into the mouse, with the transition to protocols like PS/2 on PCs, ADB on Macs, and finally USB as the standard since the late ‘90s.

In this guide, I’ll show how to use a CircuitPython board to convert the classic NeXT Computers bus mouse to work on a modern computer with USB HID.

The techniques in this guide may also be helpful in converting other classic bus mice to USB HID.

Since there are just a few pins used, the Adafruit QT Py form factor is perfect for this project. You should be able to use almost any device that supports CircuitPython and USB HID, but I chose the QT Py RP2040.

Note that if you have a NeXT Computers keyboard too, you can do a project that works with both keyboard and mouse in a daisy chain. Check out that guide here!

Parts

Video of hand holding a QT Py PCB in their hand. An LED glows rainbow colors.
What a cutie pie! Or is it... a QT Py? This diminutive dev board comes with one of our new favorite chip, the RP2040. It's been made famous in the new
$9.95
In Stock
USB Type A to Type C Cable - approx 1 meter / 3 ft long
As technology changes and adapts, so does Adafruit. This  USB Type A to Type C cable will help you with the transition to USB C, even if you're still...
$4.95
In Stock
1 x NeXT Computer Bus Mouse
with 8-pin Mini-DIN connector
1 x 8-pin Mini-DIN connector
Mini-DIN 8-pin receptacle, panel mount
2 x Mounting Screws
M2.5x6 metal screws

The NeXT mouse is a simple device without much smarts inside. It's of the general class of mouse known as the optomechanical bus mouse, which communicates with a microcontroller or dedicated chip inside a computer to decode logic signals into motion and button presses.

Mechanically, there is a ball that is able to roll on any flat surface. When it rolls, it causes two internal “wheels” to turn, one when the mouse moves horizontally, and one when it moves vertically.

These wheels have small slits in them. Together with LEDs and phototransistors, each wheel produces a quadrature signal, which can be decoded by a microcontroller into the amount of relative motion. In CircuitPython, the IncrementalEncoder object performs this task.

Diagram of a mechanical mouse (from Wikipedia by Pbrks). "Pulling the mouse turns the ball. X and Y rollers grip the ball and transfer movement. Optical encoding disks include light holes. Infrared LEDs shine through the disks. Sensors gather light pulses to convert to X and Y velocities."

Rotary encoder, with corresponding A/B signal states shown on the right (from Wikipedia by JackPotte and Delt01). In an optomechanical mouse, there is just one set of holes, but two photodetectors that are displaced so the hole exposes them to light at different times.

There are also two buttons, microswitches that can be actuated by a movable part of the mouse enclosure. These can be treated as a set of independent buttons by the keypad.Keys object, or just as digital inputs.

In the original NeXT computer, the microcontroller inside the keyboard decoded the mouse signals and transmitted them on to the computer. In this project, the task will be performed by an Adafruit QT Py RP2040 instead.

The number of I/Os required on the microcontroller is small: 4 for the two quadrature signals, plus 1 more for each button. For the NeXT mouse, that's just 6 GPIOs needed.

Happily, someone has already figured out the pinout of this mouse and many similar mice, check out the Deskthority Wiki page on bus mice!

While the mouse is reportedly specified to operate at +5V, experimentally it works fine with 3.3V which is great for interfacing with modern microcontrollers like the RP2040.

CircuitPython is a derivative of MicroPython designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the CIRCUITPY drive to iterate.

CircuitPython Quickstart

Follow this step-by-step to quickly get CircuitPython running on your board.

Click the link above to download the latest CircuitPython UF2 file.

Save it wherever is convenient for you.

To enter the bootloader, hold down the BOOT/BOOTSEL button (highlighted in red above), and while continuing to hold it (don't let go!), press and release the reset button (highlighted in blue above). Continue to hold the BOOT/BOOTSEL button until the RPI-RP2 drive appears!

If the drive does not appear, release all the buttons, and then repeat the process above.

You can also start with your board unplugged from USB, press and hold the BOOTSEL button (highlighted in red above), continue to hold it while plugging it into USB, and wait for the drive to appear before releasing the button.

A lot of people end up using charge-only USB cables and it is very frustrating! Make sure you have a USB cable you know is good for data sync.

You will see a new disk drive appear called RPI-RP2.

 

Drag the adafruit_circuitpython_etc.uf2 file to RPI-RP2.

The RPI-RP2 drive will disappear and a new disk drive called CIRCUITPY will appear.

That's it, you're done! :)

Safe Mode

You want to edit your code.py or modify the files on your CIRCUITPY drive, but find that you can't. Perhaps your board has gotten into a state where CIRCUITPY is read-only. You may have turned off the CIRCUITPY drive altogether. Whatever the reason, safe mode can help.

Safe mode in CircuitPython does not run any user code on startup, and disables auto-reload. This means a few things. First, safe mode bypasses any code in boot.py (where you can set CIRCUITPY read-only or turn it off completely). Second, it does not run the code in code.py. And finally, it does not automatically soft-reload when data is written to the CIRCUITPY drive.

Therefore, whatever you may have done to put your board in a non-interactive state, safe mode gives you the opportunity to correct it without losing all of the data on the CIRCUITPY drive.

Entering Safe Mode

To enter safe mode when using CircuitPython, plug in your board or hit reset (highlighted in red above). Immediately after the board starts up or resets, it waits 1000ms. On some boards, the onboard status LED (highlighted in green above) will blink yellow during that time. If you press reset during that 1000ms, the board will start up in safe mode. It can be difficult to react to the yellow LED, so you may want to think of it simply as a slow double click of the reset button. (Remember, a fast double click of reset enters the bootloader.)

In Safe Mode

If you successfully enter safe mode on CircuitPython, the LED will intermittently blink yellow three times.

If you connect to the serial console, you'll find the following message.

Auto-reload is off.
Running in safe mode! Not running saved code.

CircuitPython is in safe mode because you pressed the reset button during boot. Press again to exit safe mode.

Press any key to enter the REPL. Use CTRL-D to reload.

You can now edit the contents of the CIRCUITPY drive. Remember, your code will not run until you press the reset button, or unplug and plug in your board, to get out of safe mode.

Flash Resetting UF2

If your board ever gets into a really weird state and CIRCUITPY doesn't show up as a disk drive after installing CircuitPython, try loading this 'nuke' UF2 to RPI-RP2. which will do a 'deep clean' on your Flash Memory. You will lose all the files on the board, but at least you'll be able to revive it! After loading this UF2, follow the steps above to re-install CircuitPython.

Text Editor

Adafruit recommends using the Mu editor for editing your CircuitPython code. You can get more info in this guide.

Alternatively, you can use any text editor that saves simple text files.

Download the Project Bundle

Your project will use a specific set of CircuitPython libraries and the code.py file. To get everything you need, click on the Download Project Bundle link below, and uncompress the .zip file.

Hook your QT Py/board to your computer via a known good USB data+power cable. It should show up as a thumb drive named CIRCUITPY.

Using File Explorer/Finder (depending on your Operating System), drag the contents of the uncompressed bundle directory onto your board's CIRCUITPY drive, replacing any existing files or directories with the same names, and adding any new ones that are necessary.

Continue below the program listing for a breakdown of how the program functions, section by section.

# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries
# SPDX-License-Identifier: MIT
import board
import digitalio
import rotaryio
from adafruit_hid.mouse import Mouse
from usb_hid import devices

SCALE = 4

class RelativeEncoder:
    def __init__(self, pin_a, pin_b, divisor=1):
        self._encoder = rotaryio.IncrementalEncoder(pin_a, pin_b, divisor)
        self._old = self._encoder.position

    @property
    def delta(self):
        old = self._old
        new = self._old = self._encoder.position
        return new - old

xpos = RelativeEncoder(board.A0, board.A1)
ypos = RelativeEncoder(board.A2, board.A3)
lmb = digitalio.DigitalInOut(board.SCL)
lmb.pull = digitalio.Pull.UP
rmb = digitalio.DigitalInOut(board.SDA)
rmb.pull = digitalio.Pull.UP

mouse = Mouse(devices)

while True:
    dx = xpos.delta * SCALE
    dy = ypos.delta * SCALE
    l = not lmb.value
    r = not rmb.value
    mouse.report[0] = (
        mouse.MIDDLE_BUTTON if (l and r) else
        mouse.LEFT_BUTTON if l else
        mouse.RIGHT_BUTTON if r else
        0)

    if dx or dy:
        mouse.move(dx, dy)
    else:
        mouse._send_no_move() # pylint: disable=protected-access
File

How it works

Preliminaries

After the required import lines, there's a line where you can customize the amount of movement per tick. You can also check your operating system settings for mouse speed & acceleration settings.

# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries
# SPDX-License-Identifier: MIT
import board
import digitalio
import rotaryio
from adafruit_hid.mouse import Mouse
from usb_hid import devices

SCALE = 4

Next, the code defines a class that keeps track of how far the mouse has been moved since the last report to the host computer:

class RelativeEncoder:
    def __init__(self, pin_a, pin_b, divisor=1):
        self._encoder = rotaryio.IncrementalEncoder(pin_a, pin_b, divisor)
        self._old = self._encoder.position

    @property
    def delta(self):
        old = self._old
        new = self._old = self._encoder.position
        return new - old

The code then creates objects to track the X and Y movements and the state of the two buttons:

xpos = RelativeEncoder(board.A0, board.A1)
ypos = RelativeEncoder(board.A2, board.A3)
lmb = digitalio.DigitalInOut(board.SCL)
lmb.pull = digitalio.Pull.UP
rmb = digitalio.DigitalInOut(board.SDA)
rmb.pull = digitalio.Pull.UP

Finally, it's time to actually emulate a mouse. After creating the Mouse object, the forever-loop repeatedly sends events to the computer, either a "move" if any X or Y motion is detected, or a "no move" to just update the state of the buttons.

Because the buttons are "pulled up" and switch to a LOW logic value when pressed, the button values are inverted with not.

When both buttons are pressed together, a middle button event is sent instead.

In the case that there was no mouse motion, the code sends a "no move" message so that any button changes are sent to the host computer. This isn't the normal way of sending button events (but it works better with the structure of this code), hence the need to use a pylint comment to indicate this was intended and correct.

mouse = Mouse(devices)

while True:
    dx = xpos.delta * SCALE
    dy = ypos.delta * SCALE
    l = not lmb.value
    r = not rmb.value
    mouse.report[0] = (
        mouse.MIDDLE_BUTTON if (l and r) else
        mouse.LEFT_BUTTON if l else
        mouse.RIGHT_BUTTON if r else
        0)

    if dx or dy:
        mouse.move(dx, dy)
    else:
        mouse._send_no_move() # pylint: disable=protected-access

And that's it!

Wiring the adapter

Just a few connections are needed between the QT Py RP2040 and the mouse connector.

Watch out here, because the DIN wire colors do not correspond to classical wire coloring for VCC and GND! And if you didn't buy the exact same connector, check your connector's datasheet to find the correct wire colors. (pin numbers should be consistent for all Mini-DIN jacks as they are part of the standard)

The original voltage specification of the mouse is 5V, but my sample worked reliably with just 3.3V, which means no voltage level shifters are required.

The mini-DIN connector has pre-stripped, pre-tinned wires. Just insert them into the QT Py RP2040 from the top side and solder as follows:

Wire the DIN connector to the QT py as follows:

  1. Brown (VCC) to QT Py 3V
  2. White (XA) to QT Py A0
  3. Black (XB) to QT Py A1
  4. Blue (YA) to QT Py A2
  5. Green (YB) to QT Py A3
  6. Yellow (RMB) to QT Py SDA
  7. Orange (LMB) to QT Py SCL
  8. Red (GND) to QT Py GND

At this point, you you can test the software and hardware to make sure everything works. Try moving and clicking the mouse. If it's not working, verify that you've correctly copied the code and re-check the wiring. If problems persist, open the Serial REPL to check for any messages that may help you diagnose the problem. Once it's working, continue on with preparing an enclosure.

Printing the enclosure

I've prepared a 3D-printable enclosure for this project. To print, flip each part so that the large flat face is on the 3D printer bed. Use standard slicer settings, no support needed.

The zip file below contains all you need: STL and Step files for the top and bottom of the case, and the FreeCAD design file in case you want to make any modifications. (FreeCAD is a free and open source CAD package for Linux, Mac and Windows available at freecadweb.org)

Use "rotate" or "lay flat" in your slicer to flip the lid so that its large flat face is on the 3D printer bed.

To install the converter in the enclosure, place the side of the QT Py with the Stemma connector against the two PCB clips, then gently but firmly press down on the USB connector until the board clicks into place.

Use 2 M2.5x6 screw to secure the Mini-DIN connector to the enclosure.

Carefully route the wires out of the way and then snap on the enclosure lid.

If you need to, you can remove the QT Py by gently levering up on the USB connector while flexing that side of the box outwards.

Here's how the completed adapter should look on each end, with the connectors secure and accessible.

Plug the USB cable and the NeXT mouse cable into the adapter, then plug the USB cable into your computer or laptop and everything's ready to go:

This guide was first published on Jan 25, 2023. It was last updated on Jul 21, 2024.