Text Editor
Adafruit recommends using the Mu editor for editing your CircuitPython code. You can get more info in this guide.
Alternatively, you can use any text editor that saves simple text files.
Sensor Calibration
The first step, before putting the final code on your board, is to calibrate the sensor for your location. Since the magnetic sensor orientation is based on the Earth's magnetic field, your location matters!
Follow the steps shown here to get your sensor offset values, which will be printed to the REPL for you. They'll looks something like this:
Offsets_Magnetometer: (263, 315, 146)
Offsets_Gyroscope: (-1, 0, -1)
Offsets_Accelerometer: (-48, -13, -23)
Copy those values, and then paste them into the main code.py file in the lines in the BNO055 offsets section, which looks like this:
sensor.offsets_magnetometer = (263, 315, 146)
sensor.offsets_gyroscope = (-1, 0, -1)
sensor.offsets_accelerometer = (-48, -13, -23)
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}")
Download the Project Bundle
Your project will use a specific set of CircuitPython libraries, and the code.py file. To get everything you need, click on the Download Project Bundle link below, and uncompress the .zip file.
Connect your computer to the board via a known good USB power+data cable. A new flash drive should show up as CIRCUITPY.
Drag the contents of the uncompressed bundle directory onto your board's CIRCUITPY drive, replacing any existing files or directories with the same names, and adding any new ones that are necessary.
# SPDX-FileCopyrightText: 2023 JG for Cedar Grove Maker Studios # SPDX-License-Identifier: MIT """ `bno055_calibrator.py` =============================================================================== A CircuitPython module for calibrating the BNo055 9-DoF sensor. After manually calibrating the sensor, the module produces calibration offset tuples for use in project code. * Author(s): JG for Cedar Grove Maker Studios Implementation Notes -------------------- **Hardware:** * Adafruit BNo055 9-DoF sensor **Software and Dependencies:** * Driver library for the sensor in the Adafruit CircuitPython Library Bundle * Adafruit CircuitPython firmware for the supported boards: https://circuitpython.org/downloads """ import time import board import adafruit_bno055 # pylint: disable=too-few-public-methods class Mode: CONFIG_MODE = 0x00 ACCONLY_MODE = 0x01 MAGONLY_MODE = 0x02 GYRONLY_MODE = 0x03 ACCMAG_MODE = 0x04 ACCGYRO_MODE = 0x05 MAGGYRO_MODE = 0x06 AMG_MODE = 0x07 IMUPLUS_MODE = 0x08 COMPASS_MODE = 0x09 M4G_MODE = 0x0A NDOF_FMC_OFF_MODE = 0x0B NDOF_MODE = 0x0C # Uncomment these lines for UART interface connection # uart = board.UART() # sensor = adafruit_bno055.BNO055_UART(uart) # Instantiate I2C interface connection # i2c = board.I2C() # For board.SCL and board.SDA i2c = board.STEMMA_I2C() # For the built-in STEMMA QT connection sensor = adafruit_bno055.BNO055_I2C(i2c) sensor.mode = Mode.NDOF_MODE # Set the sensor to NDOF_MODE print("Magnetometer: Perform the figure-eight calibration dance.") while not sensor.calibration_status[3] == 3: # Calibration Dance Step One: Magnetometer # Move sensor away from magnetic interference or shields # Perform the figure-eight until calibrated print(f"Mag Calib Status: {100 / 3 * sensor.calibration_status[3]:3.0f}%") time.sleep(1) print("... CALIBRATED") time.sleep(1) print("Accelerometer: Perform the six-step calibration dance.") while not sensor.calibration_status[2] == 3: # Calibration Dance Step Two: Accelerometer # Place sensor board into six stable positions for a few seconds each: # 1) x-axis right, y-axis up, z-axis away # 2) x-axis up, y-axis left, z-axis away # 3) x-axis left, y-axis down, z-axis away # 4) x-axis down, y-axis right, z-axis away # 5) x-axis left, y-axis right, z-axis up # 6) x-axis right, y-axis left, z-axis down # Repeat the steps until calibrated print(f"Accel Calib Status: {100 / 3 * sensor.calibration_status[2]:3.0f}%") time.sleep(1) print("... CALIBRATED") time.sleep(1) print("Gyroscope: Perform the hold-in-place calibration dance.") while not sensor.calibration_status[1] == 3: # Calibration Dance Step Three: Gyroscope # Place sensor in any stable position for a few seconds # (Accelerometer calibration may also calibrate the gyro) print(f"Gyro Calib Status: {100 / 3 * sensor.calibration_status[1]:3.0f}%") time.sleep(1) print("... CALIBRATED") time.sleep(1) print("\nCALIBRATION COMPLETED") print("Insert these preset offset values into project code:") print(f" Offsets_Magnetometer: {sensor.offsets_magnetometer}") print(f" Offsets_Gyroscope: {sensor.offsets_gyroscope}") print(f" Offsets_Accelerometer: {sensor.offsets_accelerometer}")
How it Works
Here's how the code works.
Libraries
The code imports necessary libraries for working with the hardware, including the BNO055 sensor, USB HID interfaces for mouse and keyboard, and the Wiichuck adapter.
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
Several constants are defined, including debug flags, rates for cursor movement, keycodes for buttons, sensitivity thresholds, and debounce times.
You can toggle the DEBUG
variable to see more info print to the REPL during coding.
The CURSOR
variable can be set to true while you're coding to prevent the cursor from flying around the screen!
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)
Peripheral Setup
You'll instantiate I2C, set up HID mouse and keyboard, the Wiichuck, and the BNO055 sensor next.
# 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()
and printd()
Functions
You'll set up functions for making the piezo beep and for printing debug information based on the DEBUG
flag.
# 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
This function calculates the Euclidean distance between two sets of data. It's used to detect single taps on the BNO055 sensor.
# 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]))
9-DOF Sensor Offsets
This is where you enter your specific calibration offset values based on running the calibration described above.
# 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
The variables maintain states for the Wiichuck, including roll and pitch states, button states, and a sensor packet count for the BNO055 to limit the ratio of sensor data between the 9-DOF and the Wiichuck.
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
Beeps and Offsets
At the end of setup the piezo beeps, this is when you can point the nozzle at your screen to record a target angle offset. This is something you can re-run later with a key combo, but it is essential to keeping the relative offset of the sensor when compared to magnetic north pointed at the screen.
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)
Main Loop
In the main loop, the code does the following:
- Reads Euler angle data from the BNO055 sensor
- Calculates cursor movement based on the orientation of the sensor
- Reads inputs from the Wiichuck joystick and accelerometer
- Processes button presses and changes in sensor orientation
- Detects single taps on the BNO055 sensor and triggers actions
# 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
Page last edited January 20, 2025
Text editor powered by tinymce.