When a Garmin device is plugged into a USB port, PiFitSync is supposed to
detect it and copy the FIT files to the ~/FitSync folder. The fit-sync-usb
is used to copy the files, but detecting the USB insertion, mounting the drive
and starting the utility is surprisingly complex and involves a lot of files
and services.
The sections below document this process, mostly for the authors benefit when he will have to look at this code again in a few years time. If you know a simpler way, the author of this package would like to know it.
Most newer Garmin devices appear as MTP devices and the steps below are used
to detect the device insertion into one of the Raspberry PI's USB ports and
start fit-sync-usb
The UDEV rules relevant to MTP device mounting/unmounting in
99-fit-sync.rules
are:
SUBSYSTEM=="usb", ACTION=="add", ATTR{interface}=="MTP", RUN+="/usr/local/libexec/fit-sync/on-mtp-added %p"
SUBSYSTEM=="usb", ACTION=="remove", RUN+="/usr/local/libexec/fit-sync/on-mtp-removed %k"
And they launch two scripts, on-mtp-added
when a device is added and
on-mtp-removed
when the device is removed.
An udev rule is used to detect when a MTP device from the "usb" subsusystem is
added, to call on-mtp-added
:
SUBSYSTEM=="usb", ACTION=="add", ATTR{interface}=="MTP", RUN+="/usr/local/libexec/on-mtp-added %p"
on-mtp-added
receives the device path (sys
has to be prepended to it), it
will than:
-
determine the parent device (this is just the parent folder in the tree). The UDEV rule matches against the MTP interface, but the actual device is the parent -- this parent device is simply the parent folder.
-
determine the vendor id and product id from the
idVendor
andidProduct
files in the parent device folder, and check them against the desired device for a FR945 (script will exit if it does not) -
determine the bus and device numbers (contents of the
busnum
anddevnum
files) -- this information is needed by thejmtpfs
utility to mount the correct MTP device. This information is saved into/var/run/fit-sync/mtp-device
, and will be used by the device mount script,mount-fr945
-
determine the kernel device name -- this is the folder name of the parent device. This will be saved in
/var/run/fit-sync/mtp-kname
and will be used byon-mtp-removed
script to determine if our device was removed and the drive needs to be unmounted. -
start the "mount-fr945.service" using
systemctl start mount-fr945.service
-- thejmtpfs
utility needs to keep running while the drive is mounted and udev scripts neeed to run in a short amount of time so we cannot launch servers directly from here. -
start the "sync-fr945.service" using
systemctl start sync-fr945.service
-- this will copy the FIT files into ~/FitSync to be shared over the network. This service has a dependency listed on themount-fr945.service
so, systemd will make sure the start is delayed until the file system is mounted.
There is no remove action against the MTP device directly, so we add a rule
against any USB device and call on-mtp-removed
script to figure out the
rest:
SUBSYSTEM=="usb", ACTION=="remove", RUN+="/usr/local/libexec/on-mtp-removed %k"
on-mtp-removed
receives the kernel device name for the device that was
removed, it will than:
-
verify that the kernel name is the same as the one saved in
/var/run/fit-sync/mtp-kname
-
if it is, stop the "mount-fr945.service" using
systemctl stop mount-fr945.service
Mounting (and unmounting) the MTP device is done using a systemd service.
This is required, because the jmtpfs
utility needs to be running while the
MTP device is mounted, so it is really a "service". The contents of the
service file are as follows:
; -*- mode: conf -*-
[Unit]
Description=Mount the fr945 MTP device
[Service]
Type=forking
ExecStart=@@BINDIR@@/mount-fr945
ExecStop=/usr/bin/fusermount -u /media/fr945
Restart=no
WorkingDirectory=/
User=root
Group=root
In particular, the service is setup as forking (this ensures the system is
considered started only after the file system is mounted). Also, rather than
calling jmtpfs
directly, the mount-fr945
shell script is used. This
script will read the device from /var/run/fit-sync/mtp-device
and ensures
that the correct MTP device is mounted.
The service is set up to call fusermount
to unmount the drive when the
service is deactivated.
The fit-sync-usb
utility is used to copy FIT files from the FR945 to the
~/FitSync
folder, which is share over the network. This is done using
another systemd service, sync-fr945.service
.
; -*- mode: conf -*-
[Unit]
Description=Download FIT files from a FR945
After=fit-sync-setup.service
Requires=mount-fr945.service
After=mount-fr945.service
[Service]
Type=forking
PIDFile=/run/fit-sync/fit-sync-fr945.pid
ExecStart=@@BINDIR@@/fit-sync-usb -p /run/fit-sync/fit-sync-fr945.pid -d /media/fr945/Primary/GARMIN
Restart=no
WorkingDirectory=/media/fr945/Primary/GARMIN
User=@@USER@@
Group=@@GROUP@@
Previous generation Garmin devices show up as USB drives when plugged in and
the steps below are used to mount these drives and start up fit-sync-usb
The whole process is controlled by UDEV rules which trigger when a new
"sd[a-z]" device shows up. blkid
is used to get the drive label and, if the
label is "GARMIN", the device is mounted using /bin/mount
and the
fit-sync-usb.service
is started. Note that mounting is done by starting a
separate service ([email protected]
), since mounting file systems are
"lost" since UDEV runs the processes in as separate namespace.
The rules also arrange for an unmount command to be used when the device is removed.
KERNEL=="sd[a-z]", GOTO="start_automount"
KERNEL=="sd[a-z][0-9]", GOTO="start_automount"
GOTO="end_automount"
LABEL="start_automount"
IMPORT{program}="/sbin/blkid -o udev -p /dev/%k"
ENV{ID_FS_LABEL}=="GARMIN", GOTO="garmin_automount"
GOTO="end_automount"
### Mount a Garmin USB device on /media/garmin and start synching files
LABEL="garmin_automount"
ACTION=="add", RUN+="/bin/systemctl start mount-garmin@%k.service"
ACTION=="add", RUN+="/bin/systemctl start fit-sync-usb.service"
ACTION=="add", RUN+="/bin/systemctl start fit-sync-epo.service"
# Run umount when the device is removed (this will only useful if the device
# is prematurely removed.)
ACTION=="remove", RUN+="/bin/systemctl stop mount-garmin@%k.service"
GOTO="end_automount"
LABEL="end_automount"
The [email protected]
is a service that mounts the garmin device -- we
cannot run mount directly from an udev rule, since this process is run in a
separate namespace and the mount is not visible to the "outside" world.
The fit-sync-usb.service
simply launches fit-sync-usb
-- this is needed
since programs run as part of an udev rule must complete quickly and udev ends
up killing the process otherwise.
There is also a fit-sync-epo.service
which runs a python script to download
EPO data to the device -- this is only needed if you don't have the device
paired up with your smart phone, which already downloads that data.
Connect the FR945 and check /var/log/syslog
for the device name, should be
something like,
/sys/devices/platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/1-1.2/1-1.2:1.0
,
check with ls
that there are no subdirectories -- you want the deepest path.
Run the following command to print out the attributes for each level of the
tree.
udevadm info --attribute-walk --path=$DEVPATH
Depending on how the UDEV rules are setup, they may match multiple times in the tree (e.g. if matching only on the "idVendor" and "idProduct" attributes). The command below can be used to determine what will be run for a specific device -- note that this can be misleading, since you have to specify the device, while the udev rules will run on all devices and the rule you wrote had multiple matches...
udevadm test $DEVPATH