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

Bluetooth support #130

Merged
merged 45 commits into from
Dec 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
f959bce
Added command line option for experimental ble support
marcoveeneman Nov 3, 2020
389bcf5
Added dependency on requests library in requirements.txt
marcoveeneman Nov 3, 2020
f1538b9
Added BleDongle and BleInterface classes to bleDongle file
marcoveeneman Nov 3, 2020
cab6470
Send message to BLE instead of ANT when -b option is given
marcoveeneman Nov 3, 2020
c1d510b
Force manual option when enabling bluetooth
marcoveeneman Nov 3, 2020
e331f22
Added initial files for the BLE server
marcoveeneman Nov 3, 2020
83dcdc3
Added initial nodejs ble server
marcoveeneman Nov 5, 2020
52c0216
Send message to ble server so it can advertise the power and cadence …
marcoveeneman Nov 5, 2020
29fad0e
Added fitness machine service
marcoveeneman Nov 7, 2020
8318b0c
Added heart rate service
marcoveeneman Nov 7, 2020
5c75c30
Use FitnessMachineService and HeartRateService in the virtual trainer
marcoveeneman Nov 7, 2020
f50b8bb
Send cadence instead of pedal count, the ftms characteristic uses cad…
marcoveeneman Nov 7, 2020
330873b
Call correct function on timeout in virtual trainer
marcoveeneman Nov 7, 2020
5b11253
Fixed flagField not being correctly set due to offset being zero. Fir…
marcoveeneman Nov 7, 2020
da4154f
Added fitness machine control point characteristic
marcoveeneman Nov 7, 2020
ed9eadd
Integrate BLE server, the FTMS bluetooth protocol can now adjust the …
marcoveeneman Nov 7, 2020
cbdfc43
ANT+ and BLE should work simultaneously
marcoveeneman Nov 7, 2020
030147f
LocateHW returns true if using bluetooth and no ANT+ dongle is found
marcoveeneman Nov 8, 2020
8b2127c
Log to logfile instead of printing to console
marcoveeneman Nov 8, 2020
1270235
Start the BLE server when creating the BleDongle instance
marcoveeneman Nov 8, 2020
2559453
Added some initial documentation on bluetooth support
marcoveeneman Nov 8, 2020
d355bc8
Removed cycling power service since the fitness machine service is re…
marcoveeneman Nov 8, 2020
5187bed
Improved BLE installation steps on Windows
marcoveeneman Dec 15, 2020
60c3b50
Added SetTargetPower op code to the BLE server
marcoveeneman Dec 15, 2020
d93df30
Set the target power if target_power is received via BLE, else set th…
marcoveeneman Dec 15, 2020
52fc561
Rename VirtualTrainer to FortiusANT Trainer
marcoveeneman Dec 20, 2020
0a3f11c
Updated bluetooth documentation
marcoveeneman Dec 20, 2020
02ac9d2
Enable all BLE logging
marcoveeneman Dec 20, 2020
7e8a60f
Improved logging on receiving an unsupported opcode via BLE
marcoveeneman Dec 20, 2020
8839530
Reverted BLE interface in python code, this will be done in a separat…
marcoveeneman Dec 22, 2020
4738fbc
Revert whitespace changes
marcoveeneman Dec 22, 2020
830b36f
Removed -b options, this will be added in a different PR
marcoveeneman Dec 22, 2020
a3ec3e3
Whitespace fixes
marcoveeneman Dec 22, 2020
67a7b97
Renamed trace to debug for consistency
marcoveeneman Dec 22, 2020
01033f4
Merge branch 'master' into ble-support
marcoveeneman Dec 22, 2020
73da617
Removed ClientCharacteristicConfiguration and ServerCharacteristicCon…
marcoveeneman Dec 22, 2020
408e72f
Set PowerTargetSettingSupported bit in the FitnessMachineFeatureChara…
marcoveeneman Dec 22, 2020
cf1fcb6
Added supported power range characteristic, which is required when pr…
marcoveeneman Dec 23, 2020
6e0faba
Added the fitness machine status characteristic, which is required wh…
marcoveeneman Dec 23, 2020
b689b9b
Added version endpoint to the BLE server
marcoveeneman Dec 23, 2020
d9aaf9e
Updated bluetooth documentation
marcoveeneman Dec 23, 2020
12e7b74
Updated minimumPower to 0 in the supported power range characteristic
marcoveeneman Dec 23, 2020
92baca7
Include version number field in /ant command
marcoveeneman Dec 23, 2020
7a59078
Fixed wrong check in virtual-trainer get function causing a crash whe…
marcoveeneman Dec 23, 2020
6279f47
Merge branch 'master' into ble-support
WouterJD Dec 24, 2020
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
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*.json
pythoncode/build/
pythoncode/dist/
pythoncode/help.txt
Miscellaneous/
node_modules
package-lock.json
package-lock.json
pythoncode/help.txt
68 changes: 68 additions & 0 deletions doc/bluetooth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# FortiusANT with Bluetooth LE

