RP2040 Mouse - rev 3.0

XIAO RP2040 Mouse Ver 3.0

Over the last few months, We have been steadily improving the design of our XIAO RP2040-based mouse device. With this, ver 3.0 all the hardware bugs were finally eliminated, and we also placed the device into its first-ever enclosure.

Let us take a look at the design

The PCB and Schematic

The PCB is a very strange shape, with lots of cut-outs. This is to accommodate the big push buttons that will be mounted in the enclosure, as well as to fit nicely into the mounting area of the enclosure… This design took quite some time with a pair of callipers and CAD, but all went well, and the shape is perfectly accurate.

Schematic_RemoteMouse_2024-01-25Download

The schematic is also straight forward, with the only real changes begin to the rotary encoder. In ver 2.0, We connected the encoder to the MCP23008, but for some reason CircuitPython does not seem to like an encoder connected to an IO extender… That forced us to do some software hack to use the encoder… I have thus decided to change things around in ver 3.0 and move the encoder back to the native GPIO on the XIAO RP2040

It is also interesting to note that the circuit was initially designed for the XIAO ESP32S3, but due to issues with stock, as well as crazy prices on local parts, we made a quick turn-around and went back to the RP2040. The ESP32S3 was going to allow us to implement a wireless device, through using ESPNow protocol… That may still be done in future, but for now, I think we have done enough work on the mouse device for the time being…

Manufacturing the PCB and Assembly

The PCB for this project was sponsored by PCBWay .

Disclaimer:
Clicking on the PCBWay link will take you to the PCBWay website. It will enable you to get a $5.00 USD voucher towards your first PCB order. (Only if you sign up for a free account).

Assembly was quite easy, I chose to use a stencil, because the IO Expander chip has a very tiny footprint, as well as a leadless package… The stencil definitely helps prevent excessive solder paste, as well as saves a lot of time on reworking later…

In the picture above, we can clearly see why I had to design the PCB with such an irregular shape.

Firmware and Coding

We are still using CircuitPython for the firmware on this device. It is not perfect, but it works, well sort of anyway. What does that mean? Well… As far as the mouse functions are concerned, clicking, scrolling, moving the pointer – all of that is works perfectly, and thus allows me to use the device for basic operations every day. Drag and Drop, as well as selecting and or highlighting text DOES NOT work. This seem to be an issue with the HID code in Circuitpython, meaning it doesn’t seem to be implemented. It is also way beyond my abilities to implement it myself…

Below is the code.py file, with the boot.py below that

import time
import board
import busio
from rainbowio import colorwheel
import neopixel
import rotaryio
import microcontroller
from digitalio import Direction
from adafruit_mcp230xx.mcp23008 import MCP23008
import digitalio
i2c = busio.I2C(board.SCL, board.SDA)
mcp = MCP23008(i2c)


from analogio import AnalogIn
import usb_hid
from adafruit_hid.mouse import Mouse
joyX = board.A0
joyY = board.A1
JoyBtn = board.D2

LeftBtn = 0
CenterBtn = 1
RightBtn = 2
UpBtn = 3
DownBtn = 4
EncoderBtn = 5


mouse = Mouse(usb_hid.devices)
xAxis = AnalogIn(joyX)
yAxis = AnalogIn(joyY)

# NEOPIXEL
pixel_pin = board.NEOPIXEL
num_pixels = 1
pixels = neopixel.NeoPixel(pixel_pin, num_pixels, brightness=0.1, auto_write=False)

leftbutton = mcp.get_pin(LeftBtn)
leftbutton.direction = digitalio.Direction.INPUT
leftbutton.pull = digitalio.Pull.UP

centerbutton = mcp.get_pin(CenterBtn)
centerbutton.direction = digitalio.Direction.INPUT
centerbutton.pull = digitalio.Pull.UP

maint_btn = digitalio.DigitalInOut(JoyBtn)
maint_btn.switch_to_input(pull=digitalio.Pull.UP)

rightbutton = mcp.get_pin(RightBtn)
rightbutton.direction = digitalio.Direction.INPUT
rightbutton.pull = digitalio.Pull.UP

enc_btn = mcp.get_pin(EncoderBtn)
enc_btn.direction = digitalio.Direction.INPUT
enc_btn.pull = digitalio.Pull.UP

scroll_up = mcp.get_pin(UpBtn)
scroll_up.direction = digitalio.Direction.INPUT
scroll_up.pull = digitalio.Pull.UP

