Skip to content

Commit 05ab1e7

Browse files
authored
Merge pull request #2653 from jedgarpark/powerwash-controller
first commit powerwasher controller code
2 parents 93fff1f + 43df609 commit 05ab1e7

File tree

1 file changed

+267
-0
lines changed

1 file changed

+267
-0
lines changed

PowerWash_Controller/code.py

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
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

Comments
 (0)