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`
+
+
+ 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`
-
-
- 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`
+
+
+ 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)