Compare commits

..

3 Commits

Author SHA1 Message Date
Roberto Viola
f49ac87ff8 Update wahookickrsnapbike.cpp 2025-09-18 10:19:07 +02:00
Roberto Viola
2aba7ddfe1 adding debug popup 2025-09-18 09:38:30 +02:00
Roberto Viola
d02ca5a934 Wahoo Commands Queue 2025-09-16 15:45:30 +02:00
8 changed files with 102 additions and 623 deletions

View File

@@ -25,7 +25,6 @@ import android.widget.Toast;
import org.cagnulen.qdomyoszwift.QLog;
import android.content.Intent;
public class Ant {
private ChannelService.ChannelServiceComm mChannelService = null;
private boolean mChannelServiceBound = false;
@@ -34,27 +33,23 @@ public class Ant {
static boolean speedRequest = false;
static boolean heartRequest = false;
static boolean bikeRequest = false; // Added bike request flag
static boolean remoteControlRequest = false; // Added remote control request flag
static boolean garminKey = false;
static boolean treadmill = false;
static boolean technoGymGroupCycle = false;
static int antBikeDeviceNumber = 0;
static int antHeartDeviceNumber = 0;
static int antRemoteControlDeviceNumber = 0; // Added remote control device number
// Updated antStart method with RemoteControlRequest parameter at the end
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill, boolean BikeRequest, boolean TechnoGymGroupCycle, int AntBikeDeviceNumber, int AntHeartDeviceNumber, boolean RemoteControlRequest, int AntRemoteControlDeviceNumber) {
// Updated antStart method with BikeRequest parameter at the end
public void antStart(Activity a, boolean SpeedRequest, boolean HeartRequest, boolean GarminKey, boolean Treadmill, boolean BikeRequest, boolean TechnoGymGroupCycle, int AntBikeDeviceNumber, int AntHeartDeviceNumber) {
QLog.v(TAG, "antStart");
speedRequest = SpeedRequest;
heartRequest = HeartRequest;
treadmill = Treadmill;
garminKey = GarminKey;
bikeRequest = BikeRequest; // Set bike request flag
remoteControlRequest = RemoteControlRequest; // Set remote control request flag
technoGymGroupCycle = TechnoGymGroupCycle;
antBikeDeviceNumber = AntBikeDeviceNumber;
antHeartDeviceNumber = AntHeartDeviceNumber;
antRemoteControlDeviceNumber = AntRemoteControlDeviceNumber;
activity = a;
if(a != null)
QLog.v(TAG, "antStart activity is valid");
@@ -164,29 +159,14 @@ public class Ant {
return mChannelService.isBikeConnected();
}
public void updateBikeTransmitterExtendedMetrics(long distanceMeters, int heartRate,
double elapsedTimeSeconds, int resistance,
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,
mChannelService.updateBikeTransmitterExtendedMetrics(distanceMeters, heartRate,
elapsedTimeSeconds, resistance,
inclination);
}
// Remote Control methods
public boolean isRemoteControlConnected() {
if(mChannelService == null)
return false;
QLog.v(TAG, "isRemoteControlConnected");
return mChannelService.isRemoteControlConnected();
}
public void setRemoteControlDeviceNumber(int deviceNumber) {
if(mChannelService == null)
return;
QLog.v(TAG, "setRemoteControlDeviceNumber: " + deviceNumber);
mChannelService.setRemoteControlDeviceNumber(deviceNumber);
}
}

View File

@@ -1,438 +0,0 @@
/*
* ANT+ Remote Control implementation for QDomyos-Zwift
* Based on Golden Cheetah RemoteControl implementation
* Maps ANT+ remote control commands to workout controls
*/
package org.cagnulen.qdomyoszwift;
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.BroadcastDataMessage;
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 android.util.Log;
import org.cagnulen.qdomyoszwift.QLog;
/**
* ANT+ Remote Control Channel Controller
* Handles ANT+ Control Device Profile (Device Type 0x10)
*/
public class AntRemoteControl {
private static final String TAG = "AntRemoteControl";
// ANT+ Control Device Profile constants
private static final int CONTROL_DEVICE_TYPE = 0x10;
private static final int CONTROL_TRANSMISSION_TYPE = 0x05;
private static final short CONTROL_PERIOD = 8192; // 4 Hz
private static final int CONTROL_FREQUENCY = 57; // 2457 MHz
// ANT+ Control Generic Command Page
private static final byte ANT_CONTROL_GENERIC_CMD_PAGE = 0x49;
// ANT+ Generic Commands (from Golden Cheetah)
private static final int ANT_CONTROL_GENERIC_CMD_MENU_UP = 0x00;
private static final int ANT_CONTROL_GENERIC_CMD_MENU_DOWN = 0x01;
private static final int ANT_CONTROL_GENERIC_CMD_MENU_SELECT = 0x02;
private static final int ANT_CONTROL_GENERIC_CMD_MENU_BACK = 0x03;
private static final int ANT_CONTROL_GENERIC_CMD_HOME = 0x04;
private static final int ANT_CONTROL_GENERIC_CMD_START = 0x20;
private static final int ANT_CONTROL_GENERIC_CMD_STOP = 0x21;
private static final int ANT_CONTROL_GENERIC_CMD_RESET = 0x22;
private static final int ANT_CONTROL_GENERIC_CMD_LENGTH = 0x23;
private static final int ANT_CONTROL_GENERIC_CMD_LAP = 0x24;
private static final int ANT_CONTROL_GENERIC_CMD_USER_1 = 0x8000;
private static final int ANT_CONTROL_GENERIC_CMD_USER_2 = 0x8001;
private static final int ANT_CONTROL_GENERIC_CMD_USER_3 = 0x8002;
private AntChannel mAntChannel;
private ChannelController mChannelController;
private boolean isChannelOpen = false;
private int deviceNumber = 0; // 0 means wildcard - accept any remote
// Native methods for communicating with Qt layer
public static native void nativeOnRemoteCommand(int command);
public static native void nativeGearUp();
public static native void nativeGearDown();
/**
* Channel Controller for handling ANT+ events
*/
private class ChannelController implements IAntChannelEventHandler {
@Override
public void onChannelDeath() {
QLog.w(TAG, "onChannelDeath: Remote Control Channel Death - cleaning up");
isChannelOpen = false;
}
@Override
public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) {
QLog.v(TAG, "onReceiveMessage: messageType=" + messageType + ", parcel=" + antParcel);
switch(messageType) {
case ACKNOWLEDGED_DATA:
QLog.d(TAG, "onReceiveMessage: Received ACKNOWLEDGED_DATA");
AcknowledgedDataMessage ackMsg = new AcknowledgedDataMessage(antParcel);
byte[] ackPayload = ackMsg.getPayload();
QLog.v(TAG, "onReceiveMessage: ACKNOWLEDGED_DATA payload length=" + (ackPayload != null ? ackPayload.length : 0));
handleDataMessage(ackPayload);
break;
case BROADCAST_DATA:
QLog.d(TAG, "onReceiveMessage: Received BROADCAST_DATA");
BroadcastDataMessage broadcastMsg = new BroadcastDataMessage(antParcel);
byte[] broadcastPayload = broadcastMsg.getPayload();
QLog.v(TAG, "onReceiveMessage: BROADCAST_DATA payload length=" + (broadcastPayload != null ? broadcastPayload.length : 0));
handleDataMessage(broadcastPayload);
break;
case CHANNEL_EVENT:
ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel);
EventCode code = eventMessage.getEventCode();
QLog.d(TAG, "onReceiveMessage: CHANNEL_EVENT - eventCode=" + code);
switch(code) {
/* case CHANNEL_IN_WRONG_STATE:
QLog.w(TAG, "onReceiveMessage: CHANNEL_IN_WRONG_STATE error");
break;*/
case CHANNEL_COLLISION:
QLog.w(TAG, "onReceiveMessage: CHANNEL_COLLISION error");
break;
case TRANSFER_TX_FAILED:
QLog.w(TAG, "onReceiveMessage: TRANSFER_TX_FAILED error");
break;
case RX_SEARCH_TIMEOUT:
QLog.i(TAG, "onReceiveMessage: RX_SEARCH_TIMEOUT - no remote control found");
break;
case CHANNEL_CLOSED:
QLog.i(TAG, "onReceiveMessage: CHANNEL_CLOSED");
isChannelOpen = false;
break;
default:
QLog.v(TAG, "onReceiveMessage: Other channel event=" + code);
break;
}
break;
default:
QLog.d(TAG, "onReceiveMessage: Unhandled messageType=" + messageType + ", parcel=" + antParcel);
break;
}
}
/**
* Handle incoming data messages from ANT+ remote control
*/
private void handleDataMessage(byte[] payload) {
QLog.v(TAG, "handleDataMessage: called with payload=" + (payload != null ? "length=" + payload.length : "null"));
if (payload == null) {
QLog.w(TAG, "handleDataMessage: payload is null, ignoring");
return;
}
if (payload.length < 8) {
QLog.w(TAG, "handleDataMessage: payload too short (length=" + payload.length + "), expected 8 bytes");
return;
}
// Log raw payload for debugging
StringBuilder payloadHex = new StringBuilder();
for (int i = 0; i < payload.length; i++) {
payloadHex.append(String.format("%02X ", payload[i]));
}
QLog.v(TAG, "handleDataMessage: raw payload: " + payloadHex.toString());
// Check if this is a Generic Command Page
byte pageNumber = payload[0];
QLog.d(TAG, "handleDataMessage: pageNumber=0x" + Integer.toHexString(pageNumber & 0xFF) +
" (expected=0x" + Integer.toHexString(ANT_CONTROL_GENERIC_CMD_PAGE & 0xFF) + ")");
if (pageNumber != ANT_CONTROL_GENERIC_CMD_PAGE) {
QLog.w(TAG, "handleDataMessage: not a Generic Command Page, ignoring (pageNumber=0x" +
Integer.toHexString(pageNumber & 0xFF) + ")");
return;
}
// Extract command from payload
// Command is in bytes 1-2 (little endian)
int command = ((payload[2] & 0xFF) << 8) | (payload[1] & 0xFF);
QLog.i(TAG, "handleDataMessage: extracted ANT+ Remote Command=0x" + Integer.toHexString(command) +
" from bytes[1]=0x" + Integer.toHexString(payload[1] & 0xFF) +
", bytes[2]=0x" + Integer.toHexString(payload[2] & 0xFF));
// Map commands to actions
handleRemoteCommand(command);
}
/**
* Handle remote control commands and map them to QDomyos-Zwift actions
*/
private void handleRemoteCommand(int command) {
QLog.d(TAG, "handleRemoteCommand: processing command=0x" + Integer.toHexString(command));
switch(command) {
case ANT_CONTROL_GENERIC_CMD_MENU_UP:
QLog.i(TAG, "handleRemoteCommand: MENU_UP -> Gear Up (like Zwift Click)");
try {
nativeGearUp();
QLog.d(TAG, "handleRemoteCommand: nativeGearUp() called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeGearUp()", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_MENU_DOWN:
QLog.i(TAG, "handleRemoteCommand: MENU_DOWN -> Gear Down (like Zwift Click)");
try {
nativeGearDown();
QLog.d(TAG, "handleRemoteCommand: nativeGearDown() called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeGearDown()", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_MENU_SELECT:
QLog.i(TAG, "handleRemoteCommand: MENU_SELECT -> Select action");
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(SELECT) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(SELECT)", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_MENU_BACK:
QLog.i(TAG, "handleRemoteCommand: MENU_BACK -> Back action");
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(BACK) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(BACK)", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_HOME:
QLog.i(TAG, "handleRemoteCommand: HOME -> Home action");
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(HOME) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(HOME)", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_START:
QLog.i(TAG, "handleRemoteCommand: START -> Start workout");
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(START) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(START)", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_STOP:
QLog.i(TAG, "handleRemoteCommand: STOP -> Stop workout");
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(STOP) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(STOP)", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_LAP:
QLog.i(TAG, "handleRemoteCommand: LAP -> Lap marker");
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(LAP) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(LAP)", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_RESET:
QLog.i(TAG, "handleRemoteCommand: RESET -> Reset action");
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(RESET) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(RESET)", e);
}
break;
case ANT_CONTROL_GENERIC_CMD_USER_1:
case ANT_CONTROL_GENERIC_CMD_USER_2:
case ANT_CONTROL_GENERIC_CMD_USER_3:
QLog.i(TAG, "handleRemoteCommand: USER_" + (command - ANT_CONTROL_GENERIC_CMD_USER_1 + 1) + " -> User action");
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(USER) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(USER)", e);
}
break;
default:
QLog.w(TAG, "handleRemoteCommand: Unknown/unmapped command=0x" + Integer.toHexString(command));
try {
nativeOnRemoteCommand(command);
QLog.d(TAG, "handleRemoteCommand: nativeOnRemoteCommand(UNKNOWN) called successfully");
} catch (Exception e) {
QLog.e(TAG, "handleRemoteCommand: Error calling nativeOnRemoteCommand(UNKNOWN)", e);
}
break;
}
}
}
/**
* Constructor
*/
public AntRemoteControl() {
QLog.i(TAG, "AntRemoteControl: constructor - initializing ANT+ Remote Control");
mChannelController = new ChannelController();
QLog.d(TAG, "AntRemoteControl: constructor completed, channel controller created");
}
/**
* Open ANT+ remote control channel
*/
public boolean openChannel(AntChannel antChannel) {
QLog.i(TAG, "openChannel: request to open ANT+ Remote Control channel");
if (isChannelOpen) {
QLog.w(TAG, "openChannel: Remote control channel already open, ignoring request");
return false;
}
if (antChannel == null) {
QLog.e(TAG, "openChannel: antChannel is null, cannot proceed");
return false;
}
mAntChannel = antChannel;
QLog.d(TAG, "openChannel: antChannel assigned");
try {
// Configure the channel
ChannelId channelId = new ChannelId(deviceNumber, CONTROL_DEVICE_TYPE, CONTROL_TRANSMISSION_TYPE);
QLog.d(TAG, "openChannel: created ChannelId - deviceNumber=" + deviceNumber +
", deviceType=0x" + Integer.toHexString(CONTROL_DEVICE_TYPE) +
", transmissionType=0x" + Integer.toHexString(CONTROL_TRANSMISSION_TYPE));
QLog.i(TAG, "openChannel: configuring ANT+ Remote Control channel" +
" (deviceNumber=" + deviceNumber + ", frequency=" + CONTROL_FREQUENCY +
"MHz, period=" + CONTROL_PERIOD + ")");
// Assign the channel with slave configuration
QLog.d(TAG, "openChannel: assigning channel as SLAVE_RECEIVE_ONLY");
mAntChannel.assign(ChannelType.SLAVE_RECEIVE_ONLY);
// Set the channel ID
QLog.d(TAG, "openChannel: setting channel ID");
mAntChannel.setChannelId(channelId);
// Set the period
QLog.d(TAG, "openChannel: setting period=" + CONTROL_PERIOD);
mAntChannel.setPeriod(CONTROL_PERIOD);
// Set the RF frequency
QLog.d(TAG, "openChannel: setting RF frequency=" + CONTROL_FREQUENCY);
mAntChannel.setRfFrequency(CONTROL_FREQUENCY);
// Register event handler
QLog.d(TAG, "openChannel: registering channel event handler");
mAntChannel.setChannelEventHandler(mChannelController);
// Open the channel
QLog.d(TAG, "openChannel: opening the channel");
mAntChannel.open();
isChannelOpen = true;
QLog.i(TAG, "openChannel: ANT+ Remote Control channel opened successfully, now listening for commands");
return true;
} catch (RemoteException e) {
QLog.e(TAG, "openChannel: RemoteException while opening ANT+ Remote Control channel", e);
isChannelOpen = false;
return false;
} catch (AntCommandFailedException e) {
QLog.e(TAG, "openChannel: AntCommandFailedException while opening ANT+ Remote Control channel", e);
isChannelOpen = false;
return false;
}
}
/**
* Close ANT+ remote control channel
*/
public void closeChannel() {
QLog.i(TAG, "closeChannel: request to close ANT+ Remote Control channel");
if (!isChannelOpen && mAntChannel == null) {
QLog.d(TAG, "closeChannel: channel not open and null, nothing to do");
return;
}
if (mAntChannel == null) {
QLog.w(TAG, "closeChannel: channel marked as open but mAntChannel is null, clearing flag");
isChannelOpen = false;
return;
}
try {
QLog.d(TAG, "closeChannel: closing ANT+ Remote Control channel");
mAntChannel.close();
QLog.d(TAG, "closeChannel: channel closed, releasing resources");
mAntChannel.release();
QLog.i(TAG, "closeChannel: ANT+ Remote Control channel closed and released successfully");
} catch (RemoteException e) {
QLog.e(TAG, "closeChannel: RemoteException while closing ANT+ Remote Control channel", e);
} catch (AntCommandFailedException e) {
QLog.e(TAG, "closeChannel: AntCommandFailedException while closing ANT+ Remote Control channel", e);
}
isChannelOpen = false;
mAntChannel = null;
QLog.d(TAG, "closeChannel: cleanup completed");
}
/**
* Check if channel is open
*/
public boolean isChannelOpen() {
boolean channelOpen = isChannelOpen;
QLog.v(TAG, "isChannelOpen: returning " + channelOpen);
return channelOpen;
}
/**
* Set the device number to search for (0 = wildcard)
*/
public void setDeviceNumber(int deviceNumber) {
QLog.d(TAG, "setDeviceNumber: changing deviceNumber from " + this.deviceNumber + " to " + deviceNumber);
if (deviceNumber == 0) {
QLog.i(TAG, "setDeviceNumber: using wildcard (0) to accept any remote control");
} else {
QLog.i(TAG, "setDeviceNumber: will only accept remote control with device number " + deviceNumber);
}
this.deviceNumber = deviceNumber;
}
/**
* Get the current device number
*/
public int getDeviceNumber() {
QLog.v(TAG, "getDeviceNumber: returning " + deviceNumber);
return deviceNumber;
}
}

View File

@@ -59,7 +59,6 @@ public class ChannelService extends Service {
SDMChannelController sdmChannelController = null;
BikeChannelController bikeChannelController = null; // Added BikeChannelController reference
BikeTransmitterController bikeTransmitterController = null; // Added BikeTransmitterController reference
AntRemoteControl antRemoteControl = null; // Added AntRemoteControl reference
private ServiceConnection mAntRadioServiceConnection = new ServiceConnection() {
@Override
@@ -308,41 +307,6 @@ public class ChannelService extends Service {
return "Bike transmitter not initialized";
}
// ========== REMOTE CONTROL METHODS ==========
/**
* Check if remote control is connected
*/
boolean isRemoteControlConnected() {
QLog.v(TAG, "ChannelServiceComm.isRemoteControlConnected");
return (antRemoteControl != null && antRemoteControl.isChannelOpen());
}
/**
* Set the remote control device number (0 = wildcard)
*/
void setRemoteControlDeviceNumber(int deviceNumber) {
QLog.v(TAG, "ChannelServiceComm.setRemoteControlDeviceNumber: " + deviceNumber);
if (antRemoteControl != null) {
antRemoteControl.setDeviceNumber(deviceNumber);
QLog.d(TAG, "Remote control device number updated to: " + deviceNumber);
} else {
QLog.w(TAG, "Remote control not initialized, cannot set device number");
}
}
/**
* Get remote control status info for debugging
*/
String getRemoteControlInfo() {
if (antRemoteControl != null) {
return "Remote control initialized, channel open: " + antRemoteControl.isChannelOpen() +
", device number: " + antRemoteControl.getDeviceNumber();
}
return "Remote control not initialized";
}
/**
* Closes all channels currently added.
*/
@@ -429,37 +393,6 @@ public class ChannelService extends Service {
bikeTransmitterController = null;
}
}
// Add initialization for AntRemoteControl
if (Ant.remoteControlRequest && antRemoteControl == null) {
QLog.i(TAG, "Initializing AntRemoteControl");
try {
// Create remote control instance
antRemoteControl = new AntRemoteControl();
// Set device number (0 = wildcard for any remote)
antRemoteControl.setDeviceNumber(Ant.antRemoteControlDeviceNumber);
// Acquire channel like other controllers
AntChannel remoteChannel = acquireChannel();
if (remoteChannel != null) {
boolean channelOpened = antRemoteControl.openChannel(remoteChannel);
if (channelOpened) {
QLog.i(TAG, "AntRemoteControl initialized and channel opened successfully");
} else {
QLog.e(TAG, "Failed to open AntRemoteControl channel");
antRemoteControl = null;
}
} else {
QLog.e(TAG, "Failed to acquire channel for AntRemoteControl");
antRemoteControl = null;
}
} catch (Exception e) {
QLog.e(TAG, "Failed to initialize AntRemoteControl: " + e.getMessage(), e);
antRemoteControl = null;
}
}
}
private void closeAllChannels() {
@@ -476,9 +409,6 @@ public class ChannelService extends Service {
if (bikeTransmitterController != null) { // Added closing bikeTransmitterController
bikeTransmitterController.close(); // Use close() method like other controllers
}
if (antRemoteControl != null) { // Added closing antRemoteControl
antRemoteControl.closeChannel();
}
heartChannelController = null;
powerChannelController = null;
@@ -486,7 +416,6 @@ public class ChannelService extends Service {
sdmChannelController = null;
bikeChannelController = null; // Added nullifying bikeChannelController
bikeTransmitterController = null; // Added nullifying bikeTransmitterController
antRemoteControl = null; // Added nullifying antRemoteControl
}
AntChannel acquireChannel() throws ChannelNotAvailableException {

View File

@@ -34,6 +34,17 @@ wahookickrsnapbike::wahookickrsnapbike(bool noWriteResistance, bool noHeartServi
refresh->start(settings.value(QZSettings::poll_device_time, QZSettings::default_poll_device_time).toInt());
wheelCircumference::GearTable g;
g.printTable();
// Setup op timeout handler for serialized commands
_opTimeout.setSingleShot(true);
connect(&_opTimeout, &QTimer::timeout, this, [this]() {
// Timeout waiting for ack; clear and try next pending to avoid stall
if (_currentOp != WahooOp::None) {
emit debug(QStringLiteral("Ack timeout; releasing op and continuing queue"));
}
_currentOp = WahooOp::None;
processNextPending();
});
}
void wahookickrsnapbike::restoreDefaultWheelDiameter() {
@@ -305,10 +316,8 @@ void wahookickrsnapbike::update() {
if(KICKR_SNAP) {
inclinationChanged(lastGrade, lastGrade);
} else {
QByteArray a = setWheelCircumference(wheelCircumference::gearsToWheelDiameter(gears()));
uint8_t b[20];
memcpy(b, a.constData(), a.length());
writeCharacteristic(b, a.length(), "setWheelCircumference", false, false);
// Queue wheel circumference change (higher priority)
sendWheelCircumferenceNow(wheelCircumference::gearsToWheelDiameter(gears()));
lastGrade = 999; // to force a change
}
}
@@ -442,6 +451,26 @@ void wahookickrsnapbike::handleCharacteristicValueChanged(const QBluetoothUuid &
qDebug() << QStringLiteral(" << ") << newValue.toHex(' ') << uuid;
// Detect acks for serialized commands (format: 0x01 <cmdId> 0x01 0x00 ...)
if (newValue.size() >= 3 && (uint8_t)newValue.at(0) == 0x01) {
uint8_t cmd = (uint8_t)newValue.at(1);
uint8_t status = (uint8_t)newValue.at(2);
if ((cmd == _setSimGrade || cmd == _setWheelCircumference) && status == 0x01) {
if(cmd == _setWheelCircumference) {
homeform::singleton()->setToastRequested("Gear accepted from the trainer");
}
// Ack received for our tracked op; release and continue
if ((_currentOp == WahooOp::SimGrade && cmd == _setSimGrade) ||
(_currentOp == WahooOp::WheelCircumference && cmd == _setWheelCircumference)) {
_opTimeout.stop();
_currentOp = WahooOp::None;
processNextPending();
}
}
}
if (uuid == QBluetoothUuid::CyclingPowerMeasurement) {
lastPacket = newValue;
@@ -964,10 +993,7 @@ void wahookickrsnapbike::inclinationChanged(double grade, double percentage) {
emit debug(QStringLiteral("writing inclination ") + QString::number(grade));
double g = grade;
g += gears();
QByteArray a = setSimGrade(g);
uint8_t b[20];
memcpy(b, a.constData(), a.length());
writeCharacteristic(b, a.length(), "setSimGrade", false, false);
sendSimGradeNow(g);
} else {
if(lastCommandErgMode) {
lastGrade = grade + 1; // to force a refresh
@@ -987,14 +1013,59 @@ void wahookickrsnapbike::inclinationChanged(double grade, double percentage) {
g += gears() * 0.5;
qDebug() << "adding gear offset so " << g;
}
QByteArray a = setSimGrade(g);
uint8_t b[20];
memcpy(b, a.constData(), a.length());
writeCharacteristic(b, a.length(), "setSimGrade", false, false);
sendSimGradeNow(g);
lastCommandErgMode = false;
}
}
// Send or enqueue: SimGrade
void wahookickrsnapbike::sendSimGradeNow(double grade) {
// If an operation is in flight, store latest grade and return
if (_currentOp != WahooOp::None) {
_pendingSimGrade = true;
_pendingSimGradeValue = grade;
return;
}
QByteArray a = setSimGrade(grade);
uint8_t b[20];
memcpy(b, a.constData(), a.length());
_currentOp = WahooOp::SimGrade;
// Send without blocking; wait for explicit ack in handleCharacteristicValueChanged
writeCharacteristic(b, a.length(), "setSimGrade", false, false);
_opTimeout.start(1000);
}
// Send or enqueue: Wheel Circumference
void wahookickrsnapbike::sendWheelCircumferenceNow(double mm) {
// If an operation is in flight, prefer to hold latest wheel circ (priority for next send)
if (_currentOp != WahooOp::None) {
_pendingWheelCirc = true;
_pendingWheelCircValue = mm;
return;
}
QByteArray a = setWheelCircumference(mm);
uint8_t b[20];
memcpy(b, a.constData(), a.length());
_currentOp = WahooOp::WheelCircumference;
writeCharacteristic(b, a.length(), "setWheelCircumference", false, false);
_opTimeout.start(1000);
}
// Process next pending item, wheel circumference has priority
void wahookickrsnapbike::processNextPending() {
if (_currentOp != WahooOp::None) return;
if (_pendingWheelCirc) {
_pendingWheelCirc = false;
sendWheelCircumferenceNow(_pendingWheelCircValue);
return;
}
if (_pendingSimGrade) {
_pendingSimGrade = false;
sendSimGradeNow(_pendingSimGradeValue);
return;
}
}
bool wahookickrsnapbike::inclinationAvailableByHardware() {
return KICKR_BIKE;
}

View File

@@ -81,6 +81,18 @@ class wahookickrsnapbike : public bike {
bool writeCharacteristic(uint8_t *data, uint8_t data_len, QString info, bool disable_log = false,
bool wait_for_response = false);
// Serialized command handling for setSimGrade and setWheelCircumference
enum class WahooOp { None, SimGrade, WheelCircumference };
WahooOp _currentOp = WahooOp::None;
bool _pendingSimGrade = false;
double _pendingSimGradeValue = 0.0;
bool _pendingWheelCirc = false;
double _pendingWheelCircValue = 0.0;
QTimer _opTimeout;
void processNextPending();
void sendSimGradeNow(double grade);
void sendWheelCircumferenceNow(double mm);
uint16_t wattsFromResistance(double resistance);
metric ResistanceFromFTMSAccessory;
void startDiscover();

View File

@@ -685,8 +685,6 @@ const QString QZSettings::ftms_treadmill = QStringLiteral("ftms_treadmill");
const QString QZSettings::default_ftms_treadmill = QStringLiteral("Disabled");
const QString QZSettings::ant_speed_offset = QStringLiteral("ant_speed_offset");
const QString QZSettings::ant_speed_gain = QStringLiteral("ant_speed_gain");
const QString QZSettings::ant_remote_control = QStringLiteral("ant_remote_control");
const QString QZSettings::ant_remote_control_device_number = QStringLiteral("ant_remote_control_device_number");
const QString QZSettings::proform_rower_sport_rl = QStringLiteral("proform_rower_sport_rl");
const QString QZSettings::strava_date_prefix = QStringLiteral("strava_date_prefix");
const QString QZSettings::race_mode = QStringLiteral("race_mode");
@@ -985,7 +983,7 @@ const QString QZSettings::calories_from_hr = QStringLiteral("calories_from_hr");
const QString QZSettings::height = QStringLiteral("height");
const uint32_t allSettingsCount = 807;
const uint32_t allSettingsCount = 805;
QVariant allSettings[allSettingsCount][2] = {
{QZSettings::cryptoKeySettingsProfiles, QZSettings::default_cryptoKeySettingsProfiles},
@@ -1564,8 +1562,6 @@ QVariant allSettings[allSettingsCount][2] = {
{QZSettings::ftms_treadmill, QZSettings::default_ftms_treadmill},
{QZSettings::ant_speed_offset, QZSettings::default_ant_speed_offset},
{QZSettings::ant_speed_gain, QZSettings::default_ant_speed_gain},
{QZSettings::ant_remote_control, QZSettings::default_ant_remote_control},
{QZSettings::ant_remote_control_device_number, QZSettings::default_ant_remote_control_device_number},
{QZSettings::proform_rower_sport_rl, QZSettings::default_proform_rower_sport_rl},
{QZSettings::strava_date_prefix, QZSettings::default_strava_date_prefix},
{QZSettings::race_mode, QZSettings::default_race_mode},

View File

@@ -1930,12 +1930,6 @@ class QZSettings {
static const QString ant_speed_gain;
static constexpr float default_ant_speed_gain = 1;
static const QString ant_remote_control;
static constexpr bool default_ant_remote_control = false;
static const QString ant_remote_control_device_number;
static constexpr int default_ant_remote_control_device_number = 0;
static const QString race_mode;
static constexpr bool default_race_mode = false;

View File

@@ -1205,8 +1205,6 @@ import Qt.labs.platform 1.1
property int chart_display_mode: 0
property bool zwift_play_vibration: true
property bool toorxtreadmill_discovery_completed: false
property bool ant_remote_control: false
property int ant_remote_control_device_number: 0
}
@@ -4634,69 +4632,6 @@ import Qt.labs.platform 1.1
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
IndicatorOnlySwitch {
text: qsTr("ANT+ Remote Control")
spacing: 0
bottomPadding: 0
topPadding: 0
rightPadding: 0
leftPadding: 0
clip: false
checked: settings.ant_remote_control
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
onClicked: { settings.ant_remote_control = checked; window.settings_restart_to_apply = true; }
}
Label {
text: qsTr("Enable ANT+ Remote Control support (like Zwift Click). Menu Up/Down buttons control gear shifting. Works with standard ANT+ Control Device remotes. Default: Disabled")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
RowLayout {
spacing: 10
Label {
text: qsTr("ANT+ Remote Control Device Number (0 = any):")
Layout.fillWidth: true
}
TextField {
id: antRemoteControlDeviceNumberTextField
text: settings.ant_remote_control_device_number
horizontalAlignment: Text.AlignRight
Layout.fillHeight: false
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
inputMethodHints: Qt.ImhDigitsOnly
onAccepted: settings.ant_remote_control_device_number = text
onActiveFocusChanged: if(this.focus) this.cursorPosition = this.text.length
}
Button {
text: "OK"
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
onClicked: { settings.ant_remote_control_device_number = antRemoteControlDeviceNumberTextField.text; window.settings_restart_to_apply = true; toast.show("Setting saved!"); }
}
}
Label {
text: qsTr("Set device number to 0 to accept any ANT+ remote control, or specify a specific device number to pair with only that remote.")
font.bold: true
font.italic: true
font.pixelSize: Qt.application.font.pixelSize - 2
textFormat: Text.PlainText
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillWidth: true
color: Material.color(Material.Lime)
}
}
}
/*