Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9987b4a
library added
cagnulein Jan 20, 2025
8755219
Update build.gradle
cagnulein Jan 21, 2025
524b1ae
Update build.gradle
cagnulein Jan 21, 2025
b221a7e
Update build.gradle
cagnulein Jan 22, 2025
c5c2ad2
Merge branch 'master' into Add-Waterrower-USB-connection-Support-(Iss…
cagnulein Aug 1, 2025
fd81e5b
adding most of the code
cagnulein Aug 1, 2025
90b362e
Update WaterRowerBridge.java
cagnulein Aug 1, 2025
aa3b147
Update WaterRowerBridge.java
cagnulein Aug 1, 2025
67befe2
fix logging
cagnulein Aug 18, 2025
5fe4a2e
Merge branch 'master' into Add-Waterrower-USB-connection-Support-(Iss…
cagnulein Aug 18, 2025
66f1a39
Update device_filter.xml
cagnulein Aug 19, 2025
5fec5bb
Refactor WaterRower USB connection initialization
cagnulein Dec 3, 2025
c33eccf
Merge branch 'master' into Add-Waterrower-USB-connection-Support-(Iss…
cagnulein Dec 3, 2025
219b927
Merge remote-tracking branch 'origin/master' into codex/pr3068-update…
cagnulein Jun 27, 2026
9dbbd3d
Retry WaterRower USB device detection periodically
cagnulein Jul 1, 2026
03eafc1
Merge branch 'master' into Add-Waterrower-USB-connection-Support-(Iss…
cagnulein Jul 1, 2026
c52d64e
Fix WaterRower USB connection using usb-serial-for-android instead of…
cagnulein Jul 1, 2026
1b93ebb
Fix WaterRower stroke rate always reporting 0
cagnulein Jul 2, 2026
1647ec5
Fix WaterRower USB workout timer never advancing
cagnulein Jul 2, 2026
e9f04f8
Fix WaterRower USB device lookup
cagnulein Jul 2, 2026
9a7e529
Fix WaterRower USB distance and watts
cagnulein Jul 2, 2026
82005ec
Fix WaterRower USB stroke count and ANT distance
cagnulein Jul 3, 2026
7d0fe0f
Normalize ANT distance for WaterRower USB
cagnulein Jul 3, 2026
ad4c384
Feed ANT speed channel distance from QZ
cagnulein Jul 3, 2026
7f707f4
Skip wizard for WaterRower USB setting
cagnulein Jul 3, 2026
bb48cb3
Fix ANT FE rower broadcast data
cagnulein Jul 3, 2026
00b8c74
Smooth WaterRower USB speed
cagnulein Jul 8, 2026
54a722b
Merge remote-tracking branch 'origin/master' into Add-Waterrower-USB-…
cagnulein Jul 8, 2026
a53a95d
Fix WaterRower USB startup lifecycle
cagnulein Jul 10, 2026
5ead47d
Merge remote-tracking branch 'origin/master' into Add-Waterrower-USB-…
cagnulein Jul 10, 2026
0a3f4ff
Fix indentation and reorder property declarations
cagnulein Jul 20, 2026
1ff528a
Remove keepalive for iOS lockscreen
cagnulein Jul 20, 2026
2c5c972
Merge branch 'master' into Add-Waterrower-USB-connection-Support-(Iss…
cagnulein Jul 20, 2026
fa085e3
Update main.yml
cagnulein Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ def qtAndroidJavaSrcDir = qtAndroidJavaRootDir ? qtAndroidJavaRootDir + '/src' :
def qtAndroidJavaResDir = qtAndroidJavaRootDir ? qtAndroidJavaRootDir + '/res' : 'qt-android-missing/src/android/java/res'

dependencies {
implementation 'io.netty:netty-all:4.1.100.Final'
implementation 'com.google.guava:guava:32.1.3-android'
implementation 'com.fazecast:jSerialComm:2.10.4'
implementation 'org.slf4j:slf4j-api:2.0.9'
implementation 'org.slf4j:slf4j-simple:2.0.9'
if (qtAndroidJarDir) {
implementation fileTree(dir: qtAndroidJarDir, include: ['*.jar'])
}
Expand Down Expand Up @@ -161,6 +166,21 @@ android {
// Qt 5.15's QtLoader.java finds libraries via nativeLibraryDir on the
// filesystem, not from inside the APK, so we must force extraction.
packaging {
resources {
excludes += [
'META-INF/io.netty.versions.properties',
'META-INF/INDEX.LIST',
'META-INF/DEPENDENCIES',
'META-INF/LICENSE',
'META-INF/LICENSE.txt',
'META-INF/license.txt',
'META-INF/NOTICE',
'META-INF/NOTICE.txt',
'META-INF/notice.txt',
'META-INF/ASL2.0',
'META-INF/*.kotlin_module'
]
}
jniLibs {
useLegacyPackaging = true
}
Expand Down
5 changes: 5 additions & 0 deletions src/android/res/xml/device_filter.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
<usb-device vendor-id="17A4" product-id="0002" />
<usb-device vendor-id="17A4" product-id="0001" />

<!-- WaterRower devices - Microchip Technology Inc. -->
<usb-device vendor-id="1240" product-id="10" /> <!-- 0x04D8 / 0x000A: MCP2200 USB-to-UART -->
<usb-device vendor-id="1240" product-id="223" /> <!-- 0x04D8 / 0x00DF: WaterRower Custom Application -->
<usb-device vendor-id="1240" /> <!-- 0x04D8 / ......: All Microchip Technology devices -->

<!-- CDC driver -->
<usb-device vendor-id="9025" /> <!-- 0x2341 / ......: Arduino -->
<usb-device vendor-id="5824" product-id="1155" /> <!-- 0x16C0 / 0x0483: Teensyduino -->
Expand Down
4 changes: 2 additions & 2 deletions src/android/src/Ant.java
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,12 @@ public boolean isBikeConnected() {

public void updateBikeTransmitterExtendedMetrics(long distanceMeters, int heartRate,
double elapsedTimeSeconds, int resistance,
double inclination) {
double inclination, int equipmentType, int strokeCount) {
if(mChannelService == null)
return;
QLog.v(TAG, "updateBikeTransmitterExtendedMetrics");
mChannelService.updateBikeTransmitterExtendedMetrics(distanceMeters, heartRate,
elapsedTimeSeconds, resistance,
inclination);
inclination, equipmentType, strokeCount);
}
}
107 changes: 80 additions & 27 deletions src/android/src/BikeTransmitterController.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,14 @@ public class BikeTransmitterController {
private static final byte DATA_PAGE_GENERAL_FE = 0x10;
private static final byte DATA_PAGE_BIKE_DATA = 0x19;
private static final byte DATA_PAGE_TRAINER_DATA = 0x1A;
private static final byte DATA_PAGE_ROWER_DATA = 0x16;
private static final byte DATA_PAGE_GENERAL_SETTINGS = 0x11;
private static final int EQUIPMENT_TYPE_ROWER = 0x16;
private static final int EQUIPMENT_TYPE_TRAINER = 0x19;
private static final int FE_STATE_READY = 0x02;
private static final int FE_STATE_IN_USE = 0x03;
private static final int PAGE16_CAP_DISTANCE_ENABLED = 0x04;
private static final int PAGE22_CAP_STROKE_COUNT_ENABLED = 0x01;

private static Random randGen = new Random();

Expand All @@ -64,6 +71,8 @@ public class BikeTransmitterController {
int currentPower = 0; // Current power in watts
double currentSpeedKph = 0.0; // Current speed in km/h
long totalDistance = 0; // Total distance in meters
int equipmentType = EQUIPMENT_TYPE_TRAINER; // ANT+ FE equipment type
int strokeCount = 0; // Accumulated rower stroke count
int currentHeartRate = 0; // Heart rate in BPM
double elapsedTimeSeconds = 0.0; // Elapsed time in seconds
int currentResistance = 0; // Current resistance level (0-100)
Expand Down Expand Up @@ -210,6 +219,14 @@ public void setDistance(long distance) {
this.totalDistance = Math.max(0, distance);
}

public void setEquipmentType(int equipmentType) {
this.equipmentType = Math.max(0, Math.min(255, equipmentType));
}

public void setStrokeCount(int strokeCount) {
this.strokeCount = Math.max(0, strokeCount);
}

public void setHeartRate(int heartRate) {
this.currentHeartRate = Math.max(0, Math.min(255, heartRate));
}
Expand Down Expand Up @@ -255,9 +272,9 @@ public String getTransmissionInfo() {
}

return String.format("Transmission: ACTIVE - Cadence: %drpm, Power: %dW, " +
"Speed: %.1fkm/h, Resistance: %d, Inclination: %.1f%%",
"Speed: %.1fkm/h, Resistance: %d, Inclination: %.1f%%, Equipment: 0x%02X",
currentCadence, currentPower, currentSpeedKph,
currentResistance, currentInclination);
currentResistance, currentInclination, equipmentType);
}

/**
Expand Down Expand Up @@ -311,20 +328,13 @@ public void run() {
cnt += 1;

// Cycle through different data pages like PowerChannelController
if (cnt % 5 == 0) {
// General FE Data Page (0x10)
if (cnt % 5 == 0) {
debugString = buildGeneralFEDataPage(payload);
} else if (cnt % 5 == 1) {
// Bike Data Page (0x19)
debugString = buildBikeDataPage(payload);
} else if (cnt % 5 == 2) {
// Trainer Data Page (0x1A)
debugString = buildBikeDataPage(payload);
} else if (cnt % 5 == 1 || cnt % 5 == 2) {
debugString = buildEquipmentSpecificDataPage(payload);
} else if (cnt % 5 == 3) {
// General Settings Page (0x11)
debugString = buildGeneralSettingsPage(payload);
} else {
// Default General FE Data Page (0x10)
debugString = buildGeneralFEDataPage(payload);
}

Expand Down Expand Up @@ -367,21 +377,15 @@ public void run() {
cnt += 1;
String debugString = "";

// Cycle through different data pages like PowerChannelController
if (cnt % 16 == 1) {
// General FE Data Page (0x10)
debugString = buildGeneralFEDataPage(payload);
} else if (cnt % 16 == 5) {
// Bike Data Page (0x19)
debugString = buildBikeDataPage(payload);
debugString = buildEquipmentSpecificDataPage(payload);
} else if (cnt % 16 == 9) {
// Trainer Data Page (0x1A)
debugString = buildBikeDataPage(payload);
debugString = buildEquipmentSpecificDataPage(payload);
} else if (cnt % 16 == 13) {
// General Settings Page (0x11)
debugString = buildGeneralSettingsPage(payload);
} else {
// Default General FE Data Page (0x10)
debugString = buildGeneralFEDataPage(payload);
}

Expand Down Expand Up @@ -439,7 +443,7 @@ private String buildGeneralFEDataPage(byte[] payload) {
payload[0] = 0x10; // Data Page Number = 0x10 (Page 16)

// Byte 1: Equipment Type Bit Field (Refer to Table 8-8)
payload[1] = 0x19; // Equipment type: Bike (stationary bike = 0x19)
payload[1] = (byte) equipmentType;

// Byte 2: Elapsed Time (0.25 seconds resolution, rollover at 64s)
int elapsedTime025s = (int) (elapsedTimeSeconds * 4) & 0xFF;
Expand All @@ -458,13 +462,13 @@ private String buildGeneralFEDataPage(byte[] payload) {
// Byte 6: Heart Rate (0xFF = invalid)
payload[6] = (byte) (currentHeartRate == 0 ? 0xFF : currentHeartRate);

// Byte 7: Capabilities Bit Field (4 bits 0:3) + FE State Bit Field (4 bits 4:7)
payload[7] = 0x00; // Set to 0x00 for now (refer to Tables 8-9 and 8-10)
// Byte 7: Page 16 capabilities (bits 0:3) + FE State (bits 4:7)
payload[7] = (byte) (PAGE16_CAP_DISTANCE_ENABLED | feStateNibble());

// Create debug string
return String.format(Locale.US,
"General FE Data Page (0x10): " +
"Page=0x%02X, Equipment=0x%02X(Bike), " +
"Page=0x%02X, Equipment=0x%02X, " +
"ElapsedTime=0x%02X(%.1fs), Distance=0x%02X(%dm), " +
"Speed=0x%02X%02X(%.1fkm/h), HeartRate=0x%02X(%s), " +
"Capabilities=0x%02X",
Expand All @@ -482,6 +486,13 @@ private String buildGeneralFEDataPage(byte[] payload) {
* @param payload byte array to populate
* @return debug string with hex and parsed values
*/
private String buildEquipmentSpecificDataPage(byte[] payload) {
if (equipmentType == EQUIPMENT_TYPE_ROWER) {
return buildRowerDataPage(payload);
}
return buildBikeDataPage(payload);
}

private String buildBikeDataPage(byte[] payload) {
payload[0] = 0x19; // Data Page Number = 0x19 (Page 25)

Expand Down Expand Up @@ -511,7 +522,7 @@ private String buildBikeDataPage(byte[] payload) {
}

// Byte 7: Flags Bit Field (bits 0-3) + FE State Bit Field (bits 4-7)
payload[7] = 0x00; // Set to 0x00 for now
payload[7] = (byte) feStateNibble();

// Create debug string
String cadenceStr = currentCadence == 0 ? "Invalid" : currentCadence + "rpm";
Expand All @@ -528,6 +539,41 @@ private String buildBikeDataPage(byte[] payload) {
(payload[6] & 0x0F), payload[5] & 0xFF, powerStr,
payload[7] & 0xFF);
}

/**
* Build Specific Rower Data Page (0x16) - Page 22.
*/
private String buildRowerDataPage(byte[] payload) {
payload[0] = DATA_PAGE_ROWER_DATA;
payload[1] = (byte) 0xFF;
payload[2] = (byte) 0xFF;
payload[3] = (byte) (strokeCount & 0xFF);
payload[4] = (byte) (currentCadence == 0 ? 0xFF : currentCadence);

int rowerPower = currentPower;
if (rowerPower > 65534) {
payload[5] = (byte) 0xFF;
payload[6] = (byte) 0xFF;
} else {
payload[5] = (byte) (rowerPower & 0xFF);
payload[6] = (byte) ((rowerPower >> 8) & 0xFF);
}

payload[7] = (byte) (PAGE22_CAP_STROKE_COUNT_ENABLED | feStateNibble());

String cadenceStr = currentCadence == 0 ? "Invalid" : currentCadence + "spm";
String powerStr = rowerPower > 65534 ? "Invalid" : rowerPower + "W";

return String.format(Locale.US,
"Rower Data Page (0x16): " +
"Page=0x%02X, StrokeCount=0x%02X(%d), " +
"Cadence=0x%02X(%s), Power=0x%02X%02X(%s), Capabilities=0x%02X",
payload[0] & 0xFF,
payload[3] & 0xFF, strokeCount,
payload[4] & 0xFF, cadenceStr,
payload[6] & 0xFF, payload[5] & 0xFF, powerStr,
payload[7] & 0xFF);
}

/**
* Build General Settings Page (0x11) - Page 17
Expand Down Expand Up @@ -562,7 +608,7 @@ private String buildGeneralSettingsPage(byte[] payload) {
payload[6] = (byte) (resistanceLevel05 & 0xFF);

// Byte 7: Capabilities Bit Field (bits 0-3) + FE State Bit Field (bits 4-7)
payload[7] = 0x00; // Set to 0x00 for now
payload[7] = (byte) feStateNibble();

// Create debug string
return String.format(Locale.US,
Expand All @@ -576,6 +622,13 @@ private String buildGeneralSettingsPage(byte[] payload) {
payload[6] & 0xFF, currentResistance,
payload[7] & 0xFF);
}

private int feStateNibble() {
int state = (elapsedTimeSeconds > 0.0 || currentSpeedKph > 0.0 || currentPower > 0 || currentCadence > 0)
? FE_STATE_IN_USE
: FE_STATE_READY;
return (state & 0x0F) << 4;
}

/**
* Handle incoming control commands
Expand Down Expand Up @@ -648,4 +701,4 @@ private void handleTrackResistanceCommand(byte[] data) {
}
}
}
}
}
4 changes: 3 additions & 1 deletion src/android/src/ChannelService.java
Original file line number Diff line number Diff line change
Expand Up @@ -251,13 +251,15 @@ boolean isBikeTransmitterActive() {
*/
void updateBikeTransmitterExtendedMetrics(long distanceMeters, int heartRate,
double elapsedTimeSeconds, int resistance,
double inclination) {
double inclination, int equipmentType, int strokeCount) {
if (!Ant.treadmill && bikeTransmitterController != null) {
bikeTransmitterController.setDistance(distanceMeters);
bikeTransmitterController.setHeartRate(heartRate);
bikeTransmitterController.setElapsedTime(elapsedTimeSeconds);
bikeTransmitterController.setResistance(resistance);
bikeTransmitterController.setInclination(inclination);
bikeTransmitterController.setEquipmentType(equipmentType);
bikeTransmitterController.setStrokeCount(strokeCount);
}
}

Expand Down
Loading
Loading