From dec19fdd1f40b099a6e83475a024e61c0d622918 Mon Sep 17 00:00:00 2001 From: Lars Berning <151194+laberning@users.noreply.github.com> Date: Sat, 12 Feb 2022 11:32:31 +0000 Subject: [PATCH] adds feature to shutdown device, adds some minor UI improvements --- app/client/components/AppDialog.js | 13 +- app/client/components/DashboardActions.js | 186 ++++++++++++---------- app/client/lib/app.js | 4 + app/client/store/appState.js | 6 +- app/server.js | 28 +++- config/default.config.js | 3 + docs/backlog.md | 1 - 7 files changed, 143 insertions(+), 98 deletions(-) diff --git a/app/client/components/AppDialog.js b/app/client/components/AppDialog.js index d2f4c8e..eab5d2e 100644 --- a/app/client/components/AppDialog.js +++ b/app/client/components/AppDialog.js @@ -17,10 +17,6 @@ export class AppDialog extends AppElement { } static styles = css` - dialog::backdrop { - background: none; - backdrop-filter: contrast(15%) blur(2px); - } dialog { border: none; color: var(--theme-font-color); @@ -30,6 +26,11 @@ export class AppDialog extends AppElement { padding: 1.6rem; max-width: 80%; } + dialog::backdrop { + background: none; + backdrop-filter: contrast(15%) blur(2px); + } + button { outline:none; background-color: var(--theme-button-color); @@ -45,6 +46,10 @@ export class AppDialog extends AppElement { justify-content: center; align-items: center; } + button:hover { + filter: brightness(150%); + } + fieldset { border: 0; margin: unset; diff --git a/app/client/components/DashboardActions.js b/app/client/components/DashboardActions.js index 85f39f2..8eb5359 100644 --- a/app/client/components/DashboardActions.js +++ b/app/client/components/DashboardActions.js @@ -28,6 +28,9 @@ export class DashboardActions extends AppElement { justify-content: center; align-items: center; } + button:hover { + filter: brightness(150%); + } #fullscreen-icon { display: inline-flex; @@ -55,97 +58,108 @@ export class DashboardActions extends AppElement { } ` - @state({ type: Object }) - dialog + @state({ type: Object }) + dialog - render () { - return html` - - ${this.renderOptionalButtons()} - -
${this.peripheralMode()}
- ${this.dialog ? this.dialog : ''} + render () { + return html` + + ${this.renderOptionalButtons()} + +
${this.peripheralMode()}
+ ${this.dialog ? this.dialog : ''} + ` + } + + renderOptionalButtons () { + const buttons = [] + // changing to fullscreen mode only makes sence when the app is openend in a regular + // webbrowser (kiosk and standalone mode are always in fullscreen view) and if the + // browser supports this feature + if (this.appState?.appMode === 'BROWSER' && document.documentElement.requestFullscreen) { + buttons.push(html` + + `) + } + // add a button to power down the device, if browser is running on the device in kiosk mode + // and the shutdown feature is enabled + // (might also make sence to enable this for all clients but then we would need visual feedback) + if (this.appState?.appMode === 'KIOSK' && this.appState?.config?.shutdownEnabled) { + buttons.push(html` + + `) + } + + if (this.appState?.config?.stravaUploadEnabled) { + buttons.push(html` + + `) + } + return buttons + } + + peripheralMode () { + const value = this.appState?.config?.peripheralMode + if (value === 'PM5') { + return 'C2 PM5' + } else if (value === 'FTMSBIKE') { + return 'FTMS Bike' + } else if (value === 'FTMS') { + return 'FTMS Rower' + } else { + return '' + } + } + + toggleFullscreen () { + const fullscreenElement = document.getElementsByTagName('web-app')[0] + if (!document.fullscreenElement) { + fullscreenElement.requestFullscreen({ navigationUI: 'hide' }) + } else { + if (document.exitFullscreen) { + document.exitFullscreen() + } + } + } + + reset () { + this.sendEvent('triggerAction', { command: 'reset' }) + } + + switchPeripheralMode () { + this.sendEvent('triggerAction', { command: 'switchPeripheralMode' }) + } + + uploadTraining () { + this.dialog = html` + + ${icon_upload}
Upload to Strava?
+

Do you want to finish your workout and upload it to Strava?

+
` - } - - renderOptionalButtons () { - const buttons = [] - // changing to fullscreen mode only makes sence when the app is openend in a regular - // webbrowser (kiosk and standalone mode are always in fullscreen view) and if the - // browser supports this feature - if (this.appState.appMode === 'BROWSER' && document.documentElement.requestFullscreen) { - buttons.push(html` - - `) - } - // a shutdown button only makes sence when the app is openend as app on a mobile - // device. at some point we might also think of using this to power down the raspi - // when we are running in kiosk mode - if (this.appState.appMode === 'STANDALONE') { - buttons.push(html` - - `) - } - - if (this.appState.config.stravaUploadEnabled) { - buttons.push(html` - - `) - } - return buttons - } - - peripheralMode () { - const value = this.appState?.config?.peripheralMode - if (value === 'PM5') { - return 'C2 PM5' - } else if (value === 'FTMSBIKE') { - return 'FTMS Bike' - } else if (value === 'FTMS') { - return 'FTMS Rower' - } else { - return '' + function dialogClosed (event) { + this.dialog = undefined + if (event.detail === 'confirm') { + this.sendEvent('triggerAction', { command: 'uploadTraining' }) } } + } - toggleFullscreen () { - const fullscreenElement = document.getElementsByTagName('web-app')[0] - if (!document.fullscreenElement) { - fullscreenElement.requestFullscreen({ navigationUI: 'hide' }) - } else { - if (document.exitFullscreen) { - document.exitFullscreen() - } - } - } - - close () { - window.close() - } - - reset () { - this.sendEvent('triggerAction', { command: 'reset' }) - } - - switchPeripheralMode () { - this.sendEvent('triggerAction', { command: 'switchPeripheralMode' }) - } - - uploadTraining () { - this.dialog = html` - - Upload to Strava? -

Do you want to finish your workout and upload it to Strava?

-
- ` - function dialogClosed (event) { - this.dialog = undefined - if (event.detail === 'confirm') { - this.sendEvent('triggerAction', { command: 'uploadTraining' }) - } + shutdown () { + this.dialog = html` + + ${icon_poweroff}
Shutdown Open Rowing Monitor?
+

Do you want to shutdown the device?

+
+ ` + function dialogClosed (event) { + this.dialog = undefined + if (event.detail === 'confirm') { + this.sendEvent('triggerAction', { command: 'shutdown' }) } } + } } diff --git a/app/client/lib/app.js b/app/client/lib/app.js index 1dfd847..90f5801 100644 --- a/app/client/lib/app.js +++ b/app/client/lib/app.js @@ -137,6 +137,10 @@ export function createApp (app) { if (socket)socket.send(JSON.stringify({ command: 'uploadTraining' })) break } + case 'shutdown': { + if (socket)socket.send(JSON.stringify({ command: 'shutdown' })) + break + } default: { console.error('no handler defined for action', action) } diff --git a/app/client/store/appState.js b/app/client/store/appState.js index 81ae24a..3a93f55 100644 --- a/app/client/store/appState.js +++ b/app/client/store/appState.js @@ -13,7 +13,9 @@ export const APP_STATE = { config: { // currently can be FTMS, FTMSBIKE or PM5 peripheralMode: '', - // true if upload to strava is configured - stravaUploadEnabled: false + // true if upload to strava is enabled + stravaUploadEnabled: false, + // true if remote device shutdown is enabled + shutdownEnabled: false } } diff --git a/app/server.js b/app/server.js index 33d9e51..32ce762 100644 --- a/app/server.js +++ b/app/server.js @@ -6,7 +6,8 @@ everything together while figuring out the physics and model of the application. todo: refactor this as we progress */ -import { fork } from 'child_process' +import child_process from 'child_process' +import { promisify } from 'util' import log from 'loglevel' import config from './tools/ConfigManager.js' import { createRowingEngine } from './engine/RowingEngine.js' @@ -18,6 +19,7 @@ import { createAntManager } from './ant/AntManager.js' import { replayRowingSession } from './tools/RowingRecorder.js' import { createWorkoutRecorder } from './engine/WorkoutRecorder.js' import { createWorkoutUploader } from './engine/WorkoutUploader.js' +const exec = promisify(child_process.exec) // set the log levels log.setLevel(config.loglevel.default) @@ -66,7 +68,7 @@ function resetWorkout () { peripheralManager.notifyStatus({ name: 'reset' }) } -const gpioTimerService = fork('./app/gpio/GpioTimerService.js') +const gpioTimerService = child_process.fork('./app/gpio/GpioTimerService.js') gpioTimerService.on('message', handleRotationImpulse) function handleRotationImpulse (dataPoint) { @@ -110,7 +112,7 @@ rowingStatistics.on('rowingPaused', () => { }) if (config.heartrateMonitorBLE) { - const bleCentralService = fork('./app/ble/CentralService.js') + const bleCentralService = child_process.fork('./app/ble/CentralService.js') bleCentralService.on('message', (heartrateMeasurement) => { rowingStatistics.handleHeartrateMeasurement(heartrateMeasurement) }) @@ -132,7 +134,7 @@ workoutUploader.on('resetWorkout', () => { }) const webServer = createWebServer() -webServer.on('messageReceived', (message, client) => { +webServer.on('messageReceived', async (message, client) => { switch (message.command) { case 'switchPeripheralMode': { peripheralManager.switchPeripheralMode() @@ -146,6 +148,21 @@ webServer.on('messageReceived', (message, client) => { workoutUploader.upload(client) break } + case 'shutdown': { + if (getConfig().shutdownEnabled) { + console.info('shutting down device...') + try { + const { stdout, stderr } = await exec(config.shutdownCommand) + if (stderr) { + log.error('can not shutdown: ', stderr) + } + log.info(stdout) + } catch (error) { + log.error('can not shutdown: ', error) + } + } + break + } case 'stravaAuthorizationCode': { workoutUploader.stravaAuthorizationCode(message.data) break @@ -164,7 +181,8 @@ webServer.on('clientConnected', (client) => { function getConfig () { return { peripheralMode: peripheralManager.getPeripheralMode(), - stravaUploadEnabled: !!config.stravaClientId && !!config.stravaClientSecret + stravaUploadEnabled: !!config.stravaClientId && !!config.stravaClientSecret, + shutdownEnabled: !!config.shutdownCommand } } diff --git a/config/default.config.js b/config/default.config.js index 59ff223..9d73f2d 100644 --- a/config/default.config.js +++ b/config/default.config.js @@ -101,6 +101,9 @@ export default { // is the fallback for the default profile settings rowerSettings: rowerProfiles.DEFAULT, + // command to shutdown the device via the user interface, leave empty to disable this feature + shutdownCommand: 'halt', + // Configures the connection to Strava (to directly upload workouts to Strava) // Note that these values are not your Strava credentials // Instead you have to create a Strava API Application as described here: diff --git a/docs/backlog.md b/docs/backlog.md index 4172c39..c3bacfb 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -10,7 +10,6 @@ This is the very minimalistic Backlog for further development of this project. ## Later -* automatically upload recorded rowing sessions to training platforms (i.e. Strava) * figure out where to set the Service Advertising Data (FTMS.pdf p 15) * add some attributes to BLE DeviceInformationService * make Web UI a proper Web Application (tooling and SPA framework)