Kenneth Yang
Published © GPL3+

Xtend: Natural Movement Mouse

Extend yourself into the virtual realm by converting natural hand movements (such as twisting and rotating) into your favorite apps.

IntermediateFull instructions provided1 hour1,178
Xtend: Natural Movement Mouse

Things used in this project

Hardware components

3D Magnetic Sensor 2Go
Infineon 3D Magnetic Sensor 2Go
×1
Infineon Rot. Knob for 3D 2Go
×1
Large Tactile Pushbutton (with cap)
The link is just to the type of button that is needed. If you can find a better deal, go with it!
×2
RGB Diffused Common Cathode
RGB Diffused Common Cathode
×1
Breadboard (generic)
Breadboard (generic)
"Half+" size
×1
Resistor 221 ohm
Resistor 221 ohm
×2
Jumper wires (generic)
Jumper wires (generic)
Soldering is required
×10

Software apps and online services

Arduino IDE
Arduino IDE
Fusion 360
Autodesk Fusion 360
Used to make the casing
Blender 3D
Demo app

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Laser cutter (generic)
Laser cutter (generic)

Story

Read more

Custom parts and enclosures

Case

DXF file for laser cutting

Schematics

Breadboard view

Breadboard diagram of circuit. This is what you will be building

Schematic View

3D Magnetic Sensor 2Go

Fritzing Part used in the Breadboard schematic

Code

Python applet code

Python
Code to the Python applet that reads data from the device and converts it to mouse movements. This is just an example interpreter, do whatever you want with the data from the device!
# Edit Modes and their Functions!
modes = {
        "Blender":      ["MIDDLE", "SHIFT+MIDDLE"],
        "Fusion 360":   ["SHIFT+MIDDLE", "MIDDLE"]
}

#====== SYSTEM CODE (Don't touch it unless you know what you're doing) ======#
from tkinter import *
from tkinter import ttk
import sys
import glob
import serial
import pyautogui

root = Tk()

# ===== Global Vars =====
device = StringVar()

mode = StringVar()

mouseKeys = ["LEFT", "MIDDLE", "RIGHT"]
opKeys = ["SHIFT", "CTRL", "ALT", "WIN", "COMMAND", "OPTION"]

# =====  Functions  =====
def clearMF():
    for wid in mf.winfo_children():
        wid.destroy()

# Setup
def serial_ports():
    if sys.platform.startswith('win'):
        ports = ['COM%s' % (i + 1) for i in range(256)]
    elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'):
        # this excludes your current terminal "/dev/tty"
        ports = glob.glob('/dev/tty[A-Za-z]*')
    elif sys.platform.startswith('darwin'):
        ports = glob.glob('/dev/cu.*')
    else:
        raise EnvironmentError('Unsupported platform')

    result = []
    for port in ports:
        try:
            s = serial.Serial(port)
            s.close()
            result.append(port)
        except (OSError, serial.SerialException):
            pass
    return result
def connectDevice():
    clearMF()
    ttk.Label(mf, text="Connecting...").pack()
    try:
        s = serial.Serial(device.get())
        print(s)
        clearMF()
        setupScreen()
    except (OSError, serial.SerialException):
        print(serial.SerialException)

# main
def setupScreen():
    clearMF()

    # Connection
    ttk.Label(mf, text="Connected to: ").grid(row=1, column=1, sticky='e')
    ttk.Label(mf, text=device.get(), relief='sunken').grid(row=1, column=2, sticky='w')
    ttk.Button(mf, text="Disconnect", command=disconnect).grid(row=1, column=3, columnspan=2, sticky='ew')
    ttk.Separator(mf, orient=HORIZONTAL).grid(row=2, columnspan=5, sticky='ew', pady=3)

    # Modes
    ttk.Label(mf, text="Mode: ").grid(row=3, column=1, sticky='e')
    mdComb = ttk.Combobox(mf, textvariable=mode, state="readonly")
    mdComb.grid(row=3, column=2, sticky='w')
    mdComb['values'] = list(modes.keys())

    ttk.Button(mf, text="Launch", command=run).grid(row=4, column=1, columnspan=4, pady=5, sticky='ew')

# Main
def disconnect():
    # s.close()
    print("Disconnected")
    root.destroy()
    exit()
