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.

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

SCALE = 3

def delta(value): # Convert 8 bit value to signed number of distance steps
    value = ~value # All value is inverted on the bus
    if value & 0x80:  # and is in sign-magnitude format
        return -(value & 0x7f)
    else:
        return value & 0x7f

mouse = Mouse(devices)

spi = board.SPI()
strobe = digitalio.DigitalInOut(board.RX)
strobe.switch_to_output(False)

if not spi.try_lock():
    raise SystemExit("could not lock SPI bus")

spi.configure(baudrate=100_000, polarity=1)

# Wait for the mouse to be ready at power-on
time.sleep(2)

# A buffer to fetch mouse data into
data = bytearray(4)

print("Mouse is ready!")
while True:
    # Request fresh data
    strobe.value = True
    strobe.value = False

    # Must do read in 2 pieces with delay in between
    spi.readinto(data, end=2)
    spi.readinto(data, start=2)
    lmb = bool(~data[1] & 0x40)  # data is inverted on the bus
    rmb = bool(~data[1] & 0x80)
    dy = delta(data[2])
    dx = delta(data[3])


    # Pressing both buttons together emulates the wheel
    wheel = lmb and rmb

    # Compute the new button state
    old_buttons = mouse.report[0]
    mouse.report[0] = (
        0 if wheel else
        mouse.LEFT_BUTTON if lmb else
        mouse.RIGHT_BUTTON if rmb else
        0)

    # If there's any movement, send a move event
    if dx or dy:
        if wheel:
            mouse.move(dx * SCALE, 0, wheel=-dy)
        else:
            mouse.move(dx * SCALE, dy * SCALE)
    elif old_buttons != mouse.report[0]:
        mouse.press(0) # Send buttons previously set via report[0]

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 time
import board
import digitalio
from adafruit_hid.mouse import Mouse
from usb_hid import devices

SCALE = 3

The SNES mouse sends movement data in a specific way: one bit of direction (+ or -) and 7 bits of distance. But all the bits are inverted, too! This function converts the received value into a value from -127 to +127:

def delta(value): # Convert 8 bit value to signed number of distance steps
    value = ~value # All value is inverted on the bus
    if value & 0x80:  # and is in sign-magnitude format
        return -(value & 0x7f)
    else:
        return value & 0x7f

The mouse communicates using a protocol that is compatible with SPI, though there's also a strobe output that has to be pulsed HIGH (True) anytime new data is needed.

I was unable to determine why the 2-second delay was necessary, but without it the mouse didn't function properly.

spi = board.SPI()
strobe = digitalio.DigitalInOut(board.RX)
strobe.switch_to_output(False)

if not spi.try_lock():
    raise SystemExit("could not lock SPI bus")

spi.configure(baudrate=100_000, polarity=1)

# Wait for the mouse to be ready at power-on
time.sleep(2)

In the forever-loop, repeatedly fetch data from the mouse in its own format, then convert it to the necessary format for USB HID.

In this implementation, holding BOTH buttons at the same time starts mouse wheel emulation mode: moving the mouse up/down sends wheel events instead of normal movement events.

while True:
    # Request fresh data
    strobe.value = True
    strobe.value = False

    # Must do read in 2 pieces with delay in between
    spi.readinto(data, end=2)
    spi.readinto(data, start=2)
    lmb = bool(~data[1] & 0x40)  # data is inverted on the bus
    rmb = bool(~data[1] & 0x80)
    dy = delta(data[2])
    dx = delta(data[3])


    # Pressing both buttons together emulates the wheel
    wheel = lmb and rmb

    # Compute the new button state
    old_buttons = mouse.report[0]
    mouse.report[0] = (
        0 if wheel else
        mouse.LEFT_BUTTON if lmb else
        mouse.RIGHT_BUTTON if rmb else
        0)

    # If there's any movement, send a move event
    if dx or dy:
        if wheel:
            mouse.move(dx * SCALE, 0, wheel=-dy)
        else:
            mouse.move(dx * SCALE, dy * SCALE)
    elif old_buttons != mouse.report[0]:
        mouse.press(0) # Send buttons previously set via report[0]

This guide was first published on Mar 02, 2023. It was last updated on Feb 14, 2024.

This page (Coding the Mouse with CircuitPython) was last updated on Feb 14, 2024.

Text editor powered by tinymce.