Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Possible fix ajchellew/zwiftplay#7 #8

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@
.externalNativeBuild
.cxx
local.properties
/.vs

.fake
23 changes: 20 additions & 3 deletions Windows/ConsoleApp/BLE/AbstractZapDevice.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
using ZwiftPlayConsoleApp.Utils;
using ZwiftPlayConsoleApp.Logging;
using ZwiftPlayConsoleApp.Utils;
using ZwiftPlayConsoleApp.Zap;
using ZwiftPlayConsoleApp.Zap.Crypto;

namespace ZwiftPlayConsoleApp.BLE;

public abstract class AbstractZapDevice
{
protected readonly IZwiftLogger _logger;

protected AbstractZapDevice(IZwiftLogger logger)
{
_logger = logger;
_zapEncryption = new ZapCrypto(_localKeyProvider);

}

public static bool Debug = false;

private readonly LocalKeyProvider _localKeyProvider = new();
protected readonly ZapCrypto _zapEncryption;
protected ZapCrypto _zapEncryption;

public void ResetEncryption()
{
_zapEncryption = new ZapCrypto(_localKeyProvider);
}

/*
protected AbstractZapDevice()
{
_zapEncryption = new ZapCrypto(_localKeyProvider);
}
*/

public byte[] BuildHandshakeStart()
{
ResetEncryption();
var buffer = new ByteBuffer();
buffer.WriteBytes(ZapConstants.RIDE_ON);
buffer.WriteBytes(ZapConstants.REQUEST_START);
Expand All @@ -29,7 +47,6 @@ public void ProcessCharacteristic(string characteristicName, byte[] bytes)
{
if (Debug)
Console.WriteLine($"{characteristicName} {Utils.Utils.ByteArrayToStringHex(bytes)}");

// todo make better like below kotlin
if (bytes[0] == ZapConstants.RIDE_ON[0] && bytes[1] == ZapConstants.RIDE_ON[1] && bytes[2] == ZapConstants.RIDE_ON[2] && bytes[3] == ZapConstants.RIDE_ON[3])
{
Expand Down
145 changes: 111 additions & 34 deletions Windows/ConsoleApp/BLE/ZwiftPlayBleManager.cs
Original file line number Diff line number Diff line change
@@ -1,63 +1,140 @@
using InTheHand.Bluetooth;
using ZwiftPlayConsoleApp.Logging;
using ZwiftPlayConsoleApp.Zap;
using ZwiftPlayConsoleApp.Configuration;

namespace ZwiftPlayConsoleApp.BLE;

public class ZwiftPlayBleManager
public partial class ZwiftPlayBleManager : IDisposable
{
private readonly ZwiftPlayDevice _zapDevice = new();

private readonly ZwiftPlayDevice _zapDevice;
private readonly BluetoothDevice _device;
private readonly bool _isLeft;
private readonly IZwiftLogger _logger;
private bool _isDisposed;
private readonly object _lock = new();

private static GattCharacteristic _asyncCharacteristic;
private static GattCharacteristic _syncRxCharacteristic;
private static GattCharacteristic _syncTxCharacteristic;
private readonly Config _config;
private static GattCharacteristic? _asyncCharacteristic;
private static GattCharacteristic? _syncRxCharacteristic;
private static GattCharacteristic? _syncTxCharacteristic;

public ZwiftPlayBleManager(BluetoothDevice device, bool isLeft)
public ZwiftPlayBleManager(BluetoothDevice device, bool isLeft, IZwiftLogger logger, Config config)
{
_device = device;
_isLeft = isLeft;
_logger = new ConfigurableLogger(((ConfigurableLogger)logger)._config, nameof(ZwiftPlayBleManager));
_config = config;
_zapDevice = new ZwiftPlayDevice(new ConfigurableLogger(((ConfigurableLogger)logger)._config, nameof(ZwiftPlayDevice)), config);
}

public async void ConnectAsync()
public async Task ConnectAsync()
{
var gatt = _device.Gatt;
await gatt.ConnectAsync();

if (gatt.IsConnected)
try
{
Console.WriteLine("Connected");
_isDisposed = false; // Reset disposal state
var gatt = _device.Gatt;
await gatt.ConnectAsync();

//var services = gatt.GetPrimaryServicesAsync().GetAwaiter().GetResult();
RegisterCharacteristics(gatt);
if (gatt.IsConnected)
{
_zapDevice.ResetEncryption();
_logger.LogInfo($"Connected {(_isLeft ? "Left" : "Right")} controller");
await RegisterCharacteristics(gatt);

Console.WriteLine("Send Start");
_syncRxCharacteristic.WriteValueWithResponseAsync(_zapDevice.BuildHandshakeStart()).GetAwaiter().GetResult();
if (_syncRxCharacteristic != null)
{
var handshakeData = _zapDevice.BuildHandshakeStart();
_logger.LogDebug($"Sending handshake data: {BitConverter.ToString(handshakeData)}");
await _syncRxCharacteristic.WriteValueWithResponseAsync(handshakeData);
_logger.LogInfo("Handshake initiated");
}

}
}
catch (Exception ex)
{
_logger.LogError("Connection failed", ex);
throw;
}
}

private async void RegisterCharacteristics(RemoteGattServer gatt)
private async Task RegisterCharacteristics(RemoteGattServer gatt)
{
var zapService = gatt.GetPrimaryServiceAsync(ZapBleUuids.ZWIFT_CUSTOM_SERVICE_UUID).GetAwaiter().GetResult();
_asyncCharacteristic = zapService.GetCharacteristicAsync(ZapBleUuids.ZWIFT_ASYNC_CHARACTERISTIC_UUID)
.GetAwaiter().GetResult();
_logger.LogDebug("Starting characteristic registration");

var zapService = await gatt.GetPrimaryServiceAsync(ZapBleUuids.ZWIFT_CUSTOM_SERVICE_UUID);
if (zapService == null)
{
_logger.LogError("ZAP service not found");
return;
}

_asyncCharacteristic = await zapService.GetCharacteristicAsync(ZapBleUuids.ZWIFT_ASYNC_CHARACTERISTIC_UUID);
_syncRxCharacteristic = await zapService.GetCharacteristicAsync(ZapBleUuids.ZWIFT_SYNC_RX_CHARACTERISTIC_UUID);
_syncTxCharacteristic = await zapService.GetCharacteristicAsync(ZapBleUuids.ZWIFT_SYNC_TX_CHARACTERISTIC_UUID);

if (_asyncCharacteristic != null)
{
await _asyncCharacteristic.StartNotificationsAsync();
_asyncCharacteristic.CharacteristicValueChanged += (sender, eventArgs) =>
{
_logger.LogDebug($"Async characteristic value changed: {BitConverter.ToString(eventArgs.Value)}");
ProcessCharacteristic("Async", eventArgs.Value);
};
}

if (_syncTxCharacteristic != null)
{
await _syncTxCharacteristic.StartNotificationsAsync();
_syncTxCharacteristic.CharacteristicValueChanged += (sender, eventArgs) =>
{
_logger.LogDebug($"Sync Tx characteristic value changed: {BitConverter.ToString(eventArgs.Value)}");
ProcessCharacteristic("Sync Tx", eventArgs.Value);
};
}

_syncRxCharacteristic = zapService.GetCharacteristicAsync(ZapBleUuids.ZWIFT_SYNC_RX_CHARACTERISTIC_UUID)
.GetAwaiter().GetResult();
_syncTxCharacteristic = zapService.GetCharacteristicAsync(ZapBleUuids.ZWIFT_SYNC_TX_CHARACTERISTIC_UUID)
.GetAwaiter().GetResult();
_logger.LogInfo("Characteristic registration completed");
}
public void Dispose()
{
lock (_lock)
{
if (_isDisposed) return;

_asyncCharacteristic.StartNotificationsAsync().GetAwaiter().GetResult();
_asyncCharacteristic.CharacteristicValueChanged += (sender, eventArgs) =>
if (_asyncCharacteristic != null)
{
_asyncCharacteristic.CharacteristicValueChanged -= (sender, eventArgs) =>
ProcessCharacteristic("Async", eventArgs.Value);
}
if (_syncTxCharacteristic != null)
{
_zapDevice.ProcessCharacteristic("Async", eventArgs.Value);
};
_syncTxCharacteristic.CharacteristicValueChanged -= (sender, eventArgs) =>
ProcessCharacteristic("Sync Tx", eventArgs.Value);
}

_syncTxCharacteristic.StartNotificationsAsync().GetAwaiter().GetResult();
_syncTxCharacteristic.CharacteristicValueChanged += (sender, eventArgs) =>
if (_device?.Gatt != null && _device.Gatt.IsConnected)
{
_zapDevice.ProcessCharacteristic("Sync Tx", eventArgs.Value);
};
_device.Gatt.Disconnect();
}

_isDisposed = true;
GC.SuppressFinalize(this);
}
}
private void ProcessCharacteristic(string source, byte[] value)
{
if (_isDisposed) return;
_logger.LogDebug($"Processing {source} characteristic: {BitConverter.ToString(value)}");
_zapDevice.ProcessCharacteristic(source, value);
}

private void OnAsyncCharacteristicChanged(object sender, GattCharacteristicValueChangedEventArgs e)
{
ProcessCharacteristic("Async", e.Value);
}

private void OnSyncTxCharacteristicChanged(object sender, GattCharacteristicValueChangedEventArgs e)
{
ProcessCharacteristic("Sync Tx", e.Value);
}
}
68 changes: 50 additions & 18 deletions Windows/ConsoleApp/BLE/ZwiftPlayDevice.cs
Original file line number Diff line number Diff line change
@@ -1,71 +1,102 @@
using ZwiftPlayConsoleApp.Utils;
using ZwiftPlayConsoleApp.Logging;
using ZwiftPlayConsoleApp.Utils;
using ZwiftPlayConsoleApp.Zap;
using ZwiftPlayConsoleApp.Zap.Crypto;
using ZwiftPlayConsoleApp.Zap.Proto;
using ZwiftPlayConsoleApp.Configuration;

namespace ZwiftPlayConsoleApp.BLE;

public class ZwiftPlayDevice : AbstractZapDevice
{

//private readonly IZwiftLogger _logger;
private int _batteryLevel;
private ControllerNotification? _lastButtonState;

private readonly Config _config;

public ZwiftPlayDevice(IZwiftLogger logger, Config config) : base(logger)
{
_config = config;
_logger.LogInfo($"ZwiftPlayDevice initialized with SendKeys: {config.SendKeys}, UseMapping: {config.UseMapping}");
Console.WriteLine($"ZwiftPlayDevice initialized with SendKeys: {config.SendKeys}, UseMapping: {config.UseMapping}");
}

protected override void ProcessEncryptedData(byte[] bytes)
{
_logger.LogDebug($"Processing encrypted data length: {bytes.Length}");
try
{
//if (LOG_RAW) Timber.d("Decrypted: ${bytes.toHexString()}")

if (Debug)
_logger.LogDebug($"Processing encrypted data: {Utils.Utils.ByteArrayToStringHex(bytes)}");
var counterBytes = new byte[4];
Array.Copy(bytes, 0, counterBytes, 0, counterBytes.Length);
var counter = new ByteBuffer(counterBytes).ReadInt32();

if (Debug)
_logger.LogDebug($"Counter bytes: {Utils.Utils.ByteArrayToStringHex(counterBytes)}");
var payloadBytes = new byte[bytes.Length - 4 - EncryptionUtils.MAC_LENGTH];
Array.Copy(bytes, 4, payloadBytes, 0, payloadBytes.Length);
if (Debug)
_logger.LogDebug($"Attempting payload extraction, length: {payloadBytes.Length}");

var tagBytes = new byte[EncryptionUtils.MAC_LENGTH];
Array.Copy(bytes, EncryptionUtils.MAC_LENGTH + payloadBytes.Length, tagBytes, 0, tagBytes.Length);
if (Debug)
_logger.LogDebug($"Attempting tag extraction, starting at index: {EncryptionUtils.MAC_LENGTH + payloadBytes.Length}");

var data = _zapEncryption.Decrypt(counter, payloadBytes, tagBytes);

var type = data[0];
var data = new byte[payloadBytes.Length];
try
{
data = _zapEncryption.Decrypt(counter, payloadBytes, tagBytes);
}
catch (Exception ex)
{
_logger.LogError($"Decrypt failed - Counter: {counter}, Payload: {BitConverter.ToString(payloadBytes)}, Tag: {BitConverter.ToString(tagBytes)}", ex);
}
if (Debug)
_logger.LogDebug($"Decrypted data: {BitConverter.ToString(data)}");

var type = data[0];
var messageBytes = new byte[data.Length - 1];
Array.Copy(data, 1, messageBytes, 0, messageBytes.Length);

if (Debug)
_logger.LogDebug($"Controller notification message type: {type}");
switch (type)
{
case ZapConstants.CONTROLLER_NOTIFICATION_MESSAGE_TYPE:
ProcessButtonNotification(new ControllerNotification(messageBytes));
_logger.LogInfo("Button state change detected");
ProcessButtonNotification(new ControllerNotification(messageBytes, new ConfigurableLogger(((ConfigurableLogger)_logger)._config, nameof(ControllerNotification))));
break;
case ZapConstants.EMPTY_MESSAGE_TYPE:
if (Debug)
Console.WriteLine("Empty Message");
_logger.LogDebug("Empty Message");
break;
case ZapConstants.BATTERY_LEVEL_TYPE:
var notification = new BatteryStatus(messageBytes);
if (_batteryLevel != notification.Level)
{
_batteryLevel = notification.Level;
Console.WriteLine($"Battery level update: {_batteryLevel}");
_logger.LogInfo($"Battery level update: {_batteryLevel}");
}
break;
default:
Console.WriteLine($"Unprocessed - Type: {type} Data: {Utils.Utils.ByteArrayToStringHex(data)}");
_logger.LogWarning($"Unprocessed - Type: {type} Data: {Utils.Utils.ByteArrayToStringHex(data)}");
break;
}
}
catch (Exception ex)
{
Console.WriteLine("Decrypt failed: " + ex.Message);
_logger.LogError("Decrypt failed", ex);
}
}

private const bool SendKeys = false;
private bool SendKeys { get; set; } = false;

private void ProcessButtonNotification(ControllerNotification notification)
{
if (SendKeys)

if (_config.SendKeys)
{
var changes = notification.DiffChange(_lastButtonState);
foreach (var change in changes)
Expand All @@ -77,14 +108,15 @@ private void ProcessButtonNotification(ControllerNotification notification)
{
if (_lastButtonState == null)
{
Console.WriteLine(notification.ToString());
_logger.LogInfo($"Controller: {notification}");
}
else
{
var diff = notification.Diff(_lastButtonState);
if (!string.IsNullOrEmpty(diff)) // get repeats of the same state
if (!string.IsNullOrEmpty(diff))
{
Console.WriteLine(diff);
_logger.LogInfo($"Button: {diff}");
Console.WriteLine($"Button: {diff}");
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions Windows/ConsoleApp/Configuration/AppSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"AppSettings": {
"DefaultTaskDelay": 10,
"QuitKey": "q",
"DefaultScanTimeoutMs": 30000,
"DefaultRequiredDeviceCount": 2,
"DefaultConnectionTimeoutMs": 10000
}
}
Loading