wires strava authorization to the web ui
This commit is contained in:
parent
15cdf2e22f
commit
dfc17c09b2
|
|
@ -48,8 +48,8 @@ function createWebServer () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function notifyClients (message) {
|
function notifyClients (type, data) {
|
||||||
const messageString = JSON.stringify(message)
|
const messageString = JSON.stringify({ type, data })
|
||||||
wss.clients.forEach(function each (client) {
|
wss.clients.forEach(function each (client) {
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
client.send(messageString)
|
client.send(messageString)
|
||||||
|
|
|
||||||
|
|
@ -12,23 +12,40 @@ const rowingMetricsFields = ['strokesTotal', 'distanceTotal', 'caloriesTotal', '
|
||||||
'heartrateBatteryLevel', 'splitFormatted', 'strokesPerMinute', 'durationTotalFormatted']
|
'heartrateBatteryLevel', 'splitFormatted', 'strokesPerMinute', 'durationTotalFormatted']
|
||||||
|
|
||||||
export function createApp (app) {
|
export function createApp (app) {
|
||||||
const mode = window.location.hash
|
const urlParameters = new URLSearchParams(window.location.search)
|
||||||
const appMode = mode === '#:standalone:' ? 'STANDALONE' : mode === '#:kiosk:' ? 'KIOSK' : 'BROWSER'
|
const mode = urlParameters.get('mode')
|
||||||
|
const appMode = mode === 'standalone' ? 'STANDALONE' : mode === 'kiosk' ? 'KIOSK' : 'BROWSER'
|
||||||
app.updateState({ ...app.getState(), appMode })
|
app.updateState({ ...app.getState(), appMode })
|
||||||
|
|
||||||
|
const stravaAuthorizationCode = urlParameters.get('code')
|
||||||
|
|
||||||
let socket
|
let socket
|
||||||
|
|
||||||
initWebsocket()
|
initWebsocket()
|
||||||
resetFields()
|
resetFields()
|
||||||
requestWakeLock()
|
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 () {
|
function initWebsocket () {
|
||||||
// use the native websocket implementation of browser to communicate with backend
|
// 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 = new WebSocket(`ws://${location.host}/websocket`)
|
||||||
|
|
||||||
socket.addEventListener('open', (event) => {
|
socket.addEventListener('open', (event) => {
|
||||||
console.log('websocket opened')
|
console.log('websocket opened')
|
||||||
|
if (initialWebsocketOpenend) {
|
||||||
|
websocketOpened()
|
||||||
|
initialWebsocketOpenend = false
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.addEventListener('error', (error) => {
|
socket.addEventListener('error', (error) => {
|
||||||
|
|
@ -46,21 +63,37 @@ export function createApp (app) {
|
||||||
// todo: we should use different types of messages to make processing easier
|
// todo: we should use different types of messages to make processing easier
|
||||||
socket.addEventListener('message', (event) => {
|
socket.addEventListener('message', (event) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data)
|
const message = JSON.parse(event.data)
|
||||||
|
if (!message.type) {
|
||||||
let activeFields = rowingMetricsFields
|
console.error('message does not contain messageType specifier', message)
|
||||||
// if we are in reset state only update heart rate
|
return
|
||||||
if (data.strokesTotal === 0) {
|
|
||||||
activeFields = ['heartrate', 'heartrateBatteryLevel']
|
|
||||||
}
|
}
|
||||||
|
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)
|
const filteredData = filterObjectByKeys(data, activeFields)
|
||||||
|
let updatedState = { ...app.getState(), metrics: filteredData }
|
||||||
let updatedState = { ...app.getState(), metrics: filteredData }
|
if (data.peripheralMode) {
|
||||||
if (data.peripheralMode) {
|
updatedState = { ...app.getState(), peripheralMode: 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) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"background_color": "#0059B3",
|
"background_color": "#002b57",
|
||||||
"display": "fullscreen",
|
"display": "fullscreen",
|
||||||
"orientation": "any",
|
"orientation": "any",
|
||||||
"start_url": "/#:standalone:"
|
"start_url": "/?mode=standalone"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,32 @@
|
||||||
Handles uploading workout data to different cloud providers
|
Handles uploading workout data to different cloud providers
|
||||||
*/
|
*/
|
||||||
import log from 'loglevel'
|
import log from 'loglevel'
|
||||||
|
import EventEmitter from 'events'
|
||||||
import { createStravaAPI } from '../tools/StravaAPI.js'
|
import { createStravaAPI } from '../tools/StravaAPI.js'
|
||||||
|
import config from '../tools/ConfigManager.js'
|
||||||
|
|
||||||
function createWorkoutUploader (workoutRecorder) {
|
function createWorkoutUploader (workoutRecorder) {
|
||||||
|
const emitter = new EventEmitter()
|
||||||
|
|
||||||
|
let stravaAuthorizationCodeResolver
|
||||||
|
|
||||||
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')
|
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)
|
const stravaAPI = createStravaAPI(getStravaAuthorizationCode)
|
||||||
|
|
||||||
|
function stravaAuthorizationCode (stravaAuthorizationCode) {
|
||||||
|
if (stravaAuthorizationCodeResolver) {
|
||||||
|
stravaAuthorizationCodeResolver(stravaAuthorizationCode)
|
||||||
|
stravaAuthorizationCodeResolver = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function upload () {
|
async function upload () {
|
||||||
if (workoutRecorder.canCreateRecordings()) {
|
if (workoutRecorder.canCreateRecordings()) {
|
||||||
log.debug('uploading workout to strava...')
|
log.debug('uploading workout to strava...')
|
||||||
|
|
@ -26,9 +40,10 @@ function createWorkoutUploader (workoutRecorder) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return Object.assign(emitter, {
|
||||||
upload
|
upload,
|
||||||
}
|
stravaAuthorizationCode
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export { createWorkoutUploader }
|
export { createWorkoutUploader }
|
||||||
|
|
|
||||||
|
|
@ -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({ peripheralMode: event.req.peripheralMode })
|
webServer.notifyClients('metrics', { peripheralMode: event.req.peripheralMode })
|
||||||
event.res = true
|
event.res = true
|
||||||
} else {
|
} else {
|
||||||
log.info('unhandled Command', event.req)
|
log.info('unhandled Command', event.req)
|
||||||
|
|
@ -81,7 +81,7 @@ const workoutRecorder = createWorkoutRecorder()
|
||||||
const workoutUploader = createWorkoutUploader(workoutRecorder)
|
const workoutUploader = createWorkoutUploader(workoutRecorder)
|
||||||
|
|
||||||
rowingStatistics.on('driveFinished', (metrics) => {
|
rowingStatistics.on('driveFinished', (metrics) => {
|
||||||
webServer.notifyClients(metrics)
|
webServer.notifyClients('metrics', metrics)
|
||||||
peripheralManager.notifyMetrics('strokeStateChanged', 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` +
|
`, 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: ${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`)
|
`, 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)
|
peripheralManager.notifyMetrics('strokeFinished', metrics)
|
||||||
if (metrics.sessionState === 'rowing') {
|
if (metrics.sessionState === 'rowing') {
|
||||||
workoutRecorder.recordStroke(metrics)
|
workoutRecorder.recordStroke(metrics)
|
||||||
|
|
@ -98,7 +98,7 @@ rowingStatistics.on('recoveryFinished', (metrics) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
rowingStatistics.on('webMetricsUpdate', (metrics) => {
|
rowingStatistics.on('webMetricsUpdate', (metrics) => {
|
||||||
webServer.notifyClients(metrics)
|
webServer.notifyClients('metrics', metrics)
|
||||||
})
|
})
|
||||||
|
|
||||||
rowingStatistics.on('peripheralMetricsUpdate', (metrics) => {
|
rowingStatistics.on('peripheralMetricsUpdate', (metrics) => {
|
||||||
|
|
@ -123,6 +123,10 @@ if (config.heartrateMonitorANT) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
workoutUploader.on('authorizeStrava', (data) => {
|
||||||
|
webServer.notifyClients('authorizeStrava', data)
|
||||||
|
})
|
||||||
|
|
||||||
const webServer = createWebServer()
|
const webServer = createWebServer()
|
||||||
webServer.on('messageReceived', (message) => {
|
webServer.on('messageReceived', (message) => {
|
||||||
switch (message.command) {
|
switch (message.command) {
|
||||||
|
|
@ -138,6 +142,10 @@ webServer.on('messageReceived', (message) => {
|
||||||
workoutUploader.upload()
|
workoutUploader.upload()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 'stravaAuthorizationCode': {
|
||||||
|
workoutUploader.stravaAuthorizationCode(message.data)
|
||||||
|
break
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
log.warn('invalid command received:', message)
|
log.warn('invalid command received:', message)
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +153,7 @@ webServer.on('messageReceived', (message) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
webServer.on('clientConnected', () => {
|
webServer.on('clientConnected', () => {
|
||||||
webServer.notifyClients({ peripheralMode: peripheralManager.getPeripheralMode() })
|
webServer.notifyClients('metrics', { peripheralMode: peripheralManager.getPeripheralMode() })
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,4 @@ openbox-session &
|
||||||
# Start Chromium in kiosk mode
|
# 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/' ~/.config/chromium/'Local State'
|
||||||
sed -i 's/"exited_cleanly":false/"exited_cleanly":true/; s/"exit_type":"[^"]\+"/"exit_type":"Normal"/' ~/.config/chromium/Default/Preferences
|
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"
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
"lint": "eslint ./app ./config && markdownlint-cli2 '**/*.md' '#node_modules'",
|
"lint": "eslint ./app ./config && markdownlint-cli2 '**/*.md' '#node_modules'",
|
||||||
"start": "node app/server.js",
|
"start": "node app/server.js",
|
||||||
"dev": "npm-run-all --parallel dev:backend dev:frontend",
|
"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",
|
"dev:frontend": "snowpack dev",
|
||||||
"build": "rollup -c",
|
"build": "rollup -c",
|
||||||
"build:watch": "rollup -cw",
|
"build:watch": "rollup -cw",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue