Skip to content

Commit

Permalink
Break out common i2c interface code (#9)
Browse files Browse the repository at this point in the history
* Move blocking i2c interface code to a separate module

* Update classic_sync to use common i2c interface

* Remove dead code and cargo fmt

* Start moving common nunchuk data into core::nunchuk

* Update nunchuck to use crate::interface

* Update tests for nunchuck

* Do calibration on init for nunchuk

* Update nunchuk mocks with calibration reads

* Move nunchuk tests to their own file
  • Loading branch information
9names authored Apr 14, 2024
1 parent 976316a commit 2e754b0
Show file tree
Hide file tree
Showing 7 changed files with 624 additions and 336 deletions.
105 changes: 14 additions & 91 deletions wii-ext/src/classic_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,8 @@
// See `decode_classic_report` and `decode_classic_hd_report` for data format

use crate::core::classic::*;
use crate::ControllerIdReport;
use crate::interface::Interface;
use crate::ControllerType;
use crate::ExtHdReport;
use crate::ExtReport;
use crate::EXT_I2C_ADDR;
use crate::INTERMESSAGE_DELAY_MICROSEC_U32 as INTERMESSAGE_DELAY_MICROSEC;
use embedded_hal::i2c::I2c;

#[cfg(feature = "defmt_print")]
Expand All @@ -30,24 +26,14 @@ pub enum ClassicError<E> {
ParseError,
}

#[cfg_attr(feature = "defmt_print", derive(defmt::Format))]
/// Errors in this crate
#[derive(Debug)]
pub enum Error<E> {
/// I²C bus communication error
I2C(E),
/// Invalid input data provided
InvalidInputData,
}
use crate::interface::Error;

pub struct Classic<I2C, DELAY> {
i2cdev: I2C,
interface: Interface<I2C, DELAY>,
hires: bool,
calibration: CalibrationData,
delay: DELAY,
}

// use crate::nunchuk;
impl<T, E, DELAY> Classic<T, DELAY>
where
T: I2c<SevenBitAddress, Error = E>,
Expand All @@ -59,11 +45,11 @@ where
/// send the required init sequence in order to read data in
/// the future.
pub fn new(i2cdev: T, delay: DELAY) -> Result<Classic<T, DELAY>, Error<E>> {
let interface = Interface::new(i2cdev, delay);
let mut classic = Classic {
i2cdev,
interface,
hires: false,
calibration: CalibrationData::default(),
delay,
};
classic.init()?;
Ok(classic)
Expand All @@ -87,45 +73,6 @@ where
Ok(())
}

/// Set the cursor position for the next i2c read
///
/// This hardware has a range of 100 registers and automatically
/// increments the register read postion on each read operation, and also on
/// every write operation.
/// This should be called before a read operation to ensure you get the correct data
fn set_read_register_address(&mut self, byte0: u8) -> Result<(), Error<E>> {
self.i2cdev
.write(EXT_I2C_ADDR as u8, &[byte0])
.map_err(Error::I2C)
.and(Ok(()))
}

/// Set a single register at target address
fn set_register(&mut self, addr: u8, byte1: u8) -> Result<(), Error<E>> {
self.i2cdev
.write(EXT_I2C_ADDR as u8, &[addr, byte1])
.map_err(Error::I2C)
.and(Ok(()))
}

/// Read the button/axis data from the classic controller
fn read_report(&mut self) -> Result<ExtReport, Error<E>> {
let mut buffer: ExtReport = ExtReport::default();
self.i2cdev
.read(EXT_I2C_ADDR as u8, &mut buffer)
.map_err(Error::I2C)
.and(Ok(buffer))
}

/// Read a high-resolution version of the button/axis data from the classic controller
fn read_hd_report(&mut self) -> Result<ExtHdReport, Error<E>> {
let mut buffer: ExtHdReport = ExtHdReport::default();
self.i2cdev
.read(EXT_I2C_ADDR as u8, &mut buffer)
.map_err(Error::I2C)
.and(Ok(buffer))
}

/// Send the init sequence to the Wii extension controller
///
/// This could be a bit faster with DelayNs, but since you only init once we'll re-use delay_ms
Expand All @@ -136,15 +83,9 @@ where

// Reset to base register first - this should recover a controller in a weird state.
// Use longer delays here than normal reads - the system seems more unreliable performing these commands
self.delay.delay_us(INTERMESSAGE_DELAY_MICROSEC * 2);
self.set_read_register_address(0)?;
self.delay.delay_us(INTERMESSAGE_DELAY_MICROSEC * 2);
self.set_register(0xF0, 0x55)?;
self.delay.delay_us(INTERMESSAGE_DELAY_MICROSEC * 2);
self.set_register(0xFB, 0x00)?;
self.delay.delay_us(INTERMESSAGE_DELAY_MICROSEC * 2);
self.interface.init()?;
self.update_calibration()?;
self.delay.delay_us(INTERMESSAGE_DELAY_MICROSEC * 2);
// TODO: do we need more delay here?
Ok(())
}