FortiusANT was originally designed to provide an ANT+ interface to a Tacx Fortius. This requires the user to use ANT+ dongles in order to connect to applications such as Zwift.

On request of several users of FortiusANT support for BLE (Bluetooth Low Energy) has been added. When using the BLE interface the use of an ANT+ dongle is not mandatory anymore if you have supported BLE hardware.

## Design

The BLE support for FortiusANT is implemented in NodeJS, unlike FortiusANT itself which is written in Python. The implementation makes use of the very well working Bluetooth LE library [abandonware/bleno](https://github.com/abandonware/bleno).

Using this library FortiusANT is advertising the following services which can be discovered:
* FTMS (FiTness Machine Service)
* HRS (Heart Rate Service)

Communication between FortiusANT and the BLE server happens internally via a local http server where FortiusANT acts as the client and the BLE server as the server.

## Supported Hardware

Since BLE support in FortiusANT depends on the bleno library, hardware support is also limited to what bleno supports.

On macOS, on-board bluetooth is used, no need for an external dongle.

On Windows a bluetooth dongle is required, there is a limited set of supported hardware. It is important that your bluetooth dongle has one of the supported chipsets. See [node-bluetooth-hci-socket](https://github.com/noble/node-bluetooth-hci-socket#windows).

## Installation

### macOS

1. Install Xcode: [App Store](https://apps.apple.com/nl/app/xcode/id497799835?l=en&mt=12)
1. Install NodeJS: `brew install node`
1. Install dependencies: `cd node && npm install`

### Windows
1. Install Git for Windows (https://git-scm.com/downloads)
* Only needed if not installed yet.
1. Install NodeJS LTS version (https://nodejs.org)
* During installation, **check the box which installs the necessary tools for native modules**.
* After NodeJS installation completes, a command prompt will appear which will install the necessary tools. This will take a while, grab a drink in the mean time.
1. Install Zadig (https://zadig.akeo.ie)
1. Insert the bluetooth dongle
1. Replace the driver for your bluetooth dongle using Zadig
* Note that you cannot use the bluetooth dongle for windows itself when you perform this step. Using the exact same steps as mentioned below you can restore the old driver if you want.
1. Start Zadig
1. Select options, list all devices
1. Select the bluetooth dongle
* Note: It may be difficult to know which device is the correct BLE dongle in case your machine also has BLE on-board. Disable the on-board BLE device before inserting the BLE dongle so Zadig will see only one.
1. Remember the current driver, in case you want to restore the driver later on.
1. Check if WinUSB driver is set as target driver, this should be the default. (Choose the old driver when reverting)
1. press Replace Driver

1. Install dependencies
1. Start the windows command prompt
1. Click start
1. Type: `cmd`
1. Press enter
1. Go to the FortiusANT folder
1. Type: `cd <FortiusANT location>\node`
* `<FortiusANT location>` is the location where you downloaded FortiusANT to.
1. Press enter
1. Type: `npm install`
1. Press enter

### Linux
TODO

## Run FortiusANT with BLE support

To use BLE support in FortiusANT it should be started from the command line with the `-b` option. When [Start] is pressed the BLE interface will be started until [Stop] is pressed. FortiusANT will start advertising as 'FortiusANT Trainer' on Windows and Linux systems. On macOS, it will start advertising as your computer name.
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
const bleno = require('bleno');
const debug = require('debug')('fortiusant:fmcpc');

const RequestControl = 0x00;
const Reset = 0x01;
const SetTargetPower = 0x05;
const StartOrResume = 0x07;
const StopOrPause = 0x08;
const SetIndoorBikeSimulation = 0x11;
const ResponseCode = 0x80;

const Success = 0x01;
const OpCodeNotSupported = 0x02;
const InvalidParameter = 0x03;
const OperationFailed = 0x04;
const ControlNotPermitted = 0x05;

const CharacteristicUserDescription = '2901';
const FitnessMachineControlPoint = '2AD9';

class FitnessMachineControlPointCharacteristic extends bleno.Characteristic {
constructor(messages, fmsc) {
debug('[FitnessMachineControlPointCharacteristic] constructor')
super({
uuid: FitnessMachineControlPoint,
properties: ['write', 'indicate'],
descriptors: [
new bleno.Descriptor({
uuid: CharacteristicUserDescription,
value: 'Fitness Machine Control Point'
})
]
});

this.messages = messages;
this.fmsc = fmsc;
this.indicate = null;

this.hasControl = false;
this.isStarted = false;
}

result(opcode, result) {
let buffer = new Buffer.alloc(3);
buffer.writeUInt8(ResponseCode);
buffer.writeUInt8(opcode, 1);
buffer.writeUInt8(result, 2);
debug(buffer);
return buffer;
}

onSubscribe(maxValueSize, updateValueCallback) {
debug('[FitnessMachineControlPointCharacteristic] onSubscribe');
this.indicate = updateValueCallback;
return this.RESULT_SUCCESS;
};

onUnsubscribe() {
debug('[FitnessMachineControlPointCharacteristic] onUnsubscribe');
this.indicate = null;
return this.RESULT_UNLIKELY_ERROR;
};

onIndicate() {
debug('[FitnessMachineControlPointCharacteristic] onIndicate');
}

onWriteRequest(data, offset, withoutResponse, callback) {
debug('[FitnessMachineControlPointCharacteristic] onWriteRequest');

// first byte indicates opcode
let code = data.readUInt8(0);

// when would it not be successful?
callback(this.RESULT_SUCCESS);

let response = null;

switch(code){
case RequestControl:
debug('[FitnessMachineControlPointCharacteristic] onWriteRequest: RequestControl');
if (this.hasControl) {
debug('Error: already has control');
response = this.result(code, ControlNotPermitted);
}
else {
debug('Given control');
this.hasControl = true;
response = this.result(code, Success);
}
break;
case Reset:
debug('[FitnessMachineControlPointCharacteristic] onWriteRequest: Reset');
if (this.hasControl) {
debug('Control reset');
this.hasControl = false;
response = this.result(code, Success);

// Notify all connected clients that control has been reset
this.fmsc.notifyReset();
}
else {
debug('Error: no control');
response = this.result(code, ControlNotPermitted);
}
break;
case SetTargetPower:
debug('[FitnessMachineControlPointCharacteristic] onWriteRequest: Set target power');
if (this.hasControl) {
let targetPower = data.readInt16LE(1);

debug('Target Power(W): ' + targetPower);

let message = {
"target_power": targetPower
}

// Put in message fifo so FortiusANT can read it
this.messages.push(message);

response = this.result(code, Success);

// Notify all connected clients about the new values
this.fmsc.notifySetTargetPower(targetPower);
}
else {
debug('Error: no control');
response = this.result(code, ControlNotPermitted);
}
break;
case StartOrResume:
debug('[FitnessMachineControlPointCharacteristic] onWriteRequest: Start or Resume');
if (this.hasControl) {
if (this.isStarted) {
debug('Error: already started/resumed');
response = this.result(code, OperationFailed);
}
else {
debug('started/resumed');
this.isStarted = true;
response = this.result(code, Success);

// Notify all connected clients about the new state
this.fmsc.notifyStartOrResume();
}
}
else {
debug('Error: no control');
response = this.result(code, ControlNotPermitted);
}
break;
case StopOrPause:
debug('[FitnessMachineControlPointCharacteristic] onWriteRequest: Stop or Pause');
if (this.hasControl) {
if (this.isStarted) {
debug('stopped');
this.isStarted = false;
response = this.result(code, Success);

// Notify all connected clients about the new state
this.fmsc.notifyStopOrPause();
}
else {
debug('Error: already stopped/paused');
response = this.result(code, OperationFailed);
}
}
else {
debug('Error: no control');
response = this.result(code, ControlNotPermitted);
}
break;
case SetIndoorBikeSimulation:
debug('[FitnessMachineControlPointCharacteristic] onWriteRequest: Set indoor bike simulation');
if (this.hasControl) {
let windSpeed = data.readInt16LE(1) * 0.001;
let grade = data.readInt16LE(3) * 0.01;
let crr = data.readUInt8(5) * 0.0001;
let cw = data.readUInt8(6) * 0.01;

debug('Wind speed(mps): ' + windSpeed);
debug('Grade(%): ' + grade);
debug('crr: ' + crr);
debug('cw(Kg/m): ' + cw);

let message = {
"wind_speed": windSpeed,
"grade": grade,
"rolling_resistance_coefficient": crr,
"wind_resistance_coefficient": cw
}

// Put in message fifo so FortiusANT can read it
this.messages.push(message);

response = this.result(code, Success);

// Notify all connected clients about the new values
this.fmsc.notifySetIndoorBikeSimulation(windSpeed, grade, crr, cw);
}
else {
debug('Error: no control');
response = this.result(code, ControlNotPermitted);
}
break;
default:
debug('Unsupported OPCODE:' + code);

let d = new Buffer.from(data);
debug('Data: ' + d);
response = this.result(code, OpCodeNotSupported);
break;
}

this.indicate(response);
}
};

module.exports = FitnessMachineControlPointCharacteristic;
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const bleno = require('bleno');
const debug = require('debug')('fortiusant:fmfc');

function bit(nr) {
return (1 << nr);
}

const CadenceSupported = bit(1);
const HeartRateMeasurementSupported = bit(10);
const PowerMeasurementSupported = bit(14);

const PowerTargetSettingSupported = bit(3);
const IndoorBikeSimulationParametersSupported = bit(13);

const CharacteristicUserDescription = '2901';
const FitnessMachineFeature = '2ACC';

class FitnessMachineFeatureCharacteristic extends bleno.Characteristic {
constructor() {
debug('[FitnessMachineFeatureCharacteristic] constructor');
super({
uuid: FitnessMachineFeature,
properties: ['read'],
descriptors: [
new bleno.Descriptor({
uuid: CharacteristicUserDescription,
value: 'Fitness Machine Feature'
})
],
});
}

onReadRequest(offset, callback) {
debug('[FitnessMachineFeatureCharacteristic] onReadRequest');
let flags = new Buffer.alloc(8);
flags.writeUInt32LE(CadenceSupported | PowerMeasurementSupported);
flags.writeUInt32LE(IndoorBikeSimulationParametersSupported | PowerTargetSettingSupported, 4);
callback(this.RESULT_SUCCESS, flags);
}
}

module.exports = FitnessMachineFeatureCharacteristic;
45 changes: 45 additions & 0 deletions node/fitness-machine-service/fitness-machine-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const bleno = require('bleno');
const debug = require('debug')('fortiusant:fms');

const FitnessMachineFeatureCharacteristic = require('./fitness-machine-feature-characteristic');
const IndoorBikeDataCharacteristic = require('./indoor-bike-data-characteristic');
const FitnessMachineControlPointCharacteristic = require('./fitness-machine-control-point-characteristic');
const SupportedPowerRangeCharacteristic = require('./supported-power-range-characteristic');
const FitnessMachineStatusCharacteristic = require('./fitness-machine-status-characteristic');

const FitnessMachine = '1826'

class FitnessMachineService extends bleno.PrimaryService {
constructor(messages) {
debug('[FitnessMachineService] constructor');
let fmfc = new FitnessMachineFeatureCharacteristic();
let ibdc = new IndoorBikeDataCharacteristic();
let fmsc = new FitnessMachineStatusCharacteristic();
let fmcpc = new FitnessMachineControlPointCharacteristic(messages, fmsc);
let sprc = new SupportedPowerRangeCharacteristic();
super({
uuid: FitnessMachine,
characteristics: [
fmfc,
ibdc,
fmsc,
fmcpc,
sprc
]
});

this.fmfc = fmfc;
this.ibdc = ibdc;
this.fmsc = fmsc;
this.fmcpc = fmcpc;
this.sprc = sprc;
}

notify(event) {
debug('[FitnessMachineService] notify')
this.ibdc.notify(event);
return this.RESULT_SUCCESS;
};
}

module.exports = FitnessMachineService;
Loading