Friday, April 14, 2017

Powering an ESP-01

Just another reminder to myself about how to convert 5V, like provided by Arduino, USB, and any number of wall warts, to 3.3V as needed by the ESP-01 board, using a resistive divider.

It's a little hard to see what's going on. Here is a schematic:

This only shows 2 resistors. In the photo, I'm using 2 1kΩ resistors in series to make 2kΩ resistance. The top yellow wire is Vin, R1 is the left most resistor, which is 1kΩ, R2 is the combination of the right 2 resistors, also both 1kΩ. The formula for the math is

Thursday, March 23, 2017

How to add a Photosphere image in Google Blogger

I searched around for quite a while and never did find a suitable answer about how to add a photosphere image to a page in Blogger. I took quite a few of these photos on a recent vacation and wanted to add them to one of my other blogs. Most of the articles I could find were way out of date and required an API key, Google Maps, Street View, and so on. I figured out this method, which is fairly straightforward but tedious if you have a lot of such images. This assumes your photosphere photos are already uploaded to Google photos.

  1. Open Google photos in web browser
  2. Find the photosphere image, it will have a little circular arrow in the top right corner
  3. Right click on the circle arrow and select copy link address
  4. In blogger, add the image from step 2 to your page.
  5. Switch to html editing
  6. Find the anchor tag for the image
  7. Replace the url (href) with the link copied from step 3
  8. Edit the caption to say something like 'click to see photosphere'
  9. Save the page

Now loading the page then clicking on the image will open that image in a photosphere viewer on

Here's an example using a picture I took of the Hickman natural bridge in Capitol Reef National Park:

Click for Photosphere

Sunday, September 25, 2016

Install CHDK 1.4.1 on Canon SX20 with Arch Linux

Thought I'd write this up also since I had to do it again today.

The CHDK instructions are very complete in some places, vague in others. The suggested tools to use to help install are very dated and don't necessarily work, so these are my notes to get CHDK onto a bootable SD card. I wanted to upgrade by CHDK installation to 1.4.x so I can run the Ultimate Intervalometer, which allows the camera to take time lapse photos. Yes, there are others that work with the current 1.1 version of CHDK that I had, but not as nice as this one.

My camera, the Canon SX20, does not support the manual loading of CHDK through the firmware update process since that is not an option in the Canon menus. The only option for this camera is to make a bootable SD card by hand. I have a 16GB card.

Firmware version is 1.02D, full CHDK package is at

Small CHDK package is at

Here are the steps:

Back up all photos and scripts (if any) from the card.

Pull the card from the camera and make sure the lock is off.

Insert the card into the card reader on my laptop.

Make 2 partitions, one FAT16 to boot, one FAT32 for photo storage.

sudo fdisk /dev/mmcblk0

d to create a dos partition table

n to create a new partition
p to create a primary partition
1 to make this partition number 1 (1 should be default choice)
use default for first sector
+10M to make the partition 10MB in size

repeat to make a second partition:
n to create a new partition
p to create a primary partition
2 to make this partition number 2 (2 should be default choice)
use default for first sector
use default for last sector, this uses all remaining space

t to set partition type on partition 1
1 for partition 1
6 for FAT16 (use L to see all partition types)

t to set partition type on partition 2
2 for partition 2
b for FAT32

a to set bootable flag on partition 1
1 for partition 1

p to view partition table, should look like this:

Disk /dev/mmcblk0: 15.4 GiB, 16574840832 bytes, 32372736 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x652563a2

Device         Boot Start      End  Sectors  Size Id Type
/dev/mmcblk0p1 *     2048    22527    20480   10M  6 FAT16
/dev/mmcblk0p2      22528 32372735 32350208 15.4G  b W95 FAT32

w to write the changes and exit fdisk.

Format the partitions:

sudo mkdosfs /dev/mmcblk0p1
sudo mkdosfs -F 32 /dev/mmcblk0p2

