Compare commits

...

25 Commits

Author SHA1 Message Date
Roberto Viola
9502830c44 removing trainer data page 2025-06-07 06:14:10 +00:00
Roberto Viola
b63ef38121 fixing build 2025-06-06 19:40:15 +00:00
Roberto Viola
31e7c33478 adding debug log 2025-06-06 16:28:57 +00:00
Roberto Viola
ffae9fed4e eventcountpower removed 2025-06-06 15:26:34 +00:00
Roberto Viola
406a01f2a1 fixing power 2025-06-06 13:25:13 +00:00
Roberto Viola
688283c89a fixing power 2025-06-06 13:17:15 +00:00
Roberto Viola
9e31770c7d fixing power 2025-06-06 12:32:47 +00:00
Roberto Viola
0bf8a8f585 Merge branch 'ftmsbikeantsender' of https://github.com/cagnulein/qdomyos-zwift into ftmsbikeantsender 2025-06-06 12:00:57 +00:00
Roberto Viola
cb6ee5c0f7 fixing power 2025-06-06 12:00:55 +00:00
Roberto Viola
57b0c3c8c9 Update BikeTransmitterController.java 2025-06-06 10:59:13 +02:00
Roberto Viola
1194f08145 reverting 2025-06-06 07:54:47 +00:00
Roberto Viola
e741e1b04a adding extended metrics to java 2025-06-06 07:12:45 +00:00
Roberto Viola
90fc9f229b Merge branch 'ftmsbikeantsender' of https://github.com/cagnulein/qdomyos-zwift into ftmsbikeantsender 2025-06-06 05:18:59 +00:00
Roberto Viola
f768d3de9c fix build 2025-06-06 05:18:54 +00:00
Roberto Viola
99fb81519e Update homeform.cpp 2025-06-05 21:45:51 +02:00
Roberto Viola
5e350960e0 final one? 2025-06-05 17:29:49 +00:00
Roberto Viola
e929e8149d fixing set power and grade 2025-06-05 16:56:52 +00:00
Roberto Viola
6905ab3f5b fixing send metrics 2025-06-05 16:49:13 +00:00
Roberto Viola
1b5e98e05e fixing bytes 2025-06-05 16:03:08 +00:00
Roberto Viola
db38c897dd Merge branch 'ftmsbikeantsender' of https://github.com/cagnulein/qdomyos-zwift into ftmsbikeantsender 2025-06-05 06:53:35 +00:00
Roberto Viola
15363d7dd4 trying with a similar to PowerChanell, the previous anyway it was already showing in the garmin edge 530 2025-06-05 06:53:29 +00:00
Roberto Viola
49665d2af0 Update BikeTransmitterController.java 2025-06-04 19:06:29 +02:00
Roberto Viola
b9b8c18d62 fix build 2025-06-04 16:18:19 +00:00
Roberto Viola
b7e4566ec4 fixing build 2025-06-04 15:44:03 +00:00
Roberto Viola
bd1d29677c first commit 2025-06-04 14:56:30 +00:00
4 changed files with 955 additions and 2 deletions

View File

@@ -152,4 +152,15 @@ public class Ant {
QLog.v(TAG, "isBikeConnected");
return mChannelService.isBikeConnected();
}
public void updateBikeTransmitterExtendedMetrics(long distanceMeters, int heartRate,
double elapsedTimeSeconds, int resistance,
double inclination) {
if(mChannelService == null)
return;
QLog.v(TAG, "updateBikeTransmitterExtendedMetrics");
mChannelService.updateBikeTransmitterExtendedMetrics(distanceMeters, heartRate,
elapsedTimeSeconds, resistance,
inclination);
}
}

View File

@@ -0,0 +1,651 @@
/*
* Copyright 2012 Dynastream Innovations Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.cagnulen.qdomyoszwift;
import android.os.RemoteException;
import org.cagnulen.qdomyoszwift.QLog;
import com.dsi.ant.channel.AntChannel;
import com.dsi.ant.channel.AntCommandFailedException;
import com.dsi.ant.channel.IAntChannelEventHandler;
import com.dsi.ant.message.ChannelId;
import com.dsi.ant.message.ChannelType;
import com.dsi.ant.message.EventCode;
import com.dsi.ant.message.fromant.AcknowledgedDataMessage;
import com.dsi.ant.message.fromant.ChannelEventMessage;
import com.dsi.ant.message.fromant.MessageFromAntType;
import com.dsi.ant.message.ipc.AntMessageParcel;
import android.os.RemoteException;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.Random;
/**
* ANT+ Bike Transmitter Controller
* Follows exactly the same pattern as PowerChannelController but for Fitness Equipment
*/
public class BikeTransmitterController {
public static final int FITNESS_EQUIPMENT_SENSOR_ID = 0x9e3d4b67; // Different from power sensor
// The device type and transmission type to be part of the channel ID message
private static final int CHANNEL_FITNESS_EQUIPMENT_DEVICE_TYPE = 17; // Fitness Equipment
private static final int CHANNEL_FITNESS_EQUIPMENT_TRANSMISSION_TYPE = 5;
// The period and frequency values the channel will be configured to
private static final int CHANNEL_FITNESS_EQUIPMENT_PERIOD = 8192; // 4 Hz for FE
private static final int CHANNEL_FITNESS_EQUIPMENT_FREQUENCY = 57;
private static final String TAG = BikeTransmitterController.class.getSimpleName();
// ANT+ Data Page IDs for Fitness Equipment
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_GENERAL_SETTINGS = 0x11;
private static Random randGen = new Random();
// Current bike metrics to transmit
int currentCadence = 0; // Current cadence in RPM
int currentPower = 0; // Current power in watts
double currentSpeedKph = 0.0; // Current speed in km/h
long totalDistance = 0; // Total distance in meters
int currentHeartRate = 0; // Heart rate in BPM
double elapsedTimeSeconds = 0.0; // Elapsed time in seconds
int currentResistance = 0; // Current resistance level (0-100)
double currentInclination = 0.0; // Current inclination in percentage
// Control commands received from ANT+ devices
private int requestedResistance = -1; // Requested resistance from controller
private int requestedPower = -1; // Requested power from controller
private double requestedInclination = -100; // Requested inclination from controller
private AntChannel mAntChannel;
private ChannelEventCallback mChannelEventCallback = new ChannelEventCallback();
private boolean mIsOpen;
// Callbacks for control commands
public interface ControlCommandListener {
void onResistanceChangeRequested(int resistance);
void onPowerChangeRequested(int power);
void onInclinationChangeRequested(double inclination);
}
private ControlCommandListener controlListener = null;
public BikeTransmitterController(AntChannel antChannel) {
mAntChannel = antChannel;
openChannel();
}
/**
* Set the listener for control commands received from ANT+ devices
*/
public void setControlCommandListener(ControlCommandListener listener) {
this.controlListener = listener;
}
boolean openChannel() {
if (null != mAntChannel) {
if (mIsOpen) {
QLog.w(TAG, "Channel was already open");
} else {
// Channel ID message contains device number, type and transmission type
ChannelId channelId = new ChannelId(FITNESS_EQUIPMENT_SENSOR_ID & 0xFFFF,
CHANNEL_FITNESS_EQUIPMENT_DEVICE_TYPE, CHANNEL_FITNESS_EQUIPMENT_TRANSMISSION_TYPE);
try {
// Setting the channel event handler so that we can receive messages from ANT
mAntChannel.setChannelEventHandler(mChannelEventCallback);
// Performs channel assignment by assigning the type to the channel
mAntChannel.assign(ChannelType.BIDIRECTIONAL_MASTER);
// Configures the channel ID, messaging period and rf frequency after assigning,
// then opening the channel.
mAntChannel.setChannelId(channelId);
mAntChannel.setPeriod(CHANNEL_FITNESS_EQUIPMENT_PERIOD);
mAntChannel.setRfFrequency(CHANNEL_FITNESS_EQUIPMENT_FREQUENCY);
mAntChannel.open();
mIsOpen = true;
QLog.d(TAG, "Opened fitness equipment channel with device number: " + FITNESS_EQUIPMENT_SENSOR_ID);
} catch (RemoteException e) {
channelError(e);
} catch (AntCommandFailedException e) {
// This will release, and therefore unassign if required
channelError("Open failed", e);
}
}
} else {
QLog.w(TAG, "No channel available");
}
return mIsOpen;
}
public boolean startTransmission() {
return openChannel();
}
public void stopTransmission() {
close();
}
void channelError(RemoteException e) {
String logString = "Remote service communication failed.";
QLog.e(TAG, logString);
}
void channelError(String error, AntCommandFailedException e) {
StringBuilder logString;
if (e.getResponseMessage() != null) {
String initiatingMessageId = "0x" + Integer.toHexString(
e.getResponseMessage().getInitiatingMessageId());
String rawResponseCode = "0x" + Integer.toHexString(
e.getResponseMessage().getRawResponseCode());
logString = new StringBuilder(error)
.append(". Command ")
.append(initiatingMessageId)
.append(" failed with code ")
.append(rawResponseCode);
} else {
String attemptedMessageId = "0x" + Integer.toHexString(
e.getAttemptedMessageType().getMessageId());
String failureReason = e.getFailureReason().toString();
logString = new StringBuilder(error)
.append(". Command ")
.append(attemptedMessageId)
.append(" failed with reason ")
.append(failureReason);
}
QLog.e(TAG, logString.toString());
mAntChannel.release();
}
public void close() {
if (null != mAntChannel) {
mIsOpen = false;
// Releasing the channel to make it available for others.
// After releasing, the AntChannel instance cannot be reused.
mAntChannel.release();
mAntChannel = null;
}
QLog.e(TAG, "Fitness Equipment Channel Closed");
}
// Setter methods for updating bike metrics from the main application
public void setCadence(int cadence) {
this.currentCadence = Math.max(0, cadence);
}
public void setPower(int power) {
this.currentPower = Math.max(0, power);
}
public void setSpeedKph(double speedKph) {
this.currentSpeedKph = Math.max(0, speedKph);
}
public void setDistance(long distance) {
this.totalDistance = Math.max(0, distance);
}
public void setHeartRate(int heartRate) {
this.currentHeartRate = Math.max(0, Math.min(255, heartRate));
}
public void setElapsedTime(double timeSeconds) {
this.elapsedTimeSeconds = Math.max(0, timeSeconds);
}
public void setResistance(int resistance) {
this.currentResistance = Math.max(0, Math.min(100, resistance));
}
public void setInclination(double inclination) {
this.currentInclination = Math.max(-100, Math.min(100, inclination));
}
// Getter methods for the last requested control values
public int getRequestedResistance() {
return requestedResistance;
}
public int getRequestedPower() {
return requestedPower;
}
public double getRequestedInclination() {
return requestedInclination;
}
public void clearControlRequests() {
requestedResistance = -1;
requestedPower = -1;
requestedInclination = -100;
}
public boolean isTransmitting() {
return mIsOpen;
}
public String getTransmissionInfo() {
if (!mIsOpen) {
return "Transmission: STOPPED";
}
return String.format("Transmission: ACTIVE - Cadence: %drpm, Power: %dW, " +
"Speed: %.1fkm/h, Resistance: %d, Inclination: %.1f%%",
currentCadence, currentPower, currentSpeedKph,
currentResistance, currentInclination);
}
/**
* Helper method to convert byte array to hex string for debugging
*/
private String bytesToHex(byte[] bytes) {
StringBuilder hex = new StringBuilder();
for (byte b : bytes) {
hex.append(String.format("%02X ", b & 0xFF));
}
return hex.toString().trim();
}
/**
* Implements the Channel Event Handler Interface following PowerChannelController pattern
*/
public class ChannelEventCallback implements IAntChannelEventHandler {
int cnt = 0;
int eventCount = 0;
int eventPowerCount = 0;
int cumulativeDistance = 0;
int cumulativeWatt = 0;
int accumulatedTorque32 = 0;
Timer carousalTimer = null;
@Override
public void onChannelDeath() {
// Display channel death message when channel dies
QLog.e(TAG, "Fitness Equipment Channel Death");
}
@Override
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
QLog.d(TAG, "Rx: " + antParcel);
QLog.d(TAG, "Message Type: " + messageType);
byte[] payload = new byte[8];
// Start unsolicited transmission timer like PowerChannelController
if(carousalTimer == null) {
carousalTimer = new Timer(); // At this line a new Thread will be created
carousalTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
QLog.d(TAG, "Tx Unsolicited Fitness Equipment Data");
byte[] payload = new byte[8];
String debugString = "";
eventCount = (eventCount + 1) & 0xFF;
cumulativeDistance = (cumulativeDistance + (int)(currentSpeedKph / 3.6)) & 0xFFFF; // rough distance calc
cnt += 1;
// Cycle through different data pages like PowerChannelController
if (cnt % 5 == 0) {
// General FE Data Page (0x10)
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 == 3) {
// General Settings Page (0x11)
debugString = buildGeneralSettingsPage(payload);
} else {
// Default General FE Data Page (0x10)
debugString = buildGeneralFEDataPage(payload);
}
// Log the hex data and parsed values
QLog.d(TAG, "Tx Payload HEX: " + bytesToHex(payload));
QLog.d(TAG, debugString);
if (mIsOpen) {
try {
// Setting the data to be broadcast on the next channel period
mAntChannel.setBroadcastData(payload);
} catch (RemoteException e) {
channelError(e);
}
}
}
}, 0, 250); // Every 250ms for 4Hz
}
// Switching on message type to handle different types of messages
switch (messageType) {
case BROADCAST_DATA:
// Rx Data
break;
case ACKNOWLEDGED_DATA:
// Handle control commands
payload = new AcknowledgedDataMessage(antParcel).getPayload();
QLog.d(TAG, "AcknowledgedDataMessage: " + payload);
handleControlCommand(payload);
break;
case CHANNEL_EVENT:
// Constructing channel event message from parcel
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
EventCode code = eventMessage.getEventCode();
QLog.d(TAG, "Event Code: " + code);
// Switching on event code to handle the different types of channel events
switch (code) {
case TX:
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);
} else if (cnt % 16 == 9) {
// Trainer Data Page (0x1A)
debugString = buildBikeDataPage(payload);
} else if (cnt % 16 == 13) {
// General Settings Page (0x11)
debugString = buildGeneralSettingsPage(payload);
} else {
// Default General FE Data Page (0x10)
debugString = buildGeneralFEDataPage(payload);
}
// Log the hex data and parsed values
QLog.d(TAG, "Tx Payload HEX: " + bytesToHex(payload));
QLog.d(TAG, debugString);
if (mIsOpen) {
try {
// Setting the data to be broadcast on the next channel period
mAntChannel.setBroadcastData(payload);
} catch (RemoteException e) {
channelError(e);
}
}
break;
case CHANNEL_COLLISION:
cnt += 1;
break;
case RX_SEARCH_TIMEOUT:
QLog.e(TAG, "No Device Found");
break;
case CHANNEL_CLOSED:
case RX_FAIL:
case RX_FAIL_GO_TO_SEARCH:
case TRANSFER_RX_FAILED:
case TRANSFER_TX_COMPLETED:
case TRANSFER_TX_FAILED:
case TRANSFER_TX_START:
case UNKNOWN:
// TODO More complex communication will need to handle these events
break;
}
break;
case ANT_VERSION:
case BURST_TRANSFER_DATA:
case CAPABILITIES:
case CHANNEL_ID:
case CHANNEL_RESPONSE:
case CHANNEL_STATUS:
case SERIAL_NUMBER:
case OTHER:
// TODO More complex communication will need to handle these message types
break;
}
}
/**
* Build General Fitness Equipment Data Page (0x10) - Page 16
* Following Table 8-7 format exactly
* @param payload byte array to populate
* @return debug string with hex and parsed values
*/
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)
// Byte 2: Elapsed Time (0.25 seconds resolution, rollover at 64s)
int elapsedTime025s = (int) (elapsedTimeSeconds * 4) & 0xFF;
payload[2] = (byte) elapsedTime025s;
// Byte 3: Distance Traveled (1 meter resolution, rollover at 256m)
int distanceMeters = (int) (totalDistance) & 0xFF;
payload[3] = (byte) distanceMeters;
// Bytes 4-5: Speed (0.001 m/s resolution, 0xFFFF = invalid)
int speedMms = (int) (currentSpeedKph / 3.6 * 1000);
if (speedMms > 65534) speedMms = 65534; // Max valid value
payload[4] = (byte) (speedMms & 0xFF); // Speed LSB
payload[5] = (byte) ((speedMms >> 8) & 0xFF); // Speed MSB
// 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)
// Create debug string
return String.format(Locale.US,
"General FE Data Page (0x10): " +
"Page=0x%02X, Equipment=0x%02X(Bike), " +
"ElapsedTime=0x%02X(%.1fs), Distance=0x%02X(%dm), " +
"Speed=0x%02X%02X(%.1fkm/h), HeartRate=0x%02X(%s), " +
"Capabilities=0x%02X",
payload[0] & 0xFF, payload[1] & 0xFF,
payload[2] & 0xFF, elapsedTimeSeconds,
payload[3] & 0xFF, distanceMeters,
payload[5] & 0xFF, payload[4] & 0xFF, currentSpeedKph,
payload[6] & 0xFF, currentHeartRate == 0 ? "Invalid" : currentHeartRate + "bpm",
payload[7] & 0xFF);
}
/**
* Build Specific Trainer/Stationary Bike Data Page (0x19) - Page 25
* Following Table 8-25 format exactly
* @param payload byte array to populate
* @return debug string with hex and parsed values
*/
private String buildBikeDataPage(byte[] payload) {
payload[0] = 0x19; // Data Page Number = 0x19 (Page 25)
// Byte 1: Update Event Count (increments with each information update)
eventPowerCount = (eventPowerCount + 1) & 0xFF;
payload[1] = (byte) eventPowerCount;
// Byte 2: Instantaneous Cadence (RPM, 0xFF = invalid)
payload[2] = (byte) (currentCadence == 0 ? 0xFF : currentCadence);
// Bytes 3-4: Accumulated Power (1 watt resolution, rollover at 65536W)
// This is cumulative power, not instantaneous
cumulativeWatt = (cumulativeWatt + currentPower);
payload[3] = (byte) (cumulativeWatt & 0xFF); // Accumulated Power LSB
payload[4] = (byte) ((cumulativeWatt >> 8) & 0xFF); // Accumulated Power MSB
// Bytes 5-6: Instantaneous Power (1.5 bytes, 0xFFF = invalid for both fields)
if (currentPower > 4094) {
// 0xFFF indicates BOTH instantaneous and accumulated power fields are invalid
payload[5] = (byte) 0xFF; // Instantaneous Power LSB
payload[6] = (byte) 0xFF; // Instantaneous Power MSB (bits 0-3) + Trainer Status (bits 4-7)
} else {
payload[5] = (byte) (currentPower & 0xFF); // Instantaneous Power LSB
payload[6] = (byte) ((currentPower >> 8) & 0x0F); // Instantaneous Power MSN (bits 0-3)
// Bits 4-7 of byte 6: Trainer Status Bit Field (refer to Table 8-27)
payload[6] |= 0x00; // Trainer status = 0 for now
}
// Byte 7: Flags Bit Field (bits 0-3) + FE State Bit Field (bits 4-7)
payload[7] = 0x00; // Set to 0x00 for now
// Create debug string
String cadenceStr = currentCadence == 0 ? "Invalid" : currentCadence + "rpm";
String powerStr = currentPower > 4094 ? "Invalid" : currentPower + "W";
return String.format(Locale.US,
"Bike Data Page (0x19): " +
"Page=0x%02X, EventCount=0x%02X(%d), " +
"Cadence=0x%02X(%s), AccumPower=0x%02X%02X(%dW), " +
"InstPower=0x%X%02X(%s), Flags=0x%02X",
payload[0] & 0xFF, payload[1] & 0xFF, eventCount,
payload[2] & 0xFF, cadenceStr,
payload[4] & 0xFF, payload[3] & 0xFF, cumulativeWatt,
(payload[6] & 0x0F), payload[5] & 0xFF, powerStr,
payload[7] & 0xFF);
}
/**
* Build General Settings Page (0x11) - Page 17
* Following Table 8-11 format exactly
* @param payload byte array to populate
* @return debug string with hex and parsed values
*/
private String buildGeneralSettingsPage(byte[] payload) {
payload[0] = 0x11; // Data Page Number = 0x11 (Page 17)
// Byte 1: Reserved (0xFF - Do not interpret)
payload[1] = (byte) 0xFF;
// Byte 2: Reserved (0xFF - Do not interpret)
payload[2] = (byte) 0xFF;
// Byte 3: Cycle length (0.01 meters resolution, 0xFF = invalid)
// Length of one 'cycle' - for bike this could be wheel circumference
int cycleLengthCm = 210; // 2.1m wheel circumference = 210cm
payload[3] = (byte) (cycleLengthCm & 0xFF);
// Bytes 4-5: Incline Percentage (signed integer, 0.01% resolution, 0x7FFF = invalid)
int inclinePercent001 = (int) (currentInclination * 100); // Convert to 0.01% units
if (inclinePercent001 < -10000) inclinePercent001 = -10000; // Min -100.00%
if (inclinePercent001 > 10000) inclinePercent001 = 10000; // Max +100.00%
payload[4] = (byte) (inclinePercent001 & 0xFF); // Incline LSB
payload[5] = (byte) ((inclinePercent001 >> 8) & 0xFF); // Incline MSB
// Byte 6: Resistance Level (0.5% resolution, percentage of maximum applicable resistance)
int resistanceLevel05 = (int) (currentResistance * 2); // Convert to 0.5% units
if (resistanceLevel05 > 200) resistanceLevel05 = 200; // Max 100% = 200 in 0.5% units
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
// Create debug string
return String.format(Locale.US,
"General Settings Page (0x11): " +
"Page=0x%02X, Reserved1=0x%02X, Reserved2=0x%02X, " +
"CycleLength=0x%02X(%.2fm), Incline=0x%02X%02X(%.2f%%), " +
"Resistance=0x%02X(%d%%), Capabilities=0x%02X",
payload[0] & 0xFF, payload[1] & 0xFF, payload[2] & 0xFF,
payload[3] & 0xFF, cycleLengthCm / 100.0,
payload[5] & 0xFF, payload[4] & 0xFF, currentInclination,
payload[6] & 0xFF, currentResistance,
payload[7] & 0xFF);
}
/**
* Handle incoming control commands
*/
private void handleControlCommand(byte[] data) {
if (data.length < 8) return;
byte pageNumber = data[0];
QLog.d(TAG, "Received control command page: 0x" + String.format("%02X", pageNumber));
QLog.d(TAG, "Control Command HEX: " + bytesToHex(data));
// Handle control command pages
switch (pageNumber) {
case 0x30: // Basic Resistance
handleBasicResistanceCommand(data);
break;
case 0x31: // Target Power
handleTargetPowerCommand(data);
break;
case 0x33: // Track Resistance
handleTrackResistanceCommand(data);
break;
default:
QLog.d(TAG, "Unknown control page: 0x" + String.format("%02X", pageNumber));
break;
}
}
private void handleBasicResistanceCommand(byte[] data) {
int resistance = data[7] & 0xFF; // Resistance in 0.5% increments
double resistancePercent = resistance * 0.5;
QLog.d(TAG, String.format(Locale.US,
"Basic Resistance Command (0x30): Resistance=0x%02X(%.1f%%)",
resistance, resistancePercent));
if (resistancePercent != requestedResistance && controlListener != null) {
requestedResistance = (int) resistancePercent;
controlListener.onResistanceChangeRequested(requestedResistance);
}
}
private void handleTargetPowerCommand(byte[] data) {
int targetPower = ((data[7] & 0xFF) << 8) | (data[6] & 0xFF);
targetPower = targetPower / 4;
QLog.d(TAG, String.format(Locale.US,
"Target Power Command (0x31): Power=0x%02X%02X(%dW)",
data[7] & 0xFF, data[6] & 0xFF, targetPower));
if (targetPower != requestedPower && controlListener != null) {
requestedPower = targetPower;
controlListener.onPowerChangeRequested(targetPower);
}
}
private void handleTrackResistanceCommand(byte[] data) {
// Grade is in 0.01% increments, signed 16-bit
int gradeRaw = ((data[6] & 0xFF) << 8) | (data[5] & 0xFF);
if (gradeRaw > 32767) gradeRaw -= 65536; // Convert to signed
double grade = (gradeRaw - 0x4E20) * 0.01;
QLog.d(TAG, String.format(Locale.US,
"Track Resistance Command (0x33): Grade=0x%02X%02X(%.2f%%)",
data[6] & 0xFF, data[5] & 0xFF, grade));
if (Math.abs(grade - requestedInclination) > 0.1 && controlListener != null) {
requestedInclination = grade;
controlListener.onInclinationChangeRequested(grade);
}
}
}
}

