Joe's Lab-book

These pages are my online lab-book. Interesting things, things that I might need again, and other junk ends up here.

RSS Add a new post titled:

I watched too many of Felix Immler's youtube videos, and bought his book, and he made me buy another swiss army knife. I got a huntsman, which is awesome, but too big, particularly compared to the wenger I had in my pocket from about age eight to eighteen.

OK, simple, use that old wenger? But it's a bit precious now, and you can't get them anymore. OK, but victorinox make some versions of the old wenger models, why not use one of those? Well, they're really expensive, and they don't have a small blade. OK, victorinox make some smaller swiss army knives, with small and large blades, what about one of those? Yeah, maybe, but there isn't one with both blades, a saw, and an awl. OK, well you'll just have to make your own ****ing swiss army knife then. YES.

The 84mm walker has a saw, a large blade, and a combo-tool. The 84mm tourist has a small and large blade, the two screw drivers/openers, the cork screw, and the awl. So, if we take that lot apart we can make something that has the small and large blade, the two screw drivers/openers, the cork screw, the awl, and the saw.

It's pretty easy, pop the scales off, drill out the rivets, re-build the layers you want and rivet it back together again with some 2.2mm brass rod. Pop the scales back on and tada:

If that was made from larger 91mm swiss army knife bits, this would be called a 'camper', so this must be a small camper. And it turns out that that used to be a real model. If you could get scissors in 84mm then we could make a huntsman, but you can't.

