Skip to content

Commit

Permalink
Merge pull request #2653 from jedgarpark/powerwash-controller
Browse files Browse the repository at this point in the history
first commit powerwasher controller code
  • Loading branch information
TheKitty authored Oct 27, 2023
2 parents 93fff1f + 43df609 commit 05ab1e7
Showing 1 changed file with 267 additions and 0 deletions.
267 changes: 267 additions & 0 deletions PowerWash_Controller/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
# SPDX-FileCopyrightText: 2023 John Park for Adafruit Industries
# SPDX-License-Identifier: MIT
# PowerWash Simulator controller
"""
Hardware:
# QT Py RP2040, BNO055, Wiichuck adapter, Piezo driver on D10 ('MO' pin on silk)
User control:
nozzle heading/roll (sensor is mounted "sideways" in washer handle) = mouse x/y
nozzle tap/shake = next nozzle tip
wii C button (while level) = rotate nozzle tip
wii Z button = trigger water
wii joystick = WASD
wii roll right = change stance stand/crouch/prone
wii roll left = jump
wii pitch up + C button = set target angle offset
wii pitch down = show dirt
wii pitch down + C button = toggle aim mode
"""

import time
import math
import board
from simpleio import map_range, tone
import adafruit_bno055
import usb_hid
from adafruit_hid.mouse import Mouse
from adafruit_hid.keycode import Keycode
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_nunchuk import Nunchuk

# ===========================================
# constants
DEBUG = False
CURSOR = True # use to toggle cursor movment during testing/use
SENSOR_PACKET_FACTOR = 10 # Ratio of BNo055 data packets per Wiichuck packet
HORIZONTAL_RATE = 127 # mouse x speed
VERTICAL_RATE = 63 # mouse y speed
WII_C_KEY_1 = Keycode.R # rotate nozzle
WII_C_KEY_2 = Keycode.C # aim mode
WII_PITCH_UP = 270 # value to trigger wiichuk up state
WII_PITCH_DOWN = 730 # value to trigger wiichuck down state
WII_ROLL_LEFT = 280 # value to trigger wiichuck left state
WII_ROLL_RIGHT = 740 # value to trigger wiichuck right state
TAP_THRESHOLD = 6 # Tap sensitivity threshold; depends on the physical sensor mount
TAP_DEBOUNCE = 0.3 # Time for accelerometer to settle after tap (seconds)

# ===========================================
# Instantiate I2C interface connection
# i2c = board.I2C() # For board.SCL and board.SDA
i2c = board.STEMMA_I2C() # For the built-in STEMMA QT connection

# ===========================================
# setup USB HID mouse and keyboard
mouse = Mouse(usb_hid.devices)
keyboard = Keyboard(usb_hid.devices)
layout = KeyboardLayoutUS(keyboard)

# ===========================================
# wii nunchuk setup
wiichuk = Nunchuk(i2c)

# ===========================================
# Instantiate the BNo055 sensor
sensor = adafruit_bno055.BNO055_I2C(i2c)
sensor.mode = 0x0C # Set the sensor to NDOF_MODE

# ===========================================
# beep function
def beep(freq=440, duration=0.2):
"""Play the piezo element for duration (sec) at freq (Hz).
This is a blocking method."""
tone(board.D10, freq, duration)

# ===========================================
# debug print function
def printd(line):
"""Prints a string if DEBUG is True."""
if DEBUG:
print(line)

# ===========================================
# euclidean distance function
def euclidean_distance(reference, measured):
"""Calculate the Euclidean distance between reference and measured points
in a universe. The point position tuples can be colors, compass,
accelerometer, absolute position, or almost any other multiple value data
set.
reference: A tuple or list of reference point position values.
measured: A tuple or list of measured point position values."""
# Create list of deltas using list comprehension
deltas = [(reference[idx] - count) for idx, count in enumerate(measured)]
# Resolve squared deltas to a Euclidean difference and return the result
# pylint:disable=c-extension-no-member
return math.sqrt(sum([d ** 2 for d in deltas]))

# ===========================================
# BNO055 offsets
# Preset the sensor calibration offsets
# User sets this up once for geographic location using `bno055_calibrator.py` in library examples
sensor.offsets_magnetometer = (198, 238, 465)
sensor.offsets_gyroscope = (-2, 0, -1)
sensor.offsets_accelerometer = (-28, -5, -29)
printd(f"offsets_magnetometer set to: {sensor.offsets_magnetometer}")
printd(f"offsets_gyroscope set to: {sensor.offsets_gyroscope}")
printd(f"offsets_accelerometer set to: {sensor.offsets_accelerometer}")

# ===========================================
# controller states
wii_roll_state = 1 # roll left 0, center 1, roll right 2
wii_pitch_state = 1 # pitch down 0, center 1, pitch up 2
wii_last_roll_state = 1
wii_last_pitch_state = 1
c_button_state = False
z_button_state = False

sensor_packet_count = 0 # Initialize the BNo055 packet counter

print("PowerWash controller ready, point at center of screen for initial offset:")
beep(400, 0.1)
beep(440, 0.2)
time.sleep(3)
# The target angle offset used to reorient the wand to point at the display
#pylint:disable=(unnecessary-comprehension)
target_angle_offset = [angle for angle in sensor.euler]
beep(220, 0.4)
print("......reoriented", target_angle_offset)