View File

@@ -49,11 +49,16 @@ public class ChannelService extends Service {
private AntChannelProvider mAntChannelProvider = null;
private boolean mAllowAddChannel = false;
public static native void nativeSetResistance(int resistance);
public static native void nativeSetPower(int power);
public static native void nativeSetInclination(double inclination);
HeartChannelController heartChannelController = null;
PowerChannelController powerChannelController = null;
SpeedChannelController speedChannelController = null;
SDMChannelController sdmChannelController = null;
BikeChannelController bikeChannelController = null; // Added BikeChannelController reference
BikeTransmitterController bikeTransmitterController = null; // Added BikeTransmitterController reference
private ServiceConnection mAntRadioServiceConnection = new ServiceConnection() {
@Override
@@ -118,12 +123,20 @@ public class ChannelService extends Service {
if (null != sdmChannelController) {
sdmChannelController.speed = speed;
}
// Update bike transmitter with speed data (only if not treadmill)
if (!Ant.treadmill && null != bikeTransmitterController) {
bikeTransmitterController.setSpeedKph(speed);
}
}
void setPower(int power) {
if (null != powerChannelController) {
powerChannelController.power = power;
}
// Update bike transmitter with power data (only if not treadmill)
if (!Ant.treadmill && null != bikeTransmitterController) {
bikeTransmitterController.setPower(power);
}
}
void setCadence(int cadence) {
@@ -136,6 +149,10 @@ public class ChannelService extends Service {
if (null != sdmChannelController) {
sdmChannelController.cadence = cadence;
}
// Update bike transmitter with cadence data (only if not treadmill)
if (!Ant.treadmill && null != bikeTransmitterController) {
bikeTransmitterController.setCadence(cadence);
}
}
int getHeart() {
@@ -182,6 +199,114 @@ public class ChannelService extends Service {
return (bikeChannelController != null && bikeChannelController.isConnected());
}
// ========== BIKE TRANSMITTER METHODS ==========
/**
* Start the bike transmitter (only available if not treadmill)
*/
boolean startBikeTransmitter() {
QLog.v(TAG, "ChannelServiceComm.startBikeTransmitter");
if (Ant.treadmill) {
QLog.w(TAG, "Bike transmitter not available in treadmill mode");
return false;
}
if (bikeTransmitterController != null) {
return bikeTransmitterController.startTransmission();
}
QLog.w(TAG, "Bike transmitter controller is null");
return false;
}
/**
* Stop the bike transmitter
*/
void stopBikeTransmitter() {
QLog.v(TAG, "ChannelServiceComm.stopBikeTransmitter");
if (bikeTransmitterController != null) {
bikeTransmitterController.stopTransmission();
}
}
/**
* Check if bike transmitter is active (only if not treadmill)
*/
boolean isBikeTransmitterActive() {
if (Ant.treadmill) {
return false;
}
return (bikeTransmitterController != null && bikeTransmitterController.isTransmitting());
}
/**
* Update bike transmitter with extended metrics (only if not treadmill)
*/
void updateBikeTransmitterExtendedMetrics(long distanceMeters, int heartRate,
double elapsedTimeSeconds, int resistance,
double inclination) {
if (!Ant.treadmill && bikeTransmitterController != null) {
bikeTransmitterController.setDistance(distanceMeters);
bikeTransmitterController.setHeartRate(heartRate);
bikeTransmitterController.setElapsedTime(elapsedTimeSeconds);
bikeTransmitterController.setResistance(resistance);
bikeTransmitterController.setInclination(inclination);
}
}
/**
* Get the last requested resistance from ANT+ controller (only if not treadmill)
*/
int getRequestedResistanceFromAnt() {
if (!Ant.treadmill && bikeTransmitterController != null) {
return bikeTransmitterController.getRequestedResistance();
}
return -1;
}
/**
* Get the last requested power from ANT+ controller (only if not treadmill)
*/
int getRequestedPowerFromAnt() {
if (!Ant.treadmill && bikeTransmitterController != null) {
return bikeTransmitterController.getRequestedPower();
}
return -1;
}
/**
* Get the last requested inclination from ANT+ controller (only if not treadmill)
*/
double getRequestedInclinationFromAnt() {
if (!Ant.treadmill && bikeTransmitterController != null) {
return bikeTransmitterController.getRequestedInclination();
}
return -100.0;
}
/**
* Clear any pending control requests (only if not treadmill)
*/
void clearAntControlRequests() {
if (!Ant.treadmill && bikeTransmitterController != null) {
bikeTransmitterController.clearControlRequests();
}
}
/**
* Get transmission info for debugging (only if not treadmill)
*/
String getBikeTransmitterInfo() {
if (Ant.treadmill) {
return "Bike transmitter disabled in treadmill mode";
}
if (bikeTransmitterController != null) {
return bikeTransmitterController.getTransmissionInfo();
}
return "Bike transmitter not initialized";
}
/**
* Closes all channels currently added.
*/
@@ -203,10 +328,71 @@ public class ChannelService extends Service {
}
}
// Add initialization for BikeChannelController
// Add initialization for BikeChannelController (receiver)
if (Ant.bikeRequest && bikeChannelController == null) {
bikeChannelController = new BikeChannelController();
}
// Add initialization for BikeTransmitterController (transmitter) - only when NOT treadmill
if (!Ant.treadmill && bikeTransmitterController == null) {
QLog.v(TAG, "Initializing BikeTransmitterController (not treadmill mode)");
try {
// Acquire channel like other controllers
AntChannel transmitterChannel = acquireChannel();
if (transmitterChannel != null) {
bikeTransmitterController = new BikeTransmitterController(transmitterChannel);
// Set up control command listener to handle requests from ANT+ devices
bikeTransmitterController.setControlCommandListener(new BikeTransmitterController.ControlCommandListener() {
@Override
public void onResistanceChangeRequested(int resistance) {
QLog.d(TAG, "ChannelService: ANT+ Resistance change requested: " + resistance);
// Send broadcast intent to notify the main application
Intent intent = new Intent("org.cagnulen.qdomyoszwift.ANT_RESISTANCE_CHANGE");
intent.putExtra("resistance", resistance);
nativeSetResistance(resistance);
sendBroadcast(intent);
}
@Override
public void onPowerChangeRequested(int power) {
QLog.d(TAG, "ChannelService: ANT+ Power change requested: " + power + "W");
// Send broadcast intent to notify the main application
Intent intent = new Intent("org.cagnulen.qdomyoszwift.ANT_POWER_CHANGE");
intent.putExtra("power", power);
nativeSetPower(power);
sendBroadcast(intent);
}
@Override
public void onInclinationChangeRequested(double inclination) {
QLog.d(TAG, "ChannelService: ANT+ Inclination change requested: " + inclination + "%");
// Send broadcast intent to notify the main application
Intent intent = new Intent("org.cagnulen.qdomyoszwift.ANT_INCLINATION_CHANGE");
intent.putExtra("inclination", inclination);
nativeSetInclination(inclination);
sendBroadcast(intent);
}
});
QLog.i(TAG, "BikeTransmitterController initialized successfully (bike mode)");
// Start the bike transmitter immediately after initialization
boolean transmissionStarted = bikeTransmitterController.startTransmission();
if (transmissionStarted) {
QLog.i(TAG, "BikeTransmitterController transmission started automatically");
} else {
QLog.w(TAG, "Failed to start BikeTransmitterController transmission");
}
} else {
QLog.e(TAG, "Failed to acquire channel for BikeTransmitterController");
}
} catch (Exception e) {
QLog.e(TAG, "Failed to initialize BikeTransmitterController: " + e.getMessage());
bikeTransmitterController = null;
}
}
}
private void closeAllChannels() {
@@ -220,12 +406,16 @@ public class ChannelService extends Service {
sdmChannelController.close();
if (bikeChannelController != null) // Added closing bikeChannelController
bikeChannelController.close();
if (bikeTransmitterController != null) { // Added closing bikeTransmitterController
bikeTransmitterController.close(); // Use close() method like other controllers
}
heartChannelController = null;
powerChannelController = null;
speedChannelController = null;
sdmChannelController = null;
bikeChannelController = null; // Added nullifying bikeChannelController
bikeTransmitterController = null; // Added nullifying bikeTransmitterController
}
AntChannel acquireChannel() throws ChannelNotAvailableException {
@@ -364,4 +554,4 @@ public class ChannelService extends Service {
QLog.e(TAG, "DIE: " + error);
}
}
}

View File

@@ -5979,6 +5979,39 @@ void homeform::update() {
v += settings.value(QZSettings::ant_speed_offset, QZSettings::default_ant_speed_offset).toDouble();
KeepAwakeHelper::antObject(false)->callMethod<void>("setCadenceSpeedPower", "(FII)V", (float)v, (int)watts,
(int)cadence);
long distanceMeters = (long)(bluetoothManager->device()->odometer() * 1000.0);
// Get heart rate
int heartRate = (int)bluetoothManager->device()->currentHeart().value();
// Calculate elapsed time in seconds
double elapsedTimeSeconds = (double)(bluetoothManager->device()->elapsedTime().second() +
(bluetoothManager->device()->elapsedTime().minute() * 60) +
(bluetoothManager->device()->elapsedTime().hour() * 3600));
// Get resistance and inclination values
int resistance = 0;
double inclination = 0.0;
if (bluetoothManager->device()->deviceType() == bluetoothdevice::BIKE) {
resistance = (int)((bike*)bluetoothManager->device())->currentResistance().value();
inclination = ((bike*)bluetoothManager->device())->currentInclination().value();
} else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ELLIPTICAL) {
resistance = (int)((elliptical*)bluetoothManager->device())->currentResistance().value();
inclination = ((elliptical*)bluetoothManager->device())->currentInclination().value();
} else if (bluetoothManager->device()->deviceType() == bluetoothdevice::ROWING) {
resistance = (int)((rower*)bluetoothManager->device())->currentResistance().value();
}
// Call the extended metrics update via JNI
KeepAwakeHelper::antObject(false)->callMethod<void>("updateBikeTransmitterExtendedMetrics",
"(JIDID)V",
distanceMeters,
heartRate,
elapsedTimeSeconds,
resistance,
inclination);
}
#endif
@@ -8245,3 +8278,71 @@ void homeform::videoSeekPosition(int ms) {
auto videoPlaybackHalfPlayer = qvariant_cast<QMediaPlayer *>(videoPlaybackHalf->property("mediaObject"));
videoPlaybackHalfPlayer->setPosition(ms);
}
#ifdef Q_OS_ANDROID
extern "C" {
JNIEXPORT void JNICALL
Java_org_cagnulen_qdomyoszwift_ChannelService_nativeSetResistance(JNIEnv *env, jclass clazz, jint resistance) {
qDebug() << "Native: ANT+ Setting resistance to:" << resistance;
if (homeform::singleton()->bluetoothManager && homeform::singleton()->bluetoothManager->device()) {
bluetoothdevice::BLUETOOTH_TYPE deviceType = homeform::singleton()->bluetoothManager->device()->deviceType();
if (deviceType == bluetoothdevice::BIKE ||
deviceType == bluetoothdevice::ROWING ||
deviceType == bluetoothdevice::ELLIPTICAL) {
homeform::singleton()->bluetoothManager->device()->changeResistance(resistance);
qDebug() << "Applied ANT+ resistance change:" << resistance;
} else {
qDebug() << "Device type does not support resistance change";
}
} else {
qDebug() << "No bluetooth device connected";
}
}
JNIEXPORT void JNICALL
Java_org_cagnulen_qdomyoszwift_ChannelService_nativeSetPower(JNIEnv *env, jclass clazz, jint power) {
qDebug() << "Native: ANT+ Setting power to:" << power << "W";
if (homeform::singleton()->bluetoothManager && homeform::singleton()->bluetoothManager->device()) {
bluetoothdevice::BLUETOOTH_TYPE deviceType = homeform::singleton()->bluetoothManager->device()->deviceType();
if (deviceType == bluetoothdevice::BIKE ||
deviceType == bluetoothdevice::ROWING ||
deviceType == bluetoothdevice::ELLIPTICAL ||
deviceType == bluetoothdevice::TREADMILL) {
homeform::singleton()->bluetoothManager->device()->changePower(power);
qDebug() << "Applied ANT+ power change:" << power << "W";
} else {
qDebug() << "Device type does not support power change";
}
} else {
qDebug() << "No bluetooth device connected";
}
}
JNIEXPORT void JNICALL
Java_org_cagnulen_qdomyoszwift_ChannelService_nativeSetInclination(JNIEnv *env, jclass clazz, jdouble inclination) {
qDebug() << "Native: ANT+ Setting inclination to:" << inclination << "%";
if (homeform::singleton()->bluetoothManager && homeform::singleton()->bluetoothManager->device()) {
bluetoothdevice::BLUETOOTH_TYPE deviceType = homeform::singleton()->bluetoothManager->device()->deviceType();
if (deviceType == bluetoothdevice::BIKE ||
deviceType == bluetoothdevice::TREADMILL ||
deviceType == bluetoothdevice::ELLIPTICAL) {
homeform::singleton()->bluetoothManager->device()->changeInclination(inclination, inclination);
qDebug() << "Applied ANT+ inclination change:" << inclination << "%";
} else {
qDebug() << "Device type does not support inclination change";
}
} else {
qDebug() << "No bluetooth device connected";
}
}
}
#endif