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 tell
we can control its basic functionality, what will be good enough for us.We 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
end
Reading 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
end
All 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