scroll_down = mcp.get_pin(DownBtn)
scroll_down.direction = digitalio.Direction.INPUT
scroll_down.pull = digitalio.Pull.UP



mousewheel = rotaryio.IncrementalEncoder(board.D6, board.D7, 4)
last_position = mousewheel.position
print(mousewheel.position)

move_speed = 3
enc_down = 0

RED = (255, 0, 0)
YELLOW = (255, 150, 0)
GREEN = (0, 255, 0)
CYAN = (0, 255, 255)
BLUE = (0, 0, 255)
PURPLE = (180, 0, 255)
BLACK = (0, 0, 0)


if move_speed == 0:
    in_min, in_max, out_min, out_max = (0, 65000, -20, 20)
    filter_joystick_deadzone = (
        lambda x: int((x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min)
        if abs(x - 32768) > 500
        else 0
    )
if move_speed == 1:
    pixels.fill(GREEN)
    pixels.show()
    in_min, in_max, out_min, out_max = (0, 65000, -15, 15)
    filter_joystick_deadzone = (
        lambda x: int((x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min)
        if abs(x - 32768) > 500
        else 0
    )
if move_speed == 2:
    pixels.fill(BLUE)
    pixels.show()
    in_min, in_max, out_min, out_max = (0, 65000, -10, 10)


filter_joystick_deadzone = (
        lambda x: int((x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min)
        if abs(x - 32768) > 500
        else 0
    )
if move_speed == 3:
    pixels.fill(PURPLE)
    pixels.show()
    in_min, in_max, out_min, out_max = (0, 65000, -8, 8)
    filter_joystick_deadzone = (
        lambda x: int((x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min)
        if abs(x - 32768) > 500
        else 0
    )
if move_speed == 4:
    pixels.fill(CYAN)
    pixels.show()
    in_min, in_max, out_min, out_max = (0, 65000, -5, 5)
    filter_joystick_deadzone = (
        lambda x: int((x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min)
        if abs(x - 32768) > 500
        else 0
    )


pixels.fill(BLACK)
pixels.show()
while True:
    # Set mouse accelleration ( speed)
    #print(mousewheel.position)
    if move_speed == 0:
        pixels.fill(BLACK)
        pixels.show()
        in_min, in_max, out_min, out_max = (0, 65000, -20, 20)
        filter_joystick_deadzone = (
            lambda x: int(
                (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
            )
            if abs(x - 32768) > 500
            else 0
        )
    if move_speed == 1:
        pixels.fill(GREEN)
        pixels.show()
        in_min, in_max, out_min, out_max = (0, 65000, -15, 15)
        filter_joystick_deadzone = (
            lambda x: int(
                (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
            )
            if abs(x - 32768) > 500
            else 0
        )
    if move_speed == 2:
        pixels.fill(BLUE)
        pixels.show()
        in_min, in_max, out_min, out_max = (0, 65000, -10, 10)
        filter_joystick_deadzone = (
            lambda x: int(
                (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
            )
            if abs(x - 32768) > 500
            else 0
        )
    if move_speed == 3:
        pixels.fill(PURPLE)
        pixels.show()
        in_min, in_max, out_min, out_max = (0, 65000, -8, 8)
        filter_joystick_deadzone = (
            lambda x: int(
                (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
            )
            if abs(x - 32768) > 500
            else 0
        )
    if move_speed == 4:
        pixels.fill(CYAN)
        pixels.show()
        in_min, in_max, out_min, out_max = (0, 65000, -5, 5)
        filter_joystick_deadzone = (
            lambda x: int(
                (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
            )
            if abs(x - 32768) > 500
            else 0
        )

    current_position = mousewheel.position
    position_change = current_position - last_position

    x_offset = filter_joystick_deadzone(xAxis.value) * -1  # Invert axis
    y_offset = filter_joystick_deadzone(yAxis.value)
    mouse.move(x_offset, y_offset, 0)

    if enc_btn.value and enc_down == 1:
        move_speed = move_speed + 1
        if move_speed > 4:
            move_speed = 0

        # print (move_speed)
        enc_down = 0

    if not enc_btn.value:
        enc_down = 1

    if leftbutton.value:
        mouse.release(Mouse.LEFT_BUTTON)
        # pixels.fill(BLACK)
        # pixels.show()
    else:
        mouse.press(Mouse.LEFT_BUTTON)
        pixels.fill(GREEN)
        pixels.show()

    if centerbutton.value:
        mouse.release(Mouse.MIDDLE_BUTTON)
    else:
        mouse.press(Mouse.MIDDLE_BUTTON)
        pixels.fill(YELLOW)
        pixels.show()

    # Center button on joystick
    if maint_btn.value:
        mouse.release(Mouse.LEFT_BUTTON)
    else:
        mouse.press(Mouse.LEFT_BUTTON)
        pixels.fill(GREEN)
        pixels.show()

    if rightbutton.value:
        mouse.release(Mouse.RIGHT_BUTTON)
        # pixels.fill(BLACK)
        # pixels.show()
    else:
        mouse.press(Mouse.RIGHT_BUTTON)
        pixels.fill(PURPLE)
        pixels.show()

    if not scroll_up.value:
        mouse.move(wheel=1)
        time.sleep(0.25)
        pixels.fill(BLUE)
        pixels.show()

    if not scroll_down.value:
        mouse.move(wheel=-1)
        time.sleep(0.25)
        pixels.fill(CYAN)
        pixels.show()

    if not scroll_up.value and not scroll_down.value:
        for x in range(4):
            pixels.fill(RED)
            pixels.show()
            time.sleep(0.5)
            pixels.fill(BLACK)
            pixels.show()
            time.sleep(0.5)
        microcontroller.reset()



    if position_change > 0:
        mouse.move(wheel=position_change)
        #print(current_position)
        #pixels.fill(BLUE)
        #pixels.show()
    elif position_change < 0:
        mouse.move(wheel=position_change)
        #print(current_position)
        #pixels.fill(CYAN)
        #pixels.show()
    last_position = current_position
    pixels.fill(BLACK)
    pixels.show()

boot.py

import storage
import board, digitalio
import time
from rainbowio import colorwheel
import neopixel
import busio
from digitalio import Direction
from adafruit_mcp230xx.mcp23008 import MCP23008
import digitalio
i2c = busio.I2C(board.SCL, board.SDA)
mcp = MCP23008(i2c)



#button = digitalio.DigitalInOut(board.D8)
#button.pull = digitalio.Pull.UP

button = mcp.get_pin(6)
button.direction = digitalio.Direction.INPUT
button.pull = digitalio.Pull.UP

Rstbutton = mcp.get_pin(7)
Rstbutton.direction = digitalio.Direction.INPUT
Rstbutton.pull = digitalio.Pull.UP

# NEOPIXEL
pixel_pin = board.NEOPIXEL
num_pixels = 1
pixels = neopixel.NeoPixel(pixel_pin,num_pixels,brightness=0.2,auto_write=False)

RED = (255, 0, 0)
YELLOW = (255,150, 0)
GREEN = (0, 255, 0)
CYAN = (0, 255, 255)
BLUE = (0, 0, 255)
PURPLE = (180, 0, 255)
BLACK = (0, 0, 0)

# Disable devices only if button is not pressed.
#usb_hid.enable((), boot_device=2)
if button.value:
   pixels.fill(GREEN)
   pixels.show()
   storage.disable_usb_drive()
   usb_cdc.disable()
else:
    pixels.fill(RED)
    pixels.show()
    usb_cdc.enable(console=True, data=False)
    storage.enable_usb_drive()


time.sleep(5)
5 Likes

Wow, that case is gorgeous! How did you make it?

Hi, No, unfortunately it is still way beyond my skillset :slight_smile: 3D printing is still a distant dream… I bought it online, and then CNC cut the holes…

Well, you have excellent design sense. I’m :heart_eyes_cat: that case.

were did you buy the case?

Thank you, Trevor… we try :grin:

Don:

I bought it from a local online store, called Lazada here in Thailand…

Searched for Instrument enclosure case…

Maybe you can get similar from Ebay or Amazon as it obviously came from somewhere in China…

1 Like

@donkjr
I am including a .pdf screen print of the listing. They have company details there if you are interested.

I hope that it is not seen as advertising, thus the .pdf link only…
[HOT W] 118x78x24mm Abs Plastic Enclosure Electronics Portable Handheld Control Box Project Housing Case Lazada.co.th.pdf (3.8 MB)

Advertising is about whether you benefit from posting it.

It’s fine to share links to products. We do that often.

Affiliate links must be disclosed because they benefit you.

If a vendor is here and posts links to their products, that needs to be genuinely asked for — solicited. “Spam” would be unsolicited commercial posting. So when a vendor shows up and just starts posting links to their own stuff, that’s trying to use Maker Forums as free advertising. But if a legitimately unaffiliated member of the site (not a “sock puppet” or “astroturf” user controlled by the vendor) asks a question, it’s appropriate for the vendor to respond with links that really answer the question.

Also, a person who is an active member of the community and also a vendor or employee of a vendor can post about their work, when it’s a substantial minority of what they are posting. Maker Forum’s founder, @funinthefalls († RIP), contributed as a community member, and a few times posted about products he was selling that were of legitimate interest to the community, and folks appreciated it.

But most vendors who have shown up here have only posted spam or attacks on other vendors, rather than making the investment of becoming active community members.

2 Likes

Thank you for the once again clear guidelines,@mcdanlj
I really enjoy the few interactions that I do get on this forum, as it is always constructive and beneficial to my projects. I also enjoy reading stuff here, as it has that same useful vibe to it…

Thus, I was a bit hesitant to post the link to the product, as I didn’t want to offend.

1 Like

Interesting design and nice case. Is handheld and not pushed around on a flat surface? Could you share a photo of how you hold it/arrange your fingers for normal use with all those buttons?

Did you consider adding some sort of scroll wheel? I think I would really miss having one.

The knob below and to the right of the joystick is a rotary encoder, which is assigned to the Usb Hid mousewheel class in the controller :wink:



@marmil @easytarget

This is the position to hold it. ( This is a left handed version, since I am a leftie :slight_smile: - Right handed version will be a mirror image of this… )

To answer your question, the buttons are as follows ( Starting left top )
Reset / User Defined Button
Scroll up
Scroll down
Left Center and Right Mouse Buttons

Rotary encoder for scrolling and Center button on that is user defined, currently assigned to changing mouse pointer speed)

Joystick is moved using either pointer finger or thumb.
Center on Joystick is also a left click…

Interesting to note, that while this is a left handed version, I am actually using it with the right hand quite often, and that is also very useable and comfortable

It can also be held like a “TV remote”, which is actually how I use it most of the time. My main reason for designing this was to get a mouse off a table, like when I sit on a sofa and watch Netflix etc… where I don’t necessarily want to have a table or other surface under my hand to control the PC with a mouse… The final version will be a wireless type, where the remote functionality will be completely realised :slight_smile:

Ah nice. It makes a lot more sense to my brain now that you said this is the left handed version! And that’s cool it’s still useable with your right in this configuration. Appreciate the extra info.

1 Like

You are welcome

Given you already use the XIAO 2040, does the XIAO nRF52840 figure in this plan ?? Or something different but cunning?

the nRF board looks ideal for wireless use (BT only, also NFC but no WiFi) since it has a charge circuit built in, and ridiculously low standby power use; it uses a M4 (smartwatch) core. If you pay for the ‘sense’ version you get a 6-axis mmu too, which could be put to use in a freeheld mouse.

It is quite expensive; I’ve just ordered a (non-sense) version of this to use as part of a solar-powered LoRa sensor/relay. I had already brought a ESP32-C3 (risc-v) XIAO to use for this but I’ve found a better use for that, and the nRF is even lower power, but at twice the price.

Hi Owen,

Well, yes… as you may see on the pictures of the PCB, this was originally meant to be for the XIAO ESP32S3 board…

Local supply got slightly “overpriced” and SEEED was out of stock… So, I back-traced and changed the design to an XIAO RP2040…

The nRF board could also be a nice one, especially with that 6DOF mmu…

My initial idea was however centered on the ESP-Now protocol, the one that uses a WiFi 2.4 Ghz carrier with the custom Espressif protocol…

I am currently experimenting with that, ( The ESP32S3 also has built-in battery charging support, which works extremely well …

So, I am evaluating the battery life, practical use time, sleep modes etc etc to make this a practical thing… It may take a bit of time though, because i usually dont rush things to release … on the receiver station i similar chip will need to be used…

My other consideration at the moment is if I should have only one-way communication, or full bidirectional comms, not that I can think about anything that I would send back to the “mouse” yet…

Another possible spin-off would be a custom keypad type thingy, not necessarily a full keyboard ( That could get quite pricey, as I would obviously want some decent mechanical keys on that, and those are not cheap :slight_smile:

Since ESP-Now is a point-to-multipoint protocol, both devices could theoretically send data back to the same “receiver” …

Then, the CircuitPython HID issue… I would really like to move this stuff over to proper C++ Arduino or ESP IDF code, since, as mentioned, CircuitPython seems to only support very very basic Mouse functionality ( click and movement + scrolling ), with even a simple select and or drag drop functionality being missing…

1 Like