Skip to content

Instantly share code, notes, and snippets.

@Intyre
Last active October 1, 2024 08:59
Show Gist options
  • Save Intyre/2c0a8e337671ed6f523950ef08e3ca3f to your computer and use it in GitHub Desktop.
Save Intyre/2c0a8e337671ed6f523950ef08e3ca3f to your computer and use it in GitHub Desktop.
Wahoo Elemnt - Tips, tricks and custom images

Wahoo Elemnt - Tips, tricks and custom images

TOC

Firmware

The latest firmware version can be downloaded as .apk from version.json. All devices use the same BoltApp.apk. The BoltApp gets started by another application BoltLauncher and can be found on the device at /system/priv-app/BoltLauncher/BoltLauncher.apk.

Another interesting endpoint is firmware.json (bolt, bolt2, roam).

Install with ADB

adb install -r BoltApp.apk

Changelogs

BoltLauncher

Known versions of BoltLauncher are:

Device Code Name Info
ELEMNT 63 1.2.0.3 boltlauncher
BOLT2 10 1.0.10 boltlauncher2

Enable ADB

Enable ADB mode by pressing the power button twice, disconnect and reconnect the USB cable. RUN adb devices to see if it works. This method works on the ELEMNT, BOLT, BOLT2 and ROAM.

Tips and Tricks

Config backup

The .zip contains bolt-65535.cfg, comp.cfg poi.cfg and routes.cfg

Create

# create special directory
adb shell mkdir /sdcard/config_backup

# save to pc
adb pull /sdcard/config_backup/elemnt_config.zip .

Restore

# create special directory
adb shell mkdir /sdcard/config_restore

# save to pc
adb push elemnt_config.zip /sdcard/config_restore/elemnt_config.zip

# reboot device
adb shell reboot

Custom maps

Open Street Map is the source of the map data. MapsForge is used to generate the maps. wahooMapsCreator is a project with scripts to create maps.

Custom routing

Generate routing tiles with Valhalla/Mjolnir. The BOLT2 supports elevation.

Custom images

I'm not sure if this is the correct way of creating a custom image. I expect something did go wrong while creating a system image, my device has around 200MB of free space with only 1 map.

Create backup images from the device

#!/bin/sh
# backup.sh
for i in boot system recovery nvdata ; do
  adb pull /dev/block/platform/mtk-msdc.0/by-name/$i elemnt-backup-$i.img
done
tar czf elemnt-backup.tar.gz *.img

Keep the backups somewhere safe, Wahoo does NOT provide images.

Extract elemnt-backup-system.img

tar xzf elemnt-backup.tar.gz elemnt-backup-system.img

Test system image

Recreate a system image without changing anything and flash it to make sure it works.

# create a system directory
mkdir -p system

# mount the system image
sudo mount -t ext4 -o loop elemnt-backup-system.img system/

# create new image
sudo ./android_img_repack_tools/make_ext4fs -s -l 504M -a system new-system.img system/

# umount system
sudo umount system/

Now flash the image.

Modify and create a new image

# create a system directory
mkdir -p system

# mount the system image
sudo mount -t ext4 -o loop elemnt-backup-system.img system/

Disable updates

Add bolt.wahoofitness.com to the hosts file:

sudo sh -c "echo '127.0.0.1 bolt.wahoofitness.com'" >> system/etc/hosts

Decode and resign

Create keystore

The default BoltApp.apk is included in the BoltLauncher.apk. To install a modified .apk later you need to resign them both. First create a keystore to resign the .apks with.

The keytool requires you to enter a password and some information (empty information works) and asks if the data is correct. Respond with yes. Give it another password for the cert and a keystore is generated.

keytool -genkey -keystore wahoo.keystore -alias cert -keyalg RSA -keysize 2048 -validity 10000
# decode BoltLauncher with apktool
apktool d -r -s system/priv-app/BoltLauncher/BoltLauncher.apk -o BoltLauncher

# remove all files from META-INF in the BoltApp.apk
zip -d BoltLauncher/assets/BoltApp.apk META-INF/CERT.RSA META-INF/CERT.SF META-INF/MANIFEST.MF

# resign BoltApp.apk
jarsigner -sigalg MD5withRSA -digestalg SHA1 -keystore wahoo.keystore BoltLauncher/assets/BoltApp.apk cert

# remove all files from META-INF in BoltLauncher
rm -fr BoltLauncher/original/META-INF/*

# rebuild BoltLauncher
apktool b BoltLauncher -o BoltLauncher.apk

# resign BoltLauncher.apk
jarsigner -sigalg MD5withRSA -digestalg SHA1 -keystore wahoo.keystore BoltLauncher.apk cert

# copy BoltLauncher back to the system image
sudo cp BoltLauncher.apk system/priv-app/BoltLauncher/BoltLauncher.apk

Remove some apps from system/app (I removed Camera2, Gallery2, Calendar2, Calendar and Calculator) to make it stop complaining about no space left.

#!/bin/sh
# remove-apps.sh
for i in Camera2 Gallery2 Calendar2 Calendar Calculator; do
  sudo rm -fr system/app/$i
done

Create image

sudo ./android_img_repack_tools/make_ext4fs -s -l 504M -a system new-system.img system/

Umount system

sudo umount system/

Flash system image

Unlock the bootloader

Don't do this, this will void the warranty (bootloader) warranty: no. Reboot the device with adb shell reboot bootloader. The screen goes blank and fastboot shoult start, test it with sudo fastboot devices. Unlock with sudo fastboot oem unlock and press the power button on the device to start unlocking. The output should be something like:

(bootloader) Start unlock flow
OKAY [  9.707s]

Check it with sudo fastboot getvar all, Unlocked: yes. Start the device with sudo fastboot continue

fastboot mode with ADB

Run adb shell reboot bootloader and test with sudo fastboot devices.

fastboot mode with Linux

Make sure the device is turned off, run the following script as with sudo fastboot.sh and connect the USB cable.

#!/bin/sh
# fastboot.sh
echo -n "Waiting for Elemnt"
until [ -c "/dev/ttyACM0" ]; do
  echo -n "."
  sleep 1
done

echo -n 'FASTBOOT' > /dev/ttyACM0
echo " Starting fastboot mode"
sleep 2
fastboot devices
# start fastboot
adb shell reboot bootloader

# flash the image
sudo fastboot flash system new-system.img

# remove old stuff
sudo fastboot format cache
sudo fastboot format userdata

# boot the device
sudo fastboot continue

Update BoltApp.apk

Check the version number and change the url.

System image
# download BoltApp.apk
wget -O BoltApp.apk https://cdn.wahooligan.com/wahoo-fwu/elemnt/boltapp/6922/BoltApp.apk

# remove META-INF files
zip -d BoltApp.apk META-INF/CERT.RSA META-INF/CERT.SF META-INF/MANIFEST.MF

# resign BoltApp.apk
jarsigner -sigalg MD5withRSA -digestalg SHA1 -keystore wahoo.keystore BoltApp.apk cert

# copy BoltApp.apk to BoltLauncher
cp BoltApp.apk BoltLauncher/assets/BoltApp.apk

Rebuild the BoltLauncher, copy the BoltLauncher.apk to the system and build a new system image.

ADB update

# download BoltApp.apk
wget -O BoltApp.apk https://cdn.wahooligan.com/wahoo-fwu/elemnt/boltapp/6922/BoltApp.apk

# remove META-INF files
zip -d BoltApp.apk META-INF/CERT.RSA META-INF/CERT.SF META-INF/MANIFEST.MF

# resign BoltApp.apk
jarsigner -sigalg MD5withRSA -digestalg SHA1 -keystore wahoo.keystore BoltApp.apk cert

# install with adb
adb install -r BoltApp.apk

Tools used

apktool

jarsigner, keytool

  • openjdk

make_ext4fs

git clone -b android-5.1.0 https://github.com/ASdev/android_img_repack_tools
cd android_img_repack_tools
./configure
make
cd ..

Thanks

Thanks to Team Wahoo for making this device! :D Joshua, from the blog posts to make me decide to buy the Elemnt.

Workout .plan

A .plan file contains 2 sections.

Add or delete .plan files to /sdcard/plans and resync the workouts.

Header

The section starts with =HEADER=.

Key Description
NAME Name of workout
DESCRIPTION Description for workout
DURATION Total duration in seconds
SCHEDULED Scheduled date in yyyyMMdd format
FTP_TEST_FACTOR ?
=HEADER=
NAME=Name 
DESCRIPTION=Description
# Duration: 5 mins
DURATION=300
SCHEDULED=20200428
FTP_TEST_FACTOR=0.90

Stream

The section starts with =STREAM=. The stream section has the interval data.

=STREAM=

=INTERVAL=
=INTERVAL=
=INTERVAL=
ect...

Interval / Subinterval

A interval starts with =INTERVAL=. Inside a interval section you can add one or more =SUBINTERVAL= sections to group them.

Key Description
INTERVAL_NAME Name of interval
INTERVAL_DESCRIPTION Description for interval
REPEAT Repeats the interval and/or subintervals x times
CAD_LO Cadence Low target
CAD_HI Cadence High target
HR_LO Hearth rate Low target
HR_HI Hearth rate High target
PWR_LO Power Low target
PWR_HI Power High target
SPD_LO Speed Low target
SPD_HI Speed High target
RPE_LO Rating of perceived exertion Low target
RPE_HI Rating of perceived exertion High target
PERCENT_FTP_LO FTP Low target
PERCENT_FTP_HI FTP High target
LEVEL Kickr Level
ERG Kickr ERG
=INTERVAL=
INTERVAL_NAME=Name
INTERVAL_DESCRIPTION=Description
REPEAT=2
CAD_LO=80
CAD_HI=100
HR_LO=130
HR_HI=140
PWR_LO=200
PWR_HI=220
SPD_LO=30
SPD_HI=40
RPE_LO=6
RPE_HI=20
PERCENT_FTP_LO=60
PERCENT_FTP_HI=60
# Duration: 5 mins
MESG_DURATION_SEC>=300?EXIT
MESG_DISTANCE_M>=1000?EXIT

There are two ways to set the interval length. Include it with the interval.

Key Description
MESG_DURATION_SEC Interval duration
MESG_DISTANCE_M Interval distance

Trick from INEOS_Climbing.v1.plan

=INTERVAL=
INTERVAL_NAME=CLIMB
# 15 mins @ 75-90%
# 10 second spike to 125% every 3 mins
REPEAT=4
PERCENT_FTP_LO=75
PERCENT_FTP_HI=90
MESG_DURATION_SEC>=180?EXIT
MESG_DURATION_SEC>=170?PERCENT_FTP_LO=125
MESG_DURATION_SEC>=170?PERCENT_FTP_HI=125
@kwaaak
Copy link

kwaaak commented Jul 9, 2023

That is a deliberate choice by the Wahoo team, arguing that the ANT+ connections are better

@pqfranky
Copy link

pqfranky commented Jul 9, 2023

Hi @kwaaak ,
Thanks for your reply, but technically and in the real world, bluetooth can do data re-transmission and have better anti-interference ability, which is much better than ANT+.
So I still want to know if is there any way to add the power meter only via bluetooth.
By the way, I have tried many ways on Reddit, but they are not working.

@Intyre
Copy link
Author

Intyre commented Jul 11, 2023

@pqfranky

It looks like it can be forced to Bluetooth by creating a file with the name cfg_BtlePM on the device.
ConnectionParamsSet.setForcePMBtle(StdFileManager.isSdCfgFileExists("cfg_BtlePM"));

if (ConnectionParamsSet.sForcePMBtle.get() && productType.isPowerMeter()) {
  Log.d("ConnectionParamsSet", "getPreferredConnectionParams using BTLE for", productType);
  return b(connectionParams4);
}

@newzealandpaul
Copy link

@Intyre you are a genius for finding that!

@pqfranky you are better off with ANT+. While BLE is a more modern protocol, retransmission is not useful for streaming data, if a packet is lost it should be discarded. BLE chipsets usually allow the queue size to be defined, and I would expect most devices to have a very low queue or a queue of 1, meaning packets will drop if they get more than X behind.

Most devices have a BLE connection limit, while ANT+ is a broadcast protocol and any number of devices can work with it.

BLE might be longer range, but in most cases the range you are wanting is going to be 100cm or less, which is very close.

@Intyre
Copy link
Author

Intyre commented Jul 13, 2023

@newzealandpaul you can find things too! There are versions that have less/no obfuscation and is a good way to find out how stuff works, like 10643 or 14848. This should get you started if you have no idea where to begin. To make it even more easier use IntelliJ IDEA or Android Studio to look at the decompiled code.

To match classes and method with newer obfuscated versions the log messages can be used. For example: Log.d("ConnectionParamsSet", "getPreferredConnectionParams using BTLE for", productType);
Class name: ConnectionParamsSet
Method name: getPreferredConnectionParams

@jeterke
Copy link

jeterke commented Jul 23, 2023

Hello, Latest version (WD12-16138) does not show the .plan workouts I added to /plans, so I can no longer use custom workouts in that format. Placing .msc files still work, but that's is not the best format. Have you found a way to reenable .plan files other than downgrading? Thanks, Juan

Same problem here. Indeed .mrc files still work. But .plan are no longer visible. This is very annoying. Is there anyone than knows a trick for this?

@Intyre
Copy link
Author

Intyre commented Jul 25, 2023

@pinazo @jeterke Maybe there is something in the logs? Create a empty file with the name cfg_StdCfgManager_DisableVerboseLogLevelOverrideForDebug on the device and look for StdPlanProviderSdFolder in the log file (in the logs directory) or with adb logcat.

My device is still running a older version and have not looked at 16138 or used that feature myself. In 16137 it is still checking for .plan files.

if (file.getName().endsWith(".plan")) {
	Log.g0("StdPlanProviderSdFolder", "sync copying", file, file3);
	FileHelper.copy(file, file3);
	s4 = file.getName().replaceAll("\\.plan$", "").replaceAll("\\.PLAN", "");
} else if (CruxPlanConverterErgMrc.isFormat(file)) {
	Log.f0("StdPlanProviderSdFolder", "sync convert erg/mrc to plan", file.getName());
	final CruxPlanErgMrcConversionResult convertToPlan = CruxPlanConverterErgMrc.convertToPlan(file, file3);
	Log.l0("StdPlanProviderSdFolder", convertToPlan.success(), "sync convert erg/mrc to plan", file.getName(), convertToPlan);
	s4 = file.getName().replaceAll("\\.erg$", "").replaceAll("\\.ERG$", "").replaceAll("\\.mrg$", "").replaceAll("\\.MRC$", "");
} else if (file.getName().endsWith(".fit")) {
	Log.f0("StdPlanProviderSdFolder", "sync convert fit to plan", file.getName());
	final CruxPlanFitConversionResult convertToPlan2 = CruxPlanConverterFit.convertToPlan(file, file3);
	Log.l0("StdPlanProviderSdFolder", convertToPlan2.success(), "sync convert fit to plan", file.getName(), convertToPlan2);
	s4 = file.getName().replaceAll("\\.fit$", "").replaceAll("\\.FIT$", "");
} else {
	s4 = null;
}

if (file3.isFile() && s4 != null) {
	return new f(new CruxPlanId(3, s4), file3);
}
Log.t("StdPlanProviderSdFolder", "checkImportSdPlan convert FAILED", file3);
return null;

@xlaga
Copy link

xlaga commented Sep 11, 2023

@pinazo, @jeterke, and anyone else having issues with plan files that used to work. I contacted Wahoo support and sent them my plan files, they said I needed to include the following in the = HEADER = section

# LOCATION TYPE OUTDOOR
LOCATION_TYPE=1

# WORKOUT TYPE GROUP BIKE
WORKOUT_TYPE=0

That fixed it for me. @Intyre might be worth adding to the Workout .plan section.

@jeterke
Copy link

jeterke commented Sep 13, 2023

@xlaga amazing... the 2 extra parameters in the HEADER section indeed fixes it for me as well!
I never got that far with Wahoo support. So thanks a million for sharing this fix!

@xlaga
Copy link

xlaga commented Sep 15, 2023

@jeterke You're welcome! I was a little surprised how far Wahoo support went to help figure out the issue.

@Intyre
Copy link
Author

Intyre commented Sep 20, 2023

@xlaga The more info you provide the easier it is for the developers to reproduce the issue.
In the BoltApp.apk you can find the default Wahoo .plan files in the assets/plans/ directory. Unzip BoltApp.apk to get the files.

LOCATION TYPE

Nr Type
0 INDOOR
1 OUTDOOR

WORKOUT TYPE

Nr Type
0 BIKING
1 RUNNING
2 SWIMMING
3 WATER_SPORTS
4 SNOW_SPORTS
5 SKATING
6 GYM
7 OTHER
8 ELLIPTICAL
9 WALKING

@jeer00
Copy link

jeer00 commented Dec 10, 2023

I have a Bolt Stealth edition. Havent been able to get authorized in adb. I have tried the steps in this article, combined with some other combinations. The "furthest" i come is by starting with the device off, pressing the lef key and the power button, releasing the power button and then pressing it again.

This gives: "APK ERROR" and "safe mode". No choises, and does not give adb-authorization.

Does anyone gotten adb-access on Bolt Stealth?

@t0ffi
Copy link

t0ffi commented Apr 6, 2024

If there is possibility to flash image from v2 to v1 Roam. Recently they released new firmware with ant+ light control and it is only for v2.

@Yuki-zik
Copy link

Yuki-zik commented Jun 7, 2024

I removed the original boltapk and adjusted the logos of roam1 and roam2 in the code. I implemented a system using roam2 in roam1, but the only difficulty is that I cannot use Bluetooth after the update, and my mobile phone cannot be paired.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment