Close

[C] KMK SpaceMouse Compact emulation

A project log for Tetizmol [gd0153]

A Tetwice-layout keyboard designed to be abysmal.

kelvinakelvinA a day ago0 Comments

For the past week, I've been programming a solution to bring spacemouse emulation to KMK.

Red ESP32-S2-WROOM with Type C

Immediately upon receiving this board, I got to work on adding it to CircuitPython's supported board list, because it's surprisingly not there and there's no other boards that is essentially a S2-WROOM breakout. Long story short, I was getting MemoryError on both cpy 8 and 9, as well as discovering that pin IO38 was defective on the chip.

The long story

First I followed how to use esptool.py to communicate with the board. had to modify my PATH because I was calling MSYS Python, meaning that it was creating a linux style .venv instead of Windows.

Next, I was making a new board entry and compiling cpy 9 for espressif and then using esptool.py to flash firmware.bin. I set up the pins in Pog and got this:

So I disabled the pins from the map and checked them:

All other pins were over 3.25, most being 3.28 volts when not in the chord map, and a few mV above 0 otherwise. However, IO38 was precisely 3.202V (when out of the map), so I think there's something wrong with the chip.

Moving on, I heard pan was fixed so I tested it out and Windows 10 22H2 still complained.

Unexpectedly, I started getting hit with MemoryError, so I went to compile cpy 8, which was a can of complaints most likely because I didn't do make clear board=[boardname]. I was doing things like deleting the .espressif directory and removing all submodules when WSL unexpectedly folded and the git repo seemed corrupt.

Still memoryless, which doesn't make much sense as the RP2040 has less RAM but it worked fine.

RP2040

So the plan now is to use an RP2040 along with a 16 channel mux:

A CD74HC4067 breakout board.

It's probably for the best, since it's got a more linear ADC and more support since it has ARM cores and is the keyboard community's new favourite. For some reason, I had to swap the diode orientation again and it's because it was incorrect till Pog v1.8.2.

SpaceMouse for KMK

Here are all the USB HID descriptors that I found:

  1. https://github.com/openantz/antz/wiki/3D-Mouse
  2. https://github.com/ouser555/qmk_spacemouse/blob/main/qmk_firmware/tmk_core/protocol/usb_descriptor.c
  3. https://github.com/AndunHH/spacemouse/blob/main/SpaceNavigator.md
  4. https://github.com/AndunHH/spacemouse/blob/main/spacemouse-keys/SpaceMouseHID.h
  5. https://pastebin.com/GD5mEKW6
  6. https://thingswemake.com/six-degrees-of-syncopation/
  7. https://github.com/joshsucher/six-degrees-of-syncopation/blob/main/spacemouse_pro_emulator/boot.py
  8. https://www.cs.cornell.edu/~curran/articles/space_navigator_quirk/
  9. https://forum.3dconnexion.com/viewtopic.php?t=9827
  10. https://stackoverflow.com/questions/30805483/stm32-usb-hid-reports
  11. https://taoofmac.com/space/blog/2024/05/18/2000

My main search query in Google was:

"Usage (Multi-axis Controller)"

After 3 or so hours, I had understood enough to have an idea on what could be wrong with the AC Pan descriptor, and it worked!

I decided to use the logical minmax range of 500 instead of the more common 350 since 3DxWare supports values of -512 to +511 (signed 10b int). 

0x16, 0x0C, 0xFE,  #     Logical Minimum (-500)
0x26, 0xF4, 0x01,  #     Logical Maximum (500)
0x36, 0x00, 0x80,  #     Physical Minimum (-32768)
0x46, 0xFF, 0x7F,  #     Physical Maximum (32767)
# where 0xF4, 0x01 encodes the 2byte number 0x01F4 (500) for example

One report floating on the internet has an incorrect comment, as the bits correspond to -32768/32767 respectively:

0x16, 0x00, 0x80,     #     Logical minimum (-500)
0x26, 0xff, 0x7f,     #     Logical maximum (500)

I decided to use the report on newer devices, which have one large report on ID1 instead of a translational and rotational on ID 1 and 2 respectively. It's easier to implement and more likely to be supported for longer. 

I then went into circuitpython's device.c to pick out the descriptors, as the keyboard, pointer and consumer control descriptors now needed new report IDs.

I joined the KMK Zulip because I was tracking down an issue that didn't even exist. I was wondering how I could connect class SixAxis to SixAxisDeviceReport, not knowing I already did. I was then testing to see if it was sending PointingDeviceReports since I just copypasted the code and did the modifications.  

For some reason though, a single scroll event moved MUCH larger than it should, which is 7 in the below excel spreadsheet:

Test spreadsheet
How far a single scroll-down was

I'd go down to 1555 but only come back up to 13 for some reason. The solution was to append the SIX_AXIS device at the end. Not sure why it makes a difference.

Some buffer byte fixing later and I got this in the 3Dconnexion Viewer:

Me: *starts cheering up and down my room*

Subsequently, I implemented an extension to get the LED status:

And just a few moments before writing this log, I implemented the spacemouse_keys module. I initially intended to only expose the 2 buttons in 3DxWare but also added control for all 6 axes similar to mouse_keys. Buttons in 3DxWare change based on the application:

I didn't bother with implementing the buttons for a SpaceMouse Pro Wireless or Enterprise because a) there are already so many positive reviews of the Compact / Wireless (see below) -- so surely 3Dconnexion knows their market is fine with 2 buttons... 

...b) the button map has a few asterisks, thus it sounds tough to validate implementation correctness, and c) 3DxWare supports radial menus of 4 or 8 buttons, and when you move the cursor in its direction, the action will execute:

So, theoretically, you could probably macro up

  1. open radial menu
  2. move mouse
  3. move mouse back

to get 16 per-application buttons.

Everything seems to work, so I've submitted a pull request.

[The Next Day]

Oh, when looking at this TAO Of Mac post, I found the button mappings in pyspacemouse.py, such as the Pro Wireless:

ButtonSpec(channel=3, byte=1, bit=0),  # MENU
ButtonSpec(channel=3, byte=1, bit=1),  # FIT
ButtonSpec(channel=3, byte=1, bit=2),  # TOP
ButtonSpec(channel=3, byte=1, bit=4),  # REAR
ButtonSpec(channel=3, byte=1, bit=5),  # FRONT

ButtonSpec(channel=3, byte=2, bit=0),  # ROLL CLOCKWISE
ButtonSpec(channel=3, byte=2, bit=4),  # 1
ButtonSpec(channel=3, byte=2, bit=5),  # 2
ButtonSpec(channel=3, byte=2, bit=6),  # 3
ButtonSpec(channel=3, byte=2, bit=7),  # 4

ButtonSpec(channel=3, byte=3, bit=6),  # ESC
ButtonSpec(channel=3, byte=3, bit=7),  # ALT

ButtonSpec(channel=3, byte=4, bit=0),  # SHIFT
ButtonSpec(channel=3, byte=4, bit=1),  # CTRL
ButtonSpec(channel=3, byte=4, bit=2),  # ROTATION

Or the Enterprise:

ButtonSpec(channel=3, byte=1, bit=0), # MENU
ButtonSpec(channel=3, byte=1, bit=1), # FIT
ButtonSpec(channel=3, byte=1, bit=2), # T IN SQUARE
ButtonSpec(channel=3, byte=1, bit=4), # R IN SQUARE
ButtonSpec(channel=3, byte=1, bit=5), # F IN SQUARE

ButtonSpec(channel=3, byte=2, bit=0), # SQUARE WITH ROTATING ARROWS
ButtonSpec(channel=3, byte=2, bit=2), # ISO1
ButtonSpec(channel=3, byte=2, bit=4), # 1
ButtonSpec(channel=3, byte=2, bit=5), # 2
ButtonSpec(channel=3, byte=2, bit=6), # 3
ButtonSpec(channel=3, byte=2, bit=7), # 4

ButtonSpec(channel=3, byte=3, bit=0), # 5
ButtonSpec(channel=3, byte=3, bit=1), # 6
ButtonSpec(channel=3, byte=3, bit=2), # 7
ButtonSpec(channel=3, byte=3, bit=3), # 8
ButtonSpec(channel=3, byte=3, bit=4), # 9
ButtonSpec(channel=3, byte=3, bit=5), # 10
ButtonSpec(channel=3, byte=3, bit=6), # ESC
ButtonSpec(channel=3, byte=3, bit=7), # ALT

ButtonSpec(channel=3, byte=4, bit=0), # SHIFT
ButtonSpec(channel=3, byte=4, bit=1), # CTRL
ButtonSpec(channel=3, byte=4, bit=2), # LOCK

So I'm starting to suspect that 

Devices are NOT 'N-key Rollover' capable, meaning that there is a limited range of key combo's that generate valid events:
 - eg. On SME if you press T+F+FIT you will get an erroneous button event, (value = 105, the 'V3' button).

refers to the physical button array, and not the HID reports.

Discussions