Track Your Heartrate on Raspberry Pi with Ant+
Using the Suunto Movestick Mini and Garmin Soft Strap Heart Rate MonitorThis 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
: 0fcfidProduct
: 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()
Archived Comments
Note: I removed the Disqus integration in an effort to cut down on bloat. The following comments were retrieved with the export functionality of Disqus. If you have comments, please reach out to me by Twitter or email.