mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
fixup! feature: add analog paddle support for Zwift Ride
This commit is contained in:
@@ -4,8 +4,8 @@ import 'package:protobuf/protobuf.dart' as $pb;
|
||||
import 'package:swift_control/bluetooth/devices/base_device.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift_clickv2.dart';
|
||||
import 'package:swift_control/bluetooth/messages/ride_notification.dart';
|
||||
import 'package:swift_control/bluetooth/protocol/protobuf_parser.dart';
|
||||
import 'package:swift_control/bluetooth/protocol/zp_vendor.pb.dart';
|
||||
import 'package:swift_control/bluetooth/protocol/zwift.pb.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
@@ -208,74 +208,31 @@ class ZwiftRide extends BaseDevice {
|
||||
Future<List<ZwiftButton>?> processClickNotification(Uint8List message) async {
|
||||
final RideNotification clickNotification = RideNotification(message);
|
||||
|
||||
// Parse embedded analog paddle data from controller notification message.
|
||||
// The Zwift Ride paddles send analog pressure values (-100 to 100) that need to be
|
||||
// extracted from the raw Protocol Buffer message structure.
|
||||
//
|
||||
// Message structure (after button map at offset 7):
|
||||
// - Location 0 (left paddle): Embedded directly without 0x1a prefix
|
||||
// - Location 1-3 (right paddle, unused): Prefixed with 0x1a marker
|
||||
//
|
||||
// This implementation mirrors the JavaScript reference from:
|
||||
// https://www.makinolo.com/blog/2024/07/26/zwift-ride-protocol/
|
||||
final allAnalogValues = <int, int>{};
|
||||
var offset = 7; // Skip message type (1), field number (1), and button map (5)
|
||||
// Parse analog paddle data using the auto-generated protobuf classes.
|
||||
// All analog paddles (L0-L3) appear in field 3 as repeated RideAnalogKeyPress
|
||||
try {
|
||||
final status = RideKeyPadStatus.fromBuffer(message);
|
||||
|
||||
// Parse first analog location (L0 - left paddle) which appears directly
|
||||
// in the message without the 0x1a section marker
|
||||
if (offset < message.length && message[offset] != 0x1a) {
|
||||
try {
|
||||
final firstAnalog = ProtobufParser.parseKeyGroup(message.sublist(offset));
|
||||
allAnalogValues.addAll(firstAnalog);
|
||||
// Process all analog paddles
|
||||
for (final paddle in status.analogPaddles) {
|
||||
if (paddle.hasLocation() && paddle.hasAnalogValue()) {
|
||||
if (paddle.analogValue.abs() >= analogPaddleThreshold) {
|
||||
final button = switch (paddle.location.value) {
|
||||
0 => ZwiftButton.paddleLeft, // L0 = left paddle
|
||||
1 => ZwiftButton.paddleRight, // L1 = right paddle
|
||||
_ => null, // L2, L3 unused
|
||||
};
|
||||
|
||||
// Advance to next 0x1a section
|
||||
while (offset < message.length && message[offset] != 0x1a) {
|
||||
offset++;
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip to 0x1a sections on parse error
|
||||
while (offset < message.length && message[offset] != 0x1a) {
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse remaining analog locations (L1, L2, L3, etc.) which are wrapped
|
||||
// in Protocol Buffer message sections prefixed with 0x1a
|
||||
while (offset < message.length) {
|
||||
if (offset < message.length && message[offset] == 0x1a) {
|
||||
try {
|
||||
final analogData = message.sublist(offset);
|
||||
// Each analog section starts with 0x1a, skip it and parse the rest
|
||||
if (analogData.isNotEmpty && analogData[0] == 0x1a) {
|
||||
final parsedData = ProtobufParser.parseKeyGroup(analogData.sublist(1));
|
||||
allAnalogValues.addAll(parsedData);
|
||||
if (button != null) {
|
||||
clickNotification.buttonsClicked.add(button);
|
||||
clickNotification.analogButtons.add(button);
|
||||
}
|
||||
}
|
||||
|
||||
offset += ProtobufParser.findNextMarker(analogData, 0x1a);
|
||||
} catch (e) {
|
||||
offset++;
|
||||
}
|
||||
} else {
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert analog values to button presses when they exceed threshold.
|
||||
// Location 0 = left paddle, Location 1 = right paddle
|
||||
// Values range from -100 to 100, we use absolute value for threshold check
|
||||
for (final entry in allAnalogValues.entries) {
|
||||
if (entry.value.abs() >= analogPaddleThreshold) {
|
||||
final button = switch (entry.key) {
|
||||
0 => ZwiftButton.paddleLeft,
|
||||
1 => ZwiftButton.paddleRight,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (button != null) {
|
||||
clickNotification.buttonsClicked.add(button);
|
||||
clickNotification.analogButtons.add(button);
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Error parsing analog paddle data: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,12 +64,8 @@ class RideNotification extends BaseNotification {
|
||||
|
||||
// Process ANALOG inputs separately - now properly separated from digital
|
||||
// Note: Analog paddle parsing is handled in ZwiftRide.processClickNotification
|
||||
// by manually parsing the embedded Protocol Buffer data, as the protobuf
|
||||
// structure doesn't correctly expose paddle analog values.
|
||||
// using the auto-generated protobuf classes (field 3 AnalogPaddles).
|
||||
analogButtons = [];
|
||||
|
||||
// Combine digital and analog buttons for backward compatibility
|
||||
buttonsClicked.addAll(analogButtons);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
/// Utility class for parsing Protocol Buffer messages manually.
|
||||
///
|
||||
/// This is needed when the auto-generated protobuf classes don't correctly
|
||||
/// handle embedded or non-standard Protocol Buffer structures, such as the
|
||||
/// Zwift Ride analog paddle data.
|
||||
///
|
||||
/// Reference: https://developers.google.com/protocol-buffers/docs/encoding
|
||||
class ProtobufParser {
|
||||
/// Decodes a ZigZag-encoded signed integer to its original value.
|
||||
///
|
||||
/// ZigZag encoding maps signed integers to unsigned integers:
|
||||
/// - 0 -> 0, -1 -> 1, 1 -> 2, -2 -> 3, 2 -> 4, etc.
|
||||
///
|
||||
/// Formula: (n >>> 1) ^ -(n & 1)
|
||||
static int zigzagDecode(int encoded) {
|
||||
return (encoded >>> 1) ^ -(encoded & 1);
|
||||
}
|
||||
|
||||
/// Decodes a Protocol Buffer varint from a buffer at the given offset.
|
||||
///
|
||||
/// Returns a record of (decodedValue, bytesConsumed).
|
||||
///
|
||||
/// Varints use the most significant bit (MSB) of each byte as a continuation
|
||||
/// flag. If MSB is 1, there are more bytes to read. If MSB is 0, it's the
|
||||
/// last byte.
|
||||
static (int, int) decodeVarint(Uint8List buffer, int offset) {
|
||||
var value = 0;
|
||||
var shift = 0;
|
||||
var bytesRead = 0;
|
||||
|
||||
while (offset + bytesRead < buffer.length) {
|
||||
final byte = buffer[offset + bytesRead];
|
||||
value |= (byte & 0x7f) << shift;
|
||||
bytesRead++;
|
||||
|
||||
if ((byte & 0x80) == 0) {
|
||||
// MSB is 0, we're done
|
||||
break;
|
||||
}
|
||||
shift += 7;
|
||||
}
|
||||
|
||||
return (value, bytesRead);
|
||||
}
|
||||
|
||||
/// Extracts the field number from a Protocol Buffer tag byte.
|
||||
///
|
||||
/// Tag format: (field_number << 3) | wire_type
|
||||
static int getFieldNumber(int tag) => tag >> 3;
|
||||
|
||||
/// Extracts the wire type from a Protocol Buffer tag byte.
|
||||
///
|
||||
/// Wire types:
|
||||
/// - 0: Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum)
|
||||
/// - 1: 64-bit (fixed64, sfixed64, double)
|
||||
/// - 2: Length-delimited (string, bytes, embedded messages, packed repeated)
|
||||
/// - 3: Start group (deprecated)
|
||||
/// - 4: End group (deprecated)
|
||||
/// - 5: 32-bit (fixed32, sfixed32, float)
|
||||
static int getWireType(int tag) => tag & 0x7;
|
||||
|
||||
/// Skips a Protocol Buffer field based on its wire type.
|
||||
///
|
||||
/// Returns the number of bytes skipped.
|
||||
static int skipField(Uint8List buffer, int offset, int wireType) {
|
||||
var bytesSkipped = 0;
|
||||
|
||||
switch (wireType) {
|
||||
case 0: // Varint
|
||||
while (offset + bytesSkipped < buffer.length) {
|
||||
final byte = buffer[offset + bytesSkipped];
|
||||
bytesSkipped++;
|
||||
if ((byte & 0x80) == 0) break; // MSB is 0, done
|
||||
}
|
||||
break;
|
||||
|
||||
case 2: // Length-delimited
|
||||
if (offset + bytesSkipped < buffer.length) {
|
||||
final length = buffer[offset + bytesSkipped];
|
||||
bytesSkipped += 1 + length;
|
||||
}
|
||||
break;
|
||||
|
||||
// Wire types 1 (64-bit) and 5 (32-bit) are not used in our case
|
||||
// but could be implemented if needed
|
||||
default:
|
||||
// Unknown wire type, skip one byte
|
||||
bytesSkipped = 1;
|
||||
}
|
||||
|
||||
return bytesSkipped;
|
||||
}
|
||||
|
||||
/// Parses a Protocol Buffer message containing location and analog value.
|
||||
///
|
||||
/// Expected fields:
|
||||
/// - Field 1: location (varint)
|
||||
/// - Field 2: analogValue (sint32, ZigZag encoded)
|
||||
///
|
||||
/// Returns a map with 'location' and 'value' keys.
|
||||
static Map<String, int> parseLocationValue(Uint8List buffer) {
|
||||
int? location;
|
||||
int? analogValue;
|
||||
var offset = 0;
|
||||
|
||||
while (offset < buffer.length) {
|
||||
final tag = buffer[offset];
|
||||
final fieldNum = getFieldNumber(tag);
|
||||
final wireType = getWireType(tag);
|
||||
offset++;
|
||||
|
||||
if (fieldNum == 1 && wireType == 0) {
|
||||
// Parse location field (varint)
|
||||
final (value, bytesRead) = decodeVarint(buffer, offset);
|
||||
location = value;
|
||||
offset += bytesRead;
|
||||
} else if (fieldNum == 2 && wireType == 0) {
|
||||
// Parse analog value field (ZigZag encoded sint32)
|
||||
final (value, bytesRead) = decodeVarint(buffer, offset);
|
||||
analogValue = zigzagDecode(value);
|
||||
offset += bytesRead;
|
||||
} else {
|
||||
// Skip unknown fields
|
||||
offset += skipField(buffer, offset, wireType);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'location': location ?? 0,
|
||||
'value': analogValue ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// Parses a Protocol Buffer key group containing analog sensor data.
|
||||
///
|
||||
/// Handles two formats:
|
||||
/// 1. Field 3 (wire type 2): Nested message containing location and value
|
||||
/// 2. Field 1 + Field 2 (wire type 0): Direct location and value fields
|
||||
///
|
||||
/// Returns a map of location IDs to analog values.
|
||||
static Map<int, int> parseKeyGroup(Uint8List buffer) {
|
||||
final groupStatus = <int, int>{};
|
||||
var offset = 0;
|
||||
|
||||
while (offset < buffer.length) {
|
||||
final tag = buffer[offset];
|
||||
final fieldNum = getFieldNumber(tag);
|
||||
final wireType = getWireType(tag);
|
||||
offset++;
|
||||
|
||||
if (fieldNum == 3 && wireType == 2) {
|
||||
// Nested message format
|
||||
final length = buffer[offset++];
|
||||
final messageBuffer = buffer.sublist(offset, offset + length);
|
||||
final res = parseLocationValue(messageBuffer);
|
||||
groupStatus[res['location']!] = res['value']!;
|
||||
offset += length;
|
||||
} else if (fieldNum == 1 && wireType == 0) {
|
||||
// Direct field format - parse location
|
||||
final (location, locationBytes) = decodeVarint(buffer, offset);
|
||||
offset += locationBytes;
|
||||
|
||||
// Parse corresponding value field
|
||||
if (offset < buffer.length) {
|
||||
final valueTag = buffer[offset];
|
||||
final valueFieldNum = getFieldNumber(valueTag);
|
||||
final valueWireType = getWireType(valueTag);
|
||||
offset++;
|
||||
|
||||
if (valueFieldNum == 2 && valueWireType == 0) {
|
||||
final (value, valueBytes) = decodeVarint(buffer, offset);
|
||||
final decodedValue = zigzagDecode(value);
|
||||
groupStatus[location] = decodedValue;
|
||||
offset += valueBytes;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Skip unknown fields
|
||||
offset += skipField(buffer, offset, wireType);
|
||||
}
|
||||
}
|
||||
|
||||
return groupStatus;
|
||||
}
|
||||
|
||||
/// Finds the offset to the next section with the given marker byte.
|
||||
///
|
||||
/// Returns the number of bytes to skip to reach the next section,
|
||||
/// or the total length if no more sections exist.
|
||||
static int findNextMarker(Uint8List data, int marker) {
|
||||
for (var i = 1; i < data.length; i++) {
|
||||
if (data[i] == marker) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return data.length;
|
||||
}
|
||||
}
|
||||
@@ -480,62 +480,19 @@ class RideAnalogKeyPress extends $pb.GeneratedMessage {
|
||||
void clearAnalogValue() => clearField(2);
|
||||
}
|
||||
|
||||
class RideAnalogKeyGroup extends $pb.GeneratedMessage {
|
||||
factory RideAnalogKeyGroup({
|
||||
$core.Iterable<RideAnalogKeyPress>? groupStatus,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (groupStatus != null) {
|
||||
$result.groupStatus.addAll(groupStatus);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
RideAnalogKeyGroup._() : super();
|
||||
factory RideAnalogKeyGroup.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory RideAnalogKeyGroup.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'RideAnalogKeyGroup', package: const $pb.PackageName(_omitMessageNames ? '' : 'de.jonasbark'), createEmptyInstance: create)
|
||||
..pc<RideAnalogKeyPress>(1, _omitFieldNames ? '' : 'GroupStatus', $pb.PbFieldType.PM, protoName: 'GroupStatus', subBuilder: RideAnalogKeyPress.create)
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
RideAnalogKeyGroup clone() => RideAnalogKeyGroup()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
RideAnalogKeyGroup copyWith(void Function(RideAnalogKeyGroup) updates) => super.copyWith((message) => updates(message as RideAnalogKeyGroup)) as RideAnalogKeyGroup;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static RideAnalogKeyGroup create() => RideAnalogKeyGroup._();
|
||||
RideAnalogKeyGroup createEmptyInstance() => create();
|
||||
static $pb.PbList<RideAnalogKeyGroup> createRepeated() => $pb.PbList<RideAnalogKeyGroup>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static RideAnalogKeyGroup getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<RideAnalogKeyGroup>(create);
|
||||
static RideAnalogKeyGroup? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.List<RideAnalogKeyPress> get groupStatus => $_getList(0);
|
||||
}
|
||||
|
||||
/// The command code prepending this message is 0x23
|
||||
/// All analog paddles (L0-L3) appear as repeated RideAnalogKeyPress in field 3
|
||||
class RideKeyPadStatus extends $pb.GeneratedMessage {
|
||||
factory RideKeyPadStatus({
|
||||
$core.int? buttonMap,
|
||||
RideAnalogKeyGroup? analogButtons,
|
||||
$core.Iterable<RideAnalogKeyPress>? analogPaddles,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (buttonMap != null) {
|
||||
$result.buttonMap = buttonMap;
|
||||
}
|
||||
if (analogButtons != null) {
|
||||
$result.analogButtons = analogButtons;
|
||||
if (analogPaddles != null) {
|
||||
$result.analogPaddles.addAll(analogPaddles);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
@@ -545,7 +502,7 @@ class RideKeyPadStatus extends $pb.GeneratedMessage {
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'RideKeyPadStatus', package: const $pb.PackageName(_omitMessageNames ? '' : 'de.jonasbark'), createEmptyInstance: create)
|
||||
..a<$core.int>(1, _omitFieldNames ? '' : 'ButtonMap', $pb.PbFieldType.OU3, protoName: 'ButtonMap')
|
||||
..aOM<RideAnalogKeyGroup>(2, _omitFieldNames ? '' : 'AnalogButtons', protoName: 'AnalogButtons', subBuilder: RideAnalogKeyGroup.create)
|
||||
..pc<RideAnalogKeyPress>(3, _omitFieldNames ? '' : 'AnalogPaddles', $pb.PbFieldType.PM, protoName: 'AnalogPaddles', subBuilder: RideAnalogKeyPress.create)
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@@ -579,16 +536,8 @@ class RideKeyPadStatus extends $pb.GeneratedMessage {
|
||||
@$pb.TagNumber(1)
|
||||
void clearButtonMap() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
RideAnalogKeyGroup get analogButtons => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set analogButtons(RideAnalogKeyGroup v) { setField(2, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasAnalogButtons() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearAnalogButtons() => clearField(2);
|
||||
@$pb.TagNumber(2)
|
||||
RideAnalogKeyGroup ensureAnalogButtons() => $_ensure(1);
|
||||
@$pb.TagNumber(3)
|
||||
$core.List<RideAnalogKeyPress> get analogPaddles => $_getList(1);
|
||||
}
|
||||
|
||||
/// ------------------ Zwift Click messages
|
||||
|
||||
@@ -170,33 +170,20 @@ final $typed_data.Uint8List rideAnalogKeyPressDescriptor = $convert.base64Decode
|
||||
'lkZUFuYWxvZ0xvY2F0aW9uUghMb2NhdGlvbhIgCgtBbmFsb2dWYWx1ZRgCIAEoEVILQW5hbG9n'
|
||||
'VmFsdWU=');
|
||||
|
||||
@$core.Deprecated('Use rideAnalogKeyGroupDescriptor instead')
|
||||
const RideAnalogKeyGroup$json = {
|
||||
'1': 'RideAnalogKeyGroup',
|
||||
'2': [
|
||||
{'1': 'GroupStatus', '3': 1, '4': 3, '5': 11, '6': '.de.jonasbark.RideAnalogKeyPress', '10': 'GroupStatus'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `RideAnalogKeyGroup`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List rideAnalogKeyGroupDescriptor = $convert.base64Decode(
|
||||
'ChJSaWRlQW5hbG9nS2V5R3JvdXASQgoLR3JvdXBTdGF0dXMYASADKAsyIC5kZS5qb25hc2Jhcm'
|
||||
'suUmlkZUFuYWxvZ0tleVByZXNzUgtHcm91cFN0YXR1cw==');
|
||||
|
||||
@$core.Deprecated('Use rideKeyPadStatusDescriptor instead')
|
||||
const RideKeyPadStatus$json = {
|
||||
'1': 'RideKeyPadStatus',
|
||||
'2': [
|
||||
{'1': 'ButtonMap', '3': 1, '4': 1, '5': 13, '10': 'ButtonMap'},
|
||||
{'1': 'AnalogButtons', '3': 2, '4': 1, '5': 11, '6': '.de.jonasbark.RideAnalogKeyGroup', '10': 'AnalogButtons'},
|
||||
{'1': 'AnalogPaddles', '3': 3, '4': 3, '5': 11, '6': '.de.jonasbark.RideAnalogKeyPress', '10': 'AnalogPaddles'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `RideKeyPadStatus`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List rideKeyPadStatusDescriptor = $convert.base64Decode(
|
||||
'ChBSaWRlS2V5UGFkU3RhdHVzEhwKCUJ1dHRvbk1hcBgBIAEoDVIJQnV0dG9uTWFwEkYKDUFuYW'
|
||||
'xvZ0J1dHRvbnMYAiABKAsyIC5kZS5qb25hc2JhcmsuUmlkZUFuYWxvZ0tleUdyb3VwUg1BbmFs'
|
||||
'b2dCdXR0b25z');
|
||||
'xvZ1BhZGRsZXMYAyADKAsyIC5kZS5qb25hc2JhcmsuUmlkZUFuYWxvZ0tleVByZXNzUg1BbmFs'
|
||||
'b2dQYWRkbGVz');
|
||||
|
||||
@$core.Deprecated('Use clickKeyPadStatusDescriptor instead')
|
||||
const ClickKeyPadStatus$json = {
|
||||
|
||||
@@ -79,14 +79,11 @@ message RideAnalogKeyPress {
|
||||
optional sint32 AnalogValue = 2;
|
||||
}
|
||||
|
||||
message RideAnalogKeyGroup {
|
||||
repeated RideAnalogKeyPress GroupStatus = 1;
|
||||
}
|
||||
|
||||
// The command code prepending this message is 0x23
|
||||
// All analog paddles (L0-L3) appear as repeated RideAnalogKeyPress in field 3
|
||||
message RideKeyPadStatus {
|
||||
optional uint32 ButtonMap = 1;
|
||||
optional RideAnalogKeyGroup AnalogButtons = 2;
|
||||
repeated RideAnalogKeyPress AnalogPaddles = 3; // Field 3 contains all paddles
|
||||
}
|
||||
|
||||
//------------------ Zwift Click messages
|
||||
|
||||
@@ -5,9 +5,7 @@ enum InGameAction {
|
||||
navigateRight,
|
||||
toggleUi,
|
||||
increaseResistance,
|
||||
decreaseResistance,
|
||||
steerLeft,
|
||||
steerRight;
|
||||
decreaseResistance;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -23,7 +21,7 @@ enum ZwiftButton {
|
||||
navigationRight._(InGameAction.navigateRight),
|
||||
onOffLeft._(InGameAction.toggleUi),
|
||||
sideButtonLeft._(InGameAction.shiftDown),
|
||||
paddleLeft._(null),
|
||||
paddleLeft._(InGameAction.shiftDown),
|
||||
|
||||
// zwift ride only
|
||||
shiftUpLeft._(InGameAction.shiftDown),
|
||||
@@ -37,7 +35,7 @@ enum ZwiftButton {
|
||||
y._(null),
|
||||
onOffRight._(InGameAction.toggleUi),
|
||||
sideButtonRight._(InGameAction.shiftUp),
|
||||
paddleRight._(null),
|
||||
paddleRight._(InGameAction.shiftUp),
|
||||
|
||||
// zwift ride only
|
||||
shiftUpRight._(InGameAction.shiftUp),
|
||||
|
||||
Reference in New Issue
Block a user