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

Peloton Direct Polling support #43

Merged
merged 9 commits into from
Feb 12, 2021
2 changes: 1 addition & 1 deletion src/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import bleno from '@abandonware/bleno';

import {once} from 'events';

import {createBikeClient, getBikeTypes} from '../bikes';
import {GymnasticonServer} from '../servers/ble';
import {AntServer} from '../servers/ant';
import {createBikeClient, getBikeTypes} from '../bikes';
import {Simulation} from './simulation';
import {Timer} from '../util/timer';
import {Logger} from '../util/logger';
Expand Down
44 changes: 42 additions & 2 deletions src/bikes/peloton.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,20 @@ import util from 'util';
const SerialPort = require('serialport')
const Delimiter = require('@serialport/parser-delimiter')

/**
* Cadence and Power are both direct values returned by the bike.
* Resistance, on the otherhand, is a raw value returned from the bike to the
* Tablet, and doesn't necessarily add value for our use case. However, we are
* choosing to poll for it to allow for a usecase where Gymnasticon provides
* the polling, with the bike Tx split to the Tablet as well.
*/
const MEASUREMENTS_HEX_ENUM = {
CADENCE: Buffer.from("f6f54136", 'hex'),
POWER: Buffer.from("f6f54439", 'hex'),
RESISTANCE: Buffer.from("f6f54a3f", 'hex')
}
jeremydk marked this conversation as resolved.
Show resolved Hide resolved
const PACKET_DELIMITER = Buffer.from('f6', 'hex');
const POLL_RATE = 100;
const STATS_TIMEOUT = 1.0;

const debuglog = util.debuglog('gymnasticon:bikes:peloton');
Expand All @@ -23,6 +36,7 @@ export class PelotonBikeClient extends EventEmitter {
this.onStatsUpdate = this.onStatsUpdate.bind(this);
this.onSerialMessage = this.onSerialMessage.bind(this);
this.onSerialClose = this.onSerialClose.bind(this);
this.pollMetric = this.pollMetric.bind(this);

// initial stats
this.power = 0;
Expand All @@ -31,6 +45,10 @@ export class PelotonBikeClient extends EventEmitter {
// reset stats to 0 when the user leaves the ride screen or turns the bike off
this.statsTimeout = new Timer(STATS_TIMEOUT, {repeats: false});
this.statsTimeout.on('timeout', this.onStatsTimeout.bind(this));

// Let's collect interval handles for cancellation
this.intervalHandles = new Map();
this.nextMetric = 0;
}

async connect() {
Expand All @@ -47,6 +65,9 @@ export class PelotonBikeClient extends EventEmitter {
this._parser.on('data', this.onSerialMessage);

this.state = 'connected';

// Begin sending polling requests to the Peloton bike
this.intervalHandles['poll'] = setInterval(this.pollMetric, POLL_RATE, this._port);
tracelog("Serial Connected");
}

Expand All @@ -69,16 +90,18 @@ export class PelotonBikeClient extends EventEmitter {
onSerialMessage(data) {
tracelog("RECV: ", data);
switch(data[1]) {
case 65:
case 65: // Cadence
this.cadence = decodePeloton(data, data[2], false);
this.onStatsUpdate();
this.statsTimeout.reset();
return;
case 68:
case 68: // Power
this.power = decodePeloton(data, data[2], true);
this.onStatsUpdate();
this.statsTimeout.reset();
return;
case 74: // Resistance
return; // While we can parse this, we don't do anything with it.
default:
debuglog("Unrecognized Message Type: ", data[1]);
return;
Expand All @@ -87,6 +110,7 @@ export class PelotonBikeClient extends EventEmitter {

onSerialClose() {
this.emit('disconnect', {address: this.address});
clearInterval(this.intervalHandles['poll']);
tracelog("Serial Closed");
}

Expand All @@ -96,6 +120,22 @@ export class PelotonBikeClient extends EventEmitter {
tracelog("StatsTimeout exceeded");
this.onStatsUpdate();
}

pollMetric(port) {
let metric = Object.keys(MEASUREMENTS_HEX_ENUM)[this.nextMetric];

port.write(MEASUREMENTS_HEX_ENUM[metric], function(err) {
if (err) { throw new Error(`Error requesting ${metric}: ${err.message}`); }
})
port.drain();

if (this.nextMetric === Object.keys(MEASUREMENTS_HEX_ENUM).length -1) {
this.nextMetric = 0;
} else {
this.nextMetric++;
}
}
jeremydk marked this conversation as resolved.
Show resolved Hide resolved

}

export function decodePeloton(bufferArray, byteLength, isPower) {
Expand Down