Skip to content

Commit

Permalink
Peloton Direct Polling support (#43)
Browse files Browse the repository at this point in the history
Co-authored-by: Matt Stevens <[email protected]>
  • Loading branch information
jeremydk and vodlogic authored Feb 12, 2021
1 parent 49d33d4 commit 40cd8ce
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 3 deletions.
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')
}
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++;
}
}

}

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

0 comments on commit 40cd8ce

Please sign in to comment.