Expand All @@ -154,9 +95,7 @@ where
/// analogue axis as a u8, rather than packing smaller integers in a structure.
/// If your controllers supports this mode, you should use it. It is much better.
pub fn enable_hires(&mut self) -> Result<(), Error<E>> {
self.delay.delay_us(INTERMESSAGE_DELAY_MICROSEC * 2);
self.set_register(0xFE, 0x03)?;
self.delay.delay_us(INTERMESSAGE_DELAY_MICROSEC * 2);
self.interface.enable_hires()?;
self.hires = true;
self.update_calibration()?;
Ok(())
Expand All @@ -172,52 +111,36 @@ where
/// TODO: work out why, make it public when it works
#[allow(dead_code)]
fn disable_hires(&mut self) -> Result<(), Error<E>> {
self.delay.delay_us(INTERMESSAGE_DELAY_MICROSEC * 2);
self.set_register(0xFE, 0x01)?;
self.delay.delay_us(INTERMESSAGE_DELAY_MICROSEC * 2);
self.interface.disable_hires()?;
self.hires = false;
self.update_calibration()?;
Ok(())
}

fn read_id(&mut self) -> Result<ControllerIdReport, Error<E>> {
self.set_read_register_address(0xfa)?;
let i2c_id = self.read_report()?;
Ok(i2c_id)
}

pub fn identify_controller(&mut self) -> Result<Option<ControllerType>, Error<E>> {
let i2c_id = self.read_id()?;
Ok(crate::common::identify_controller(i2c_id))
}

/// tell the extension controller to prepare a sample by setting the read cursor to 0
fn start_sample(&mut self) -> Result<(), Error<E>> {
self.set_read_register_address(0x00)?;
Ok(())
self.interface.identify_controller()
}

/// poll the controller for the latest data
fn read_classic_report(&mut self) -> Result<ClassicReading, Error<E>> {
if self.hires {
let buf = self.read_hd_report()?;
let buf = self.interface.read_hd_report()?;
ClassicReading::from_data(&buf).ok_or(Error::InvalidInputData)
} else {
let buf = self.read_report()?;
let buf = self.interface.read_report()?;
ClassicReading::from_data(&buf).ok_or(Error::InvalidInputData)
}
}

/// Simple read helper helper with no delay. Works for testing, not on real hardware
pub fn read_classic_no_wait(&mut self) -> Result<ClassicReading, Error<E>> {
self.start_sample()?;
self.interface.start_sample()?;
self.read_classic_report()
}

/// Simple blocking read helper that will start a sample, wait 10ms, then read the value
pub fn read_report_blocking(&mut self) -> Result<ClassicReading, Error<E>> {
self.start_sample()?;
self.delay.delay_us(INTERMESSAGE_DELAY_MICROSEC);
self.interface.start_sample_and_wait()?;
self.read_classic_report()
}

Expand Down
1 change: 1 addition & 0 deletions wii-ext/src/core.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod classic;
pub mod nunchuk;
81 changes: 81 additions & 0 deletions wii-ext/src/core/nunchuk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#[cfg(feature = "defmt_print")]
use defmt;

#[cfg_attr(feature = "defmt_print", derive(defmt::Format))]
#[derive(Debug)]
pub struct NunchukReading {
pub joystick_x: u8,
pub joystick_y: u8,
pub accel_x: u16, // 10-bit
pub accel_y: u16, // 10-bit
pub accel_z: u16, // 10-bit
pub button_c: bool,
pub button_z: bool,
}

impl NunchukReading {
pub fn from_data(data: &[u8]) -> Option<NunchukReading> {
if data.len() < 6 {
None
} else {
Some(NunchukReading {
joystick_x: data[0],
joystick_y: data[1],
accel_x: (u16::from(data[2]) << 2) | ((u16::from(data[5]) >> 6) & 0b11),
accel_y: (u16::from(data[3]) << 2) | ((u16::from(data[5]) >> 4) & 0b11),
accel_z: (u16::from(data[4]) << 2) | ((u16::from(data[5]) >> 2) & 0b11),
button_c: (data[5] & 0b10) == 0,
button_z: (data[5] & 0b01) == 0,
})
}
}
}

/// Relaxed/Center positions for each axis
///
/// These are used to calculate the relative deflection of each access from their center point
#[derive(Default)]
pub struct CalibrationData {
pub joystick_x: u8,
pub joystick_y: u8,
}

/// Data from a Nunchuk after calibration data has been applied
///
/// Calibration is done by subtracting the resting values from the current
/// values, which means that going lower on the axis will go negative.
/// Due to this, we now store analog values as signed integers
///
/// We'll only calibrate the joystick axes, leave accelerometer readings as-is
#[cfg_attr(feature = "defmt_print", derive(defmt::Format))]
#[derive(Debug, Default)]
pub struct NunchukReadingCalibrated {
pub joystick_x: i8,
pub joystick_y: i8,
pub accel_x: u16, // 10-bit
pub accel_y: u16, // 10-bit
pub accel_z: u16, // 10-bit
pub button_c: bool,
pub button_z: bool,
}

impl NunchukReadingCalibrated {
pub fn new(r: NunchukReading, c: &CalibrationData) -> NunchukReadingCalibrated {
/// Just in case `data` minus `calibration data` is out of range, perform all operations
/// on i16 and clamp to i8 limits before returning
fn ext_u8_sub(a: u8, b: u8) -> i8 {
let res = (a as i16) - (b as i16);
res.clamp(i8::MIN as i16, i8::MAX as i16) as i8
}

NunchukReadingCalibrated {
joystick_x: ext_u8_sub(r.joystick_x, c.joystick_x),
joystick_y: ext_u8_sub(r.joystick_y, c.joystick_y),
accel_x: r.accel_x,
accel_y: r.accel_y, // 10-bit
accel_z: r.accel_z, // 10-bit
button_c: r.button_c,
button_z: r.button_z,
}
}
}
Loading

0 comments on commit 2e754b0

Please sign in to comment.