Update on Garmin Forerunner 935

It’s been a few weeks since I picked up my Garmin Forerunner 935. I must say I’m pretty impressed.

Step counting

I’ve been using it to record my day to day step count and all day heart rate data as well as all my training and the London Triathlon.

The battery life is great, I’m getting a good 2 weeks out of a charge even when using it to record activities with GPS and ANT+ sensor data. It seams to take about 2 hours to fully charge.

Having it sync with the phone is useful as it means I don’t need to keep a Windows box kicking around just (OK, I do still need one for Zwift but that is less regular) to run the Garmin Connect application to upload my workouts to the web and Strava. There is built in WiFi support as well which can allow it to sync without having the phone, I’ve not enabled this at the moment as even if I’m not always carrying my phone while training it is pretty much always going to be around when I get back.

Another change is that the ANT+ sensors now live in a collective pool rather than being bound to something like a bike profile so you don’t need to remember to pick the right profile if you have multiple bikes. The watch will just pick all the relevant sensors it can see as you select the activity type. The only downside I can see to this is if you lend somebody a bike and both go riding at the same time. To get round this you can force it to pick one if it can see multiple versions of the same sensor. But it does mean I don’t need 3 different profiles, one for the Propel, Defy and the Defy on the turbo trainer.

Rest indicator

The new training tracking feature is also helpful, giving indications of how much rest time you should take between activities and also a training load number. The training load number is supposed to unique to each user so not something you can compare with others, but should show if the system thinks you are over training (looks like I need to back off a little)

Training Load

The only extra I have purchased is a glass screen protector as I managed to get a very small scratch in the plastic face on the first day wearing it. I’ve no idea how it did it as I doing remember knocking or catching it against anything. The protector is very thin and fits nearly flush with the bezel and you can’t tell it’s there. Given I’m planning on wearing this as my day to day watch as well as for activity tracking this is a little disappointing, but this is probably why it’s cheaper than the equivalently spec’d Fenix 5.

London Triathlon 2017

As I mentioned in the last post, I did the London Triathlon at the weekend. I got round in a total time of 2:40:33 which is 3min quicker than WTS even in Leeds I did about a month ago. I’m slowly working my way back towards the sub 2:30:00 times I managed in 2015.

The weather forecast changed all week but always with rain at some point in the day, early on it looked like it might stay dry until at least the run, but this was dashed when I got properly soaked while riding from Leytonstone down to Excel before the start.

As usual The London Triathlon runs a number of different courses over the weekend, I was racing the “shortest” loop version of the Olympic distance which was made up a 2 lap swim, 4 lap bike and a 4 lap run.

The “long” loop version on Sunday morning is a 1 lap swim and a 1.5 lap bike (down to Parliament and back) and a 3 lap run which I’ve done a couple of times before.

Swim

There was a break in the rain just in time for the start of the swim

The 2 lap swim has it’s good and bad points over the 1 lap version

  • good: you can see the between all the turning buoys. For the 1 lap version you can’t see the first buoy from the start line.
  • bad: Waves set off in 2 halves with 2 mins between halves and 20mins between waves, which means that as you start your pretty much straight into the back of the mid pace swimmers from the wave before on their second lap. Also with the shorter legs the waves don’t spread out as much so the was a lot more bumping and jostling all the way round and especially at the run in to the exit.

Ride

By the time I was out of the water and on to the bike the rain had well and truly kicked back in. The course was a 10k loop between 2 roundabouts, but the turns were the short way round the which made them very tight, this combined with a little technical section just west of Excel made for some treacherous areas. The course was pretty much pan flat except the climb over the flyover just before the first turn. I averaged 30kph, over the 40km which is OK considering how wet it was.

Run

It stopped raining again for the run which is again nearly totally flat, apart from the climb up into the Excel each lap to pass the turn to the
finish straight. The indoor loop was a bit longer this year.

The new Garmin 935 worked really well, the triathlon mode is very similar to the 910XT with the lap button being used to move between disciplines. One feature that I think is new is the ability to set the auto button lock on a per activity basis, I used this to lock the buttons for the openwater swim. I did this because unlike the 910XT the the start/stop and lap button are on the right hand edge of the watch and as I wear my watch on my right wrist this put the buttons up against the edge of my wetsuit so I was a little worried they might get push by accident. This just meant I had to press and hold one of the buttons when I got out of the water to unlock things before pressing the lap button to signal entering T1.

