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
Nice, thanks. I got my servo working last night with a Wiichuk , which is my first baby steps.
ReplyDeleteSuppose it's a bit late to tell you about :
http://todbot.com/blog/2008/02/18/wiichuck-wii-nunchuck-adapter-available/
:)
rasputnik: Yes, I've seen that, but I didn't order it as I'd have to wait another week.
ReplyDeleteAnd in my case I think it wouldn't work anyway, because my setup doesn't work without external pull-up resistors, and wiichuck doesn't have them.
Well, thanks for the code anyway. I noticed a lot of jitter with my setup too, and the hysteresis idea is close to what I was thinking of doing.
ReplyDeleteis there a way to get the apple script to work on an XP machine i am look for ways to but cant figure it out yet.
ReplyDeleteGregjkm: I have no idea. If you had Linux it would be trivial to replace iTunes+applescript with AmaroK+dcop. About XP I really don't have the faintest clue.
ReplyDeleteI just checked, and osascript is the script engine for "AppleScripts and other OSA language scripts" according to its man page.
ReplyDeleteWhat Gregjkm and others are looking for will depend on the intermediary language; in this case, Ruby, though I prefer Python. There should be a module available to connect to the Windows object subsystem, and through that to iTunes, but it is likely to be supplied by a third party rather than being part of the standard library. Or you could use a Microsoft language like C# or Visual Basic, provided you know how to connect to "external" objects through them.
Beyond that, search for a tutorial on controlling programs in windows.
Just wanted to note that the nunchuck is a 3.3 volt device, not 5V like the Arduino... Looks like you can run the nunchuck on 5 volts but I don't know if this affects its life time.
ReplyDeleteOmegas: I know it's 3.3V, but it works perfectly fine on 5V, other people seem to have the same experience here.
ReplyDeleteVery late to the show here, but hopefully this will be helpful for you going forward:
ReplyDeleteI believe that the reason you need the external pull-ups is because you're signaling by switching the pin between input and output; between high impedance (which, when combined with the pull-up resistor, causes the line to go high) and low impedance where you then write low to the pin which causes the line to go low.
However, writing low to an output pin on the Arduino also disables the internal pull up resistors, and so when you switch back to high impedance the line ends up floating. You might be able to fix this by writing high to the pin immediately after switching back to input mode (thus re-enabling the internal pull-up), but then you also have to ensure that you write low to the pin immediately prior to switching to output mode, or you'll be writing high to the line through the pin which is, as you noted, a Bad Plan.
In other words, it doesn't matter which mode (input/output) the pin is in when you write to it; when you switch to the other mode, it will be like you had written the same thing. (In input mode, write high, pull-up resistor becomes enabled, switch to output, pin will be at high, write low, pin goes to low, switch to input, pull-up resistor is disabled).
So it's probably just as well that you're using external pull-ups.
tcepsa: What you're saying makes sense, but these are I2C pins used with official Wire library for I2C - so such things are supposed to be handled.
ReplyDeleteIn any case external pull-ups worked and I moved on to tinkering with other gadgets ;-)