From 230bc73e56025ae63e1ab7164fc90c1ec84512d0 Mon Sep 17 00:00:00 2001 From: Kevin Crawley Date: Sun, 6 Sep 2020 23:45:28 +0100 Subject: [PATCH 1/2] added peloton bike decoding service --- CHANGELOG.md | 3 + README.md | 3 + package-lock.json | 235 +++++++++++++++++++++++++++-------------- package.json | 2 + src/app/app.js | 3 + src/app/cli-options.js | 6 +- src/bikes/index.js | 8 ++ src/bikes/peloton.js | 99 +++++++++++++++++ 8 files changed, 277 insertions(+), 82 deletions(-) create mode 100644 src/bikes/peloton.js diff --git a/CHANGELOG.md b/CHANGELOG.md index f4324148..bfa4abc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 1.1.0 +- Added support for Peloton data source via USB serial device patched to the Peloton data cable + ## 1.0.4 - Add power-scale and power-offset CLI options. [d6c0e4e0](https://github.com/ptx2/gymnasticon/commit/d6c0e4e067317e4903fafbe1a9016e02087e402f) diff --git a/README.md b/README.md index 98db53f8..08ce0662 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Gymnasticon enables the obsolete Flywheel Home Bike to work with Zwift and other ## Bikes tested * Flywheel (tested) +* Peloton (v1) * LifeFitness IC5 (probably works) ## Apps tested @@ -117,6 +118,8 @@ Options: --bike-adapter for bike connection [default: "hci0"] --flywheel-address --flywheel-name + --peloton-path usb serial device path + [string] [default: "/dev/ttyUSB0"] --bot-power initial bot power [number] --bot-cadence initial bot cadence [number] --bot-host for power/cadence control over udp [string] diff --git a/package-lock.json b/package-lock.json index 00c97885..650d0eac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -959,6 +959,106 @@ "to-fast-properties": "^2.0.0" } }, + "@serialport/binding-abstract": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@serialport/binding-abstract/-/binding-abstract-9.0.1.tgz", + "integrity": "sha512-ncUFSRyVdpyCRuah2dzrs99UfEWWMAhV31ae2FT6j4f8TypQ8OgAF8KkcHiD4M3wORDh3UKCCTS7n8aJWge1RA==", + "requires": { + "debug": "^4.1.1" + } + }, + "@serialport/binding-mock": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-9.0.1.tgz", + "integrity": "sha512-C01T6iX+nNKB7S6BhQEy5nfk4lUk/CkdFEfen9DDPYhtFtIsm5GCGvRB3Fjnp+8oDrGWJOrZfxFf3kWOOx665A==", + "requires": { + "@serialport/binding-abstract": "^9.0.1", + "debug": "^4.1.1" + } + }, + "@serialport/bindings": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@serialport/bindings/-/bindings-9.0.1.tgz", + "integrity": "sha512-O5QuwCdnHuZygBKw7tVq2wHysfOnCbOyKtR/k9T9zHqptd89Tzy6xJQNtnrcbV/2D22noKX6yWj+1wqvNe6NRA==", + "requires": { + "@serialport/binding-abstract": "^9.0.1", + "@serialport/parser-readline": "^9.0.1", + "bindings": "^1.5.0", + "debug": "^4.1.1", + "nan": "^2.14.1", + "prebuild-install": "^5.3.5" + }, + "dependencies": { + "nan": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==" + }, + "prebuild-install": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.5.tgz", + "integrity": "sha512-YmMO7dph9CYKi5IR/BzjOJlRzpxGGVo1EsLSUZ0mt/Mq0HWZIHOKHHcHdT69yG54C9m6i45GpItwRHpk0Py7Uw==", + "requires": { + "detect-libc": "^1.0.3", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp": "^0.5.1", + "napi-build-utils": "^1.0.1", + "node-abi": "^2.7.0", + "noop-logger": "^0.1.1", + "npmlog": "^4.0.1", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^3.0.3", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0", + "which-pm-runs": "^1.0.0" + } + } + } + }, + "@serialport/parser-byte-length": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-9.0.1.tgz", + "integrity": "sha512-1Ikv4lgCNw8OMf35yCpgzjHwkpgBEkhBuXFXIdWZk+ixaHFLlAtp03QxGPZBmzHMK58WDmEQoBHC1V5BkkAKSQ==" + }, + "@serialport/parser-cctalk": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-9.0.1.tgz", + "integrity": "sha512-GtMda2DeJ+23bNqOc79JYV06dax2n3FLLFM3zA7nfReCOi98QbuDj4TUbFESMOnp4DB0oMO0GYHCR9gHOedTkg==" + }, + "@serialport/parser-delimiter": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-9.0.1.tgz", + "integrity": "sha512-+oaSl5zEu47OlrRiF5p5tn2qgGqYuhVcE+NI+Pv4E1xsNB/A0fFxxMv/8XUw466CRLEJ5IESIB9qbFvKE6ltaQ==" + }, + "@serialport/parser-readline": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-9.0.1.tgz", + "integrity": "sha512-38058gxvyfgdeLpg3aUyD98NuWkVB9yyTLpcSdeQ3GYiupivwH6Tdy/SKPmxlHIw3Ml2qil5MR2mtW2fLPB5CQ==", + "requires": { + "@serialport/parser-delimiter": "^9.0.1" + } + }, + "@serialport/parser-ready": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-9.0.1.tgz", + "integrity": "sha512-lgzGkVJaaV1rJVx26WwI2UKyPxc0vu1rsOeldzA3VVbF+ABrblUQA06+cRPpT6k96GY+X4+1fB1rWuPpt8HbgQ==" + }, + "@serialport/parser-regex": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-9.0.1.tgz", + "integrity": "sha512-BHTV+Lkl+J8hSecFtDRENaR4fgA6tw44J+dmA1vEKEyum0iDN4bihbu8yvztYyo4PhBGUKDfm/PnD5EkJm0dPA==" + }, + "@serialport/stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-9.0.1.tgz", + "integrity": "sha512-S1xaf99vygbrMDNS/9GHYZYskWJHXJy6dCksW+ME2dzNXEXpz64vF0iug1tC1EIAhME9oD/s3ky2C9CUAd/GUg==", + "requires": { + "debug": "^4.1.1" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -1013,8 +1113,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "optional": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "3.2.1", @@ -1051,14 +1150,12 @@ "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "optional": true + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "are-we-there-yet": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "optional": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -1253,8 +1350,7 @@ "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", - "optional": true + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, "bcrypt-pbkdf": { "version": "1.0.2", @@ -1282,7 +1378,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "optional": true, "requires": { "file-uri-to-path": "1.0.0" } @@ -1291,7 +1386,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", - "optional": true, "requires": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -1302,7 +1396,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "optional": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -1385,7 +1478,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.5.0.tgz", "integrity": "sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==", - "optional": true, "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" @@ -1473,8 +1565,7 @@ "chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "optional": true + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "class-utils": { "version": "0.3.6", @@ -1559,8 +1650,7 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "optional": true + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "collection-visit": { "version": "1.0.0", @@ -1618,8 +1708,7 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "optional": true + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "convert-source-map": { "version": "1.7.0", @@ -1658,8 +1747,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "optional": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cross-spawn": { "version": "6.0.5", @@ -1707,7 +1795,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "optional": true, "requires": { "mimic-response": "^2.0.0" } @@ -1745,8 +1832,7 @@ "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "optional": true + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, "deep-is": { "version": "0.1.3", @@ -1823,14 +1909,12 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "optional": true + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "optional": true + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, "doctrine": { "version": "3.0.0", @@ -1875,7 +1959,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "optional": true, "requires": { "once": "^1.4.0" } @@ -2169,8 +2252,7 @@ "expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "optional": true + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" }, "extend": { "version": "3.0.2", @@ -2326,8 +2408,7 @@ "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "optional": true + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, "fill-range": { "version": "4.0.0", @@ -2443,8 +2524,7 @@ "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "optional": true + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "fs-minipass": { "version": "1.2.7", @@ -3045,7 +3125,6 @@ "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "optional": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -3087,8 +3166,7 @@ "github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", - "optional": true + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" }, "glob": { "version": "7.1.6", @@ -3178,8 +3256,7 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "optional": true + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "has-value": { "version": "1.0.0", @@ -3238,8 +3315,7 @@ "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "optional": true + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, "ignore": { "version": "4.0.6", @@ -3289,8 +3365,7 @@ "ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "optional": true + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "inquirer": { "version": "7.1.0", @@ -3535,7 +3610,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3663,8 +3737,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "optional": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -3886,8 +3959,7 @@ "mimic-response": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "optional": true + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" }, "minimatch": { "version": "3.0.4", @@ -3955,8 +4027,7 @@ "mkdirp-classic": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz", - "integrity": "sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g==", - "optional": true + "integrity": "sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g==" }, "ms": { "version": "2.1.2", @@ -3998,8 +4069,7 @@ "napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "optional": true + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" }, "napi-thread-safe-callback": { "version": "0.0.6", @@ -4044,7 +4114,6 @@ "version": "2.15.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.15.0.tgz", "integrity": "sha512-FeLpTS0F39U7hHZU1srAK4Vx+5AHNVOTP+hxBNQknR/54laTHSFIJkDWDqiquY1LeLUgTfPN7sLPhMubx0PLAg==", - "optional": true, "requires": { "semver": "^5.4.1" } @@ -4129,8 +4198,7 @@ "noop-logger": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", - "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=", - "optional": true + "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" }, "nopt": { "version": "4.0.3", @@ -4179,7 +4247,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "optional": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -4190,8 +4257,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "optional": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "oauth-sign": { "version": "0.9.0", @@ -4202,8 +4268,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "optional": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-copy": { "version": "0.1.0", @@ -4378,6 +4443,14 @@ "callsites": "^3.0.0" } }, + "parser-delimiter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parser-delimiter/-/parser-delimiter-1.0.2.tgz", + "integrity": "sha512-w7XmCahMtT4GUIgxbfyXgrhRfrxfo4ftg7zFJGb4G3Fp1ZqMvC/3gXm7W0N1/NaYlcoNv2nL4nIXFNbh+gcjBw==", + "requires": { + "safe-buffer": "^5.1.1" + } + }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -4481,8 +4554,7 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "optional": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "progress": { "version": "2.0.3", @@ -4500,7 +4572,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -4521,7 +4592,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "optional": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -4533,7 +4603,6 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4813,6 +4882,23 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, + "serialport": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/serialport/-/serialport-9.0.1.tgz", + "integrity": "sha512-35Ms8dqjtAb73lptfEZG2l/nFZOxHt3hUjCHvl+g3Mu737gzFLDpSBrRywBJw4G4eS5ozZ3YcthwYnop1WO+ng==", + "requires": { + "@serialport/binding-mock": "^9.0.1", + "@serialport/bindings": "^9.0.1", + "@serialport/parser-byte-length": "^9.0.1", + "@serialport/parser-cctalk": "^9.0.1", + "@serialport/parser-delimiter": "^9.0.1", + "@serialport/parser-readline": "^9.0.1", + "@serialport/parser-ready": "^9.0.1", + "@serialport/parser-regex": "^9.0.1", + "@serialport/stream": "^9.0.1", + "debug": "^4.1.1" + } + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -4876,14 +4962,12 @@ "simple-concat": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", - "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=", - "optional": true + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" }, "simple-get": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", - "optional": true, "requires": { "decompress-response": "^4.2.0", "once": "^1.3.1", @@ -5136,7 +5220,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5200,7 +5283,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -5209,7 +5291,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5217,8 +5298,7 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "optional": true + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "supports-color": { "version": "5.5.0", @@ -5336,7 +5416,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", - "optional": true, "requires": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -5348,7 +5427,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz", "integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==", - "optional": true, "requires": { "bl": "^4.0.1", "end-of-stream": "^1.4.1", @@ -5361,7 +5439,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "optional": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -5463,7 +5540,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "optional": true, "requires": { "safe-buffer": "^5.0.1" } @@ -5625,8 +5701,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "optional": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { "version": "3.4.0", @@ -5692,8 +5767,7 @@ "which-pm-runs": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", - "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", - "optional": true + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" }, "which-typed-array": { "version": "1.1.2", @@ -5713,7 +5787,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "optional": true, "requires": { "string-width": "^1.0.2 || 2" } diff --git a/package.json b/package.json index 84fa476b..c47783aa 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "dependencies": { "@abandonware/bleno": "^0.5.1-2", "@abandonware/noble": "^1.9.2-8", + "parser-delimiter": "^1.0.2", + "serialport": "^9.0.1", "yargs": "^15.3.1" }, "devDependencies": { diff --git a/src/app/app.js b/src/app/app.js index 8d5d1c72..6ac8060f 100644 --- a/src/app/app.js +++ b/src/app/app.js @@ -22,6 +22,9 @@ export const defaults = { flywheelAddress: undefined, // mac address of bike flywheelName: undefined, // name of bike + // peloton bike options + pelotonPath: '/dev/ttyUSB0', // default path for usb to serial device + // test bike options botPower: 0, // power botCadence: 0, // cadence diff --git a/src/app/cli-options.js b/src/app/cli-options.js index b776f824..7ead97c8 100644 --- a/src/app/cli-options.js +++ b/src/app/cli-options.js @@ -34,7 +34,11 @@ export const options = { 'flywheel-name': { describe: '', }, - + 'peloton-path': { + describe: ' usb serial device path', + type: 'string', + default: defaults.pelotonPath, + }, 'bot-power': { describe: ' initial bot power', type: 'number', diff --git a/src/bikes/index.js b/src/bikes/index.js index 2e1886df..08ecc1af 100644 --- a/src/bikes/index.js +++ b/src/bikes/index.js @@ -1,9 +1,11 @@ import {FlywheelBikeClient} from './flywheel'; +import {PelotonBikeClient} from './peloton'; import {BotBikeClient} from './bot'; import {macAddress} from '../util/mac-address'; const factories = { 'flywheel': createFlywheelBikeClient, + 'peloton': createPelotonBikeClient, 'bot': createBotBikeClient, }; @@ -28,6 +30,12 @@ function createFlywheelBikeClient(options, noble) { return new FlywheelBikeClient(noble, filters); } +function createPelotonBikeClient(options, noble) { + const filters = {}; + if (options.pelotonPath) filters.path = options.pelotonPath; + return new PelotonBikeClient(noble, filters); +} + function createBotBikeClient(options, noble) { const args = [ options.botPower, diff --git a/src/bikes/peloton.js b/src/bikes/peloton.js new file mode 100644 index 00000000..cd6ddc2b --- /dev/null +++ b/src/bikes/peloton.js @@ -0,0 +1,99 @@ +import {once, EventEmitter} from 'events'; +import {Timer} from '../util/timer'; +import util from 'util'; + +const SerialPort = require('serialport') +const Delimiter = require('@serialport/parser-delimiter') + +const PACKET_DELIMITER = new Buffer('f6', 'hex'); + +const debuglog = util.debuglog('peloton'); + +export class PelotonBikeClient extends EventEmitter { + /** + * Create a PelotonBikeClient instance. + * @param {Noble} noble - a Noble instance. + * @param {object} filters - filters to specify bike when more than one is present + * @param {string} filters.path - device path to usb serial device + */ + constructor(noble, filters) { + super(); + this.noble = noble; + this.filters = filters; + + this.onStatsUpdate = this.onStatsUpdate.bind(this); + this.onSerialMessage = this.onSerialMessage.bind(this); + + // initial stats + this.power = 0; + this.cadence = 0; + + this._timer = new Timer(1); + this._timer.on('timeout', this.onStatsUpdate); + } + + async connect() { + if (this.state === 'connected') { + throw new Error('Already connected'); + } + + this._port = new SerialPort(this.filters.path, { baudRate: 19200 }); + this._parser = this._port.pipe(new Delimiter({ delimiter: PACKET_DELIMITER })); + this._parser.on('data', this.onSerialMessage); + + this._timer.reset(); + this.state = 'connected'; + } + + /** + * Get the bike's MAC address. + * @returns {string} mac address + */ + get address() { + return this._port.path; + } + + /** + * @private + */ + onStatsUpdate() { + const {power, cadence} = this; + this.emit('stats', {power, cadence}); + } + + onSerialMessage(data) { + // @todo handle bike versions (v1/v2) -- data[0] + switch(data[1]) { + case 65: + this.cadence = decodePeloton(data, data[2], false); + return; + case 68: + this.power = decodePeloton(data, data[2], true); + return; + } + } +} + +function decodePeloton(bufferArray, byteLength, isPower) { + let decimalPlace = 1; + let precision = 0.0; + let accumulator = 0; + let iteratorOffset = 3; + + for (let iteratorTemp = iteratorOffset; iteratorTemp < iteratorOffset + byteLength; iteratorTemp++) { + let offsetVal = bufferArray[iteratorTemp] - 48; + if (offsetVal < 0 || offsetVal > 9) { + console.log("invalid value detected: ", offsetVal); + return; + } + + if (!isPower || iteratorTemp != iteratorOffset) { + accumulator += (offsetVal * decimalPlace); + decimalPlace *= 10; + } else { + precision = decimalPlace * offsetVal / 10.0; + } + } + + return accumulator + precision; +} \ No newline at end of file From 47e254620ace824288b851036e9c4996807f9124 Mon Sep 17 00:00:00 2001 From: Kevin Crawley <3759919+notsureifkevin@users.noreply.github.com> Date: Tue, 6 Oct 2020 08:41:08 -0500 Subject: [PATCH 2/2] updates test description --- src/test/bikes/peloton.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/bikes/peloton.js b/src/test/bikes/peloton.js index 2e034d6b..add39ada 100644 --- a/src/test/bikes/peloton.js +++ b/src/test/bikes/peloton.js @@ -1,7 +1,7 @@ import test from 'tape'; import {decodePeloton} from '../../bikes/peloton'; -test('parse() parses Peloton stats messages', t => { +test('decodePeloton() parses Peloton stats messages', t => { const bufPower = Buffer.from('f14405363333323038', 'hex'); const power = decodePeloton(bufPower, bufPower[2], true); const bufRPM = Buffer.from('f14103323930d0', 'hex');