while True:
# ===========================================
# BNO055
# Get the Euler angle values from the sensor
# The Euler angle limits are: +180 to -180 pitch, +360 to -360 heading, +90 to -90 roll
sensor_euler = sensor.euler
sensor_packet_count += 1 # Increment the BNo055 packet counter
# Adjust the Euler angle values with the target_position_offset
heading, roll, pitch = [
position - target_angle_offset[idx] for idx,
position in enumerate(sensor_euler)
]
printd(f"heading {heading}, roll {roll}")
# Scale the heading for horizontal movement range
# horizontal_mov = map_range(heading, 220, 260, -30.0, 30.0)
horizontal_mov = int(map_range(heading, -16, 16, HORIZONTAL_RATE*-1, HORIZONTAL_RATE))
printd(f"mouse x: {horizontal_mov}")

# Scale the roll for vertical movement range
vertical_mov = int(map_range(roll, 9, -9, VERTICAL_RATE*-1, VERTICAL_RATE))
printd(f"mouse y: {vertical_mov}")
if CURSOR:
mouse.move(x=horizontal_mov)
mouse.move(y=vertical_mov)

# ===========================================
# sensor packet ratio
# Read the wiichuck every "n" times the BNo055 is read
if sensor_packet_count >= SENSOR_PACKET_FACTOR:
sensor_packet_count = 0 # Reset the BNo055 packet counter

# ===========================================
# wiichuck joystick
joy_x, joy_y = wiichuk.joystick
printd(f"joystick = {wiichuk.joystick}")
if joy_x < 25:
keyboard.press(Keycode.A)
else:
keyboard.release(Keycode.A)

if joy_x > 225:
keyboard.press(Keycode.D)
else:
keyboard.release(Keycode.D)

if joy_y > 225:
keyboard.press(Keycode.W)
else:
keyboard.release(Keycode.W)

if joy_y < 25:
keyboard.press(Keycode.S)
else:
keyboard.release(Keycode.S)

# ===========================================
# wiichuck accel
wii_roll, wii_pitch, wii_az = wiichuk.acceleration
printd(f"roll:, {wii_roll}, pitch:, {wii_pitch}")
if wii_roll <= WII_ROLL_LEFT:
wii_roll_state = 0
if wii_last_roll_state != 0:
keyboard.press(Keycode.SPACE) # jump
wii_last_roll_state = 0
elif WII_ROLL_LEFT < wii_roll < WII_ROLL_RIGHT: # centered
wii_roll_state = 1
if wii_last_roll_state != 1:
keyboard.release(Keycode.LEFT_CONTROL)
keyboard.release(Keycode.SPACE)
wii_last_roll_state = 1
else:
wii_roll_state = 2
if wii_last_roll_state != 2:
keyboard.press(Keycode.LEFT_CONTROL) # change stance
wii_last_roll_state = 2

if wii_pitch <= WII_PITCH_UP: # up used as modifier
wii_pitch_state = 0
if wii_last_pitch_state != 0:
beep(freq=660)
wii_last_pitch_state = 0
elif WII_PITCH_UP < wii_pitch < WII_PITCH_DOWN: # level
wii_pitch_state = 1
if wii_last_pitch_state != 1:
wii_last_pitch_state = 1
else:
wii_pitch_state = 2 # down sends command and is modifier
if wii_last_pitch_state != 2:
keyboard.send(Keycode.TAB)
beep(freq=110)
wii_last_pitch_state = 2

# ===========================================
# wiichuck buttons
if wii_pitch_state == 0: # button use when wiichuck is held level
if wiichuk.buttons.C and c_button_state is False:
target_angle_offset = [angle for angle in sensor_euler]
beep()
beep()
c_button_state = True
if not wiichuk.buttons.C and c_button_state is True:
c_button_state = False

elif wii_pitch_state == 1: # level
if wiichuk.buttons.C and c_button_state is False:
keyboard.press(WII_C_KEY_1)
c_button_state = True
if not wiichuk.buttons.C and c_button_state is True:
keyboard.release(WII_C_KEY_1)
c_button_state = False

elif wii_pitch_state == 2: # down
if wiichuk.buttons.C and c_button_state is False:
keyboard.press(WII_C_KEY_2)
c_button_state = True
if not wiichuk.buttons.C and c_button_state is True:
keyboard.release(WII_C_KEY_2)
c_button_state = False

if wiichuk.buttons.Z and z_button_state is False:
mouse.press(Mouse.LEFT_BUTTON)
z_button_state = True
if not wiichuk.buttons.Z and z_button_state is True:
mouse.release(Mouse.LEFT_BUTTON)
z_button_state = False

# ===========================================
# BNO055 tap detection
# Detect a single tap on any axis of the BNo055 accelerometer
accel_sample_1 = sensor.acceleration # Read one sample
accel_sample_2 = sensor.acceleration # Read the next sample
if euclidean_distance(accel_sample_1, accel_sample_2) >= TAP_THRESHOLD:
# The difference between two consecutive samples exceeded the threshold ()
# (equivalent to a high-pass filter)
mouse.move(wheel=1)
printd("SINGLE tap detected")
beep()
time.sleep(TAP_DEBOUNCE) # Debounce delay

0 comments on commit 05ab1e7

Please sign in to comment.