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;
|
||||||
|
|
@ -55,97 +58,108 @@ export class DashboardActions extends AppElement {
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
@state({ type: Object })
|
@state({ type: Object })
|
||||||
dialog
|
dialog
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return html`
|
return html`
|
||||||
<button @click=${this.reset}>${icon_undo}</button>
|
<button @click=${this.reset}>${icon_undo}</button>
|
||||||
${this.renderOptionalButtons()}
|
${this.renderOptionalButtons()}
|
||||||
<button @click=${this.switchPeripheralMode}>${icon_bluetooth}</button>
|
<button @click=${this.switchPeripheralMode}>${icon_bluetooth}</button>
|
||||||
<div class="peripheral-mode">${this.peripheralMode()}</div>
|
<div class="peripheral-mode">${this.peripheralMode()}</div>
|
||||||
${this.dialog ? this.dialog : ''}
|
${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`
|
||||||
|
<button @click=${this.toggleFullscreen}>
|
||||||
|
<div id="fullscreen-icon">${icon_expand}</div>
|
||||||
|
<div id="windowed-icon">${icon_compress}</div>
|
||||||
|
</button>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
// 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`
|
||||||
|
<button @click=${this.shutdown}>${icon_poweroff}</button>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.appState?.config?.stravaUploadEnabled) {
|
||||||
|
buttons.push(html`
|
||||||
|
<button @click=${this.uploadTraining}>${icon_upload}</button>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
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`
|
||||||
|
<app-dialog @close=${dialogClosed}>
|
||||||
|
<legend>${icon_upload}<br/>Upload to Strava?</legend>
|
||||||
|
<p>Do you want to finish your workout and upload it to Strava?</p>
|
||||||
|
</app-dialog>
|
||||||
`
|
`
|
||||||
}
|
function dialogClosed (event) {
|
||||||
|
this.dialog = undefined
|
||||||
renderOptionalButtons () {
|
if (event.detail === 'confirm') {
|
||||||
const buttons = []
|
this.sendEvent('triggerAction', { command: 'uploadTraining' })
|
||||||
// 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`
|
|
||||||
<button @click=${this.toggleFullscreen}>
|
|
||||||
<div id="fullscreen-icon">${icon_expand}</div>
|
|
||||||
<div id="windowed-icon">${icon_compress}</div>
|
|
||||||
</button>
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
// 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`
|
|
||||||
<button @click=${this.close}>${icon_poweroff}</button>
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.appState.config.stravaUploadEnabled) {
|
|
||||||
buttons.push(html`
|
|
||||||
<button @click=${this.uploadTraining}>${icon_upload}</button>
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
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 () {
|
shutdown () {
|
||||||
const fullscreenElement = document.getElementsByTagName('web-app')[0]
|
this.dialog = html`
|
||||||
if (!document.fullscreenElement) {
|
<app-dialog @close=${dialogClosed}>
|
||||||
fullscreenElement.requestFullscreen({ navigationUI: 'hide' })
|
<legend>${icon_poweroff}<br/>Shutdown Open Rowing Monitor?</legend>
|
||||||
} else {
|
<p>Do you want to shutdown the device?</p>
|
||||||
if (document.exitFullscreen) {
|
</app-dialog>
|
||||||
document.exitFullscreen()
|
`
|
||||||
}
|
function dialogClosed (event) {
|
||||||
}
|
this.dialog = undefined
|
||||||
}
|
if (event.detail === 'confirm') {
|
||||||
|
this.sendEvent('triggerAction', { command: 'shutdown' })
|
||||||
close () {
|
|
||||||
window.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
reset () {
|
|
||||||
this.sendEvent('triggerAction', { command: 'reset' })
|
|
||||||
}
|
|
||||||
|
|
||||||
switchPeripheralMode () {
|
|
||||||
this.sendEvent('triggerAction', { command: 'switchPeripheralMode' })
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadTraining () {
|
|
||||||
this.dialog = html`
|
|
||||||
<app-dialog @close=${dialogClosed}>
|
|
||||||
<legend>Upload to Strava?</legend>
|
|
||||||
<p>Do you want to finish your workout and upload it to Strava?</p>
|
|
||||||
</app-dialog>
|
|
||||||
`
|
|
||||||
function dialogClosed (event) {
|
|
||||||
this.dialog = undefined
|
|
||||||
if (event.detail === 'confirm') {
|
|
||||||
this.sendEvent('triggerAction', { command: 'uploadTraining' })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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