Garmin Forerunner 935

My trusty Garmin Forerunner 910xt has finally been put out to pasture, 2 years ago the barometric altimeter failed and I got it replaced with a refurbed version and over the last 3 months the power button has been getting harder and harder to push. My best guess is that the micro switch has lifted off of the board so it needs to be push at just the right angle to get it to line up with the contacts and actually activate.

My Fitbit HR had also given up the ghost as well in the last few months so I went looking for a replacement that would cover for both. I looked at both the Garmin 735 and the 935. Both do step counting and have a optical HR sensor in the back. Reviews of the HR sensor on the 735 were not so great and it was missing a barometric altimeter so that didn’t help it’s case. Wiggle were also doing a week of extra discount (17%) at the time as well which helped to bring the price of the 935 down to something slightly more sensible than list price.

So as you can guess by the title of this post I opted for the 935. It arrived this morning so I don’t have a lot to say about it just yet, but the first impressions are:

Garmin forerunner 935
Garmin forerunner 935
  • It’s a lot smaller than the 910xt and even a bit smaller than the Suunto Vector that I have been wearing as a day to day watch
  • It’s also lighter than I expected, I’m used to wearing something with a bit of heft (My first serious sailing watch was a Citizen Yatchmaster which was stainless steel, when I took it off my arm used to float) to it so this it will take a day or two to get used to how light it is.

A lot of the features need a bit of time to learn my training pattern and my day to day activity profile so I’ll give it a week to bed in and write some more about it, I’m also doing the London Tri next weekend so that will be a good chance to give it a proper workout.

Both the 735 and the 935 both support the 2 new HRM belts from Garmin that support recording HR data while swimming (the HRM-Tri and HRM-Swim), while I already have a ant+ HRM belt I’m seriously tempted by both of these (mainly for the geekiness) so I may have to grab one or both soon.

Multipart HTTP Post requests with Volley on Android

It’s been a little while since I’ve done any really serious Android development, but a couple of projects have brought me back to it.

Early on in one of those projects I had to make some HTTP requests, my first thought was to make use of the Apache HTTP Client classes as I had done many times before on Android. Which is why I was a little surprised when the usual ctrl-space didn’t auto complete any of the expected class names.

It turns out the classes were removed in Android 6.0 and the notice suggests using the HttpURLConnection class. A little bit more digging turned up a wrapper for this called Volley.

Volley is a wrapper round the HttpURLConnection class to provides a neat asynchronous interface that does IO in the background and then delivers results to the Main thread so UI updates can be done with out further faffing around switching threads. There is also a nice set of tutorials on the Android Developers pages.

The first few requests all worked fine, but there was one which was a little bit more tricky. The HTTP endpoint in question accepts a multipart-form payload. A bit of googling/searching on Stackoverflow turned up a number of approaches to this and best seamed to be documented in this gist

This was close to what I wanted but not quite what I needed so I have taken some of the core concepts and built my own MultipathRequest object.

...
MultipartRequest request = new MultipartRequest(url, headers, 
    new Response.Listener<NetworkResponse>() {
        @Override
        public void onResponse(NetworkResponse response) {
        ...
        }
    },
    new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
        ...
        }
    });
    
request.addPart(new FormPart(fieldName,value));
request.addPart(new FilePart(fileFieldName, mimeType, fileName, data);

requestQueue.add(request);
...

I’ve stuck the code up on github here. You can include it in your Android Project by adding the following to the build.gradle in the root of the project:

allprojects {
  repositories {
    ...
    maven { url 'https://jitpack.io' }
  }
}

And then this to the dependencies section of the modules build.gradle:

dependencies {
  compile 'com.github.hardillb:MultiPartVolley:0.0.3'
}

Fist pass TRÅDFRI MQTT Bridge

I’ve been working on integrating the new IKEA TRÅDFRI Lights into my Home Automation system. I’d really like a native NodeJS system so I can plug it directly into Node-RED, but I’ve not found a working CoAP over DTLS setup just yet.

So in the mean time I’ve been working on a very basic MQTT to CoAP client bridge in Java using the Eclipse Californium library.

It still needs some work, but here is the first pass:

package uk.me.hardill.coap2mqtt;

import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.logging.Level;

import org.eclipse.californium.core.CaliforniumLogger;
import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapHandler;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.Utils;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.core.network.config.NetworkConfig;
import org.eclipse.californium.scandium.DTLSConnector;
import org.eclipse.californium.scandium.ScandiumLogger;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.californium.scandium.dtls.pskstore.StaticPskStore;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.json.JSONArray;
import org.json.JSONObject;

/**
 * @author hardillb
 *
 */
public class Main {
  
  static {
    CaliforniumLogger.disableLogging();
    ScandiumLogger.disable();
//    ScandiumLogger.initialize();
//    ScandiumLogger.setLevel(Level.FINE);
  }
  
  private DTLSConnector dtlsConnector;
  private MqttClient mqttClient;
  private CoapEndpoint endPoint;
  
  private String ip;
  
  private HashMap<String, Integer> name2id = new HashMap<>();
  
  Main(String psk, String ip) {
    this.ip = ip;
    DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder(new InetSocketAddress(0));
    builder.setPskStore(new StaticPskStore("", psk.getBytes()));
    dtlsConnector = new DTLSConnector(builder.build());
    endPoint = new CoapEndpoint(dtlsConnector, NetworkConfig.getStandard());
    
    MemoryPersistence persistence = new MemoryPersistence();
    try {
      mqttClient = new MqttClient("tcp://localhost", MqttClient.generateClientId(), persistence);
      mqttClient.connect();
      mqttClient.setCallback(new MqttCallback() {
        
        @Override
        public void messageArrived(String topic, MqttMessage message) throws Exception {
          // TODO Auto-generated method stub
          System.out.println(topic + " " + message.toString());
          String parts[] = topic.split("/");
          int id = name2id.get(parts[1]);
          System.out.println(id);
          String command = parts[3];
          System.out.println(command);
          try{
            JSONObject json = new JSONObject("{\"9001\":\"Living Room Light\",\"9020\":1491515804,\"9002\":1491158817,\"9003\":65537,\"9054\":0,\"5750\":2,\"3\":{\"0\":\"IKEA of Sweden\",\"1\":\"TRADFRI bulb E27 opal 1000lm\",\"2\":\"\",\"3\":\"1.1.1.0-5.7.2.0\",\"6\":1},\"9019\":1,\"3311\":[{\"5850\":1,\"5851\":10,\"9003\":0}]}");
            JSONObject settings = json.getJSONArray("3311").getJSONObject(0);
            if (command.equals("dim")) {
              settings.put("5851", Integer.parseInt(message.toString()));
            } else if (command.equals("on")) {
              if (message.toString().equals("0")) {
                settings.put("5850", 0);
                settings.put("5851", 0);
              } else {
                settings.put("5850", 1);
                settings.put("5851", 128);
              }
            }
            String payload = json.toString();
            Main.this.set("coaps://" + ip + "//15001/" + id, payload);
          } catch (Exception e) {
            e.printStackTrace();
          }
        }
        
        @Override
        public void deliveryComplete(IMqttDeliveryToken token) {
          // TODO Auto-generated method stub
        }
        
        @Override
        public void connectionLost(Throwable cause) {
          // TODO Auto-generated method stub
        }
      });
      mqttClient.subscribe("TRÅDFRI/+/control/+");
    } catch (MqttException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }
  
  private void discover() {
    try {
      URI uri = new URI("coaps://" + ip + "//15001");
      CoapClient client = new CoapClient(uri);
      client.setEndpoint(endPoint);
      CoapResponse response = client.get();
      JSONArray array = new JSONArray(response.getResponseText());
      for (int i=0; i<array.length(); i++) {
        String devUri = "coaps://"+ ip + "//15001/" + array.getInt(i);
        this.watch(devUri);
      }
      client.shutdown();
    } catch (URISyntaxException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }
  
  private void set(String uriString, String payload) {
    System.out.println("payload\n" + payload);
    try {
      URI uri = new URI(uriString);
      CoapClient client = new CoapClient(uri);
      client.setEndpoint(endPoint);
      CoapResponse response = client.put(payload, MediaTypeRegistry.TEXT_PLAIN);
      if (response.isSuccess()) {
        System.out.println("Yay");
      } else {
        System.out.println("Boo");
      }
      
      client.shutdown();
      
    } catch (URISyntaxException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }
  
  private void watch(String uriString) {
    
    try {
      URI uri = new URI(uriString);
      
      CoapClient client = new CoapClient(uri);
      client.setEndpoint(endPoint);
      CoapHandler handler = new CoapHandler() {
        
        @Override
        public void onLoad(CoapResponse response) {
          System.out.println(response.getResponseText());
          JSONObject json = new JSONObject(response.getResponseText());
          if (json.has("3311")){
            MqttMessage message = new MqttMessage();
            int state = json.getJSONArray("3311").getJSONObject(0).getInt("5850");
            message.setPayload(Integer.toString(state).getBytes());
            message.setRetained(true);
            String topic = "TRÅDFRI/" + json.getString("9001") + "/state/on";
            String topic2 = "TRÅDFRI/" + json.getString("9001") + "/state/dim";
            name2id.put(json.getString("9001"), json.getInt("9003"));
            MqttMessage message2 = new MqttMessage();
            int dim = json.getJSONArray("3311").getJSONObject(0).getInt("5851");
            message2.setPayload(Integer.toString(dim).getBytes());
            message2.setRetained(true);
            try {
              mqttClient.publish(topic, message);
              mqttClient.publish(topic2, message2);
            } catch (MqttException e) {
              // TODO Auto-generated catch block
              e.printStackTrace();
            }
          } else {
            System.out.println("not bulb");
          }
        }
        
        @Override
        public void onError() {
          // TODO Auto-generated method stub
          
        }
      };
      client.observe(handler);
    } catch (URISyntaxException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    
  }

  /**
   * @param args
   */
  public static void main(String[] args) throws InterruptedException {
    String psk = args[0];
    String ip = args[1];
    Main m = new Main(psk, ip);
    
    m.discover();
  }

}

I’ve tagged the code onto the gist as well for now, but I’ll check the whole thing in as a separate project soon.

EDIT: now with it’s own Github repo here

More improvements to the WeMo Node-RED Nodes

Having got all my bulbs working again with my WeMo Link device it was time to have another play with the Node-RED WeMo nodes.

  • First up close the gap between what the doc claims the nodes do and what they actually do. This is mainly the WeMo Out node which claimed to support an input object that could set both the state and the brightness at the same time for light bulbs and groups. In actuality it only set the state. This update fixes this along with supporting setting colour and colour temperature as well if the bulb supports those capabilities.
  • The WeMo in node (event node) now includes the capability name as well as it’s code when a bulb or group changes.
  • Fixed Light groups to actually work
  • Automatically set the node name to the discovered device name to stop you having to set it via the name field in the config

The biggest change is the addition on the new WeMo Lookup node. This node queries a given device for it’s current state.

For Sockets the node sets the msg.payload to something very similar to the event node’s staus field, so 0 for off and 1 for on (and 8 for on/standby in the case of the insight socket.). For lights/light groups it outputs an object similar to the Event node, with keys for each capability the light/group has. At the moment the color field is still in X,Y values not RGB.

This makes it possible to implement flows that carry out relative changes without having to keep a permanent record of the state of the device in the context. This let’s you do fun things like this:

Powermate dimmer with Node-RED

This flow looks up the current brightness level and then increases or decreases it based on the direction the Powermate is turned and toggles the light on/off when it’s pressed.

Version 0.1.11 should have gone live on npmjs.org today.

TRÅDFRI – Ikea’s new Smart lighting system

This week Ikea announced a new set of Smart Lighting products called TRÅDFRI (translates to ‘Wire Free’).

Reading the various articles it sounds like it’s a Zigbee Light Link based system so I was interested in having a play to see if I could make the bulbs work with the WeMo kit I already have, but also given how cheap the gateway is I thought I’d grab one of those as well to see if I could work out what the network protocol is so I can write a similar Node-RED node to the existing WeMo one.

A (remarkably) short trip to the local branch and I came a way with 2 items:

A E27 Blub that can be dimmed and a remote dimmer £15

E27 LED Bulb + Dimmer
E27 LED Bulb + Dimmer

The dimmer looks very cool. It looks like it’s accelerometer based, comes with a little magnetic mount that can be stuck or screwed to the wall or it can be just stuck to the front of the fridge. If you turn it slowly it dims/brightens or if you turn it faster it turns the light on and off. You can pair each remote with up to 10 bulbs, so it can control a whole room (all be it all at once).

Bulbs on their own start at £9 and they are doing E14, GU10 formats as well as several versions of E27.

A Network Gateway that plugs into the router £25

TRÅDFRI Ethernet bridge
TRÅDFRI Ethernet bridge

The bridge is USB powered and comes with little USB power supply and a length of ethernet cable to connect it to your router.

I’m going to set the bridge up on a separate network and capture the traffic between the Android App and the bridge to see what the network traffic looks like. I’ll post again with the captured data and my progress as well as sticking it all up on github so others can build libraries for other systems, like OpenHab.

EDIT:
I’m keeping the notes on how I’m getting on with working out how to drive the gateway here

node-red-contrib-alexa-home-skill

It’s finally ready. I’ve been working on a Node-RED node to act on Amazon Alexa Home Skill directives since November last year. The skill was approved some time very early this morning and now should be available in the UK, US and Germany.

I’ll be mailing all the folks that have already signed up some time later today to let them know they can finally start using the skill, but for the rest of you here is a brief introduction (full details in earlier post).

Alexa Home Skill’s allow you to say the much more natural “Alexa, turn on the kitchen light” rather than “Alexa, ask Jeeves to turn on the kitchen light”, where “Jeeves” is the name of skill you have to remember. Some of the basic commands are:

  • Turn On/Off
  • Dim/Brighten
  • Set/Get Temperature
  • Lock/Unlock

With this node and service you can wire those commands to nearly anything you can control via Node-RED.

Node-RED - Alexa Smart Home Skill

You can install the node with the following commands:

cd ~/.node-red
npm install node-red-contrib-alexa-home-skill

Or via the Manage Palette option in the Node-RED editor.

If you have already installed this module please make sure you update to the latest version (0.1.13) to get the best support for all the voice commands.

There are detailed instructions on how to set everything up here.

Here is an example flow using the node. This turns a light on then automatically turns it off after 5mins. It uses the switch node to detect if it’s a request to turn the light on or off. When following the On branch it uses a trigger node to first send a payload of true then, 5 minutes later it sends false to the WeMo node.

On then Auto Off flow

This sort of flow would be great for a set of outside lights or maybe an electric heater. I also have some updates to the node-red-nodes-wemo package to make dimming/brightening by specific amounts easier, I’ll try and get them out by the weekend.

EDIT:
If you have problems with this node please do not post comments here, it really isn’t the best place to work issues. Open a issue on github here then it can be properly tracked.

Making the WeMo Link work with 3rd party bulbs

innr bulb
I’ve been playing with my WeMo Link device again. A couple of weekends ago I made a mistake, I used the Belkin WeMo Android app to turn a light on (rather than ask Alexa to turn it on via Node-RED). While doing this the app suggested I update the firmware on the Link device. I decided to let it do the update, this led to whole host of issues with the device not wanting to connect to the WiFi.

In the end I had to reset the device, and re-pair the bulbs. The re-pairing worked, but the Osram bulbs didn’t show up in the WeMo android app any more. It turns out the app no longer show bulbs which haven’t been “WeMo Certified”. They still show up when you query the API directly and I can control them via my Node-RED nodes but I couldn’t add the bulbs to groups. The WeMo Link supports the ZigBee Light Link protocol just like Osram Lightify, Philips Hue and the Innr lights so there is no reason why all of these things shouldn’t be able to play nicely with each other.

This sent me back to reverse engineering the SOAP API to interact with the WeMo Link.

I’ve had a pretty good go at working out the protocol already, resulting in the wemo-control.js script and the Node-RED WeMo nodes, but this is just basic discovery and control not really “admin” tasks. I decided to break this work out into it’s own script.

There are 2 main tasks that this script will have to do, add new bulbs and create groups.

Adding bulbs

This was actually pretty easy, the API end points were pretty obvious in the list.

  • OpenNetwork – allows bulbs to join the mesh
  • GetEndDevices – when used with the UNPAIRED_LIST filter it shows just the new bulbs.
  • IdentifyDevice – makes a bulb flash so you can work out which bulb is which if you discover new bulbs
  • AddDevice – adds a new bulb to the mesh
  • CloseNetwork – stops bulbs joining the mesh

None of these calls take any complex arguments and all are available either via discovery responses or other simple calls. If you chain them together in the order above you end up with your new bulbs available (at least to my scripts even if not in the WeMo app).

Creating Groups

This one was a little harder, while the API list a CreateGroup endpoint, it says that it takes as a single argument if type ReqCreateGroup which is listed to be a string. Now from experience I can guess that this string is actually a URL encoded XML fragment. There are no hints as to what this XML fragment might look like. This led to a slight diversion down a rabbit hole to set up a raspberry pi as a WiFi AccessPoint bridged on to my local network so I could run tcpdump to make sure I captured all sides of the conversation between my tablet and the WeMo Link. A little bit of formatting and collating in Wireshark and we hit pay dirt:

<?xml version="1.0" encoding="UTF-8"?>
<CreateGroup>
    <GroupID>1489757700</GroupID>
    <GroupName>Lighting Group</GroupName>
    <DeviceIDList>94103EA2B278030F,94103EA2B27803ED</DeviceIDList>
    <GroupCapabilityIDs>10008,10006,30008,30009,3000A</GroupCapabilityIDs>
    <GroupCapabilityValues>255:0,0,,,</GroupCapabilityValues>
</CreateGroup>

The full capture can be found here.

It all looks pretty self explanatory,

  • A unique id for the group (looks like it’s epoch time in seconds)
  • Name of the group (probably should be XML escaped, but we’ll keep them simple)
  • The list of device IDs to include in the group
  • The subset of capabilities that all the devices in the group support
  • Some starting values for those capabilities

Now I had the format of the messages I need to send it’s time to actually write some code. The first pass is up here, it’s a little rough and ready but I’ll try and clean it up a bit later and add a command to rename devices, but it’s Friday afternoon and I’m typing this up in a bar…

To add a bulb you would follow this flow of commands:

node bridge.js open
...
node bridge.js unpaired
...
<?xml version="1.0" encoding="utf-8"?><DeviceLists><DeviceList><DeviceListType>Unpaired</DeviceListType><DeviceInfos><DeviceInfo><DeviceIndex>5</DeviceIndex><DeviceID>00158D0001696252</DeviceID><FriendlyName>OnOff Light</FriendlyName><IconVersion>1</IconVersion><FirmwareVersion>01</FirmwareVersion><CapabilityIDs>10006,10008,30008,30009,3000A</CapabilityIDs><CurrentState>,,,,</CurrentState><Manufacturer>innr</Manufacturer><ModelCode>BY 165</ModelCode><productName>lighting</productName><WeMoCertified>NO</WeMoCertified></DeviceInfo></DeviceInfos></DeviceList></DeviceLists>
node bridge.js indentify <device id>
...
node bridge.js add <device id>
...
node bridge.js close
...

To create a new group:

node bridge.js createGroup "Group Name" "00158D0001696252,00158D0001696253" "10006,10008,30008,30009,3000A", "0,255:0,,,"

Updated Pi Zero Gadgets

Following on from my last post I’ve continued to work on improving my instructions for a USB connectable gadget based on a Raspberry Pi Zero.

Firstly I’ve got a slight improvement to the dnsmasq config I mentioned last time. This removes the dnsmasq.leases file which can cause issues if you plug the Zero into multiple computers. This can be a problem because while I had managed to fix the mac address for Host computer end of the connection the OS still pushes down the host name and it’s own unique id when it requests a DHCP address from dnsmasq, this causes dnsmasq to cycle through it’s small pool of addresses. This combined with the fact the clock on Zero is not battery backed up so only gets set correctly when it can reach internet can cause strangeness with addresses getting expired in strange ways. Anyway there is a pretty simple fix.

Adding leasefile-ro to the dnsmasq config causes it to not write lease information to disk, but rely on the dhcp-script to keep track of things. To do this I needed to add handling for a new option to the script to deal with dnsmasq wanting to read the current lease state at startup.

#!/bin/bash
op="${1:-op}"
mac="${2:-mac}"
ip="${3:-ip}"
host="${4}"

if [[ $op == "init" ]]; then
  exit 0
fi

if [[ $op == "add" ]] || [[ $op == "old" ]]; then
  route add default gw $ip usb0
fi

Now on to getting things working better with Windows machines.

To do this properly we need to move from the g_ether to the g_multi kernel module, this lets the Zero be a USB Mass Storage device, a network device (and a serial device) at the same time. This is useful because it lets me bundle .inf files that tell Windows which drivers to use on the device it’s self so it they can be installed just by plugging it in.

The first order of business is to fix the cmdline.txt to load the right module, after making the changes in the last post it looks like this:

dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait modules-load=dwc2,g_ether

The g_ether needs replacing with g_multi so it looks like this:

dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait modules-load=dwc2,g_multi

next we need to fix the options passed to the module, these are in the /etc/modprobe.d directory probably in a file called g_ether.conf. We should don’t need to, but to make it obvious when we come back to this a lot later on we’ll rename it to g_multi.conf. Now we’ve renamed it we need to add a few more options.

It currently looks like this:

options g_ether host_addr=00:dc:c8:f7:75:15 dev_addr=00:dd:dc:eb:6d:a1

It needs the g_ether needs changing to e_multi and some options adding to point to a disk image.

options g_multi file=/opt/disk.iso cdrom=y ro=y host_addr=00:dc:c8:f7:75:15 dev_addr=00:dd:dc:eb:6d:a1

Now we have the config all sorted we just need to build the disk image. First we need to create a directory called disk-image to use as the root of the file system we will share from the Zero. Then we need to get hold of the 2 .inf files, they ship with the Linux Kernel doc, but can be found online (serial port, RNDIS Ethernet).

Once we have the files place them in a directory called disk-image/drivers. We should also create a README.txt to explain what’s going on, put that in the root of disk-image. Along side that we want to create a file called Autorun.inf, this tell Windows about what’s on the “cd” when it’s inserted and where it should search for the driver definitions.

[AutoRun]
open="documentation\index.html"
icon=clock.ico,0

[DeviceInstall]
DriverPath=drivers

[Content]
MusicFiles=no
PictureFiles=no
VideoFiles=no

Full details of what can go in the Autorun.inf file can be found here, but the interesting bits are the DriverPath=drivers which points to the directory that we put the .inf files in earlier. Also the open=”documentation\index.html” which should open documentation/index.html when the device is plugged in which explains how to install the drivers. I also added an icon file so the drive shows up looking like a clock in the file manager.

That should be the bare minimum that needs to be on the image, but I ran into an issue with the g_multi module complaining the disk image being too small, to get round this I just stuck a 4mb image in the directory as well. To build the iso image run the following command:

mkisofs -o disk.iso -r -J -V "Zero Clock" disk-image

This will output a file called disk.iso that you should copy to /opt/disk.iso on the Zero (I built the image on my laptop as it’s easier to edit files and mkisofs is not installed in the default raspbian image).

This is working well on Linux and Windows, but I’m still getting errors on OSx to do with the file system. It’s not helped by the fact I don’t have a Mac to test on so I end up having to find friends that will let me stick a random but of hardware in to the side of their MacBook.

Once I’ve got the OSx bits sorted out I’ll put together script to build images for anything you want.

So now we have a Pi Zero that should look like a CD-ROM drive and a Network adapter when plugged into pretty much any machine going, it brings the driver information with it for windows, sets up a network connection with a static IP address and a Avahi/Bonjour/mDNS address to access it. I’m planning on using this to set up my Linear Clock on the local WiFi but there are all manner of interesting things that could be done with a device like this. e.g. an offline Certificate Authority, a 2FA token, a Hardware VPN solution or a Web Controllable display device.

brr