Skip to content

Commit

Permalink
Detect Keiser bike version and set stats timeout accordingly. (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
chriselsen authored Feb 24, 2021
1 parent 8c926e5 commit 75caf76
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 10 deletions.
58 changes: 48 additions & 10 deletions src/bikes/keiser.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ const KEISER_VALUE_MAGIC = Buffer.from([0x02, 0x01]); // identifies Keiser data
const KEISER_VALUE_IDX_POWER = 10; // 16-bit power (watts) data offset within packet
const KEISER_VALUE_IDX_CADENCE = 6; // 16-bit cadence (1/10 rpm) data offset within packet
const KEISER_VALUE_IDX_REALTIME = 4; // Indicates whether the data present is realtime (0, or 128 to 227)
const KEISER_STATS_TIMEOUT = 2.0; // If no stats have been received within this time, reset power and cadence to 0
const KEISER_BIKE_TIMEOUT = 300.0; // Consider bike disconnected if no stats have been received for 300 sec / 5 minutes
const KEISER_VALUE_IDX_VER_MAJOR = 2; // 8-bit Version Major data offset within packet
const KEISER_VALUE_IDX_VER_MINOR = 3; // 8-bit Version Major data offset within packet
const KEISER_STATS_NEWVER_MINOR = 30; // Version Minor when broadcast interval was changed from ~ 2 sec to ~ 0.3 sec
const KEISER_STATS_TIMEOUT_OLD = 7.0; // Old Bike: If no stats received within 7 sec, reset power and cadence to 0
const KEISER_STATS_TIMEOUT_NEW = 1.0; // New Bike: If no stats received within 1 sec, reset power and cadence to 0
const KEISER_BIKE_TIMEOUT = 60.0; // Consider bike disconnected if no stats have been received for 60 sec / 1 minutes

const debuglog = util.debuglog('gymnasticon:bikes:keiser');

Expand Down Expand Up @@ -45,8 +49,24 @@ export class KeiserBikeClient extends EventEmitter {
throw new Error('Already connected');
}

// Scan for bike
this.filters = {};
this.filters.name = (v) => v == KEISER_LOCALNAME;
this.peripheral = await scan(this.noble, null, this.filters);

this.state = 'connected';

// Determine bike firmware version and set stats timeout
let bikestatstimeout = KEISER_STATS_TIMEOUT_OLD; // Fallback for unknown firmware version
try {
bikestatstimeout = bikeVersion(this.peripheral.advertisement.manufacturerData).timeout;
} catch (e) {
console.log("Keiser M3 bike: Unknown version detected");
this.onBikeTimeout(); // Disconnect as this data cannot be handled
}

// Reset stats to 0 when bike suddenly dissapears
this.statsTimeout = new Timer(KEISER_STATS_TIMEOUT, {repeats: false});
this.statsTimeout = new Timer(bikestatstimeout, {repeats: false});
this.statsTimeout.on('timeout', this.onStatsTimeout.bind(this));

// Consider bike disconnected if no stats have been received for certain time
Expand All @@ -56,13 +76,6 @@ export class KeiserBikeClient extends EventEmitter {
// Create filter to fix power and cadence dropouts
this.fixDropout = createDropoutFilter();

// Scan for bike
this.filters = {};
this.filters.name = (v) => v == KEISER_LOCALNAME;
this.peripheral = await scan(this.noble, null, this.filters);

this.state = 'connected';

// Waiting for data
await this.noble.startScanningAsync(null, true);
this.noble.on('discover', this.onReceive);
Expand Down Expand Up @@ -160,6 +173,31 @@ export class KeiserBikeClient extends EventEmitter {
}
}

/**
* Determine Keiser Bike Firmware version.
* This helps determine the correct value for the Stats
* timeout. Older versions of the bike send data only every
* 2 seconds, while newer bikes send data every 300 ms.
* @param {buffer} data - raw characteristic value.
* @returns {string} version - bike version number as string
* @returns {object} timeout - stats timeout for this bike version
*/
export function bikeVersion(data) {
let version = "Unknown";
let timeout = KEISER_STATS_TIMEOUT_OLD;
if (data.indexOf(KEISER_VALUE_MAGIC) === 0) {
const major = data.readUInt8(KEISER_VALUE_IDX_VER_MAJOR);
const minor = data.readUInt8(KEISER_VALUE_IDX_VER_MINOR);
version = major.toString(16) + "." + minor.toString(16);
if ((major === 6) && (minor >= parseInt(KEISER_STATS_NEWVER_MINOR, 16))) {
timeout = KEISER_STATS_TIMEOUT_NEW;
}
console.log("Keiser M3 bike version: ", version, " (Stats timeout: ", timeout, " sec.)");
return { version, timeout };
}
throw new Error('unable to parse bike version data');
}

/**
* Parse Keiser Bike Data characteristic value.
* Consider if provided value are realtime or review mode
Expand Down
33 changes: 33 additions & 0 deletions src/test/bikes/keiser.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import test from 'tape';
import {parse} from '../../bikes/keiser';
import {bikeVersion} from '../../bikes/keiser';

/**
* See https://dev.keiser.com/mseries/direct/#data-parse-example for a
Expand All @@ -13,3 +14,35 @@ test('parse() parses Keiser indoor bike data values', t => {
t.equal(cadence, 82, 'cadence (rpm)');
t.end();
});

test('bikeVersion() Tests Keiser bike version (6.40)', t => {
const bufver = Buffer.from('0201064000383803460573000D00042701000A', 'hex');
const {version, timeout} = bikeVersion(bufver);
t.equal(version, '6.40', 'Version: 6.40');
t.equal(timeout, 1, 'Timeout: 1 second');
t.end();
});

test('bikeVersion() Tests Keiser bike version (6.30)', t => {
const bufver = Buffer.from('0201063000383803460573000D00042701000A', 'hex');
const {version, timeout} = bikeVersion(bufver);
t.equal(version, '6.30', 'Version: 6.30');
t.equal(timeout, 1, 'Timeout: 1 second');
t.end();
});

test('bikeVersion() Tests Keiser bike version (6.22)', t => {
const bufver = Buffer.from('0201062200383803460573000D00042701000A', 'hex');
const {version, timeout} = bikeVersion(bufver);
t.equal(version, '6.22', 'Version: 6.22');
t.equal(timeout, 7, 'Timeout: 7 second');
t.end();
});

test('bikeVersion() Tests Keiser bike version (5.12)', t => {
const bufver = Buffer.from('0201051200383803460573000D00042701000A', 'hex');
const {version, timeout} = bikeVersion(bufver);
t.equal(version, '5.12', 'Version: 5.12');
t.equal(timeout, 7, 'Timeout: 7 second');
t.end();
});

0 comments on commit 75caf76

Please sign in to comment.