Make the first partition bootable (setting the flag in fdisk isn't sufficient):

echo -n BOOTDISK | sudo dd bs=1 count=8 seek=64 of=/dev/mmcblk0p1

Copy the CHDK files and the Ultimate Intervalometer script to both partitions. I'm sure it's not necessary to have the script in both places, but I'm not sure which one the camera loads, plus the file is small, only about 40kb, so no big deal to have it in both places:

sudo mount -t vfat /dev/mmcblk0p1 /mnt/sd
sudo cp -r /home/danson/docs/camera/CHDK_1.4.1/* /mnt/sd
sudo cp /home/danson/downloads/ultimate.lua /mnt/sd/CHDK/SCRIPTS/
sudo umount /mnt/sd
sudo mount -t vfat /dev/mmcblk0p2 /mnt/sd
sudo cp -r /home/danson/docs/camera/CHDK_1.4.1/* /mnt/sd
sudo cp /home/danson/downloads/ultimate.lua /mnt/sd/CHDK/SCRIPTS/
sudo umount /mnt/sd

After a little more research, it appears I could have used the small CHDK package in the FAT16 partition and the complete one in the FAT32 partition, but again, they aren't that big and the FAT16 partition has enough space, so I installed the complete package in both.

Remove the SD card from the laptop, turn the lock on the card to locked. CHDK will handle the locking for the camera.

Insert the card into the camera, turn it on, and see the CHDK. Works like a charm.

Making a time lapse movie

I'm using CHDK on my old Canon Powershot SX20 and have the Ultimate Intervalometer script installed. This script lets me set the camera to start and stop shooting at specific times and at a specific interval. I had done this a couple of years ago for something (a brewing day, I think), but had forgotten the details, hence this post (nice camera, doesn't get used much since the camera on my phone is pretty decent for most things). After running the script, there are a number of individual jpg files that need to be combined into a movie.

To create video out of time lapse stills:

sd card has 2 partitions, one for the CHDK boot code, the other for pictures:
CHDK: mmcblk0p1
pictures: mmcblk0p2

(Side note: to add a new script, copy it to the pictures partition under CHDK/SCRIPTS.)

mount sd card at /mnt/sd:
make sure card is writable (be sure to set the write lock back when putting the card back into the camera)
sudo mount -t vfat /dev/mmcblk0p2 /mnt/sd

cd to DCIM dir on memory card, then:

mencoder mf://*.JPG -mf fps=10:type=jpg -ovc x264 -x264encopts bitrate=1200:threads=2 -o /home/danson/tmp/outputfile.mkv

change fps (frames per second) and bitrate as needed, rest should be good.

change mkv to avi for upload to youtube or whatever:

mencoder mf://*.JPG -mf fps=10:type=jpg -ovc x264 -x264encopts bitrate=1200:threads=2 -o /home/danson/tmp/07262012.avi

Can use ffmpeg (haven't tested this, copied the directions from somewhere on the internet):

For creating a video from many images:

ffmpeg -f image2 -i foo-%03d.jpeg -r 12 -s WxH foo.avi

The syntax foo-%03d.jpeg specifies to use a decimal number composed of three digits padded with zeroes to express the sequence number. It is the same syntax supported by the C printf function, but only formats accepting a normal integer are suitable.

When importing an image sequence, -i also supports expanding shell-like wildcard patterns (globbing) internally. To lower the chance of interfering with your actual file names and the shell’s glob expansion, you are required to activate glob meta characters by prefixing them with a single % character, like in foo-%*.jpeg, foo-%?%?%?.jpeg or foo-00%[234%]%*.jpeg. If your filename actually contains a character sequence of a % character followed by a glob character, you must double the % character to escape it. Imagine your files begin with %?-foo-, then you could use a glob pattern like %%?-foo-%*.jpeg. For input patterns that could be both a printf or a glob pattern, ffmpeg will assume it is a glob pattern.

Thursday, September 8, 2016

Installing Arch Linux and XFCE

Notes to self about reinstalling Arch Linux

I got a new 500 GB SSD drive for my laptop, so I'm installing Arch on it.

Before installation, copy all of ~ to my network drive:

mkdir /whatever/on/network/drive
cd ~
cp -rpP .

Turns out my network drive doesn't support the 'p' option, so no timestamps were saved. Note also that this command copies everything, including all the dot files and the 35GB of crap that was in the trash folder that I forgot about.

Burn the latest Arch installation .iso to a DVD. The file is a little too large for a CD.

Remove the old hard drive, install the new one.

Insert the Arch installation disk, reboot.

Choose 'Boot Arch Linux'. This gets to a command prompt as root.

Check the networking, just ping google or somewhere, it should just work.

Partition the drive. Partition plan:
/dev/sda1 ext4 / 50 GB
/dev/sda2 swap 1 GB
/dev/sda3 ext4 /home All remaining space

I used fdisk to create the partitions. Before creating the above partitions, use the 'o' option to create an MBR sector. All 3 were set as primary partitions. Use the 'a' option to set /dev/sda1 as bootable.

Format the partitions:
mkfs -t ext4 /dev/sda1
mkfs -t ext4 /dev/sda3

Mount the partitions:
mkdir /mnt
mkdir /mnt/home
mount /dev/sda1 /mnt
mount /dev/sda3 /mnt/home

Base Arch installation:
pacstrap /mnt base base-devel

This takes a while.

Create fstab:
genfstab /mnt >> /mnt/etc/fstab

Check it with

less /mnt/etc/fstab

There should be an entry for / and one for /home.

Chroot into the new system:
arch-chroot /mnt

Set the timezone:
ln -s /usr/share/zoneinfo/US/Mountain /etc/localtime

Set the hardware clock:
hwclock -systohc --utc

Set the locale:
nano /etc/locale.gen
Find the line with en_US.UTF-8 and uncomment it, then save the file and


Set the LANG variable:
echo LANG=en_US.UTF-8 > /etc/locale.conf

I actually set the locale much later when I found out that Perl whines non-stop about it.

Set the hostname:

echo deadlock > /etc/hostname

Update /etc/hosts and add an entry for deadlock.

Set the root password:


Install grub:

pacman -S grub-bios
grub-install /dev/sda
grub-mkconfig -o /boot/grub/grub.cfg

Unmount the partitions:
exit (gets out of chroot environment)
umount /mnt/home
umount /mnt
reboot (leave the installation disk in)

When the Arch installation choices come up, choose boot from existing OS.

Turn on dhcp:
systemctl enable dhcpcd

Then reboot again, but this time remove the Arch installation disk first.

Install xfce:

pacman -S alsa-utils

pacman -S xorg-server xorg-server-utils xorg-xinit

pacman -S xfce4 xfcd4-goodies

lightdm: (display manager, starts xfce and asks for username/password)
pacman -S lightdm

Enable lightdm:
systemctl enable lightdm.service

add myself as a user:
uncomment the line %wheel ALL=(ALL) ALL, save
useradd -m -g users -G storage,power,wheel -s /bin/bash danson
passwd danson

Reboot. The system should come up with the new lightdm display manager asking for a user name and password.

Copy all the files from the network drive to ~.

Spend the next couple of weeks installing and configuring stuff, like there aren't any printers, ssh isn't available yet, java needs installed, etc.

Update 12 Sep 2016:
I realized I didn't install the wireless network stuff. The kernel driver is already installed, so first, add me to the 'users' group:

gpasswd -a danson users

Log out and log back in for this to take effect. Install wicd and the gtk gui:

pacman -S wicd wicd-gtk

Start wicd:
systemctl start wicd

Have wicd start on start up:
systemctl enable wicd

Kill and disable dhcpcd:
kill -9 dhcpcd
systemctl disable dhcpcd

Update 19 Sep 2016:
The touchpad is wonky. I've been using a regular mouse, so didn't notice this until I was traveling for a few days.

pacman -S xf86-input-synaptics

Create /etc/X11/xorg.conf.d/70-synaptics.conf with this content:

Section "InputClass"
    Identifier "touchpad"
    Driver "synaptics"
    MatchIsTouchpad "on"
        Option "TapButton1" "1"
        Option "TapButton2" "3"
        Option "TapButton3" "2"
        Option "VertEdgeScroll" "on"
        Option "VertTwoFingerScroll" "on"
        Option "HorizEdgeScroll" "on"
        Option "HorizTwoFingerScroll" "on"
        Option "CircularScrolling" "on"
        Option "CircScrollTrigger" "2"
        Option "EmulateTwoFingerMinZ" "40"
        Option "EmulateTwoFingerMinW" "8"
        Option "CoastingSpeed" "0"
        Option "FingerLow" "30"
        Option "FingerHigh" "50"
        Option "MaxTapTime" "125"

Log out and log back in (or otherwise restart X)

Thursday, October 29, 2015

Using Antlr with jEdit

I've written a number of parsers in the past with javacc, but my recent struggles in modifying my Java 7 grammar to support Java 8 got me to looking at other options. I'd been reading good things about Antlr 4 (Antlr is at, so I gave it a try. I've been very impressed. It's easy to use, well documented, and well architected. The on-line documentation is great, but it's worth buying the book.

Of course, I'm creating and editing Antlr grammars with jEdit, so I wrote a few things for jEdit to make it easier to work with Antlr.

First is an edit mode for the *.g4 grammar files:

This lets jEdit provide syntax highlighting for the grammar files. Here's an example using the Java 8 grammar:

grammar Java8;

@lexer::members {
    public static final int WHITESPACE = 1;
    public static final int COMMENTS = 2;

 * Productions from §3 (Lexical Structure)

    :   IntegerLiteral
    |   FloatingPointLiteral
    |   BooleanLiteral
    |   CharacterLiteral
    |   StringLiteral
    |   NullLiteral

 * Productions from §4 (Types, Values, and Variables)

    :   primitiveType
    |   referenceType

    :   annotation* numericType
    |   annotation* 'boolean'

Antlr produces a couple of *.tokens files, one for the parser, and one for the lexer. It turns out these are just properties files, which jEdit already has good support for. To get jEdit to recognize these token files as property files, it is just a matter of going to Utilities - Global Options - Editing, choosing the Properties edit mode, and adding "token" as a file name extension to the existing list of extensions. Now the token files look like this:


Notice there is no order to these, so the JavaSideKick plugin, which supports properties files, is very useful:

Even more useful is an Antlr Sidekick. The parser for this Sidekick is written in Antlr. I packaged Antlr itself in a separate library plugin so it can be updated independently of the Antlr Sidekick plugin.

Notice the lexer and parser rules are in separate nodes in the tree and the usual sidekick features such as sorting and filtering are supported.

The Antlr Sidekick also has one action that is really useful, if a *.g4 file is the current file in jEdit, then going to Plugins - AntlrSidekick - Generate files will run Antlr on the g4 file and generate (or regenerate) the parser, lexer, and listener files, so there is no need to go to the command line to do so.

Friday, August 28, 2015

Getting rid of the Java beep

This has been quite an adventure. I didn't think that something as simple-sounding as adding an option to jEdit to turn off the beeping noise that some of the components make would be a big deal. I thought it would be something along the lines of adding a utility method that checked a property to see if the beep should happen or not, then change all the beep calls to use this new utility method. Not even close to the right solution. The problem with this approach is that many of the JVM provided Swing components also beep and, of course, those components would have no idea about my utility method.
After some research on the internet and perusing the Swing source code, the right way to cause a component to beep is to use the look and feel to provide error feedback to the user. In fact, that is the exact method in the look and feel to use: provideErrorFeedback. By default, this method simply calls Toolkit.beep(). Here is the complete method as implemented in Java 1.8:

     * Invoked when the user attempts an invalid operation,
     * such as pasting into an uneditable <code>JTextField</code>
     * that has focus. The default implementation beeps. Subclasses
     * that wish different behavior should override this and provide
     * the additional feedback.
     * @param component the <code>Component</code> the error occurred in,
     *                  may be <code>null</code>
     *                  indicating the error condition is not directly
     *                  associated with a <code>Component</code>
     * @since 1.4
    public void provideErrorFeedback(Component component) {
        Toolkit toolkit = null;
        if (component != null) {
            toolkit = component.getToolkit();
        } else {
            toolkit = Toolkit.getDefaultToolkit();
    } // provideErrorFeedback()

I grepped the Swing source code, and no component calls Toolkit.beep() directly. The only call to Toolkit.beep() is in the provideErrorFeedback method. So this leads me to believe that the proper way to implement a beep is via the look and feel rather than calling Toolkit.beep() directly. My next thought was to use one of the byte code manipulation libraries like BCEL or Javaassist in the LookAndFeel plugin to change the method to look like this:
 public void provideErrorFeedback(Component component) {
     if (!"true".equals(System.getProperty("allowBeep")))
     Toolkit toolkit = null;
     if (component != null) {
         toolkit = component.getToolkit();
     } else {
         toolkit = Toolkit.getDefaultToolkit();
 } // provideErrorFeedback()

That seemed pretty straightforward, so I wrote a class loader for the LookAndFeel plugin that would insert the line or the whole method. This worked well, until I realized that this will only work for a look and feel loaded through the LookAndFeel plugin, not any of the Swing look and feels. I know many people are satisfied with Metal or Nimbus, and this approach wouldn't work for those. I did go ahead and give it a go anyway, adjusting the LookAndFeel plugin to let the user select the built-in look and feels and attempt to patch them on the fly, but the JVM security manager wouldn't allow it.
A note about the byte code manipulation libraries, I ended up using Javassist as it is easier to work with. BCEL is fine, but works at a lower level and has a fairly steep learning curve.
My last approach, and the most successful, is to use the Java instrumentation API and use that to patch javax.swing.LookAndFeel directly. This works well since every look and feel that I have the source code to does not override the provideErrorFeedback method in that class, so it's the single place where all look and feels, and thus all components, go to make a beep. The downside of this is the code must kick in at JVM start up time, which means it can't be in a regular jEdit plugin, since those don't get loaded until well after the JVM classes.
Using the instrumentation API is really simple. All that is needed is a Java agent. The one I wrote looks like this:

package javassist;

import java.lang.instrument.*;

 * Simple java agent to fix the <code>provideErrorFeedback</code> in javax.swing.LookAndFeel.
 * This adds a line to that method to check the System property "allowBeep" before it 
 * proceeds with beeping. If "allowBeep" is anything other than "true", the method returns
 * immediately without providing any error feedback.
public final class LNFAgent implements ClassFileTransformer {

    private static final String CLASS_TO_PATCH = "javax/swing/LookAndFeel";

    public static void premain( String agentArgument, final Instrumentation instrumentation ) {
        LNFAgent agent = null;

        try {
            agent = new LNFAgent();
        } catch ( Exception e ) {
            System.out.println("LNFAgent not installed.");
        instrumentation.addTransformer( agent );
        System.setProperty("LNFAgentInstalled", "true");

    public byte[] transform( final ClassLoader loader, String className, final Class classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer ) throws IllegalClassFormatException {
        byte[] result = null;

        if ( className.equals( CLASS_TO_PATCH ) ) {
            try {
                CtClass ctClass = ClassPool.getDefault().makeClass( new DataInputStream( new ByteArrayInputStream( classfileBuffer ) ) );
                CtMethod ctMethod = ctClass.getDeclaredMethod( "provideErrorFeedback" );
                ctMethod.insertBefore( "if (!\"true\".equals(System.getProperty(\"allowBeep\"))) return;" );
                result = ctClass.toBytecode();
            } catch ( Exception e ) {
                System.err.println("Unable to patch javax.swing.LookAndFeel to disable beeps.");
        return result;
Here's a quick run through of this code:
  • First, I put this code in the javassist package. My thought is to go ahead and make a plugin out of Javassist anyway, and insert this agent into the Javassist jar file.
  • The premain method is the method required by the instrumentation API. This method gets called on JVM start up and initializes the agent. For this purpose. I'm setting a System property to indicate that the agent has been installed so that my LookAndFeel plugin can know.
  • The transform method is where the actual code gets inserted into the provideErrorFeedback method. Again, this method is specified by the instrumentation API. Notice that the Javassist code is only 3 lines. The corresponding BCEL code was about 30.
To get the JVM to load this code on start up requires two more adjustments.
First, the manifest in the jar needs a couple of settings. These 3 lines need to be added:

Premain-Class: javassist.LNFAgent
Agent-Class: javassist.LNFAgent
Boot-Class-Path: JavassistPlugin.jar
Technically, all that is required by the instrumentation API is the Premain-Class and Agent-Class settings. The Boot-Class-Path is required to get the rest of Javassist loaded.
Once this adjusted manifest is added to the jar file, all coding is complete.
The last step is to adjust the command line to start jEdit to invoke the agent on start up. I have a small script I use to start jEdit that sets a couple of environment variables. It looks like this:

export JEDIT_HOME=/home/danson/apps/jedit/current

# Antialias fonts everywhere possible.
ANTIALIAS_ALL="-Dawt.useSystemAAFontSettings=on -Dswing.aatext=true"
export VISUAL=/home/danson/bin/jeditcommit

# this one for general usage
#java -Xmx620m -Xms512m ${ANTIALIAS_ALL} "-Djedit.home=$JEDIT_HOME" -jar "$JEDIT_HOME/jedit.jar" -reuseview "$@"

# this one for debugging
#java ${ANTIALIAS_ALL} -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8001,suspend=n -Xmx512m -jar /home/danson/apps/jedit/current/jedit.jar -reuseview > /dev/null 2>&1 &

# this one for no beeps
java -javaagent:/home/danson/.jedit/jars/JavassistPlugin.jar -Xmx620m -Xms512m ${ANTIALIAS_ALL} "-Djedit.home=$JEDIT_HOME" -jar "$JEDIT_HOME/jedit.jar" -reuseview "$@"

The key piece here is that -javaagent part. This tells the JVM to look in JavassistPlugin.jar for a java agent. The manifest tells the JVM the rest of the details. Once the agent is loaded, every single class that the JVM loads is passed to the transform method in the agent, which looks only for the javax.swing.LookAndFeel class, and inserts the tiny bit of code needed to toggle the beep.
That's really all there is to it. My main problems with this approach are
  • The command line to start jEdit needs to be set by hand. There isn't a way to get the agent code to run without including the -javaagent parameter.
  • Packaging this as a jEdit plugin is not right, since jEdit loads plugins late in the start up process and loads them each in their own classloader. Since this agent code is loaded way before jEdit, it can't take advantage of any of the usual plugin facilities.
  • When used as an agent, the plugin cannot be reloaded since it is loaded in the system classloader.
  • And the biggest problem is that I shouldn't need to patch a JVM class. Really, Sun/Oracle should have included what is basically a one-liner to allow apps to turn off the error feedback. It's a simple fix that quite a few people would take advantage of, myself included.
One more note: the right way to make a beep is to make a call like this:
Instead of null, the appropriate component can be passed. Using Toolkit.beep() directly short-circuits the ability of the look and feel to provide it's own implementation of appropriate error feedback, so don't do it! Now I need to go through the jEdit code itself and change out all 156 of those calls to Toolkit.beep()

Friday, May 22, 2015

Arduino speaker controller

I've had this project in mind for a while, and finally got around to it. I wanted a way to turn on or off the various speakers around the house. In addition to the front room, where the main stereo is, we have speakers in the dining room, hot tub area, and back patio. Previously, I could turn on the amp, pick a playlist from Kodi, and make it play all from my phone, but if I was outside on the patio, I had to go back in the house and push a button to turn on the speakers. No more! Now I have a simple web app and an Arduino controller to turn on the speakers.

 I got a lot of inspiration and ideas for this project from There is even a link to a nice looking Android app to control the speaker relays, but I couldn't get it to work, so I just did a little web app that runs on Arduino to control the speakers. I got just about all the parts from Amazon:

Here is the wiring to control the relay board. The brown wire on the left connects to the 9V output on the Arduino to power the relays themselves. The black wire on the right is the ground, and is common to the board and the relays. The white wire connects to the 5V output on the Arduino to power the relay circuit board.

A close up of the connections on the relay board. Again, the brown wire on the right is connected to the 9V output on the Arduino and powers the relays themselves, while the white wire is connected to the 5V output on the Arduino to power the relay circuit board. The other wires are connected to pins 2 through 9 on the Arduino to actually trigger the relays.

Here are the connections on the Arduino side. You can see the 5V white wire, the black ground wire, and the 9V brown wire. The other wires are connected to pins 2 though 9 to control the 8 relays.

Just a big picture of the completed wiring between the Arduino and the relay board

A shot of everything in the box. My dremel skills aren't the best, but this will be in a cabinet and essentially hidden from view, so it's okay. There are cut outs for the ethernet, USB, and power connections.

The speakers will connect here. It might have been better if I'd found similar connectors that were all black. For this use, each speaker gets two connectors. The negative wire is cut, then one of the cut ends goes in the top row of connectors and the other cut end connects to the corresponding connector in the bottom row. The relay then completes the circuit. These are installed right to left, so the right most connector is controlled by relay 1, and the left most connector is controlled by relay 8. I did it this way so it would be easier to wire into my stereo cabinet. Note that this piece is one of about 6 that make all the speakers work. 

A shot of the inside. It looks messy, but it's pretty straightforward.

The code is a simple web server. The IP address is hard-coded to so it's easy to bookmark in my phone's browser. The web page it produces looks like this, both on my phone and on my desktop browser:

Pretty plain, but it does the right thing.

Simple web server to serve a form to turn on or off digital pins. In this case,
the pins are connected to relays to turn speakers on or off. This uses pins
2 through 9 in pairs, so 2 and 3 control the hot tub speakers, 4 and 5 control
the dining room speakers, and 6 and 7 control the portico speakers. 8 and 9 are
available in case I ever find a need to connect one more pair of speakers.

#include <SPI.h>
#include <Ethernet.h>
#include <string.h>

// MAC address can be anything that is unique within the local network.
byte mac[] = {0x00, 0x1E, 0x2A, 0x77, 0x24, 0x02 };

// Some unused IP address on the local network.
byte ip[] = {192, 168, 2, 251 }; 

// web server, nothing fancy, just port 80 for http
EthernetServer server( 80 ); 

#define RELAY_ON 0
#define RELAY_OFF 1

// true, just show speaker status or false, actually change the speaker states
boolean showStatus = false;

// which zones are on or off, initially, all are off
boolean zone1 = false;
boolean zone2 = false;
boolean zone3 = false;
boolean zone4 = false;

// pin definition, one pin per speaker, 
// so the left speaker in zone 1 is pin 2, etc
static const int zone1L = 2;
static const int zone1R = 3;
static const int zone2L = 4;
static const int zone2R = 5;
static const int zone3L = 6;
static const int zone3R = 7;
static const int zone4L = 8;
static const int zone4R = 9;

// set up pins, initially all speakers in all zones are off
void setupPins() {
    digitalWrite(zone1L, RELAY_OFF);
    digitalWrite(zone1R, RELAY_OFF);
    digitalWrite(zone2L, RELAY_OFF);
    digitalWrite(zone2R, RELAY_OFF);
    digitalWrite(zone3L, RELAY_OFF);
    digitalWrite(zone3R, RELAY_OFF);
    digitalWrite(zone4L, RELAY_OFF);
    digitalWrite(zone4R, RELAY_OFF);

    pinMode( zone1L, OUTPUT );
    pinMode( zone1R, OUTPUT );
    pinMode( zone2L, OUTPUT );
    pinMode( zone2R, OUTPUT );
    pinMode( zone3L, OUTPUT );
    pinMode( zone3R, OUTPUT );
    pinMode( zone4L, OUTPUT );
    pinMode( zone4R, OUTPUT );

void setup() {
    Ethernet.begin( mac, ip );


// set up buffer for reading web requests
static const int bufferMax = 128;
int bufferSize;
char buffer[ bufferMax ];

void loop() {
    EthernetClient client = server.available();
    if ( client ) {
        waitForRequest( client );

void waitForRequest( EthernetClient client ) 
    bufferSize = 0;

    while ( client.connected() ) {
        if ( client.available() ) {
            char c =;
            if ( c == '\n' ) {
            else {
                if ( bufferSize < bufferMax ) {
                    buffer[ bufferSize++ ] = c;
                else {

void handleRequest() {
    // Received buffer contains a standard HTTP GET line, something like
    // "GET /?X=X&Y=Y HTTP/1.1". 
    // Could have up to 4 parameters, 1=on&2=on&3=on&4=on,
    // one for each set of speakers. All that is necessary here is to
    // extract the query string and check for each of the zone numbers.
    String request = String(buffer);
    int firstSpace = request.indexOf(" ");   // right after GET
    int lastSpace = request.indexOf(" ", firstSpace + 1); // just after the query string
    request = request.substring(firstSpace, lastSpace);
    showStatus = request.indexOf("?") == -1;
    zone1 = request.indexOf("1") > -1;
    zone2 = request.indexOf("2") > -1;
    zone3 = request.indexOf("3") > -1;
    zone4 = request.indexOf("4") > -1;

    if (!showStatus) {

void setSpeakerState() {
    digitalWrite(zone1L, zone1 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone1R, zone1 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone2L, zone2 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone2R, zone2 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone3L, zone3 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone3R, zone3 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone4L, zone4 ? RELAY_ON : RELAY_OFF);
    digitalWrite(zone4R, zone4 ? RELAY_ON : RELAY_OFF);

void sendPage() {

// the 'bootstrap' css makes the page look good in desktop browsers and on phones
void printHead() {
    server.print("<html>\n<head>\n<title>Speaker Control</title>\n");
    server.print("<meta name='viewport' content='width=device-width, initial-scale=1'>\n");
    server.print("<link rel='stylesheet' href=''>\n");

void printForm() {
   server.print("<div class='container'>\n");
   server.print("<h2>Speaker Control</h2><br/>\n");
   server.print("<form action='/' role='form'>\n");
   int pinState = digitalRead(zone4L);                                           
   server.print("<div class='checkbox'><label><input type='checkbox' name='4'");
   server.print(pinState == RELAY_ON ? " checked='checked'" : "");
   server.print(">Hot tub</label></div><br/>\n");

   pinState = digitalRead(zone3L);                                           
   server.print("<div class='checkbox'><label><input type='checkbox' name='3'");
   server.print(pinState == RELAY_ON ? " checked='checked'" : "");               
   server.print(">Dining room</label></div><br/>\n");                            
   pinState = digitalRead(zone2L);                                           
   server.print("<div class='checkbox'><label><input type='checkbox' name='2'");
   server.print(pinState == RELAY_ON ? " checked='checked'" : "");               
   pinState = digitalRead(zone1L);
   server.print("<div class='checkbox'><label><input type='checkbox' name='1'");
   server.print(pinState == RELAY_ON ? " checked='checked'" : "");               
   server.print("<button type='submit' class='btn btn-default'>Save</button>\n");

void printTail() {