adds feature to shutdown device, adds some minor UI improvements
This commit is contained in:
parent
943518b986
commit
dec19fdd1f
|
|
@ -17,10 +17,6 @@ export class AppDialog extends AppElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
dialog::backdrop {
|
|
||||||
background: none;
|
|
||||||
backdrop-filter: contrast(15%) blur(2px);
|
|
||||||
}
|
|
||||||
dialog {
|
dialog {
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--theme-font-color);
|
color: var(--theme-font-color);
|
||||||
|
|
@ -30,6 +26,11 @@ export class AppDialog extends AppElement {
|
||||||
padding: 1.6rem;
|
padding: 1.6rem;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
}
|
}
|
||||||
|
dialog::backdrop {
|
||||||
|
background: none;
|
||||||
|
backdrop-filter: contrast(15%) blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
outline:none;
|
outline:none;
|
||||||
background-color: var(--theme-button-color);
|
background-color: var(--theme-button-color);
|
||||||
|
|
@ -45,6 +46,10 @@ export class AppDialog extends AppElement {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
button:hover {
|
||||||
|
filter: brightness(150%);
|
||||||
|
}
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
border: 0;
|
border: 0;
|
||||||
margin: unset;
|
margin: unset;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ export class DashboardActions extends AppElement {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
button:hover {
|
||||||
|
filter: brightness(150%);
|
||||||
|
}
|
||||||
|
|
||||||
#fullscreen-icon {
|
#fullscreen-icon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
@ -73,7 +76,7 @@ export class DashboardActions extends AppElement {
|
||||||
// changing to fullscreen mode only makes sence when the app is openend in a regular
|
// 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
|
// webbrowser (kiosk and standalone mode are always in fullscreen view) and if the
|
||||||
// browser supports this feature
|
// browser supports this feature
|
||||||
if (this.appState.appMode === 'BROWSER' && document.documentElement.requestFullscreen) {
|
if (this.appState?.appMode === 'BROWSER' && document.documentElement.requestFullscreen) {
|
||||||
buttons.push(html`
|
buttons.push(html`
|
||||||
<button @click=${this.toggleFullscreen}>
|
<button @click=${this.toggleFullscreen}>
|
||||||
<div id="fullscreen-icon">${icon_expand}</div>
|
<div id="fullscreen-icon">${icon_expand}</div>
|
||||||
|
|
@ -81,16 +84,16 @@ export class DashboardActions extends AppElement {
|
||||||
</button>
|
</button>
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
// a shutdown button only makes sence when the app is openend as app on a mobile
|
// add a button to power down the device, if browser is running on the device in kiosk mode
|
||||||
// device. at some point we might also think of using this to power down the raspi
|
// and the shutdown feature is enabled
|
||||||
// when we are running in kiosk mode
|
// (might also make sence to enable this for all clients but then we would need visual feedback)
|
||||||
if (this.appState.appMode === 'STANDALONE') {
|
if (this.appState?.appMode === 'KIOSK' && this.appState?.config?.shutdownEnabled) {
|
||||||
buttons.push(html`
|
buttons.push(html`
|
||||||
<button @click=${this.close}>${icon_poweroff}</button>
|
<button @click=${this.shutdown}>${icon_poweroff}</button>
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.appState.config.stravaUploadEnabled) {
|
if (this.appState?.config?.stravaUploadEnabled) {
|
||||||
buttons.push(html`
|
buttons.push(html`
|
||||||
<button @click=${this.uploadTraining}>${icon_upload}</button>
|
<button @click=${this.uploadTraining}>${icon_upload}</button>
|
||||||
`)
|
`)
|
||||||
|
|
@ -122,10 +125,6 @@ export class DashboardActions extends AppElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
close () {
|
|
||||||
window.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
reset () {
|
reset () {
|
||||||
this.sendEvent('triggerAction', { command: 'reset' })
|
this.sendEvent('triggerAction', { command: 'reset' })
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +136,7 @@ export class DashboardActions extends AppElement {
|
||||||
uploadTraining () {
|
uploadTraining () {
|
||||||
this.dialog = html`
|
this.dialog = html`
|
||||||
<app-dialog @close=${dialogClosed}>
|
<app-dialog @close=${dialogClosed}>
|
||||||
<legend>Upload to Strava?</legend>
|
<legend>${icon_upload}<br/>Upload to Strava?</legend>
|
||||||
<p>Do you want to finish your workout and upload it to Strava?</p>
|
<p>Do you want to finish your workout and upload it to Strava?</p>
|
||||||
</app-dialog>
|
</app-dialog>
|
||||||
`
|
`
|
||||||
|
|
@ -148,4 +147,19 @@ export class DashboardActions extends AppElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shutdown () {
|
||||||
|
this.dialog = html`
|
||||||
|
<app-dialog @close=${dialogClosed}>
|
||||||
|
<legend>${icon_poweroff}<br/>Shutdown Open Rowing Monitor?</legend>
|
||||||
|
<p>Do you want to shutdown the device?</p>
|
||||||
|
</app-dialog>
|
||||||
|
`
|
||||||
|
function dialogClosed (event) {
|
||||||
|
this.dialog = undefined
|
||||||
|
if (event.detail === 'confirm') {
|
||||||
|
this.sendEvent('triggerAction', { command: 'shutdown' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,10 @@ export function createApp (app) {
|
||||||
if (socket)socket.send(JSON.stringify({ command: 'uploadTraining' }))
|
if (socket)socket.send(JSON.stringify({ command: 'uploadTraining' }))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 'shutdown': {
|
||||||
|
if (socket)socket.send(JSON.stringify({ command: 'shutdown' }))
|
||||||
|
break
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
console.error('no handler defined for action', action)
|
console.error('no handler defined for action', action)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ export const APP_STATE = {
|
||||||
config: {
|
config: {
|
||||||
// currently can be FTMS, FTMSBIKE or PM5
|
// currently can be FTMS, FTMSBIKE or PM5
|
||||||
peripheralMode: '',
|
peripheralMode: '',
|
||||||
// true if upload to strava is configured
|
// true if upload to strava is enabled
|
||||||
stravaUploadEnabled: false
|
stravaUploadEnabled: false,
|
||||||
|
// true if remote device shutdown is enabled
|
||||||
|
shutdownEnabled: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
everything together while figuring out the physics and model of the application.
|
everything together while figuring out the physics and model of the application.
|
||||||
todo: refactor this as we progress
|
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 log from 'loglevel'
|
||||||
import config from './tools/ConfigManager.js'
|
import config from './tools/ConfigManager.js'
|
||||||
import { createRowingEngine } from './engine/RowingEngine.js'
|
import { createRowingEngine } from './engine/RowingEngine.js'
|
||||||
|
|
@ -18,6 +19,7 @@ import { createAntManager } from './ant/AntManager.js'
|
||||||
import { replayRowingSession } from './tools/RowingRecorder.js'
|
import { replayRowingSession } from './tools/RowingRecorder.js'
|
||||||
import { createWorkoutRecorder } from './engine/WorkoutRecorder.js'
|
import { createWorkoutRecorder } from './engine/WorkoutRecorder.js'
|
||||||
import { createWorkoutUploader } from './engine/WorkoutUploader.js'
|
import { createWorkoutUploader } from './engine/WorkoutUploader.js'
|
||||||
|
const exec = promisify(child_process.exec)
|
||||||
|
|
||||||
// set the log levels
|
// set the log levels
|
||||||
log.setLevel(config.loglevel.default)
|
log.setLevel(config.loglevel.default)
|
||||||
|
|
@ -66,7 +68,7 @@ function resetWorkout () {
|
||||||
peripheralManager.notifyStatus({ name: 'reset' })
|
peripheralManager.notifyStatus({ name: 'reset' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const gpioTimerService = fork('./app/gpio/GpioTimerService.js')
|
const gpioTimerService = child_process.fork('./app/gpio/GpioTimerService.js')
|
||||||
gpioTimerService.on('message', handleRotationImpulse)
|
gpioTimerService.on('message', handleRotationImpulse)
|
||||||
|
|
||||||
function handleRotationImpulse (dataPoint) {
|
function handleRotationImpulse (dataPoint) {
|
||||||
|
|
@ -110,7 +112,7 @@ rowingStatistics.on('rowingPaused', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (config.heartrateMonitorBLE) {
|
if (config.heartrateMonitorBLE) {
|
||||||
const bleCentralService = fork('./app/ble/CentralService.js')
|
const bleCentralService = child_process.fork('./app/ble/CentralService.js')
|
||||||
bleCentralService.on('message', (heartrateMeasurement) => {
|
bleCentralService.on('message', (heartrateMeasurement) => {
|
||||||
rowingStatistics.handleHeartrateMeasurement(heartrateMeasurement)
|
rowingStatistics.handleHeartrateMeasurement(heartrateMeasurement)
|
||||||
})
|
})
|
||||||
|
|
@ -132,7 +134,7 @@ workoutUploader.on('resetWorkout', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const webServer = createWebServer()
|
const webServer = createWebServer()
|
||||||
webServer.on('messageReceived', (message, client) => {
|
webServer.on('messageReceived', async (message, client) => {
|
||||||
switch (message.command) {
|
switch (message.command) {
|
||||||
case 'switchPeripheralMode': {
|
case 'switchPeripheralMode': {
|
||||||
peripheralManager.switchPeripheralMode()
|
peripheralManager.switchPeripheralMode()
|
||||||
|
|
@ -146,6 +148,21 @@ webServer.on('messageReceived', (message, client) => {
|
||||||
workoutUploader.upload(client)
|
workoutUploader.upload(client)
|
||||||
break
|
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': {
|
case 'stravaAuthorizationCode': {
|
||||||
workoutUploader.stravaAuthorizationCode(message.data)
|
workoutUploader.stravaAuthorizationCode(message.data)
|
||||||
break
|
break
|
||||||
|
|
@ -164,7 +181,8 @@ webServer.on('clientConnected', (client) => {
|
||||||
function getConfig () {
|
function getConfig () {
|
||||||
return {
|
return {
|
||||||
peripheralMode: peripheralManager.getPeripheralMode(),
|
peripheralMode: peripheralManager.getPeripheralMode(),
|
||||||
stravaUploadEnabled: !!config.stravaClientId && !!config.stravaClientSecret
|
stravaUploadEnabled: !!config.stravaClientId && !!config.stravaClientSecret,
|
||||||
|
shutdownEnabled: !!config.shutdownCommand
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,9 @@ export default {
|
||||||
// is the fallback for the default profile settings
|
// is the fallback for the default profile settings
|
||||||
rowerSettings: rowerProfiles.DEFAULT,
|
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)
|
// Configures the connection to Strava (to directly upload workouts to Strava)
|
||||||
// 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:
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ This is the very minimalistic Backlog for further development of this project.
|
||||||
|
|
||||||
## Later
|
## 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)
|
* figure out where to set the Service Advertising Data (FTMS.pdf p 15)
|
||||||
* add some attributes to BLE DeviceInformationService
|
* add some attributes to BLE DeviceInformationService
|
||||||
* make Web UI a proper Web Application (tooling and SPA framework)
|
* make Web UI a proper Web Application (tooling and SPA framework)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue