# Project export: Physical Digital Darts (Wii Darts)

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 12.0
- Tagline: "Playing a game," involving a computer usually means using a keyboard and mouse. Through affordable and open-source design, we're here to change that by providing a customizable alternative.
- Devpost: https://devpost.com/software/digital-physical-darts-wii-darts
- GitHub: https://github.com/commonkestrel/physical-digital-darts
- Video: https://www.youtube.com/embed/d-0ktDKMHS8?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Cal Hacks: Best Beginner Hack)
- Team: 3 GitHub contributor(s) — slipperee_slime (22 commits), commonkestrel (11 commits), Gianna (1 commits)

## Devpost submission (written by the team)

### Overview

💡

### Inspiration

As a team who values accessible and immersive gaming experiences, we wanted to create a hack that embodied both of these ideas across software, hardware, and design. Taking inspiration from the Nintendo Wii and Switch consoles, we choose to create a Physical Digital Dart which allows the user to perform throwing motions as they would in real life, captures that motion with an accelerometer, and simulates the result on a digital dartboard. 🎯 What It Does After the MPU6050 accelerometer reads its current angles, those angles are filtered for noise on the Arduino Nano which then transmits the button state, roll, pitch, and yaw of the Physical Digital Dart via serial monitor to our Python dartboard simulator which launches a dart in the correct direction on the button’s release at the angle given by the MPU6050. To account for drift, a double tap of the button re-calibrates the dart’s orientation to a standardized orientation. 🛠️ How We Built It To optimize space and keep the dart as small as possible, we utilized small gauge wires for connections and assembled them to minimize wire overlap. The dart-like 3D printed casing contains an Arduino nano which is connected to a MPU6050 accelerometer via perfboard Along the side of the Physical Digital Dart is one button to keep track of the start (when pressed) and end of the throw (when released). 🧱 Challenges We Ran Into The accelerometer we were provided is an MPU6050 accelerometer and gyroscope. This type of small MEMS sensor is great for finding orientation, but velocity and position are far out of reach for the level of inaccuracy and drift found in the sensor. In order to fix this, we developed a specialized filtering algorithm for our application that was able to combat these issues and keep the drift in check. Additionally, as we sought to use the 3D printers on the first day of the hackathon, we found that 3/4 were malfunctioning. In order to print our design and let others print theirs we worked through the night to fix the Prusa I3 Mark 2, the Ender 3, and the Ender 3 V2 SE, ultimately getting all 3 printers into a functioning state. 🏆 Accomplishments That We're Proud Of Combining software, hardware, AND 3D design into a working product with very limited time Creating a product which can be adjusted in shape to meet the needs of its users Creating a product which combines the physical and digital worlds, redefining what it means to “play a game." 📚 What We Learned For our future hackathons, we noted that it would be best to bring as much personal hardware as needed, considering that there may not be a sufficient amount of hardware available to borrow from the venue. 🚀 What's Next for Digital Physical Darts (Wii Darts) 2D → 3D Since our current version is 2D, we would like to implement a 3D version with better visuals and more character. Some additional features include: Player-versus-player gameplay A Wii Sports-like aesthetics Fix Accelerometer Drift Originally, our team wanted to track the position of the dart at all times but found that the drift of the MPU6050 made the double integration necessary to find that position highly accurate. A more precise accelerometer combined with a more intense filtering algorithm such as a Kalman filter would yield better results.

## README (from the GitHub repository)

# Physical Digital Darts
## 💡 Inspiration

As a team who values accessible and immersive gaming experiences, we wanted to create a hack that embodied both of these ideas across software, hardware, and design. Taking inspiration from the Nintendo Wii and Switch consoles, we choose to create a Physical Digital Dart which allows the user to perform throwing motions as they would in real life, captures that motion with an accelerometer, and simulates the result on a digital dartboard.

---

## 🎯 What It Does

After the MPU6050 accelerometer reads its current angles, those angles are filtered for noise on the Arduino Nano which then transmits the button state, roll, pitch, and yaw of the Physical Digital Dart via serial monitor to our Python dartboard simulator which launches a dart in the correct direction on the button’s release at the angle given by the MPU6050.

To account for drift, a double tap of the button re-calibrates the dart’s orientation to a standardized orientation.

---

## 🛠️ How We Built It

To optimize space and keep the dart as small as possible, we utilized small gauge wires for connections and assembled them to minimize wire overlap. The dart-like 3D printed casing contains an Arduino nano which is connected to a MPU6050 accelerometer via perfboard  Along the side of the Physical Digital Dart is one button to keep track of the start (when pressed) and end of the throw (when released).

---

## 🧱 Challenges We Ran Into

The accelerometer we were provided is an MPU6050 accelerometer and gyroscope. This type of small MEMS sensor is great for finding orientation, but velocity and position are far out of reach for the level of inaccuracy and drift found in the sensor. In order to fix this, we developed a specialized filtering algorithm for our application that was able to combat these issues and keep the drift in check. 

Additionally, as we sought to use the 3D printers on the first day of the hackathon, we found that 3/4 were malfunctioning. In order to print our design and let others print theirs we worked through the night to fix the Prusa I3 Mark 2, the Ender 3, and the Ender 3 V2 SE, ultimately getting all 3 printers into a functioning state.

---

## 🏆 Accomplishments That We're Proud Of

1. Combining software, hardware, AND 3D design into a working product with very limited time 
2. Creating a product which can be adjusted in shape to meet the needs of its users
3. Creating a product which combines the physical and digital worlds, redefining what it means to “play a game."

---

## 📚 What We Learned

For our future hackathons, we noted that it would be best to bring as much personal hardware as needed, considering that there may not be a sufficient amount of hardware available to borrow from the venue. 

---

## 🚀 What's Next for Digital Physical Darts (Wii Darts)

### 2D → 3D
Since our current version is 2D, we would like to implement a 3D version with better visuals and more character. Some additional features include:
- Player-versus-player gameplay
- A Wii Sports-like aesthetics

### Fix Accelerometer Drift
Originally, our team wanted to track the position of the dart at all times but found that the drift of the MPU6050 made the double integration necessary to find that position highly accurate. A more precise accelerometer combined with a more intense filtering algorithm such as a Kalman filter would yield better results.


## Detected evidence (automated analysis)

Indexed codebase: 10 recognized source files, 34 KB.
- Python (language) — detected in the code
- Rust (language) — detected in the code

## Codebase structure (from repository index)

### Files (21 of 21)

```
accel/.cargo/config.toml
accel/.gitignore
accel/Cargo.lock
accel/Cargo.toml
accel/LICENSE-APACHE
accel/LICENSE-MIT
accel/Ravedude.toml
accel/README.md
accel/rust-toolchain.toml
accel/src/f32_ufmt.rs
accel/src/main.rs
accel/src/mpu6050.rs
accel/src/rolling_buffer.rs
cad files/DartBody.stl
cad files/DartLid.stl
cad files/Fin (x3).stl
python/DartboardClasses.py
python/DartboardSimulator.py
python/PhysicsSimClasses.py
python/SimulateDartboard.py
README.md
```

### Dependencies

- accel/Cargo.toml: arduino-hal, avr-device@0.7.0, embedded-hal@1.0, micromath@2.1.0, nb@1.1.0, panic-halt@1.0.0, ufmt@0.2.0

### Recent commits (newest first)

- read.me added
- WE ARE NOW OPEN SOURCE
- It's working
- Merge remote-tracking branch 'origin/main'
- OH MY GOD ITS WORKING
- convert to polar coordinates :p
- Merge remote-tracking branch 'origin/main'
- AHHHHHHHHHHHHHHHHHH
- finish accel program :3
- Merge branch 'main' of https://github.com/commonkestrel/physical-digital-darts
- fix x velocity!
- was trying to center the board but gave up
- working on edge cases
- Darts can be thrown backwards
- data is parsing!
- Merge branch 'main' of https://github.com/commonkestrel/physical-digital-darts
- filtered :)
- Serial reader
- shot preview is visible
- serial read simulator is working!

## Key source files (fetched from GitHub, selected and truncated for size)

### accel/Cargo.toml

```
[package]
name = "accel"
version = "0.1.0"
authors = ["Kestrel Bergthold <dev@kestrel.pet>"]
edition = "2021"
license = "MIT OR Apache-2.0"

[[bin]]
name = "accel"
test = false
bench = false

[dependencies]
panic-halt = "1.0.0"
ufmt = "0.2.0"
nb = "1.1.0"
embedded-hal = "1.0"
avr-device = "0.7.0"
micromath = "2.1.0"

[dependencies.arduino-hal]
git = "https://github.com/rahix/avr-hal"
rev = "5ccdd12d724d5b65273eb33a27cb55ab2ddcc865"
features = ["arduino-nano"]

# Configure the build for minimal size - AVRs have very little program memory
[profile.dev]
panic = "abort"
lto = true
opt-level = "s"

[profile.release]
panic = "abort"
codegen-units = 1
debug = true
lto = true
opt-level = "s"

```

### accel/src/main.rs

```rust
#![no_std]
#![no_main]
#![feature(abi_avr_interrupt)]

mod mpu6050;
mod rolling_buffer;
mod f32_ufmt;

use core::cell;

use micromath::F32Ext;
use panic_halt as _;
use arduino_hal::{delay_ms, i2c::I2c, prelude::*};

use crate::{f32_ufmt::ThreeFmt, mpu6050::Mpu6050, rolling_buffer::Collection};

const IMU_ADDR: u8 = 0x68;
const ALPHA: f32 = 0.85;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);
    let mut serial = arduino_hal::default_serial!(dp, pins, 115200);

    millis_init(dp.TC0);

    // Enable interrupts globally
    unsafe { avr_device::interrupt::enable() };

    let button = pins.d3.into_pull_up_input();
    let i2c = I2c::new(
        dp.TWI,
        pins.a4.into_pull_up_input(),
        pins.a5.into_pull_up_input(),
        400000,
    );

    let mut imu = match Mpu6050::new(i2c, IMU_ADDR) {
        Ok(imu) => imu,
        Err(err) => {
            ufmt::uwriteln!(&mut serial, "{:?}", err).unwrap_infallible();
            panic!();
        }
    };

    ufmt::uwriteln!(&mut serial, "Connected!").unwrap_infallible();

    let init = imu.update().unwrap();
    let mut roll = (-init.accel_x).atan2((init.accel_y*init.accel_y + init.accel_z*init.accel_z).sqrt()).to_degrees();
    let mut pitch = init.accel_y.atan2(init.accel_z).to_degrees();
    let mut yaw = 0.;

    let mut vx = 0.;
    let mut prev_time = millis();
    let mut ax_gain = 0.;
    let mut gz_gain = 0.;
    let mut rolling: Collection<10> = Collection::new();
    let mut rolling_yaw: Collection<5> = Collection::new();
    let mut last_button = 0;

    let mut prev = button.is_low();
    loop {
        if let Ok(data) = imu.update() {
            let new = button.is_low();
            let event = match (prev, new) {
                (false, false) => 0,
                (true, true) => 1,
                (false, true) => 2,
                (true, false) => {
                    prev_time = millis();
                    if prev_time - last_button < 500 {
                        ax_gain = data.accel_x;
                        rolling.clear();
                        gz_gain = data.gyro_z;
                        rolling_yaw.clear();
                        roll = (-init.accel_x).atan2((init.accel_y*init.accel_y + init.accel_z*init.accel_z).sqrt()).to_degrees();
                        pitch = init.accel_y.atan2(init.accel_z).to_degrees();
                        yaw = 0.;
                        vx = 0.;
                        continue;
                    }
                    last_button = prev_time;
                    3
                },
            };
            prev = new;

            let time = millis();
            let dt = (time - prev_time) as f32 / 1000.0;

            if rolling_yaw.full() && (rolling_yaw.avg() - data.gyro_z).abs() < 0.25 {
                rolling_yaw.clear();
                gz_gain = data.gyro_z;
            } else {
                rolling_yaw.push(data.gyro_z);
            }

            roll += data.gyro_x * dt;
            pitch += data.gyro_y * dt;
            yaw += (data.gyro_z - gz_gain) * dt;
            
            // complimentary filter on our orientation :)
            let new_pitch = data.accel_y.atan2(data.accel_z).to_degrees();
            let new_roll = (-data.accel_x).atan2((data.accel_y*data.accel_y + data.accel_z*data.accel_z).sqrt()).to_degrees();

            if roll.signum() != new_pitch.signum() {
                pitch = new_pitch;
            }

            roll = ALPHA * roll + (1. - ALPHA) * new_roll;
            pitch = ALPHA * pitch + (1. - ALPHA) * new_pitch;

            if rolling.full() && (rolling.avg() - data.accel_x).abs() < 0.25 {
                rolling.clear();
                ax_gain = data.accel_x;
            } else {
                rolling.push(data.accel_x);
            }

            vx += deadband(data.accel_x - ax_gain, 1.) * dt;
            vx *= 0.5;

            prev_time = time;

            let fmtx = ThreeFmt(roll);
            let fmty = ThreeFmt(fix_pitch(pitch));
            let fmtz = ThreeFmt(yaw);
            let gx = ThreeFmt(vx.abs());
            ufmt::uwriteln!(&mut serial, "{} {} {} {} {}", event, fmtx, fmty, fmtz, gx).unwrap_infallible();

            delay_ms(50);
        }
    }
}

#[inline]
fn deadband(input: f32, band: f32) -> f32 {
    if input.abs() < band { 0. } else { input }
}

#[inline]
fn fix_pitch(pitch: f32) -> f32 {
    if pitch < 0. {
        return -90. - pitch;
    } else {
        return 270. - pitch;
    }
}

// Possible Values:
//
// ╔═══════════╦══════════════╦═══════════════════╗
// ║ PRESCALER ║ TIMER_COUNTS ║ Overflow Interval ║
// ╠═══════════╬══════════════╬═══════════════════╣
// ║        64 ║          250 ║              1 ms ║
// ║       256 ║          125 ║              2 ms ║
// ║       256 ║          250 ║              4 ms ║
// ║      1024 ║          125 ║              8 ms ║
// ║      1024 ║          250 ║             16 ms ║
// ╚═══════════╩══════════════╩═══════════════════╝
const PRESCALER: u32 = 1024;
const TIMER_COUNTS: u32 = 125;

const MILLIS_INCREMENT: u32 = PRESCALER * TIMER_COUNTS / 16000;

static MILLIS_COUNTER: avr_device::interrupt::Mutex<cell::Cell<u32>> =
    avr_device::interrupt::Mutex::new(cell::Cell::new(0));

fn millis_init(tc0: arduino_hal::pac::TC0) {
    // Configure the timer for the above interval (in CTC mode)
    // and enable its interrupt.
    tc0.tccr0a.write(|w| w.wgm0().ctc());
    tc0.ocr0a.write(|w| w.bits(TIMER_COUNTS as u8));
    tc0.tccr0b.write(|w| match PRESCALER {
        8 => w.cs0().prescale_8(),
        64 => w.cs0().prescale_64(),
        256 => w.cs0().prescale_256(),
        1024 => w.cs0().prescale_1024(),
        _ => panic!(),
    });
    tc0.timsk0.write(|w| w.ocie0a().set_bit());

    // Reset the global millisecond counter
    avr_device::interrupt::free(|cs| {
        MILLIS_COUNTER.borrow(cs).set(0);
    });
}

#[avr_device::interrupt(atmega328p)]

[truncated — 312 more characters]
```

### python/PhysicsSimClasses.py

```python
from DartboardClasses import Dartboard, Dart
import pygame

class DartManager:
    DART_GRAVITY = -.98
    VELOCITY_SCALARS = (1,1,1)

    def __init__(self, dartboard, screen):
        self.screen = screen
        self.dartboard = dartboard

    def update_dart_positions(self, deltaTime):
        for dart in Dart.all_darts:
            if not dart.should_decay:
                dart.velocity[1] -= self.DART_GRAVITY
                dart.position[0] += dart.velocity[0] * self.VELOCITY_SCALARS[0] * deltaTime
                dart.position[1] += dart.velocity[1] * self.VELOCITY_SCALARS[1] * deltaTime
                dart.position[2] += dart.velocity[2] * self.VELOCITY_SCALARS[2] * deltaTime
                #print(dart)

    def throw_dart(self, exit_velocity):
        Dart(self.screen.get_width()//2, self.screen.get_height()//2, 1, exit_velocity, self.screen, self.dartboard)

    def throw_dart_using_mouse(self, x, y, z, exit_velocity):
        Dart(x, y, z, exit_velocity, self.screen, self.dartboard)

```

### python/SimulateDartboard.py

```python
import serial.tools.list_ports
import time, math
from DartboardSimulator import DartboardSimulator

useSerial = True

if useSerial:
    ports = serial.tools.list_ports.comports()
    serialInst = serial.Serial('COM5', 115200)
    time.sleep(0.5)
    print("Serial Connected!")

#event roll pitch yaw velocity

#0 angle angle angle speed
#0 - not pressed
#1 - if pressed
#2 - just pressed
#3 - just released

dartboardSimulator = DartboardSimulator(3)
max_parsed_speed = 0.35
desired_parsed_speed = 150

def throttle_curve(throttle):
    return desired_parsed_speed*math.tanh(3*throttle/max_parsed_speed)

while dartboardSimulator.loop(should_throw_debug=True, should_preview_debug=False):
    if useSerial:
        try:
            data = serialInst.readline().decode('utf-8').strip()
        except serial.SerialException:
            print("Serial monitor disconnected")
            continue
        
        try:
            parsedData = list(map(float, data.strip().split()))
            #print(parsedData)
        except ValueError:
            print(f'parsedData ({data}) is not a list of floats separated by spaces')
            continue
        
        if parsedData:
            parsedButtonState = parsedData[0]
            parsedRoll, parsedPitch, parsedYaw = parsedData[1], parsedData[2], parsedData[3] 
            parsedSpeed = parsedData[4] 
            
            #print(f'Angles before time of throw: Yaw: {parsedYaw}, Pitch: {parsedPitch}, Speed:{parsedSpeed}')

            dart_should_be_thrown = parsedButtonState == 3
            if dart_should_be_thrown:
                #parsedSpeed = throttle_curve(parsedSpeed)
                parsedSpeed=desired_parsed_speed
                #print(f"Adjusted parsedSpeed {parsedSpeed}")
                depth = 1*math.cos(math.radians(parsedYaw))*math.sin(math.radians(parsedPitch))
                left_and_right_movement = math.sin(math.radians(parsedYaw))*math.sin(math.radians(parsedPitch))*parsedSpeed
                up_and_down_movement = math.cos(math.radians(parsedPitch))*parsedSpeed
                dartboardSimulator.throw_dart([left_and_right_movement, up_and_down_movement, depth])

#serialInst.close()
print("This actually happened!")
```

### python/DartboardSimulator.py

```python
import pygame
from DartboardClasses import Dartboard, Dart
from PhysicsSimClasses import DartManager
import math

class DartboardSimulator:
    FPS = 60
    PREVIEW_RADIUS = Dart.MIN_RADIUS + Dart.RADIUS

    YAWS = iter(range(-45,45,15))
    PITCHES = iter(range(45,-45,-15))

    def __init__(self, dartboard_depth, screen_width=800, screen_height=800):
        pygame.init()
        #self.screen = pygame.display.set_mode((screen_width, screen_height), pygame.RESIZABLE)
        self.screen = pygame.display.set_mode((screen_width, screen_height))

        pygame.display.set_caption("PHYSICAL DIGITAL DARTS")

        self.clock = pygame.time.Clock()
        
        self.dartboard = Dartboard(self.screen, [0, 0, dartboard_depth]) # x and y are obselete
        self.dart_manager = DartManager(self.dartboard, self.screen)
        self.mouse_pos = (0,0)
        #font = pygame.font.SysFont('Arial', 40)
        #self.text_to_blit = font.render(f'x:0, y:0, z:0', True, (0, 0, 255))

        #print(self.dartboard)

    def loop(self, should_throw_debug=False, should_preview_debug=False):
        self.screen.fill((96, 59, 42))
        
        self.dartboard.draw_self()
        Dart.draw_all(self.screen)
        self.dartboard.check_for_collisions(Dart.all_darts)
        self.dart_manager.update_dart_positions(self.clock.tick(self.FPS)/1000)

        if should_throw_debug or should_preview_debug:
            self.mouse_pos = pygame.mouse.get_pos()

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                return False
            
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE:
                    speed = 150
                    yaw=0
                    pitch=45
                    #pitch = max(0,pitch)

                    left_and_right_movement = speed * math.sin(math.radians(yaw)) * math.sin(math.radians(pitch))
                    up_and_down_movement = speed * math.cos(math.radians(pitch))
                    depth = 1 * math.cos(math.radians(yaw)) * math.sin(math.radians(pitch))

                    print(yaw, pitch)
                    print(left_and_right_movement, up_and_down_movement, depth)
                    
                    self.throw_dart([left_and_right_movement, up_and_down_movement, depth])

                    # pitch = 45
                    # yaw = 45
                    # speed = 100
                    #With x as depth/default to yaw=0
                    #left_and_right_movement = 1 * math.cos(math.radians(yaw)) * math.sin(math.radians(pitch))

                else:
                    Dart.all_darts = []
            
            if should_throw_debug:
                self.debug_mouse_throw(event)

        if should_preview_debug:
            self.debug_mouse_preview()

        #if self.text_to_blit:
            #self.screen.blit(self.text_to_blit, (0, 0))

        pygame.display.update()
        return True
    
    def draw_dart_pos_preview(self, x, y):
        pygame.draw.circle(self.screen, Dart.COLOR, (x,y), self.PREVIEW_RADIUS)

    def debug_mouse_throw(self, event):
        if event.type == pygame.MOUSEBUTTONDOWN:
            self.dart_manager.throw_dart_using_mouse(self.mouse_pos[0], self.mouse_pos[1], 1, [0,100,2])

    def debug_mouse_preview(self):
        self.draw_dart_pos_preview(self.mouse_pos[0], self.mouse_pos[1])

    def throw_dart(self, exit_velocity):
        #font = pygame.font.SysFont('Arial', 40)
        #if self.magnitude_of_vector3(exit_velocity):
            #self.text_to_blit = font.render(f'l/r:{round(exit_velocity[0]/self.magnitude_of_vector3(exit_velocity),2)}, u/d:{round(exit_velocity[1]/self.magnitude_of_vector3(exit_velocity),2)}, depth:{round(exit_velocity[2]/self.magnitude_of_vector3(exit_velocity),2)}', True, (0, 0, 255))

        self.dart_manager.throw_dart([exit_velocity[0], exit_velocity[1], exit_velocity[2]])

    def magnitude_of_vector3(self, vector3):
        return math.sqrt(vector3[0]**2+vector3[1]**2+vector3[2]**2)


```

### python/DartboardClasses.py

```python
import math
import pygame

class Dart:
    RADIUS = 40
    MIN_RADIUS = 10
    COLOR = (100,100,255)
    LIFETIME = 100
    TOLERANCE = 10
    all_darts = []
    def __init__(self, x, y, z, exit_velocity, screen, dartboard):
        Dart.all_darts.append(self)
        self.dartboard = dartboard
        self.should_decay = False
        self.on_dartboard = False
        self.velocity = exit_velocity
        self.velocity[1] = -self.velocity[1]
        self.lifetime = Dart.LIFETIME
        self.screen = screen
        self.starting_distance_from_dartboard = z #CURRENTLY DOESN'T ACTUALLY REPRESENT DISTANCE FROM DARTBOARD ON LAUNCH
        self.position = [x, y, z]

    def draw_self(self):
        if self.has_reached_dartboard_depth() and self.on_dartboard:
            self.should_decay = True

        if self.should_decay and self.lifetime > 0:
            self.lifetime -= 1

        if (self.should_decay and self.lifetime <= 0) or self.position[1] > self.screen.get_height()*2:
            Dart.all_darts.remove(self)
        
        percent_of_distance_traveled = (self.position[2]-self.starting_distance_from_dartboard)/(self.dartboard.position[2]-self.starting_distance_from_dartboard)
        pygame.draw.circle(self.screen, self.COLOR, self.position[:2], self.MIN_RADIUS+self.RADIUS*(1-percent_of_distance_traveled))

    def has_reached_dartboard_depth(self):
        at_or_past_dartboard = self.position[2] >= self.dartboard.position[2] and self.position[2] <= self.dartboard.position[2] + self.TOLERANCE
        return at_or_past_dartboard
       
    def __repr__(self):
        return "Dart Coordinates: " + str(self.position)
    
    @staticmethod
    def draw_all(screen):
        for dart in Dart.all_darts:
            dart.draw_self()

class Polar_Rectangle:
    HIT_COLOR = (0,0,255)
    def __init__(self, radius1, radius2, theta1, theta2, color, dartboard, screen):
        self.screen = screen
        self.dartboard = dartboard
        self.non_hit_color = color
        self.color = self.non_hit_color
        self.xy_corners = []
        self.xy_corners.append((radius1*math.cos(theta1), radius1*math.sin(theta1)))
        self.xy_corners.append((radius1*math.cos(theta2), radius1*math.sin(theta2)))
        self.xy_corners.append((radius2*math.cos(theta2), radius2*math.sin(theta2)))
        self.xy_corners.append((radius2*math.cos(theta1), radius2*math.sin(theta1)))
        self.xy_corners = Dartboard.cartesian_coordinates_to_pygame_coordinates(self.xy_corners, screen)

    def draw_self(self):
        pygame.draw.polygon(self.screen, self.color, self.xy_corners, 0)

    def check_for_collision(self, darts):
        has_collided = False
        for dart in darts:
            has_collided = self.point_in_polygon(dart.position, self.xy_corners)
            if has_collided:
                if dart.position[2] < self.dartboard.position[2]:
                    self.when_projected_collide()
                if dart.has_reached_dartboard_depth():
                    self.when_collide(dart)
                return True #Collided only in x-y projection
        self.color = self.non_hit_color
        return False
    
    def when_collide(self, dart):
        dart.on_dartboard = True
        #print(f'Collided with: {dart}')

    def when_projected_collide(self):
        self.color = self.HIT_COLOR
    
    def point_in_polygon(self, point, polygon):
        num_vertices = len(polygon)
        x, y = point[0], point[1]
        inside = False

        # Store the first point in the polygon and initialize the second point
        p1 = polygon[0]

        # Loop through each edge in the polygon
        for i in range(1, num_vertices + 1):
            # Get the next point in the polygon
            p2 = polygon[i % num_vertices]

            # Check if the point is above the minimum y coordinate of the edge
            if y > min(p1[1], p2[1]):
                # Check if the point is below the maximum y coordinate of the edge
                if y <= max(p1[1], p2[1]):
                    # Check if the point is to the left of the maximum x coordinate of the edge
                    if x <= max(p1[0], p2[0]):
                        # Calculate the x-intersection of the line connecting the point to the edge
                        x_intersection = (y - p1[1]) * (p2[0] - p1[0]) / (p2[1] - p1[1]) + p1[0]

                        # Check if the point is on the same line as the edge or to the left of the x-intersection
                        if p1[0] == p2[0] or x <= x_intersection:
                            # Flip the inside flag
                            inside = not inside

            # Store the current point as the first point for the next iteration
            p1 = p2

        # Return the value of the inside flag
        return inside


    def __repr__(self):
        return "Polar Rectangle Corners: " + str(self.xy_corners)
    
class Polar_Circle:
    def __init__(self, list_of_polar_rectangles, dartboard, screen):
        self.screen = screen
        self.dartboard = dartboard
        self.list_of_polar_rectangles = list_of_polar_rectangles

    def draw_self(self):
        for polar_rectangle in self.list_of_polar_rectangles:
            polar_rectangle.draw_self()

    def check_for_collision(self, darts):
        any_projected_collisions = any([polar_rectangle.check_for_collision(darts) for polar_rectangle in self.list_of_polar_rectangles])
        if any_projected_collisions:
            for polar_rectangle in self.list_of_polar_rectangles:
                polar_rectangle.when_projected_collide()
                
    def __repr__(self):
        return "Polar Circle: " + str(self.list_of_polar_rectangles)

class Dartboard:
    GREEN = (0,255,0)
    RED = (255,0,0)
    BLACK = (0,0,0)
    WHITE = (255,255,255)
    THICK_RING_THICKNESS = 100
    THIN_RING_THICKNESS = 75
    BULLSEYE_THICKNESS = 25
    PER_REGION_DELTA_THETA = math.pi/10
    THETA_OFFSET = math.pi/20
    #DARTBOARD_SIZE - THIN_R
[truncated — 3192 more characters]
```

### accel/src/f32_ufmt.rs

```rust
use micromath::F32Ext;
use ufmt::uDisplay;

#[repr(transparent)]
pub struct ThreeFmt(pub f32);

impl uDisplay for ThreeFmt {
    fn fmt<W>(&self, fmt: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error>
        where
            W: ufmt::uWrite + ?Sized {
        let float = if self.0 < 0. {
            ufmt::uwrite!(fmt, "-")?;
            -self.0
        } else {
            self.0
        };

        let rounded = float.round();
        let shifted = ((float * 1000.) - rounded).round() as u64;
        let thousanth = shifted % 10;
        let hundreth = (shifted / 10) % 10;
        let tenth = (shifted / 100) % 10;
        
        ufmt::uwrite!(fmt, "{}.{}{}{}", rounded as u64, tenth, hundreth, thousanth)
    }
}

```

### accel/src/rolling_buffer.rs

```rust
use core::mem::MaybeUninit;

pub struct Collection<const LEN: usize> {
    arr: [MaybeUninit<f32>; LEN],
    len: usize,
    offset: usize,
}

impl<const LEN: usize> Collection<LEN> {
    pub fn new() -> Self {
        return Self {
            arr: [MaybeUninit::uninit(); LEN],
            len: 0,
            offset: 0
        }
    }

    pub fn push(&mut self, item: f32) {
        if self.len == LEN {
            self.arr[self.offset] = MaybeUninit::new(item);
            self.offset = (self.offset + 1) % LEN;
        } else {
            self.arr[self.len] = MaybeUninit::new(item);
            self.len += 1;
        }
    }

    pub fn avg(&self) -> f32 {
        let mut accum = 0.;

        for item in &self.arr[0..self.len] {
            accum += unsafe { item.assume_init_read() };
        }

        return accum / (self.len as f32);
    }

    pub fn clear(&mut self) {
        self.len = 0;
        self.offset = 0;
    }

    pub fn full(&self) -> bool {
        return self.len == LEN;
    }
}

```

### accel/src/mpu6050.rs

```rust
use arduino_hal::i2c::I2c;
use arduino_hal::{delay_ms, hal};
use embedded_hal::i2c::I2c as _;
use ufmt::derive::uDebug;

#[derive(Debug, uDebug)]
pub enum Error {
    I2c(hal::i2c::Error),
    /// Invalid byte read from the whoami register
    Identification,
    NoData,
}

impl From<hal::i2c::Error> for Error {
    fn from(value: hal::i2c::Error) -> Self {
        Self::I2c(value)
    }
}

const WHOAMI: u8 = 0x68;
const WHOAMI_REG: u8 = 0x75;
const PWR_MGMT_1: u8 = 0x6B;
const MPU_CONFIG: u8 = 0x1A;
const SMPLRT_DIV: u8 = 0x19;
const GYRO_CONFIG: u8 = 0x1B;
const INT_PIN_CFG: u8 = 0x37;
const INT_ENABLE: u8 = 0x38;
const INT_STATUS: u8 = 0x3A;
const ACCEL_XOUT_H: u8 = 0x3B;

const A_RES: f32 = 16.0 / 32768.0;
const G_RES: f32 = 2000.0 / 32768.0;

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MpuData {
    pub accel_y: f32,
    pub accel_z: f32,
    pub accel_x: f32,
    pub gyro_x: f32,
    pub gyro_y: f32,
    pub gyro_z: f32,
    pub temp: f32,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CalData {
    pub accel_bias: [f32; 3],
    pub gyro_bias: [f32; 3],
}

impl Default for CalData {
    fn default() -> Self {
        CalData { accel_bias: [0.; 3], gyro_bias: [0.; 3] }
    }
}

pub struct Mpu6050{
    i2c: I2c,
    address: u8,
    calibration: CalData,
}

impl Mpu6050 {
    pub fn new(i2c: I2c, address: u8) -> Result<Mpu6050, Error> {
        let mut new = Mpu6050 { i2c, address, calibration: CalData::default() };
        new.init()?;

        return Ok(new)
    }

    pub fn init(&mut self) -> Result<(), Error> {
        let whoami = read_byte(&mut self.i2c, self.address, WHOAMI_REG)?;
        if whoami != WHOAMI {
            return Err(Error::Identification)
        }

        // reset device
        write_byte(&mut self.i2c, self.address, PWR_MGMT_1, 0x80)?;
        delay_ms(100);
        // wake up device
        write_byte(&mut self.i2c, self.address, PWR_MGMT_1, 0x00)?;
        delay_ms(100);

        // get stable time source
        write_byte(&mut self.i2c, self.address, PWR_MGMT_1, 0x03)?;
        delay_ms(200);

        // configure gyro and thermometer
        write_byte(&mut self.i2c, self.address, MPU_CONFIG, 0x03)?;
        // set sample rate
        write_byte(&mut self.i2c, self.address, SMPLRT_DIV, 0x03)?;

        let mut c= read_byte(&mut self.i2c, self.address,  GYRO_CONFIG)?;
        c = c & !0x03; // Clear Fchoice bits [1:0]
        c = c & !0x18; // Clear GFS bits [4:3]
        c = c | 3 << 3; // Set 2000dps full scale range for the gyro (11 on 4:3)
        write_byte(&mut self.i2c, self.address,  GYRO_CONFIG,c)?;

        write_byte(&mut self.i2c, self.address, MPU_CONFIG,  0x03)?;

        write_byte(&mut self.i2c, self.address,  INT_PIN_CFG,0x22)?;
        write_byte(&mut self.i2c, self.address, INT_ENABLE,0x01)?;

        delay_ms(100);
        return Ok(());
    }

    pub fn update(&mut self) -> Result<MpuData, Error> {
        let data_available = read_byte(&mut self.i2c, self.address, INT_STATUS)? & 0x01 > 0;
        if !data_available {
            return Err(Error::NoData);
        }

        let mut imu_count: [i16; 7] = [0; 7];
        let mut raw_data: [u8; 14] = [0; 14]; 

        read_bytes(&mut self.i2c, self.address, ACCEL_XOUT_H, &mut raw_data)?;

        imu_count[0] = ((raw_data[0] as i16) << 8) | (raw_data[1] as i16);
        imu_count[1] = ((raw_data[2] as i16) << 8) | (raw_data[3] as i16);
        imu_count[2] = ((raw_data[4] as i16) << 8) | (raw_data[5] as i16);
        imu_count[3] = ((raw_data[6] as i16) << 8) | (raw_data[7] as i16);
        imu_count[4] = ((raw_data[8] as i16) << 8) | (raw_data[9] as i16);
        imu_count[5] = ((raw_data[10] as i16) << 8) | (raw_data[11] as i16);
        imu_count[6] = ((raw_data[12] as i16) << 8) | (raw_data[13] as i16);

        let ax = (imu_count[0] as f32) * A_RES - self.calibration.accel_bias[0];
        let ay = (imu_count[1] as f32) * A_RES - self.calibration.accel_bias[1];
        let az = (imu_count[2] as f32) * A_RES - self.calibration.accel_bias[2];

        let temp = ((imu_count[3] as f32) / 340.) + 36.53;
        
        let gx = (imu_count[4] as f32) * G_RES - self.calibration.gyro_bias[0];
        let gy = (imu_count[5] as f32) * G_RES - self.calibration.gyro_bias[1];
        let gz = (imu_count[6] as f32) * G_RES - self.calibration.gyro_bias[2];

        return Ok(MpuData {
            accel_x: ax,
            accel_y: ay,
            accel_z: az,
            gyro_x: gx,
            gyro_y: gy,
            gyro_z: gz,
            temp,
        })
    }
}

fn read_byte(i2c: &mut I2c, address: u8, register: u8) -> Result<u8, Error> {
    let mut buf = [0];
    i2c.write_read(address, &[register], &mut buf)?;
    return Ok(buf[0]);
}

fn write_byte(i2c: &mut I2c, address: u8, register: u8, data: u8) -> Result<(), Error> {
    i2c.write(address,&[register, data])?;
    return Ok(())
}

fn read_bytes(i2c: &mut I2c, address: u8, register: u8, data: &mut [u8]) -> Result<(), Error> {
    i2c.write_read(address, &[register], data)?;
    return Ok(())
}

```