implements confirmation dialogs, improvements to strava upload
This commit is contained in:
parent
dfc17c09b2
commit
943518b986
|
|
@ -28,14 +28,14 @@ function createWebServer () {
|
||||||
|
|
||||||
const wss = new WebSocketServer({ server })
|
const wss = new WebSocketServer({ server })
|
||||||
|
|
||||||
wss.on('connection', function connection (ws) {
|
wss.on('connection', function connection (client) {
|
||||||
log.debug('websocket client connected')
|
log.debug('websocket client connected')
|
||||||
emitter.emit('clientConnected', ws)
|
emitter.emit('clientConnected', client)
|
||||||
ws.on('message', function incoming (data) {
|
client.on('message', function incoming (data) {
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(data)
|
const message = JSON.parse(data)
|
||||||
if (message) {
|
if (message) {
|
||||||
emitter.emit('messageReceived', message)
|
emitter.emit('messageReceived', message, client)
|
||||||
} else {
|
} else {
|
||||||
log.warn(`invalid message received: ${data}`)
|
log.warn(`invalid message received: ${data}`)
|
||||||
}
|
}
|
||||||
|
|
@ -43,11 +43,22 @@ function createWebServer () {
|
||||||
log.error(err)
|
log.error(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
ws.on('close', function () {
|
client.on('close', function () {
|
||||||
log.debug('websocket client disconnected')
|
log.debug('websocket client disconnected')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function notifyClient (client, type, data) {
|
||||||
|
const messageString = JSON.stringify({ type, data })
|
||||||
|
if (wss.clients.has(client)) {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(messageString)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.error('trying to send message to a client that does not exist')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function notifyClients (type, data) {
|
function notifyClients (type, data) {
|
||||||
const messageString = JSON.stringify({ type, data })
|
const messageString = JSON.stringify({ type, data })
|
||||||
wss.clients.forEach(function each (client) {
|
wss.clients.forEach(function each (client) {
|
||||||
|
|
@ -58,6 +69,7 @@ function createWebServer () {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.assign(emitter, {
|
return Object.assign(emitter, {
|
||||||
|
notifyClient,
|
||||||
notifyClients
|
notifyClients
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
'use strict'
|
||||||
|
/*
|
||||||
|
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
|
||||||
|
|
||||||
|
Component that renders a html dialog
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AppElement, html, css } from './AppElement.js'
|
||||||
|
import { customElement, property } from 'lit/decorators.js'
|
||||||
|
import { ref, createRef } from 'lit/directives/ref.js'
|
||||||
|
|
||||||
|
@customElement('app-dialog')
|
||||||
|
export class AppDialog extends AppElement {
|
||||||
|
constructor () {
|
||||||
|
super()
|
||||||
|
this.dialog = createRef()
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
dialog::backdrop {
|
||||||
|
background: none;
|
||||||
|
backdrop-filter: contrast(15%) blur(2px);
|
||||||
|
}
|
||||||
|
dialog {
|
||||||
|
border: none;
|
||||||
|
color: var(--theme-font-color);
|
||||||
|
background-color: var(--theme-widget-color);
|
||||||
|
border-radius: var(--theme-border-radius);
|
||||||
|
box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 2px 6px 2px;
|
||||||
|
padding: 1.6rem;
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
outline:none;
|
||||||
|
background-color: var(--theme-button-color);
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--theme-border-radius);
|
||||||
|
color: var(--theme-font-color);
|
||||||
|
margin: 0.2em 0;
|
||||||
|
font-size: 60%;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 4em;
|
||||||
|
height: 2.5em;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
fieldset {
|
||||||
|
border: 0;
|
||||||
|
margin: unset;
|
||||||
|
padding: unset;
|
||||||
|
margin-block-end: 1em;
|
||||||
|
}
|
||||||
|
::slotted(*) { font-size: 80%; }
|
||||||
|
::slotted(p) { font-size: 55%; }
|
||||||
|
|
||||||
|
menu {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
dialogOpen
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return html`
|
||||||
|
<dialog ${ref(this.dialog)} @close=${this.close}>
|
||||||
|
<form method="dialog">
|
||||||
|
<fieldset role="document">
|
||||||
|
<slot></slot>
|
||||||
|
</fieldset>
|
||||||
|
<menu>
|
||||||
|
<button value="cancel">Cancel</button>
|
||||||
|
<button value="confirm">OK</button>
|
||||||
|
</menu>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
close (event) {
|
||||||
|
if (event.target.returnValue !== 'confirm') {
|
||||||
|
this.dispatchEvent(new CustomEvent('close', { detail: 'cancel' }))
|
||||||
|
} else {
|
||||||
|
this.dispatchEvent(new CustomEvent('close', { detail: 'confirm' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated () {
|
||||||
|
this.dialog.value.showModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
updated (changedProperties) {
|
||||||
|
if (changedProperties.has('dialogOpen')) {
|
||||||
|
if (this.dialogOpen) {
|
||||||
|
this.dialog.value.showModal()
|
||||||
|
} else {
|
||||||
|
this.dialog.value.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,8 +6,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AppElement, html, css } from './AppElement.js'
|
import { AppElement, html, css } from './AppElement.js'
|
||||||
import { customElement } from 'lit/decorators.js'
|
import { customElement, state } from 'lit/decorators.js'
|
||||||
import { icon_undo, icon_expand, icon_compress, icon_poweroff, icon_bluetooth, icon_upload } from '../lib/icons.js'
|
import { icon_undo, icon_expand, icon_compress, icon_poweroff, icon_bluetooth, icon_upload } from '../lib/icons.js'
|
||||||
|
import './AppDialog.js'
|
||||||
|
|
||||||
@customElement('dashboard-actions')
|
@customElement('dashboard-actions')
|
||||||
export class DashboardActions extends AppElement {
|
export class DashboardActions extends AppElement {
|
||||||
|
|
@ -16,6 +17,7 @@ export class DashboardActions extends AppElement {
|
||||||
outline:none;
|
outline:none;
|
||||||
background-color: var(--theme-button-color);
|
background-color: var(--theme-button-color);
|
||||||
border: 0;
|
border: 0;
|
||||||
|
border-radius: var(--theme-border-radius);
|
||||||
color: var(--theme-font-color);
|
color: var(--theme-font-color);
|
||||||
margin: 0.2em 0;
|
margin: 0.2em 0;
|
||||||
font-size: 60%;
|
font-size: 60%;
|
||||||
|
|
@ -53,13 +55,16 @@ export class DashboardActions extends AppElement {
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@state({ type: Object })
|
||||||
|
dialog
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return html`
|
return html`
|
||||||
<button @click=${this.reset}>${icon_undo}</button>
|
<button @click=${this.reset}>${icon_undo}</button>
|
||||||
${this.renderOptionalButtons()}
|
${this.renderOptionalButtons()}
|
||||||
<button @click=${this.uploadTraining}>${icon_upload}</button>
|
|
||||||
<button @click=${this.switchPeripheralMode}>${icon_bluetooth}</button>
|
<button @click=${this.switchPeripheralMode}>${icon_bluetooth}</button>
|
||||||
<div class="peripheral-mode">${this.peripheralMode()}</div>
|
<div class="peripheral-mode">${this.peripheralMode()}</div>
|
||||||
|
${this.dialog ? this.dialog : ''}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,17 +89,25 @@ export class DashboardActions extends AppElement {
|
||||||
<button @click=${this.close}>${icon_poweroff}</button>
|
<button @click=${this.close}>${icon_poweroff}</button>
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.appState.config.stravaUploadEnabled) {
|
||||||
|
buttons.push(html`
|
||||||
|
<button @click=${this.uploadTraining}>${icon_upload}</button>
|
||||||
|
`)
|
||||||
|
}
|
||||||
return buttons
|
return buttons
|
||||||
}
|
}
|
||||||
|
|
||||||
peripheralMode () {
|
peripheralMode () {
|
||||||
const value = this.appState?.peripheralMode
|
const value = this.appState?.config?.peripheralMode
|
||||||
if (value === 'PM5') {
|
if (value === 'PM5') {
|
||||||
return 'C2 PM5'
|
return 'C2 PM5'
|
||||||
} else if (value === 'FTMSBIKE') {
|
} else if (value === 'FTMSBIKE') {
|
||||||
return 'FTMS Bike'
|
return 'FTMS Bike'
|
||||||
} else {
|
} else if (value === 'FTMS') {
|
||||||
return 'FTMS Rower'
|
return 'FTMS Rower'
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,6 +135,17 @@ export class DashboardActions extends AppElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadTraining () {
|
uploadTraining () {
|
||||||
|
this.dialog = html`
|
||||||
|
<app-dialog @close=${dialogClosed}>
|
||||||
|
<legend>Upload to Strava?</legend>
|
||||||
|
<p>Do you want to finish your workout and upload it to Strava?</p>
|
||||||
|
</app-dialog>
|
||||||
|
`
|
||||||
|
function dialogClosed (event) {
|
||||||
|
this.dialog = undefined
|
||||||
|
if (event.detail === 'confirm') {
|
||||||
this.sendEvent('triggerAction', { command: 'uploadTraining' })
|
this.sendEvent('triggerAction', { command: 'uploadTraining' })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ export class PerformanceDashboard extends AppElement {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0.5em 0.2em 0 0.2em;
|
padding: 0.5em 0.2em 0 0.2em;
|
||||||
|
border-radius: var(--theme-border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
dashboard-actions {
|
dashboard-actions {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
--theme-font-family: Verdana, "Lucida Sans Unicode", sans-serif;
|
--theme-font-family: Verdana, "Lucida Sans Unicode", sans-serif;
|
||||||
--theme-font-color: #f5f5f5;
|
--theme-font-color: #f5f5f5;
|
||||||
--theme-warning-color: #ff0000;
|
--theme-warning-color: #ff0000;
|
||||||
|
--theme-border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,10 @@ export function createApp (app) {
|
||||||
}
|
}
|
||||||
const data = message.data
|
const data = message.data
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
|
case 'config': {
|
||||||
|
app.updateState({ ...app.getState(), config: data })
|
||||||
|
break
|
||||||
|
}
|
||||||
case 'metrics': {
|
case 'metrics': {
|
||||||
let activeFields = rowingMetricsFields
|
let activeFields = rowingMetricsFields
|
||||||
// if we are in reset state only update heart rate
|
// if we are in reset state only update heart rate
|
||||||
|
|
@ -78,11 +82,7 @@ export function createApp (app) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredData = filterObjectByKeys(data, activeFields)
|
const filteredData = filterObjectByKeys(data, activeFields)
|
||||||
let updatedState = { ...app.getState(), metrics: filteredData }
|
app.updateState({ ...app.getState(), metrics: filteredData })
|
||||||
if (data.peripheralMode) {
|
|
||||||
updatedState = { ...app.getState(), peripheralMode: data.peripheralMode }
|
|
||||||
}
|
|
||||||
app.updateState(updatedState)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'authorizeStrava': {
|
case 'authorizeStrava': {
|
||||||
|
|
@ -91,7 +91,7 @@ export function createApp (app) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
console.error(`unknown message type: ${message.type}`)
|
console.error(`unknown message type: ${message.type}`, message.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,12 @@
|
||||||
export const APP_STATE = {
|
export const APP_STATE = {
|
||||||
// currently can be STANDALONE (Mobile Home Screen App), KIOSK (Raspberry Pi deployment) or '' (default)
|
// currently can be STANDALONE (Mobile Home Screen App), KIOSK (Raspberry Pi deployment) or '' (default)
|
||||||
appMode: '',
|
appMode: '',
|
||||||
// currently can be FTMS, FTMSBIKE or PM5
|
|
||||||
peripheralMode: 'FTMS',
|
|
||||||
// contains all the rowing metrics that are delivered from the backend
|
// contains all the rowing metrics that are delivered from the backend
|
||||||
metrics: {}
|
metrics: {},
|
||||||
|
config: {
|
||||||
|
// currently can be FTMS, FTMSBIKE or PM5
|
||||||
|
peripheralMode: '',
|
||||||
|
// true if upload to strava is configured
|
||||||
|
stravaUploadEnabled: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,10 @@ function createWorkoutRecorder () {
|
||||||
|
|
||||||
async function createTcxFile () {
|
async function createTcxFile () {
|
||||||
const tcxRecord = await activeWorkoutToTcx()
|
const tcxRecord = await activeWorkoutToTcx()
|
||||||
|
if (tcxRecord === undefined) {
|
||||||
|
log.error('error creating tcx file')
|
||||||
|
return
|
||||||
|
}
|
||||||
const directory = `${config.dataDirectory}/recordings/${startTime.getFullYear()}/${(startTime.getMonth() + 1).toString().padStart(2, '0')}`
|
const directory = `${config.dataDirectory}/recordings/${startTime.getFullYear()}/${(startTime.getMonth() + 1).toString().padStart(2, '0')}`
|
||||||
const filename = `${directory}/${tcxRecord.filename}${config.gzipTcxFiles ? '.gz' : ''}`
|
const filename = `${directory}/${tcxRecord.filename}${config.gzipTcxFiles ? '.gz' : ''}`
|
||||||
log.info(`saving session as tcx file ${filename}...`)
|
log.info(`saving session as tcx file ${filename}...`)
|
||||||
|
|
@ -71,6 +75,8 @@ function createWorkoutRecorder () {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function activeWorkoutToTcx () {
|
async function activeWorkoutToTcx () {
|
||||||
|
// we need at least two strokes to generate a valid tcx file
|
||||||
|
if (strokes.length < 2) return
|
||||||
const stringifiedStartTime = startTime.toISOString().replace(/T/, '_').replace(/:/g, '-').replace(/\..+/, '')
|
const stringifiedStartTime = startTime.toISOString().replace(/T/, '_').replace(/:/g, '-').replace(/\..+/, '')
|
||||||
const filename = `${stringifiedStartTime}_rowing.tcx`
|
const filename = `${stringifiedStartTime}_rowing.tcx`
|
||||||
|
|
||||||
|
|
@ -199,8 +205,8 @@ function createWorkoutRecorder () {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canCreateRecordings()) {
|
if (!minimumRecordingTimeHasPassed()) {
|
||||||
log.debug('workout is shorter than minimum workout time, skipping creation of recordings...')
|
log.debug('workout is shorter than minimum workout time, skipping automatic creation of recordings...')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,7 +221,7 @@ function createWorkoutRecorder () {
|
||||||
await Promise.all(parallelCalls)
|
await Promise.all(parallelCalls)
|
||||||
}
|
}
|
||||||
|
|
||||||
function canCreateRecordings () {
|
function minimumRecordingTimeHasPassed () {
|
||||||
const minimumRecordingTimeInSeconds = 10
|
const minimumRecordingTimeInSeconds = 10
|
||||||
const rotationImpulseTimeTotal = rotationImpulses.reduce((acc, impulse) => acc + impulse, 0)
|
const rotationImpulseTimeTotal = rotationImpulses.reduce((acc, impulse) => acc + impulse, 0)
|
||||||
const strokeTimeTotal = strokes.reduce((acc, stroke) => acc + stroke.strokeTime, 0)
|
const strokeTimeTotal = strokes.reduce((acc, stroke) => acc + stroke.strokeTime, 0)
|
||||||
|
|
@ -225,7 +231,6 @@ function createWorkoutRecorder () {
|
||||||
return {
|
return {
|
||||||
recordStroke,
|
recordStroke,
|
||||||
recordRotationImpulse,
|
recordRotationImpulse,
|
||||||
canCreateRecordings,
|
|
||||||
handlePause,
|
handlePause,
|
||||||
activeWorkoutToTcx,
|
activeWorkoutToTcx,
|
||||||
reset
|
reset
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,11 @@ function createWorkoutUploader (workoutRecorder) {
|
||||||
const emitter = new EventEmitter()
|
const emitter = new EventEmitter()
|
||||||
|
|
||||||
let stravaAuthorizationCodeResolver
|
let stravaAuthorizationCodeResolver
|
||||||
|
let requestingClient
|
||||||
|
|
||||||
function getStravaAuthorizationCode () {
|
function getStravaAuthorizationCode () {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
log.info('please open https://www.strava.com/oauth/authorize?client_id=&response_type=code&redirect_uri=http://localhost/index.html&approval_prompt=force&scope=activity:write')
|
emitter.emit('authorizeStrava', { stravaClientId: config.stravaClientId }, requestingClient)
|
||||||
emitter.emit('authorizeStrava', { stravaClientId: config.stravaClientId })
|
|
||||||
stravaAuthorizationCodeResolver = resolve
|
stravaAuthorizationCodeResolver = resolve
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -31,12 +31,20 @@ function createWorkoutUploader (workoutRecorder) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upload () {
|
async function upload (client) {
|
||||||
if (workoutRecorder.canCreateRecordings()) {
|
|
||||||
log.debug('uploading workout to strava...')
|
log.debug('uploading workout to strava...')
|
||||||
await stravaAPI.uploadActivityTcx(await workoutRecorder.activeWorkoutToTcx())
|
try {
|
||||||
|
requestingClient = client
|
||||||
|
// todo: we might signal back to the client whether we had success or not
|
||||||
|
const tcxActivity = await workoutRecorder.activeWorkoutToTcx()
|
||||||
|
if (tcxActivity !== undefined) {
|
||||||
|
await stravaAPI.uploadActivityTcx(tcxActivity)
|
||||||
|
emitter.emit('resetWorkout')
|
||||||
} else {
|
} else {
|
||||||
log.debug('workout is shorter than minimum workout time, skipping upload')
|
log.error('can not upload an empty workout to strava')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error('can not upload workout to strava:', error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ peripheralManager.on('control', (event) => {
|
||||||
peripheralManager.notifyStatus({ name: 'startedOrResumedByUser' })
|
peripheralManager.notifyStatus({ name: 'startedOrResumedByUser' })
|
||||||
event.res = true
|
event.res = true
|
||||||
} else if (event?.req?.name === 'peripheralMode') {
|
} else if (event?.req?.name === 'peripheralMode') {
|
||||||
webServer.notifyClients('metrics', { peripheralMode: event.req.peripheralMode })
|
webServer.notifyClients('config', getConfig())
|
||||||
event.res = true
|
event.res = true
|
||||||
} else {
|
} else {
|
||||||
log.info('unhandled Command', event.req)
|
log.info('unhandled Command', event.req)
|
||||||
|
|
@ -123,12 +123,16 @@ if (config.heartrateMonitorANT) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
workoutUploader.on('authorizeStrava', (data) => {
|
workoutUploader.on('authorizeStrava', (data, client) => {
|
||||||
webServer.notifyClients('authorizeStrava', data)
|
webServer.notifyClient(client, 'authorizeStrava', data)
|
||||||
|
})
|
||||||
|
|
||||||
|
workoutUploader.on('resetWorkout', () => {
|
||||||
|
resetWorkout()
|
||||||
})
|
})
|
||||||
|
|
||||||
const webServer = createWebServer()
|
const webServer = createWebServer()
|
||||||
webServer.on('messageReceived', (message) => {
|
webServer.on('messageReceived', (message, client) => {
|
||||||
switch (message.command) {
|
switch (message.command) {
|
||||||
case 'switchPeripheralMode': {
|
case 'switchPeripheralMode': {
|
||||||
peripheralManager.switchPeripheralMode()
|
peripheralManager.switchPeripheralMode()
|
||||||
|
|
@ -139,7 +143,7 @@ webServer.on('messageReceived', (message) => {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'uploadTraining': {
|
case 'uploadTraining': {
|
||||||
workoutUploader.upload()
|
workoutUploader.upload(client)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'stravaAuthorizationCode': {
|
case 'stravaAuthorizationCode': {
|
||||||
|
|
@ -152,10 +156,18 @@ webServer.on('messageReceived', (message) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
webServer.on('clientConnected', () => {
|
webServer.on('clientConnected', (client) => {
|
||||||
webServer.notifyClients('metrics', { peripheralMode: peripheralManager.getPeripheralMode() })
|
webServer.notifyClient(client, 'config', getConfig())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// todo: extract this into some kind of state manager
|
||||||
|
function getConfig () {
|
||||||
|
return {
|
||||||
|
peripheralMode: peripheralManager.getPeripheralMode(),
|
||||||
|
stravaUploadEnabled: !!config.stravaClientId && !!config.stravaClientSecret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
replayRowingSession(handleRotationImpulse, {
|
replayRowingSession(handleRotationImpulse, {
|
||||||
filename: 'recordings/WRX700_2magnets.csv',
|
filename: 'recordings/WRX700_2magnets.csv',
|
||||||
|
|
|
||||||
|
|
@ -12,45 +12,45 @@ import fs from 'fs/promises'
|
||||||
|
|
||||||
const clientId = config.stravaClientId
|
const clientId = config.stravaClientId
|
||||||
const clientSecret = config.stravaClientSecret
|
const clientSecret = config.stravaClientSecret
|
||||||
|
const stravaTokenFile = './config/stravatoken'
|
||||||
|
|
||||||
function createAuthorizedConnection (getStravaAuthorizationCode) {
|
function createAuthorizedConnection (getStravaAuthorizationCode) {
|
||||||
const controller = new AbortController()
|
|
||||||
let accessToken
|
let accessToken
|
||||||
let refreshToken
|
let refreshToken
|
||||||
|
|
||||||
const authorizedConnection = axios.create({
|
const authorizedConnection = axios.create({
|
||||||
baseURL: 'https://www.strava.com/api/v3',
|
baseURL: 'https://www.strava.com/api/v3'
|
||||||
signal: controller.signal
|
|
||||||
})
|
})
|
||||||
|
|
||||||
authorizedConnection.interceptors.request.use(async config => {
|
authorizedConnection.interceptors.request.use(async config => {
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
try {
|
try {
|
||||||
refreshToken = await fs.readFile('./config/stravatoken', 'utf-8')
|
refreshToken = await fs.readFile(stravaTokenFile, 'utf-8')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.info('no strava token available yet')
|
log.info('no strava token available yet')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if no refresh token is set, then the app has not yet been authorized with Strava
|
// if no refresh token is set, then the app has not yet been authorized with Strava
|
||||||
|
// start oAuth authorization process
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
const authorizationCode = await getStravaAuthorizationCode();
|
const authorizationCode = await getStravaAuthorizationCode();
|
||||||
({ accessToken, refreshToken } = await authorize(authorizationCode))
|
({ accessToken, refreshToken } = await authorize(authorizationCode))
|
||||||
|
await writeToken('', refreshToken)
|
||||||
|
// otherwise we just need to get a valid accessToken
|
||||||
} else {
|
} else {
|
||||||
|
const oldRefreshToken = refreshToken;
|
||||||
({ accessToken, refreshToken } = await getAccessTokens(refreshToken))
|
({ accessToken, refreshToken } = await getAccessTokens(refreshToken))
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
log.error('strava token is invalid, delete config/stravatoken and try again')
|
log.error(`strava token is invalid, deleting ${stravaTokenFile}...`)
|
||||||
|
await fs.unlink(stravaTokenFile)
|
||||||
|
// if the refreshToken has changed, persist it
|
||||||
} else {
|
} else {
|
||||||
try {
|
await writeToken(oldRefreshToken, refreshToken)
|
||||||
await fs.writeFile('./config/stravatoken', refreshToken, 'utf-8')
|
|
||||||
} catch (error) {
|
|
||||||
log.info('can not persist strava token', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
log.error('strava authorization not successful')
|
log.error('strava authorization not successful')
|
||||||
controller.abort()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(config.headers, { Authorization: `Bearer ${accessToken}` })
|
Object.assign(config.headers, { Authorization: `Bearer ${accessToken}` })
|
||||||
|
|
@ -112,6 +112,16 @@ function createAuthorizedConnection (getStravaAuthorizationCode) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function writeToken (oldToken, newToken) {
|
||||||
|
if (oldToken !== newToken) {
|
||||||
|
try {
|
||||||
|
await fs.writeFile(stravaTokenFile, newToken, 'utf-8')
|
||||||
|
} catch (error) {
|
||||||
|
log.info(`can not write strava token to file ${stravaTokenFile}`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return authorizedConnection
|
return authorizedConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,9 @@ export default {
|
||||||
// Note that these values are not your Strava credentials
|
// Note that these values are not your Strava credentials
|
||||||
// Instead you have to create a Strava API Application as described here:
|
// Instead you have to create a Strava API Application as described here:
|
||||||
// https://developers.strava.com/docs/getting-started/#account and use the corresponding values
|
// https://developers.strava.com/docs/getting-started/#account and use the corresponding values
|
||||||
|
// WARNING: if you enabled the network share via the installer script, then this config file will be
|
||||||
|
// exposed via network share on your local network. You might consider disabling (or password protect)
|
||||||
|
// the Configuration share in smb.conf
|
||||||
// The "Client ID" of your Strava API Application
|
// The "Client ID" of your Strava API Application
|
||||||
stravaClientId: '',
|
stravaClientId: '',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ export default {
|
||||||
upgrade: (req, socket, head) => {
|
upgrade: (req, socket, head) => {
|
||||||
const defaultWSHandler = (err, req, socket, head) => {
|
const defaultWSHandler = (err, req, socket, head) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('proxy error', err)
|
|
||||||
socket.destroy()
|
socket.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue