diff --git a/features.ini b/features.ini index f68b0aec..44a3ff13 100644 --- a/features.ini +++ b/features.ini @@ -2,7 +2,7 @@ build_flags = -D FT_PROJECT=1 -D FT_SECURITY=1 - -D FT_MQTT=1 + -D FT_MQTT=0 -D FT_NTP=1 - -D FT_OTA=1 + -D FT_OTA=0 -D FT_UPLOAD_FIRMWARE=1 diff --git a/interface/.env b/interface/.env index a312b2a0..f7c32c25 100644 --- a/interface/.env +++ b/interface/.env @@ -1,5 +1,5 @@ # This is the name of your project. It appears on the sign-in page and in the menu bar. -REACT_APP_PROJECT_NAME=ESP8266 React +REACT_APP_PROJECT_NAME=Christmas Lights # This is the url path your project will be exposed under. REACT_APP_PROJECT_PATH=project diff --git a/interface/package-lock.json b/interface/package-lock.json index 014040ec..78363b20 100644 --- a/interface/package-lock.json +++ b/interface/package-lock.json @@ -1198,6 +1198,11 @@ "@hapi/hoek": "^8.3.0" } }, + "@icons/material": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==" + }, "@jest/console": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz", @@ -1739,6 +1744,15 @@ "@types/react": "*" } }, + "@types/react-color": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.4.tgz", + "integrity": "sha512-EswbYJDF1kkrx93/YU+BbBtb46CCtDMvTiGmcOa/c5PETnwTiSWoseJ1oSWeRl/4rUXkhME9bVURvvPg0W5YQw==", + "requires": { + "@types/react": "*", + "@types/reactcss": "*" + } + }, "@types/react-dom": { "version": "16.9.9", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.9.tgz", @@ -1783,6 +1797,14 @@ "@types/react": "*" } }, + "@types/reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-d2gQQ0IL6hXLnoRfVYZukQNWHuVsE75DzFTLPUuyyEhJS8G2VvlE+qfQQ91SJjaMqlURRCNIsX7Jcsw6cEuJlA==", + "requires": { + "@types/react": "*" + } + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -8363,6 +8385,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, + "lodash-es": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz", + "integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==" + }, "lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", @@ -8470,6 +8497,11 @@ "object-visit": "^1.0.0" } }, + "material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -10940,6 +10972,20 @@ } } }, + "react-color": { + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", + "integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==", + "requires": { + "@icons/material": "^0.2.4", + "lodash": "^4.17.15", + "lodash-es": "^4.17.15", + "material-colors": "^1.2.1", + "prop-types": "^15.5.10", + "reactcss": "^1.2.0", + "tinycolor2": "^1.4.1" + } + }, "react-dev-utils": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz", @@ -11269,6 +11315,14 @@ "prop-types": "^15.6.2" } }, + "reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "requires": { + "lodash": "^4.0.1" + } + }, "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -13160,6 +13214,11 @@ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, + "tinycolor2": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", + "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==" + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", diff --git a/interface/package.json b/interface/package.json index b4080e10..04833145 100644 --- a/interface/package.json +++ b/interface/package.json @@ -9,6 +9,7 @@ "@types/lodash": "^4.14.165", "@types/node": "^12.12.32", "@types/react": "^16.9.56", + "@types/react-color": "^3.0.4", "@types/react-dom": "^16.9.9", "@types/react-material-ui-form-validator": "^2.1.0", "@types/react-router": "^5.1.8", @@ -20,6 +21,7 @@ "moment": "^2.29.1", "notistack": "^1.0.1", "react": "^16.14.0", + "react-color": "^2.19.3", "react-dom": "^16.14.0", "react-dropzone": "^11.2.4", "react-form-validator-core": "^1.0.0", diff --git a/interface/public/app/icon.png b/interface/public/app/icon.png index 13dd442c..89d7df63 100644 Binary files a/interface/public/app/icon.png and b/interface/public/app/icon.png differ diff --git a/interface/public/app/manifest.json b/interface/public/app/manifest.json index f7756611..889f4f32 100644 --- a/interface/public/app/manifest.json +++ b/interface/public/app/manifest.json @@ -1,5 +1,5 @@ { - "name":"ESP8266 React", + "name":"Christmas Lights", "icons":[ { "src":"/app/icon.png", diff --git a/interface/public/favicon.ico b/interface/public/favicon.ico index 399ccae7..4f31a657 100644 Binary files a/interface/public/favicon.ico and b/interface/public/favicon.ico differ diff --git a/interface/src/CustomMuiTheme.tsx b/interface/src/CustomMuiTheme.tsx index 570ed7ff..add43bc2 100644 --- a/interface/src/CustomMuiTheme.tsx +++ b/interface/src/CustomMuiTheme.tsx @@ -2,12 +2,16 @@ import React, { Component } from 'react'; import { CssBaseline } from '@material-ui/core'; import { MuiThemeProvider, createMuiTheme, StylesProvider } from '@material-ui/core/styles'; -import { blueGrey, indigo, orange, red, green } from '@material-ui/core/colors'; +import { blueGrey, orange, red, green } from '@material-ui/core/colors'; const theme = createMuiTheme({ palette: { - primary: indigo, - secondary: blueGrey, + primary: { + main: green[900] + }, + secondary: { + main: "#5E201B" + }, info: { main: blueGrey[900] }, diff --git a/interface/src/ap/AccessPoint.tsx b/interface/src/ap/AccessPoint.tsx index eba011e2..93b89cdf 100644 --- a/interface/src/ap/AccessPoint.tsx +++ b/interface/src/ap/AccessPoint.tsx @@ -21,7 +21,7 @@ class AccessPoint extends Component { const { authenticatedContext } = this.props; return ( - + diff --git a/interface/src/components/MenuAppBar.tsx b/interface/src/components/MenuAppBar.tsx index dfe5f227..bf911532 100644 --- a/interface/src/components/MenuAppBar.tsx +++ b/interface/src/components/MenuAppBar.tsx @@ -25,9 +25,6 @@ import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext'; const drawerWidth = 290; const styles = (theme: Theme) => createStyles({ - root: { - display: 'flex', - }, drawer: { [theme.breakpoints.up('md')]: { width: drawerWidth, @@ -64,7 +61,10 @@ const styles = (theme: Theme) => createStyles({ width: drawerWidth, }, content: { - flexGrow: 1 + flexGrow: 1, + [theme.breakpoints.up('md')]: { + marginLeft: drawerWidth, + }, }, authMenu: { zIndex: theme.zIndex.tooltip, @@ -219,7 +219,7 @@ class MenuAppBar extends React.Component { ); return ( -
+
extends WithSnackbarProps { - handleValueChange: (name: keyof D) => (event: React.ChangeEvent) => void; + handleValueChange: (name: keyof D, callback?: () => void) => (event: React.ChangeEvent) => void; setData: (data: D, callback?: () => void) => void; saveData: () => void; @@ -93,9 +93,9 @@ export function restController>(endpointUrl: }); } - handleValueChange = (name: keyof D) => (event: React.ChangeEvent) => { + handleValueChange = (name: keyof D, callback?: () => void) => (event: React.ChangeEvent) => { const data = { ...this.state.data!, [name]: extractEventValue(event) }; - this.setState({ data }); + this.setState({ data }, callback); } render() { diff --git a/interface/src/components/WebSocketController.tsx b/interface/src/components/WebSocketController.tsx index 5fe9fa33..f1759a85 100644 --- a/interface/src/components/WebSocketController.tsx +++ b/interface/src/components/WebSocketController.tsx @@ -7,11 +7,11 @@ import { addAccessTokenParameter } from '../authentication'; import { extractEventValue } from '.'; export interface WebSocketControllerProps extends WithSnackbarProps { - handleValueChange: (name: keyof D) => (event: React.ChangeEvent) => void; + handleValueChange: (name: keyof D, callback?: () => void) => (event: React.ChangeEvent) => void; setData: (data: D, callback?: () => void) => void; saveData: () => void; - saveDataAndClear(): () => void; + saveDataAndClear(newData?: D): () => void; connected: boolean; data?: D; @@ -103,29 +103,30 @@ export function webSocketController>(ws } }, wsThrottle); - saveDataAndClear = throttle(() => { + saveDataAndClear = throttle((newData?: D) => { const { ws, connected, data } = this.state; if (connected) { this.setState({ data: undefined - }, () => ws.json(data)); + }, () => ws.json(newData || data)); } }, wsThrottle); - handleValueChange = (name: keyof D) => (event: React.ChangeEvent) => { + handleValueChange = (name: keyof D, callback?: () => void) => (event: React.ChangeEvent) => { const data = { ...this.state.data!, [name]: extractEventValue(event) }; - this.setState({ data }); + this.setState({ data }, callback); } render() { + const { connected, data } = this.state; return ; } diff --git a/interface/src/components/WebSocketFormLoader.tsx b/interface/src/components/WebSocketFormLoader.tsx index ee5f335a..5946a209 100644 --- a/interface/src/components/WebSocketFormLoader.tsx +++ b/interface/src/components/WebSocketFormLoader.tsx @@ -17,7 +17,7 @@ const useStyles = makeStyles((theme: Theme) => }) ); -export type WebSocketFormProps = Omit, "connected"> & { data: D }; +export type WebSocketFormProps = Omit, "connected" | "data"> & { data: D }; interface WebSocketFormLoaderProps extends WebSocketControllerProps { render: (props: WebSocketFormProps) => JSX.Element; diff --git a/interface/src/mqtt/Mqtt.tsx b/interface/src/mqtt/Mqtt.tsx index 8daca772..bfa1c6d0 100644 --- a/interface/src/mqtt/Mqtt.tsx +++ b/interface/src/mqtt/Mqtt.tsx @@ -20,7 +20,7 @@ class Mqtt extends Component { const { authenticatedContext } = this.props; return ( - + diff --git a/interface/src/ntp/NetworkTime.tsx b/interface/src/ntp/NetworkTime.tsx index ebefb6e4..5cdbd29d 100644 --- a/interface/src/ntp/NetworkTime.tsx +++ b/interface/src/ntp/NetworkTime.tsx @@ -21,7 +21,7 @@ class NetworkTime extends Component { const { authenticatedContext } = this.props; return ( - + diff --git a/interface/src/project/AudioLightSettingsController.tsx b/interface/src/project/AudioLightSettingsController.tsx new file mode 100644 index 00000000..87a86f14 --- /dev/null +++ b/interface/src/project/AudioLightSettingsController.tsx @@ -0,0 +1,30 @@ +import { Container } from '@material-ui/core'; +import React, { Component } from 'react'; + +import { SectionContent, webSocketController, WebSocketControllerProps, WebSocketFormLoader } from '../components'; +import AudioLightSettingsForm from './AudioLightSettingsForm'; +import PaletteSettingsLoader from './PaletteSettingsLoader'; +import { AudioLightMode, AUDIO_LIGHT_SETTINGS_ENDPOINT } from './types'; + +type AudioLightSettingsControllerProps = WebSocketControllerProps>; + +class AudioLightSettingsController extends Component { + + render() { + return ( + + + + } + /> + + + + ); + } + +} + +export default webSocketController(AUDIO_LIGHT_SETTINGS_ENDPOINT, 100, AudioLightSettingsController); \ No newline at end of file diff --git a/interface/src/project/AudioLightSettingsForm.tsx b/interface/src/project/AudioLightSettingsForm.tsx new file mode 100644 index 00000000..74e7346d --- /dev/null +++ b/interface/src/project/AudioLightSettingsForm.tsx @@ -0,0 +1,85 @@ +import React, { Fragment } from 'react'; +import { TextField, MenuItem } from '@material-ui/core'; +import SaveIcon from '@material-ui/icons/Save'; +import LoadIcon from '@material-ui/icons/SaveAlt'; + +import { FormActions, FormButton, WebSocketFormProps } from '../components'; +import { AudioLightModeType, AudioLightMode, AUDIO_LIGHT_LOAD_SETTINGS_ENDPOINT, AUDIO_LIGHT_SAVE_SETTINGS_ENDPOINT, AUDIO_LIGHT_MODE_METADATA } from './types'; +import { redirectingAuthorizedFetch } from '../authentication'; + +type AudioLightSettingsFormProps = WebSocketFormProps>; + +class AudioLightSettingsForm extends React.Component { + + saveMode = async () => { + const { enqueueSnackbar } = this.props; + try { + const response = await redirectingAuthorizedFetch(AUDIO_LIGHT_SAVE_SETTINGS_ENDPOINT, { method: 'POST' }); + if (response.status === 200) { + enqueueSnackbar("Mode settings saved", { variant: 'success' }); + } else { + throw new Error(`Unexpected response code ${response.status}`); + } + } catch (error) { + const errorMessage = error.message || "Unknown error"; + enqueueSnackbar("Problem saving: " + errorMessage, { variant: 'error' }); + } + } + + loadMode = async () => { + const { enqueueSnackbar } = this.props; + try { + const response = await redirectingAuthorizedFetch(AUDIO_LIGHT_LOAD_SETTINGS_ENDPOINT, { method: 'POST' }); + if (response.status === 200) { + enqueueSnackbar("Mode settings loaded", { variant: 'success' }); + } else { + throw new Error(`Unexpected response code ${response.status}`); + } + } catch (error) { + const errorMessage = error.message || "Unknown error"; + enqueueSnackbar("Problem loading: " + errorMessage, { variant: 'error' }); + } + } + + selectModeComponent(): React.ElementType | undefined { + const { mode_id } = this.props.data; + return mode_id && AUDIO_LIGHT_MODE_METADATA[mode_id].renderer; + } + + render() { + const { data, saveDataAndClear } = this.props; + const ModeComponent = this.selectModeComponent(); + return ( + + saveDataAndClear({ mode_id: event.target.value as AudioLightModeType })} + fullWidth + margin="normal" + variant="outlined" + select> + { + Object.entries(AudioLightModeType).map(([, mode_id]) => ( + + {AUDIO_LIGHT_MODE_METADATA[mode_id].label} + + )) + } + + { ModeComponent && } + + } variant="contained" color="primary" onClick={this.saveMode}> + Save Settings + + } variant="contained" color="secondary" onClick={this.loadMode}> + Load Settings + + + + ); + } +} + +export default AudioLightSettingsForm; diff --git a/interface/src/project/DemoInformation.tsx b/interface/src/project/DemoInformation.tsx deleted file mode 100644 index 2b9967bd..00000000 --- a/interface/src/project/DemoInformation.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React, { Component } from 'react'; -import { Typography, Box, List, ListItem, ListItemText } from '@material-ui/core'; -import { SectionContent } from '../components'; - -class DemoInformation extends Component { - - render() { - return ( - - - This simple demo project allows you to control the built-in LED. - It demonstrates how the esp8266-react framework may be extended for your own IoT project. - - - It is recommended that you keep your project interface code under the project directory. - This serves to isolate your project code from the from the rest of the user interface which should - simplify merges should you wish to update your project with future framework changes. - - - The demo project interface code is stored in the 'interface/src/project' directory: - - - - - - - - - - - - - - - - - - - - - - - - - - - See the project README for a full description of the demo project. - - - - ) - } - -} - -export default DemoInformation; diff --git a/interface/src/project/DemoProject.tsx b/interface/src/project/DemoProject.tsx deleted file mode 100644 index 74f25e53..00000000 --- a/interface/src/project/DemoProject.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { Component } from 'react'; -import { Redirect, Switch, RouteComponentProps } from 'react-router-dom' - -import { Tabs, Tab } from '@material-ui/core'; - -import { PROJECT_PATH } from '../api'; -import { MenuAppBar } from '../components'; -import { AuthenticatedRoute } from '../authentication'; - -import DemoInformation from './DemoInformation'; -import LightStateRestController from './LightStateRestController'; -import LightStateWebSocketController from './LightStateWebSocketController'; -import LightMqttSettingsController from './LightMqttSettingsController'; - -class DemoProject extends Component { - - handleTabChange = (event: React.ChangeEvent<{}>, path: string) => { - this.props.history.push(path); - }; - - render() { - return ( - - - - - - - - - - - - - - - - ) - } - -} - -export default DemoProject; diff --git a/interface/src/project/LedSettingsController.tsx b/interface/src/project/LedSettingsController.tsx new file mode 100644 index 00000000..fd98ed71 --- /dev/null +++ b/interface/src/project/LedSettingsController.tsx @@ -0,0 +1,32 @@ +import { Container } from '@material-ui/core'; +import React, { Component } from 'react'; + +import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components'; +import LedSettingsForm from './LedSettingsForm'; + +import { LedSettings, LED_SETTINGS_ENDPOINT } from './types'; + +type LedSettingsControllerProps = RestControllerProps; + +class LedSettingsController extends Component { + + componentDidMount() { + this.props.loadData(); + } + + render() { + return ( + + + } + /> + + + ) + } + +} + +export default restController(LED_SETTINGS_ENDPOINT, LedSettingsController); diff --git a/interface/src/project/LedSettingsForm.tsx b/interface/src/project/LedSettingsForm.tsx new file mode 100644 index 00000000..882b17be --- /dev/null +++ b/interface/src/project/LedSettingsForm.tsx @@ -0,0 +1,65 @@ +import React from 'react'; + +import { Box, FormLabel, Slider, withWidth, WithWidthProps } from '@material-ui/core'; +import SaveIcon from '@material-ui/icons/Save'; +import { ValidatorForm } from 'react-material-ui-form-validator'; + +import { FormActions, FormButton, RestFormProps } from '../components'; +import { LedSettings } from './types'; + +type LedSettingsFormProps = RestFormProps & WithWidthProps; + +const milliwatsToWatts = (milliwatts: number) => milliwatts / 1000; + +class LedSettingsForm extends React.Component { + + handleSliderChange = (name: keyof LedSettings) => (event: React.ChangeEvent<{}>, value: number | number[]) => { + const { setData } = this.props; + setData({ ...this.props.data!, [name]: value }); + } + + render() { + const { data, saveData } = this.props; + return ( + + + LED Max Power in Watts (0 = unlimited) + + Audio Dead Zone + + Audio Smoothing Factor + + + + } variant="contained" color="primary" type="submit"> + Save + + + + ); + } + +} + +export default withWidth()(LedSettingsForm); + diff --git a/interface/src/project/LightMqttSettingsController.tsx b/interface/src/project/LightMqttSettingsController.tsx deleted file mode 100644 index 7e4db5cf..00000000 --- a/interface/src/project/LightMqttSettingsController.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { Component } from 'react'; -import { ValidatorForm, TextValidator } from 'react-material-ui-form-validator'; - -import { Typography, Box } from '@material-ui/core'; -import SaveIcon from '@material-ui/icons/Save'; - -import { ENDPOINT_ROOT } from '../api'; -import { restController, RestControllerProps, RestFormLoader, RestFormProps, FormActions, FormButton, SectionContent } from '../components'; - -import { LightMqttSettings } from './types'; - -export const LIGHT_BROKER_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "brokerSettings"; - -type LightMqttSettingsControllerProps = RestControllerProps; - -class LightMqttSettingsController extends Component { - - componentDidMount() { - this.props.loadData(); - } - - render() { - return ( - - ( - - )} - /> - - ) - } - -} - -export default restController(LIGHT_BROKER_SETTINGS_ENDPOINT, LightMqttSettingsController); - -type LightMqttSettingsControllerFormProps = RestFormProps; - -function LightMqttSettingsControllerForm(props: LightMqttSettingsControllerFormProps) { - const { data, saveData, handleValueChange } = props; - return ( - - - - The LED is controllable via MQTT with the demo project designed to work with Home Assistant's auto discovery feature. - - - - - - - } variant="contained" color="primary" type="submit"> - Save - - - - ); -} diff --git a/interface/src/project/LightStateRestController.tsx b/interface/src/project/LightStateRestController.tsx deleted file mode 100644 index 764ce350..00000000 --- a/interface/src/project/LightStateRestController.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React, { Component } from 'react'; -import { ValidatorForm } from 'react-material-ui-form-validator'; - -import { Typography, Box, Checkbox } from '@material-ui/core'; -import SaveIcon from '@material-ui/icons/Save'; - -import { ENDPOINT_ROOT } from '../api'; -import { restController, RestControllerProps, RestFormLoader, RestFormProps, FormActions, FormButton, SectionContent, BlockFormControlLabel } from '../components'; - -import { LightState } from './types'; - -export const LIGHT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "lightState"; - -type LightStateRestControllerProps = RestControllerProps; - -class LightStateRestController extends Component { - - componentDidMount() { - this.props.loadData(); - } - - render() { - return ( - - ( - - )} - /> - - ) - } - -} - -export default restController(LIGHT_SETTINGS_ENDPOINT, LightStateRestController); - -type LightStateRestControllerFormProps = RestFormProps; - -function LightStateRestControllerForm(props: LightStateRestControllerFormProps) { - const { data, saveData, handleValueChange } = props; - return ( - - - - The form below controls the LED via the RESTful service exposed by the ESP device. - - - - } - label="LED State?" - /> - - } variant="contained" color="primary" type="submit"> - Save - - - - ); -} diff --git a/interface/src/project/LightStateWebSocketController.tsx b/interface/src/project/LightStateWebSocketController.tsx deleted file mode 100644 index a0b99a2e..00000000 --- a/interface/src/project/LightStateWebSocketController.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { Component } from 'react'; -import { ValidatorForm } from 'react-material-ui-form-validator'; - -import { Typography, Box, Switch } from '@material-ui/core'; -import { WEB_SOCKET_ROOT } from '../api'; -import { WebSocketControllerProps, WebSocketFormLoader, WebSocketFormProps, webSocketController } from '../components'; -import { SectionContent, BlockFormControlLabel } from '../components'; - -import { LightState } from './types'; - -export const LIGHT_SETTINGS_WEBSOCKET_URL = WEB_SOCKET_ROOT + "lightState"; - -type LightStateWebSocketControllerProps = WebSocketControllerProps; - -class LightStateWebSocketController extends Component { - - render() { - return ( - - ( - - )} - /> - - ) - } - -} - -export default webSocketController(LIGHT_SETTINGS_WEBSOCKET_URL, 100, LightStateWebSocketController); - -type LightStateWebSocketControllerFormProps = WebSocketFormProps; - -function LightStateWebSocketControllerForm(props: LightStateWebSocketControllerFormProps) { - const { data, saveData, setData } = props; - - const changeLedOn = (event: React.ChangeEvent) => { - setData({ led_on: event.target.checked }, saveData); - } - - return ( - - - - The switch below controls the LED via the WebSocket. It will automatically update whenever the LED state changes. - - - - } - label="LED State?" - /> - - ); -} diff --git a/interface/src/project/LightsProject.tsx b/interface/src/project/LightsProject.tsx new file mode 100644 index 00000000..3dee5e28 --- /dev/null +++ b/interface/src/project/LightsProject.tsx @@ -0,0 +1,43 @@ +import React, { Component } from 'react'; +import { Redirect, Switch, RouteComponentProps } from 'react-router-dom' + +import { Tabs, Tab } from '@material-ui/core'; + +import { PROJECT_PATH } from '../api'; +import { MenuAppBar } from '../components'; +import { AuthenticatedRoute } from '../authentication'; + +import SpectrumAnalyzer from './SpectrumAnalyzer'; +import AudioLightSettingsController from './AudioLightSettingsController'; +import PaletteSettingsController from './PaletteSettingsController'; +import LedSettingsController from './LedSettingsController'; + +class LightsProject extends Component { + + handleTabChange = (event: React.ChangeEvent<{}>, path: string) => { + this.props.history.push(path); + }; + + render() { + return ( + + + + + + + + + + + + + + + + ) + } + +} + +export default LightsProject; diff --git a/interface/src/project/PaletteForm.tsx b/interface/src/project/PaletteForm.tsx new file mode 100644 index 00000000..8b5f5f72 --- /dev/null +++ b/interface/src/project/PaletteForm.tsx @@ -0,0 +1,102 @@ +import React, { RefObject } from 'react'; +import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator'; + +import { Dialog, DialogTitle, DialogContent, DialogActions, Box, Slider } from '@material-ui/core'; + +import { FormButton } from '../components'; +import { generateGradient, Palette } from './types'; +import { ChromePicker, ColorResult } from 'react-color'; + +interface PaletteFormProps { + creating: boolean; + palette: Palette; + uniqueName: (name: any) => boolean; + updatePalette: (palette: Palette) => void; + onDoneEditing: () => void; + onCancelEditing: () => void; +} + +interface PaletteFormState { + color: number; +} + +class PaletteForm extends React.Component { + + state: PaletteFormState = { + color: 0 + } + + formRef: RefObject = React.createRef(); + + componentDidMount() { + ValidatorForm.addValidationRule('uniqueName', this.props.uniqueName); + } + + submit = () => { + this.formRef.current.submit(); + } + + selectColor = (event: React.ChangeEvent<{}>, value: number | number[]) => { + this.setState({ color: value as number }); + } + + updateName = (event: React.ChangeEvent) => { + const { updatePalette, palette } = this.props; + updatePalette({ ...palette, name: event.target.value }); + } + + changeColor = (result: ColorResult) => { + const { color } = this.state; + const { updatePalette, palette } = this.props; + const colors = [...palette.colors]; + colors[color] = result.hex; + updatePalette({ ...palette, colors }); + } + + render() { + const { palette, creating, onDoneEditing, onCancelEditing } = this.props; + const { color } = this.state; + + return ( + + + {creating ? 'Add' : 'Modify'} Palette + + + + + + + + + Cancel + + + Done + + + + + ); + } +} + +export default PaletteForm; diff --git a/interface/src/project/PaletteSettingsContext.tsx b/interface/src/project/PaletteSettingsContext.tsx new file mode 100644 index 00000000..df3da935 --- /dev/null +++ b/interface/src/project/PaletteSettingsContext.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { PaletteSettings } from "./types"; + +export type PaletteSettingsContextType = { + paletteSettingsUpdated: (paletteSettings: PaletteSettings) => void; + paletteSettings: PaletteSettings; +} + +const PaletteSettingsContextDefaultValue = {} as PaletteSettingsContextType; +export const PaletteSettingsContext = React.createContext( + PaletteSettingsContextDefaultValue +); diff --git a/interface/src/project/PaletteSettingsController.tsx b/interface/src/project/PaletteSettingsController.tsx new file mode 100644 index 00000000..e521a9ed --- /dev/null +++ b/interface/src/project/PaletteSettingsController.tsx @@ -0,0 +1,32 @@ +import { Container } from '@material-ui/core'; +import React, { Component } from 'react'; + +import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components'; +import PaletteSettingsForm from './PaletteSettingsForm'; + +import { PaletteSettings, PALETTE_SETTINGS_ENDPOINT } from './types'; + +type PaletteSettingsControllerProps = RestControllerProps; + +class PaletteSettingsController extends Component { + + componentDidMount() { + this.props.loadData(); + } + + render() { + return ( + + + } + /> + + + ) + } + +} + +export default restController(PALETTE_SETTINGS_ENDPOINT, PaletteSettingsController); diff --git a/interface/src/project/PaletteSettingsForm.tsx b/interface/src/project/PaletteSettingsForm.tsx new file mode 100644 index 00000000..77d4f6f8 --- /dev/null +++ b/interface/src/project/PaletteSettingsForm.tsx @@ -0,0 +1,149 @@ +import { Button, IconButton, isWidthDown, Table, TableBody, TableCell, TableFooter, TableHead, TableRow, withWidth, WithWidthProps } from '@material-ui/core'; +import React, { Fragment } from 'react'; +import { ValidatorForm } from 'react-material-ui-form-validator'; + +import EditIcon from '@material-ui/icons/Edit'; +import DeleteIcon from '@material-ui/icons/Delete'; +import SaveIcon from '@material-ui/icons/Save'; +import PersonAddIcon from '@material-ui/icons/PersonAdd'; + +import { FormActions, FormButton, RestFormProps } from '../components'; +import { DEFAULT_PALETTE, generateGradient, Palette, PaletteSettings } from './types'; +import PaletteForm from './PaletteForm'; + +type PaletteSettingsFormProps = RestFormProps & WithWidthProps; + +interface PaletteSettingsFormState { + creating: boolean; + palette?: Palette; +} + +class PaletteSettingsForm extends React.Component { + + state: PaletteSettingsFormState = { + creating: false + }; + + uniqueName = (name: string) => { + return !this.props.data.palettes.find(p => p.name === name); + } + + removePalette = (palette: Palette) => { + const { data } = this.props; + const palettes = data.palettes.filter(p => p.name !== palette.name); + this.props.setData({ ...data, palettes }); + } + + startEditingPalette = (palette: Palette) => { + this.setState({ + creating: false, + palette + }); + }; + + cancelEditingPalette = () => { + this.setState({ + palette: undefined + }); + } + + doneEditingPalette = () => { + const { palette } = this.state; + if (palette) { + const { data } = this.props; + const palettes = data.palettes.filter(p => p.name !== palette.name); + palettes.push(palette); + this.props.setData({ ...data, palettes }); + this.setState({ + palette: undefined + }); + } + }; + + createPalette = () => { + this.setState({ + creating: true, + palette: { + name: "", + colors: [...DEFAULT_PALETTE] + } + }); + }; + + updatePalette = (palette: Palette) => { + this.setState({ palette }); + }; + + renderPalettes = () => { + const { data } = this.props; + return data.palettes.sort((a, b) => a.name.localeCompare(b.name)).map(palette => ( + + + {palette.name} + + + + this.removePalette(palette)}> + + + this.startEditingPalette(palette)}> + + + + + )); + } + + render() { + const { saveData, width } = this.props; + const { creating, palette } = this.state; + + return ( + + + + + + Name + Palette + + + + + {this.renderPalettes()} + + + + + + + + + +
+ + } variant="contained" color="primary" type="submit"> + Save + + +
+ { + palette && + + } +
+ ); + } + +} + +export default withWidth()(PaletteSettingsForm); diff --git a/interface/src/project/PaletteSettingsLoader.tsx b/interface/src/project/PaletteSettingsLoader.tsx new file mode 100644 index 00000000..26bdd6e3 --- /dev/null +++ b/interface/src/project/PaletteSettingsLoader.tsx @@ -0,0 +1,39 @@ +import React, { Component } from 'react'; + +import { restController, RestControllerProps, RestFormLoader, RestFormProps } from '../components'; +import { PaletteSettingsContext } from './PaletteSettingsContext'; +import { PaletteSettings, PALETTE_SETTINGS_ENDPOINT } from './types'; + +class PaletteSettingsLoader extends Component> { + + componentDidMount() { + this.props.loadData(); + } + + render() { + return ( + } + /> + ) + } + +} + +class PaletteSettingsContextProvider extends Component> { + + render() { + return ( + + {this.props.children} + + ) + } + +} + +export default restController(PALETTE_SETTINGS_ENDPOINT, PaletteSettingsLoader); diff --git a/interface/src/project/ProjectMenu.tsx b/interface/src/project/ProjectMenu.tsx index b7d27397..37d48809 100644 --- a/interface/src/project/ProjectMenu.tsx +++ b/interface/src/project/ProjectMenu.tsx @@ -1,10 +1,12 @@ import React, { Component } from 'react'; import { Link, withRouter, RouteComponentProps } from 'react-router-dom'; -import {List, ListItem, ListItemIcon, ListItemText} from '@material-ui/core'; -import SettingsRemoteIcon from '@material-ui/icons/SettingsRemote'; +import { List, ListItem, ListItemIcon, ListItemText } from '@material-ui/core'; +import FlareIcon from '@material-ui/icons/Flare'; -import { PROJECT_PATH } from '../api'; +import { PROJECT_PATH, WEB_SOCKET_ROOT } from '../api'; + +export const LIGHT_SETTINGS_WEBSOCKET_URL = WEB_SOCKET_ROOT + "spectrum"; class ProjectMenu extends Component { @@ -12,11 +14,11 @@ class ProjectMenu extends Component { const path = this.props.match.url; return ( - + - + - + ) diff --git a/interface/src/project/ProjectRouting.tsx b/interface/src/project/ProjectRouting.tsx index fc378e6d..f3b652bb 100644 --- a/interface/src/project/ProjectRouting.tsx +++ b/interface/src/project/ProjectRouting.tsx @@ -4,7 +4,7 @@ import { Redirect, Switch } from 'react-router'; import { PROJECT_PATH } from '../api'; import { AuthenticatedRoute } from '../authentication'; -import DemoProject from './DemoProject'; +import LightsProject from './LightsProject'; class ProjectRouting extends Component { @@ -16,14 +16,14 @@ class ProjectRouting extends Component { * Add your project page routing below. */ } - + { /* * The redirect below caters for the default project route and redirecting invalid paths. * The "to" property must match one of the routes above for this to work correctly. */ } - + ) } diff --git a/interface/src/project/SpectrumAnalyzer.tsx b/interface/src/project/SpectrumAnalyzer.tsx new file mode 100644 index 00000000..778b2354 --- /dev/null +++ b/interface/src/project/SpectrumAnalyzer.tsx @@ -0,0 +1,115 @@ +import { Container } from '@material-ui/core'; +import React, { Component } from 'react'; +import { WEB_SOCKET_ROOT } from '../api'; +import { SectionContent, webSocketController, WebSocketControllerProps, WebSocketFormLoader, WebSocketFormProps } from '../components'; +import { FrequencyData } from './types'; + +export const LIGHT_SETTINGS_WEBSOCKET_URL = WEB_SOCKET_ROOT + "frequencies"; + +function hslToRgb(h: number, s: number, l: number) { + var r, g, b; + + if (s === 0) { + r = g = b = l; + } else { + var hue2rgb = function hue2rgb(p: number, q: number, t: number) { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + } + + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; +} + +const calculateColorForPercentage = (percentage: number) => { + var hue = (60 - (percentage * 60)) / 360; + var rgb = hslToRgb(hue, 1, 0.5); + return 'rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ')'; +} + +type SpectrumAnalyzerWebSocketControllerProps = WebSocketControllerProps; + +class SpectrumAnalyzerWebSocketController extends Component { + + render() { + return ( + + + ( + + )} + /> + + + ) + } + +} + +export default webSocketController(LIGHT_SETTINGS_WEBSOCKET_URL, 100, SpectrumAnalyzerWebSocketController); + +type LightStateWebSocketControllerFormProps = WebSocketFormProps; + + +class SpectrumAnalyzer extends Component { + + private canvas = React.createRef(); + + componentDidUpdate() { + const currentCanvas = this.canvas.current; + const canvasContext = currentCanvas?.getContext("2d"); + + if (!currentCanvas || !canvasContext) { + return; + } + const visualizerData = this.props.data.bands; + + // Make it visually fill the positioned parent + currentCanvas.style.backgroundColor = "#C5CAE9"; + currentCanvas.style.width = '100%'; + currentCanvas.style.height = '300px'; + + // ...then set the internal size to match + currentCanvas.width = currentCanvas.offsetWidth; + currentCanvas.height = currentCanvas.offsetHeight; + + if (!visualizerData) { + return; + } + + // clear the canvas + canvasContext.clearRect(0, 0, currentCanvas.width, currentCanvas.height); + + // calculate bar width + var barWidth = currentCanvas.width / visualizerData.length; + + // output the data + for (var i = 0; i < visualizerData.length; i++) { + var barOffset = i * barWidth; + var barValue = visualizerData[i]; + var barPercentage = barValue > 0 ? barValue / 4096 : 0; + var barHeight = -currentCanvas.height * barPercentage; + canvasContext.fillStyle = calculateColorForPercentage(barPercentage); + canvasContext.fillRect(barOffset, currentCanvas.height, barWidth, barHeight); + } + } + + render() { + return ( + + ) + } + +} diff --git a/interface/src/project/components/ColorPicker.tsx b/interface/src/project/components/ColorPicker.tsx new file mode 100644 index 00000000..44d77f72 --- /dev/null +++ b/interface/src/project/components/ColorPicker.tsx @@ -0,0 +1,42 @@ +import React, { Fragment } from 'react'; + +import { Box } from '@material-ui/core'; +import { ColorChangeHandler, HuePicker, TwitterPicker } from 'react-color' + +import { SimpleColors } from './Colors' + +interface ColorPickerProps { + color: string; + onChange: ColorChangeHandler; +} + +class ColorPicker extends React.Component { + render() { + const { + color, + onChange + } = this.props; + return ( + + + + + + + + + ); + } +} + +export default ColorPicker; diff --git a/interface/src/project/components/Colors.ts b/interface/src/project/components/Colors.ts new file mode 100644 index 00000000..bd06beb4 --- /dev/null +++ b/interface/src/project/components/Colors.ts @@ -0,0 +1,5 @@ +export const SimpleColors = [ + "#FFFFFF", "#FF0000", "#00FF00", "#0000FF", "#CD5C5C", + "#00FF7F", "#4682B4", "#B22222", "#00BFFF", "#4B0082", + "#FF7F50", "#40E0D0", "#800080", "#FFFF00", "#00008B", + "#FF1493"]; diff --git a/interface/src/project/components/IncludedBands.tsx b/interface/src/project/components/IncludedBands.tsx new file mode 100644 index 00000000..78e7d19b --- /dev/null +++ b/interface/src/project/components/IncludedBands.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import Switch from '@material-ui/core/Switch'; + +interface IncludedBandProps { + value: boolean[]; + onChange: (value: boolean[]) => void; +} + +class IncludedBands extends React.Component { + + handleChange = (ordinal: number) => (event: React.ChangeEvent) => { + const { value, onChange } = this.props; + const newValue = [...value]; + newValue[ordinal] = event.target.checked; + onChange(newValue); + }; + + render() { + const { value } = this.props; + return ( +
+ {value.map((v, i) => ( + + ))} +
+ ); + } +} + +export default IncludedBands; \ No newline at end of file diff --git a/interface/src/project/components/ModeTransferList.tsx b/interface/src/project/components/ModeTransferList.tsx new file mode 100644 index 00000000..b5d79279 --- /dev/null +++ b/interface/src/project/components/ModeTransferList.tsx @@ -0,0 +1,159 @@ +import React, { FC, useEffect } from 'react'; + +import { makeStyles } from '@material-ui/core/styles'; +import Grid from '@material-ui/core/Grid'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemText from '@material-ui/core/ListItemText'; +import Checkbox from '@material-ui/core/Checkbox'; +import Button from '@material-ui/core/Button'; +import Paper from '@material-ui/core/Paper'; + +import { ROTATE_AUDIO_LIGHT_MODES, AudioLightModeType, AUDIO_LIGHT_MODE_METADATA } from '../types'; + +const useStyles = makeStyles((theme) => ({ + paper: { + marginTop: theme.spacing(2), + width: 170, + height: 230, + overflow: 'auto', + }, + button: { + margin: theme.spacing(0.5, 0), + minWidth: 35 + } +})); + +function not(a: AudioLightModeType[], b: AudioLightModeType[]) { + return a.filter((value) => b.indexOf(value) === -1); +} + +const intersection = (a: AudioLightModeType[], b: AudioLightModeType[]) => { + return a.filter((type) => b.includes(type)); +} + +interface TranserListProps { + selected: AudioLightModeType[]; + onSelectionChanged: (selected: AudioLightModeType[]) => void; +} + +const ModeTransferList: FC = ({ selected, onSelectionChanged }) => { + const classes = useStyles(); + const [checked, setChecked] = React.useState([]); + const [left, setLeft] = React.useState(not(selected, ROTATE_AUDIO_LIGHT_MODES)); + const [right, setRight] = React.useState(selected); + + const leftChecked = intersection(checked, left); + const rightChecked = intersection(checked, right); + + useEffect(() => { + setLeft(not(ROTATE_AUDIO_LIGHT_MODES, selected)); + setRight(selected); + }, [selected]); + + const handleToggle = (value: AudioLightModeType) => () => { + const currentIndex = checked.indexOf(value); + const newChecked = [...checked]; + + if (currentIndex === -1) { + newChecked.push(value); + } else { + newChecked.splice(currentIndex, 1); + } + + setChecked(newChecked); + }; + + const handleAllRight = () => { + setChecked(not(checked, leftChecked)); + onSelectionChanged(ROTATE_AUDIO_LIGHT_MODES); + }; + + const handleCheckedRight = () => { + onSelectionChanged(right.concat(leftChecked)); + setChecked(not(checked, leftChecked)); + }; + + const handleCheckedLeft = () => { + onSelectionChanged(not(right, rightChecked)); + setChecked(not(checked, rightChecked)); + }; + + const handleAllLeft = () => { + setChecked(not(checked, rightChecked)); + onSelectionChanged([]); + }; + + const customList = (items: AudioLightModeType[]) => ( + + + {items.map((value) => { + return ( + + + + + ); + })} + + + + ); + + return ( + + {customList(left)} + + + + + + + + + {customList(right)} + + ); +} + +export default ModeTransferList; diff --git a/interface/src/project/components/PalettePicker.tsx b/interface/src/project/components/PalettePicker.tsx new file mode 100644 index 00000000..742441da --- /dev/null +++ b/interface/src/project/components/PalettePicker.tsx @@ -0,0 +1,40 @@ +import React, { FC, useContext } from 'react'; +import { Box, ListItemText, MenuItem, TextField } from '@material-ui/core'; +import { PaletteSettingsContext } from '../PaletteSettingsContext'; +import { generateGradient } from '../types'; + +interface PalettePickerProps { + name: string; + label: string; + value: string; + onChange: (event: React.ChangeEvent) => void; +} + +const PalettePicker: FC = ({ name, label, value, onChange }) => { + const context = useContext(PaletteSettingsContext); + return ( + + {context.paletteSettings.palettes.map(palette => ( + + + + + + + + + ))} + + ); +} + + +export default PalettePicker; diff --git a/interface/src/project/modes/AudioLightColorMode.tsx b/interface/src/project/modes/AudioLightColorMode.tsx new file mode 100644 index 00000000..281f3c2a --- /dev/null +++ b/interface/src/project/modes/AudioLightColorMode.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import FormLabel from '@material-ui/core/FormLabel'; +import Slider from '@material-ui/core/Slider'; + +import { ColorModeSettings } from '../types'; +import { audioLightMode, AudioLightModeProps } from './AudioLightMode'; +import { Box, Switch } from '@material-ui/core'; +import ColorPicker from '../components/ColorPicker'; +import IncludedBands from '../components/IncludedBands'; + +type AudioLightColorModeProps = AudioLightModeProps; + +class AudioLightColorMode extends React.Component { + + render() { + const { data, handleChange, handleValueChange, handleSliderChange, handleColorChange } = this.props; + + return ( +
+ + Audio Enabled + + + + Color + + + + Brightness + + + Included Bands + +
+ ); + } +} + +export default audioLightMode(AudioLightColorMode); diff --git a/interface/src/project/modes/AudioLightConfettiMode.tsx b/interface/src/project/modes/AudioLightConfettiMode.tsx new file mode 100644 index 00000000..4627a4ed --- /dev/null +++ b/interface/src/project/modes/AudioLightConfettiMode.tsx @@ -0,0 +1,66 @@ +import React from 'react'; + +import FormLabel from '@material-ui/core/FormLabel'; +import Slider from '@material-ui/core/Slider'; +import { Box } from '@material-ui/core'; + +import { ConfettiModeSettings } from '../types'; +import { audioLightMode, AudioLightModeProps } from './AudioLightMode'; +import PalettePicker from '../components/PalettePicker'; + +type AudioLightConfettiModeProps = AudioLightModeProps; + +class AudioLightConfettiMode extends React.Component { + + render() { + const { data, handleSliderChange, handleValueChange } = this.props; + + return ( +
+ + + + + Palette Changes (per cycle) + + Brightness + + Delay + + +
+ ); + } +} + +export default audioLightMode(AudioLightConfettiMode); diff --git a/interface/src/project/modes/AudioLightFireMode.tsx b/interface/src/project/modes/AudioLightFireMode.tsx new file mode 100644 index 00000000..0cd0e410 --- /dev/null +++ b/interface/src/project/modes/AudioLightFireMode.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import FormLabel from '@material-ui/core/FormLabel'; +import Slider from '@material-ui/core/Slider'; + +import { FireModeSettings } from '../types'; +import { audioLightMode, AudioLightModeProps } from './AudioLightMode'; +import { Box, Switch } from '@material-ui/core'; +import PalettePicker from '../components/PalettePicker'; + +type AudioLightFireModeProps = AudioLightModeProps; + +class AudioLightFireMode extends React.Component { + + render() { + const { data, handleValueChange, handleSliderChange } = this.props; + + return ( +
+ + + Reverse + + + + Cooling + + Sparking + + +
+ ); + } +} + +export default audioLightMode(AudioLightFireMode); diff --git a/interface/src/project/modes/AudioLightLightningMode.tsx b/interface/src/project/modes/AudioLightLightningMode.tsx new file mode 100644 index 00000000..8716617b --- /dev/null +++ b/interface/src/project/modes/AudioLightLightningMode.tsx @@ -0,0 +1,66 @@ +import React from 'react'; + +import FormLabel from '@material-ui/core/FormLabel'; +import Slider from '@material-ui/core/Slider'; + +import { LightningModeSettings } from '../types'; +import IncludedBands from '../components/IncludedBands'; +import ColorPicker from '../components/ColorPicker'; +import { audioLightMode, AudioLightModeProps } from './AudioLightMode'; +import { Box } from '@material-ui/core'; + +type AudioLightLightningModeProps = AudioLightModeProps; + +class AudioLightLightningMode extends React.Component { + + render() { + const { data, handleChange, handleSliderChange, handleColorChange } = this.props; + + return ( +
+ + Color + + + + Brightness + + Flashes + + Threshold + + + + Included Bands + + +
+ ); + } +} + +export default audioLightMode(AudioLightLightningMode); diff --git a/interface/src/project/modes/AudioLightMode.tsx b/interface/src/project/modes/AudioLightMode.tsx new file mode 100644 index 00000000..e71e8b09 --- /dev/null +++ b/interface/src/project/modes/AudioLightMode.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { ColorResult } from "react-color"; +import { extractEventValue, WebSocketFormProps } from "../../components"; +import { AudioLightMode, AudioLightModeSettings } from "../types"; + +export interface AudioLightModeProps { + handleValueChange: (name: keyof D, callback?: () => void) => (event: React.ChangeEvent) => void; + handleChange: (name: keyof D) => (value: T) => void; + handleSliderChange: (name: keyof D) => (event: React.ChangeEvent<{}>, value: number | number[]) => void; + handleColorChange: (name: keyof D) => (value: ColorResult) => void; + data: D; +} + +export function audioLightMode(AudioLightModeComponent: React.ComponentType>) { + return class extends React.Component> { + + handleChange = (name: keyof D) => (value: T) => { + const { data, setData, saveData } = this.props; + setData({ ...data, settings: { ...data.settings, [name]: value } }, saveData); + } + + handleValueChange = (name: keyof D) => (event: React.ChangeEvent) => { + const { data, setData, saveData } = this.props; + setData({ ...data, settings: { ...data.settings, [name]: extractEventValue(event) } }, saveData) + } + + handleSliderChange = (name: keyof D) => (event: React.ChangeEvent<{}>, value: number | number[]) => { + const { data, setData, saveData } = this.props; + setData({ ...data, settings: { ...data.settings, [name]: value } }, saveData); + } + + handleColorChange = (name: keyof D) => (value: ColorResult) => { + const { data, setData, saveData } = this.props; + setData({ ...data, settings: { ...data.settings, [name]: value.hex } }, saveData); + } + + render() { + return ( + + ); + } + + } +} diff --git a/interface/src/project/modes/AudioLightPacificaMode.tsx b/interface/src/project/modes/AudioLightPacificaMode.tsx new file mode 100644 index 00000000..a4ef4824 --- /dev/null +++ b/interface/src/project/modes/AudioLightPacificaMode.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import { PacificaModeSettings } from '../types'; +import { audioLightMode, AudioLightModeProps } from './AudioLightMode'; +import PalettePicker from '../components/PalettePicker'; + +type AudioLightPacificaModeProps = AudioLightModeProps; + +class AudioLightPacificaMode extends React.Component { + + render() { + const { data, handleValueChange } = this.props; + + return ( +
+ + + +
+ ); + } +} + +export default audioLightMode(AudioLightPacificaMode); diff --git a/interface/src/project/modes/AudioLightPrideMode.tsx b/interface/src/project/modes/AudioLightPrideMode.tsx new file mode 100644 index 00000000..a4cd7a95 --- /dev/null +++ b/interface/src/project/modes/AudioLightPrideMode.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import FormLabel from '@material-ui/core/FormLabel'; +import Slider from '@material-ui/core/Slider'; + +import { PrideModeSettings } from '../types'; +import { audioLightMode, AudioLightModeProps } from './AudioLightMode'; +import { Box } from '@material-ui/core'; + +type AudioLightPrideModeProps = AudioLightModeProps; + +class AudioLightPrideMode extends React.Component { + render() { + const { data, handleSliderChange } = this.props; + + return ( +
+ + Brightness BPM + + Brightness Freq Min + + Brightness Freq Max + + Hue BPM + + Hue Delta Min + + Hue Delta Max + + +
+ ); + } +} + +export default audioLightMode(AudioLightPrideMode); diff --git a/interface/src/project/modes/AudioLightRainbowMode.tsx b/interface/src/project/modes/AudioLightRainbowMode.tsx new file mode 100644 index 00000000..ca70d1cd --- /dev/null +++ b/interface/src/project/modes/AudioLightRainbowMode.tsx @@ -0,0 +1,59 @@ +import React from 'react'; + +import FormLabel from '@material-ui/core/FormLabel'; +import Slider from '@material-ui/core/Slider'; + +import { RainbowModeSettings } from '../types'; +import { audioLightMode, AudioLightModeProps } from './AudioLightMode'; +import { Box, Switch } from '@material-ui/core'; + +type AudioLightRainbowModeProps = AudioLightModeProps; + +class AudioLightRainbowMode extends React.Component { + + render() { + const { data, handleValueChange, handleSliderChange } = this.props; + + return ( +
+ + Audio Enabled + + + + Brightness + + Rotate Speed + + Hue Delta + + +
+ ); + } +} + +export default audioLightMode(AudioLightRainbowMode); diff --git a/interface/src/project/modes/AudioLightRotateMode.tsx b/interface/src/project/modes/AudioLightRotateMode.tsx new file mode 100644 index 00000000..262ca765 --- /dev/null +++ b/interface/src/project/modes/AudioLightRotateMode.tsx @@ -0,0 +1,46 @@ +import { Box, FormLabel, Slider } from '@material-ui/core'; +import React from 'react'; +import ModeTransferList from '../components/ModeTransferList'; + +import { RotateModeSettings } from '../types'; +import { audioLightMode, AudioLightModeProps } from './AudioLightMode'; + +type AudioLightRotateModeProps = AudioLightModeProps; + +const millisToMinutesAndSeconds = (millis: number) => { + var minutes = Math.floor(millis / 60000); + var seconds = Math.floor((millis % 60000) / 1000); + return minutes + ":" + (seconds < 10 ? '0' : '') + seconds; +} + +class AudioLightRotateMode extends React.Component { + + changeModes = this.props.handleChange("modes"); + + render() { + const { data, handleSliderChange } = this.props; + return ( +
+ + Delay + + + + Modes + + +
+ ); + } +} + +export default audioLightMode(AudioLightRotateMode); diff --git a/interface/src/project/types.ts b/interface/src/project/types.ts index 32212557..525ba750 100644 --- a/interface/src/project/types.ts +++ b/interface/src/project/types.ts @@ -1,9 +1,206 @@ -export interface LightState { - led_on: boolean; +import { ENDPOINT_ROOT, WEB_SOCKET_ROOT } from "../api"; +import { WebSocketFormProps } from "../components"; + +import AudioLightColorMode from "./modes/AudioLightColorMode"; +import AudioLightConfettiMode from "./modes/AudioLightConfettiMode"; +import AudioLightFireMode from "./modes/AudioLightFireMode"; +import AudioLightLightningMode from "./modes/AudioLightLightningMode"; +import AudioLightPacificaMode from "./modes/AudioLightPacificaMode"; +import AudioLightPrideMode from "./modes/AudioLightPrideMode"; +import AudioLightRainbowMode from "./modes/AudioLightRainbowMode"; +import AudioLightRotateMode from "./modes/AudioLightRotateMode"; + +export interface FrequencyData { + bands: number[]; } -export interface LightMqttSettings { - unique_id : string; +export interface Palette { name: string; - mqtt_path : string; + colors: string[]; +} + +export enum AudioLightModeType { + OFF = "off", + COLOR = "color", + RAINBOW = "rainbow", + LIGHTNING = "lightning", + CONFETTI = "confetti", + FIRE = "fire", + PACIFICA = "pacifica", + PRIDE = "pride", + ROTATE = "rotate" +} + +export interface OffModeSettings { +} + +export interface ColorModeSettings { + color: string; + brightness: number; + audio_enabled: boolean; + included_bands: boolean[] +} + +export interface RainbowModeSettings { + brightness: number; + rotate_speed: 32; + audio_enabled: boolean; + hue_delta: number; +} + +export interface LightningModeSettings { + color: string; + brightness: number; + threshold: number; + flashes: number; + audio_enabled: boolean; + included_bands: boolean[] +} + +export interface ConfettiModeSettings { + palette1: string; + palette2: string; + palette3: string; + max_changes: number; + brightness: number; + delay: number; +} + +export interface FireModeSettings { + palette: string; + cooling: number; + sparking: number; + reverse: boolean; +} + +export interface PacificaModeSettings { + palette1: string; + palette2: string; + palette3: string; +} + +export interface PrideModeSettings { + brightness_bpm: number; + brightness_freq_min: number; + brightness_freq_max: number; + hue_bpm: number; + hue_delta_min: number; + hue_delta_max: number; +} + +export interface RotateModeSettings { + modes: AudioLightModeType[]; + delay: number; +} + +export interface PaletteSettings { + palettes: Palette[]; +} + +export interface LedSettings { + max_power_milliwatts: number; + dead_zone: number; + smoothing_factor: number; +} + +export type AudioLightModeSettings = ( + OffModeSettings | + ColorModeSettings | + RainbowModeSettings | + LightningModeSettings | + ConfettiModeSettings | + FireModeSettings | + PacificaModeSettings | + PrideModeSettings | + RotateModeSettings +) + +export interface AudioLightMode { + mode_id: AudioLightModeType; + settings: AudioLightModeSettings; } + +export const LED_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "ledSettings"; +export const PALETTE_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "paletteSettings"; +export const AUDIO_LIGHT_SAVE_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "saveModeSettings"; +export const AUDIO_LIGHT_LOAD_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "loadModeSettings"; +export const AUDIO_LIGHT_SETTINGS_ENDPOINT = WEB_SOCKET_ROOT + "audioLightSettings"; + +export const generateGradient = (palette: Palette) => { + return `linear-gradient(0.25turn, ${palette.colors.join(', ')})`; +} + +export const DEFAULT_PALETTE = [ + "#ff0000", + "#d52a00", + "#ab5500", + "#ab7f00", + "#abab00", + "#56d500", + "#00ff00", + "#00d52a", + "#00ab55", + "#0056aa", + "#0000ff", + "#2a00d5", + "#5500ab", + "#7f0081", + "#ab0055", + "#d5002b" +]; + +export interface AudioLightModeMetadata { + label: string; + renderer?: React.ComponentType>; + rotate: boolean; +} + +export const AUDIO_LIGHT_MODE_METADATA: { [type in AudioLightModeType]: AudioLightModeMetadata } = { + off: { + label: "Off", + rotate: false + }, + color: { + label: "Color", + renderer: AudioLightColorMode, + rotate: true + }, + rainbow: { + label: "Rainbow", + renderer: AudioLightRainbowMode, + rotate: true + }, + lightning: { + label: "Lightning", + renderer: AudioLightLightningMode, + rotate: true + }, + confetti: { + label: "Confetti", + renderer: AudioLightConfettiMode, + rotate: true + }, + fire: { + label: "Fire", + renderer: AudioLightFireMode, + rotate: true + }, + pacifica: { + label: "Pacifica", + renderer: AudioLightPacificaMode, + rotate: true + }, + pride: { + label: "Pride", + renderer: AudioLightPrideMode, + rotate: true + }, + rotate: { + label: "Rotate", + renderer: AudioLightRotateMode, + rotate: false + } +} + +export const AUDIO_LIGHT_MODES = Object.entries(AudioLightModeType).map(value => value[1]); +export const ROTATE_AUDIO_LIGHT_MODES = Object.entries(AudioLightModeType).map(value => value[1]).filter(value => AUDIO_LIGHT_MODE_METADATA[value].rotate); diff --git a/interface/src/security/Security.tsx b/interface/src/security/Security.tsx index 4e99769b..a12f81a0 100644 --- a/interface/src/security/Security.tsx +++ b/interface/src/security/Security.tsx @@ -20,7 +20,7 @@ class Security extends Component { render() { return ( - + diff --git a/interface/src/system/System.tsx b/interface/src/system/System.tsx index 671d5e07..d3e5dd45 100644 --- a/interface/src/system/System.tsx +++ b/interface/src/system/System.tsx @@ -24,7 +24,7 @@ class System extends Component { const { authenticatedContext, features } = this.props; return ( - + {features.ota && ( diff --git a/interface/src/wifi/WiFiConnection.tsx b/interface/src/wifi/WiFiConnection.tsx index c6cef06f..b2beb3e0 100644 --- a/interface/src/wifi/WiFiConnection.tsx +++ b/interface/src/wifi/WiFiConnection.tsx @@ -42,7 +42,7 @@ class WiFiConnection extends Component - + diff --git a/lib/framework/SecuritySettingsService.h b/lib/framework/SecuritySettingsService.h index 236bfe4e..77c8badd 100644 --- a/lib/framework/SecuritySettingsService.h +++ b/lib/framework/SecuritySettingsService.h @@ -38,7 +38,7 @@ class SecuritySettings { // users JsonArray users = root.createNestedArray("users"); - for (User user : settings.users) { + for (const User& user : settings.users) { JsonObject userRoot = users.createNestedObject(); userRoot["username"] = user.username; userRoot["password"] = user.password; @@ -103,7 +103,7 @@ class SecuritySettingsService : public SecurityManager { SecuritySettingsService(AsyncWebServer* server, FS* fs); ~SecuritySettingsService(); - // minimal set of functions to support framework with security settings disabled + // minimal set of functions to support framework with security settings disabled Authentication authenticateRequest(AsyncWebServerRequest* request); ArRequestFilterFunction filterRequest(AuthenticationPredicate predicate); ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate); diff --git a/platformio.ini b/platformio.ini index 174353c1..52f62c48 100644 --- a/platformio.ini +++ b/platformio.ini @@ -2,8 +2,8 @@ extra_configs = factory_settings.ini features.ini -default_envs = esp12e -;default_envs = node32s +;default_envs = esp12e +default_envs = node32s [env] build_flags= @@ -11,10 +11,12 @@ build_flags= ${features.build_flags} -D NO_GLOBAL_ARDUINOOTA ; Uncomment ENABLE_CORS to enable Cross-Origin Resource Sharing (required for local React development) - ;-D ENABLE_CORS + -D ENABLE_CORS -D CORS_ORIGIN=\"http://localhost:3000\" ; Uncomment PROGMEM_WWW to enable the storage of the WWW data in PROGMEM -D PROGMEM_WWW + -D FASTLED_ALLOW_INTERRUPTS=0 + -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 ; ensure transitive dependencies are included for correct platforms only lib_compat_mode = strict @@ -29,12 +31,13 @@ framework = arduino monitor_speed = 115200 extra_scripts = - pre:scripts/build_interface.py + pre:scripts/build_interface.py lib_deps = ArduinoJson@>=6.0.0,<7.0.0 ESP Async WebServer@>=1.2.0,<2.0.0 AsyncMqttClient@>=0.8.2,<1.0.0 + FastLED [env:esp12e] platform = espressif8266 diff --git a/src/AudioLightMode.h b/src/AudioLightMode.h new file mode 100644 index 00000000..3507d5c3 --- /dev/null +++ b/src/AudioLightMode.h @@ -0,0 +1,105 @@ +#ifndef AudioLightMode_h +#define AudioLightMode_h + +#include +#include +#include + +#define AUDIO_LIGHT_MODE_FILE_PATH_PREFIX "/modes/" +#define AUDIO_LIGHT_MODE_FILE_PATH_SUFFIX ".json" +#define AUDIO_LIGHT_MODE_SERVICE_PATH_PREFIX "/rest/modes/" + +class AudioLightMode { + public: + virtual const String& getId() = 0; + virtual void begin() = 0; + virtual void readFromFS() = 0; + virtual void writeToFS() = 0; + virtual void tick(CRGB* leds, const uint16_t numLeds) = 0; + virtual void enable() = 0; + virtual void sampleComplete(){}; + virtual bool canRotate() { + return true; + }; + virtual void readAsJson(JsonObject& root) = 0; + virtual StateUpdateResult updateFromJson(JsonObject& root, const String& originId) = 0; +}; + +template +class AudioLightModeImpl : public StatefulService, public AudioLightMode { + protected: + String _id; + String _filePath; + String _servicePath; + JsonStateReader _stateReader; + JsonStateUpdater _stateUpdater; + PaletteSettingsService* _paletteSettingsService; + FrequencySampler* _frequencySampler; + HttpEndpoint _httpEndpoint; + FSPersistence _fsPersistence; + + public: + AudioLightModeImpl(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler, + JsonStateReader stateReader, + JsonStateUpdater stateUpdater, + const String& id) : + _id(id), + _filePath(AUDIO_LIGHT_MODE_SERVICE_PATH_PREFIX + id), + _servicePath(AUDIO_LIGHT_MODE_FILE_PATH_PREFIX + id + AUDIO_LIGHT_MODE_FILE_PATH_SUFFIX), + _stateReader(stateReader), + _stateUpdater(stateUpdater), + _paletteSettingsService(paletteSettingsService), + _frequencySampler(frequencySampler), + _httpEndpoint(stateReader, stateUpdater, this, server, _filePath, securityManager), + _fsPersistence(stateReader, stateUpdater, this, fs, _servicePath.c_str()) { + } + + /* + * Get the code for the mode as a string + */ + const String& getId() { + return _id; + } + + /* + * Read the config from the file system and disable the update handler + */ + void begin() { + _fsPersistence.disableUpdateHandler(); + _fsPersistence.readFromFS(); + } + + /* + * Load the mode config from the file system + */ + void readFromFS() { + _fsPersistence.readFromFS(); + } + + /* + * Save the mode config to the file system + */ + void writeToFS() { + _fsPersistence.writeToFS(); + } + + /* + * Read the mode settings as json + */ + void readAsJson(JsonObject& root) { + StatefulService::read(root, _stateReader); + } + + /* + * Update the mode settings from json + */ + StateUpdateResult updateFromJson(JsonObject& root, const String& originId) { + return StatefulService::update(root, _stateUpdater, originId); + } +}; + +#endif // end AudioLightMode_h diff --git a/src/AudioLightSettingsService.cpp b/src/AudioLightSettingsService.cpp new file mode 100644 index 00000000..11b95f9b --- /dev/null +++ b/src/AudioLightSettingsService.cpp @@ -0,0 +1,149 @@ +#include + +AudioLightSettingsService::AudioLightSettingsService(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + LedSettingsService* ledSettingsService, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler) : + _httpEndpoint(std::bind(&AudioLightSettingsService::read, this, std::placeholders::_1, std::placeholders::_2), + std::bind(&AudioLightSettingsService::update, this, std::placeholders::_1, std::placeholders::_2), + this, + server, + AUDIO_LIGHT_SERVICE_PATH, + securityManager, + AuthenticationPredicates::IS_AUTHENTICATED), + _audioLightModeTxRx( + std::bind(&AudioLightSettingsService::read, this, std::placeholders::_1, std::placeholders::_2), + std::bind(&AudioLightSettingsService::update, this, std::placeholders::_1, std::placeholders::_2), + this, + server, + AUDIO_LIGHT_WS_PATH, + securityManager, + AuthenticationPredicates::IS_AUTHENTICATED), + _ledSettingsService(ledSettingsService) { + server->on( + AUDIO_LIGHT_SAVE_MODE_PATH, + HTTP_POST, + securityManager->wrapRequest(std::bind(&AudioLightSettingsService::saveModeConfig, this, std::placeholders::_1), + AuthenticationPredicates::IS_AUTHENTICATED)); + server->on( + AUDIO_LIGHT_LOAD_MODE_PATH, + HTTP_POST, + securityManager->wrapRequest(std::bind(&AudioLightSettingsService::loadModeConfig, this, std::placeholders::_1), + AuthenticationPredicates::IS_AUTHENTICATED)); + addUpdateHandler([&](const String& originId) { enableMode(); }, false); + frequencySampler->addUpdateHandler([&](const String& originId) { handleSample(); }, false); + ledSettingsService->addUpdateHandler([&](const String& originId) { updateSettings(); }, false); + paletteSettingsService->addUpdateHandler([&](const String& originId) { enableMode(); }, false); + _ledController = &FastLED.addLeds(_leds, NUM_LEDS); + _modes[0] = new OffMode(server, fs, securityManager, paletteSettingsService, frequencySampler); + _modes[1] = new ColorMode(server, fs, securityManager, paletteSettingsService, frequencySampler); + _modes[2] = new RainbowMode(server, fs, securityManager, paletteSettingsService, frequencySampler); + _modes[3] = new LightningMode(server, fs, securityManager, paletteSettingsService, frequencySampler); + _modes[4] = new ConfettiMode(server, fs, securityManager, paletteSettingsService, frequencySampler); + _modes[5] = new FireMode(server, fs, securityManager, paletteSettingsService, frequencySampler); + _modes[6] = new PacificaMode(server, fs, securityManager, paletteSettingsService, frequencySampler); + _modes[7] = new PrideMode(server, fs, securityManager, paletteSettingsService, frequencySampler); + _modes[8] = new RotateMode(server, + fs, + securityManager, + paletteSettingsService, + frequencySampler, + std::bind(&AudioLightSettingsService::getMode, this, std::placeholders::_1)); +} + +void AudioLightSettingsService::begin() { + // configure current mode + _state.currentMode = _modes[8]; + + // initialize all modes + for (uint8_t i = 0; i < NUM_MODES; i++) { + _modes[i]->begin(); + } + + // update the settings + updateSettings(); +} + +void AudioLightSettingsService::updateSettings() { + _ledSettingsService->read([&](LedSettings& ledSettings) { + FastLED.setMaxPowerInMilliWatts(ledSettings.maxPowerMilliwatts == 0 ? 0xFFFFFFFF : ledSettings.maxPowerMilliwatts); + }); + enableMode(); +} + +void AudioLightSettingsService::loop() { + _state.currentMode->tick(_leds, NUM_LEDS); +} + +AudioLightMode* AudioLightSettingsService::getMode(const String& modeId) { + for (uint8_t i = 0; i < NUM_MODES; i++) { + AudioLightMode* mode = _modes[i]; + if (mode->getId() == modeId) { + return mode; + } + } + return nullptr; +} + +void AudioLightSettingsService::enableMode() { + _state.currentMode->enable(); +} + +void AudioLightSettingsService::handleSample() { + _state.currentMode->sampleComplete(); +} + +void AudioLightSettingsService::read(AudioLightSettings& settings, JsonObject& root) { + if (settings.currentMode) { + root["mode_id"] = settings.currentMode->getId(); + JsonObject settingsRoot = root.createNestedObject("settings"); + settings.currentMode->readAsJson(settingsRoot); + } +} + +StateUpdateResult AudioLightSettingsService::update(JsonObject& root, AudioLightSettings& settings) { + // we must have a mode id + if (!root["mode_id"].is()) { + return StateUpdateResult::ERROR; + } + + // change mode if required, exit early on error + StateUpdateResult modeUpdateResult = StateUpdateResult::UNCHANGED; + String modeId = root["mode_id"]; + if (settings.currentMode->getId() != modeId) { + AudioLightMode* mode = getMode(modeId); + if (!mode) { + return StateUpdateResult::ERROR; + } + settings.currentMode = mode; + modeUpdateResult = StateUpdateResult::CHANGED; + } + + // change settings, exit early on error + StateUpdateResult settingsUpdateResult = StateUpdateResult::UNCHANGED; + if (root["settings"].is()) { + JsonObject modeSettings = root["settings"].as(); + settingsUpdateResult = settings.currentMode->updateFromJson(modeSettings, LOCAL_ORIGIN); + if (settingsUpdateResult == StateUpdateResult::ERROR) { + return StateUpdateResult::ERROR; + } + } + + // calculate update state + return modeUpdateResult == StateUpdateResult::CHANGED || settingsUpdateResult == StateUpdateResult::CHANGED + ? StateUpdateResult::CHANGED + : StateUpdateResult::UNCHANGED; +} + +void AudioLightSettingsService::saveModeConfig(AsyncWebServerRequest* request) { + _state.currentMode->writeToFS(); + request->send(200, "text/plain", "Saved"); +} + +void AudioLightSettingsService::loadModeConfig(AsyncWebServerRequest* request) { + _state.currentMode->readFromFS(); + _state.currentMode->enable(); + request->send(200, "text/plain", "Loaded"); +} diff --git a/src/AudioLightSettingsService.h b/src/AudioLightSettingsService.h new file mode 100644 index 00000000..3a6a6254 --- /dev/null +++ b/src/AudioLightSettingsService.h @@ -0,0 +1,66 @@ +#ifndef AudioLightSettingsService_h +#define AudioLightSettingsService_h + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NUM_MODES 9 + +#define AUDIO_LIGHT_SERVICE_PATH "/rest/audioLightSettings" +#define AUDIO_LIGHT_WS_PATH "/ws/audioLightSettings" + +#define AUDIO_LIGHT_SAVE_MODE_PATH "/rest/saveModeSettings" +#define AUDIO_LIGHT_LOAD_MODE_PATH "/rest/loadModeSettings" + +#define LOCAL_ORIGIN "local" + +class AudioLightSettings { + public: + AudioLightMode* currentMode = nullptr; +}; + +class AudioLightSettingsService : public StatefulService { + public: + AudioLightSettingsService(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + LedSettingsService* ledSettingsService, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler); + + void begin(); + void loop(); + + private: + HttpEndpoint _httpEndpoint; + WebSocketTxRx _audioLightModeTxRx; + LedSettingsService* _ledSettingsService; + CRGB _leds[NUM_LEDS]; + CLEDController* _ledController; + AudioLightMode* _modes[NUM_MODES]; + + void read(AudioLightSettings& settings, JsonObject& root); + StateUpdateResult update(JsonObject& root, AudioLightSettings& settings); + AudioLightMode* getMode(const String& modeId); + + void updateSettings(); + void enableMode(); + void handleSample(); + void saveModeConfig(AsyncWebServerRequest* request); + void loadModeConfig(AsyncWebServerRequest* request); +}; + +#endif // end AudioLightSettingsService_h \ No newline at end of file diff --git a/src/ColorMode.cpp b/src/ColorMode.cpp new file mode 100644 index 00000000..0f6244e9 --- /dev/null +++ b/src/ColorMode.cpp @@ -0,0 +1,31 @@ +#include + +ColorMode::ColorMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler) : + AudioLightModeImpl(server, + fs, + securityManager, + paletteSettingsService, + frequencySampler, + ColorModeSettings::read, + ColorModeSettings::update, + COLOR_MODE_ID) { + addUpdateHandler([&](const String& originId) { enable(); }, false); +}; + +void ColorMode::enable() { + _refresh = true; +} + +void ColorMode::tick(CRGB* leds, const uint16_t numLeds) { + if (_refresh || _state.audioEnabled) { + FrequencyData* frequencyData = _frequencySampler->getFrequencyData(); + fill_solid(leds, numLeds, _state.color); + FastLED.show(_state.audioEnabled ? frequencyData->calculateEnergyFloat(_state.includedBands) * _state.brightness + : _state.brightness); + _refresh = false; + } +} diff --git a/src/ColorMode.h b/src/ColorMode.h new file mode 100644 index 00000000..3f381076 --- /dev/null +++ b/src/ColorMode.h @@ -0,0 +1,59 @@ +#ifndef ColorMode_h +#define ColorMode_h + +#include +#include +#include + +#ifndef FACTORY_COLOR_MODE_COLOR +#define FACTORY_COLOR_MODE_COLOR CRGB::White +#endif + +#ifndef FACTORY_COLOR_MODE_AUDIO_ENABLED +#define FACTORY_COLOR_MODE_AUDIO_ENABLED false +#endif + +#ifndef FACTORY_COLOR_MODE_BRIGHTNESS +#define FACTORY_COLOR_MODE_BRIGHTNESS 128 +#endif + +#define COLOR_MODE_ID "color" + +class ColorModeSettings { + public: + CRGB color; + uint8_t brightness; + bool audioEnabled; + bool includedBands[NUM_BANDS]; + + static void read(ColorModeSettings& settings, JsonObject& root) { + writeColorToJson(root, &settings.color); + writeByteToJson(root, &settings.brightness, "brightness"); + writeBoolToJson(root, &settings.audioEnabled, "audio_enabled"); + writeBooleanArrayToJson(root, settings.includedBands, NUM_BANDS, "included_bands"); + } + + static StateUpdateResult update(JsonObject& root, ColorModeSettings& settings) { + updateColorFromJson(root, &settings.color, FACTORY_COLOR_MODE_COLOR); + updateByteFromJson(root, &settings.brightness, FACTORY_COLOR_MODE_BRIGHTNESS, "brightness"); + updateBoolFromJson(root, &settings.audioEnabled, FACTORY_COLOR_MODE_AUDIO_ENABLED, "audio_enabled"); + updateBooleanArrayFromJson(root, settings.includedBands, NUM_BANDS, true, "included_bands"); + return StateUpdateResult::CHANGED; + } +}; + +class ColorMode : public AudioLightModeImpl { + private: + boolean _refresh = true; + + public: + ColorMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler); + void tick(CRGB* leds, const uint16_t numLeds); + void enable(); +}; + +#endif diff --git a/src/ConfettiMode.cpp b/src/ConfettiMode.cpp new file mode 100644 index 00000000..257c51ef --- /dev/null +++ b/src/ConfettiMode.cpp @@ -0,0 +1,79 @@ +#include + +ConfettiMode::ConfettiMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler) : + AudioLightModeImpl(server, + fs, + securityManager, + paletteSettingsService, + frequencySampler, + std::bind(&ConfettiMode::read, this, std::placeholders::_1, std::placeholders::_2), + std::bind(&ConfettiMode::update, this, std::placeholders::_1, std::placeholders::_2), + CONFETTI_MODE_ID) { + addUpdateHandler([&](const String& originId) { enable(); }, false); +}; + +void ConfettiMode::enable() { + _refresh = true; + updateWithoutPropagation([&](ConfettiModeSettings& settings) { + _paletteSettingsService->refreshPalette(settings.palette1); + _paletteSettingsService->refreshPalette(settings.palette2); + _paletteSettingsService->refreshPalette(settings.palette3); + return StateUpdateResult::CHANGED; + }); +} + +void ConfettiMode::tick(CRGB* leds, const uint16_t numLeds) { + if (_refresh) { + fill_solid(leds, numLeds, CHSV(255, 0, 0)); + FastLED.show(); + _refresh = false; + } + + EVERY_N_MILLISECONDS(100) { + nblendPaletteTowardPalette(_currentPalette, _targetPalette, _state.maxChanges); + } + + uint8_t secondHand = (millis() / 1000) % 15; + static uint8_t lastSecond = 99; + if (lastSecond != secondHand) { + lastSecond = secondHand; + switch (secondHand) { + case 0: + _targetPalette = _state.palette1.colors; + _inc = 1; + _hue = 192; + _fade = 2; + _hueDelta = 255; + break; + case 5: + _targetPalette = _state.palette2.colors; + _inc = 2; + _hue = 128; + _fade = 8; + _hueDelta = 64; + break; + case 10: + _targetPalette = _state.palette3.colors; + _inc = 1; + _hue = random16(255); + _fade = 1; + _hueDelta = 16; + break; + case 15: + break; + } + } + + EVERY_N_MILLIS_I(confettiTimer, FACTORY_CONFETTI_MODE_DELAY) { + fadeToBlackBy(leds, numLeds, _fade); + int pos = random16(numLeds); + leds[pos] = ColorFromPalette(_currentPalette, _hue + random16(_hueDelta) / 4, _state.brightness, _currentBlending); + _hue = _hue + _inc; + FastLED.show(); + confettiTimer.setPeriod(_state.delay); + } +} diff --git a/src/ConfettiMode.h b/src/ConfettiMode.h new file mode 100644 index 00000000..d43b9b59 --- /dev/null +++ b/src/ConfettiMode.h @@ -0,0 +1,90 @@ +#ifndef ConfettiMode_h +#define ConfettiMode_h + +#include +#include + +#ifndef FACTORY_CONFETTI_MODE_MAX_CHANGES +#define FACTORY_CONFETTI_MODE_MAX_CHANGES 24 +#endif + +#ifndef FACTORY_CONFETTI_MODE_BRIGHTNESS +#define FACTORY_CONFETTI_MODE_BRIGHTNESS 255 +#endif + +#ifndef FACTORY_CONFETTI_MODE_DELAY +#define FACTORY_CONFETTI_MODE_DELAY 5 +#endif + +#ifndef FACTORY_CONFETTI_MODE_PALETTE1 +#define FACTORY_CONFETTI_MODE_PALETTE1 "Ocean" +#endif + +#ifndef FACTORY_CONFETTI_MODE_PALETTE2 +#define FACTORY_CONFETTI_MODE_PALETTE2 "Lava" +#endif + +#ifndef FACTORY_CONFETTI_MODE_PALETTE3 +#define FACTORY_CONFETTI_MODE_PALETTE3 "Forest" +#endif + +#define CONFETTI_MODE_ID "confetti" + +// could make palettes configurable, pick from a list perhaps... +class ConfettiModeSettings { + public: + uint8_t maxChanges; // number of changes per cycle + uint8_t brightness; // Brightness of a sequence. Remember, max_brightnessght is the overall limiter. + uint8_t delay; // We don't need much delay (if any) + + Palette palette1; + Palette palette2; + Palette palette3; +}; + +class ConfettiMode : public AudioLightModeImpl { + private: + // Variables used by the sequences. + uint8_t _fade = 8; // How quickly does it fade? Lower = slower fade rate. + int _hue = 50; // Starting hue. + uint8_t _inc = 1; // Incremental value for rotating hues + int _hueDelta = 255; // Range of random #'s to use for hue + + CRGBPalette16 _currentPalette; // Current primary target + CRGBPalette16 _targetPalette; // Pallette we are blending to + TBlendType _currentBlending = LINEARBLEND; // NOBLEND or LINEARBLEND + + bool _refresh = true; // For applying config updates or enabling the mode + + void read(ConfettiModeSettings& settings, JsonObject& root) { + writeByteToJson(root, &settings.maxChanges, "max_changes"); + writeByteToJson(root, &settings.brightness, "brightness"); + writeByteToJson(root, &settings.delay, "delay"); + + root["palette1"] = settings.palette1.name; + root["palette2"] = settings.palette2.name; + root["palette3"] = settings.palette3.name; + } + + StateUpdateResult update(JsonObject& root, ConfettiModeSettings& settings) { + updateByteFromJson(root, &settings.maxChanges, FACTORY_CONFETTI_MODE_MAX_CHANGES, "max_changes"); + updateByteFromJson(root, &settings.brightness, FACTORY_CONFETTI_MODE_BRIGHTNESS, "brightness"); + updateByteFromJson(root, &settings.delay, FACTORY_CONFETTI_MODE_DELAY, "delay"); + + settings.palette1 = _paletteSettingsService->getPalette(root["palette1"] | FACTORY_CONFETTI_MODE_PALETTE1); + settings.palette2 = _paletteSettingsService->getPalette(root["palette2"] | FACTORY_CONFETTI_MODE_PALETTE2); + settings.palette3 = _paletteSettingsService->getPalette(root["palette3"] | FACTORY_CONFETTI_MODE_PALETTE3); + return StateUpdateResult::CHANGED; + } + + public: + ConfettiMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler); + void tick(CRGB* leds, const uint16_t numLeds); + void enable(); +}; + +#endif diff --git a/src/FastLEDSettings.h b/src/FastLEDSettings.h new file mode 100644 index 00000000..1d7e4f05 --- /dev/null +++ b/src/FastLEDSettings.h @@ -0,0 +1,20 @@ +#ifndef FastLEDSettings_h +#define FastLEDSettings_h + +#ifndef LED_DATA_PIN +#define LED_DATA_PIN 21 +#endif + +#ifndef COLOR_ORDER +#define COLOR_ORDER GRB +#endif // RGB + +#ifndef LED_TYPE +#define LED_TYPE WS2812 +#endif // WS2811 + +#ifndef NUM_LEDS +#define NUM_LEDS 9 +#endif // 100 + +#endif diff --git a/src/FireMode.cpp b/src/FireMode.cpp new file mode 100644 index 00000000..ba4c9635 --- /dev/null +++ b/src/FireMode.cpp @@ -0,0 +1,68 @@ +#include + +FireMode::FireMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler) : + AudioLightModeImpl(server, + fs, + securityManager, + paletteSettingsService, + frequencySampler, + std::bind(&FireMode::read, this, std::placeholders::_1, std::placeholders::_2), + std::bind(&FireMode::update, this, std::placeholders::_1, std::placeholders::_2), + FIRE_MODE_ID){}; + +void FireMode::enable() { + _refresh = true; + updateWithoutPropagation([&](FireModeSettings& settings) { + selectPalette(settings.palette.name, settings); + return StateUpdateResult::CHANGED; + }); +} + +void FireMode::tick(CRGB* leds, const uint16_t numLeds) { + if (_refresh) { + fill_solid(leds, numLeds, CHSV(255, 0, 0)); // clear leds + FastLED.show(); // render all leds black + memset(_heatMap, 0, sizeof(uint8_t) * numLeds); // clear heat map + _refresh = false; // clear refresh flag + } + // make firey stuff at ~100FPS + EVERY_N_MILLIS(10) { + // Step 1. Cool down every cell a little + for (int i = 0; i < numLeds; i++) { + _heatMap[i] = qsub8(_heatMap[i], random8(0, ((_state.cooling * 10) / numLeds) + 2)); + } + + // Step 2. Heat from each cell drifts 'up' and diffuses a little + for (int k = numLeds - 1; k >= 2; k--) { + _heatMap[k] = (_heatMap[k - 1] + _heatMap[k - 2] + _heatMap[k - 2]) / 3; + } + + // Step 3. Randomly ignite new 'sparks' of heat near the bottom + if (random8() < _state.sparking) { + int y = random8(7); + _heatMap[y] = qadd8(_heatMap[y], random8(160, 255)); + } + + // Step 4. Map from heat cells to LED colors + for (int j = 0; j < numLeds; j++) { + // Scale the heat value from 0-255 down to 0-240 + // for best results with color palettes. + byte colorindex = scale8(_heatMap[j], 240); + CRGB color = ColorFromPalette(_state.palette.colors, colorindex); + int pixelnumber; + if (_state.reverse) { + pixelnumber = (numLeds - 1) - j; + } else { + pixelnumber = j; + } + leds[pixelnumber] = color; + } + + // Step 5. Render the update + FastLED.show(); + } +} diff --git a/src/FireMode.h b/src/FireMode.h new file mode 100644 index 00000000..28cd768d --- /dev/null +++ b/src/FireMode.h @@ -0,0 +1,71 @@ +#ifndef FireMode_h +#define FireMode_h + +#include +#include + +#ifndef FACTORY_FIRE_MODE_SPARKING +#define FACTORY_FIRE_MODE_SPARKING 120 +#endif + +#ifndef FACTORY_FIRE_MODE_REVERSE +#define FACTORY_FIRE_MODE_REVERSE false +#endif + +#ifndef FACTORY_FIRE_MODE_COOLING +#define FACTORY_FIRE_MODE_COOLING 80 +#endif + +#ifndef FACTORY_FIRE_MODE_PALETTE +#define FACTORY_FIRE_MODE_PALETTE "heat" +#endif + +#define FIRE_MODE_ID "fire" + +// audio enable +// make palette customizable +class FireModeSettings { + public: + Palette palette; + bool reverse; + uint8_t cooling; + uint8_t sparking; +}; + +class FireMode : public AudioLightModeImpl { + private: + bool _refresh = true; // For applying config updates or enabling the mode + uint8_t _heatMap[NUM_LEDS]; // Intensity map the led strip - statically allocated ATM + + void read(FireModeSettings& settings, JsonObject& root) { + writeByteToJson(root, &settings.cooling, "cooling"); + writeByteToJson(root, &settings.sparking, "sparking"); + writeBoolToJson(root, &settings.reverse, "reverse"); + root["palette"] = settings.palette.name; + } + + StateUpdateResult update(JsonObject& root, FireModeSettings& settings) { + updateByteFromJson(root, &settings.sparking, FACTORY_FIRE_MODE_SPARKING, "sparking"); + updateByteFromJson(root, &settings.cooling, FACTORY_FIRE_MODE_COOLING, "cooling"); + updateBoolFromJson(root, &settings.reverse, FACTORY_FIRE_MODE_REVERSE, "reverse"); + selectPalette(root["palette"] | FACTORY_FIRE_MODE_PALETTE, settings); + return StateUpdateResult::CHANGED; + } + + void selectPalette(const String& name, FireModeSettings& settings) { + settings.palette = _paletteSettingsService->getPalette(name); + settings.palette.colors[0] = 0x000000; + } + + public: + FireMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler); + void tick(CRGB* leds, const uint16_t numLeds); + void enable(); + void refreshPalettes(const String& originId); +}; + +#endif diff --git a/src/FrequencySampler.cpp b/src/FrequencySampler.cpp new file mode 100644 index 00000000..1df5b399 --- /dev/null +++ b/src/FrequencySampler.cpp @@ -0,0 +1,64 @@ +#include + +FrequencySampler::FrequencySampler(LedSettingsService* ledSettingsService) : _ledSettingsService(ledSettingsService) { + ledSettingsService->addUpdateHandler([&](const String& originId) { updateSettings(); }, false); +} + +void FrequencySampler::begin() { + pinMode(FREQUENCY_SAMPLER_RESET_PIN, OUTPUT); + pinMode(FREQUENCY_SAMPLER_STROBE_PIN, OUTPUT); + pinMode(FREQUENCY_SAMPLER_ANALOG_PIN, INPUT); + digitalWrite(FREQUENCY_SAMPLER_RESET_PIN, LOW); + digitalWrite(FREQUENCY_SAMPLER_STROBE_PIN, HIGH); + updateSettings(); +} + +void FrequencySampler::loop() { + // take samples 100 times a second (max) + EVERY_N_MILLIS(10) { + update( + [&](FrequencyData& frequencyData) { + // Reset MSGEQ7 IC + digitalWrite(FREQUENCY_SAMPLER_RESET_PIN, HIGH); + delayMicroseconds(5); + digitalWrite(FREQUENCY_SAMPLER_RESET_PIN, LOW); + + // read samples + for (uint8_t i = 0; i < NUM_BANDS; i++) { + // trigger each value in turn + digitalWrite(FREQUENCY_SAMPLER_STROBE_PIN, LOW); + + // allow the output to settle + delayMicroseconds(36); + + // read frequency for pin + uint16_t value = analogRead(FREQUENCY_SAMPLER_ANALOG_PIN); + + // re-map frequency to eliminate low level noise + value = value > _deadZone ? map(value - _deadZone, 0, ADC_MAX_VALUE - _deadZone, 0, ADC_MAX_VALUE) : 0; + + // crappy smoothing to avoid crazy flickering + frequencyData.bands[i] = _smoothingFactor * frequencyData.bands[i] + (1 - _smoothingFactor) * value; + + // strobe pin high again for next loop + digitalWrite(FREQUENCY_SAMPLER_STROBE_PIN, HIGH); + + // wait 72 microseconds + delayMicroseconds(36); + } + return StateUpdateResult::CHANGED; + }, + "loop"); + } +} + +void FrequencySampler::updateSettings() { + _ledSettingsService->read([&](LedSettings& ledSettings) { + _smoothingFactor = ledSettings.smoothingFactor; + _deadZone = ledSettings.deadZone; + }); +} + +FrequencyData* FrequencySampler::getFrequencyData() { + return &_state; +} diff --git a/src/FrequencySampler.h b/src/FrequencySampler.h new file mode 100644 index 00000000..413c3332 --- /dev/null +++ b/src/FrequencySampler.h @@ -0,0 +1,61 @@ +#ifndef FrequencySampler_h +#define FrequencySampler_h + +#include +#include +#include +#include + +#define FREQUENCY_SAMPLER_DEAD_ZONE 700 +#define FREQUENCY_SAMPLER_RESET_PIN 4 +#define FREQUENCY_SAMPLER_STROBE_PIN 5 +#define FREQUENCY_SAMPLER_ANALOG_PIN 36 + +#define NUM_BANDS 7 +#define ADC_MAX_VALUE 4096 + +class FrequencyData { + public: + uint16_t bands[NUM_BANDS]; + + float calculateEnergyFloat(bool* includedBands = NULL) { + uint16_t currentLevel = 0; + uint16_t numBands = 0; + for (uint16_t i = 0; i < NUM_BANDS; i++) { + if (includedBands == NULL || includedBands[i]) { + currentLevel += bands[i]; + numBands++; + } + } + return (float)currentLevel / (numBands * ADC_MAX_VALUE); + } + + uint8_t calculateEnergyByte(bool* includedBands = NULL) { + return calculateEnergyFloat(includedBands) * 255; + } + + static void read(FrequencyData& settings, JsonObject& root) { + JsonArray array = root.createNestedArray("bands"); + for (uint16_t i = 0; i < NUM_BANDS; i++) { + array.add(settings.bands[i]); + } + } +}; + +class FrequencySampler : public StatefulService { + public: + FrequencySampler(LedSettingsService* ledSettingsService); + + void begin(); + void loop(); + FrequencyData* getFrequencyData(); + + private: + LedSettingsService* _ledSettingsService; + float _smoothingFactor = 0; + float _deadZone = 0; + + void updateSettings(); +}; + +#endif // end FrequencySampler_h diff --git a/src/FrequencyTransmitter.cpp b/src/FrequencyTransmitter.cpp new file mode 100644 index 00000000..3516bf82 --- /dev/null +++ b/src/FrequencyTransmitter.cpp @@ -0,0 +1,26 @@ +#include + +FrequencyTransmitter::FrequencyTransmitter(AsyncWebServer* server, + SecurityManager* securityManager, + FrequencySampler* frequencySampler) : + _frequencySampler(frequencySampler), + _webSocketTx(FrequencyData::read, + this, + server, + FREQUENCY_TRANSMITTER_WS_PATH, + securityManager, + AuthenticationPredicates::IS_AUTHENTICATED) { + frequencySampler->addUpdateHandler([&](const String& originId) { handleSample(); }, false); +} + +void FrequencyTransmitter::handleSample() { + EVERY_N_MILLISECONDS(200) { + update( + [&](FrequencyData& state) { + FrequencyData* frequencyData = _frequencySampler->getFrequencyData(); + std::copy(std::begin(frequencyData->bands), std::end(frequencyData->bands), std::begin(state.bands)); + return StateUpdateResult::CHANGED; + }, + "system"); + } +} diff --git a/src/FrequencyTransmitter.h b/src/FrequencyTransmitter.h new file mode 100644 index 00000000..73d0c6a7 --- /dev/null +++ b/src/FrequencyTransmitter.h @@ -0,0 +1,20 @@ +#ifndef FrequencyTransmitter_h +#define FrequencyTransmitter_h + +#define FREQUENCY_TRANSMITTER_WS_PATH "/ws/frequencies" + +#include +#include +#include + +class FrequencyTransmitter : public StatefulService { + public: + FrequencyTransmitter(AsyncWebServer* server, SecurityManager* securityManager, FrequencySampler* frequencySampler); + + private: + FrequencySampler* _frequencySampler; + WebSocketTx _webSocketTx; + void handleSample(); +}; + +#endif // end FrequencyTransmitter_h diff --git a/src/JsonUtil.cpp b/src/JsonUtil.cpp new file mode 100644 index 00000000..3476119d --- /dev/null +++ b/src/JsonUtil.cpp @@ -0,0 +1,90 @@ +#include + +void updateBooleanArrayFromJson(JsonObject& root, bool booleanArray[], uint16_t len, bool def, String key) { + for (uint8_t i = 0; i < len; i++) { + booleanArray[i] = def; + } + if (root.containsKey(key) && root[key].is()) { + JsonArray jsonArray = root[key]; + for (uint8_t i = 0; i < len && i < jsonArray.size(); i++) { + if (jsonArray[i].is()) { + booleanArray[i] = jsonArray[i]; + } + } + } +} + +void writeBooleanArrayToJson(JsonObject& root, bool booleanArray[], uint16_t len, String key) { + JsonArray array = root.createNestedArray(key); + for (uint8_t i = 0; i < len; i++) { + array.add(booleanArray[i]); + } +} + +void updatePaletteFromJson(JsonObject& root, CRGBPalette16* palette, CRGB def, String key) { + for (uint8_t i = 0; i < 16; i++) { + palette->entries[i] = def; + } + if (root.containsKey(key) && root[key].is()) { + JsonArray jsonArray = root[key]; + for (uint8_t i = 0; i < 16 && i < jsonArray.size(); i++) { + if (jsonArray[i].is()) { + String colorString = jsonArray[i]; + if (colorString.length() == 7) { + palette->entries[i] = CRGB(strtoll(&colorString[1], NULL, 16)); + } + } + } + } +} + +void writePaletteToJson(JsonObject& root, const CRGBPalette16* palette, String key) { + JsonArray array = root.createNestedArray(key); + for (uint8_t i = 0; i < 16; i++) { + CRGB color = palette->entries[i]; + char colorString[8]; + sprintf(colorString, "#%02x%02x%02x", color.r, color.g, color.b); + array.add(colorString); + } +} + +void updateColorFromJson(JsonObject& root, CRGB* color, CRGB def, String key) { + if (root.containsKey(key) && root[key].is()) { + String colorString = root[key]; + if (colorString.length() == 7) { + *color = CRGB(strtoll(&colorString[1], NULL, 16)); + } + } else { + *color = def; + } +} + +void writeColorToJson(JsonObject& root, CRGB* color, String key) { + char colorString[8]; + sprintf(colorString, "#%02x%02x%02x", color->r, color->g, color->b); + root[key] = colorString; +} + +void updateByteFromJson(JsonObject& root, uint8_t* value, uint8_t def, String key) { + if (root.containsKey(key) && root[key].is()) { + *value = (uint8_t)root[key]; + } else { + *value = def; + } +} + +void writeByteToJson(JsonObject& root, uint8_t* value, String key) { + root[key] = (uint8_t)*value; +} + +void updateBoolFromJson(JsonObject& root, bool* value, bool def, String key) { + if (root.containsKey(key) && root[key].is()) { + *value = (bool)root[key]; + } else { + *value = def; + } +} + +void writeBoolToJson(JsonObject& root, bool* value, String key) { + root[key] = (bool)*value; +} diff --git a/src/JsonUtil.h b/src/JsonUtil.h new file mode 100644 index 00000000..c2c846b6 --- /dev/null +++ b/src/JsonUtil.h @@ -0,0 +1,22 @@ +#ifndef JsonUtil_h +#define JsonUtil_h + +#include +#include + +void updateBooleanArrayFromJson(JsonObject& root, bool booleanArray[], uint16_t len, bool def, String key); +void writeBooleanArrayToJson(JsonObject& root, bool booleanArray[], uint16_t maxSize, String key); + +void updateColorFromJson(JsonObject& root, CRGB* color, CRGB def, String key = "color"); +void writeColorToJson(JsonObject& root, CRGB* color, String key = "color"); + +void updatePaletteFromJson(JsonObject& root, CRGBPalette16* palette, CRGB def, String key = "colors"); +void writePaletteToJson(JsonObject& root, const CRGBPalette16* palette, String key = "colors"); + +void updateByteFromJson(JsonObject& root, uint8_t* value, uint8_t def, String key); +void writeByteToJson(JsonObject& root, uint8_t* value, String key); + +void updateBoolFromJson(JsonObject& root, bool* value, bool def, String key); +void writeBoolToJson(JsonObject& root, bool* value, String key); + +#endif diff --git a/src/LedSettingsService.cpp b/src/LedSettingsService.cpp new file mode 100644 index 00000000..02c56206 --- /dev/null +++ b/src/LedSettingsService.cpp @@ -0,0 +1,16 @@ +#include + +LedSettingsService::LedSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : + _httpEndpoint(LedSettings::read, + LedSettings::update, + this, + server, + LED_SETTINGS_SERVICE_PATH, + securityManager, + AuthenticationPredicates::IS_AUTHENTICATED), + _fsPersistence(LedSettings::read, LedSettings::update, this, fs, LED_SETTINGS_FILE) { +} + +void LedSettingsService::begin() { + _fsPersistence.readFromFS(); +} diff --git a/src/LedSettingsService.h b/src/LedSettingsService.h new file mode 100644 index 00000000..421433b3 --- /dev/null +++ b/src/LedSettingsService.h @@ -0,0 +1,54 @@ +#ifndef LedSettingsService_h +#define LedSettingsService_h + +#define LED_SETTINGS_FILE "/config/ledSettings.json" +#define LED_SETTINGS_SERVICE_PATH "/rest/ledSettings" + +#include +#include +#include + +#ifndef FACTORY_LED_MAX_POWER_MILLIWATTS +#define FACTORY_LED_MAX_POWER_MILLIWATTS 0 +#endif + +#ifndef FACTORY_LED_SMOOTHING_FACTOR +#define FACTORY_LED_SMOOTHING_FACTOR 0.15 +#endif + +#ifndef FACTORY_LED_DEAD_ZONE +#define FACTORY_LED_DEAD_ZONE 700 +#endif + +class LedSettings { + public: + uint32_t maxPowerMilliwatts; + uint16_t deadZone; + float smoothingFactor; + + static void read(LedSettings& settings, JsonObject& root) { + root["max_power_milliwatts"] = settings.maxPowerMilliwatts; + root["dead_zone"] = settings.deadZone; + root["smoothing_factor"] = settings.smoothingFactor; + } + + static StateUpdateResult update(JsonObject& root, LedSettings& settings) { + settings.maxPowerMilliwatts = root["max_power_milliwatts"] | FACTORY_LED_MAX_POWER_MILLIWATTS; + settings.deadZone = root["dead_zone"] | FACTORY_LED_DEAD_ZONE; + settings.smoothingFactor = root["smoothing_factor"] | FACTORY_LED_SMOOTHING_FACTOR; + return StateUpdateResult::CHANGED; + } +}; + +class LedSettingsService : public StatefulService { + public: + LedSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); + + void begin(); + + private: + HttpEndpoint _httpEndpoint; + FSPersistence _fsPersistence; +}; + +#endif // end LedSettingsService_h diff --git a/src/LightMqttSettingsService.cpp b/src/LightMqttSettingsService.cpp deleted file mode 100644 index bddd9098..00000000 --- a/src/LightMqttSettingsService.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include - -LightMqttSettingsService::LightMqttSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : - _httpEndpoint(LightMqttSettings::read, - LightMqttSettings::update, - this, - server, - LIGHT_BROKER_SETTINGS_PATH, - securityManager, - AuthenticationPredicates::IS_AUTHENTICATED), - _fsPersistence(LightMqttSettings::read, LightMqttSettings::update, this, fs, LIGHT_BROKER_SETTINGS_FILE) { -} - -void LightMqttSettingsService::begin() { - _fsPersistence.readFromFS(); -} diff --git a/src/LightMqttSettingsService.h b/src/LightMqttSettingsService.h deleted file mode 100644 index 23a22181..00000000 --- a/src/LightMqttSettingsService.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef LightMqttSettingsService_h -#define LightMqttSettingsService_h - -#include -#include -#include - -#define LIGHT_BROKER_SETTINGS_FILE "/config/brokerSettings.json" -#define LIGHT_BROKER_SETTINGS_PATH "/rest/brokerSettings" - -class LightMqttSettings { - public: - String mqttPath; - String name; - String uniqueId; - - static void read(LightMqttSettings& settings, JsonObject& root) { - root["mqtt_path"] = settings.mqttPath; - root["name"] = settings.name; - root["unique_id"] = settings.uniqueId; - } - - static StateUpdateResult update(JsonObject& root, LightMqttSettings& settings) { - settings.mqttPath = root["mqtt_path"] | ESPUtils::defaultDeviceValue("homeassistant/light/"); - settings.name = root["name"] | ESPUtils::defaultDeviceValue("light-"); - settings.uniqueId = root["unique_id"] | ESPUtils::defaultDeviceValue("light-"); - return StateUpdateResult::CHANGED; - } -}; - -class LightMqttSettingsService : public StatefulService { - public: - LightMqttSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); - void begin(); - - private: - HttpEndpoint _httpEndpoint; - FSPersistence _fsPersistence; -}; - -#endif // end LightMqttSettingsService_h diff --git a/src/LightStateService.cpp b/src/LightStateService.cpp deleted file mode 100644 index 81696222..00000000 --- a/src/LightStateService.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#include - -LightStateService::LightStateService(AsyncWebServer* server, - SecurityManager* securityManager, - AsyncMqttClient* mqttClient, - LightMqttSettingsService* lightMqttSettingsService) : - _httpEndpoint(LightState::read, - LightState::update, - this, - server, - LIGHT_SETTINGS_ENDPOINT_PATH, - securityManager, - AuthenticationPredicates::IS_AUTHENTICATED), - _mqttPubSub(LightState::haRead, LightState::haUpdate, this, mqttClient), - _webSocket(LightState::read, - LightState::update, - this, - server, - LIGHT_SETTINGS_SOCKET_PATH, - securityManager, - AuthenticationPredicates::IS_AUTHENTICATED), - _mqttClient(mqttClient), - _lightMqttSettingsService(lightMqttSettingsService) { - // configure led to be output - pinMode(LED_PIN, OUTPUT); - - // configure MQTT callback - _mqttClient->onConnect(std::bind(&LightStateService::registerConfig, this)); - - // configure update handler for when the light settings change - _lightMqttSettingsService->addUpdateHandler([&](const String& originId) { registerConfig(); }, false); - - // configure settings service update handler to update LED state - addUpdateHandler([&](const String& originId) { onConfigUpdated(); }, false); -} - -void LightStateService::begin() { - _state.ledOn = DEFAULT_LED_STATE; - onConfigUpdated(); -} - -void LightStateService::onConfigUpdated() { - digitalWrite(LED_PIN, _state.ledOn ? LED_ON : LED_OFF); -} - -void LightStateService::registerConfig() { - if (!_mqttClient->connected()) { - return; - } - String configTopic; - String subTopic; - String pubTopic; - - DynamicJsonDocument doc(256); - _lightMqttSettingsService->read([&](LightMqttSettings& settings) { - configTopic = settings.mqttPath + "/config"; - subTopic = settings.mqttPath + "/set"; - pubTopic = settings.mqttPath + "/state"; - doc["~"] = settings.mqttPath; - doc["name"] = settings.name; - doc["unique_id"] = settings.uniqueId; - }); - doc["cmd_t"] = "~/set"; - doc["stat_t"] = "~/state"; - doc["schema"] = "json"; - doc["brightness"] = false; - - String payload; - serializeJson(doc, payload); - _mqttClient->publish(configTopic.c_str(), 0, false, payload.c_str()); - - _mqttPubSub.configureTopics(pubTopic, subTopic); -} diff --git a/src/LightStateService.h b/src/LightStateService.h deleted file mode 100644 index e9c9a9cd..00000000 --- a/src/LightStateService.h +++ /dev/null @@ -1,88 +0,0 @@ -#ifndef LightStateService_h -#define LightStateService_h - -#include - -#include -#include -#include - -#define LED_PIN 2 -#define PRINT_DELAY 5000 - -#define DEFAULT_LED_STATE false -#define OFF_STATE "OFF" -#define ON_STATE "ON" - -// Note that the built-in LED is on when the pin is low on most NodeMCU boards. -// This is because the anode is tied to VCC and the cathode to the GPIO 4 (Arduino pin 2). -#ifdef ESP32 -#define LED_ON 0x1 -#define LED_OFF 0x0 -#elif defined(ESP8266) -#define LED_ON 0x0 -#define LED_OFF 0x1 -#endif - -#define LIGHT_SETTINGS_ENDPOINT_PATH "/rest/lightState" -#define LIGHT_SETTINGS_SOCKET_PATH "/ws/lightState" - -class LightState { - public: - bool ledOn; - - static void read(LightState& settings, JsonObject& root) { - root["led_on"] = settings.ledOn; - } - - static StateUpdateResult update(JsonObject& root, LightState& lightState) { - boolean newState = root["led_on"] | DEFAULT_LED_STATE; - if (lightState.ledOn != newState) { - lightState.ledOn = newState; - return StateUpdateResult::CHANGED; - } - return StateUpdateResult::UNCHANGED; - } - - static void haRead(LightState& settings, JsonObject& root) { - root["state"] = settings.ledOn ? ON_STATE : OFF_STATE; - } - - static StateUpdateResult haUpdate(JsonObject& root, LightState& lightState) { - String state = root["state"]; - // parse new led state - boolean newState = false; - if (state.equals(ON_STATE)) { - newState = true; - } else if (!state.equals(OFF_STATE)) { - return StateUpdateResult::ERROR; - } - // change the new state, if required - if (lightState.ledOn != newState) { - lightState.ledOn = newState; - return StateUpdateResult::CHANGED; - } - return StateUpdateResult::UNCHANGED; - } -}; - -class LightStateService : public StatefulService { - public: - LightStateService(AsyncWebServer* server, - SecurityManager* securityManager, - AsyncMqttClient* mqttClient, - LightMqttSettingsService* lightMqttSettingsService); - void begin(); - - private: - HttpEndpoint _httpEndpoint; - MqttPubSub _mqttPubSub; - WebSocketTxRx _webSocket; - AsyncMqttClient* _mqttClient; - LightMqttSettingsService* _lightMqttSettingsService; - - void registerConfig(); - void onConfigUpdated(); -}; - -#endif diff --git a/src/LightningMode.cpp b/src/LightningMode.cpp new file mode 100644 index 00000000..ffc9c303 --- /dev/null +++ b/src/LightningMode.cpp @@ -0,0 +1,73 @@ +#include + +LightningMode::LightningMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler) : + AudioLightModeImpl(server, + fs, + securityManager, + paletteSettingsService, + frequencySampler, + LightningModeSettings::read, + LightningModeSettings::update, + LIGHTNING_MODE_ID) { + addUpdateHandler([&](const String& originId) { enable(); }, false); +}; + +void LightningMode::tick(CRGB* leds, const uint16_t numLeds) { + if (_refresh) { + _status = Status::IDLE; // assert idle mode + fill_solid(leds, numLeds, CHSV(255, 0, 0)); // clear leds + FastLED.show(); // render all leds black + _refresh = false; // clear refresh flag + return; // eager return + } + + if (_status == Status::TRIGGERED) { + _ledstart = random8(numLeds); // Determine starting location of flash + _ledlen = random8(numLeds - _ledstart); // Determine length of flash (not to go beyond numLeds-1) + _numFlashes = random8(3, _state.flashes); // Calculate the random number of flashes we will show + _flashCounter = 0; // The number of flashes we have shown + _status = Status::RUNNING; + } + + if (_status == Status::RUNNING && isWaitTimeElapsed()) { + _dimmer = (_flashCounter == 0) ? 5 : random8(1, 3); // leader scaled by a 5, return strokes brighter + fill_solid(leds + _ledstart, _ledlen, _state.color); // draw the flash + FastLED.show(_state.brightness / _dimmer); // show the flash + delay(random8(4, 10)); // wait a small amount of time (use non blocking delay?) + fill_solid(leds + _ledstart, _ledlen, CHSV(255, 0, 0)); // hide flash + FastLED.show(); // draw hidden leds + resetWaitTime(); // reset wait time for next flash + + // decrement flash counter, and reset to idle if done + if (++_flashCounter >= _numFlashes) { + _status = Status::IDLE; + } + } +} + +void LightningMode::enable() { + _refresh = true; +} + +void LightningMode::sampleComplete() { + if (_status == Status::IDLE) { + FrequencyData* frequencyData = _frequencySampler->getFrequencyData(); + if (frequencyData->calculateEnergyByte(_state.includedBands) >= _state.threshold) { + _status = Status::TRIGGERED; + } + } +} + +bool LightningMode::isWaitTimeElapsed() { + unsigned long waitTimeElapsed = (unsigned long)(millis() - _waitStartedAt); + return waitTimeElapsed >= _waitDuration; +} + +void LightningMode::resetWaitTime() { + _waitStartedAt = millis(); // mark current time for the non blocking delay + _waitDuration = (_flashCounter == 0 ? 200 : 50) + random8(100); // set wait, lead flash longer (make configurable) +} diff --git a/src/LightningMode.h b/src/LightningMode.h new file mode 100644 index 00000000..a78bf2bf --- /dev/null +++ b/src/LightningMode.h @@ -0,0 +1,88 @@ +#ifndef LightningMode_h +#define LightningMode_h + +#include +#include + +#ifndef FACTORY_LIGHTNING_MODE_COLOR +#define FACTORY_LIGHTNING_MODE_COLOR CRGB::White +#endif + +#ifndef FACTORY_LIGHTNING_MODE_FLASHES +#define FACTORY_LIGHTNING_MODE_FLASHES 8 +#endif + +#ifndef FACTORY_LIGHTNING_MODE_THRESHOLD +#define FACTORY_LIGHTNING_MODE_THRESHOLD 128 +#endif + +#ifndef FACTORY_LIGHTNING_MODE_BRIGHTNESS +#define FACTORY_LIGHTNING_MODE_BRIGHTNESS 255 +#endif + +#ifndef FACTORY_LIGHTNING_AUDIO_ENABLED +#define FACTORY_LIGHTNING_AUDIO_ENABLED false +#endif + +#define LIGHTNING_MODE_ID "lightning" + +class LightningModeSettings { + public: + CRGB color; + uint8_t brightness; + uint8_t threshold; + uint8_t flashes; + bool audioEnabled; + bool includedBands[NUM_BANDS]; + + static void read(LightningModeSettings& settings, JsonObject& root) { + writeColorToJson(root, &settings.color, "color"); + writeByteToJson(root, &settings.brightness, "brightness"); + writeByteToJson(root, &settings.threshold, "threshold"); + writeByteToJson(root, &settings.flashes, "flashes"); + writeBoolToJson(root, &settings.audioEnabled, "audio_enabled"); + writeBooleanArrayToJson(root, settings.includedBands, NUM_BANDS, "included_bands"); + } + + static StateUpdateResult update(JsonObject& root, LightningModeSettings& settings) { + updateColorFromJson(root, &settings.color, FACTORY_LIGHTNING_MODE_COLOR, "color"); + updateByteFromJson(root, &settings.brightness, FACTORY_LIGHTNING_MODE_BRIGHTNESS, "brightness"); + updateByteFromJson(root, &settings.threshold, FACTORY_LIGHTNING_MODE_THRESHOLD, "threshold"); + updateByteFromJson(root, &settings.flashes, FACTORY_LIGHTNING_MODE_FLASHES, "flashes"); + updateBoolFromJson(root, &settings.audioEnabled, FACTORY_LIGHTNING_AUDIO_ENABLED, "audio_enabled"); + updateBooleanArrayFromJson(root, settings.includedBands, NUM_BANDS, true, "included_bands"); + return StateUpdateResult::CHANGED; + } +}; + +class LightningMode : public AudioLightModeImpl { + private: + enum class Status { IDLE, TRIGGERED, RUNNING }; + + // State variables + Status _status = Status::IDLE; + uint8_t _ledstart; // Starting location of a flash + uint8_t _ledlen; // Length of a flash + uint16_t _dimmer = 1; // State for dimming initial flash by a factor + uint8_t _flashCounter = 0; // Keeps track of how many flashes we have done + uint8_t _numFlashes = 0; // Keeps track of how many flashes we will do + bool _refresh = true; // For applying config updates or enabling the mode + + // Pause tracking + unsigned long _waitStartedAt; + uint16_t _waitDuration; + + public: + LightningMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler); + void tick(CRGB* leds, const uint16_t numLeds); + void enable(); + void sampleComplete(); + bool isWaitTimeElapsed(); + void resetWaitTime(); +}; + +#endif diff --git a/src/OffMode.cpp b/src/OffMode.cpp new file mode 100644 index 00000000..e5d6b69c --- /dev/null +++ b/src/OffMode.cpp @@ -0,0 +1,31 @@ +#include + +OffMode::OffMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler) : + AudioLightModeImpl(server, + fs, + securityManager, + paletteSettingsService, + frequencySampler, + OffModeSettings::read, + OffModeSettings::update, + OFF_MODE_ID){}; + +void OffMode::enable() { + _refresh = true; +} + +void OffMode::tick(CRGB* leds, const uint16_t numLeds) { + if (_refresh) { + fill_solid(leds, numLeds, CHSV(255, 0, 0)); // clear leds + FastLED.show(); // render all leds black + _refresh = false; // clear refresh flag + } +} + +bool OffMode::canRotate() { + return false; +}; diff --git a/src/OffMode.h b/src/OffMode.h new file mode 100644 index 00000000..1c679d79 --- /dev/null +++ b/src/OffMode.h @@ -0,0 +1,33 @@ +#ifndef OffMode_h +#define OffMode_h + +#include +#include + +#define OFF_MODE_ID "off" + +class OffModeSettings { + public: + static void read(OffModeSettings& settings, JsonObject& root) { + } + + static StateUpdateResult update(JsonObject& root, OffModeSettings& settings) { + return StateUpdateResult::CHANGED; + } +}; + +class OffMode : public AudioLightModeImpl { + private: + bool _refresh = true; // For applying config updates or enabling the mode + public: + OffMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler); + void tick(CRGB* leds, const uint16_t numLeds); + void enable(); + bool canRotate(); +}; + +#endif diff --git a/src/PacificaMode.cpp b/src/PacificaMode.cpp new file mode 100644 index 00000000..41f6151f --- /dev/null +++ b/src/PacificaMode.cpp @@ -0,0 +1,125 @@ +#include + +PacificaMode::PacificaMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler) : + AudioLightModeImpl(server, + fs, + securityManager, + paletteSettingsService, + frequencySampler, + std::bind(&PacificaMode::read, this, std::placeholders::_1, std::placeholders::_2), + std::bind(&PacificaMode::update, this, std::placeholders::_1, std::placeholders::_2), + PACIFICA_MODE_ID) { + addUpdateHandler([&](const String& originId) { enable(); }, false); +}; + +void PacificaMode::enable() { + _refresh = true; + updateWithoutPropagation([&](PacificaModeSettings& settings) { + _paletteSettingsService->refreshPalette(settings.palette1); + _paletteSettingsService->refreshPalette(settings.palette2); + _paletteSettingsService->refreshPalette(settings.palette3); + return StateUpdateResult::CHANGED; + }); +} + +void PacificaMode::tick(CRGB* leds, const uint16_t numLeds) { + // Increment the four "color index start" counters, one for each wave layer. + // Each is incremented at a different speed, and the speeds vary over time. + static uint16_t sCIStart1, sCIStart2, sCIStart3, sCIStart4; + static uint32_t sLastms = 0; + uint32_t ms = millis(); + uint32_t deltams = ms - sLastms; + sLastms = ms; + uint16_t speedfactor1 = beatsin16(3, 179, 269); + uint16_t speedfactor2 = beatsin16(4, 179, 269); + uint32_t deltams1 = (deltams * speedfactor1) / 256; + uint32_t deltams2 = (deltams * speedfactor2) / 256; + uint32_t deltams21 = (deltams1 + deltams2) / 2; + sCIStart1 += (deltams1 * beatsin88(1011, 10, 13)); + sCIStart2 -= (deltams21 * beatsin88(777, 8, 11)); + sCIStart3 -= (deltams1 * beatsin88(501, 5, 7)); + sCIStart4 -= (deltams2 * beatsin88(257, 4, 6)); + + // Clear out the LED array to a dim background blue-green + fill_solid(leds, numLeds, CRGB(2, 6, 10)); + + // Render each of four layers, with different scales and speeds, that vary over time + pacifica_one_layer(leds, + numLeds, + _state.palette1.colors, + sCIStart1, + beatsin16(3, 11 * 256, 14 * 256), + beatsin8(10, 70, 130), + 0 - beat16(301)); + pacifica_one_layer(leds, + numLeds, + _state.palette2.colors, + sCIStart2, + beatsin16(4, 6 * 256, 9 * 256), + beatsin8(17, 40, 80), + beat16(401)); + pacifica_one_layer(leds, numLeds, _state.palette3.colors, sCIStart3, 6 * 256, beatsin8(9, 10, 38), 0 - beat16(503)); + pacifica_one_layer(leds, numLeds, _state.palette3.colors, sCIStart4, 5 * 256, beatsin8(8, 10, 28), beat16(601)); + + // Add brighter 'whitecaps' where the waves lines up more + pacifica_add_whitecaps(leds, numLeds); + + // Deepen the blues and greens a bit + pacifica_deepen_colors(leds, numLeds); + + // Show the leds + FastLED.show(); +} + +// Add one layer of waves into the led array +void PacificaMode::pacifica_one_layer(CRGB* leds, + const uint16_t numLeds, + CRGBPalette16& p, + uint16_t cistart, + uint16_t wavescale, + uint8_t bri, + uint16_t ioff) { + uint16_t ci = cistart; + uint16_t waveangle = ioff; + uint16_t wavescale_half = (wavescale / 2) + 20; + for (uint16_t i = 0; i < numLeds; i++) { + waveangle += 250; + uint16_t s16 = sin16(waveangle) + 32768; + uint16_t cs = scale16(s16, wavescale_half) + wavescale_half; + ci += cs; + uint16_t sindex16 = sin16(ci) + 32768; + uint8_t sindex8 = scale16(sindex16, 240); + CRGB c = ColorFromPalette(p, sindex8, bri, LINEARBLEND); + leds[i] += c; + } +} + +// Add extra 'white' to areas where the four layers of light have lined up brightly +void PacificaMode::pacifica_add_whitecaps(CRGB* leds, const uint16_t numLeds) { + uint8_t basethreshold = beatsin8(9, 55, 65); + uint8_t wave = beat8(7); + + for (uint16_t i = 0; i < numLeds; i++) { + uint8_t threshold = scale8(sin8(wave), 20) + basethreshold; + wave += 7; + uint8_t l = leds[i].getAverageLight(); + if (l > threshold) { + uint8_t overage = l - threshold; + uint8_t overage2 = qadd8(overage, overage); + leds[i] += CRGB(overage, overage2, qadd8(overage2, overage2)); + } + } +} + +// Deepen the blues and greens +void PacificaMode::pacifica_deepen_colors(CRGB* leds, const uint16_t numLeds) { + for (uint16_t i = 0; i < numLeds; i++) { + leds[i].blue = scale8(leds[i].blue, 145); + leds[i].green = scale8(leds[i].green, 200); + leds[i] |= CRGB(2, 5, 7); + } +} diff --git a/src/PacificaMode.h b/src/PacificaMode.h new file mode 100644 index 00000000..db7e7f7e --- /dev/null +++ b/src/PacificaMode.h @@ -0,0 +1,66 @@ +#ifndef PacificaMode_h +#define PacificaMode_h + +#include +#include +#include + +#ifndef FACTORY_PACIFICA_MODE_PALETTE1 +#define FACTORY_PACIFICA_MODE_PALETTE1 "Pacifica 1" +#endif + +#ifndef FACTORY_PACIFICA_MODE_PALETTE2 +#define FACTORY_PACIFICA_MODE_PALETTE2 "Pacifica 2" +#endif + +#ifndef FACTORY_PACIFICA_MODE_PALETTE3 +#define FACTORY_PACIFICA_MODE_PALETTE3 "Pacifica 3" +#endif + +#define PACIFICA_MODE_ID "pacifica" + +class PacificaModeSettings { + public: + Palette palette1; + Palette palette2; + Palette palette3; +}; + +class PacificaMode : public AudioLightModeImpl { + private: + boolean _refresh = true; + + void read(PacificaModeSettings& settings, JsonObject& root) { + root["palette1"] = settings.palette1.name; + root["palette2"] = settings.palette2.name; + root["palette3"] = settings.palette3.name; + } + + StateUpdateResult update(JsonObject& root, PacificaModeSettings& settings) { + settings.palette1 = _paletteSettingsService->getPalette(root["palette1"] | FACTORY_PACIFICA_MODE_PALETTE1); + settings.palette2 = _paletteSettingsService->getPalette(root["palette2"] | FACTORY_PACIFICA_MODE_PALETTE2); + settings.palette3 = _paletteSettingsService->getPalette(root["palette3"] | FACTORY_PACIFICA_MODE_PALETTE3); + return StateUpdateResult::CHANGED; + } + + void pacifica_one_layer(CRGB* leds, + const uint16_t numLeds, + CRGBPalette16& p, + uint16_t cistart, + uint16_t wavescale, + uint8_t bri, + uint16_t ioff); + void pacifica_deepen_colors(CRGB* leds, const uint16_t numLeds); + void pacifica_add_whitecaps(CRGB* leds, const uint16_t numLeds); + + public: + PacificaMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler); + void tick(CRGB* leds, const uint16_t numLeds); + void enable(); +}; + +#endif diff --git a/src/PaletteSettingsService.h b/src/PaletteSettingsService.h new file mode 100644 index 00000000..26a1a862 --- /dev/null +++ b/src/PaletteSettingsService.h @@ -0,0 +1,160 @@ +#ifndef PaletteSettingsService_h +#define PaletteSettingsService_h + +#include +#include +#include +#include +#include +#include + +#define PALETTE_SETTINGS_FILE "/config/paletteSettings.json" +#define PALETTE_SETTINGS_SERVICE_PATH "/rest/paletteSettings" + +const TProgmemRGBPalette16 PacificaColors1_p FL_PROGMEM = {0x000507, + 0x000409, + 0x00030B, + 0x00030D, + 0x000210, + 0x000212, + 0x000114, + 0x000117, + 0x000019, + 0x00001C, + 0x000026, + 0x000031, + 0x00003B, + 0x000046, + 0x14554B, + 0x28AA50}; + +const TProgmemRGBPalette16 PacificaColors2_p FL_PROGMEM = {0x000507, + 0x000409, + 0x00030B, + 0x00030D, + 0x000210, + 0x000212, + 0x000114, + 0x000117, + 0x000019, + 0x00001C, + 0x000026, + 0x000031, + 0x00003B, + 0x000046, + 0x0C5F52, + 0x19BE5F}; + +const TProgmemRGBPalette16 PacificaColors3_p FL_PROGMEM = {0x000208, + 0x00030E, + 0x000514, + 0x00061A, + 0x000820, + 0x000927, + 0x000B2D, + 0x000C33, + 0x000E39, + 0x001040, + 0x001450, + 0x001860, + 0x001C70, + 0x002080, + 0x1040BF, + 0x2060FF}; + +class Palette { + public: + String name; + CRGBPalette16 colors; + + // the default palette - rainbow + Palette() : name("Rainbow"), colors(RainbowColors_p) { + } + + // custom palettes + Palette(String name, CRGBPalette16 colors) : name(name), colors(colors) { + } +}; + +class PaletteSettings { + public: + std::list palettes; + + static void read(PaletteSettings& settings, JsonObject& root) { + JsonArray palettes = root.createNestedArray("palettes"); + for (Palette palette : settings.palettes) { + JsonObject paletteRoot = palettes.createNestedObject(); + paletteRoot["name"] = palette.name; + writePaletteToJson(paletteRoot, &palette.colors); + } + } + + static StateUpdateResult update(JsonObject& root, PaletteSettings& settings) { + settings.palettes.clear(); + if (root["palettes"].is()) { + for (JsonObject paletteRoot : root["palettes"].as()) { + String name = paletteRoot["name"]; + if (!name.isEmpty()) { + Palette palette = Palette(); + palette.name = name; + updatePaletteFromJson(paletteRoot, &palette.colors, CRGB::Black); + settings.palettes.push_back(palette); + } + } + } else { + settings.palettes.push_back(Palette()); + settings.palettes.push_back(Palette("Party", PartyColors_p)); + settings.palettes.push_back(Palette("Heat", HeatColors_p)); + settings.palettes.push_back(Palette("Rainbow Stripe", RainbowStripeColors_p)); + settings.palettes.push_back(Palette("Cloud", CloudColors_p)); + settings.palettes.push_back(Palette("Lava", LavaColors_p)); + settings.palettes.push_back(Palette("Ocean", OceanColors_p)); + settings.palettes.push_back(Palette("Forest", ForestColors_p)); + settings.palettes.push_back(Palette("Pacifica 1", PacificaColors1_p)); + settings.palettes.push_back(Palette("Pacifica 2", PacificaColors2_p)); + settings.palettes.push_back(Palette("Pacifica 3", PacificaColors3_p)); + } + return StateUpdateResult::CHANGED; + } +}; + +/** + * May be extended to support a palette editor in the UI. + */ +class PaletteSettingsService : public StatefulService { + private: + HttpEndpoint _httpEndpoint; + FSPersistence _fsPersistence; + + public: + PaletteSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : + _httpEndpoint(PaletteSettings::read, + PaletteSettings::update, + this, + server, + PALETTE_SETTINGS_SERVICE_PATH, + securityManager, + AuthenticationPredicates::IS_AUTHENTICATED, + 10240), + _fsPersistence(PaletteSettings::read, PaletteSettings::update, this, fs, PALETTE_SETTINGS_FILE, 10240) { + } + + void begin() { + _fsPersistence.readFromFS(); + } + + void refreshPalette(Palette& palette) { + palette = getPalette(palette.name); + } + + Palette getPalette(const String& name) { + for (Palette palette : _state.palettes) { + if (palette.name == name) { + return palette; + } + } + return Palette(); + } +}; + +#endif diff --git a/src/PrideMode.cpp b/src/PrideMode.cpp new file mode 100644 index 00000000..af9dc1e2 --- /dev/null +++ b/src/PrideMode.cpp @@ -0,0 +1,63 @@ +#include + +PrideMode::PrideMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler) : + AudioLightModeImpl(server, + fs, + securityManager, + paletteSettingsService, + frequencySampler, + PrideModeSettings::read, + PrideModeSettings::update, + PRIDE_MODE_ID) { + addUpdateHandler([&](const String& originId) { enable(); }, false); +}; + +void PrideMode::enable() { + _refresh = true; +} + +void PrideMode::tick(CRGB* leds, const uint16_t numLeds) { + static uint16_t sPseudotime = 0; + static uint16_t sLastMillis = 0; + static uint16_t sHue16 = 0; + + uint8_t sat8 = beatsin88(87, 220, 250); + uint8_t brightdepth = beatsin88(341, 96, 224); + uint16_t brightnessthetainc16 = beatsin88(203, (25 * 256), (40 * 256)); + uint8_t msmultiplier = beatsin88(_state.brightnessBpm, _state.brightnessFreqMin, _state.brightnessFreqMax); + + uint16_t hue16 = sHue16; // gHue * 256; + uint16_t hueinc16 = beatsin88(_state.hueBpm, _state.hueDeltaMin, _state.hueDeltaMax); + + uint16_t ms = millis(); + uint16_t deltams = ms - sLastMillis; + sLastMillis = ms; + sPseudotime += deltams * msmultiplier; + sHue16 += deltams * beatsin88(400, 5, 9); + uint16_t brightnesstheta16 = sPseudotime; + + for (uint16_t i = 0; i < numLeds; i++) { + hue16 += hueinc16; + uint8_t hue8 = hue16 / 256; + + brightnesstheta16 += brightnessthetainc16; + uint16_t b16 = sin16(brightnesstheta16) + 32768; + + uint16_t bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; + uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536; + bri8 += (255 - brightdepth); + + CRGB newcolor = CHSV(hue8, sat8, bri8); + + uint16_t pixelnumber = i; + pixelnumber = (numLeds - 1) - pixelnumber; + + nblend(leds[pixelnumber], newcolor, 64); + } + + FastLED.show(); +} diff --git a/src/PrideMode.h b/src/PrideMode.h new file mode 100644 index 00000000..68c772ff --- /dev/null +++ b/src/PrideMode.h @@ -0,0 +1,80 @@ +#ifndef PrideMode_h +#define PrideMode_h + +#include +#include +#include + +#define PRIDE_MODE_ID "pride" + +#ifndef FACTORY_PRIDE_MODE_BRIGHTNESS_MULTIPLIER_MIN +#define FACTORY_PRIDE_MODE_BRIGHTNESS_MULTIPLIER_MIN 23 +#endif + +#ifndef FACTORY_PRIDE_MODE_BRIGHTNESS_MULTIPLIER_MAX +#define FACTORY_PRIDE_MODE_BRIGHTNESS_MULTIPLIER_MAX 60 +#endif + +#ifndef FACTORY_PRIDE_MODE_BRIGHTNESS_BPM +#define FACTORY_PRIDE_MODE_BRIGHTNESS_BPM 147 +#endif + +#ifndef FACTORY_PRIDE_MODE_HUE_BPM +#define FACTORY_PRIDE_MODE_HUE_BPM 113 +#endif + +#ifndef FACTORY_PRIDE_MODE_HUDE_DELTA_MIN +#define FACTORY_PRIDE_MODE_HUDE_DELTA_MIN 1 +#endif + +#ifndef FACTORY_PRIDE_MODE_HUDE_DELTA_MAX +#define FACTORY_PRIDE_MODE_HUDE_DELTA_MAX 3000 +#endif + +class PrideModeSettings { + public: + uint8_t brightnessBpm; + uint8_t brightnessFreqMin; + uint8_t brightnessFreqMax; + + uint8_t hueBpm; + uint16_t hueDeltaMin; + uint16_t hueDeltaMax; + + static void read(PrideModeSettings& settings, JsonObject& root) { + writeByteToJson(root, &settings.brightnessBpm, "brightness_bpm"); + writeByteToJson(root, &settings.brightnessFreqMin, "brightness_freq_min"); + writeByteToJson(root, &settings.brightnessFreqMax, "brightness_freq_max"); + writeByteToJson(root, &settings.hueBpm, "hue_bpm"); + root["hue_delta_min"] = settings.hueDeltaMin; + root["hue_delta_max"] = settings.hueDeltaMax; + } + + static StateUpdateResult update(JsonObject& root, PrideModeSettings& settings) { + updateByteFromJson(root, &settings.brightnessBpm, FACTORY_PRIDE_MODE_BRIGHTNESS_BPM, "brightness_bpm"); + updateByteFromJson( + root, &settings.brightnessFreqMin, FACTORY_PRIDE_MODE_BRIGHTNESS_MULTIPLIER_MIN, "brightness_freq_min"); + updateByteFromJson( + root, &settings.brightnessFreqMax, FACTORY_PRIDE_MODE_BRIGHTNESS_MULTIPLIER_MAX, "brightness_freq_max"); + updateByteFromJson(root, &settings.hueBpm, FACTORY_PRIDE_MODE_HUE_BPM, "hue_bpm"); + settings.hueDeltaMin = root["hue_delta_min"] | FACTORY_PRIDE_MODE_HUDE_DELTA_MIN; + settings.hueDeltaMax = root["hue_delta_max"] | FACTORY_PRIDE_MODE_HUDE_DELTA_MAX; + return StateUpdateResult::CHANGED; + } +}; + +class PrideMode : public AudioLightModeImpl { + private: + boolean _refresh = true; + + public: + PrideMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler); + void tick(CRGB* leds, const uint16_t numLeds); + void enable(); +}; + +#endif diff --git a/src/RainbowMode.cpp b/src/RainbowMode.cpp new file mode 100644 index 00000000..76c5213f --- /dev/null +++ b/src/RainbowMode.cpp @@ -0,0 +1,53 @@ +#include + +RainbowMode::RainbowMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler) : + AudioLightModeImpl(server, + fs, + securityManager, + paletteSettingsService, + frequencySampler, + RainbowModeSettings::read, + RainbowModeSettings::update, + RAINBOW_MODE_ID) { + _ledsPerBand = NUM_LEDS / NUM_BANDS; + _remainingLeds = NUM_LEDS % NUM_BANDS; +}; + +void RainbowMode::enable() { + _lastFrameMicros = micros(); +} + +void RainbowMode::tick(CRGB* leds, const uint16_t numLeds) { + FrequencyData* frequencyData = _frequencySampler->getFrequencyData(); + + // rotate hue in time based manner + if (_state.rotateSpeed > 0) { + unsigned long rotateDelayMicros = 1000000 / _state.rotateSpeed; + unsigned long currentMicros = micros(); + unsigned long microsElapsed = (unsigned long)(currentMicros - _lastFrameMicros); + if (microsElapsed >= rotateDelayMicros) { + _lastFrameMicros = currentMicros; + _initialhue++; + } + } + + // fill the rainbow + fill_rainbow(leds, numLeds, _initialhue, _state.hueDelta); + + // fade each segment if audio-enabled + if (_state.audioEnabled) { + CRGB* startLed = leds; + for (uint8_t i = 0; i < NUM_BANDS; i++) { + uint16_t numLeds = _ledsPerBand + (i == NUM_BANDS - 1 ? _remainingLeds : 0); + fadeToBlackBy(startLed, numLeds, 255 - map(frequencyData->bands[i], 0, ADC_MAX_VALUE, 0, 255)); + startLed += _ledsPerBand; + } + } + + // update the leds + FastLED.show(_state.brightness); +} diff --git a/src/RainbowMode.h b/src/RainbowMode.h new file mode 100644 index 00000000..da54a436 --- /dev/null +++ b/src/RainbowMode.h @@ -0,0 +1,70 @@ +#ifndef RainbowMode_h +#define RainbowMode_h + +#include +#include + +#ifndef FACTORY_RAINBOW_MODE_HUE_DELTA +#define FACTORY_RAINBOW_MODE_HUE_DELTA 250 +#endif + +#ifndef FACTORY_RAINBOW_MODE_BRIGHTNESS +#define FACTORY_RAINBOW_MODE_BRIGHTNESS 128 +#endif + +#ifndef FACTORY_RAINBOW_MODE_ROTATE_SPEED +#define FACTORY_RAINBOW_MODE_ROTATE_SPEED 32 +#endif + +#ifndef FACTORY_RAINBOW_MODE_AUDIO_ENABLED +#define FACTORY_RAINBOW_MODE_AUDIO_ENABLED false +#endif + +#define RAINBOW_MODE_ID "rainbow" + +class RainbowModeSettings { + public: + uint8_t brightness; + uint8_t rotateSpeed; + bool audioEnabled; + uint8_t hueDelta; + + static void read(RainbowModeSettings& settings, JsonObject& root) { + writeByteToJson(root, &settings.brightness, "brightness"); + writeByteToJson(root, &settings.rotateSpeed, "rotate_speed"); + writeBoolToJson(root, &settings.audioEnabled, "audio_enabled"); + writeByteToJson(root, &settings.hueDelta, "hue_delta"); + } + + static StateUpdateResult update(JsonObject& root, RainbowModeSettings& settings) { + updateByteFromJson(root, &settings.brightness, FACTORY_RAINBOW_MODE_BRIGHTNESS, "brightness"); + updateByteFromJson(root, &settings.rotateSpeed, FACTORY_RAINBOW_MODE_ROTATE_SPEED, "rotate_speed"); + updateBoolFromJson(root, &settings.audioEnabled, FACTORY_RAINBOW_MODE_AUDIO_ENABLED, "audio_enabled"); + updateByteFromJson(root, &settings.hueDelta, FACTORY_RAINBOW_MODE_HUE_DELTA, "hue_delta"); + return StateUpdateResult::CHANGED; + } +}; + +class RainbowMode : public AudioLightModeImpl { + private: + // various state and settings for rainbow mode + uint16_t _ledsPerBand; + uint16_t _remainingLeds; + + // delay state for rotation + unsigned long _lastFrameMicros = 0; + + uint8_t _initialhue = 0; + boolean _refresh = true; + + public: + RainbowMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler); + void tick(CRGB* leds, const uint16_t numLeds); + void enable(); +}; + +#endif diff --git a/src/RotateMode.cpp b/src/RotateMode.cpp new file mode 100644 index 00000000..5896b303 --- /dev/null +++ b/src/RotateMode.cpp @@ -0,0 +1,94 @@ +#include + +RotateMode::RotateMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler, + ModeFetcher modeFetcher) : + AudioLightModeImpl(server, + fs, + securityManager, + paletteSettingsService, + frequencySampler, + std::bind(&RotateMode::read, this, std::placeholders::_1, std::placeholders::_2), + std::bind(&RotateMode::update, this, std::placeholders::_1, std::placeholders::_2), + ROTATE_MODE_ID), + _modeFetcher(modeFetcher) { + addUpdateHandler([&](const String& originId) { enable(); }, false); +}; + +void RotateMode::read(RotateModeSettings& settings, JsonObject& root) { + JsonArray modes = root.createNestedArray("modes"); + for (String mode : settings.modes) { + modes.add(mode); + } + root["delay"] = settings.delay; +} + +StateUpdateResult RotateMode::update(JsonObject& root, RotateModeSettings& settings) { + settings.modes.clear(); + if (root["modes"].is()) { + for (String mode : root["modes"].as()) { + AudioLightMode* candidateMode = _modeFetcher(mode); + if (candidateMode && candidateMode->canRotate()) { + settings.modes.push_back(mode); + } + } + } + settings.delay = root["delay"] | FACTORY_ROTATE_MODE_DELAY; + return StateUpdateResult::CHANGED; +} + +void RotateMode::enable() { + _refresh = true; +} + +void RotateMode::tick(CRGB* leds, const uint16_t numLeds) { + unsigned long currentMillis = millis(); + + // refresh if we are required to + if (_refresh) { + selectMode(0, leds, numLeds); + _modeChangedAt = millis(); + _refresh = false; + } + + // hand off to the selected mode to do its thing + if (_selectedMode) { + _selectedMode->tick(leds, numLeds); + } + + // change mode if it's time + if ((unsigned long)(currentMillis - _modeChangedAt) >= _state.delay) { + selectMode(_currentMode + 1, leds, numLeds); + _modeChangedAt = millis(); + } +} + +void RotateMode::selectMode(uint8_t newMode, CRGB* leds, const uint16_t numLeds) { + AudioLightMode* nextMode = nullptr; + // select the next mode + if (_state.modes.size() > 0) { + // increment mode, resetting to zero if we've gone past the end of the array + _currentMode = newMode < _state.modes.size() ? newMode : 0; + // find the next mode + nextMode = _modeFetcher(*std::next(_state.modes.begin(), _currentMode)); + } + // activate the next mode if it's differnt from the selected mode + if (nextMode != _selectedMode || _refresh) { + _selectedMode = nextMode; + if (_selectedMode) { + // new mode selected, enable it + _selectedMode->enable(); + } else { + // no mode selected, clear LEDs + fill_solid(leds, numLeds, CHSV(255, 0, 0)); // clear leds + FastLED.show(); // render all leds black + } + } +} + +bool RotateMode::canRotate() { + return false; +}; diff --git a/src/RotateMode.h b/src/RotateMode.h new file mode 100644 index 00000000..d98af3d4 --- /dev/null +++ b/src/RotateMode.h @@ -0,0 +1,46 @@ +#ifndef RotateMode_h +#define RotateMode_h + +#include +#include +#include + +#ifndef FACTORY_ROTATE_MODE_DELAY +#define FACTORY_ROTATE_MODE_DELAY 30000 +#endif + +#define ROTATE_MODE_ID "rotate" + +using ModeFetcher = std::function; + +class RotateModeSettings { + public: + std::list modes; + uint32_t delay; +}; + +class RotateMode : public AudioLightModeImpl { + private: + boolean _refresh = true; + uint8_t _currentMode = 0; + unsigned long _modeChangedAt = 0; + ModeFetcher _modeFetcher; + AudioLightMode* _selectedMode; + + void read(RotateModeSettings& settings, JsonObject& root); + StateUpdateResult update(JsonObject& root, RotateModeSettings& settings); + void selectMode(uint8_t newMode, CRGB* leds, const uint16_t numLeds); + + public: + RotateMode(AsyncWebServer* server, + FS* fs, + SecurityManager* securityManager, + PaletteSettingsService* paletteSettingsService, + FrequencySampler* frequencySampler, + ModeFetcher modeFetcher); + void tick(CRGB* leds, const uint16_t numLeds); + void enable(); + bool canRotate(); +}; + +#endif diff --git a/src/main.cpp b/src/main.cpp index 0b1081a4..b63ceaa9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,17 +1,24 @@ #include -#include -#include +#include +#include +#include +#include #define SERIAL_BAUD_RATE 115200 AsyncWebServer server(80); ESP8266React esp8266React(&server); -LightMqttSettingsService lightMqttSettingsService = - LightMqttSettingsService(&server, esp8266React.getFS(), esp8266React.getSecurityManager()); -LightStateService lightStateService = LightStateService(&server, - esp8266React.getSecurityManager(), - esp8266React.getMqttClient(), - &lightMqttSettingsService); + +LedSettingsService ledSettingsService(&server, esp8266React.getFS(), esp8266React.getSecurityManager()); +PaletteSettingsService paletteSettingsService(&server, esp8266React.getFS(), esp8266React.getSecurityManager()); +FrequencySampler frequencySampler(&ledSettingsService); +FrequencyTransmitter frequencyTransmitter(&server, esp8266React.getSecurityManager(), &frequencySampler); +AudioLightSettingsService audioLightSettingsService(&server, + esp8266React.getFS(), + esp8266React.getSecurityManager(), + &ledSettingsService, + &paletteSettingsService, + &frequencySampler); void setup() { // start serial and filesystem @@ -20,11 +27,17 @@ void setup() { // start the framework and demo project esp8266React.begin(); - // load the initial light settings - lightStateService.begin(); + // load general settings + ledSettingsService.begin(); + + // set up the sampler + frequencySampler.begin(); + + // load the palettes + paletteSettingsService.begin(); - // start the light service - lightMqttSettingsService.begin(); + // load all of the defaults + audioLightSettingsService.begin(); // start the server server.begin(); @@ -33,4 +46,10 @@ void setup() { void loop() { // run the framework's loop function esp8266React.loop(); + + // allow the sampler to run if needed + frequencySampler.loop(); + + // refresh the LEDs + audioLightSettingsService.loop(); }