|
| 1 | +# SPDX-FileCopyrightText: 2023 John Park for Adafruit Industries |
| 2 | +# SPDX-License-Identifier: MIT |
| 3 | +# PowerWash Simulator controller |
| 4 | +""" |
| 5 | +Hardware: |
| 6 | +# QT Py RP2040, BNO055, Wiichuck adapter, Piezo driver on D10 ('MO' pin on silk) |
| 7 | + User control: |
| 8 | + nozzle heading/roll (sensor is mounted "sideways" in washer handle) = mouse x/y |
| 9 | + nozzle tap/shake = next nozzle tip |
| 10 | + wii C button (while level) = rotate nozzle tip |
| 11 | + wii Z button = trigger water |
| 12 | + wii joystick = WASD |
| 13 | + wii roll right = change stance stand/crouch/prone |
| 14 | + wii roll left = jump |
| 15 | + wii pitch up + C button = set target angle offset |
| 16 | + wii pitch down = show dirt |
| 17 | + wii pitch down + C button = toggle aim mode |
| 18 | +""" |
| 19 | + |
| 20 | +import time |
| 21 | +import math |
| 22 | +import board |
| 23 | +from simpleio import map_range, tone |
| 24 | +import adafruit_bno055 |
| 25 | +import usb_hid |
| 26 | +from adafruit_hid.mouse import Mouse |
| 27 | +from adafruit_hid.keycode import Keycode |
| 28 | +from adafruit_hid.keyboard import Keyboard |
| 29 | +from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS |
| 30 | +from adafruit_nunchuk import Nunchuk |
| 31 | + |
| 32 | +# =========================================== |
| 33 | +# constants |
| 34 | +DEBUG = False |
| 35 | +CURSOR = True # use to toggle cursor movment during testing/use |
| 36 | +SENSOR_PACKET_FACTOR = 10 # Ratio of BNo055 data packets per Wiichuck packet |
| 37 | +HORIZONTAL_RATE = 127 # mouse x speed |
| 38 | +VERTICAL_RATE = 63 # mouse y speed |
| 39 | +WII_C_KEY_1 = Keycode.R # rotate nozzle |
| 40 | +WII_C_KEY_2 = Keycode.C # aim mode |
| 41 | +WII_PITCH_UP = 270 # value to trigger wiichuk up state |
| 42 | +WII_PITCH_DOWN = 730 # value to trigger wiichuck down state |
| 43 | +WII_ROLL_LEFT = 280 # value to trigger wiichuck left state |
| 44 | +WII_ROLL_RIGHT = 740 # value to trigger wiichuck right state |
| 45 | +TAP_THRESHOLD = 6 # Tap sensitivity threshold; depends on the physical sensor mount |
| 46 | +TAP_DEBOUNCE = 0.3 # Time for accelerometer to settle after tap (seconds) |
| 47 | + |
| 48 | +# =========================================== |
| 49 | +# Instantiate I2C interface connection |
| 50 | +# i2c = board.I2C() # For board.SCL and board.SDA |
| 51 | +i2c = board.STEMMA_I2C() # For the built-in STEMMA QT connection |
| 52 | + |
| 53 | +# =========================================== |
| 54 | +# setup USB HID mouse and keyboard |
| 55 | +mouse = Mouse(usb_hid.devices) |
| 56 | +keyboard = Keyboard(usb_hid.devices) |
| 57 | +layout = KeyboardLayoutUS(keyboard) |
| 58 | + |
| 59 | +# =========================================== |
| 60 | +# wii nunchuk setup |
| 61 | +wiichuk = Nunchuk(i2c) |
| 62 | + |
| 63 | +# =========================================== |
| 64 | +# Instantiate the BNo055 sensor |
| 65 | +sensor = adafruit_bno055.BNO055_I2C(i2c) |
| 66 | +sensor.mode = 0x0C # Set the sensor to NDOF_MODE |
| 67 | + |
| 68 | +# =========================================== |
| 69 | +# beep function |
| 70 | +def beep(freq=440, duration=0.2): |
| 71 | + """Play the piezo element for duration (sec) at freq (Hz). |
| 72 | + This is a blocking method.""" |
| 73 | + tone(board.D10, freq, duration) |
| 74 | + |
| 75 | +# =========================================== |
| 76 | +# debug print function |
| 77 | +def printd(line): |
| 78 | + """Prints a string if DEBUG is True.""" |
| 79 | + if DEBUG: |
| 80 | + print(line) |
| 81 | + |
| 82 | +# =========================================== |
| 83 | +# euclidean distance function |
| 84 | +def euclidean_distance(reference, measured): |
| 85 | + """Calculate the Euclidean distance between reference and measured points |
| 86 | + in a universe. The point position tuples can be colors, compass, |
| 87 | + accelerometer, absolute position, or almost any other multiple value data |
| 88 | + set. |
| 89 | + reference: A tuple or list of reference point position values. |
| 90 | + measured: A tuple or list of measured point position values.""" |
| 91 | + # Create list of deltas using list comprehension |
| 92 | + deltas = [(reference[idx] - count) for idx, count in enumerate(measured)] |
| 93 | + # Resolve squared deltas to a Euclidean difference and return the result |
| 94 | + # pylint:disable=c-extension-no-member |
| 95 | + return math.sqrt(sum([d ** 2 for d in deltas])) |
| 96 | + |
| 97 | +# =========================================== |
| 98 | +# BNO055 offsets |
| 99 | +# Preset the sensor calibration offsets |
| 100 | +# User sets this up once for geographic location using `bno055_calibrator.py` in library examples |
| 101 | +sensor.offsets_magnetometer = (198, 238, 465) |
| 102 | +sensor.offsets_gyroscope = (-2, 0, -1) |
| 103 | +sensor.offsets_accelerometer = (-28, -5, -29) |
| 104 | +printd(f"offsets_magnetometer set to: {sensor.offsets_magnetometer}") |
| 105 | +printd(f"offsets_gyroscope set to: {sensor.offsets_gyroscope}") |
| 106 | +printd(f"offsets_accelerometer set to: {sensor.offsets_accelerometer}") |
| 107 | + |
| 108 | +# =========================================== |
| 109 | +# controller states |
| 110 | +wii_roll_state = 1 # roll left 0, center 1, roll right 2 |
| 111 | +wii_pitch_state = 1 # pitch down 0, center 1, pitch up 2 |
| 112 | +wii_last_roll_state = 1 |
| 113 | +wii_last_pitch_state = 1 |
| 114 | +c_button_state = False |
| 115 | +z_button_state = False |
| 116 | + |
| 117 | +sensor_packet_count = 0 # Initialize the BNo055 packet counter |
| 118 | + |
| 119 | +print("PowerWash controller ready, point at center of screen for initial offset:") |
| 120 | +beep(400, 0.1) |
| 121 | +beep(440, 0.2) |
| 122 | +time.sleep(3) |
| 123 | +# The target angle offset used to reorient the wand to point at the display |
| 124 | +#pylint:disable=(unnecessary-comprehension) |
| 125 | +target_angle_offset = [angle for angle in sensor.euler] |
| 126 | +beep(220, 0.4) |
| 127 | +print("......reoriented", target_angle_offset) |
| 128 | + |
| 129 | + |
| 130 | +while True: |
| 131 | + # =========================================== |
| 132 | + # BNO055 |
| 133 | + # Get the Euler angle values from the sensor |
| 134 | + # The Euler angle limits are: +180 to -180 pitch, +360 to -360 heading, +90 to -90 roll |
| 135 | + sensor_euler = sensor.euler |
| 136 | + sensor_packet_count += 1 # Increment the BNo055 packet counter |
| 137 | + # Adjust the Euler angle values with the target_position_offset |
| 138 | + heading, roll, pitch = [ |
| 139 | + position - target_angle_offset[idx] for idx, |
| 140 | + position in enumerate(sensor_euler) |
| 141 | + ] |
| 142 | + printd(f"heading {heading}, roll {roll}") |
| 143 | + # Scale the heading for horizontal movement range |
| 144 | + # horizontal_mov = map_range(heading, 220, 260, -30.0, 30.0) |
| 145 | + horizontal_mov = int(map_range(heading, -16, 16, HORIZONTAL_RATE*-1, HORIZONTAL_RATE)) |
| 146 | + printd(f"mouse x: {horizontal_mov}") |
| 147 | + |
| 148 | + # Scale the roll for vertical movement range |
| 149 | + vertical_mov = int(map_range(roll, 9, -9, VERTICAL_RATE*-1, VERTICAL_RATE)) |
| 150 | + printd(f"mouse y: {vertical_mov}") |
| 151 | + if CURSOR: |
| 152 | + mouse.move(x=horizontal_mov) |
| 153 | + mouse.move(y=vertical_mov) |
| 154 | + |
| 155 | + # =========================================== |
| 156 | + # sensor packet ratio |
| 157 | + # Read the wiichuck every "n" times the BNo055 is read |
| 158 | + if sensor_packet_count >= SENSOR_PACKET_FACTOR: |
| 159 | + sensor_packet_count = 0 # Reset the BNo055 packet counter |
| 160 | + |
| 161 | + # =========================================== |
| 162 | + # wiichuck joystick |
| 163 | + joy_x, joy_y = wiichuk.joystick |
| 164 | + printd(f"joystick = {wiichuk.joystick}") |
| 165 | + if joy_x < 25: |
| 166 | + keyboard.press(Keycode.A) |
| 167 | + else: |
| 168 | + keyboard.release(Keycode.A) |
| 169 | + |
| 170 | + if joy_x > 225: |
| 171 | + keyboard.press(Keycode.D) |
| 172 | + else: |
| 173 | + keyboard.release(Keycode.D) |
| 174 | + |
| 175 | + if joy_y > 225: |
| 176 | + keyboard.press(Keycode.W) |
| 177 | + else: |
| 178 | + keyboard.release(Keycode.W) |
| 179 | + |
| 180 | + if joy_y < 25: |
| 181 | + keyboard.press(Keycode.S) |
| 182 | + else: |
| 183 | + keyboard.release(Keycode.S) |
| 184 | + |
| 185 | + # =========================================== |
| 186 | + # wiichuck accel |
| 187 | + wii_roll, wii_pitch, wii_az = wiichuk.acceleration |
| 188 | + printd(f"roll:, {wii_roll}, pitch:, {wii_pitch}") |
| 189 | + if wii_roll <= WII_ROLL_LEFT: |
| 190 | + wii_roll_state = 0 |
| 191 | + if wii_last_roll_state != 0: |
| 192 | + keyboard.press(Keycode.SPACE) # jump |
| 193 | + wii_last_roll_state = 0 |
| 194 | + elif WII_ROLL_LEFT < wii_roll < WII_ROLL_RIGHT: # centered |
| 195 | + wii_roll_state = 1 |
| 196 | + if wii_last_roll_state != 1: |
| 197 | + keyboard.release(Keycode.LEFT_CONTROL) |
| 198 | + keyboard.release(Keycode.SPACE) |
| 199 | + wii_last_roll_state = 1 |
| 200 | + else: |
| 201 | + wii_roll_state = 2 |
| 202 | + if wii_last_roll_state != 2: |
| 203 | + keyboard.press(Keycode.LEFT_CONTROL) # change stance |
| 204 | + wii_last_roll_state = 2 |
| 205 | + |
| 206 | + if wii_pitch <= WII_PITCH_UP: # up used as modifier |
| 207 | + wii_pitch_state = 0 |
| 208 | + if wii_last_pitch_state != 0: |
| 209 | + beep(freq=660) |
| 210 | + wii_last_pitch_state = 0 |
| 211 | + elif WII_PITCH_UP < wii_pitch < WII_PITCH_DOWN: # level |
| 212 | + wii_pitch_state = 1 |
| 213 | + if wii_last_pitch_state != 1: |
| 214 | + wii_last_pitch_state = 1 |
| 215 | + else: |
| 216 | + wii_pitch_state = 2 # down sends command and is modifier |
| 217 | + if wii_last_pitch_state != 2: |
| 218 | + keyboard.send(Keycode.TAB) |
| 219 | + beep(freq=110) |
| 220 | + wii_last_pitch_state = 2 |
| 221 | + |
| 222 | + # =========================================== |
| 223 | + # wiichuck buttons |
| 224 | + if wii_pitch_state == 0: # button use when wiichuck is held level |
| 225 | + if wiichuk.buttons.C and c_button_state is False: |
| 226 | + target_angle_offset = [angle for angle in sensor_euler] |
| 227 | + beep() |
| 228 | + beep() |
| 229 | + c_button_state = True |
| 230 | + if not wiichuk.buttons.C and c_button_state is True: |
| 231 | + c_button_state = False |
| 232 | + |
| 233 | + elif wii_pitch_state == 1: # level |
| 234 | + if wiichuk.buttons.C and c_button_state is False: |
| 235 | + keyboard.press(WII_C_KEY_1) |
| 236 | + c_button_state = True |
| 237 | + if not wiichuk.buttons.C and c_button_state is True: |
| 238 | + keyboard.release(WII_C_KEY_1) |
| 239 | + c_button_state = False |
| 240 | + |
| 241 | + elif wii_pitch_state == 2: # down |
| 242 | + if wiichuk.buttons.C and c_button_state is False: |
| 243 | + keyboard.press(WII_C_KEY_2) |
| 244 | + c_button_state = True |
| 245 | + if not wiichuk.buttons.C and c_button_state is True: |
| 246 | + keyboard.release(WII_C_KEY_2) |
| 247 | + c_button_state = False |
| 248 | + |
| 249 | + if wiichuk.buttons.Z and z_button_state is False: |
| 250 | + mouse.press(Mouse.LEFT_BUTTON) |
| 251 | + z_button_state = True |
| 252 | + if not wiichuk.buttons.Z and z_button_state is True: |
| 253 | + mouse.release(Mouse.LEFT_BUTTON) |
| 254 | + z_button_state = False |
| 255 | + |
| 256 | + # =========================================== |
| 257 | + # BNO055 tap detection |
| 258 | + # Detect a single tap on any axis of the BNo055 accelerometer |
| 259 | + accel_sample_1 = sensor.acceleration # Read one sample |
| 260 | + accel_sample_2 = sensor.acceleration # Read the next sample |
| 261 | + if euclidean_distance(accel_sample_1, accel_sample_2) >= TAP_THRESHOLD: |
| 262 | + # The difference between two consecutive samples exceeded the threshold () |
| 263 | + # (equivalent to a high-pass filter) |
| 264 | + mouse.move(wheel=1) |
| 265 | + printd("SINGLE tap detected") |
| 266 | + beep() |
| 267 | + time.sleep(TAP_DEBOUNCE) # Debounce delay |
0 commit comments