diff --git a/app/WebServer.js b/app/WebServer.js index 368e0fa..af1e11f 100644 --- a/app/WebServer.js +++ b/app/WebServer.js @@ -48,8 +48,8 @@ function createWebServer () { }) }) - function notifyClients (message) { - const messageString = JSON.stringify(message) + function notifyClients (type, data) { + const messageString = JSON.stringify({ type, data }) wss.clients.forEach(function each (client) { if (client.readyState === WebSocket.OPEN) { client.send(messageString) diff --git a/app/client/lib/app.js b/app/client/lib/app.js index 5e78463..75fc22f 100644 --- a/app/client/lib/app.js +++ b/app/client/lib/app.js @@ -12,23 +12,40 @@ const rowingMetricsFields = ['strokesTotal', 'distanceTotal', 'caloriesTotal', ' 'heartrateBatteryLevel', 'splitFormatted', 'strokesPerMinute', 'durationTotalFormatted'] export function createApp (app) { - const mode = window.location.hash - const appMode = mode === '#:standalone:' ? 'STANDALONE' : mode === '#:kiosk:' ? 'KIOSK' : 'BROWSER' + const urlParameters = new URLSearchParams(window.location.search) + const mode = urlParameters.get('mode') + const appMode = mode === 'standalone' ? 'STANDALONE' : mode === 'kiosk' ? 'KIOSK' : 'BROWSER' app.updateState({ ...app.getState(), appMode }) + const stravaAuthorizationCode = urlParameters.get('code') + let socket initWebsocket() resetFields() requestWakeLock() + function websocketOpened () { + if (stravaAuthorizationCode) { + handleStravaAuthorization(stravaAuthorizationCode) + } + } + + function handleStravaAuthorization (stravaAuthorizationCode) { + if (socket)socket.send(JSON.stringify({ command: 'stravaAuthorizationCode', data: stravaAuthorizationCode })) + } + + let initialWebsocketOpenend = true function initWebsocket () { // use the native websocket implementation of browser to communicate with backend - // eslint-disable-next-line no-undef socket = new WebSocket(`ws://${location.host}/websocket`) socket.addEventListener('open', (event) => { console.log('websocket opened') + if (initialWebsocketOpenend) { + websocketOpened() + initialWebsocketOpenend = false + } }) socket.addEventListener('error', (error) => { @@ -46,21 +63,37 @@ export function createApp (app) { // todo: we should use different types of messages to make processing easier socket.addEventListener('message', (event) => { try { - const data = JSON.parse(event.data) - - let activeFields = rowingMetricsFields - // if we are in reset state only update heart rate - if (data.strokesTotal === 0) { - activeFields = ['heartrate', 'heartrateBatteryLevel'] + const message = JSON.parse(event.data) + if (!message.type) { + console.error('message does not contain messageType specifier', message) + return } + const data = message.data + switch (message.type) { + case 'metrics': { + let activeFields = rowingMetricsFields + // if we are in reset state only update heart rate + if (data.strokesTotal === 0) { + activeFields = ['heartrate', 'heartrateBatteryLevel'] + } - const filteredData = filterObjectByKeys(data, activeFields) - - let updatedState = { ...app.getState(), metrics: filteredData } - if (data.peripheralMode) { - updatedState = { ...app.getState(), peripheralMode: data.peripheralMode } + const filteredData = filterObjectByKeys(data, activeFields) + let updatedState = { ...app.getState(), metrics: filteredData } + if (data.peripheralMode) { + updatedState = { ...app.getState(), peripheralMode: data.peripheralMode } + } + app.updateState(updatedState) + break + } + case 'authorizeStrava': { + const currentUrl = encodeURIComponent(window.location.href) + window.location.href = `https://www.strava.com/oauth/authorize?client_id=${data.stravaClientId}&response_type=code&redirect_uri=${currentUrl}&approval_prompt=force&scope=activity:write` + break + } + default: { + console.error(`unknown message type: ${message.type}`) + } } - app.updateState(updatedState) } catch (err) { console.log(err) } diff --git a/app/client/manifest.json b/app/client/manifest.json index aad8d39..9c3c018 100644 --- a/app/client/manifest.json +++ b/app/client/manifest.json @@ -9,8 +9,8 @@ "type": "image/png" } ], - "background_color": "#0059B3", + "background_color": "#002b57", "display": "fullscreen", "orientation": "any", - "start_url": "/#:standalone:" + "start_url": "/?mode=standalone" } diff --git a/app/engine/WorkoutUploader.js b/app/engine/WorkoutUploader.js index 3d0c3af..de4c08e 100644 --- a/app/engine/WorkoutUploader.js +++ b/app/engine/WorkoutUploader.js @@ -5,18 +5,32 @@ Handles uploading workout data to different cloud providers */ import log from 'loglevel' +import EventEmitter from 'events' import { createStravaAPI } from '../tools/StravaAPI.js' +import config from '../tools/ConfigManager.js' function createWorkoutUploader (workoutRecorder) { + const emitter = new EventEmitter() + + let stravaAuthorizationCodeResolver + function getStravaAuthorizationCode () { 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') - setTimeout(() => { resolve('') }, 10) + emitter.emit('authorizeStrava', { stravaClientId: config.stravaClientId }) + stravaAuthorizationCodeResolver = resolve }) } const stravaAPI = createStravaAPI(getStravaAuthorizationCode) + function stravaAuthorizationCode (stravaAuthorizationCode) { + if (stravaAuthorizationCodeResolver) { + stravaAuthorizationCodeResolver(stravaAuthorizationCode) + stravaAuthorizationCodeResolver = undefined + } + } + async function upload () { if (workoutRecorder.canCreateRecordings()) { log.debug('uploading workout to strava...') @@ -26,9 +40,10 @@ function createWorkoutUploader (workoutRecorder) { } } - return { - upload - } + return Object.assign(emitter, { + upload, + stravaAuthorizationCode + }) } export { createWorkoutUploader } diff --git a/app/server.js b/app/server.js index 1fedbb8..100fbac 100644 --- a/app/server.js +++ b/app/server.js @@ -52,7 +52,7 @@ peripheralManager.on('control', (event) => { peripheralManager.notifyStatus({ name: 'startedOrResumedByUser' }) event.res = true } else if (event?.req?.name === 'peripheralMode') { - webServer.notifyClients({ peripheralMode: event.req.peripheralMode }) + webServer.notifyClients('metrics', { peripheralMode: event.req.peripheralMode }) event.res = true } else { log.info('unhandled Command', event.req) @@ -81,7 +81,7 @@ const workoutRecorder = createWorkoutRecorder() const workoutUploader = createWorkoutUploader(workoutRecorder) rowingStatistics.on('driveFinished', (metrics) => { - webServer.notifyClients(metrics) + webServer.notifyClients('metrics', metrics) peripheralManager.notifyMetrics('strokeStateChanged', metrics) }) @@ -90,7 +90,7 @@ rowingStatistics.on('recoveryFinished', (metrics) => { `, split: ${metrics.splitFormatted}, ratio: ${metrics.powerRatio.toFixed(2)}, dist: ${metrics.distanceTotal.toFixed(1)}m` + `, cal: ${metrics.caloriesTotal.toFixed(1)}kcal, SPM: ${metrics.strokesPerMinute.toFixed(1)}, speed: ${metrics.speed.toFixed(2)}km/h` + `, cal/hour: ${metrics.caloriesPerHour.toFixed(1)}kcal, cal/minute: ${metrics.caloriesPerMinute.toFixed(1)}kcal`) - webServer.notifyClients(metrics) + webServer.notifyClients('metrics', metrics) peripheralManager.notifyMetrics('strokeFinished', metrics) if (metrics.sessionState === 'rowing') { workoutRecorder.recordStroke(metrics) @@ -98,7 +98,7 @@ rowingStatistics.on('recoveryFinished', (metrics) => { }) rowingStatistics.on('webMetricsUpdate', (metrics) => { - webServer.notifyClients(metrics) + webServer.notifyClients('metrics', metrics) }) rowingStatistics.on('peripheralMetricsUpdate', (metrics) => { @@ -123,6 +123,10 @@ if (config.heartrateMonitorANT) { }) } +workoutUploader.on('authorizeStrava', (data) => { + webServer.notifyClients('authorizeStrava', data) +}) + const webServer = createWebServer() webServer.on('messageReceived', (message) => { switch (message.command) { @@ -138,6 +142,10 @@ webServer.on('messageReceived', (message) => { workoutUploader.upload() break } + case 'stravaAuthorizationCode': { + workoutUploader.stravaAuthorizationCode(message.data) + break + } default: { log.warn('invalid command received:', message) } @@ -145,7 +153,7 @@ webServer.on('messageReceived', (message) => { }) webServer.on('clientConnected', () => { - webServer.notifyClients({ peripheralMode: peripheralManager.getPeripheralMode() }) + webServer.notifyClients('metrics', { peripheralMode: peripheralManager.getPeripheralMode() }) }) /* diff --git a/install/webbrowserkiosk.sh b/install/webbrowserkiosk.sh index d9336f6..17ea519 100644 --- a/install/webbrowserkiosk.sh +++ b/install/webbrowserkiosk.sh @@ -12,4 +12,4 @@ openbox-session & # Start Chromium in kiosk mode sed -i 's/"exited_cleanly":false/"exited_cleanly":true/' ~/.config/chromium/'Local State' sed -i 's/"exited_cleanly":false/"exited_cleanly":true/; s/"exit_type":"[^"]\+"/"exit_type":"Normal"/' ~/.config/chromium/Default/Preferences -chromium-browser --disable-infobars --kiosk --noerrdialogs --disable-session-crashed-bubble --disable-pinch --check-for-update-interval=604800 --app="http://127.0.0.1/#:kiosk:" +chromium-browser --disable-infobars --kiosk --noerrdialogs --disable-session-crashed-bubble --disable-pinch --check-for-update-interval=604800 --app="http://127.0.0.1/?mode=kiosk" diff --git a/package.json b/package.json index dc0fb14..dc7694d 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "lint": "eslint ./app ./config && markdownlint-cli2 '**/*.md' '#node_modules'", "start": "node app/server.js", "dev": "npm-run-all --parallel dev:backend dev:frontend", - "dev:backend": "nodemon app/server.js", + "dev:backend": "nodemon --ignore 'app/client/**/*' app/server.js", "dev:frontend": "snowpack dev", "build": "rollup -c", "build:watch": "rollup -cw",