You also have enough left over bits to make another knife (and you've spent about as much as getting one of the wenger like models that were too expensive).

Posted Tags:

Plotting routes in the APRS network

By accident, I left a radio listing to the APRS frequency overnight with direwolf decoding and logging packets to a file. Just for fun I thought I'd try and plot some of the routes packets took to my low dipole in IO92XD.

In the plot below, I'm drawing stations with locations measured off-air with green borders, the ones with red borders I had to add manually. The lines between stations represent routes packets took, and their weight is proportional to the number of packets traversing that route.

Looks like the longest route might have been from G4GVZ via MB7USW, MB7UG, and MB7UM. The most distant station is probably MB7UB in Bath.

The aprs.log file looks a bit like this:

[0 2020-04-05 19:19] G4MRS-2>APNU19,MB7UM*,WIDE1-1:!5203.38N/00116.81E#UIDIGI1.9B3: Martlesham RS - 2
[0 2020-04-05 19:19] G4MRS-2>APNU19,MB7UH,MB7UPI*:!5203.38N/00116.81E#UIDIGI1.9B3: Martlesham RS - 2
[0 2020-04-05 19:19] 2E0PBY>UQQVS0,G3OJZ,MB7UH,MB7UM*:'w0%l <0x1c>R/]<0x0d>
[0 2020-04-05 19:19] 2E0PBY>UQQVS0,G3OJZ,MB7UH,MB7UPI*:'w0%l <0x1c>R/]<0x0d>
[0 2020-04-05 19:21] MB7VH>APMI04,MB7UM*,WIDE2-1:@050621z5202.08N/00031.09W#WX3in1Mini U=13.6V. Central Bedfordshire<0x20>
[0 2020-04-05 19:21] MB7VH>APMI04,MB7UPI*,WIDE2-1:@050621z5202.08N/00031.09W#WX3in1Mini U=13.6V. Central Bedfordshire<0x20>
[0 2020-04-05 19:21] G3OJZ>APU25N,MB7UH,WIDE1,MB7UM*,WIDE2-1:@051821z5106.07N/00112.91E_169/008g010t059r000p000P000h59b10127DAVIS VP2 & WDISPLAY<0x0d>
[0 2020-04-05 19:21] G3OJZ>APU25N,MB7UH,WIDE1,MB7UPI*,WIDE2-1:@051821z5106.07N/00112.91E_169/008g010t059r000p000P000h59b10127DAVIS VP2 & WDISPLAY<0x0d>
[0 2020-04-05 19:23] MB7UH>APMI03,MB7UM*,WIDE2-1:!5154.53N/00053.93E#
[0 2020-04-05 19:23] MB7UH>APMI03,MB7UPI*,WIDE2-1:!5154.53N/00053.93E#

You can see the route as the first few fields. So far, the script only parses the simpler packet formats (about 1500 of 3000 total), so there's more information to be had.

vim: set ft=markdown wrap spell :

Posted Tags:

A simple set of toy traffic lights using an at-tiny85 at not much else, execpt the LEDs obviously.

There is a button to turn the thing on, and make the lights change before the timer says so. Software has two parts: a simple state machine managing light states and going to sleep and an ISR that generates software PWM on three pins for the LEDs.

There's no off switch, after a while the tiny85 will go to sleep and a press on the button will wake it up again. I use a cheap li-ion pouch cell and USB charger from amazon, one charge lasts for months asleep.

There really isn't much other than the attiny, the LEDs and the switch:

Browse the source or build using arduino-makefile (included), say:

$ git clone
$ cd trafficlights
$ make
$ make ispload

after updating the makefile to point to an arduino running the arduino ISP sketch, and connecting that to the avrisp connector on the traffic lights.

Posted Tags:

I build the 40dB tap from the same QST article as my powermeter.

To make it useful, I need to measure the coupling and insertion loss, so here goes. S1 and S2 are the through ports, S3 is the tap. It's pretty flat over 7-433MHz, with a through loss of <1dB and a coupling of 33-34dB.

Test setup is the FT817 in FM mode and lowest power as the signal source, through a 20dB pad to the source port of the coupler, the powermeter connected to the measured port, and my better 50R termination for the remaining port.

So, now I can connect a dummy load to S2, my new ubitx to S1, and the power meter via a 20dB pad to S3, shout oooollllllla into the mic and read -16dBm off the power meter.

Thats -16dBm after 20dB of attenuation and a coupling of -34dB, so I'm ooolllllaaa-ing at -16+20+34=38dBm, or six-point-something watts. Maybe a bit low, bit it'll do for now.

F [MHz] S1S2 [dBm] S1S3 [dBm] S2S3 [dBm] SRC [dBm] loss S1S2 [dB] loss S2S3 [dB]
0 7.15 4 -30 -30 4 0 34
1 10.00 4 -30 -29 4 0 33
2 29.00 5 -29 -28 5 0 33
3 50.50 5 -28 -28 5 0 33
4 145.50 4 -29 -29 5 1 34
5 433.00 3 -30 -31 3 0 34
Posted Tags:

I've made a few headset adaptors recently, seemed worth sticking a note here so I can work out what they do later.

This one adapts a cheap four pin phone headset to the FT817 (and probably similiar rigs), and adds a toggle switch for PTT. Used to good effect in the 2m backpackers contest last month, after a wrap with insulating tape:

And this one adapts a kenwood-style 2.5 + 3.5mm headset to two 3.5mm plugs to use with my ubitx:

Both liberally potted with hot glue...

Posted Tags:

I've got a minicircuits ZDC-20-3 20dB directional coupler that I picked up cheap at a rally. I'm interested to see how it matches up to the spec.

| | |

The datasheet claims 0.3dB insertion loss, -19.3dB coupling (forward), and 50dB directivity at 1MHz. Right now, I'm interested in results at 5MHz, but that's the nearest point from the spec.

I've no hope of measuring insertion loss to 0.3dB, so lets look at directivity and coupling instead. Tools available are an FT817, and my W7ZOI powermeter.

To get a low power signal source I've set the rig to low power, and I'm using the tune tone from wsjt-x and adjusting the audio drive to control power.

To measure coupling:

  1. Terminate the cpl port in 50R
  2. Connect the rig to the in port
  3. Connect the power meter to the out port
  4. Transmit and adjust the transmit power (and audio drive level) to show 0dBm forward power on the meter
  5. Holding the transmit power the same, terminate out in 50R and connect the power meter to cpl and record the reading, that's coupling.

To measure directivity:

  1. Terminate the out port in 50R
  2. Connect the rig to the in port
  3. Connect the power meter to the cpl port
  4. Transmit and adjust the transmit power (and audio drive level) to show 0dBm forward power on the meter
  5. Reverse the in and out ports and measure power, that's directivity.
Frequency [MHz] Coupling [dB] Directivity [dB]
1.80 -18 -44
5.36 -18 -40
10.11 -17 -38
50.00 -17 -25
145.00 -18 -14
435.00 -22 -8

The directivity doesn't match the datasheet, even at low frequencies, look at what I'm using for the 50R termination (below left) no surprise that it doesn't perform well at anything about a meg or two.

Not a very good termination Rather better.

Repeating the measurement with a better 50R terminator shows much improved directivity (shock horror!).

Directivity [dB]
Frequency [MHz] Poor termination [dB] Good termination [dB]
1.80 -44 -54
5.36 -40 -52
10.11 -38 -50
50.00 -25 -40
145.00 -14 -33
435.00 -8 -24
Posted Tags:

Ages ago, so long ago that the battery says use by 2016, I built the W7ZOI/W7PUA power meter from QST. See picture below - meter scale generated by meters.

I dug out the meter for an experiment recently, and needed the calibration data. I've lost the plots I made at the time, but do have the raw measurements from my logbook. Plots and measurements reproduced below for next time.

Frequency responsePower Response

Supporting data:


I've not done any contesting for a while, and this is to remind me that it can be quite good fun, even (particularly?) with very little kit. Operating from a secret location on the only hill in IO92xd for the last hour of the september 144MHz backpackers contest I worked 9 stations, limited by time not by equipment.

Operating position and antenna on the only hill in IO92xd...

Running 2.5W to a 3-ele cheapYagi made from junk found in the garage at about 3m AGL best DX was F6HPP/P at 397km.

Should do that again sometime...



The uv5r doesn't have the best reputation for spectral purity, let's see how this one does...

We're measuring conducted emissions, so lets cable a spectrum analyzer into the antenna socket. There's a 10dB pad too, just to protect the analyzer.

Putting a marker on the carrier, and delta markers on the harmonics it's easy to see their relative levels:

145MHz, high power145MHz, lower power
435MHz, high power435MHz, lower power

Summarizing, and adding 11dB to account for the 10dB attenuator, and cable loss, which I didn't measure:

frequency_Hz power p1_dBm d2_dBc d3_dBc d4_dBc d5_dBc
0 145000000.0 high 36.22 -52.19 -48.61 -71.77 -67.32
1 145000000.0 low 29.40 -58.08 -44.57 -65.02 -65.16
2 435000000.0 high 34.18 -60.66 -44.76 -62.71 -68.49
3 435000000.0 low 28.82 -47.54 -42.43 -59.03 -66.66

None of the harmonics are above the FCC limit (-40dBc) which is a good start (ofcom's regulations on off-frequency emissions are not so helpfully precise).

Looking at the absolute powers of the harmonics, the worst is at -10.6dBm, and all of the third harmonics are greater than the FCC limit of -16dBm. Measurement uncertainty here is pretty large, but not enough to cover a fail by several dB.

frequency_Hz power p1_dBm p2_dBm p3_dBm p4_dBm p5_dBm
0 145000000.0 high 36.22 -15.97 -12.39 -35.55 -31.10
1 145000000.0 low 29.40 -28.68 -15.17 -35.62 -35.76
2 435000000.0 high 34.18 -26.48 -10.58 -28.53 -34.31
3 435000000.0 low 28.82 -18.72 -13.61 -30.21 -37.84

I've been automating my greenhouse...

Arduino source

Pump and filter
Electronics on breadboard
First installation

Say you want to adjust the antenna on your wifi access point for maximum signal strength at your computer, but you can't see the computer from the antenna - have the computer speak the link quality while you adjust the antenna!

while 1; do flite -t $(iwconfig ath0 | sed -e '/Link Quality/!d' -e 's/.*Quality=\([^\/]*\)\/.*/\1/'); done

Twitter's recent change to oauth broke my favorite twitter client twyt. It seems that it also broke twitters android client so they've left basic auth available to any client identifying with the parameter source=twitterandroid - after the applying the patch below twyt will identify as the twitter android client and will continue to work with basic authenticatoin.

=== modified file 'twyt/'
--- twyt/ 2009-11-19 21:48:45 +0000
+++ twyt/ 2010-09-14 08:53:28 +0000
@@ -97,6 +97,8 @@
            url = self.baseurl + handler

+       data.append(('source', 'twitterandroid'))
        req = None
        if method == 'GET':
            url = url + '?' + urllib.urlencode(data)
Posted Tags:

Sometimes it's useful to use an instutional webcache from home - getting at things on IEEEXplore for example - but many webcaches don't allow access from the internet. SSH to the rescue.

Assuming you have access to a machine inside the institution, host.inst, and that the webcache is called webcache.inst on port 8080 then:

ssh host@inst -L 8000:webcache.inst:8080 -T

Will forward port 8000 on your local machine to port 8080 on the webcache, set up your browser to expect a wep proxy on localhost:8000.

Posted Tags:

Sometimes to annotate a diagram (made with GraphViz) it would be good to add extra edges. For example, given the final analysis example from KF86 (figure 1) it would be helpful to show the path taken by the algorithm through this tree (see for the dot file).

Figure 1: Final Analysis Data Structure

A naive addition of the algorithm path to the dot file is shown below:

n1 -> n2 -> n3 -> n5 -> n4 -> n7 -> n11 [color=red];

When creating the layout dot treats all edges identically, giving the layout shown in figure 2 (see

Figure 2: Layout change after additional path

Setting the edge attribute constrained to false prevents the consideration of that edge in setting node ranks, so the code:

n1 -> n2 -> n3 -> n5 -> n4 -> n7 -> n11 [color=red, constrained=false];

Gives the graph shown in figure 3, and in

Figure 3: Original layout, after addition of `constrained` parameter
Posted Tags:

Following up from Wrapping LibC Functions a couple of months ago, here's how you can build those functions into a dynamically loaded library.

The code to log calloc() and free() looks something like:

#include <stdlib.h>

void *calloc (size_t count, size_t size)
     void *ptr = (void *) __libc_calloc(count, size);
     DEBUG("calloc(%d, %d) = %p\n", count, size, ptr);
     return ptr;

void free (void * ptr)

If the file's called test_alloc.c build the library with:

$ gcc -shared -ldl -o test_alloc.c

Then run a program with these intercept functions:

$ ./program
Posted Tags:

Say you have a list of country names and you want to highlight those countries on a map, shaded by the number of repitions of that country in the list. You might want to go from a list like this:

locations = ['Slovenia',
         'Russian Federation',
         'United Kingdom',
         'United Kingdom']

To an image like this, from google charts:

The google charts API requires ISO 3166 country codes, pycountry can translate country names to ISO 3166 codes:

codes = [pycountry.countries.get(name=l).alpha2 for l in locations]

Counting the repitions in codes is easy:

code_count = defaultdict(int)
for code in codes:
    code_count[code] += 1

The last thing to do is save the parameters and make the charts URL:

base = ''
params = {
    'chco': 'FFFFFF,CCFFCC,88FF88,00FF00',
    'chf': 'bg,s,EAF7FE',
    'chs': '440x220',
    'cht': 't',
    'chtm': 'world'}

params['chd'] = 't:'+
                for k in sorted(counts.keys()))

params['chld'] = "".join(sorted(counts.keys()))

URL = base + "&".join(
            "=".join([k, params[k]) for k in params.keys())

After all that URL looks like:,20,20,100,40,100,60,20,20,20,20,20,20,100,20,60,60&chf=bg,s,EAF7FE&chco=FFFFFF,CCFFCC,88FF88,00FF00&chtm=world&chld=BEBYCZDEFRGBITMDNOPEPLPTRSRUSESIUA&chs=440x220&cht=t

Which is the link to the map shown above.

Posted Tags:

Running two monitors of different sizes as one combined desktop leaves a dead zone - an area of desktop not shown on either monitor. Annoyingly sawfish will someties place a window in this dead zone. To prevent it you can fill the dead zone an x message window, using a command like:

xmessage -geometry 480x480+800+1024 hi &!

With some BIOS versions the eee can drive a 1920x1200 monitor, but it seems it can't do decent power management at the same time! The table below lists those bios versions I've tested.

BIOS Version1920x1200 SupportWorking ACPI
8804 Yes No
0703 No Yes
0910 No Yes
1001 No Yes
1302 No Yes
Posted Tags:

Gmail will offer to automatically unsubscribe from a mailing list1, so why can't mutt?

Gmail parses the List-Unsubscribe header looking for a mailto URL, so it should be quite possible to knock up something similar for mutt.

A typical List-Unsubscribe looks like:


So a bit of formail and sed action could easily parse out the link.

Posted Tags:

It's often useful to define element-wise operators for tuples, particularly when they are used to represent coordinate pairs. In python a simple operator factory function can be used to create these tuple operators from their scalar equivalents.

>>> import operator

>>> def tuple_operator_factory (operator):
...     def toperator (*args):
...         return tuple(map(lambda x: operator(*x), zip(*args)))
...     return toperator

>>> tsub = tuple_operator_factory(operator.sub)
>>> tsub((2,3), (1,1))
>>> tmul = tuple_operator_factory(operator.mul)
>>> tmul((2,2), (3,4))
Posted Tags:


ldiff <filename>


Ldiff highlights changes between version of a latex document. The original file is taken from an automatically discovered git repository and the new file from the path specified by filename.


ldiff requires python version 2.5, and the git-python module. The changes ldiff makes to the specified latex file assumes that the following commands are present in the document's preamble:

\definecolor{insertcolor}{rgb}{0.81, 1, 0.81}
\definecolor{deletecolor}{rgb}{1, 0.53, 0.53}

Of course the definitions of insertcolor and deletecolor can be amended to taste.


ldiff attempts to locate the relevant git repository by searching the all directories in the absolute path of filename for a directory named .git.


Example Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Ut purus elit, vestibu-lum ut, placerat ac, adipiscing vitae, felis. Curabitur dictum gravida mauris.

Nam arcu libero, nonummy eget, consectetuer id, vulputate a, magna. Donecvehicula augue eu neque. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Mauris ut leo. Cras viverra metusrhoncus sem. Nulla et lectus vestibulum urna fringilla ultrices. Phasellus eu tellus sit amet tortor gravida placerat. Integer sapien est, iaculis in, pretiumquis, viverra ac, nunc. Praesent eget sem vel leo ultrices bibendum. Aenean faucibus. Morbi dolor nulla, malesuada eu, pulvinar at, mollis ac, nulla. Cur-abitur auctor semper nulla. Donec varius orci eget risus. Duis nibh mi, congue eu, accumsan eleifend, sagittis quis, diam. Duis eget orci sit amet orci dignissimrutrum.This sentence has been replaced. ------------

Nam dui ligula, fringilla a, euismod sodales, sollicitudin vel, wisi. Morbiauctor lorem non justo. Nam lacus libero, pretium at, lobortis vitae, ultricies et, tellus. Donec aliquet, tortor sed accumsan bibendum, erat ligula aliquet magna,vitae ornare odio metus a mi. Morbi ac orci et nisl hendrerit mollis. Suspendisse ut massa. Cras nec ante. Pellentesque a nulla. Cum sociis natoque penatibus etmagnis dis parturient montes, nascetur ridiculus mus. Aliquam tincidunt urna. Nulla ullamcorper vestibulum turpis. Pellentesque cursus luctus mauris.Nulla malesuada porttitor diam. Donec felis erat, congue non, volutpat at, tincidunt tristique, libero. Vivamus viverra fermentum felis. Donec nonummypellentesque ante. Phasellus adipiscing semper elit. Proin fermentum massa ac quam. Sed diam turpis, molestie vitae, placerat a, molestie nec, leo. Mae-cenas lacinia. Nam ipsum ligula, eleifend at, accumsan nec, suscipit a, ipsum. Morbi blandit ligula feugiat magna. Fusce mauris. Vestibulum luctus nibh at lectus.

This line had been added.


ldiff can be downloaded from my git repository.

Posted Tags:

If you want to instrument functions from libc then there's no need to mess around with dlsyms, try prepending __libc_ to the function you're interested in:

void *calloc (size_t count, size_t size)
     void *ptr = __libc_calloc(count, size);
     DEBUG("calloc(%d, %d) = %p\n", count, size, ptr);
     return ptr;
Posted Tags:

Toggle Pointer Head - Warp the pointer between xinerama heads

Try adding the following code to your sawfish config, and bind the toggle-pointer-head function to a key.

(defun toggle-pointer-head ()
  (defun cons+ (a b)
      (+ (car a) (car b))
      (+ (cdr a) (cdr b))))

  (defun head-centre (head)
     (cons-centre (head-dimensions head))
     (head-offset head)))

  (defun cons-centre (a)
    (cons (/ (car a) 2) (/ (cdr a) 2)))

  (defun warp-cursor-cons (c)
    (warp-cursor (car c) (cdr c)))

  (defun warp-pointer-to-head (head)
    (warp-cursor-cons (head-centre head)))

    ((>= (1+ (pointer-head)) (head-count))
     (warp-pointer-to-head 0))
    (t (warp-pointer-to-head (1+ (pointer-head))))))

(bind-keys global-keymap  "M-c" '(toggle-pointer-head))
Posted Tags:

If you have several projects in one git tree, perhaps something like:


And you want several individual git repos:


Then to take all sub-folders from ~/git/src and create new git repos in ~/git:

set -e
cd git_tmp
git clone $srcrepo ./orig
mkdir new
cd new
for dir in ../orig/*;
    dir=$(basename $dir)
    git clone ../orig $dir
    pushd $dir
    git filter-branch --subdirectory-filter $dir HEAD -- --all
    git reset --hard
    git gc --aggressive
    git prune
    git clone --bare $dir ~/git/$dir
rm -rf ~/git_tmp/*
Posted Tags:

To convert a set of subversion repos in ~/svn to git repos in ~/git

set -e
git config svn.authorsfile << EOF
joe = Joe Milbourn <>
mkdir ~/git_tmp
cd ~/git_tmp
for x in ~/svn/*;
    repo=$(basename $x)
    mkdir $repo
    pushd $repo
    git svn init file://$HOME/svn/$repo --no-metadata
    git svn fetch
    git clone --bare $repo ~/git/$repo
rm -rf git_tmp
Posted Tags:

Downloading previous tracks:

gpsbabel -i garmin -f usb: -o gpx -F file.gpx

Uploading routes

gpsbabel -r -i gpx -f file.gpx -o garmin -F usb:

Creating routes:

  1. Draw route on Google Maps.
  2. Use GMapToGPX to create a route.
  3. Edit created route file to combine all <rte></rte> blocks.

Use GPSVisualizer to plot downloaded routes.

Posted Tags: