diff --git a/app/client/components/App.js b/app/client/components/App.js
deleted file mode 100644
index b42c0e5..0000000
--- a/app/client/components/App.js
+++ /dev/null
@@ -1,63 +0,0 @@
-'use strict'
-/*
- Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
-
- Main Component of the Open Rowing Monitor App
-*/
-
-import { AppElement, html, css } from './AppElement'
-import { APP_STATE } from '../store/appState'
-import { customElement, state } from 'lit/decorators.js'
-import { createApp } from '../lib/network'
-import './PerformanceDashboard'
-
-@customElement('web-app')
-export class App extends AppElement {
- static get styles () {
- return css`
- `
- }
-
- @state()
- globalAppState = APP_STATE
-
- constructor () {
- super()
- this.app = createApp(this.globalAppState)
- window.app = this.app
- this.app.setMetricsCallback(metrics => this.metricsUpdated(metrics))
-
- // this is how we implement changes to the global state:
- // once any child component sends this CustomEvent we update the global state according
- // to the changes that were passed to us
- this.addEventListener('appStateChanged', (event) => {
- const newState = event.detail
- this.globalAppState = newState
- })
-
- this.addEventListener('triggerAction', (event) => {
- this.app.handleAction(event.detail)
- })
- }
-
- static properties = {
- metrics: { state: true }
- };
-
- render () {
- return html`
-
- `
- }
-
- metricsUpdated (metrics) {
- this.metrics = Object.assign({}, metrics)
- }
-
- createRenderRoot () {
- return this
- }
-}
diff --git a/app/client/components/AppElement.js b/app/client/components/AppElement.js
index 3f29159..181dce5 100644
--- a/app/client/components/AppElement.js
+++ b/app/client/components/AppElement.js
@@ -19,7 +19,7 @@ export class AppElement extends LitElement {
// ..and state changes are send back to the root component of the app by dispatching
// a CustomEvent
updateState () {
- this.sendEvent('appStateChanged', { ...this.appState })
+ this.sendEvent('appStateChanged', this.appState)
}
// a helper to dispatch events to the parent components
diff --git a/app/client/components/DashboardActions.js b/app/client/components/DashboardActions.js
index dffa946..a295961 100644
--- a/app/client/components/DashboardActions.js
+++ b/app/client/components/DashboardActions.js
@@ -6,7 +6,7 @@
*/
import { AppElement, html, css } from './AppElement'
-import { customElement, property } from 'lit/decorators.js'
+import { customElement } from 'lit/decorators.js'
import { icon_undo, icon_expand, icon_compress, icon_poweroff, icon_bluetooth } from '../lib/icons'
@customElement('dashboard-actions')
@@ -16,23 +16,37 @@ export class DashboardActions extends AppElement {
`
}
- @property({ type: String })
- peripheralMode = ''
-
render () {
return html`
-
-
-
+ ${this.renderOptionalButtons()}
-
${this.appState.peripheralMode}
+
${this.appState?.metrics?.peripheralMode?.value}
`
}
- //
${this.peripheralMode}
+
+ 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)
+ if (this.appState.appMode === '') {
+ 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`
+
+ `)
+ }
+ return buttons
+ }
toggleFullscreen () {
const fullscreenElement = document.getElementsByTagName('web-app')[0]
@@ -54,9 +68,6 @@ export class DashboardActions extends AppElement {
}
switchPeripheralMode () {
- // todo: this is just a test property to see if the concept works...
- // this.appState.peripheralMode = 'PM5'
- // this.updateState()
this.sendEvent('triggerAction', { command: 'switchPeripheralMode' })
}
}
diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js
index fffb925..29c6ac8 100644
--- a/app/client/components/PerformanceDashboard.js
+++ b/app/client/components/PerformanceDashboard.js
@@ -26,19 +26,20 @@ export class PerformanceDashboard extends AppElement {
appState = APP_STATE
render () {
+ const metrics = this.appState.metrics
return html`
-
-
-
-
- ${this.metrics?.heartrate?.value
+
+
+
+
+ ${metrics?.heartrate?.value
? html``
- : html``}
-
-
-
+ .value=${metrics?.heartrate?.value}
+ .batteryLevel=${metrics?.heartrateBatteryLevel?.value}>`
+ : html``}
+
+
+
`
}
}
diff --git a/app/client/index.js b/app/client/index.js
index 60cb120..adf7d9b 100644
--- a/app/client/index.js
+++ b/app/client/index.js
@@ -2,7 +2,72 @@
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
- Initialization file for the web application
+ Main Initialization Component of the Web Component App
*/
-import './components/App'
+import { LitElement, html } from 'lit'
+import { customElement, state } from 'lit/decorators.js'
+import { APP_STATE } from './store/appState'
+import { createApp } from './lib/app.js'
+import './components/PerformanceDashboard'
+
+@customElement('web-app')
+export class App extends LitElement {
+ @state()
+ appState = APP_STATE
+
+ @state()
+ metrics
+
+ constructor () {
+ super()
+
+ this.app = createApp({
+ updateState: this.updateState,
+ getState: this.getState
+ // todo: we also want a mechanism here to get notified of state changes
+ })
+
+ // this is how we implement changes to the global state:
+ // once any child component sends this CustomEvent we update the global state according
+ // to the changes that were passed to us
+ this.addEventListener('appStateChanged', (event) => {
+ this.updateState(event.detail)
+ })
+
+ // notify the app about the triggered action
+ // todo: lets see if this solution sticks...
+ this.addEventListener('triggerAction', (event) => {
+ this.app.handleAction(event.detail)
+ })
+ }
+
+ // the global state is updated by replacing the appState with a copy of the new state
+ // todo: maybe it is more convenient to just pass the state elements that should be changed?
+ // i.e. do something like this.appState = { ..this.appState, ...newState }
+ updateState = (newState) => {
+ this.appState = { ...newState }
+ // console.table(this.appState.metrics)
+ }
+
+ // return a copy of the state to other components to minimize risk of side effects
+ getState = () => {
+ return { ...this.appState }
+ }
+
+ // once we have multiple views, then we would rather reference some kind of router here
+ // instead of embedding the performance-dashboard directly
+ render () {
+ return html`
+
+ `
+ }
+
+ // there is no need to put this initialization component into a shadow root
+ createRenderRoot () {
+ return this
+ }
+}
diff --git a/app/client/lib/app.js b/app/client/lib/app.js
new file mode 100644
index 0000000..6d95404
--- /dev/null
+++ b/app/client/lib/app.js
@@ -0,0 +1,134 @@
+'use strict'
+/*
+ Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
+
+ Initialization file of the Open Rowing Monitor App
+*/
+
+import NoSleep from 'nosleep.js'
+
+export function createApp (app) {
+ const fields = ['strokesTotal', 'distanceTotal', 'caloriesTotal', 'power', 'heartrate',
+ 'splitFormatted', 'strokesPerMinute', 'durationTotalFormatted', 'peripheralMode']
+ // todo: formatting should happen in the related components (i.e.) PerformanceDashboard
+ const fieldFormatter = {
+ peripheralMode: (value) => {
+ if (value === 'PM5') {
+ return 'C2 PM5'
+ } else if (value === 'FTMSBIKE') {
+ return 'FTMS Bike'
+ } else {
+ return 'FTMS Rower'
+ }
+ },
+ distanceTotal: (value) => value >= 10000
+ ? { value: (value / 1000).toFixed(1), unit: 'km' }
+ : { value: Math.round(value), unit: 'm' },
+ caloriesTotal: (value) => Math.round(value),
+ power: (value) => Math.round(value),
+ strokesPerMinute: (value) => Math.round(value)
+ }
+ const mode = window.location.hash
+ const appMode = mode === '#:standalone:' ? 'STANDALONE' : mode === '#:kiosk:' ? 'KIOSK' : ''
+ app.updateState({ ...app.getState(), appMode })
+
+ const metrics = {}
+
+ let socket
+
+ initWebsocket()
+ resetFields()
+ requestWakeLock()
+
+ function initWebsocket () {
+ // 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.addEventListener('open', (event) => {
+ console.log('websocket opened')
+ })
+
+ socket.addEventListener('error', (error) => {
+ console.log('websocket error', error)
+ socket.close()
+ })
+
+ socket.addEventListener('close', (event) => {
+ console.log('websocket closed, attempting reconnect')
+ setTimeout(() => {
+ initWebsocket()
+ }, 1000)
+ })
+
+ socket.addEventListener('message', (event) => {
+ try {
+ const data = JSON.parse(event.data)
+
+ let activeFields = fields
+ // if we are in reset state only update heart rate and peripheral mode
+ if (data.strokesTotal === 0) {
+ activeFields = ['heartrate', 'peripheralMode']
+ }
+
+ // todo: formatting should happen in the related components (i.e.) PerformanceDashboard
+ for (const [key, value] of Object.entries(data)) {
+ if (activeFields.includes(key)) {
+ const valueFormatted = fieldFormatter[key] ? fieldFormatter[key](value) : value
+ if (valueFormatted.value !== undefined && valueFormatted.unit !== undefined) {
+ metrics[key] = {
+ value: valueFormatted.value,
+ unit: valueFormatted.unit
+ }
+ } else {
+ metrics[key] = {
+ value: valueFormatted
+ }
+ }
+ }
+ }
+
+ app.updateState({ ...app.getState(), metrics })
+ } catch (err) {
+ console.log(err)
+ }
+ })
+ }
+
+ async function requestWakeLock () {
+ // Chrome enables the new Wake Lock API only if the connection is secured via SSL
+ // This is quite annoying for IoT use cases like this one, where the device sits on the
+ // local network and is directly addressed by its IP.
+ // In this case the only way of using SSL is by creating a self signed certificate, and
+ // that would pop up different warnings in the browser (and also prevents fullscreen via
+ // a home screen icon so it can show these warnings). Okay, enough ranting :-)
+ // In this case we use the good old hacky way of keeping the screen on via a hidden video.
+ const noSleep = new NoSleep()
+ document.addEventListener('click', function enableNoSleep () {
+ document.removeEventListener('click', enableNoSleep, false)
+ noSleep.enable()
+ }, false)
+ }
+
+ function resetFields () {
+ for (const key of fields.filter((elem) => elem !== 'peripheralMode' && elem !== 'heartrate')) {
+ metrics[key] = { value: '--' }
+ }
+ app.updateState({ ...app.getState(), metrics })
+ }
+
+ function handleAction (action) {
+ if (action.command === 'switchPeripheralMode') {
+ if (socket)socket.send(JSON.stringify({ command: 'switchPeripheralMode' }))
+ } else if (action.command === 'reset') {
+ resetFields()
+ if (socket)socket.send(JSON.stringify({ command: 'reset' }))
+ } else {
+ console.error('no handler defined for action', action)
+ }
+ }
+
+ return {
+ handleAction
+ }
+}
diff --git a/app/client/lib/network.js b/app/client/lib/network.js
index 0d1b1fc..bcca50f 100644
--- a/app/client/lib/network.js
+++ b/app/client/lib/network.js
@@ -2,150 +2,4 @@
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
- This is currently a very simple Web UI that displays the training metrics.
*/
-import NoSleep from 'nosleep.js'
-
-export function createApp () {
- const fields = ['strokesTotal', 'distanceTotal', 'caloriesTotal', 'power', 'heartrate',
- 'splitFormatted', 'strokesPerMinute', 'durationTotalFormatted', 'peripheralMode']
- const fieldFormatter = {
- peripheralMode: (value) => {
- if (value === 'PM5') {
- return 'C2 PM5'
- } else if (value === 'FTMSBIKE') {
- return 'FTMS Bike'
- } else {
- return 'FTMS Rower'
- }
- },
- distanceTotal: (value) => value >= 10000
- ? { value: (value / 1000).toFixed(1), unit: 'km' }
- : { value: Math.round(value), unit: 'm' },
- caloriesTotal: (value) => Math.round(value),
- power: (value) => Math.round(value),
- strokesPerMinute: (value) => Math.round(value)
- }
- // const standalone = (window.location.hash === '#:standalone:')
- let metricsCallback
- const metrics = {
- }
- /*
- if (standalone) {
- document.getElementById('close-button').style.display = 'inline-block'
- document.getElementById('fullscreen-button').style.display = 'none'
- } else {
- document.getElementById('fullscreen-button').style.display = 'inline-block'
- document.getElementById('close-button').style.display = 'none'
- } */
-
- let socket
-
- initWebsocket()
- resetFields()
- requestWakeLock()
-
- function initWebsocket () {
- // 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.addEventListener('open', (event) => {
- console.log('websocket opened')
- })
-
- socket.addEventListener('error', (error) => {
- console.log('websocket error', error)
- socket.close()
- })
-
- socket.addEventListener('close', (event) => {
- console.log('websocket closed, attempting reconnect')
- setTimeout(() => {
- initWebsocket()
- }, 1000)
- })
-
- socket.addEventListener('message', (event) => {
- try {
- const data = JSON.parse(event.data)
-
- let activeFields = fields
- // if we are in reset state only update heart rate and peripheral mode
- if (data.strokesTotal === 0) {
- activeFields = ['heartrate', 'peripheralMode']
- }
-
- for (const [key, value] of Object.entries(data)) {
- if (activeFields.includes(key)) {
- const valueFormatted = fieldFormatter[key] ? fieldFormatter[key](value) : value
- if (valueFormatted.value !== undefined && valueFormatted.unit !== undefined) {
- metrics[key] = {
- value: valueFormatted.value,
- unit: valueFormatted.unit
- }
- } else {
- metrics[key] = {
- value: valueFormatted
- }
- }
- }
- }
-
- if (metricsCallback) {
- metricsCallback(metrics)
- }
- } catch (err) {
- console.log(err)
- }
- })
- }
-
- async function requestWakeLock () {
- // Chrome enables the new Wake Lock API only if the connection is secured via SSL
- // This is quite annoying for IoT use cases like this one, where the device sits on the
- // local network and is directly addressed by its IP.
- // In this case the only way of using SSL is by creating a self signed certificate, and
- // that would pop up different warnings in the browser (and also prevents fullscreen via
- // a home screen icon so it can show these warnings). Okay, enough ranting :-)
- // In this case we use the good old hacky way of keeping the screen on via a hidden video.
- const noSleep = new NoSleep()
- document.addEventListener('click', function enableNoSleep () {
- document.removeEventListener('click', enableNoSleep, false)
- noSleep.enable()
- }, false)
- }
-
- function resetFields () {
- for (const key of fields.filter((elem) => elem !== 'peripheralMode' && elem !== 'heartrate')) {
- metrics[key] = { value: '--' }
- if (metricsCallback) {
- metricsCallback(metrics)
- }
- }
- }
-
- function handleAction (action) {
- if (action.command === 'switchPeripheralMode') {
- if (socket)socket.send(JSON.stringify({ command: 'switchPeripheralMode' }))
- } else if (action.command === 'reset') {
- resetFields()
- if (socket)socket.send(JSON.stringify({ command: 'reset' }))
- } else {
- console.error('no handler defined for action', action)
- }
- }
-
- function setMetricsCallback (callback) {
- metricsCallback = callback
- if (metricsCallback) {
- metricsCallback(metrics)
- }
- }
-
- return {
- handleAction,
- metrics,
- setMetricsCallback
- }
-}
diff --git a/app/client/store/appState.js b/app/client/store/appState.js
index 525adc2..7e497a0 100644
--- a/app/client/store/appState.js
+++ b/app/client/store/appState.js
@@ -6,6 +6,10 @@
*/
export const APP_STATE = {
- // todo: this is just a test property to see if the concept works...
- peripheralMode: 'FTMSROWER'
+ // currently can be STANDALONE (Mobile Home Screen App), KIOSK (Raspberry Pi deployment) or '' (default)
+ appMode: '',
+ // todo: this is currently embedded into the metrics object, but should probably be extracted
+ peripheralMode: 'FTMSROWER',
+ // contains all the rowing metrics that are delivered from the backend
+ metrics: {}
}