Track Your Heartrate on Raspberry Pi with Ant+Using the Suunto Movestick Mini and Garmin Soft Strap Heart Rate Monitor

This blog post is inspired by the excellent blog post “Monitoring your developer health with an ANT+ sensor” by Tom Wardill. His code works great and this blog does nothing to improve the code. Instead I focus on getting the stick to run on Raspberry Pi.

What you’ll need

Image Description
A Raspberry Pi with power supply and SD card. I’m using Raspbian as the operating system.
The Suunto Pods Movestick Mini, SS016591000, which serves as the Ant+ receiver.
The Garmin Premium Soft Strap Heart Rate Monitor, which serves as the Ant+ sender.

Step 1: Hardware

Log in to the Raspberry Pi, then insert the Movestick mini. Check that the stick is recognized by running

pi@skyemittari ~ $ dmesg | tail
[ 2430.589870] usb 1-1.3: new full-speed USB device number 4 using dwc_otg
[ 2430.698954] usb 1-1.3: New USB device found, idVendor=0fcf, idProduct=1008
[ 2430.698992] usb 1-1.3: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[ 2430.699012] usb 1-1.3: Product: Movestick mini
[ 2430.699029] usb 1-1.3: Manufacturer: Suunto
[ 2430.699047] usb 1-1.3: SerialNumber: 1339803961

You should see entries for the Suunto Movestick mini and the corresponding vendor id and product id. For the Movestick mini they should always be:

  • idVendor: 0fcf
  • idProduct: 1008

You can also get those ids by running lsusb. The Movestick mini appears as Dynastream Innovations, Inc.. The four digits after ID are the vendor id, the four digits after that the product id:

pi@skyemittari ~ $ lsusb
Bus 001 Device 002: ID 0424:9512 Standard Microsystems Corp. 
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. 
Bus 001 Device 004: ID 0fcf:1008 Dynastream Innovations, Inc.

To get the usb serial kernel driver to create a node for our Movestick mini we need to create an udev rule. First unplug the Movestick then create the following file:

pi@skyemittari ~ $ sudo vi /etc/udev/rules.d/garmin-ant2.rules

with the following content (all one one line):

SUBSYSTEM=="usb", ATTRS{idVendor}=="0fcf", ATTRS{idProduct}=="1008", RUN+="/sbin/modprobe usbserial vendor=0x0fcf product=0x1008", MODE="0666", OWNER="pi", GROUP="root"

Next reinsert the Movestick mini. Now you should have a /dev/ttyUSB0 node:

pi@skyemittari ~ $ ls /dev/ttyUSB0
/dev/ttyUSB0

This is all you need to do to prepare the Movestick mini.

Step 2: Software

I’m using Python-Ant to read the heart rate. First clone python-ant (see the Appendix why I’m using a fork of python-ant instead of the trunk repository):

pi@skyemittari ~ $ git clone https://github.com/baderj/python-ant.git
Cloning into 'python-ant'...
remote: Reusing existing pack: 444, done.
remote: Counting objects: 31, done.
remote: Compressing objects: 100% (21/21), done.
remote: Total 475 (delta 10), reused 20 (delta 4)
Receiving objects: 100% (475/475), 92.16 KiB, done.
Resolving deltas: 100% (224/224), done.

Next install python-setuptools and with that python-ant:

pi@skyemittari ~ $ sudo apt-get install -y python-setuptools
pi@skyemittari ~ $ cd python-ant/
pi@skyemittari ~/python-ant $ sudo python setup.py install

To test the code use the following small Python snippet:

"""
    Code based on:

https://github.com/mvillalba/python-ant/blob/develop/demos/ant.core/03-basicchannel.py

    in the python-ant repository and

https://github.com/tomwardill/developerhealth

    by Tom Wardill
"""
import sys
import time
from ant.core import driver, node, event, message, log
from ant.core.constants import CHANNEL_TYPE_TWOWAY_RECEIVE, TIMEOUT_NEVER

class HRM(event.EventCallback):

    def __init__(self, serial, netkey):
        self.serial = serial
        self.netkey = netkey
        self.antnode = None
        self.channel = None

    def start(self):
        print("starting node")
        self._start_antnode()
        self._setup_channel()
        self.channel.registerCallback(self)
        print("start listening for hr events")

    def stop(self):
        if self.channel:
            self.channel.close()
            self.channel.unassign()
        if self.antnode:
            self.antnode.stop()

    def __enter__(self):
        return self

    def __exit__(self, type_, value, traceback):
        self.stop()

    def _start_antnode(self):
        stick = driver.USB2Driver(self.serial)
        self.antnode = node.Node(stick)
        self.antnode.start()

    def _setup_channel(self):
        key = node.NetworkKey('N:ANT+', self.netkey)
        self.antnode.setNetworkKey(0, key)
        self.channel = self.antnode.getFreeChannel()
        self.channel.name = 'C:HRM'
        self.channel.assign('N:ANT+', CHANNEL_TYPE_TWOWAY_RECEIVE)
        self.channel.setID(120, 0, 0)
        self.channel.setSearchTimeout(TIMEOUT_NEVER)
        self.channel.setPeriod(8070)
        self.channel.setFrequency(57)
        self.channel.open()

    def process(self, msg):
        if isinstance(msg, message.ChannelBroadcastDataMessage):
            print("heart rate is {}".format(ord(msg.payload[-1])))

SERIAL = '/dev/ttyUSB0'
NETKEY = 'B9A521FBBD72C345'.decode('hex')

with HRM(serial=SERIAL, netkey=NETKEY) as hrm:
    hrm.start()
    while True:
        try:
            time.sleep(1)
        except KeyboardInterrupt:
            sys.exit(0)

You can find the code on GitHub. Clone it, then run it with python garmin_ant_demo.py:

pi@skyemittari ~ $ ~
pi@skyemittari ~ $ git clone https://gist.github.com/c41d2bbe0aeded3506cf.git hrmdemo
Cloning into 'hrmdemo'...
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
pi@skyemittari ~ $ cd hrmdemo/
pi@skyemittari ~/hrmdemo $ python garmin_ant_demo.py
starting node
start listening for hr events
heart rate is 69
heart rate is 69
heart rate is 69
heart rate is 70
heart rate is 70

You can stop the script with Ctrl+C; it should end gracefully. Sometimes the python-ant can’t connect to the Movestick and the script stalls at starting node. In this case unplug and reinsert the Movestick and retry.

Appendix: Why use a fork of Python-Ant?

Unfortunately, the current state of the python-ant library has a few minor problems with the Ant Movestick mini. One of those is that you’ll probably see the following error message:

AttributeError: 'Interface' object has no attribute 'AlternateSetting'

Tom Wardill patched those problems in his fork of python-ant. His code worked well for me, I often got the following exception though:

Traceback (most recent call last):
  File "hrm.py", line 34, in <module>
    antnode.start()
  File "/usr/local/lib/python2.7/dist-packages/ant-develop-py2.7.egg/ant/core/node.py", line 158, in start
    self.driver.open()
  File "/usr/local/lib/python2.7/dist-packages/ant-develop-py2.7.egg/ant/core/driver.py", line 61, in open
    self._open()
  File "/usr/local/lib/python2.7/dist-packages/ant-develop-py2.7.egg/ant/core/driver.py", line 191, in _open
    dev.set_configuration()
  File "/usr/local/lib/python2.7/dist-packages/pyusb-1.0.0b1-py2.7.egg/usb/core.py", line 559, in set_configuration
    self._ctx.managed_set_configuration(self, configuration)
  File "/usr/local/lib/python2.7/dist-packages/pyusb-1.0.0b1-py2.7.egg/usb/core.py", line 92, in managed_set_configuration
    self.backend.set_configuration(self.handle, cfg.bConfigurationValue)
  File "/usr/local/lib/python2.7/dist-packages/pyusb-1.0.0b1-py2.7.egg/usb/backend/libusb1.py", line 741, in set_configuration
    _check(self.lib.libusb_set_configuration(dev_handle.handle, config_value))
  File "/usr/local/lib/python2.7/dist-packages/pyusb-1.0.0b1-py2.7.egg/usb/backend/libusb1.py", line 571, in _check
    raise USBError(_str_error[ret], ret, _libusb_errno[ret])
usb.core.USBError: [Errno 16] Resource busy

The exception is no longer thrown if you detach the kernel driver if already attached. To this end I changed USB2Driver in python-ant/src/ant/core/driver.py:

if dev is None:
raise DriverError('Could not open device (not found)')
+
+ # make sure the kernel driver is not active
+ if dev.is_kernel_driver_active(0):
+ try:
+ dev.detach_kernel_driver(0)
+ except usb.core.USBError as e:
+ sys.exit("could not detach kernel driver: {}".format(e))
+
dev.set_configuration()
cfg = dev.get_active_configuration()
comments powered by Disqus