Here's my latest project. It involves way too many technologies for the sole purpose of controlling iTunes with Wii Nunchuck. Signal flow is almost uni-directional, so let's go from step by step from the Nunchuck to iTunes.
Wii Nunchuck and I2C
I2C is a very simple protocol back from the 80s. There are four wires. Vcc (traditionally +5V, but lower voltages are used with I2C too) and ground are obvious. Clock and data lines require some more explanation.I2C is a bus protocol - so multiple devices can operate on the same wires. Even in the simplest configuration we have a master (Arduino board), and a slave (Wii Nunchuck), both of which can read and write to the same lines. And it doesn't take much science to know that when one device writes 0 while other writes 1 to the same line hilarity ensues, also known as a circuit shorted, everyone dies.
The way I2C and many other protocols solve this problem is by using "open drain" design. In open drain no device is allowed to write 1 to the bus - you can only write 0 (connect to the ground), or leave the connection in high impedance state. The bus is connected to Vcc using a pull-up resistor. This way if any device writes 0, bus will be at 0. If none is writing, it will be at 1. That's somewhat unintuitive at first, if it confuses you just check the Internet for explanation.
Fortunately Arduino has hardware support for I2C over its analog input pins 4 and 5, and library for that (called
Wire) is included in the distribution. So it would seem that we don't have anything to do... except it all doesn't work.Now I'm not 100% sure about that, but here's my guess what happens. Arduino analog input pins can be set to output low, output high, high impedance, and high impedance with pull-up (allegedly 20k). For I2C we're interested in output low, and high impedance with pull-up. However - this built-in pull-up seems too weak, or it's not working for some other reason. Directly connecting clock and data lines to Vcc via 10k pull-up resistors (lower resistance = more pull-up, at least until you fry it) makes it all work. Nice way to spend an entire night, isn't it?
So after cutting the Wii Nunchuck wire, the connections are:
- Nunchuck white (GND) to Arduino GND
- Nunchuck red (VCC) to Arduino 5V
- Nunchuck yellow (clock) to Arduino analog input pin 5
- Nunchuck green (data) to Arduino analog input pin 4
- Clock line to VCC via 10k resistor
- Data line to VCC via 10k resistor
Speaking with Wii Nunchuck
Now that we have electric signals handled, we need to engage Wii Nunchuck in a meaningful conversation. It has device ID 0x52 (82), and we need to write 0x40 0x00 (64 0) to initialize it, and then request data in packets of six bytes, and write 0x00 (0) to confirm we got it.Now I try to avoid word like "retarded" on this blog, but Wii Nunchuck data is "encrypted", and we need to "decrypt" it first:
- decrypted = (0x17 XOR encrypted) + 0x17
After that it's just a straightforward decoding, and we dump data onto serial interface. Here's full code:
#define CPU_FREQ 16000000L
#define TWI_FREQ 100000L
#include <Wire.h>
int diode = HIGH;
void blink() {
  digitalWrite(13, diode);
  diode = (diode == HIGH) ? LOW : HIGH;
}
void setup() {
  Serial.begin(19200);
  pinMode(13, OUTPUT);
  Wire.begin();
  Wire.beginTransmission(0x52);
  Wire.send(0x40);
  Wire.send(0x00);
  Wire.endTransmission();
}
void send_zero() {
  Wire.beginTransmission(0x52);
  Wire.send(0x00);
  Wire.endTransmission();
}
void loop() {
  int cnt = 0;
  uint8_t outbuf[6];
  Wire.requestFrom(0x52, 6);
  while(Wire.available()) {
    if(cnt < 6) {
      outbuf[cnt] = (0x17 ^ Wire.receive()) + 0x17;
    }
    cnt++;
  }
  int b = ~outbuf[5] & 3;
  int joy_x = outbuf[0]-125;
  int joy_y = outbuf[1]-128;
  int accel_x = (outbuf[2] << 2 | ((outbuf[5] >> 2) & 0x03)) - 500;
  int accel_y = (outbuf[3] << 2 | ((outbuf[5] >> 4) & 0x03)) - 488;
  int accel_z = (outbuf[4] << 2 | ((outbuf[5] >> 6) & 0x03)) - 504;
  Serial.print("B=");
  Serial.print(b);
  Serial.print(" XY=");
  Serial.print(joy_x);
  Serial.print(",");
  Serial.print(joy_y);
  Serial.print(" XYZ=");
  Serial.print(accel_x);
  Serial.print(",");
  Serial.print(accel_y);
  Serial.print(",");
  Serial.print(accel_z);
  Serial.print("\n");
  
  send_zero();
  blink();
  delay(50);
}Controlling iTunes
iTunes is an outrageously bad music player, but at least it has one redeeming quality of being controllable from command line via AppleScript.
By issuing silly commands like
tell application iTunes
  next track
end tellWe also need to get some information from iTunes. It doesn't have "increase/decrease volume" commands, so we need to find current volume, and then set it to higher/lower values. It also doesn't have a single play/pause command, so we need to figure out what state we're in - if we're playing then pause, otherwise play. Some of the nasty code refactored to
OSA class to keep ITunes class a bit cleaner, but not by much.The code doesn't depend on the rest of the project, so you can use it in your own projects, or just look what it's doing and use it directly from command line.
class OSA
  def get(var)
    `osascript -e 'tell application "#{name}" to #{var}'`
  end
  def do!(cmd)
    system 'osascript', '-e', %Q[tell application "#{name}"], '-e', cmd, '-e', 'end tell'
  end
end
class ITunes < OSA
  def name
    'iTunes'
  end
  def get_volume
    get('sound volume as integer').to_i
  end
  def get_state
    get('player state as string').chomp
  end
  def set_volume(v)
    system 'osascript', '-e', %Q[tell application "iTunes" to set sound volume to #{v}]
  end
  def next!
    do! 'next track'
  end
  def prev!
    do! 'previous track'
  end
  def pause!
    do! 'pause'
  end
  def play!
    do! 'play'
  end
  def pause_flip!
    if get_state == 'playing'
      pause!
    else
      play!
    end
  end
  def vol_up!
    set_volume(get_volume + 5)
  end
  def vol_down!
    set_volume(get_volume - 5)
  end
endReading data from Arduino
Communication is strictly uni-directional. Arduino writes to virtual serial interface, and we're reading from it as the data arrives. The data is current position of buttons, analog stick, and accelerometer. We're not really interested in any of it directly - we care about button presses and releases, and about stick going up/down/left/right (analog stick is used only as fake D-pad here). So change of state, not state as such.
For buttons it's straight forward. We get a bit for each button, so we just check it. For the stick, there's a small trickery involved. The range on each axis is from about -100 to +100, so we could just set points like +64/-64 from which we count it as a up/down. But what if stick is hold around that level? Sensor is analog (and user's hand is shaking), so if user hold the stick around +64, the reading would go +64, +63, +65, +63, +64, +64, +65 ..., what such naive implementation would treat as plenty of up presses, even though there weren't any.
This is an extremely common problem in electronics, and there's a simple solution - hysteresis. We simply take two slightly different levels for two sides of the same transition. If stick goes above +64 it's up. If it goes below +48 it's neutral. If it's betweer +48 and +64, we just keep it where it was. So unless the user has Parkinsons's and wiggles it a lot, it won't register any spurious clicks.
Other than that the code is pretty straightforward. We throw away first five readings because electronics tend to take some time after power-up to stabilize. And with every reading we set state to what we read, put all transitions into
@cur_events instance variable, and yield.require 'rubygems'
require 'serialport'
class SerialDevice
  def get_device
    while true
      dev = Dir["/dev/cu.usb*"][0]
      return dev if dev
      sleep 1
    end
  end
  def initialize
    @sp = SerialPort.new(get_device, 19200)
  end
end
class Nunchuck < SerialDevice
  attr_reader :cur_events, :b, :sx, :sy, :sxs, :sys
  attr_accessor :ax, :ay, :az
  def initialize
    super
    @b,@sx,@sy,@ax,@ay,@az,@sxs,@sxs = 0,0,0,0,0,0,0,0
  end
  def each_state
    skip = 5
    while line = @sp.readline
      unless line =~ /B=(\d) XY=(\S+),(\S+) XYZ=(\S+),(\S+),(\S+)/
        puts line
        next
      end
      if skip > 0
        skip -= 1
        next
      end
      @cur_events = []
      self.b, self.sx, self.sy, self.ax, self.ay, self.az = [$1,$2,$3,$4,$5,$6].map{|x| x.to_i}
      yield
    end
  end
  def b=(nv)
    @cur_events << :press_lo   if nv & ~@b & 1 != 0
    @cur_events << :release_lo if @b & ~nv & 1 != 0
    @cur_events << :press_hi   if nv & ~@b & 2 != 0
    @cur_events << :release_hi if @b & ~nv & 2 != 0
    @b = nv
  end
  def hysteresis(new_measurement, old_state, *ranges)
    while true
      state, bounds = ranges.shift, ranges.shift
      if bounds.nil? or
        (new_measurement < bounds.first) or
        (new_measurement < bounds.last and old_state <= state)
        return state
      end
    end
  end
  def sxs=(nv)
    @cur_events << :sxs_change if nv != @sxs
    @sxs = nv
  end
  def sys=(nv)
    @cur_events << :sys_change if nv != @sys
    @sys = nv
  end
  def sx=(nv)
    self.sxs = hysteresis(nv, sxs, -1, -64..-48, 0, 48..64, 1)
    @sx = nv
  end
  def sy=(nv)
    self.sys = hysteresis(nv, sys, -1, -64..-48, 0, 48..64, 1)
    @sy = nv
  end
  def raw
    [@b,@sx,@sy,@ax,@ay,@az]
  end
endAll of it together
The final bit of code is trivial. We just instantiate
ITunes and Nunchuck objects, and connect them in obvious way:- Press C to play/pause
- Analog stick right/left for next/previous
- Analog stick up/down to increase/decrease volume
osascript commands on every reading (if stick is above +64 increase volume, if under -64 decrease volume), it would be too slow, at least in this naive implementation, and you'd only see there would be a pretty big lag between moving the stick and iTunes reacting.i = ITunes.new
n = Nunchuck.new
n.each_state do
  if n.cur_events.include?(:sxs_change)
    if n.sxs == 1
      i.next!
    elsif n.sxs == -1
      i.prev!
    end
  end
  if n.cur_events.include?(:sys_change)
    if n.sys == 1
      i.vol_up!
    elsif n.sys == -1
      i.vol_down!
    end
  end
  if n.cur_events.include?(:press_hi)
    i.pause_flip!
  end
  p(n.raw + n.cur_events)
end 






 
 
 
 
 
 
 
 
 
 