def run():
    firstRun = True
    prevDeg = 0
    while True:
        with serial.Serial(device.get(), 9600, timeout=1) as ser:
            dat = str(ser.readline())
            dat = dat[2:]
            dat = dat[:-5]
            print(dat)
            split = [int(dat[0]), int(dat[1]), int(float(dat[2:]))]
            print(split)

            if firstRun:
                prevDeg = split[2]
                firstRun = False

            function = modes[mode.get()][split[0]]
            calls = function.split('+')
            fail = False
            mouseK = ''
            if '' in calls:
                calls.remove('')
            for command in calls:
                print(command.lower())
                if command in opKeys:
                    pyautogui.keyDown(command.lower()) # press opKey
                elif command not in mouseKeys:
                    if command == '':
                        command = '+'
                    pyautogui.press(command.lower()) # press regular key
                else:
                    if command in mouseKeys:
                        mouseK = command.lower()
                    else:
                        print("Not a valid command")
                        fail = True
                        break
            if fail:
                print("ended")
                break

            #calculate distance
            curDeg = split[2]
            movePx = curDeg - prevDeg

            if abs(movePx) > 300:
                if movePx < 0:
                    movePx = 360 - prevDeg + curDeg
                else:
                    movePx = 360 - curDeg + prevDeg

            if split[1] == 0:
                pyautogui.dragRel(movePx, 0, 0.1, button=mouseK)
            elif split[1] == 1:
                pyautogui.dragRel(0, movePx, 0.1, button=mouseK)
            else:
                print("ended2")
                break

            prevDeg = curDeg


# =====   Setup     =====
root.title("Xtend Interpreter")

mf = ttk.Frame(root, padding="5 5 5 5")
mf.grid(row=0, column=0, sticky=(N,E,W,S))

# Connecting Screen
ttk.Label(mf, text="Welcome!").grid(row=1)
cnPrompt = StringVar()
ttk.Label(mf, textvariable=cnPrompt).grid(row=2)

avPorts = serial_ports()
if len(avPorts) == 1: # Only One device detected
    device.set(avPorts[0])
    cnPrompt.set("Connect to your device:")
    ttk.Button(mf, text="Connect to %s!"%avPorts[0], command=connectDevice).grid(row=4)
    device.set(avPorts[0])
elif len(avPorts) == 0: # No devices detected
    cnPrompt.set("No devices detected, please connect device and try again!")
else: # Multiple devices detected
    cnPrompt.set("Please select your device from the dropdown:")
    device.set(avPorts[0])
    dvPorts = ttk.Combobox(mf, textvariable=device, state="readonly")
    dvPorts.grid(row=3, pady=10)
    dvPorts['values'] = avPorts
    ttk.Button(mf, text="Connect!", command=connectDevice).grid(row=4)




# =====    Main     =====
root.protocol("WM_DELETE_WINDOW", disconnect)
root.mainloop()

Arduino Firmware

Arduino
Upload it to the 3D Magnetic sensor 2Go. FEEL FREE TO MAKE CHANGES
#include <Tle493d_w2b6.h>

//LEDs
#define redLED 5
#define greenLED 4

//Btns
#define redBTN 10
#define greenBTN 9
int depressedState = LOW; // state of pin when btn is depressed (change if this is different for you)

Tle493d_w2b6 sen = Tle493d_w2b6();

unsigned int func = 0;
unsigned int isUp = 2;


void setup() {
  pinMode(redLED, OUTPUT);
  pinMode(greenLED, OUTPUT);

  pinMode(redBTN, INPUT);
  pinMode(greenBTN, INPUT);

  Serial.begin(9600);
  while (!Serial);
  sen.begin();
  sen.enableTemp();
}
void loop() {
  sen.begin();
  sen.updateData();

  float angle = map(atan2(sen.getY(), sen.getX()) * (180 / M_PI), -180.00, 180.00, 0.00, 360.00);

  if (digitalRead(redBTN) == depressedState) {
    func = 0;
    offAll();
    digitalWrite(redLED, HIGH);
  } else if (digitalRead(greenBTN) == depressedState) {
    func = 1;
    offAll();
    digitalWrite(greenLED, HIGH);
  }

  upCheck();

  if (digitalRead(redBTN) == depressedState && digitalRead(greenBTN) == depressedState) {
    isUp = 2;
  }

  Serial.print(func);
  Serial.print(isUp);
  Serial.println(angle);
}

void upCheck() {
  double phi = sqrt(sq(sen.getX()) + sq(sen.getY()) + sq(sen.getZ()));
  if (phi > 50) {
    isUp = 1;
  } else {
    isUp = 0;
  }
}
void offAll() {
  digitalWrite(redLED, LOW);
  digitalWrite(greenLED, LOW);
}

Credits

Kenneth Yang

Kenneth Yang

8 projects • 100 followers
Maker, developer, 3D content creator

Comments