This is an old revision of the document!


Darts Game

Introduction

A smart darts game in which the throw of the dart is not needed. This is meant for people that want the fun of playing darts, without the risk of poking holes in the walls. I came up with this idea while looking for a darts game and realizing that I can just build it myself. The basic concept is that the “dart” you are holding while playing is plotting your throw on the board, by just analyzing the fake throw acceleration and angle. Once the throw is confirmed, the corresponding LED on the board lights up.

General Description

The project is split in 2 parts: The dart and The board:

The Dart

I am using an ESP32 for this since is has built-in wi-fi for wireless communication with my PC and Free_RTOS for light sleep functionality. The acceleration and gyroscopic angles are measured by a MPU6050, that has a motion detection interrupt available. Using the interrupt, I can just alert the board when a throw is in process. This way, my ESP can stand by in light sleep most of the time, preserving energy. When a throw is detected, the accelerometer reads data until it reaches maximum velocity, then the board sends the throw data to my PC via bluetooth. At the same time, a distance measurement protocol activates: the dart “syncs” with the board, using an infra red LED and a ultrasound distance measurement unit. The distance from the board is necessary for the trajectory calculation.

The Board

Since I only have one ESP board, I will be using my laptop to receive the data from the dart and transmit it along to my Arduino controlling the board, using UART. If needed, the PC can also process the data before sending it. For the visual feedback of the throw, I will be having 82 LEDs attached to the dart board. One for each segment. When the dart would hit a segment on the board, the corresponding LED lights up. I am multiplexing them by connecting a pair of antiparallel LEDs between each of the pins used. That way, with only 10 pins, I can control up to 90 LEDs, of which I will only be using 82

Hardware Design

Component Count PIN Role
ESP32 1 N/A Dart MCU: Wi-Fi data transmission to PC, FreeRTOS light sleep standby
MPU6050 1 E-GPIO21(SDA)/E-GPIO22(SCL) Measures dart acceleration & gyroscopic angles; triggers motion interrupt to wake ESP32
IR LED 1 A-GPIO13 Dart-side sync signal for distance measurement protocol
Ultrasound Sensor 2 E-GPIO16(Trig)/A-GPIO12(Echo) Measures dart-to-board distance for trajectory calculation
Arduino 1 N/A Board MCU: receives processed data from PC via UART, controls board mechanisms
LEDs 82 A-GPIO2→A-GPIO11 Board lighting: illuminate scored segments
LED Screen 1 A-SDA/A-SCL Displays score, game state, or throw feedback

Software Design

The firmware for both the ESP32 and the Arduino is developed using PlatformIO. The ESP32 runs on top of FreeRTOS, spending most of its time in light sleep until the MPU6050 motion interrupt signals a throw. The Arduino side uses bare-metal avr-libc, without the Arduino library, for direct register-level control of the charlieplexed LED matrix.

Libraries and Frameworks

The Dart (ESP32 / ESP-IDF + FreeRTOS)
  • ESP-IDF I2C Master Driver (driver/i2c_master.h) — communicates with the MPU6050 over I2C (GPIO21 SDA, GPIO22 SCL, 100 kHz)
  • ESP-IDF GPIO Driver (driver/gpio.h) — configures GPIO34 as an interrupt input for the MPU6050 motion detect signal
  • ESP-IDF Sleep API (esp_sleep.h) — manages light sleep between throws to conserve power
  • FreeRTOS Semaphore — an ISR-safe binary semaphore synchronizes the motion interrupt with the main game loop task
  • NimBLE (esp-nimble-cpp v2.5.0) — BLE GATT server that exposes a single Notify characteristic for streaming throw data to the PC
The Handler (Python / PC)
  • Bleak — asynchronous BLE client library, connects to the Dart controller and subscribes to throw notifications
The Board (ATmega328P / Bare-metal AVR)
  • avr-libc — direct port/pin register manipulation for charlieplexing
  • Custom charlieplex_dartboard driver — maps all 82 segments to rail pairs stored in PROGMEM, minimizing RAM usage on the 2 KB ATmega328P

Firmware

The Dart

MPU6050 Initialization: The sensor is configured for ±8g accelerometer range, ±500°/s gyroscope range, with a digital low-pass filter at 94 Hz. The motion detection interrupt is set to trigger at 0.50g with a 1 ms duration threshold:

void init_mpu() {
    i2c_write(0x6B, 0x00); // Wake up the MPU-6050
    i2c_write(0x1A, 0x02); // Set DLPF to 94Hz
 
    i2c_write(0x1B, 0x08); // Set gyro full scale to ±500°/s
    i2c_write(0x1C, 0x18); // Set accelerometer full scale to ±8g
 
    i2c_write(0x1F, 0x04); // Set motion threshold (~0.51g)
    i2c_write(0x20, 0x01); // Set motion duration to 1ms
 
    i2c_write(0x37, 0x20); // Latch mode until interrupt cleared
    i2c_write(0x38, 0x40); // Enable Motion Interrupt
}

Interrupt-driven throw detection: When the MPU6050 detects motion above the threshold, it asserts its INT pin. An ISR on GPIO34 gives a FreeRTOS binary semaphore, waking the main game task without polling:

static SemaphoreHandle_t throwSemaphore;
 
static void IRAM_ATTR mpu_isr_handler(void* arg) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(throwSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

Main game loop: The task blocks indefinitely on the semaphore. When a throw is detected, it reads 6 accelerometer and gyroscope values, packs them into a 24-byte float array, and sends it to the PC via BLE Notify:

while(1) {
    xSemaphoreTake(throwSemaphore, portMAX_DELAY);
 
    i2c_read(0x3A, buff); // Clear interrupt flag
    read_mpu_acc(&accX, &accY, &accZ);
    read_mpu_gyro(&gyroX, &gyroY, &gyroZ);
 
    float payload[6] = {accX, accY, accZ, gyroX, gyroY, gyroZ};
    pCharacteristic->setValue((uint8_t*)payload, sizeof(payload));
    pCharacteristic->notify();
}
The Handler

The Python handler bridges the dart and the board. A background thread runs the Bleak BLE event loop, and each notification triggers a hitscan calculation.

Hitscan and scoring: The accelerometer Y and Z values are scaled by a sensitivity constant to produce impact coordinates in mm. The scoring function uses polar coordinates to determine the ring (bullseye, single, triple, double, miss) and sector (standard 20-sector layout):

HITSCAN_SENSITIVITY = 27.5
 
def calculate_hitscan(acc_y, acc_z):
    impact_x = acc_y * HITSCAN_SENSITIVITY
    impact_y = acc_z * HITSCAN_SENSITIVITY
    return impact_x, impact_y
 
def get_dartboard_score(x_mm, y_mm):
    distance = math.sqrt(x_mm**2 + y_mm**2)
    if distance <= 6.35:  return "Inner Bullseye (50)"
    if distance <= 15.9:  return "Outer Bullseye (25)"
    if distance > 170:    return "Miss (0)"
 
    angle = math.degrees(math.atan2(y_mm, x_mm))
    if angle < 0: angle += 360
    sector_angle = (angle + 9) % 360
    sector_index = int(sector_angle / 18)
 
    sectors = [6, 10, 15, 2, 17, 3, 19, 7, 16, 8,
               11, 14, 9, 12, 5, 20, 1, 18, 4, 13]
    base = sectors[sector_index]
 
    if 99 <= distance <= 107:  return f"Triple {base} ({base * 3})"
    if 162 <= distance <= 170: return f"Double {base} ({base * 2})"
    return f"Single {base} ({base})"
The Board

The board firmware uses a charlieplexing driver to control 82 LEDs with only 10 GPIO pins (Arduino D3–D12, mapped to AVR ports PD3–PD7 and PB0–PB4).

Segment-to-rail mapping: A PROGMEM lookup table stores the (HIGH rail, LOW rail) pair for each of the 82 dartboard segments. The mapping was derived from the Eagle schematic, where each diode's anode and cathode pins were cross-referenced with the physical board layout:

static const struct {
    uint8_t hi;     /* rail_t for anode (HIGH) */
    uint8_t lo;     /* rail_t for cathode (LOW) */
} seg_map[NUM_SEGMENTS] PROGMEM = {
    [SEG_BEYE  ] = { RAIL_R6 , RAIL_R8  },  /* bulseye */
    [SEG_CTR   ] = { RAIL_R8 , RAIL_R6  },  /* bullseye ring */
    [SEG_DBL1  ] = { RAIL_R3 , RAIL_R1  },  /* double points foe segment 1 */
    [SEG_DBL2  ] = { RAIL_R2 , RAIL_R10 },  /* double points foe segment 2 */
    /* ... */
};

LED control (led_on): To light a specific LED, the function reads the two rail descriptors from flash, sets exactly those two pins as OUTPUT (one HIGH, one LOW), and leaves all other pins in high-impedance. The write sequence is ordered to prevent glitches:

void led_on(segment_t seg) {
    if ((uint8_t)seg >= (uint8_t)NUM_SEGMENTS) {
        leds_off();
        return;
    }
 
    uint8_t hi_rail = pgm_read_byte(&seg_map[seg].hi);
    uint8_t lo_rail = pgm_read_byte(&seg_map[seg].lo);
 
    rail_pin_t h = read_rail(hi_rail);
    rail_pin_t l = read_rail(lo_rail);
 
    uint8_t ddr_d = 0, ddr_b = 0;
    uint8_t port_d = 0, port_b = 0;
 
    /* Anode (HIGH) */
    if (h.port_id == PORT_ID_D) { ddr_d |= h.mask; port_d |= h.mask; }
    else                        { ddr_b |= h.mask; port_b |= h.mask; }
 
    /* Cathode (LOW): OUTPUT but PORT bit stays 0 */
    if (l.port_id == PORT_ID_D) { ddr_d |= l.mask; }
    else                        { ddr_b |= l.mask; }
 
    /* Glitch-free sequence: tri-state all → set PORT → enable OUTPUT */
    DDRD  &= (uint8_t)~RAILS_MASK_D;
    DDRB  &= (uint8_t)~RAILS_MASK_B;
    PORTD = (uint8_t)((PORTD & ~RAILS_MASK_D) | port_d);
    PORTB = (uint8_t)((PORTB & ~RAILS_MASK_B) | port_b);
    DDRD |= ddr_d;
    DDRB |= ddr_b;
}

When integrated with the Handler, the Arduino will receive a segment ID over UART (from the PC) and call led_on() with the corresponding segment to illuminate the hit zone.

Results

In this moment, only the dart is finished. It works as intended, as you can see in the clip provided on moodle. The dartboard n screen is a proof of concept written in python. It simulates the real life throw from a set distance and height. When the project will be in its final state, the virtual dartboard will be switched with the real one and the script will only handle data computation and transmition.

As for the Dartboard, it's still in the making. All of the LEDs have been soldered, and the connections are almost done. The result will be a matrix of multiplexed LEDs that light up when needed. The LED screen and distance sensors well be added after this step.

Code

The whole project code, including the Dart, the Handler and the Board can be found Here.

Journal

Bibliography/Resources

Datasheets for ESP32, ATmega328, MPU6050 PM labs

pm/prj2026/jan.vaduva/dragos.ciobanu1706.1779657834.txt.gz · Last modified: 2026/05/25 00:23 by dragos.ciobanu1706
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0