diff --git a/ESP source.code-workspace b/ESP source.code-workspace new file mode 100644 index 0000000..1c9b3a9 --- /dev/null +++ b/ESP source.code-workspace @@ -0,0 +1,63 @@ +{ + "folders": [ + { + "path": ".", + "name": "ESP source" + }, + ], + "settings": { + "files.exclude": { + "**/.git": true, + "WebConfigUI": true + }, + "files.associations": { + "array": "cpp", + "deque": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "vector": "cpp", + "initializer_list": "cpp", + "functional": "cpp", + "bitset": "cpp", + "regex": "cpp", + "utility": "cpp", + "*.tcc": "cpp", + "cmath": "cpp", + "cstdio": "cpp", + "new": "cpp", + "algorithm": "cpp", + "random": "cpp", + "cctype": "cpp", + "clocale": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "exception": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "fstream": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "limits": "cpp", + "memory": "cpp", + "ostream": "cpp", + "numeric": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "cinttypes": "cpp", + "typeinfo": "cpp" + }, + "editor.detectIndentation": false, + "editor.insertSpaces": false + } +} \ No newline at end of file diff --git a/WebConfigUI/.gitignore b/WebConfigUI/.gitignore new file mode 100644 index 0000000..a7ca8fd --- /dev/null +++ b/WebConfigUI/.gitignore @@ -0,0 +1,3 @@ +node_modules/** +build/** +package-lock.json \ No newline at end of file diff --git a/WebConfigUI/.vscode/tasks.json b/WebConfigUI/.vscode/tasks.json new file mode 100644 index 0000000..11a0f08 --- /dev/null +++ b/WebConfigUI/.vscode/tasks.json @@ -0,0 +1,28 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "build", + "label": "npm: build", + "detail": "set NODE_ENV=production && set PURGE_FILES=1 && webpack --mode production", + }, + { + "type": "shell", + "label": "Build and copy", + "command": "copy", + "dependsOn": "npm: build", + "dependsOrder": "sequence", + "options": { + "cwd": "${workspaceFolder}" + }, + "args": [ + "${workspaceFolder}\\build\\index.js.gz", + "\"${workspaceFolder}\\../data/index.js.gz\"", + "/Y" + ], + "group": "build", + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/WebConfigUI/WebUI.code-workspace b/WebConfigUI/WebUI.code-workspace new file mode 100644 index 0000000..df281b1 --- /dev/null +++ b/WebConfigUI/WebUI.code-workspace @@ -0,0 +1,13 @@ +{ + "folders": [ + { + "path": ".", + "name": "Web UI" + } + ], + "settings": { + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 2 + } +} \ No newline at end of file diff --git a/WebConfigUI/babel.config.json b/WebConfigUI/babel.config.json new file mode 100644 index 0000000..b5c4c08 --- /dev/null +++ b/WebConfigUI/babel.config.json @@ -0,0 +1,4 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-react"], + "plugins": ["@babel/plugin-transform-runtime"] +} diff --git a/WebConfigUI/index.html b/WebConfigUI/index.html new file mode 100644 index 0000000..b037a25 --- /dev/null +++ b/WebConfigUI/index.html @@ -0,0 +1,12 @@ + + + + + + LED Pixel Clock Configuration + + +
+ + + diff --git a/WebConfigUI/package.json b/WebConfigUI/package.json new file mode 100644 index 0000000..e6bf72c --- /dev/null +++ b/WebConfigUI/package.json @@ -0,0 +1,43 @@ +{ + "name": "web-config-ui", + "version": "1.0.0", + "description": "", + "main": "src/main.jsx", + "scripts": { + "dev": "set NODE_ENV=development && set PURGE_FILES=0 && vite", + "build": "set NODE_ENV=production && set PURGE_FILES=1 && webpack --mode production", + "start": "set NODE_ENV=development && set PURGE_FILES=0 && webpack serve --mode development" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@babel/core": "^7.13.15", + "@babel/plugin-transform-runtime": "^7.13.15", + "@babel/preset-env": "^7.13.15", + "@babel/preset-react": "^7.13.13", + "@headlessui/react": "^1.0.0", + "@tailwindcss/forms": "^0.3.2", + "autoprefixer": "^10.2.5", + "babel-loader": "^8.2.2", + "brotli-webpack-plugin": "^1.1.0", + "compression-webpack-plugin": "^7.1.2", + "css-loader": "^5.2.4", + "html-webpack-plugin": "^5.3.1", + "postcss": "^8.2.10", + "postcss-loader": "^5.2.0", + "postcss-preset-env": "^6.7.0", + "style-loader": "^2.0.0", + "tailwindcss": "^2.1.1", + "vite": "^2.2.1", + "webpack": "^5.34.0", + "webpack-cli": "^4.6.0", + "webpack-dev-server": "^3.11.2" + }, + "dependencies": { + "@heroicons/react": "^1.0.1", + "esbuild": "^0.11.13", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-router-dom": "^5.2.0" + } +} diff --git a/WebConfigUI/postcss.config.js b/WebConfigUI/postcss.config.js new file mode 100644 index 0000000..d64cc37 --- /dev/null +++ b/WebConfigUI/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/WebConfigUI/src/App.jsx b/WebConfigUI/src/App.jsx new file mode 100644 index 0000000..dcbfcab --- /dev/null +++ b/WebConfigUI/src/App.jsx @@ -0,0 +1,81 @@ +import React, { useEffect, useState } from "react"; +import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; + +import Navigation from "./sites/Navigation"; +import WIFI_setup from "./sites/WIFI-setup"; +import Settings from "./sites/Settings"; +import Dialog from './components/Dialog'; + +export default function App() { + const [saveResponse, setSaveResponse] = useState("ERROR: Not saved"); + const [dialogShown, setDialogShown] = useState(false); + + // Function for fetching the config file from the server + const getConfigFile = async (fileUrl, hook) => { + const requestOptions = { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }; + const response = await fetch(fileUrl, requestOptions); + if (!response.ok) { + console.log("Error while getting config file: " + response.statusText); + return; + } + const parsedJson = await response.json(); + hook(parsedJson); + } + + const putConfigFile = async (fileUrl, value) => { + console.log("Putting: " + value) + console.log("To: " + fileUrl) + const requestOptions = { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: value + }; + const response = await fetch(fileUrl, requestOptions); + if (!response.ok) { + console.log("Error while putting config file: " + response.statusText); + return false; + } + setSaveResponse(await response.text()); + setDialogShown(true); + return true; + } + + + const handleSubmit = (domain, values, setter) => { + putConfigFile(domain, values); + if(setter != null) + { + getConfigFile(domain + '.json', setter); + } + } + + return ( +
+ + + } /> + + handleSubmit(d, v, null)}/>} /> + + getConfigFile('UI_definitions.json', setter)} getValues={(setter)=>getConfigFile('BaseConfig.json', setter)}> + General Settings + } /> + + getConfigFile('UI_definitions.json', setter)} getValues={(setter)=>getConfigFile('ColorConfig.json', setter)}> + Color Settings + } /> + + getConfigFile('UI_definitions.json', setter)} getValues={(setter)=>getConfigFile('HWConfig.json', setter)}> + Hardware Settings + } /> + + + setDialogShown(false)} confirmMessage="OK" title="Info" type="OK"> + {saveResponse} + +
+ ); +} diff --git a/WebConfigUI/src/components/Dialog.jsx b/WebConfigUI/src/components/Dialog.jsx new file mode 100644 index 0000000..9e3daa8 --- /dev/null +++ b/WebConfigUI/src/components/Dialog.jsx @@ -0,0 +1,87 @@ +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useRef} from "react"; + +export default function MyModal(props) { + let OkayButtonRef = useRef(); + + props.type == "OK/Cancel" + + let cancelButton = "" + let okayButtonLabel = "OK" + + if(props.confirmMessage) + { + okayButtonLabel = props.confirmMessage + } + + if(props.type == "OK/Cancel") + { + cancelButton = ( + + ) + } + + return ( + + props.onClose(false)} + > +
+ + + + +
+ + {props.title} + + +

+ {props.children} +

+
+ +
+ + {cancelButton} +
+
+
+
+
+
+ ); +} diff --git a/WebConfigUI/src/components/SettingsForm.jsx b/WebConfigUI/src/components/SettingsForm.jsx new file mode 100644 index 0000000..df3a3a9 --- /dev/null +++ b/WebConfigUI/src/components/SettingsForm.jsx @@ -0,0 +1,271 @@ +import React, { Fragment, useEffect, useState } from "react"; +import Dialog from '../components/Dialog' +import { Listbox, Transition } from '@headlessui/react' +import { CheckIcon, SelectorIcon } from '@heroicons/react/solid' + + +const loadConfig = (UIDefinition, InitialValues) => { + const settings = [...UIDefinition]; + UIDefinition.forEach((element, index) => { + settings[index].value = InitialValues[element.target]; + }); + return settings; +} + +const SettingsForm = (props) => { + const [Settings, setSettings] = useState([]); + const [Values, setValues] = useState([]); + const [dialogShown, setDialogShown] = useState(false); + + useEffect(() => { + props.getUI((value)=>{ + setSettings(value[props.domain]); + }); + props.getValues(setValues); + }, []); + + const handleSubmit = (event) => { + event.preventDefault(); + setDialogShown(true); + } + + const handleDialogClose = (result) => { + setDialogShown(false); + if(result == true) + { + let results = {}; + Settings.forEach((setting, i) => { + if(Settings[i].type == "color") + { + results[setting.target] = toColorValue(Values[setting.target]); + } + else + { + results[setting.target] = Values[setting.target]; + } + }); + props.onSubmit(props.domain, JSON.stringify(results)); + } + } + + const valueChangedHandler = (event, target) => { + const newSettingValue = {...Values}; + const targetSettingIndex = Settings.findIndex(p => p.target === target); + const setting = Settings[targetSettingIndex]; + + const newValue = event.target.value; + switch(setting.type) + { + case "bool": + newSettingValue[target] = event.target.checked; + break; + case "float": + case "int": + if(isNaN(newValue) || (newValue < setting.min || newValue > setting.max) && newValue != "" || newValue.includes("e") || (newValue.includes(".") && setting.type === "int")) + { + console.log("Invalid value"); + return; + } + newSettingValue[target] = parseInt(newValue, 10); + case "color": + parseRGBColor(target); + switch (event.target.id) { + case "r": + newSettingValue[target].r = event.target.value; + break; + case "g": + newSettingValue[target].g = event.target.value; + break; + case "b": + newSettingValue[target].b = event.target.value; + break; + default: + break; + } + break; + default: + newSettingValue[target] = newValue; + break; + } + setValues(newSettingValue); + } + + const isDisabled = (setting) => { + if("onlyIf" in setting) + { + const invert = setting.onlyIf.startsWith("!"); + const onlyIfTarget = setting.onlyIf.replace("!", ""); + + const targetSettingIndex = Settings.findIndex(p => p.target === onlyIfTarget); + if(Settings[targetSettingIndex].type === "bool") + { + if(invert) + { + return Values[onlyIfTarget]; + } + else + { + return !Values[onlyIfTarget]; + } + } + } + return false; + } + + const parseRGBColor = (target) => { + if(typeof(Values[target]) !== 'object' && Values[target] !== null) + { + const newSettingValue = {...Values}; + newSettingValue[target] = toColor(Values[target]); + setValues(newSettingValue); + return newSettingValue[target]; + } + return Values[target]; + } + + const toColor = (num) => { + const rgbColor = { + b: num & 0xFF, + g: (num & 0xFF00) >> 8, + r: (num & 0xFF0000) >> 16 + } + return rgbColor; + } + + const toColorStyle = (rgbColor) => { + return "rgba(" + [rgbColor.r, rgbColor.g, rgbColor.b, 1].join(",") + ")"; + } + + const toColorValue = (rgbColor) => { + return (rgbColor.r << 16) + (rgbColor.g << 8) + rgbColor.b; + } + + const settingsForm = Settings.map((settingEntry, index) => { + const Disabled = isDisabled(settingEntry); + let stepSize = 1; + switch(settingEntry.type) + { + case "string": + return ( +
+ +
+ valueChangedHandler(e, settingEntry.target)} autoComplete="off" type="text"/> +
+
+ ) + case "bool": + return ( +
+ valueChangedHandler(e, settingEntry.target)}/> + +
+ ) + case "float": + stepSize = 0.1 + case "int": + return ( +
+ +
+ valueChangedHandler(e, settingEntry.target)} autoComplete="off" type="number"/> +
+
+ ) + case "color": + let rgbColor = parseRGBColor(settingEntry.target); + return ( +
+ +
+
+
+
+ + valueChangedHandler(e, settingEntry.target)}/> +
+
+ + valueChangedHandler(e, settingEntry.target)}/> +
+
+ + valueChangedHandler(e, settingEntry.target)}/> +
+
+
+
+ ) + case "dropdown": + var selectedItem = Values[settingEntry.target] || ''; + var listboxOptions; + if(Array.isArray(settingEntry.list)) + { + listboxOptions = settingEntry.list.map((entry) => ( + `${active ? 'text-amber-900 bg-amber-100' : 'text-gray-900'} cursor-default select-none relative py-2 pl-10 pr-4`} + value={entry.value} + > + {({ selected, active }) => ( + <> + + {entry.label} + + {selected ? ( + + + ) : null} + + )} + + )) + } + return ( +
+ +
+ valueChangedHandler(e, settingEntry.target)}> +
+ + {selectedItem} + + + + + {listboxOptions} + +
+
+
+
+ ); + default: + console.log(settingEntry.target + ": has invalid type: " + settingEntry.type); + return (
); + } + }) + return ( +
+
+ {settingsForm} +
+ +
+
+ + + To save the changes the device may reboot. You may loose connection to this interface temporarily. + +
+ ); +} + +export default SettingsForm; diff --git a/WebConfigUI/src/css/tailwind.css b/WebConfigUI/src/css/tailwind.css new file mode 100644 index 0000000..f9ea511 --- /dev/null +++ b/WebConfigUI/src/css/tailwind.css @@ -0,0 +1,58 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + + +@layer base { + [type='text'], + [type='email'], + [type='url'], + [type='password'], + [type='number'], + [type='date'], + [type='datetime-local'], + [type='month'], + [type='search'], + [type='tel'], + [type='time'], + [type='week'], + [multiple], + textarea, + select { + @apply w-full border-gray-300 rounded-lg shadow-sm focus:border-indigo-500 focus:ring-indigo-500 transform transition; + } + [type='checkbox'], + [type='radio'] { + @apply rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 transform transition; + } +} + +@layer components { + .btn { + @apply justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 + } + + .btn-primary { + @apply text-white bg-indigo-600 hover:bg-indigo-700 + } + + .btn-secondary { + @apply text-indigo-900 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-400 + } + + [type='range'] { + @apply appearance-none w-full h-6 rounded-md bg-gray-200 outline-none opacity-70 transition-opacity; + } + + [type='range']:hover { + @apply opacity-100; + } + + [type='range']::-webkit-slider-thumb { + @apply h-6 w-5 bg-indigo-600 appearance-none rounded-md cursor-pointer; + } + + [type='range']::-moz-range-thumb { + @apply h-6 w-5 bg-indigo-600 rounded-md cursor-pointer; + } +} diff --git a/WebConfigUI/src/data/BaseConfig.json b/WebConfigUI/src/data/BaseConfig.json new file mode 100644 index 0000000..65f5742 --- /dev/null +++ b/WebConfigUI/src/data/BaseConfig.json @@ -0,0 +1,27 @@ +{ + "ENABLE_BLYNK": true, + "BLYNK_AUTH_TOKEN": "AUTH_TOKEN_GOES_HERE", + "BLYNK_SERVER": "blynk-cloud.com", + "HOST_NAME": "LED-Shelf", + "NUM_RETRIES": 50, + "SMART_CONFIG": true, + "NTP_SERVER": "pool.ntp.org", + "TIMEZONE_INFO": "CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00", + "TIME_SYNC_INTERVAL": 1800, + "TIMER_FLASH_TIME": false, + "TIMER_FLASH_COUNT": 10, + "ALARM_NOTIFICATION_PERIOD": 600, + "NOTIFICATION_BRIGHTNESS": 125, + "TIME_UPDATE_INTERVAL": 500, + "DISPLAY_0_AT_MIDNIGHT": false, + "DISPLAY_SWITCH_OFF_AT_0": false, + "USE_24_HOUR_FORMAT": false, + "LOADING_ANIMATION_DURATION": 3000, + "BRIGHTNESS_INTERPOLATION": 3000, + "ANIMATION_TARGET_FPS": 60, + "ANIMATION_AFTERGLOW": 0.2, + "DOT_FLASH_SPEED": 2000, + "DOT_FLASH_INTERVAL": 4000, + "NUM_SEPARATION_DOTS": 2, + "DIGIT_ANIMATION_SPEED": 900 +} \ No newline at end of file diff --git a/WebConfigUI/src/data/ColorConfig.json b/WebConfigUI/src/data/ColorConfig.json new file mode 100644 index 0000000..7425c52 --- /dev/null +++ b/WebConfigUI/src/data/ColorConfig.json @@ -0,0 +1,11 @@ +{ + "HOUR": 16777215, + "MINUTE": 15794175, + "INTERNAL": 255, + "SEPARATION_DOT": 255, + "OTA_UPDATE": 16753920, + "WIFI_CONNECTING": 255, + "WIFI_CONNECTION_SUCCESSFUL": 32768, + "WIFI_SMART_CONFIG": 16776960, + "ERROR": 16711680 +} diff --git a/WebConfigUI/src/data/HWConfig.json b/WebConfigUI/src/data/HWConfig.json new file mode 100644 index 0000000..b8a8f78 --- /dev/null +++ b/WebConfigUI/src/data/HWConfig.json @@ -0,0 +1,28 @@ +{ + "LED_DATA_PIN": 21, + "NUM_SEGMENTS": 32, + "NUM_LEDS_PER_SEGMENT": 12, + "APPEND_DOWN_LIGHTERS": true, + "ADDITIONAL_LEDS": 12, + "DOWNLIGHT_LED_DATA_PIN": 22, + "NUM_DISPLAYS": 7, + "NUM_SEGMENTS_PROGRESS": 16, + "DISPLAY_FOR_SEPARATION_DOT": 3, + "HIGHER_DIGIT_HOUR_DISPLAY": 0, + "FIRST_INTERMEDIATE_DISPLAY": 1, + "LOWER_DIGIT_HOUR_DISPLAY": 2, + "SECOND_INTERMEDIATE_DISPLAY": 3, + "HIGHER_DIGIT_MINUTE_DISPLAY": 4, + "THIRD_INTERMEDIATE_DISPLAY": 5, + "LOWER_DIGIT_MINUTE_DISPLAY": 6, + "ENABLE_LIGHT_SENSOR": false, + "LIGHT_SENSOR_PIN": 34, + "LIGHT_SENSOR_AVERAGE": 15, + "LIGHT_SENSOR_MEDIAN_WIDTH": 5, + "LIGHT_SENSOR_READ_DELAY": 500, + "LIGHT_SENSOR_MIN": 0, + "LIGHT_SENSOR_MAX": 4095, + "LIGHT_SENSOR_SENSITIVITY": 100, + "TIME_MANAGER_DEMO_MODE": false, + "FASTLED_SAFE_DELAY_MS": 20 +} diff --git a/WebConfigUI/src/data/UI_definitions.json b/WebConfigUI/src/data/UI_definitions.json new file mode 100644 index 0000000..2f51c4b --- /dev/null +++ b/WebConfigUI/src/data/UI_definitions.json @@ -0,0 +1,393 @@ +{ + "BaseConfig": [ + { + "label": "Enable Blynk functionality", + "type": "bool", + "target": "ENABLE_BLYNK" + }, + { + "label": "Blynk Auth token", + "type": "string", + "target": "BLYNK_AUTH_TOKEN", + "onlyIf": "ENABLE_BLYNK" + }, + { + "label": "Blynk server URL", + "type": "string", + "target": "BLYNK_SERVER", + "onlyIf": "ENABLE_BLYNK" + }, + { + "label": "Host name", + "type": "string", + "target": "HOST_NAME" + }, + { + "label": "Number of retries for WIFI reconnection before smart config is started (if enabled)", + "type": "int", + "target": "NUM_RETRIES", + "min": 1, + "max": 1000 + }, + { + "label": "Enable Smart config if WIFI connection was lost", + "type": "bool", + "target": "SMART_CONFIG" + }, + { + "label": "NTP server URL", + "type": "string", + "target": "NTP_SERVER" + }, + { + "label": "Enter a configuration string for you timezone according to this website: https://remotemonitoringsystems.ca/time-zone-abbreviations.php", + "type": "string", + "target": "TIMEZONE_INFO" + }, + { + "label": "Time in seconds for the interval in which the time should be synchronized with the time (NTP) server", + "type": "int", + "target": "TIME_SYNC_INTERVAL", + "min": 1, + "max": 4294967295 + }, + { + "label": "Flash the current time in case a timer is expired instead of flashing 00:00", + "type": "bool", + "target": "TIMER_FLASH_TIME" + }, + { + "label": "Number of flashes until an alarm is considered expired and the system goes back to normal", + "type": "int", + "target": "TIMER_FLASH_COUNT", + "min": 1, + "max": 65535 + }, + { + "label": "For how long the Display should flash when an alarm was fired in seconds. For example: An Alarm for 12:00 would stop flashing at 12:01 if this was set to 60", + "type": "int", + "target": "ALARM_NOTIFICATION_PERIOD", + "min": 1, + "max": 65535 + }, + { + "label": "How bright the clock should flash when an alarm or timer was triggered", + "type": "int", + "target": "NOTIFICATION_BRIGHTNESS", + "min": 0, + "max": 255 + }, + { + "label": "How often the time on the displays is updated in MS", + "type": "int", + "target": "TIME_UPDATE_INTERVAL", + "min": 1, + "max": 65535 + }, + { + "label": "If turned on the display will show 0 at midnight and 12 otherwise", + "type": "bool", + "target": "DISPLAY_0_AT_MIDNIGHT" + }, + { + "label": "If turned on the higher digit displays will turn off in case they would show 0", + "type": "bool", + "target": "DISPLAY_SWITCH_OFF_AT_0" + }, + { + "label": "If set to true 24 hour format will be used. Only usable if the hour display has all 7 segments", + "type": "bool", + "target": "USE_24_HOUR_FORMAT" + }, + { + "label": "The time it shall take for one iteration of the loading animation", + "type": "int", + "target": "LOADING_ANIMATION_DURATION", + "min": 1, + "max": 65535 + }, + { + "label": "How fast the brightness interpolation shall react to brightness changes", + "type": "int", + "target": "BRIGHTNESS_INTERPOLATION", + "min": 1, + "max": 65535 + }, + { + "label": "Target frames per second for the smoothness of animations", + "type": "int", + "target": "ANIMATION_TARGET_FPS", + "min": 1, + "max": 255 + }, + { + "label": "Length of sooth animation transition from fully on to black and vice versa in percent. NOTE: The higher this number the less obvious easing effects like bounce or elastic will be", + "type": "float", + "target": "ANIMATION_AFTERGLOW", + "min": 0, + "max": 1 + }, + { + "label": "Length of the dot/s fading animation in MS. One flash fades in and out", + "type": "int", + "target": "DOT_FLASH_SPEED", + "min": 1, + "max": 65535 + }, + { + "label": "Interval in MS in which the dot/s should flash", + "type": "int", + "target": "DOT_FLASH_INTERVAL", + "min": 1, + "max": 65535 + }, + { + "label": "Number of separation dots that shall be displayed", + "type": "int", + "target": "NUM_SEPARATION_DOTS", + "min": 0, + "max": 2 + }, + { + "label": "The time it takes in MS for one digit to morph into another", + "type": "int", + "target": "DIGIT_ANIMATION_SPEED", + "min": 1, + "max": 65535 + } + ], + "ColorConfig": [ + { + "label": "Color of the hour segments", + "type": "color", + "target": "HOUR", + "min": 0, + "max": 16777215 + }, + { + "label": "Color of the minute segments", + "type": "color", + "target": "MINUTE", + "min": 0, + "max": 16777215 + }, + { + "label": "Color of the internal LEDs", + "type": "color", + "target": "INTERNAL", + "min": 0, + "max": 16777215 + }, + { + "label": "Color of the separation dot LEDs", + "type": "color", + "target": "SEPARATION_DOT", + "min": 0, + "max": 16777215 + }, + { + "label": "Color of the LEDs for the OTA update progress bar", + "type": "color", + "target": "OTA_UPDATE", + "min": 0, + "max": 16777215 + }, + { + "label": "Color of the LEDs while searching for a WIFI network", + "type": "color", + "target": "WIFI_CONNECTING", + "min": 0, + "max": 16777215 + }, + { + "label": "Color of the LEDs signaling a successful WIFI connection", + "type": "color", + "target": "WIFI_CONNECTION_SUCCESSFUL", + "min": 0, + "max": 16777215 + }, + { + "label": "Color of the LEDs if system is waiting for WIFI smart config", + "type": "color", + "target": "WIFI_SMART_CONFIG", + "min": 0, + "max": 16777215 + }, + { + "label": "Color of the LEDs signaling an error of some sort", + "type": "color", + "target": "ERROR", + "min": 0, + "max": 16777215 + } + ], + "HWConfig": [ + { + "label": "Pin to which the led strip data pin is connected to", + "type": "int", + "target": "LED_DATA_PIN", + "min": 0, + "max": 42 + }, + { + "label": "Total number of segments that have LEDs in the shelf", + "type": "int", + "target": "NUM_SEGMENTS", + "min": 1, + "max": 65535 + }, + { + "label": "Number of LEDs in each segment", + "type": "int", + "target": "NUM_LEDS_PER_SEGMENT", + "min": 1, + "max": 255 + }, + { + "label": "If you wired the down lighter LEDs to the end of the LED strips set this to true", + "type": "bool", + "target": "APPEND_DOWN_LIGHTERS" + }, + { + "label": "If you wired the down lighter LEDs to the end of the LED strips enable this", + "type": "int", + "target": "DOWNLIGHT_LED_DATA_PIN", + "min": 0, + "max": 42, + "onlyIf": "!APPEND_DOWN_LIGHTERS" + }, + { + "label": "Number of LEDs For interior lights", + "type": "int", + "target": "ADDITIONAL_LEDS", + "min": 0, + "max": 65535 + }, + { + "label": "Number of seven segment displays on the clock", + "type": "int", + "target": "NUM_DISPLAYS", + "min": 4, + "max": 7 + }, + { + "label": "The number of segments to use for displaying a progress bar for the OTA updates. ATTENTION: this also depends on the configured animation which is used", + "type": "int", + "target": "NUM_SEGMENTS_PROGRESS", + "min": 1, + "max": 65535 + }, + { + "label": "The display on which the separation dot should be shown", + "type": "dropdown", + "target": "DISPLAY_FOR_SEPARATION_DOT", + "list": [ + {"value": 0, "label": "Higher digit hour display"}, + {"value": 1, "label": "First intermediate display"}, + {"value": 2, "label": "Lower digit hour display"}, + {"value": 3, "label": "Second intermediate display"}, + {"value": 4, "label": "Higher digit minute display"}, + {"value": 5, "label": "Third intermediate display"}, + {"value": 6, "label": "Lower digit minute display"} + ] + }, + { + "label": "The display on which the separation dot should be shown", + "type": "dropdown_invalid", + "target": "DISPLAY_FOR_SEPARATION_DOT1", + "list": "DisplayIDs", + "min": 1, + "max": 7 + }, + { + "label": "The indices of the displays in the order they are configured in", + "type": "ListSelection", + "elements": [ + {"target": "HIGHER_DIGIT_HOUR_DISPLAY", "label": "Higher digit hour display"}, + {"target": "FIRST_INTERMEDIATE_DISPLAY", "label": "First intermediate display"}, + {"target": "LOWER_DIGIT_HOUR_DISPLAY", "label": "Lower digit hour display"}, + {"target": "SECOND_INTERMEDIATE_DISPLAY", "label": "Second intermediate display"}, + {"target": "HIGHER_DIGIT_MINUTE_DISPLAY", "label": "Higher digit minute display"}, + {"target": "THIRD_INTERMEDIATE_DISPLAY", "label": "Third intermediate display"}, + {"target": "LOWER_DIGIT_MINUTE_DISPLAY", "label": "Lower digit minute display"} + ], + "id": "DisplayIDs", + "deactivated": -1, + "min": 1, + "max": 7 + }, + { + "label": "Enable automatic brightness adjustments based on a light sensor", + "type": "bool", + "target": "ENABLE_LIGHT_SENSOR" + }, + { + "label": "ADC pin to which the light sensor is connected to", + "type": "int", + "target": "LIGHT_SENSOR_PIN", + "min": 0, + "max": 42, + "onlyIf": "ENABLE_LIGHT_SENSOR" + }, + { + "label": "How many measurements shall be averaged. Higher number -> smoother but slower change", + "type": "int", + "target": "LIGHT_SENSOR_AVERAGE", + "min": 0, + "max": 255, + "onlyIf": "ENABLE_LIGHT_SENSOR" + }, + { + "label": "Width of the median calculation. Higher number -> smoother change Should never be higher than the LIGHT_SENSOR_AVERAGE", + "type": "int", + "target": "LIGHT_SENSOR_MEDIAN_WIDTH", + "min": 0, + "max": "LIGHT_SENSOR_AVERAGE", + "onlyIf": "ENABLE_LIGHT_SENSOR" + }, + { + "label": "Time that should pass in MS before the light sensor is read again. Higher number -> slower adjustments but also changes will be more sudden", + "type": "int", + "target": "LIGHT_SENSOR_READ_DELAY", + "min": 0, + "max": 65535, + "onlyIf": "ENABLE_LIGHT_SENSOR" + }, + { + "label": "AnalogRead value if the light sensor reads complete darkness", + "type": "int", + "target": "LIGHT_SENSOR_MIN", + "min": 0, + "max": 4095, + "onlyIf": "ENABLE_LIGHT_SENSOR" + }, + { + "label": "AnalogRead value if the light sensor reads the brightest", + "type": "int", + "target": "LIGHT_SENSOR_MAX", + "min": 0, + "max": 4095, + "onlyIf": "ENABLE_LIGHT_SENSOR" + }, + { + "label": "How much the can the light sensor values influence the led brightness", + "type": "int", + "target": "LIGHT_SENSOR_SENSITIVITY", + "min": 0, + "max": 255, + "onlyIf": "ENABLE_LIGHT_SENSOR" + }, + { + "label": "(Debugging Feature) Enable for a demo of all the digit transitions", + "type": "bool", + "target": "TIME_MANAGER_DEMO_MODE" + }, + { + "label": "(Debugging Feature) The minimum delay between led updates in MS. Usually anything faster than 20MS causes problems. Increase if your LEDs do not refresh properly", + "type": "int", + "target": "FASTLED_SAFE_DELAY_MS", + "min": 0, + "max": 65535 + } + ] +} diff --git a/WebConfigUI/src/main.jsx b/WebConfigUI/src/main.jsx new file mode 100644 index 0000000..bb314c3 --- /dev/null +++ b/WebConfigUI/src/main.jsx @@ -0,0 +1,7 @@ +import React from "react"; +import ReactDOM from "react-dom"; + +import App from "./App"; +import "./css/tailwind.css"; + +ReactDOM.render(, document.getElementById("root")); diff --git a/WebConfigUI/src/sites/Navigation.jsx b/WebConfigUI/src/sites/Navigation.jsx new file mode 100644 index 0000000..4233c45 --- /dev/null +++ b/WebConfigUI/src/sites/Navigation.jsx @@ -0,0 +1,39 @@ +import { Link, withRouter } from "react-router-dom"; +import { Menu } from "@headlessui/react"; +import React from "react"; + +const pages = [ + {link: "/WIFI", label: "WIFI Setup"}, + {link: "/BaseSettings", label: "Base Settings"}, + {link: "/Colors", label: "Colors"}, + {link: "/HWSetup", label: "Hardware Setup"}, + {link: "/", label: "Display Setup"}, + {link: "/", label: "Animation Setup"}, +]; + +function NavigationMenu(props) { + return ( +
+

LED Pixel Clock

+
+ + + {pages.map((page) =>( + + {({ active }) => ( + + {page.label} + + )} + + ))} + + +
+
+ ); +} + +export default withRouter(NavigationMenu); diff --git a/WebConfigUI/src/sites/Settings.jsx b/WebConfigUI/src/sites/Settings.jsx new file mode 100644 index 0000000..f429b73 --- /dev/null +++ b/WebConfigUI/src/sites/Settings.jsx @@ -0,0 +1,17 @@ +import React, { useEffect, useState } from "react"; +import SettingsForm from '../components/SettingsForm' + +const Settings = (props) => { + return ( +
+
+
+

{props.children}

+
+ +
+
+ ); +} + +export default Settings; diff --git a/WebConfigUI/src/sites/WIFI-setup.jsx b/WebConfigUI/src/sites/WIFI-setup.jsx new file mode 100644 index 0000000..cbe3791 --- /dev/null +++ b/WebConfigUI/src/sites/WIFI-setup.jsx @@ -0,0 +1,75 @@ +import React, { useState } from 'react'; +import Dialog from '../components/Dialog' + +const WIFI_new = (props) => { + const [SSID, setSSID] = useState(""); + const [PW, setPW] = useState(""); + const [createAP, setCreateAP] = useState(false); + const [dialogShown, setDialogShown] = useState(false); + + const handleSSIDChanged = (object) => { + setSSID(object.target.value); + } + + const handlePWChanged = (object) => { + setPW(object.target.value); + } + + const handleCreateAPChanged = (object) => { + setCreateAP(object.target.checked); + } + + const handleSubmit = (event) => { + event.preventDefault(); + setDialogShown(true); + } + + const handleDialogClose = (result) => { + setDialogShown(false); + if(result == true) + { + setSSID(""); + setPW(""); + setCreateAP(false); + props.onSubmit(props.domain, JSON.stringify({SSID: SSID, PW: PW, CreateAP: createAP})); + } + } + + return ( +
+
+
+

Connect to your WIFI

+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + +
+
+ +
+
+
+ To change WIFI settings the device has to restart. Please reconnect to the new device IP once it restarted. +
+ ) +} + +export default WIFI_new; diff --git a/WebConfigUI/tailwind.config.js b/WebConfigUI/tailwind.config.js new file mode 100644 index 0000000..9057575 --- /dev/null +++ b/WebConfigUI/tailwind.config.js @@ -0,0 +1,15 @@ +module.exports = { + purge: { + enabled: process.env.PURGE_FILES == 1, + content: ["./src/**/*.jsx", "./index.html"], + }, + + darkMode: false, // or 'media' or 'class' + theme: {}, + variants: { + extend: { + backgroundColor: ["active"], + }, + }, + plugins: [require('@tailwindcss/forms')], +}; diff --git a/WebConfigUI/vite.config.js b/WebConfigUI/vite.config.js new file mode 100644 index 0000000..74284c7 --- /dev/null +++ b/WebConfigUI/vite.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + root: ".", + publicDir: "./src/data" +}) diff --git a/WebConfigUI/webpack.config.js b/WebConfigUI/webpack.config.js new file mode 100644 index 0000000..30a3c65 --- /dev/null +++ b/WebConfigUI/webpack.config.js @@ -0,0 +1,49 @@ +const path = require("path"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const CompressionPlugin = require("compression-webpack-plugin"); + +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + use: ['style-loader', 'css-loader', 'postcss-loader'] + }, + { + test: /\.jsx$/, + exclude: /node_modules/, + use: ["babel-loader"] + }, + { + test: /\.js$/, + exclude: /node_modules/, + use: ["babel-loader"] + } + ] + }, + optimization: { + // splitChunks: { chunks: "all" }, + minimize: true + }, + entry: { + index: path.resolve(__dirname, "src", "main.jsx") + }, + output: { + path: path.resolve(__dirname, "build"), + publicPath: '/' + }, + devServer: { + historyApiFallback: true, + }, + plugins: [ + new CompressionPlugin({ + test: /\.(js|jsx|css|svg)$/ + }), + new HtmlWebpackPlugin({ + template: path.resolve(__dirname, "index.html") + }) + ], + resolve: { + extensions: ['', '.js', '.jsx'], + } +}; diff --git a/data/BaseConfig.json b/data/BaseConfig.json new file mode 100644 index 0000000..65f5742 --- /dev/null +++ b/data/BaseConfig.json @@ -0,0 +1,27 @@ +{ + "ENABLE_BLYNK": true, + "BLYNK_AUTH_TOKEN": "AUTH_TOKEN_GOES_HERE", + "BLYNK_SERVER": "blynk-cloud.com", + "HOST_NAME": "LED-Shelf", + "NUM_RETRIES": 50, + "SMART_CONFIG": true, + "NTP_SERVER": "pool.ntp.org", + "TIMEZONE_INFO": "CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00", + "TIME_SYNC_INTERVAL": 1800, + "TIMER_FLASH_TIME": false, + "TIMER_FLASH_COUNT": 10, + "ALARM_NOTIFICATION_PERIOD": 600, + "NOTIFICATION_BRIGHTNESS": 125, + "TIME_UPDATE_INTERVAL": 500, + "DISPLAY_0_AT_MIDNIGHT": false, + "DISPLAY_SWITCH_OFF_AT_0": false, + "USE_24_HOUR_FORMAT": false, + "LOADING_ANIMATION_DURATION": 3000, + "BRIGHTNESS_INTERPOLATION": 3000, + "ANIMATION_TARGET_FPS": 60, + "ANIMATION_AFTERGLOW": 0.2, + "DOT_FLASH_SPEED": 2000, + "DOT_FLASH_INTERVAL": 4000, + "NUM_SEPARATION_DOTS": 2, + "DIGIT_ANIMATION_SPEED": 900 +} \ No newline at end of file diff --git a/data/ColorConfig.json b/data/ColorConfig.json new file mode 100644 index 0000000..7425c52 --- /dev/null +++ b/data/ColorConfig.json @@ -0,0 +1,11 @@ +{ + "HOUR": 16777215, + "MINUTE": 15794175, + "INTERNAL": 255, + "SEPARATION_DOT": 255, + "OTA_UPDATE": 16753920, + "WIFI_CONNECTING": 255, + "WIFI_CONNECTION_SUCCESSFUL": 32768, + "WIFI_SMART_CONFIG": 16776960, + "ERROR": 16711680 +} diff --git a/data/ConfigChangeEventIDs.json b/data/ConfigChangeEventIDs.json new file mode 100644 index 0000000..3df1599 --- /dev/null +++ b/data/ConfigChangeEventIDs.json @@ -0,0 +1,62 @@ +{ + "ENABLE_BLYNK": 1, + "BLYNK_AUTH_TOKEN": 1, + "BLYNK_SERVER": 1, + "HOST_NAME": 0, + "NUM_RETRIES": 0, + "SMART_CONFIG": 0, + "NTP_SERVER": 2, + "TIMEZONE_INFO": 2, + "TIME_SYNC_INTERVAL": 2, + "TIMER_FLASH_TIME": 2, + "TIMER_FLASH_COUNT": 2, + "ALARM_NOTIFICATION_PERIOD": 2, + "NOTIFICATION_BRIGHTNESS": 2, + "TIME_UPDATE_INTERVAL": 2, + "DISPLAY_0_AT_MIDNIGHT": 2, + "DISPLAY_SWITCH_OFF_AT_0": 2, + "USE_24_HOUR_FORMAT": 2, + "LOADING_ANIMATION_DURATION": 2, + "BRIGHTNESS_INTERPOLATION": 2, + "ANIMATION_TARGET_FPS": 2, + "ANIMATION_AFTERGLOW": 2, + "DOT_FLASH_SPEED": 2, + "DOT_FLASH_INTERVAL": 2, + "NUM_SEPARATION_DOTS": 2, + "DIGIT_ANIMATION_SPEED": 2, + "HOUR": 3, + "MINUTE": 3, + "INTERNAL": 3, + "SEPARATION_DOT": 3, + "OTA_UPDATE": 3, + "WIFI_CONNECTING": 3, + "WIFI_CONNECTION_SUCCESSFUL": 3, + "WIFI_SMART_CONFIG": 3, + "ERROR": 3, + "LED_DATA_PIN": 4, + "NUM_SEGMENTS": 4, + "NUM_LEDS_PER_SEGMENT": 4, + "APPEND_DOWN_LIGHTERS": 4, + "ADDITIONAL_LEDS": 4, + "DOWNLIGHT_LED_DATA_PIN": 4, + "NUM_DISPLAYS": 4, + "NUM_SEGMENTS_PROGRESS": 4, + "DISPLAY_FOR_SEPARATION_DOT": 4, + "HIGHER_DIGIT_HOUR_DISPLAY": 4, + "FIRST_INTERMEDIATE_DISPLAY": 4, + "LOWER_DIGIT_HOUR_DISPLAY": 4, + "SECOND_INTERMEDIATE_DISPLAY": 4, + "HIGHER_DIGIT_MINUTE_DISPLAY": 4, + "THIRD_INTERMEDIATE_DISPLAY": 4, + "LOWER_DIGIT_MINUTE_DISPLAY": 4, + "ENABLE_LIGHT_SENSOR": 4, + "LIGHT_SENSOR_PIN": 4, + "LIGHT_SENSOR_AVERAGE": 4, + "LIGHT_SENSOR_MEDIAN_WIDTH": 4, + "LIGHT_SENSOR_READ_DELAY": 4, + "LIGHT_SENSOR_MIN": 4, + "LIGHT_SENSOR_MAX": 4, + "LIGHT_SENSOR_SENSITIVITY": 4, + "TIME_MANAGER_DEMO_MODE": 4, + "FASTLED_SAFE_DELAY_MS": 4 +} \ No newline at end of file diff --git a/data/HWConfig.json b/data/HWConfig.json new file mode 100644 index 0000000..b8a8f78 --- /dev/null +++ b/data/HWConfig.json @@ -0,0 +1,28 @@ +{ + "LED_DATA_PIN": 21, + "NUM_SEGMENTS": 32, + "NUM_LEDS_PER_SEGMENT": 12, + "APPEND_DOWN_LIGHTERS": true, + "ADDITIONAL_LEDS": 12, + "DOWNLIGHT_LED_DATA_PIN": 22, + "NUM_DISPLAYS": 7, + "NUM_SEGMENTS_PROGRESS": 16, + "DISPLAY_FOR_SEPARATION_DOT": 3, + "HIGHER_DIGIT_HOUR_DISPLAY": 0, + "FIRST_INTERMEDIATE_DISPLAY": 1, + "LOWER_DIGIT_HOUR_DISPLAY": 2, + "SECOND_INTERMEDIATE_DISPLAY": 3, + "HIGHER_DIGIT_MINUTE_DISPLAY": 4, + "THIRD_INTERMEDIATE_DISPLAY": 5, + "LOWER_DIGIT_MINUTE_DISPLAY": 6, + "ENABLE_LIGHT_SENSOR": false, + "LIGHT_SENSOR_PIN": 34, + "LIGHT_SENSOR_AVERAGE": 15, + "LIGHT_SENSOR_MEDIAN_WIDTH": 5, + "LIGHT_SENSOR_READ_DELAY": 500, + "LIGHT_SENSOR_MIN": 0, + "LIGHT_SENSOR_MAX": 4095, + "LIGHT_SENSOR_SENSITIVITY": 100, + "TIME_MANAGER_DEMO_MODE": false, + "FASTLED_SAFE_DELAY_MS": 20 +} diff --git a/data/UI_definitions.json b/data/UI_definitions.json new file mode 100644 index 0000000..6d9eda3 --- /dev/null +++ b/data/UI_definitions.json @@ -0,0 +1,379 @@ +{ + "BaseConfig": [ + { + "label": "Enable Blynk functionality", + "type": "bool", + "target": "ENABLE_BLYNK" + }, + { + "label": "Blynk Auth token", + "type": "string", + "target": "BLYNK_AUTH_TOKEN", + "onlyIf": "ENABLE_BLYNK" + }, + { + "label": "Blynk server URL", + "type": "string", + "target": "BLYNK_SERVER", + "onlyIf": "ENABLE_BLYNK" + }, + { + "label": "Host name", + "type": "string", + "target": "HOST_NAME" + }, + { + "label": "Number of retries for WIFI reconnection before smart config is started (if enabled)", + "type": "int", + "target": "NUM_RETRIES", + "min": 1, + "max": 1000 + }, + { + "label": "Enable Smart config if WIFI connection was lost", + "type": "bool", + "target": "SMART_CONFIG" + }, + { + "label": "NTP server URL", + "type": "string", + "target": "NTP_SERVER" + }, + { + "label": "Enter a configuration string for you timezone according to this website: https://remotemonitoringsystems.ca/time-zone-abbreviations.php", + "type": "string", + "target": "TIMEZONE_INFO" + }, + { + "label": "Time in seconds for the interval in which the time should be synchronized with the time (NTP) server", + "type": "int", + "target": "TIME_SYNC_INTERVAL", + "min": 1, + "max": 4294967295 + }, + { + "label": "Flash the current time in case a timer is expired instead of flashing 00:00", + "type": "bool", + "target": "TIMER_FLASH_TIME" + }, + { + "label": "Number of flashes until an alarm is considered expired and the system goes back to normal", + "type": "int", + "target": "TIMER_FLASH_COUNT", + "min": 1, + "max": 65535 + }, + { + "label": "For how long the Display should flash when an alarm was fired in seconds. For example: An Alarm for 12:00 would stop flashing at 12:01 if this was set to 60", + "type": "int", + "target": "ALARM_NOTIFICATION_PERIOD", + "min": 1, + "max": 65535 + }, + { + "label": "How bright the clock should flash when an alarm or timer was triggered", + "type": "int", + "target": "NOTIFICATION_BRIGHTNESS", + "min": 0, + "max": 255 + }, + { + "label": "How often the time on the displays is updated in MS", + "type": "int", + "target": "TIME_UPDATE_INTERVAL", + "min": 1, + "max": 65535 + }, + { + "label": "If turned on the display will show 0 at midnight and 12 otherwise", + "type": "bool", + "target": "DISPLAY_0_AT_MIDNIGHT" + }, + { + "label": "If turned on the higher digit displays will turn off in case they would show 0", + "type": "bool", + "target": "DISPLAY_SWITCH_OFF_AT_0" + }, + { + "label": "If set to true 24 hour format will be used. Only usable if the hour display has all 7 segments", + "type": "bool", + "target": "USE_24_HOUR_FORMAT" + }, + { + "label": "The time it shall take for one iteration of the loading animation", + "type": "int", + "target": "LOADING_ANIMATION_DURATION", + "min": 1, + "max": 65535 + }, + { + "label": "How fast the brightness interpolation shall react to brightness changes", + "type": "int", + "target": "BRIGHTNESS_INTERPOLATION", + "min": 1, + "max": 65535 + }, + { + "label": "Target frames per second for the smoothness of animations", + "type": "int", + "target": "ANIMATION_TARGET_FPS", + "min": 1, + "max": 255 + }, + { + "label": "Length of sooth animation transition from fully on to black and vice versa in percent. NOTE: The higher this number the less obvious easing effects like bounce or elastic will be", + "type": "float", + "target": "ANIMATION_AFTERGLOW", + "min": 0, + "max": 1 + }, + { + "label": "Length of the dot/s fading animation in MS. One flash fades in and out", + "type": "int", + "target": "DOT_FLASH_SPEED", + "min": 1, + "max": 65535 + }, + { + "label": "Interval in MS in which the dot/s should flash", + "type": "int", + "target": "DOT_FLASH_INTERVAL", + "min": 1, + "max": 65535 + }, + { + "label": "Number of separation dots that shall be displayed", + "type": "int", + "target": "NUM_SEPARATION_DOTS", + "min": 0, + "max": 2 + }, + { + "label": "The time it takes in MS for one digit to morph into another", + "type": "int", + "target": "DIGIT_ANIMATION_SPEED", + "min": 1, + "max": 65535 + } + ], + "ColorConfig": [ + { + "label": "Color of the hour segments", + "type": "color", + "target": "HOUR", + "min": 0, + "max": 16777215 + }, + { + "label": "Color of the minute segments", + "type": "color", + "target": "MINUTE", + "min": 0, + "max": 16777215 + }, + { + "label": "Color of the internal LEDs", + "type": "color", + "target": "INTERNAL", + "min": 0, + "max": 16777215 + }, + { + "label": "Color of the separation dot LEDs", + "type": "color", + "target": "SEPARATION_DOT", + "min": 0, + "max": 16777215 + }, + { + "label": "Color of the LEDs for the OTA update progress bar", + "type": "color", + "target": "OTA_UPDATE", + "min": 0, + "max": 16777215 + }, + { + "label": "Color of the LEDs while searching for a WIFI network", + "type": "color", + "target": "WIFI_CONNECTING", + "min": 0, + "max": 16777215 + }, + { + "label": "Color of the LEDs signaling a successful WIFI connection", + "type": "color", + "target": "WIFI_CONNECTION_SUCCESSFUL", + "min": 0, + "max": 16777215 + }, + { + "label": "Color of the LEDs if system is waiting for WIFI smart config", + "type": "color", + "target": "WIFI_SMART_CONFIG", + "min": 0, + "max": 16777215 + }, + { + "label": "Color of the LEDs signaling an error of some sort", + "type": "color", + "target": "ERROR", + "min": 0, + "max": 16777215 + } + ], + "HWConfig": [ + { + "label": "Pin to which the led strip data pin is connected to", + "type": "int", + "target": "LED_DATA_PIN", + "min": 0, + "max": 42 + }, + { + "label": "Total number of segments that have LEDs in the shelf", + "type": "int", + "target": "NUM_SEGMENTS", + "min": 1, + "max": 65535 + }, + { + "label": "Number of LEDs in each segment", + "type": "int", + "target": "NUM_LEDS_PER_SEGMENT", + "min": 1, + "max": 255 + }, + { + "label": "If you wired the down lighter LEDs to the end of the LED strips set this to true", + "type": "bool", + "target": "APPEND_DOWN_LIGHTERS" + }, + { + "label": "If you wired the down lighter LEDs to the end of the LED strips enable this", + "type": "int", + "target": "DOWNLIGHT_LED_DATA_PIN", + "min": 0, + "max": 42, + "onlyIf": "!APPEND_DOWN_LIGHTERS" + }, + { + "label": "Number of LEDs For interior lights", + "type": "int", + "target": "ADDITIONAL_LEDS", + "min": 0, + "max": 65535 + }, + { + "label": "Number of seven segment displays on the clock", + "type": "int", + "target": "NUM_DISPLAYS", + "min": 4, + "max": 7 + }, + { + "label": "The number of segments to use for displaying a progress bar for the OTA updates. ATTENTION: this also depends on the configured animation which is used", + "type": "int", + "target": "NUM_SEGMENTS_PROGRESS", + "min": 1, + "max": 65535 + }, + { + "label": "The display on which the separation dot should be shown", + "type": "enum selector", + "target": "DISPLAY_FOR_SEPARATION_DOT", + "enum": "DisplayIDs", + "min": 1, + "max": 7 + }, + { + "label": "The indices of the displays in the order they are configured in", + "type": "customizable enum", + "elements": [ + {"target": "HIGHER_DIGIT_HOUR_DISPLAY", "label": "Higher digit hour display"}, + {"target": "FIRST_INTERMEDIATE_DISPLAY", "label": "First intermediate display"}, + {"target": "LOWER_DIGIT_HOUR_DISPLAY", "label": "Lower digit hour display"}, + {"target": "SECOND_INTERMEDIATE_DISPLAY", "label": "Second intermediate display"}, + {"target": "HIGHER_DIGIT_MINUTE_DISPLAY", "label": "Higher digit minute display"}, + {"target": "THIRD_INTERMEDIATE_DISPLAY", "label": "Third intermediate display"}, + {"target": "LOWER_DIGIT_MINUTE_DISPLAY", "label": "Lower digit minute display"} + ], + "target": "DisplayIDs", + "deactivated": -1, + "min": 1, + "max": 7 + }, + { + "label": "Enable automatic brightness adjustments based on a light sensor", + "type": "bool", + "target": "ENABLE_LIGHT_SENSOR" + }, + { + "label": "ADC pin to which the light sensor is connected to", + "type": "int", + "target": "LIGHT_SENSOR_PIN", + "min": 0, + "max": 42, + "onlyIf": "ENABLE_LIGHT_SENSOR" + }, + { + "label": "How many measurements shall be averaged. Higher number -> smoother but slower change", + "type": "int", + "target": "LIGHT_SENSOR_AVERAGE", + "min": 0, + "max": 255, + "onlyIf": "ENABLE_LIGHT_SENSOR" + }, + { + "label": "Width of the median calculation. Higher number -> smoother change Should never be higher than the LIGHT_SENSOR_AVERAGE", + "type": "int", + "target": "LIGHT_SENSOR_MEDIAN_WIDTH", + "min": 0, + "max": "LIGHT_SENSOR_AVERAGE", + "onlyIf": "ENABLE_LIGHT_SENSOR" + }, + { + "label": "Time that should pass in MS before the light sensor is read again. Higher number -> slower adjustments but also changes will be more sudden", + "type": "int", + "target": "LIGHT_SENSOR_READ_DELAY", + "min": 0, + "max": 65535, + "onlyIf": "ENABLE_LIGHT_SENSOR" + }, + { + "label": "AnalogRead value if the light sensor reads complete darkness", + "type": "int", + "target": "LIGHT_SENSOR_MIN", + "min": 0, + "max": 4095, + "onlyIf": "ENABLE_LIGHT_SENSOR" + }, + { + "label": "AnalogRead value if the light sensor reads the brightest", + "type": "int", + "target": "LIGHT_SENSOR_MAX", + "min": 0, + "max": 4095, + "onlyIf": "ENABLE_LIGHT_SENSOR" + }, + { + "label": "How much the can the light sensor values influence the led brightness", + "type": "int", + "target": "LIGHT_SENSOR_SENSITIVITY", + "min": 0, + "max": 255, + "onlyIf": "ENABLE_LIGHT_SENSOR" + }, + { + "label": "(Debugging Feature) Enable for a demo of all the digit transitions", + "type": "bool", + "target": "TIME_MANAGER_DEMO_MODE" + }, + { + "label": "(Debugging Feature) The minimum delay between led updates in MS. Usually anything faster than 20MS causes problems. Increase if your LEDs do not refresh properly", + "type": "int", + "target": "FASTLED_SAFE_DELAY_MS", + "min": 0, + "max": 65535 + } + ] +} diff --git a/data/index.html b/data/index.html new file mode 100644 index 0000000..c8d2b2b --- /dev/null +++ b/data/index.html @@ -0,0 +1,15 @@ + + + + + + + LED Pixel Clock Configuration + + + + +
+ + + \ No newline at end of file diff --git a/lib/LED_clock/Config/Animations/clock-only/Animations.cpp b/lib/LED_clock/Config/Animations/clock-only/Animations.cpp index a88b201..f1c22d8 100644 --- a/lib/LED_clock/Config/Animations/clock-only/Animations.cpp +++ b/lib/LED_clock/Config/Animations/clock-only/Animations.cpp @@ -104,7 +104,7 @@ Animator::ComplexAmination* InitIndefiniteLoadingAnimation(uint16_t totalAnimati step18->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToBottom, NO_ANIMATION}; step18->easingEffects = new EasingBase*[LENGTH] {NO_EASING, NO_EASING}; - LinkedList* allSteps = new LinkedList(); + DynamicList* allSteps = new DynamicList(); allSteps->add(step0); allSteps->add(step1); allSteps->add(step2); @@ -209,7 +209,7 @@ Animator::ComplexAmination* InitLoadingProgressAnimation(uint16_t totalAnimation step17->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateInToBottom}; step17->easingEffects = new EasingBase*[LENGTH] {NO_EASING}; - LinkedList* allSteps = new LinkedList(); + DynamicList* allSteps = new DynamicList(); allSteps->add(step0); allSteps->add(step1); allSteps->add(step2); diff --git a/lib/LED_clock/Config/Animations/default/Animations.cpp b/lib/LED_clock/Config/Animations/default/Animations.cpp index 2c05cd4..3fb1f42 100644 --- a/lib/LED_clock/Config/Animations/default/Animations.cpp +++ b/lib/LED_clock/Config/Animations/default/Animations.cpp @@ -95,7 +95,7 @@ Animator::ComplexAmination* InitIndefiniteLoadingAnimation(uint16_t totalAnimati step16->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToBottom, NO_ANIMATION}; step16->easingEffects = new EasingBase*[LENGTH] {NO_EASING, NO_EASING}; - LinkedList* allSteps = new LinkedList(); + DynamicList* allSteps = new DynamicList(); allSteps->add(step0); allSteps->add(step1); allSteps->add(step2); @@ -191,7 +191,7 @@ Animator::ComplexAmination* InitLoadingProgressAnimation(uint16_t totalAnimation step15->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateInToBottom}; step15->easingEffects = new EasingBase*[LENGTH] {NO_EASING}; - LinkedList* allSteps = new LinkedList(); + DynamicList* allSteps = new DynamicList(); allSteps->add(step0); allSteps->add(step1); allSteps->add(step2); diff --git a/lib/LED_clock/Config/Animations/diy-machines/Animations.cpp b/lib/LED_clock/Config/Animations/diy-machines/Animations.cpp index 319f3a2..968e9f3 100644 --- a/lib/LED_clock/Config/Animations/diy-machines/Animations.cpp +++ b/lib/LED_clock/Config/Animations/diy-machines/Animations.cpp @@ -55,7 +55,7 @@ Animator::ComplexAmination* InitIndefiniteLoadingAnimation(uint16_t totalAnimati step6->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToBottom, NO_ANIMATION}; step6->easingEffects = new EasingBase*[LENGTH] {NO_EASING, NO_EASING}; - LinkedList* allSteps = new LinkedList(); + DynamicList* allSteps = new DynamicList(); allSteps->add(step0); allSteps->add(step1); allSteps->add(step2); @@ -100,7 +100,7 @@ Animator::ComplexAmination* InitLoadingProgressAnimation(uint16_t totalAnimation step5->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateInToBottom}; step5->easingEffects = new EasingBase*[LENGTH] {NO_EASING}; - LinkedList* allSteps = new LinkedList(); + DynamicList* allSteps = new DynamicList(); allSteps->add(step0); allSteps->add(step1); allSteps->add(step2); diff --git a/lib/LED_clock/Config/Blynk/default/BlynkConfig.cpp b/lib/LED_clock/Config/Blynk/default/BlynkConfig.cpp index 47202d3..f9c2aa5 100644 --- a/lib/LED_clock/Config/Blynk/default/BlynkConfig.cpp +++ b/lib/LED_clock/Config/Blynk/default/BlynkConfig.cpp @@ -6,402 +6,433 @@ #include "BlynkConfig.h" -#if IS_BLYNK_ACTIVE == true +/** + * \note if you use a different controller make sure to change the include here + */ +#include - /** - * \note if you use a different controller make sure to change the include here - */ - #include +BlynkConfig* BlynkConfig::instance = nullptr; +BlynkConfig* BlynkC = nullptr; +ClockState* ClockS = nullptr; +TimeManager* TimeM = nullptr; - BlynkConfig* BlynkConfig::instance = nullptr; - BlynkConfig* BlynkC = BlynkConfig::getInstance(); - ClockState* ClockS = ClockState::getInstance(); - TimeManager* TimeM = TimeManager::getInstance(); +bool blynkReconfigure(ConfigManager* config); - /** - * \brief Construct a new Blynk Config object. Also poulate all variables of the class with meaningful values - * - */ - BlynkConfig::BlynkConfig() - { - ShelfDisplays = DisplayManager::getInstance(); - isClearAction = false; - ColorSelection = CHANGE_HOURS_COLOR; - blynkUIUpdateRequired = false; - } +/** + * \brief Construct a new Blynk Config object. Also poulate all variables of the class with meaningful values + * + */ +BlynkConfig::BlynkConfig() +{ + BlynkC = this; + ClockS = ClockState::getInstance(); + TimeM = TimeManager::getInstance(); + ShelfDisplays = DisplayManager::getInstance(); + config = ConfigManager::getInstance(); + isClearAction = false; + ColorSelection = CHANGE_HOURS_COLOR; + blynkUIUpdateRequired = false; + isTaskRunning = false; + config->registerOnChangedCallback(ConfigManager::BLYNK_CONFIG_CHANGED, blynkReconfigure); +} - /** - * \brief Destroy the Blynk Config object. Ensure proper deletion - * - */ - BlynkConfig::~BlynkConfig() - { - instance = nullptr; - } +/** + * \brief Destroy the Blynk Config object. Ensure proper deletion + * + */ +BlynkConfig::~BlynkConfig() +{ + instance = nullptr; +} - /** - * \brief Either instantiate a new BlynkConfig object by calling the private constructor and return it's address - * or it an instance of it already exists just return that. - * - * \return BlynkConfig* address of the new/already existing blynk config object - */ - BlynkConfig* BlynkConfig::getInstance() +/** + * \brief Either instantiate a new BlynkConfig object by calling the private constructor and return it's address + * or it an instance of it already exists just return that. + * + * \return BlynkConfig* address of the new/already existing blynk config object + */ +BlynkConfig* BlynkConfig::getInstance() +{ + if(instance == nullptr) { - if(instance == nullptr) - { - instance = new BlynkConfig(); - } - return instance; + instance = new BlynkConfig(); } + return instance; +} - /** - * \brief Terminates the blynk task running on the second core - * - */ - void BlynkConfig::stop() +/** + * \brief Terminates the blynk task running on the second core + * + */ +void BlynkConfig::stop() +{ + if(isTaskRunning == true) { vTaskDelete(BlynkLoop); + isTaskRunning = false; } +} - /** - * \brief Code for the second thread running on the second core of the ESP handling all the blynk code since - * all of it is coded in a blocking way and we don't want to influence the animation smoothness - * - */ - void BlynkLoopCode(void* pvParameters) +/** + * \brief Code for the second thread running on the second core of the ESP handling all the blynk code since + * all of it is coded in a blocking way and we don't want to influence the animation smoothness + * + */ +void BlynkLoopCode(void* pvParameters) +{ + Serial.printf("Loop task running on core %d\n\r", xPortGetCoreID()); + BlynkConfig* blynkC = BlynkConfig::getInstance(); + for(;;) { - Serial.printf("Loop task running on core %d\n\r", xPortGetCoreID()); - BlynkConfig* blynkC = BlynkConfig::getInstance(); - for(;;) - { - #if IS_BLYNK_ACTIVE == true - Blynk.run(); - if(blynkC->blynkUIUpdateRequired == true) + Blynk.run(); + if(blynkC->blynkUIUpdateRequired == true) + { + blynkC->blynkUIUpdateRequired = false; + if(ClockS->getMode() == ClockState::CLOCK_MODE) { - blynkC->blynkUIUpdateRequired = false; - if(ClockS->getMode() == ClockState::CLOCK_MODE) - { - Blynk.virtualWrite(BLYNK_CHANNEL_TIMER_START_BUTTON, 0); - } - else if(ClockS->getMode() == ClockState::ALARM_NOTIFICATION) - { - Blynk.setProperty(BLYNK_CHANNEL_ALARM_START_BUTTON, "onLabel", "Clear"); - } - else - { - // TimeManager::TimeInfo currentTimerValue = timeM->getCurrentTime(); - // int time = currentTimerValue.hours * 3600 + currentTimerValue.minutes * 60 + currentTimerValue.seconds; - // Blynk.virtualWrite(BLYNK_CHANNEL_TIMER_START_BUTTON, time, 0); - } + Blynk.virtualWrite(BLYNK_CHANNEL_TIMER_START_BUTTON, 0); } - #endif - delay(1); //needed to serve the wdg - } + else if(ClockS->getMode() == ClockState::ALARM_NOTIFICATION) + { + Blynk.setProperty(BLYNK_CHANNEL_ALARM_START_BUTTON, "onLabel", "Clear"); + } + else + { + // TimeManager::TimeInfo currentTimerValue = timeM->getCurrentTime(); + // int time = currentTimerValue.hours * 3600 + currentTimerValue.minutes * 60 + currentTimerValue.seconds; + // Blynk.virtualWrite(BLYNK_CHANNEL_TIMER_START_BUTTON, time, 0); + } + } + delay(1); //needed to serve the wdg } +} - /** - * \brief call Blynk.config and start the separate thread on the second core - * - */ - void BlynkConfig::setup() +/** + * \brief call Blynk.config and start the separate thread on the second core + * + */ +void BlynkConfig::setup() +{ + if(config->getProperty(ConfigManager::BASE_CONFIG, BLYNK_ACTIVE_KEY) == true && isTaskRunning == false) { - Blynk.config(BLYNK_AUTH_TOKEN, BLYNK_SERVER, 80); + Blynk.config(config->getProperty(ConfigManager::BASE_CONFIG, BLYNK_TOKEN_KEY), + config->getProperty(ConfigManager::BASE_CONFIG, BLYNK_SERVER_KEY), 80); Serial.println("Starting blynk on core 0..."); - //Setup the loop task on the second core + //Setup Blynk in another task because it is coded in a blocking way xTaskCreatePinnedToCore( BlynkLoopCode, // Task function. "BlynkLoop", // name of task. - 10000, // Stack size of task + 8192, // Stack size of task NULL, // parameter of the task 1, // priority of the task &BlynkLoop, // Task handle to keep track of created task 0); // pin task to core 0 + isTaskRunning = true; } +} - /** - * \brief Notify the Blynk thread that a UI update is needed. - * What exactly needs to be updated will be figured out in the thread loop itself. - * - */ - void BlynkConfig::updateUI() +void BlynkConfig::setEnabled(bool enabled) +{ + if(enabled == true) { - blynkUIUpdateRequired = true; - } - - /** - * \brief Sync all channels as soon as blynk connects to the server - * Setup default UI states - * - */ - BLYNK_CONNECTED() + setup(); + } else if(enabled == false) { - Blynk.syncVirtual(BLYNK_CHANNEL_BRIGHTNESS_SLIDER); - Blynk.syncVirtual(BLYNK_CHANNEL_LIGHT_GROUP_SELECTOR); - Blynk.syncVirtual(BLYNK_CHANNEL_CURRENT_COLOR_PICKER); - Blynk.syncVirtual(BLYNK_CHANNEL_TIMER_TIME_INPUT); - Blynk.syncVirtual(BLYNK_CHANNEL_HOUR_COLOR_SAVE); - Blynk.syncVirtual(BLYNK_CHANNEL_MINUTE_COLOR_SAVE); - Blynk.syncVirtual(BLYNK_CHANNEL_INTERNAL_COLOR_SAVE); - Blynk.syncVirtual(BLYNK_CHANNEL_DOT_COLOR_SAVE); - Blynk.syncVirtual(BLYNK_CHANNEL_NIGHT_MODE_BRIGHTNESS); - Blynk.syncVirtual(BLYNK_CHANNEL_NIGHT_MODE_TIME_INPUT); - Blynk.syncVirtual(BLYNK_CHANNEL_NUM_SEPERATION_DOTS); - Blynk.syncVirtual(BLYNK_CHANNEL_ALARM_START_BUTTON); - Blynk.syncVirtual(BLYNK_CHANNEL_ALARM_TIME_INPUT); - Blynk.virtualWrite(BLYNK_CHANNEL_TIMER_START_BUTTON, 0); + stop(); } +} - /** - * \brief Handle a brigness change by the user through the blynk app - * - */ - BLYNK_WRITE(BLYNK_CHANNEL_BRIGHTNESS_SLIDER) - { - ClockS->clockBrightness = param[0].asInt(); - BlynkC->ShelfDisplays->setGlobalBrightness(ClockS->clockBrightness); - } +bool blynkReconfigure(ConfigManager* config) +{ + Blynk.config(config->getProperty(ConfigManager::BASE_CONFIG, BLYNK_TOKEN_KEY), + config->getProperty(ConfigManager::BASE_CONFIG, BLYNK_SERVER_KEY), 80); + BlynkC->setEnabled(config->getProperty(ConfigManager::BASE_CONFIG, BLYNK_ACTIVE_KEY)); + return SYSTEM_RESTART_REQUIRED; +} - /** - * \brief Handle the change of the light group selector by the user through the blynk app - * - */ - BLYNK_WRITE(BLYNK_CHANNEL_LIGHT_GROUP_SELECTOR) - { - switch (param.asInt()) - { - case 1: - BlynkC->ColorSelection = BlynkConfig::CHANGE_HOURS_COLOR; - Blynk.virtualWrite(BLYNK_CHANNEL_CURRENT_COLOR_PICKER, BlynkC->HourColor.r, BlynkC->HourColor.g, BlynkC->HourColor.b); - break; - case 2: - BlynkC->ColorSelection = BlynkConfig::CHANGE_MINUTES_COLOR; - Blynk.virtualWrite(BLYNK_CHANNEL_CURRENT_COLOR_PICKER, BlynkC->MinuteColor.r, BlynkC->MinuteColor.g, BlynkC->MinuteColor.b); - break; - case 3: - BlynkC->ColorSelection = BlynkConfig::CHANGE_INTERRIOR_COLOR; - Blynk.virtualWrite(BLYNK_CHANNEL_CURRENT_COLOR_PICKER, BlynkC->InternalColor.r, BlynkC->InternalColor.g, BlynkC->InternalColor.b); - break; - case 4: - BlynkC->ColorSelection = BlynkConfig::CHANGE_DOT_COLOR; - Blynk.virtualWrite(BLYNK_CHANNEL_CURRENT_COLOR_PICKER, BlynkC->DotColor.r, BlynkC->DotColor.g, BlynkC->DotColor.b); - break; - } - } +/** + * \brief Notify the Blynk thread that a UI update is needed. + * What exactly needs to be updated will be figured out in the thread loop itself. + * + */ +void BlynkConfig::updateUI() +{ + blynkUIUpdateRequired = true; +} - /** - * \brief Handle a color change for the currently selected light group by the user through the blynk app - * - */ - BLYNK_WRITE(BLYNK_CHANNEL_CURRENT_COLOR_PICKER) - { - CRGB currentColor; - currentColor.r = param[0].asInt(); - currentColor.g = param[1].asInt(); - currentColor.b = param[2].asInt(); - switch (BlynkC->ColorSelection) - { - case BlynkConfig::CHANGE_HOURS_COLOR: - BlynkC->ShelfDisplays->setHourSegmentColors(currentColor); - Blynk.virtualWrite(BLYNK_CHANNEL_HOUR_COLOR_SAVE, currentColor.r, currentColor.g, currentColor.b); - BlynkC->HourColor = currentColor; - break; - case BlynkConfig::CHANGE_MINUTES_COLOR: - BlynkC->ShelfDisplays->setMinuteSegmentColors(currentColor); - Blynk.virtualWrite(BLYNK_CHANNEL_MINUTE_COLOR_SAVE, currentColor.r, currentColor.g, currentColor.b); - BlynkC->MinuteColor = currentColor; - break; - case BlynkConfig::CHANGE_INTERRIOR_COLOR: - BlynkC->ShelfDisplays->setInternalLEDColor(currentColor); - Blynk.virtualWrite(BLYNK_CHANNEL_INTERNAL_COLOR_SAVE, currentColor.r, currentColor.g, currentColor.b); - BlynkC->InternalColor = currentColor; - break; - case BlynkConfig::CHANGE_DOT_COLOR: - BlynkC->ShelfDisplays->setDotLEDColor(currentColor); - Blynk.virtualWrite(BLYNK_CHANNEL_DOT_COLOR_SAVE, currentColor.r, currentColor.g, currentColor.b); - BlynkC->DotColor = currentColor; - break; - } - } +/** + * \brief Sync all channels as soon as blynk connects to the server + * Setup default UI states + * + */ +BLYNK_CONNECTED() +{ + Blynk.syncVirtual(BLYNK_CHANNEL_BRIGHTNESS_SLIDER); + Blynk.syncVirtual(BLYNK_CHANNEL_LIGHT_GROUP_SELECTOR); + Blynk.syncVirtual(BLYNK_CHANNEL_CURRENT_COLOR_PICKER); + Blynk.syncVirtual(BLYNK_CHANNEL_TIMER_TIME_INPUT); + Blynk.syncVirtual(BLYNK_CHANNEL_HOUR_COLOR_SAVE); + Blynk.syncVirtual(BLYNK_CHANNEL_MINUTE_COLOR_SAVE); + Blynk.syncVirtual(BLYNK_CHANNEL_INTERNAL_COLOR_SAVE); + Blynk.syncVirtual(BLYNK_CHANNEL_DOT_COLOR_SAVE); + Blynk.syncVirtual(BLYNK_CHANNEL_NIGHT_MODE_BRIGHTNESS); + Blynk.syncVirtual(BLYNK_CHANNEL_NIGHT_MODE_TIME_INPUT); + Blynk.syncVirtual(BLYNK_CHANNEL_NUM_SEPERATION_DOTS); + Blynk.syncVirtual(BLYNK_CHANNEL_ALARM_START_BUTTON); + Blynk.syncVirtual(BLYNK_CHANNEL_ALARM_TIME_INPUT); + Blynk.virtualWrite(BLYNK_CHANNEL_TIMER_START_BUTTON, 0); +} - /** - * \brief Use an unused virtual channel to permanently store the color for the hours light group to be able - * to restore it's state after a reset. - * - */ - BLYNK_WRITE(BLYNK_CHANNEL_HOUR_COLOR_SAVE) - { - CRGB SavedColor; - SavedColor.r = param[0].asInt(); - SavedColor.g = param[1].asInt(); - SavedColor.b = param[2].asInt(); - BlynkC->ShelfDisplays->setHourSegmentColors(SavedColor); - BlynkC->HourColor = SavedColor; - } +/** + * \brief Handle a brigness change by the user through the blynk app + * + */ +BLYNK_WRITE(BLYNK_CHANNEL_BRIGHTNESS_SLIDER) +{ + ClockS->clockBrightness = param[0].asInt(); + BlynkC->ShelfDisplays->setGlobalBrightness(ClockS->clockBrightness); +} - /** - * \brief Use an unused virtual channel to permanently store the color for the minutes light group to be able - * to restore it's state after a reset. - * - */ - BLYNK_WRITE(BLYNK_CHANNEL_MINUTE_COLOR_SAVE) +/** + * \brief Handle the change of the light group selector by the user through the blynk app + * + */ +BLYNK_WRITE(BLYNK_CHANNEL_LIGHT_GROUP_SELECTOR) +{ + switch (param.asInt()) { - CRGB SavedColor; - SavedColor.r = param[0].asInt(); - SavedColor.g = param[1].asInt(); - SavedColor.b = param[2].asInt(); - BlynkC->ShelfDisplays->setMinuteSegmentColors(SavedColor); - BlynkC->MinuteColor = SavedColor; + case 1: + BlynkC->ColorSelection = BlynkConfig::CHANGE_HOURS_COLOR; + Blynk.virtualWrite(BLYNK_CHANNEL_CURRENT_COLOR_PICKER, BlynkC->HourColor.r, BlynkC->HourColor.g, BlynkC->HourColor.b); + break; + case 2: + BlynkC->ColorSelection = BlynkConfig::CHANGE_MINUTES_COLOR; + Blynk.virtualWrite(BLYNK_CHANNEL_CURRENT_COLOR_PICKER, BlynkC->MinuteColor.r, BlynkC->MinuteColor.g, BlynkC->MinuteColor.b); + break; + case 3: + BlynkC->ColorSelection = BlynkConfig::CHANGE_INTERRIOR_COLOR; + Blynk.virtualWrite(BLYNK_CHANNEL_CURRENT_COLOR_PICKER, BlynkC->InternalColor.r, BlynkC->InternalColor.g, BlynkC->InternalColor.b); + break; + case 4: + BlynkC->ColorSelection = BlynkConfig::CHANGE_DOT_COLOR; + Blynk.virtualWrite(BLYNK_CHANNEL_CURRENT_COLOR_PICKER, BlynkC->DotColor.r, BlynkC->DotColor.g, BlynkC->DotColor.b); + break; } +} - /** - * \brief Use an unused virtual channel to permanently store the color for the internal light group to be able - * to restore it's state after a reset. - * - */ - BLYNK_WRITE(BLYNK_CHANNEL_INTERNAL_COLOR_SAVE) +/** + * \brief Handle a color change for the currently selected light group by the user through the blynk app + * + */ +BLYNK_WRITE(BLYNK_CHANNEL_CURRENT_COLOR_PICKER) +{ + CRGB currentColor; + currentColor.r = param[0].asInt(); + currentColor.g = param[1].asInt(); + currentColor.b = param[2].asInt(); + switch (BlynkC->ColorSelection) { - CRGB SavedColor; - SavedColor.r = param[0].asInt(); - SavedColor.g = param[1].asInt(); - SavedColor.b = param[2].asInt(); - BlynkC->ShelfDisplays->setInternalLEDColor(SavedColor); - BlynkC->InternalColor = SavedColor; + case BlynkConfig::CHANGE_HOURS_COLOR: + BlynkC->ShelfDisplays->setHourSegmentColors(currentColor); + Blynk.virtualWrite(BLYNK_CHANNEL_HOUR_COLOR_SAVE, currentColor.r, currentColor.g, currentColor.b); + BlynkC->HourColor = currentColor; + break; + case BlynkConfig::CHANGE_MINUTES_COLOR: + BlynkC->ShelfDisplays->setMinuteSegmentColors(currentColor); + Blynk.virtualWrite(BLYNK_CHANNEL_MINUTE_COLOR_SAVE, currentColor.r, currentColor.g, currentColor.b); + BlynkC->MinuteColor = currentColor; + break; + case BlynkConfig::CHANGE_INTERRIOR_COLOR: + BlynkC->ShelfDisplays->setInternalLEDColor(currentColor); + Blynk.virtualWrite(BLYNK_CHANNEL_INTERNAL_COLOR_SAVE, currentColor.r, currentColor.g, currentColor.b); + BlynkC->InternalColor = currentColor; + break; + case BlynkConfig::CHANGE_DOT_COLOR: + BlynkC->ShelfDisplays->setDotLEDColor(currentColor); + Blynk.virtualWrite(BLYNK_CHANNEL_DOT_COLOR_SAVE, currentColor.r, currentColor.g, currentColor.b); + BlynkC->DotColor = currentColor; + break; } +} - /** - * \brief Use an unused virtual channel to permanently store the color for the dot light group to be able - * to restore it's state after a reset. - * - */ - BLYNK_WRITE(BLYNK_CHANNEL_DOT_COLOR_SAVE) - { - CRGB SavedColor; - SavedColor.r = param[0].asInt(); - SavedColor.g = param[1].asInt(); - SavedColor.b = param[2].asInt(); - BlynkC->ShelfDisplays->setDotLEDColor(SavedColor); - BlynkC->DotColor = SavedColor; - } +/** + * \brief Use an unused virtual channel to permanently store the color for the hours light group to be able + * to restore it's state after a reset. + * + */ +BLYNK_WRITE(BLYNK_CHANNEL_HOUR_COLOR_SAVE) +{ + CRGB SavedColor; + SavedColor.r = param[0].asInt(); + SavedColor.g = param[1].asInt(); + SavedColor.b = param[2].asInt(); + BlynkC->ShelfDisplays->setHourSegmentColors(SavedColor); + BlynkC->HourColor = SavedColor; +} + +/** + * \brief Use an unused virtual channel to permanently store the color for the minutes light group to be able + * to restore it's state after a reset. + * + */ +BLYNK_WRITE(BLYNK_CHANNEL_MINUTE_COLOR_SAVE) +{ + CRGB SavedColor; + SavedColor.r = param[0].asInt(); + SavedColor.g = param[1].asInt(); + SavedColor.b = param[2].asInt(); + BlynkC->ShelfDisplays->setMinuteSegmentColors(SavedColor); + BlynkC->MinuteColor = SavedColor; +} + +/** + * \brief Use an unused virtual channel to permanently store the color for the internal light group to be able + * to restore it's state after a reset. + * + */ +BLYNK_WRITE(BLYNK_CHANNEL_INTERNAL_COLOR_SAVE) +{ + CRGB SavedColor; + SavedColor.r = param[0].asInt(); + SavedColor.g = param[1].asInt(); + SavedColor.b = param[2].asInt(); + BlynkC->ShelfDisplays->setInternalLEDColor(SavedColor); + BlynkC->InternalColor = SavedColor; +} + +/** + * \brief Use an unused virtual channel to permanently store the color for the dot light group to be able + * to restore it's state after a reset. + * + */ +BLYNK_WRITE(BLYNK_CHANNEL_DOT_COLOR_SAVE) +{ + CRGB SavedColor; + SavedColor.r = param[0].asInt(); + SavedColor.g = param[1].asInt(); + SavedColor.b = param[2].asInt(); + BlynkC->ShelfDisplays->setDotLEDColor(SavedColor); + BlynkC->DotColor = SavedColor; +} - /** - * \brief Handle a timer duration change by the user through the blynk app - * - */ - BLYNK_WRITE(BLYNK_CHANNEL_TIMER_TIME_INPUT) +/** + * \brief Handle a timer duration change by the user through the blynk app + * + */ +BLYNK_WRITE(BLYNK_CHANNEL_TIMER_TIME_INPUT) +{ + TimeManager::TimeInfo TimerDuration; + TimeInputParam t(param); + TimerDuration.hours = t.getStartHour(); + TimerDuration.minutes = t.getStartMinute(); + TimerDuration.seconds = t.getStartSecond(); + //Serial.printf("StartTime: %d:%d:%d\n\r", TimerDuration.hours, TimerDuration.minutes, TimerDuration.seconds); + TimeM->setTimerDuration(TimerDuration); +} + +/** + * \brief Handle the start of the timer by the user through the blynk app + * + */ +BLYNK_WRITE(BLYNK_CHANNEL_TIMER_START_BUTTON) +{ + if(param[0].asInt() == 1) { - TimeManager::TimeInfo TimerDuration; - TimeInputParam t(param); - TimerDuration.hours = t.getStartHour(); - TimerDuration.minutes = t.getStartMinute(); - TimerDuration.seconds = t.getStartSecond(); - //Serial.printf("StartTime: %d:%d:%d\n\r", TimerDuration.hours, TimerDuration.minutes, TimerDuration.seconds); - TimeM->setTimerDuration(TimerDuration); + TimeM->startTimer(); + Serial.println("Timer Started"); + ClockS->switchMode(ClockState::TIMER_MODE); } - - /** - * \brief Handle the start of the timer by the user through the blynk app - * - */ - BLYNK_WRITE(BLYNK_CHANNEL_TIMER_START_BUTTON) + else { - if(param[0].asInt() == 1) - { - TimeM->startTimer(); - Serial.println("Timer Started"); - ClockS->switchMode(ClockState::TIMER_MODE); - } - else - { - TimeM->stopTimer(); - Serial.println("Timer Stopped"); - Blynk.syncVirtual(BLYNK_CHANNEL_TIMER_TIME_INPUT); - BlynkC->ShelfDisplays->setGlobalBrightness(ClockS->clockBrightness); - ClockS->switchMode(ClockState::CLOCK_MODE); - } + TimeM->stopTimer(); + Serial.println("Timer Stopped"); + Blynk.syncVirtual(BLYNK_CHANNEL_TIMER_TIME_INPUT); + BlynkC->ShelfDisplays->setGlobalBrightness(ClockS->clockBrightness); + ClockS->switchMode(ClockState::CLOCK_MODE); } +} + +/** + * \brief Handle the nightime window setting change by the user through the blynk app + * + */ +BLYNK_WRITE(BLYNK_CHANNEL_NIGHT_MODE_TIME_INPUT) +{ + TimeInputParam t(param); + ClockS->NightModeStartTime.hours = t.getStartHour(); + ClockS->NightModeStartTime.minutes = t.getStartMinute(); + ClockS->NightModeStartTime.seconds = t.getStartSecond(); + ClockS->NightModeStopTime.hours = t.getStopHour(); + ClockS->NightModeStopTime.minutes = t.getStopMinute(); + ClockS->NightModeStopTime.seconds = t.getStopSecond(); +} - /** - * \brief Handle the nightime window setting change by the user through the blynk app - * - */ - BLYNK_WRITE(BLYNK_CHANNEL_NIGHT_MODE_TIME_INPUT) +/** + * \brief Handle the nighttime brightness setting change by the user through the blynk app + * + */ +BLYNK_WRITE(BLYNK_CHANNEL_NIGHT_MODE_BRIGHTNESS) +{ + ClockS->nightModeBrightness = param[0].asInt(); + if(TimeM->isInBetween(ClockS->NightModeStartTime, ClockS->NightModeStopTime)) { - TimeInputParam t(param); - ClockS->NightModeStartTime.hours = t.getStartHour(); - ClockS->NightModeStartTime.minutes = t.getStartMinute(); - ClockS->NightModeStartTime.seconds = t.getStartSecond(); - ClockS->NightModeStopTime.hours = t.getStopHour(); - ClockS->NightModeStopTime.minutes = t.getStopMinute(); - ClockS->NightModeStopTime.seconds = t.getStopSecond(); + BlynkC->ShelfDisplays->setGlobalBrightness(ClockS->nightModeBrightness); } +} - /** - * \brief Handle the nighttime brightness setting change by the user through the blynk app - * - */ - BLYNK_WRITE(BLYNK_CHANNEL_NIGHT_MODE_BRIGHTNESS) +/** + * \brief Handle the change of the dots display setting by the user through the blynk app + * + */ +BLYNK_WRITE(BLYNK_CHANNEL_NUM_SEPERATION_DOTS) +{ + ClockS->numDots = param[0].asInt() - 1; +} + +/** + * \brief Handle an alarm time change by the user through the blynk app + * + */ +BLYNK_WRITE(BLYNK_CHANNEL_ALARM_TIME_INPUT) +{ + TimeInputParam t(param); + TimeManager::TimeInfo AlarmTime + { + .hours = (uint8_t)t.getStartHour(), + .minutes = (uint8_t)t.getStartMinute(), + .seconds = (uint8_t)t.getStartSecond() + }; + uint8_t activeDays = 0x00; + for (uint8_t i = 0; i < 7; i++) { - ClockS->nightModeBrightness = param[0].asInt(); - if(TimeM->isInBetween(ClockS->NightModeStartTime, ClockS->NightModeStopTime)) + if (t.isWeekdaySelected(i + 1)) // 1 to 7 for every weekday in blynk config { - BlynkC->ShelfDisplays->setGlobalBrightness(ClockS->nightModeBrightness); + activeDays |= (1 << i); } } + TimeM->setAlarmTime(AlarmTime, (TimeManager::Weekdays)activeDays); +} - /** - * \brief Handle the change of the dots display setting by the user through the blynk app - * - */ - BLYNK_WRITE(BLYNK_CHANNEL_NUM_SEPERATION_DOTS) +/** + * \brief Handle the activation/deactivation/clearing of the alarm function by the user through the blynk app + * + */ +BLYNK_WRITE(BLYNK_CHANNEL_ALARM_START_BUTTON) +{ + if(ClockS->getMode() == ClockState::ALARM_NOTIFICATION) { - ClockS->numDots = param[0].asInt() - 1; + Blynk.setProperty(BLYNK_CHANNEL_ALARM_START_BUTTON, "onLabel", "Deactivate"); + TimeM->clearAlarm(); + Blynk.virtualWrite(BLYNK_CHANNEL_ALARM_START_BUTTON, 1); + BlynkC->isClearAction = true; } - - /** - * \brief Handle an alarm time change by the user through the blynk app - * - */ - BLYNK_WRITE(BLYNK_CHANNEL_ALARM_TIME_INPUT) + else if(BlynkC->isClearAction == true) { - TimeInputParam t(param); - TimeManager::TimeInfo AlarmTime - { - .hours = (uint8_t)t.getStartHour(), - .minutes = (uint8_t)t.getStartMinute(), - .seconds = (uint8_t)t.getStartSecond() - }; - uint8_t activeDays = 0x00; - for (uint8_t i = 0; i < 7; i++) - { - if (t.isWeekdaySelected(i + 1)) // 1 to 7 for every weekday in blynk config - { - activeDays |= (1 << i); - } - } - TimeM->setAlarmTime(AlarmTime, (TimeManager::Weekdays)activeDays); + BlynkC->isClearAction = false; } - - /** - * \brief Handle the activation/deactivation/clearing of the alarm function by the user through the blynk app - * - */ - BLYNK_WRITE(BLYNK_CHANNEL_ALARM_START_BUTTON) + else { - if(ClockS->getMode() == ClockState::ALARM_NOTIFICATION) - { - Blynk.setProperty(BLYNK_CHANNEL_ALARM_START_BUTTON, "onLabel", "Deactivate"); - TimeM->clearAlarm(); - Blynk.virtualWrite(BLYNK_CHANNEL_ALARM_START_BUTTON, 1); - BlynkC->isClearAction = true; - } - else if(BlynkC->isClearAction == true) - { - BlynkC->isClearAction = false; - } - else - { - TimeM->setAlarmMode(param[0].asInt() == 1); - } + TimeM->setAlarmMode(param[0].asInt() == 1); } -#endif +} diff --git a/lib/LED_clock/Config/Blynk/default/BlynkConfig.h b/lib/LED_clock/Config/Blynk/default/BlynkConfig.h index 2ccd68b..1ed4ab9 100644 --- a/lib/LED_clock/Config/Blynk/default/BlynkConfig.h +++ b/lib/LED_clock/Config/Blynk/default/BlynkConfig.h @@ -8,98 +8,106 @@ #define _BLYNK_CONFIG_H_ #include "Configuration.h" +#include "ConfigManager.h" +#include +#define FASTLED_INTERNAL +#include "FastLED.h" +#include "ClockState.h" -#if IS_BLYNK_ACTIVE == true - #include - #define FASTLED_INTERNAL - #include "FastLED.h" - #include "ClockState.h" +/** + * \addtogroup BlynkChannels + * \brief These are the channel definitions for Blynk + * \{ + */ +#define BLYNK_CHANNEL_BRIGHTNESS_SLIDER V0 +#define BLYNK_CHANNEL_LIGHT_GROUP_SELECTOR V1 +#define BLYNK_CHANNEL_CURRENT_COLOR_PICKER V2 +#define BLYNK_CHANNEL_TIMER_TIME_INPUT V3 +#define BLYNK_CHANNEL_TIMER_START_BUTTON V4 +#define BLYNK_CHANNEL_ALARM_TIME_INPUT V5 +#define BLYNK_CHANNEL_ALARM_START_BUTTON V6 +#define BLYNK_CHANNEL_NIGHT_MODE_TIME_INPUT V7 +#define BLYNK_CHANNEL_NIGHT_MODE_BRIGHTNESS V8 +#define BLYNK_CHANNEL_NUM_SEPERATION_DOTS V9 +#define BLYNK_CHANNEL_HOUR_COLOR_SAVE V10 +#define BLYNK_CHANNEL_MINUTE_COLOR_SAVE V11 +#define BLYNK_CHANNEL_INTERNAL_COLOR_SAVE V12 +#define BLYNK_CHANNEL_DOT_COLOR_SAVE V13 +/** \} */ + +/** + * \brief Loop function which is executed on the second core of the ESP + * + */ +void BlynkLoopCode(void* pvParameters); - /** - * \addtogroup BlynkChannels - * \brief These are the channel definitions for Blynk - * \{ - */ - #define BLYNK_CHANNEL_BRIGHTNESS_SLIDER V0 - #define BLYNK_CHANNEL_LIGHT_GROUP_SELECTOR V1 - #define BLYNK_CHANNEL_CURRENT_COLOR_PICKER V2 - #define BLYNK_CHANNEL_TIMER_TIME_INPUT V3 - #define BLYNK_CHANNEL_TIMER_START_BUTTON V4 - #define BLYNK_CHANNEL_ALARM_TIME_INPUT V5 - #define BLYNK_CHANNEL_ALARM_START_BUTTON V6 - #define BLYNK_CHANNEL_NIGHT_MODE_TIME_INPUT V7 - #define BLYNK_CHANNEL_NIGHT_MODE_BRIGHTNESS V8 - #define BLYNK_CHANNEL_NUM_SEPERATION_DOTS V9 - #define BLYNK_CHANNEL_HOUR_COLOR_SAVE V10 - #define BLYNK_CHANNEL_MINUTE_COLOR_SAVE V11 - #define BLYNK_CHANNEL_INTERNAL_COLOR_SAVE V12 - #define BLYNK_CHANNEL_DOT_COLOR_SAVE V13 - /** \} */ +/** + * \brief Configuration class for storing all Blynk related information + * + */ +class BlynkConfig +{ +private: + static BlynkConfig* instance; + ConfigManager* config; + TaskHandle_t BlynkLoop; + bool isTaskRunning; - /** - * \brief Loop function which is executed on the second core of the ESP - * - */ - void BlynkLoopCode(void* pvParameters); - /** - * \brief Configuration class for storing all Blynk related information - * - */ - class BlynkConfig - { - private: - static BlynkConfig* instance; - TaskHandle_t BlynkLoop; + BlynkConfig(); +public: + /** + * \brief possible selection options of the segmented switch responsible for selecting which color should be + * changed in the App + * + */ + enum ColorSelector {CHANGE_INTERRIOR_COLOR, CHANGE_HOURS_COLOR, CHANGE_MINUTES_COLOR, CHANGE_DOT_COLOR}; + ColorSelector ColorSelection; + CRGB InternalColor; + CRGB HourColor; + CRGB MinuteColor; + CRGB DotColor; + bool blynkUIUpdateRequired; + DisplayManager* ShelfDisplays; + bool isClearAction; - BlynkConfig(); - public: - /** - * \brief possible selection options of the segmented switch responsible for selecting which color should be - * changed in the App - * - */ - enum ColorSelector {CHANGE_INTERRIOR_COLOR, CHANGE_HOURS_COLOR, CHANGE_MINUTES_COLOR, CHANGE_DOT_COLOR}; - ColorSelector ColorSelection; - CRGB InternalColor; - CRGB HourColor; - CRGB MinuteColor; - CRGB DotColor; - bool blynkUIUpdateRequired; - DisplayManager* ShelfDisplays; - bool isClearAction; - ~BlynkConfig(); - /** - * \brief Get the Instance object. - * Use this to get an instance to the Blynk config sigelton instead of its constructor. - * - * \return BlynkConfig* pointer to the already existing or newly created BlynkConfig object. - */ - static BlynkConfig* getInstance(); + ~BlynkConfig(); + /** + * \brief Get the Instance object. + * Use this to get an instance to the Blynk config sigelton instead of its constructor. + * + * \return BlynkConfig* pointer to the already existing or newly created BlynkConfig object. + */ + static BlynkConfig* getInstance(); - /** - * \brief to be called as part of the setup function - * - * \pre WIFI connection has to be established already by calling #wifiSetup before this method - */ - void setup(); + /** + * \brief to be called as part of the setup function + * + * \pre WIFI connection has to be established already by calling #wifiSetup before this method + */ + void setup(); - /** - * \brief Stop the execution of Blynk - * - */ - void stop(); + /** + * \brief Stop the execution of Blynk + * + */ + void stop(); - /** - * \brief Call this if a UI update is needed - * Possible cases for this are that the timer ticked or was triggered or the alarm was triggered - * - */ - void updateUI(); - }; + /** + * \brief Call this if a UI update is needed + * Possible cases for this are that the timer ticked or was triggered or the alarm was triggered + * + */ + void updateUI(); -#endif + /** + * \brief Enable or disable the blynk functionality on runtime + * + * \param enabled if true blynk will be started if not running yet. If false blynk will be stopped if already running + */ + void setEnabled(bool enabled); +}; #endif diff --git a/lib/LED_clock/Config/Setup/clock-only/Configuration.h b/lib/LED_clock/Config/Setup/clock-only/Configuration.h index 4f6a255..3e16d70 100644 --- a/lib/LED_clock/Config/Setup/clock-only/Configuration.h +++ b/lib/LED_clock/Config/Setup/clock-only/Configuration.h @@ -9,80 +9,51 @@ #ifndef _CONFIGURATIONS_H_ #define _CONFIGURATIONS_H_ +//Place all new configs here: + +/** + * \brief Time period in which to retry connecting to the WIFI if the connection got lost + */ +#define BROKEN_WIFI_CONNECT_RETRY 120000 + +// Legacy configs: + /** * \addtogroup MainConfiguration * \brief Main configuration settings * \{ */ + /** - * \brief If you want to run the system in a minimal mode to test some basic functionality or - * debug something it could be useful to disable wifi functionality completley. + * \brief In case the blynk communication is not working this line causes Blynk to send debug output to the serial port. + * If you are not worried about Blynk or have to diagnose some other issue you can comment this line out. */ -#define RUN_WITHOUT_WIFI false - -#if RUN_WITHOUT_WIFI == false - /** - * \brief If you want Blynk functionality set this to true and set your authentication token. Otherwise set it to false - */ - #define IS_BLYNK_ACTIVE true - - #if IS_BLYNK_ACTIVE == true - /** - * \brief If you want Blynk functionality paste your authentication token here. - */ - #define BLYNK_AUTH_TOKEN "AUTH_TOKEN_GOES_HERE" - - /** - * \brief In case the blynk communication is not working this line causes Blynk to send debug output to the serial port. - * If you are not worried about Blynk or have to diagnose some other issue you can comment this line out. - */ - #define BLYNK_PRINT Serial - - /** - * \brief Set the Blynk server address. - * \note I had troubles with using the proper blynk domain so I am using the IP address instead. - * Maybe this could create problems in the future so it is recommended to use the official domain. - */ - #define BLYNK_SERVER "blynk-cloud.com" // "139.59.206.133" - - #endif +#define BLYNK_PRINT Serial - /** - * \brief If you want to use OTA upload instead or in addition to the normal cable upload set this option to true. - * To actually flash something via OTA you have to uncomment the OTA flash lines in the \ref platformio.ini file - * This is a nice addition to cable upload but it doesn't replace it completely. - * If the microcontroller crashes because of bad configuration you still have to use a cable - */ - #define ENABLE_OTA_UPLOAD true +/** + * \brief The host name that shall be used for OTA updates. If you change this here it must also be changed + * in the \ref platformio.ini file + */ +#define OTA_UPDATE_HOST_NAME "LED-Clock" - #if ENABLE_OTA_UPLOAD == true - /** - * \brief The host name that shall be used for OTA updates. If you change this here it must also be changed - * in the \ref platformio.ini file - */ - #define OTA_UPDATE_HOST_NAME "LED-Clock" - #endif +/** + * \brief The number of times the controller tries to connect to wifi before it fails and goes into + * smartConfig mode (if that is enabled) + */ +#define NUM_RETRIES 50 - /** - * \brief The number of times the controller tries to connect to wifi before it fails and goes into - * smartConfig mode (if that is enabled) - */ - #define NUM_RETRIES 50 +/** + * \brief Use the ESP smart config to setup the wifi network. If you want to set it manually set this to false + */ +#define USE_ESPTOUCH_SMART_CONFIG true +#if USE_ESPTOUCH_SMART_CONFIG == false /** - * \brief Use the ESP smart config to setup the wifi network. If you want to set it manually set this to false + * \brief WIFI_SSID and WIFI_PW are only needed if smart setup is disabled */ - #define USE_ESPTOUCH_SMART_CONFIG true - - #if USE_ESPTOUCH_SMART_CONFIG == false - /** - * \brief WIFI_SSID and WIFI_PW are only needed if smart setup is disabled - */ - #define WIFI_SSID "SET_YOUR_SSID_HERE" - #define WIFI_PW "SET_YOUR_PASSWORD_HERE" - #endif - + #define WIFI_SSID "SET_YOUR_SSID_HERE" + #define WIFI_PW "SET_YOUR_PASSWORD_HERE" #endif /** diff --git a/lib/LED_clock/Config/Setup/default/Configuration.h b/lib/LED_clock/Config/Setup/default/Configuration.h index 07f911e..51cfc2b 100644 --- a/lib/LED_clock/Config/Setup/default/Configuration.h +++ b/lib/LED_clock/Config/Setup/default/Configuration.h @@ -9,80 +9,49 @@ #ifndef _CONFIGURATIONS_H_ #define _CONFIGURATIONS_H_ +//Place all new configs here: +#define BROKEN_WIFI_CONNECT_RETRY 120000 + + +// Legacy configs: + + /** * \addtogroup MainConfiguration * \brief Main configuration settings * \{ */ + /** - * \brief If you want to run the system in a minimal mode to test some basic functionality or - * debug something it could be useful to disable wifi functionality completley. + * \brief In case the blynk communication is not working this line causes Blynk to send debug output to the serial port. + * If you are not worried about Blynk or have to diagnose some other issue you can comment this line out. */ -#define RUN_WITHOUT_WIFI false - -#if RUN_WITHOUT_WIFI == false - /** - * \brief If you want Blynk functionality set this to true and set your authentication token. Otherwise set it to false - */ - #define IS_BLYNK_ACTIVE true - - #if IS_BLYNK_ACTIVE == true - /** - * \brief If you want Blynk functionality paste your authentication token here. - */ - #define BLYNK_AUTH_TOKEN "AUTH_TOKEN_GOES_HERE" - - /** - * \brief In case the blynk communication is not working this line causes Blynk to send debug output to the serial port. - * If you are not worried about Blynk or have to diagnose some other issue you can comment this line out. - */ - #define BLYNK_PRINT Serial - - /** - * \brief Set the Blynk server address. - * \note I had troubles with using the proper blynk domain so I am using the IP address instead. - * Maybe this could create problems in the future so it is recommended to use the official domain. - */ - #define BLYNK_SERVER "blynk-cloud.com" // "139.59.206.133" - - #endif +#define BLYNK_PRINT Serial - /** - * \brief If you want to use OTA upload instead or in addition to the normal cable upload set this option to true. - * To actually flash something via OTA you have to uncomment the OTA flash lines in the \ref platformio.ini file - * This is a nice addition to cable upload but it doesn't replace it completely. - * If the microcontroller crashes because of bad configuration you still have to use a cable - */ - #define ENABLE_OTA_UPLOAD true +/** + * \brief The host name that shall be used for OTA updates. If you change this here it must also be changed + * in the \ref platformio.ini file + */ +#define OTA_UPDATE_HOST_NAME "LED-Shelf" - #if ENABLE_OTA_UPLOAD == true - /** - * \brief The host name that shall be used for OTA updates. If you change this here it must also be changed - * in the \ref platformio.ini file - */ - #define OTA_UPDATE_HOST_NAME "LED-Shelf" - #endif +/** + * \brief The number of times the controller tries to connect to wifi before it fails and goes into + * smartConfig mode (if that is enabled) + */ +#define NUM_RETRIES 50 - /** - * \brief The number of times the controller tries to connect to wifi before it fails and goes into - * smartConfig mode (if that is enabled) - */ - #define NUM_RETRIES 50 +/** + * \brief Use the ESP smart config to setup the wifi network. If you want to set it manually set this to false + */ +#define USE_ESPTOUCH_SMART_CONFIG true +#if USE_ESPTOUCH_SMART_CONFIG == false /** - * \brief Use the ESP smart config to setup the wifi network. If you want to set it manually set this to false + * \brief WIFI_SSID and WIFI_PW are only needed if smart setup is disabled */ - #define USE_ESPTOUCH_SMART_CONFIG true - - #if USE_ESPTOUCH_SMART_CONFIG == false - /** - * \brief WIFI_SSID and WIFI_PW are only needed if smart setup is disabled - */ - #define WIFI_SSID "SET_YOUR_SSID_HERE" - #define WIFI_PW "SET_YOUR_PASSWORD_HERE" - #endif - + #define WIFI_SSID "SET_YOUR_SSID_HERE" + #define WIFI_PW "SET_YOUR_PASSWORD_HERE" #endif /** diff --git a/lib/LED_clock/Config/Setup/diy-machines/Configuration.h b/lib/LED_clock/Config/Setup/diy-machines/Configuration.h index e486ac7..d1959c4 100644 --- a/lib/LED_clock/Config/Setup/diy-machines/Configuration.h +++ b/lib/LED_clock/Config/Setup/diy-machines/Configuration.h @@ -16,73 +16,34 @@ */ /** - * \brief If you want to run the system in a minimal mode to test some basic functionality or - * debug something it could be useful to disable wifi functionality completley. + * \brief In case the blynk communication is not working this line causes Blynk to send debug output to the serial port. + * If you are not worried about Blynk or have to diagnose some other issue you can comment this line out. */ -#define RUN_WITHOUT_WIFI false +#define BLYNK_PRINT Serial -#if RUN_WITHOUT_WIFI == false - /** - * \brief If you want Blynk functionality set this to true and set your authentication token. Otherwise set it to false - */ - #define IS_BLYNK_ACTIVE true - - #if IS_BLYNK_ACTIVE == true - /** - * \brief If you want Blynk functionality paste your authentication token here. - */ - #define BLYNK_AUTH_TOKEN "AUTH_TOKEN_GOES_HERE" - - /** - * \brief In case the blynk communication is not working this line causes Blynk to send debug output to the serial port. - * If you are not worried about Blynk or have to diagnose some other issue you can comment this line out. - */ - #define BLYNK_PRINT Serial - - /** - * \brief Set the Blynk server address. - * \note I had troubles with using the proper blynk domain so I am using the IP address instead. - * Maybe this could create problems in the future so it is recommended to use the official domain. - */ - #define BLYNK_SERVER "blynk-cloud.com" // "139.59.206.133" - - #endif - - /** - * \brief If you want to use OTA upload instead or in addition to the normal cable upload set this option to true. - * To actually flash something via OTA you have to uncomment the OTA flash lines in the \ref platformio.ini file - * This is a nice addition to cable upload but it doesn't replace it completely. - * If the microcontroller crashes because of bad configuration you still have to use a cable - */ - #define ENABLE_OTA_UPLOAD true +/** + * \brief The host name that shall be used for OTA updates. If you change this here it must also be changed + * in the \ref platformio.ini file + */ +#define OTA_UPDATE_HOST_NAME "LED-Shelf" - #if ENABLE_OTA_UPLOAD == true - /** - * \brief The host name that shall be used for OTA updates. If you change this here it must also be changed - * in the \ref platformio.ini file - */ - #define OTA_UPDATE_HOST_NAME "LED-Shelf" - #endif +/** + * \brief The number of times the controller tries to connect to wifi before it fails and goes into + * smartConfig mode (if that is enabled) + */ +#define NUM_RETRIES 50 - /** - * \brief The number of times the controller tries to connect to wifi before it fails and goes into - * smartConfig mode (if that is enabled) - */ - #define NUM_RETRIES 50 +/** + * \brief Use the ESP smart config to setup the wifi network. If you want to set it manually set this to false + */ +#define USE_ESPTOUCH_SMART_CONFIG true +#if USE_ESPTOUCH_SMART_CONFIG == false /** - * \brief Use the ESP smart config to setup the wifi network. If you want to set it manually set this to false + * \brief WIFI_SSID and WIFI_PW are only needed if smart setup is disabled */ - #define USE_ESPTOUCH_SMART_CONFIG true - - #if USE_ESPTOUCH_SMART_CONFIG == false - /** - * \brief WIFI_SSID and WIFI_PW are only needed if smart setup is disabled - */ - #define WIFI_SSID "SET_YOUR_SSID_HERE" - #define WIFI_PW "SET_YOUR_PASSWORD_HERE" - #endif - + #define WIFI_SSID "SET_YOUR_SSID_HERE" + #define WIFI_PW "SET_YOUR_PASSWORD_HERE" #endif /** diff --git a/lib/LED_clock/Config/Transitions/default/SegmentTransitions.cpp b/lib/LED_clock/Config/Transitions/default/SegmentTransitions.cpp index 99ce914..fe1ea0d 100644 --- a/lib/LED_clock/Config/Transitions/default/SegmentTransitions.cpp +++ b/lib/LED_clock/Config/Transitions/default/SegmentTransitions.cpp @@ -13,6 +13,8 @@ CubicEase* cubicEaseIn = new CubicEase(EASE_IN); CubicEase* cubicEaseOut = new CubicEase(EASE_OUT); /** \} */ +typedef Animator::ComplexAmination* (*AnimationInitFunction)(uint16_t totalAnimationLength); + /** * \brief Function prototypes for all segment transition animation init functions * \addtogroup TransitionAnimationInitFunctions @@ -45,38 +47,6 @@ Animator::ComplexAmination* InitAnimate0to9(uint16_t totalAnimationLength); Animator::ComplexAmination* InitAnimate0to5(uint16_t totalAnimationLength); /** \} */ -/** - * \brief Global variables for all segment transition animations - * \addtogroup TransitionAnimations - * \{ - */ -Animator::ComplexAmination* Animate0to1 = InitAnimate0to1(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate1to2 = InitAnimate1to2(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate2to3 = InitAnimate2to3(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate3to4 = InitAnimate3to4(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate2to0 = InitAnimate2to0(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate4to5 = InitAnimate4to5(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate5to6 = InitAnimate5to6(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate5to0 = InitAnimate5to0(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate6to7 = InitAnimate6to7(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate7to8 = InitAnimate7to8(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate8to9 = InitAnimate8to9(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate9to0 = InitAnimate9to0(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* AnimateOFFto1 = InitAnimateOFFto1(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate1toOFF = InitAnimate1toOFF(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate9to8 = InitAnimate9to8(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate8to7 = InitAnimate8to7(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate7to6 = InitAnimate7to6(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate6to5 = InitAnimate6to5(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate5to4 = InitAnimate5to4(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate4to3 = InitAnimate4to3(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate3to2 = InitAnimate3to2(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate2to1 = InitAnimate2to1(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate1to0 = InitAnimate1to0(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate0to9 = InitAnimate0to9(DIGIT_ANIMATION_SPEED); -Animator::ComplexAmination* Animate0to5 = InitAnimate0to5(DIGIT_ANIMATION_SPEED); -/** \} */ - /** * \brief This transformation lookup table defines which animation to call for which transition. * Every row decides from which digits we want to morph and than the column of the digit we want to morph to is selected. @@ -84,20 +54,84 @@ Animator::ComplexAmination* Animate0to5 = InitAnimate0to5(DIGIT_ANIMATION_SPEED * */ Animator::ComplexAmination* TransformationLookupTable[11][11] = { - //To:0 1 2 3 4 5 6 7 8 9 OFF -/*from 0 */{nullptr , Animate0to1 , nullptr , nullptr , nullptr , Animate0to5 , nullptr , nullptr , nullptr , Animate0to9 , nullptr }, -/*from 1 */{Animate1to0 , nullptr , Animate1to2 , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , Animate1toOFF}, -/*from 2 */{Animate2to0 , Animate2to1 , nullptr , Animate2to3 , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr }, -/*from 3 */{nullptr , nullptr , Animate3to2 , nullptr , Animate3to4 , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr }, -/*from 4 */{nullptr , nullptr , nullptr , Animate4to3 , nullptr , Animate4to5 , nullptr , nullptr , nullptr , nullptr , nullptr }, -/*from 5 */{Animate5to0 , nullptr , nullptr , nullptr , Animate5to4 , nullptr , Animate5to6 , nullptr , nullptr , nullptr , nullptr }, -/*from 6 */{nullptr , nullptr , nullptr , nullptr , nullptr , Animate6to5 , nullptr , Animate6to7 , nullptr , nullptr , nullptr }, -/*from 7 */{nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , Animate7to6 , nullptr , Animate7to8 , nullptr , nullptr }, -/*from 8 */{nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , Animate8to7 , nullptr , Animate8to9 , nullptr }, -/*from 9 */{Animate9to0 , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , Animate9to8 , nullptr , nullptr }, -/*from OFF */{nullptr , AnimateOFFto1, nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr } + //To:0 1 2 3 4 5 6 7 8 9 OFF +/*from 0 */{nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr }, +/*from 1 */{nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr }, +/*from 2 */{nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr }, +/*from 3 */{nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr }, +/*from 4 */{nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr }, +/*from 5 */{nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr }, +/*from 6 */{nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr }, +/*from 7 */{nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr }, +/*from 8 */{nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr }, +/*from 9 */{nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr }, +/*from OFF */{nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr } +}; + +AnimationInitFunction TransformationInitLookupTable[11][11] = { + //To:0 1 2 3 4 5 6 7 8 9 OFF +/*from 0 */{nullptr , InitAnimate0to1 , nullptr , nullptr , nullptr , InitAnimate0to5 , nullptr , nullptr , nullptr , InitAnimate0to9 , nullptr }, +/*from 1 */{InitAnimate1to0 , nullptr , InitAnimate1to2 , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , InitAnimate1toOFF }, +/*from 2 */{InitAnimate2to0 , InitAnimate2to1 , nullptr , InitAnimate2to3 , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr }, +/*from 3 */{nullptr , nullptr , InitAnimate3to2 , nullptr , InitAnimate3to4 , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr }, +/*from 4 */{nullptr , nullptr , nullptr , InitAnimate4to3 , nullptr , InitAnimate4to5 , nullptr , nullptr , nullptr , nullptr , nullptr }, +/*from 5 */{InitAnimate5to0 , nullptr , nullptr , nullptr , InitAnimate5to4 , nullptr , InitAnimate5to6 , nullptr , nullptr , nullptr , nullptr }, +/*from 6 */{nullptr , nullptr , nullptr , nullptr , nullptr , InitAnimate6to5 , nullptr , InitAnimate6to7 , nullptr , nullptr , nullptr }, +/*from 7 */{nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , InitAnimate7to6 , nullptr , InitAnimate7to8 , nullptr , nullptr }, +/*from 8 */{nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , InitAnimate8to7 , nullptr , InitAnimate8to9 , nullptr }, +/*from 9 */{InitAnimate9to0 , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , InitAnimate9to8 , nullptr , nullptr }, +/*from OFF */{nullptr , InitAnimateOFFto1 , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr , nullptr } }; + +void cleanupMorphAnimation(Animator::ComplexAmination* animation) +{ + int animationChainLength = animation->animations->size(); + for (int i = 0; i < animationChainLength; i++) + { + Animator::animationStep* currentStep = animation->animations->get(i); + delete currentStep->animationEffects; + delete currentStep->arrayIndex; + delete currentStep->easingEffects; + delete currentStep; + } + animation->animations->clear(); + delete animation->animations; //TODO: check if problematic + delete animation; +} + +void SetupAllMorphAnimations(uint16_t animationLength) +{ + for (uint8_t i = 0; i < 11; i++) + { + for (uint8_t j = 0; j < 11; j++) + { + if(TransformationInitLookupTable[i][j] != nullptr) + { + if(TransformationLookupTable[i][j] != nullptr) + { + cleanupMorphAnimation(TransformationLookupTable[i][j]); + } + TransformationLookupTable[i][j] = TransformationInitLookupTable[i][j](animationLength); + } + } + } +} + +void ChangeMorphAnimationSpeed(uint16_t newAnimationLength) +{ + for (uint8_t i = 0; i < 11; i++) + { + for (uint8_t j = 0; j < 11; j++) + { + if(TransformationLookupTable[i][j] != nullptr) + { + TransformationLookupTable[i][j]->LengthPerAnimation = newAnimationLength / (TransformationLookupTable[i][j]->animations->size() + 1); + } + } + } +} + Animator::ComplexAmination* InitAnimate0to1(uint16_t totalAnimationLength) { #undef LENGTH @@ -111,7 +145,7 @@ Animator::ComplexAmination* InitAnimate0to1(uint16_t totalAnimationLength) step1->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToRight, AnimationEffects::AnimateOutToRight}; step1->easingEffects = new EasingBase*[LENGTH] {cubicEaseOut, cubicEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); AnimationStepSequence->add(step1); @@ -139,7 +173,7 @@ Animator::ComplexAmination* InitAnimate1to2(uint16_t totalAnimationLength) step2->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateInToRight, AnimationEffects::AnimateInToLeft}; step2->easingEffects = new EasingBase*[LENGTH] {bounceEaseOut, bounceEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); AnimationStepSequence->add(step1); AnimationStepSequence->add(step2); @@ -160,7 +194,7 @@ Animator::ComplexAmination* InitAnimate2to3(uint16_t totalAnimationLength) step0->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToBottom, AnimationEffects::AnimateInToTop}; step0->easingEffects = new EasingBase*[LENGTH] {bounceEaseOut, bounceEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); Animator::ComplexAmination* animation = new Animator::ComplexAmination(); @@ -179,7 +213,7 @@ Animator::ComplexAmination* InitAnimate2to0(uint16_t totalAnimationLength) step0->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateInToBottom, AnimationEffects::AnimateOutToRight, AnimationEffects::AnimateInToBottom}; step0->easingEffects = new EasingBase*[LENGTH] {bounceEaseOut, cubicEaseInOut, bounceEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); Animator::ComplexAmination* animation = new Animator::ComplexAmination(); @@ -198,7 +232,7 @@ Animator::ComplexAmination* InitAnimate3to4(uint16_t totalAnimationLength) step0->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToRight, AnimationEffects::AnimateInToBottom, AnimationEffects::AnimateOutToLeft}; step0->easingEffects = new EasingBase*[LENGTH] {bounceEaseOut, bounceEaseOut, bounceEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); Animator::ComplexAmination* animation = new Animator::ComplexAmination(); @@ -217,7 +251,7 @@ Animator::ComplexAmination* InitAnimate4to5(uint16_t totalAnimationLength) step0->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToTop, AnimationEffects::AnimateInToLeft, AnimationEffects::AnimateInToLeft}; step0->easingEffects = new EasingBase*[LENGTH] {bounceEaseOut, bounceEaseOut, bounceEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); Animator::ComplexAmination* animation = new Animator::ComplexAmination(); @@ -236,7 +270,7 @@ Animator::ComplexAmination* InitAnimate5to6(uint16_t totalAnimationLength) step0->animationEffects = new AnimatableObject::AnimationFunction[1] {AnimationEffects::AnimateInToTop}; step0->easingEffects = new EasingBase*[LENGTH] {bounceEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); Animator::ComplexAmination* animation = new Animator::ComplexAmination(); @@ -255,7 +289,7 @@ Animator::ComplexAmination* InitAnimate5to0(uint16_t totalAnimationLength) step0->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToRight, AnimationEffects::AnimateInToBottom, AnimationEffects::AnimateInToTop}; step0->easingEffects = new EasingBase*[LENGTH] {bounceEaseOut, bounceEaseOut, bounceEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); Animator::ComplexAmination* animation = new Animator::ComplexAmination(); @@ -282,7 +316,7 @@ Animator::ComplexAmination* InitAnimate6to7(uint16_t totalAnimationLength) step2->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToRight, NO_ANIMATION, NO_ANIMATION}; step2->easingEffects = new EasingBase*[LENGTH] {cubicEaseOut, NO_EASING, NO_EASING}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); AnimationStepSequence->add(step1); AnimationStepSequence->add(step2); @@ -308,7 +342,7 @@ Animator::ComplexAmination* InitAnimate7to8(uint16_t totalAnimationLength) step1->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateInToTop, AnimationEffects::AnimateInToRight}; step1->easingEffects = new EasingBase*[LENGTH] {cubicEaseOut, cubicEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); AnimationStepSequence->add(step1); @@ -328,7 +362,7 @@ Animator::ComplexAmination* InitAnimate8to9(uint16_t totalAnimationLength) step0->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToBottom}; step0->easingEffects = new EasingBase*[LENGTH] {bounceEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); Animator::ComplexAmination* animation = new Animator::ComplexAmination(); @@ -347,7 +381,7 @@ Animator::ComplexAmination* InitAnimate9to0(uint16_t totalAnimationLength) step0->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToLeft, AnimationEffects::AnimateInToBottom}; step0->easingEffects = new EasingBase*[LENGTH] {bounceEaseOut, bounceEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); Animator::ComplexAmination* animation = new Animator::ComplexAmination(); @@ -366,7 +400,7 @@ Animator::ComplexAmination* InitAnimate1toOFF(uint16_t totalAnimationLength) step0->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToBottom, AnimationEffects::AnimateOutToTop}; step0->easingEffects = new EasingBase*[LENGTH] {cubicEaseInOut, cubicEaseInOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); Animator::ComplexAmination* animation = new Animator::ComplexAmination(); @@ -385,7 +419,7 @@ Animator::ComplexAmination* InitAnimateOFFto1(uint16_t totalAnimationLength) step0->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateInToTop, AnimationEffects::AnimateInToBottom}; step0->easingEffects = new EasingBase*[LENGTH] {cubicEaseInOut, cubicEaseInOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); Animator::ComplexAmination* animation = new Animator::ComplexAmination(); @@ -404,7 +438,7 @@ Animator::ComplexAmination* InitAnimate9to8(uint16_t totalAnimationLength) step0->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateInToTop}; step0->easingEffects = new EasingBase*[LENGTH] {bounceEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); Animator::ComplexAmination* animation = new Animator::ComplexAmination(); @@ -427,7 +461,7 @@ Animator::ComplexAmination* InitAnimate8to7(uint16_t totalAnimationLength) step1->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToRight, NO_ANIMATION, NO_ANIMATION}; step1->easingEffects = new EasingBase*[LENGTH] {cubicEaseOut, cubicEaseOut, NO_EASING}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); AnimationStepSequence->add(step1); @@ -451,7 +485,7 @@ Animator::ComplexAmination* InitAnimate7to6(uint16_t totalAnimationLength) step1->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateInToRight, AnimationEffects::AnimateInToTop, NO_ANIMATION}; step1->easingEffects = new EasingBase*[LENGTH] {cubicEaseOut, cubicEaseOut, NO_EASING}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); AnimationStepSequence->add(step1); @@ -471,7 +505,7 @@ Animator::ComplexAmination* InitAnimate6to5(uint16_t totalAnimationLength) step0->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToBottom}; step0->easingEffects = new EasingBase*[LENGTH] {bounceEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); Animator::ComplexAmination* animation = new Animator::ComplexAmination(); @@ -490,7 +524,7 @@ Animator::ComplexAmination* InitAnimate5to4(uint16_t totalAnimationLength) step0->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToRight, AnimationEffects::AnimateInToBottom, AnimationEffects::AnimateOutToRight}; step0->easingEffects = new EasingBase*[LENGTH] {bounceEaseOut, bounceEaseOut, bounceEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); Animator::ComplexAmination* animation = new Animator::ComplexAmination(); @@ -509,7 +543,7 @@ Animator::ComplexAmination* InitAnimate4to3(uint16_t totalAnimationLength) step0->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToTop, AnimationEffects::AnimateInToRight, AnimationEffects::AnimateInToLeft}; step0->easingEffects = new EasingBase*[LENGTH] {bounceEaseOut, bounceEaseOut, bounceEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); Animator::ComplexAmination* animation = new Animator::ComplexAmination(); @@ -528,7 +562,7 @@ Animator::ComplexAmination* InitAnimate3to2(uint16_t totalAnimationLength) step0->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToBottom, AnimationEffects::AnimateInToTop}; step0->easingEffects = new EasingBase*[LENGTH] {bounceEaseOut, bounceEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); Animator::ComplexAmination* animation = new Animator::ComplexAmination(); @@ -555,7 +589,7 @@ Animator::ComplexAmination* InitAnimate2to1(uint16_t totalAnimationLength) step2->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateInToTop, AnimationEffects::AnimateOutToRight}; step2->easingEffects = new EasingBase*[LENGTH] {NO_EASING, NO_EASING}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); AnimationStepSequence->add(step1); AnimationStepSequence->add(step2); @@ -580,7 +614,7 @@ Animator::ComplexAmination* InitAnimate1to0(uint16_t totalAnimationLength) step1->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateInToTop, AnimationEffects::AnimateInToBottom}; step1->easingEffects = new EasingBase*[LENGTH] {bounceEaseOut, bounceEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); AnimationStepSequence->add(step1); @@ -600,7 +634,7 @@ Animator::ComplexAmination* InitAnimate0to9(uint16_t totalAnimationLength) step0->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToTop, AnimationEffects::AnimateInToRight}; step0->easingEffects = new EasingBase*[LENGTH] {bounceEaseOut, bounceEaseOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); Animator::ComplexAmination* animation = new Animator::ComplexAmination(); @@ -619,7 +653,7 @@ Animator::ComplexAmination* InitAnimate0to5(uint16_t totalAnimationLength) step0->animationEffects = new AnimatableObject::AnimationFunction[LENGTH] {AnimationEffects::AnimateOutToBottom, AnimationEffects::AnimateInToLeft, AnimationEffects::AnimateOutToBottom}; step0->easingEffects = new EasingBase*[LENGTH] {cubicEaseInOut, cubicEaseInOut, cubicEaseInOut}; - LinkedList* AnimationStepSequence = new LinkedList(); + DynamicList* AnimationStepSequence = new DynamicList(); AnimationStepSequence->add(step0); Animator::ComplexAmination* animation = new Animator::ComplexAmination(); diff --git a/lib/LED_clock/Config/Transitions/default/SegmentTransitions.h b/lib/LED_clock/Config/Transitions/default/SegmentTransitions.h index 6d79e03..7c5e0e8 100644 --- a/lib/LED_clock/Config/Transitions/default/SegmentTransitions.h +++ b/lib/LED_clock/Config/Transitions/default/SegmentTransitions.h @@ -16,36 +16,6 @@ */ extern Animator::ComplexAmination* TransformationLookupTable[11][11]; -/** - * \brief All avaliable animations to morph between digits - * \addtogroup DigitMorphAnimations - * \{ - */ -extern Animator::ComplexAmination* Animate0to1; -extern Animator::ComplexAmination* Animate1to2; -extern Animator::ComplexAmination* Animate2to3; -extern Animator::ComplexAmination* Animate3to4; -extern Animator::ComplexAmination* Animate2to0; -extern Animator::ComplexAmination* Animate4to5; -extern Animator::ComplexAmination* Animate5to6; -extern Animator::ComplexAmination* Animate5to0; -extern Animator::ComplexAmination* Animate6to7; -extern Animator::ComplexAmination* Animate7to8; -extern Animator::ComplexAmination* Animate8to9; -extern Animator::ComplexAmination* Animate9to0; -extern Animator::ComplexAmination* AnimateOFFto1; -extern Animator::ComplexAmination* Animate1toOFF; -extern Animator::ComplexAmination* Animate9to8; -extern Animator::ComplexAmination* Animate8to7; -extern Animator::ComplexAmination* Animate7to6; -extern Animator::ComplexAmination* Animate6to5; -extern Animator::ComplexAmination* Animate5to4; -extern Animator::ComplexAmination* Animate4to3; -extern Animator::ComplexAmination* Animate3to2; -extern Animator::ComplexAmination* Animate2to1; -extern Animator::ComplexAmination* Animate1to0; -extern Animator::ComplexAmination* Animate0to9; -extern Animator::ComplexAmination* Animate0to5; - -/** \} */ +void SetupAllMorphAnimations(uint16_t animationLength); +void ChangeMorphAnimationSpeed(uint16_t newAnimationLength); #endif \ No newline at end of file diff --git a/lib/LED_clock/Modules/Animator/inc/AnimatableObject.h b/lib/LED_clock/Modules/Animator/inc/AnimatableObject.h index 475bf69..2a5a1ec 100644 --- a/lib/LED_clock/Modules/Animator/inc/AnimatableObject.h +++ b/lib/LED_clock/Modules/Animator/inc/AnimatableObject.h @@ -42,7 +42,7 @@ class AnimatableObject /** * \brief Callback typedef to use when an animation starts/finishes */ - typedef void AnimationCallBack(void); + typedef void (*AnimationCallBack)(void); /** * \brief Typedef for animation effect functions. These should be implemented explicitly for every object that inherits from #AnimatableObject @@ -66,8 +66,8 @@ class AnimatableObject bool animationStarted; void* complexAnimationInst; - AnimationCallBack* finishedCallback; - AnimationCallBack* startCallback; + AnimationCallBack finishedCallback; + AnimationCallBack startCallback; Animator* ComplexAnimationManager; ComplexAnimationCallBack ComplexAnimStartCallback; ComplexAnimationCallBack ComplexAnimDoneCallback; @@ -161,14 +161,14 @@ class AnimatableObject * * \param callback function to call */ - void setAnimationDoneCallback(AnimationCallBack* callback); + void setAnimationDoneCallback(AnimationCallBack callback); /** * \brief Set a callback to be executued once an animation is started * * \param callback function to call */ - void setAnimationStartCallback(AnimationCallBack* callback); + void setAnimationStartCallback(AnimationCallBack callback); }; diff --git a/lib/LED_clock/Modules/Animator/inc/Animator.h b/lib/LED_clock/Modules/Animator/inc/Animator.h index 055b80f..22f9058 100644 --- a/lib/LED_clock/Modules/Animator/inc/Animator.h +++ b/lib/LED_clock/Modules/Animator/inc/Animator.h @@ -8,7 +8,7 @@ #define __ANIMATOR_H_ #include -#include +#include "LinkedList.h" #define FASTLED_INTERNAL #include #include "AnimatableObject.h" @@ -51,7 +51,7 @@ class Animator typedef struct { uint8_t animationComplexity; uint16_t LengthPerAnimation; - LinkedList* animations; + DynamicList* animations; } ComplexAmination; struct ComplexAnimationInstance { @@ -60,11 +60,12 @@ class Animator uint16_t counter; AnimatableObject** objects; bool running; + AnimatableObject::AnimationCallBack complexAnimationFinishedCallBack; }; private: static Animator* currentInstance; - LinkedList AnimatableObjects; + DynamicList AnimatableObjects; static unsigned long lastLEDUpdate; int16_t getIndexInList(AnimatableObject* object); diff --git a/lib/LED_clock/Modules/Animator/src/AnimatableObject.cpp b/lib/LED_clock/Modules/Animator/src/AnimatableObject.cpp index 9d19e1f..f5bf360 100644 --- a/lib/LED_clock/Modules/Animator/src/AnimatableObject.cpp +++ b/lib/LED_clock/Modules/Animator/src/AnimatableObject.cpp @@ -139,12 +139,12 @@ int32_t AnimatableObject::getState() return map(currentAnimationTime, 0, AnimationDuration, 0, numStates); } -void AnimatableObject::setAnimationDoneCallback(AnimationCallBack* callback) +void AnimatableObject::setAnimationDoneCallback(AnimationCallBack callback) { finishedCallback = callback; } -void AnimatableObject::setAnimationStartCallback(AnimationCallBack* callback) +void AnimatableObject::setAnimationStartCallback(AnimationCallBack callback) { startCallback = callback; } diff --git a/lib/LED_clock/Modules/Animator/src/Animator.cpp b/lib/LED_clock/Modules/Animator/src/Animator.cpp index 941789d..9e2ea4c 100644 --- a/lib/LED_clock/Modules/Animator/src/Animator.cpp +++ b/lib/LED_clock/Modules/Animator/src/Animator.cpp @@ -16,6 +16,7 @@ Animator* Animator::getInstance() if(currentInstance == nullptr) { currentInstance = new Animator(); + Serial.println("[D]: Animator instance created"); } return currentInstance; } @@ -153,6 +154,10 @@ void Animator::animationIterationDoneCallback(AnimatableObject* sourceObject) currentObject->complexAnimationInst = nullptr; } } + if(currentAnimation->complexAnimationFinishedCallBack != nullptr) + { + currentAnimation->complexAnimationFinishedCallBack(); + } delete currentAnimation; } } @@ -167,6 +172,11 @@ void Animator::startAnimationStep(uint16_t stepindex, ComplexAnimationInstance* } animationStep* StepToStart = animationInst->animation->animations->get(stepindex); + if(StepToStart == nullptr) + { + Serial.printf("[E] Complex animation stepindex was invalid. Animation step %d was not started\n\r", stepindex); + return; + } bool hasCallbacks = false; bool wasEmpty = true; AnimatableObject* currentObject; @@ -175,18 +185,25 @@ void Animator::startAnimationStep(uint16_t stepindex, ComplexAnimationInstance* if(StepToStart->arrayIndex[j] != -1) { currentObject = animationInst->objects[StepToStart->arrayIndex[j]]; - setAnimationDuration(currentObject, animationInst->animation->LengthPerAnimation); - currentObject->ComplexAnimationManager = this; - if(hasCallbacks == false) //only assign the callbacks to one object as all of them should start and end at the same time + if(currentObject != nullptr) { - hasCallbacks = true; - currentObject->complexAnimationInst = animationInst; - currentObject->ComplexAnimDoneCallback = &Animator::animationIterationDoneCallback; - currentObject->ComplexAnimStartCallback = &Animator::animationIterationStartCallback; + setAnimationDuration(currentObject, animationInst->animation->LengthPerAnimation); + currentObject->ComplexAnimationManager = this; + if(hasCallbacks == false) //only assign the callbacks to one object as all of them should start and end at the same time + { + hasCallbacks = true; + currentObject->complexAnimationInst = animationInst; + currentObject->ComplexAnimDoneCallback = &Animator::animationIterationDoneCallback; + currentObject->ComplexAnimStartCallback = &Animator::animationIterationStartCallback; + } + startAnimation(currentObject, StepToStart->animationEffects[j], StepToStart->easingEffects[j]); + animationInst->running = true; + wasEmpty = false; + } + else + { + Serial.printf("[E] Complex animation object was nullpointer. Animation step %d was not started\n\r", stepindex); } - startAnimation(currentObject, StepToStart->animationEffects[j], StepToStart->easingEffects[j]); - animationInst->running = true; - wasEmpty = false; } } if(wasEmpty == true) @@ -257,7 +274,8 @@ Animator::ComplexAnimationInstance* Animator::BuildComplexAnimation(ComplexAmina .loop = looping, .counter = 0, .objects = animationObjectsArray, - .running = false + .running = false, + .complexAnimationFinishedCallBack = nullptr }; if(ComplexAnimation == nullptr) { diff --git a/lib/LED_clock/Modules/ClockState/src/ClockState.cpp b/lib/LED_clock/Modules/ClockState/src/ClockState.cpp index bb8a176..f215fbf 100644 --- a/lib/LED_clock/Modules/ClockState/src/ClockState.cpp +++ b/lib/LED_clock/Modules/ClockState/src/ClockState.cpp @@ -28,6 +28,7 @@ ClockState* ClockState::getInstance() if(instance == nullptr) { instance = new ClockState(); + Serial.println("[D]: ClockState instance created"); } return instance; } diff --git a/lib/LED_clock/Modules/ConfigManager/inc/ConfigManager.h b/lib/LED_clock/Modules/ConfigManager/inc/ConfigManager.h new file mode 100644 index 0000000..d33f850 --- /dev/null +++ b/lib/LED_clock/Modules/ConfigManager/inc/ConfigManager.h @@ -0,0 +1,142 @@ +/** + * \file ConfigManager.h + * \author Florian Laschober + * \brief Config manager class definition + */ + +#ifndef __CONFIG_MANAGER_H_ +#define __CONFIG_MANAGER_H_ + +#include +#include +#include +#include "LinkedList.h" +#include "Configuration.h" + +/** + * \addtogroup Config_manager_config + * \brief Configuration for the config manager + * \{ + */ + +#define BASE_CONFIG_FILE_NAME "/BaseConfig.json" +#define COLOR_CONFIG_FILE_NAME "/ColorConfig.json" +#define HW_CONFIG_FILE_NAME "/HWConfig.json" +#define EVENT_IDS_FILE_NAME "/ConfigChangeEventIDs.json" +#define JSON_BUFFER_BASE_CONFIG 1536 +#define JSON_BUFFER_COLOR_CONFIG 384 +#define JSON_BUFFER_HW_CONFIG 1536 +#define JSON_BUFFER_EVENT_IDS 2106 + +#define JSON_BUFFER_CHUNK_SIZE 256 +#define JSON_BUFFER_INITIAL_SIZE 200 +#define MAX_JSON_BUFFER 5000 + +#define BLYNK_ACTIVE_KEY "ENABLE_BLYNK" +#define BLYNK_TOKEN_KEY "BLYNK_AUTH_TOKEN" +#define BLYNK_SERVER_KEY "BLYNK_SERVER" +#define DIGIT_ANIMATION_SPEED_KEY "DIGIT_ANIMATION_SPEED" + +#define WIFI_CONNECTING_COLOR_KEY "WIFI_CONNECTING" +#define WIFI_CONNECTED_COLOR_KEY "WIFI_CONNECTION_SUCCESSFUL" +#define WIFI_CONFIG_COLOR_KEY "WIFI_SMART_CONFIG" +#define ERROR_COLOR_KEY "ERROR" + +#define DEMO_MODE_KEY "TIME_MANAGER_DEMO_MODE" + +/** \} */ + +// const char* Wifi_config_classes[] = {}; + +#define SYSTEM_RESTART_REQUIRED true +#define SYSTEM_NO_RESTART_REQUIRED false + +/** + * \brief The config manager is responsible to handle the json configuration files. + * It stores all config values in variables to be able to use them in the code + */ +class ConfigManager +{ +public: + typedef bool (*propertyChangedCallback)(ConfigManager* config); + enum ConfigType {BASE_CONFIG, COLOR_CONFIG, HW_CONFIG, _CONFIG_TYPE_LENGTH}; +private: + struct changedEventCallback { + propertyChangedCallback callback; + uint8_t configEvent; + }; + + enum ConfigClass_id { + WIFI_CONFIG_CLASS_ID = 0, + BLYNK_CONFIG_CLASS_ID = 1, + BASE_CONFIG_CLASS_ID = 2, + COLOR_CONFIG_CLASS_ID = 3, + HW_CONFIG_CLASS_ID = 4, + _CONFIG_CLASS_ID_LENGTH = 5 + }; + + static ConfigManager* instance; + DynamicList eventCallbacks; + DynamicJsonDocument* BaseConfig; + DynamicJsonDocument* ColorConfig; + DynamicJsonDocument* HWConfig; + DynamicJsonDocument* EventIDs; + bool restartRequested; + + bool configClassWasUpdated[_CONFIG_CLASS_ID_LENGTH]; + bool configNeedsSaving[_CONFIG_TYPE_LENGTH]; + + ConfigManager(); + DynamicJsonDocument* deserializeDynamically(fs::File &input, uint64_t initialDocSize); + bool deserializeDynamically(fs::File &input, DynamicJsonDocument* initialDocument); + + template + T getProperty(DynamicJsonDocument* root, const char* key); + + bool loadConfigFromMemory(const char* filename, DynamicJsonDocument* doc); + DynamicJsonDocument* getDocument(ConfigType config); + const char* getFileName(ConfigType config); + + void checkCallbacks(uint8_t ConfigEvent); + bool isSameValueType(JsonVariant type1, JsonVariant type2); +public: + + enum ConfigEvents { + WIFI_CONFIG_CHANGED = 0x01 << WIFI_CONFIG_CLASS_ID, + BLYNK_CONFIG_CHANGED = 0x01 << BLYNK_CONFIG_CLASS_ID, + BASE_CONFIG_CHANGED = 0x01 << BASE_CONFIG_CLASS_ID, + COLOR_CONFIG_CHANGED = 0x01 << COLOR_CONFIG_CLASS_ID, + HW_CONFIG_CHANGED = 0x01 << HW_CONFIG_CLASS_ID + }; + + /** + * \brief Destroys the Config Manager object and cause #ConfigManager::getInstance to create a new object the next time it is called + */ + ~ConfigManager(); + + /** + * \brief Get the instance of the ConfigManager object or create it if it was not yet instantiated. + * + * \return ConfigManager* returns the address to the ConfigManager object + */ + static ConfigManager* getInstance(); + + template + T getProperty(ConfigType config, const char* key); + + template + bool setProperty(ConfigType config, char* key, T value); + + bool setProperty(JsonPair& keyValuePair, ConfigType config); + + void saveConfigPersistent(ConfigType config); + void applyChanges(); + void applySaveAndRestart(ConfigType config); + void restartIfRequired(); + + void registerOnChangedCallback(uint8_t ConfigEvent, propertyChangedCallback Callback); + void unregisterOnChangedCallback(propertyChangedCallback Callback); + +}; + +#endif diff --git a/lib/LED_clock/Modules/ConfigManager/src/ConfigManager.cpp b/lib/LED_clock/Modules/ConfigManager/src/ConfigManager.cpp new file mode 100644 index 0000000..8576f21 --- /dev/null +++ b/lib/LED_clock/Modules/ConfigManager/src/ConfigManager.cpp @@ -0,0 +1,401 @@ +/** + * \file ConfigManager.cpp + * \author Florian Laschober + * \brief Implementation of the ConfigManager class member functions + */ + + +#include "ConfigManager.h" + +ConfigManager* ConfigManager::instance = nullptr; + +//Template types that can be used + +template bool ConfigManager::getProperty(ConfigType config, const char* key); +template int ConfigManager::getProperty(ConfigType config, const char* key); +template const char* ConfigManager::getProperty(ConfigType config, const char* key); + +ConfigManager::ConfigManager() +{ + restartRequested = false; + if(!SPIFFS.begin(true)) + { + Serial.println("Filesystem mount failed aborting..."); + abort(); + } + + Serial.printf("Filesystem mount successful; %d out of %d bytes used\n\r", SPIFFS.usedBytes(), SPIFFS.totalBytes()); + + BaseConfig = new DynamicJsonDocument(JSON_BUFFER_BASE_CONFIG); + ColorConfig = new DynamicJsonDocument(JSON_BUFFER_COLOR_CONFIG); + HWConfig = new DynamicJsonDocument(JSON_BUFFER_HW_CONFIG); + EventIDs = new DynamicJsonDocument(JSON_BUFFER_EVENT_IDS); + + bool loadSuccessful = true; + loadSuccessful &= loadConfigFromMemory(BASE_CONFIG_FILE_NAME, BaseConfig); + loadSuccessful &= loadConfigFromMemory(COLOR_CONFIG_FILE_NAME, ColorConfig); + loadSuccessful &= loadConfigFromMemory(HW_CONFIG_FILE_NAME, HWConfig); + loadSuccessful &= loadConfigFromMemory(EVENT_IDS_FILE_NAME, EventIDs); + if(loadSuccessful == false) + { + Serial.println("[E]: Loading of json config from filesystem unsuccessful"); + } + for (uint8_t i = 0; i < _CONFIG_CLASS_ID_LENGTH; i++) + { + configClassWasUpdated[i] = false; + } + for (uint8_t i = 0; i < _CONFIG_TYPE_LENGTH; i++) + { + configNeedsSaving[i] = false; + } +} + +ConfigManager::~ConfigManager() +{ + instance = nullptr; +} + +ConfigManager* ConfigManager::getInstance() +{ + if(instance == nullptr) + { + instance = new ConfigManager(); + Serial.println("[D]: ConfigManager instance created"); + } + return instance; +} + +DynamicJsonDocument* ConfigManager::deserializeDynamically(fs::File &input, uint64_t initialDocSize) +{ + //try to initialize a json document big enough to fit all the data + uint64_t DocumentSize = initialDocSize; + DynamicJsonDocument* doc; + bool successful = false; + + do + { + doc = new DynamicJsonDocument(DocumentSize); + DeserializationError error = deserializeJson(*doc, input); + if (error) + { + delete doc; + if (error.code() == DeserializationError::NoMemory) + { + DocumentSize += JSON_BUFFER_CHUNK_SIZE; + } + else + { + Serial.println("E: json:" + String(error.c_str())); + break; + } + } + else + { + successful = true; + break; //no error detected -> memory size is okay + } + } while (DocumentSize < MAX_JSON_BUFFER); + + if(successful == true) + { + return doc; + } + return nullptr; +} + +bool ConfigManager::deserializeDynamically(fs::File &input, DynamicJsonDocument* doc) +{ + if(doc == nullptr) + { + return false; + } + bool successful = false; + bool firstTry = true; + size_t DocumentSize = (*doc).size(); + + do + { + if(firstTry == true) + { + firstTry = false; + } + else + { + doc = new DynamicJsonDocument(DocumentSize); + } + + DeserializationError error = deserializeJson(*doc, input); + if (error) + { + if (error.code() == DeserializationError::NoMemory) + { + delete doc; + DocumentSize += JSON_BUFFER_CHUNK_SIZE; + } + else + { + Serial.println("E: json:" + String(error.c_str())); + break; + } + } + else + { + successful = true; + break; //no error detected -> memory size is okay + } + } while (DocumentSize < MAX_JSON_BUFFER); + + return successful; +} + +bool ConfigManager::loadConfigFromMemory(const char* filename, DynamicJsonDocument* doc) +{ + #define ERROR_HANDLING(x, key) if(x == false) { Serial.printf("E: Key %s is mandatory", key); delete doc; return false;} + bool loadOK = false; + if(SPIFFS.exists(filename)) + { + File jsonFile = SPIFFS.open(filename, "r"); + if (!jsonFile) + { + Serial.println("E: Open config file failed"); + } + else + { + if(deserializeDynamically(jsonFile, doc) == true) + { + loadOK = true; + } + jsonFile.close(); + } + } + else + { + Serial.printf("E: Config file %s does not exist on the filesystem yet. Please create it first\n\r", filename); + loadOK = false; + } + return loadOK; +} + + +template +T ConfigManager::getProperty(DynamicJsonDocument* root, const char* key) +{ + JsonVariant jvalue = (*root)[key]; + if (!jvalue.isNull()) + { + if(!jvalue.is()) + { + Serial.printf("[E]: BaseConfig key \"%s\" is not the correct type\n\r", key); + } + else + { + return jvalue.as(); + } + } + return T(); +} + +template +T ConfigManager::getProperty(ConfigType config, const char* key) +{ + return getProperty(getDocument(config), key); +} + +template +bool ConfigManager::setProperty(ConfigType config, char* key, T value) +{ + DynamicJsonDocument* doc = getDocument(config); + if(doc->containsKey(key)) + { + return (*doc)[key].set(key); + } + Serial.printf("[E]: Config does not contain key %s\n\r", key); + return false; +} + +DynamicJsonDocument* ConfigManager::getDocument(ConfigType config) +{ + switch (config) + { + case BASE_CONFIG: + return BaseConfig; + case COLOR_CONFIG: + return ColorConfig; + case HW_CONFIG: + return HWConfig; + default: + return nullptr; + } +} + +const char* ConfigManager::getFileName(ConfigType config) +{ + switch (config) + { + case BASE_CONFIG: + return BASE_CONFIG_FILE_NAME; + case COLOR_CONFIG: + return COLOR_CONFIG_FILE_NAME; + case HW_CONFIG: + return HW_CONFIG_FILE_NAME; + default: + return nullptr; + } +} + +bool ConfigManager::setProperty(JsonPair& keyValuePair, ConfigType config) +{ + if(config >= _CONFIG_TYPE_LENGTH) + { + Serial.printf("[E]: Config id %d is higher than maximum allowed of %d\n\r", config, _CONFIG_TYPE_LENGTH); + return false; + } + DynamicJsonDocument* doc = getDocument(config); + if(doc == nullptr) + { + Serial.printf("[E]: Config with config id %d was invalid\n\r", config); + return false; + } + if(doc->containsKey(keyValuePair.key())) + { + if((*doc)[keyValuePair.key()] != keyValuePair.value()) + { + if(isSameValueType((*doc)[keyValuePair.key()], keyValuePair.value()) == true) + { + //TODO: find a better solution for this + uint8_t eventID = (*EventIDs)[keyValuePair.key()]; + configClassWasUpdated[eventID] = true; + configNeedsSaving[config] = true; + return (*doc)[keyValuePair.key()].set(keyValuePair.value()); + } + else + { + Serial.printf("[E]: value of key %s is not the right type\n\r", keyValuePair.key().c_str()); + return false; + } + } + else + { + return true; + } + } + Serial.printf("[E]: Config does not contain key %s\n\r", keyValuePair.key().c_str()); + return false; +} + +bool ConfigManager::isSameValueType(JsonVariant type1, JsonVariant type2) +{ + uint8_t typeOf1 = 0; + uint8_t typeOf2 = 0; + if(type1.is()) + { + typeOf1 = 1; + } else if(type1.is()) + { + typeOf1 = 2; + } else if(type1.is()) + { + typeOf1 = 3; + } else if(type1.is()) + { + typeOf1 = 4; + } + + if(type1.is()) + { + typeOf2 = 1; + } else if(type1.is()) + { + typeOf2 = 2; + } else if(type1.is()) + { + typeOf2 = 3; + } else if(type1.is()) + { + typeOf2 = 4; + } + + return typeOf1 == typeOf2 && typeOf1 != 0 && typeOf2 != 0; +} + +void ConfigManager::saveConfigPersistent(ConfigType config) +{ + if(config >= _CONFIG_TYPE_LENGTH) + { + return; + } + if(configNeedsSaving[config]) + { + File configFile = SPIFFS.open(getFileName(config), FILE_WRITE); + serializeJson(*getDocument(config), configFile); + configFile.close(); + configNeedsSaving[config] = false; + } + else + { + Serial.printf("[I]: Skipped saving of %s to memory as there was no change.\n\r", getFileName(config)); + } +} + +void ConfigManager::applyChanges() +{ + uint8_t updatedConfigClasses = 0x00; + for (uint8_t i = 0; i < _CONFIG_CLASS_ID_LENGTH; i++) + { + if(configClassWasUpdated[i] == true) + { + updatedConfigClasses |= 1 << i; + configClassWasUpdated[i] = false; + } + } + checkCallbacks(updatedConfigClasses); +} + +void ConfigManager::applySaveAndRestart(ConfigType config) +{ + applyChanges(); + saveConfigPersistent(config); + restartIfRequired(); +} + +void ConfigManager::restartIfRequired() +{ + if(restartRequested == true) + { + ESP.restart(); + } +} + +void ConfigManager::checkCallbacks(uint8_t ConfigEvent) +{ + bool needsRestart = false; + for (int i = 0; i < eventCallbacks.size(); i++) + { + if((eventCallbacks.get(i)->configEvent & ConfigEvent) != 0x00) + { + needsRestart |= eventCallbacks.get(i)->callback(this); + } + } + if(needsRestart == true) + { + restartRequested = true; + Serial.println("System restart was requested due to config change."); + } +} + +void ConfigManager::registerOnChangedCallback(uint8_t ConfigEvent, propertyChangedCallback Callback) +{ + changedEventCallback* newCallback = new changedEventCallback{.callback = Callback, .configEvent = ConfigEvent}; + eventCallbacks.add(newCallback); +} + +void ConfigManager::unregisterOnChangedCallback(propertyChangedCallback Callback) +{ + for (int i = 0; i < eventCallbacks.size(); i++) + { + if(eventCallbacks.get(i)->callback == Callback) + { + eventCallbacks.remove(i); + } + } +} \ No newline at end of file diff --git a/lib/LED_clock/Modules/DisplayManager/inc/DisplayManager.h b/lib/LED_clock/Modules/DisplayManager/inc/DisplayManager.h index b7ae746..bace530 100644 --- a/lib/LED_clock/Modules/DisplayManager/inc/DisplayManager.h +++ b/lib/LED_clock/Modules/DisplayManager/inc/DisplayManager.h @@ -17,6 +17,7 @@ #include "Configuration.h" #include "LinkedList.h" #include "Animations.h" +#include "ConfigManager.h" /** * \brief Macro to shorten then name of the function to make usage easier in the animation config files. @@ -39,6 +40,7 @@ class DisplayManager static uint8_t diplayIndex[NUM_SEGMENTS]; static DisplayManager* instance; + ConfigManager* config; Animator* animationManager; Segment* allSegments[NUM_SEGMENTS]; SevenSegment* Displays[NUM_DISPLAYS]; @@ -59,7 +61,7 @@ class DisplayManager SegmentPositions_t segmentPosition; DisplayIDs Display; } SegmentInstanceError; - static LinkedList* SegmentIndexErrorList; + static DynamicList* SegmentIndexErrorList; CRGB leds[NUM_LEDS]; #if APPEND_DOWN_LIGHTERS == false @@ -67,7 +69,7 @@ class DisplayManager #endif #if ENABLE_LIGHT_SENSOR == true - LinkedList lightSensorMeasurements; + DynamicList lightSensorMeasurements; uint64_t lastSensorMeasurement; uint8_t lightSensorBrightness; void takeBrightnessMeasurement(); @@ -75,6 +77,7 @@ class DisplayManager void AnimationManagersTemporaryOverride(Animator* OverrideanimationManager); void restoreAnimationManagers(); + // bool reconfigureDisplays(ConfigManager* config); DisplayManager(); public: diff --git a/lib/LED_clock/Modules/DisplayManager/src/DisplayManager.cpp b/lib/LED_clock/Modules/DisplayManager/src/DisplayManager.cpp index f5d4cdc..870a619 100644 --- a/lib/LED_clock/Modules/DisplayManager/src/DisplayManager.cpp +++ b/lib/LED_clock/Modules/DisplayManager/src/DisplayManager.cpp @@ -8,10 +8,13 @@ #include "DisplayManager.h" DisplayManager* DisplayManager::instance = nullptr; -LinkedList* DisplayManager::SegmentIndexErrorList = nullptr; +DynamicList* DisplayManager::SegmentIndexErrorList = nullptr; + +bool reconfigureDisplays(ConfigManager* config); DisplayManager::DisplayManager() { + config = ConfigManager::getInstance(); FastLED.addLeds(leds, NUM_LEDS); // GRB ordering is typical #if APPEND_DOWN_LIGHTERS == false @@ -51,6 +54,8 @@ DisplayManager::DisplayManager() progressTotal = 0; currentProgressOffset = 0; currentProgressStep = 0; + + config->registerOnChangedCallback(ConfigManager::BASE_CONFIG_CHANGED, reconfigureDisplays); } DisplayManager::~DisplayManager() @@ -59,11 +64,18 @@ DisplayManager::~DisplayManager() instance = nullptr; } +bool reconfigureDisplays(ConfigManager* config) +{ + ChangeMorphAnimationSpeed(config->getProperty(ConfigManager::BASE_CONFIG, DIGIT_ANIMATION_SPEED_KEY)); + return SYSTEM_NO_RESTART_REQUIRED; +} + DisplayManager* DisplayManager::getInstance() { if(instance == nullptr) { instance = new DisplayManager(); + Serial.println("[D]: DisplayManager instance created"); } return instance; } @@ -106,7 +118,7 @@ void DisplayManager::takeBrightnessMeasurement() } else { - LinkedList sortedMeasurements; + DynamicList sortedMeasurements; //copy all the values to a new temporary list in order to sort them for (uint16_t i = 0; i < lightSensorMeasurements.size(); i++) { @@ -146,6 +158,7 @@ void DisplayManager::setMinuteSegmentColors(CRGB color) void DisplayManager::InitSegments(uint16_t indexOfFirstLed, uint8_t ledsPerSegment, CRGB initialColor, uint8_t initBrightness) { + SetupAllMorphAnimations(DIGIT_ANIMATION_SPEED); for (uint8_t i = 0; i < NUM_DISPLAYS; i++) { if(Displays[i] != nullptr) @@ -397,9 +410,10 @@ int16_t DisplayManager::getGlobalSegmentIndex(SegmentPositions_t segmentPosition { if(SegmentIndexErrorList == nullptr) { - SegmentIndexErrorList = new LinkedList(); + SegmentIndexErrorList = new DynamicList(); } - SegmentInstanceError newError = {.segmentPosition = segmentPosition, .Display = Display}; + SegmentInstanceError* newError = new SegmentInstanceError{.segmentPosition = segmentPosition, .Display = Display}; + SegmentIndexErrorList->add(newError); } else // once Serial was initialized print errors right away to not forget about them @@ -413,10 +427,11 @@ void DisplayManager::printAnimationInitErrors() { if(SegmentIndexErrorList != nullptr) { + SegmentInstanceError* currentError; while(SegmentIndexErrorList->size() > 0) { - SegmentInstanceError currentError = SegmentIndexErrorList->pop(); - Serial.printf("[W] Segment not valid; Position: %d; Display: %d\n\r", currentError.segmentPosition, currentError.Display); + currentError = SegmentIndexErrorList->pop(); + Serial.printf("[W] Segment not valid; Position: %d; Display: %d\n\r", currentError->segmentPosition, currentError->Display); } } } diff --git a/lib/LED_clock/Modules/Misc/inc/LinkedList.h b/lib/LED_clock/Modules/Misc/inc/LinkedList.h new file mode 100644 index 0000000..c49ff55 --- /dev/null +++ b/lib/LED_clock/Modules/Misc/inc/LinkedList.h @@ -0,0 +1,421 @@ +// It was required to change the name of the Linked list class as it would have otherwise crashed with the linked list implementation from ESPAsyncWebServer + +/* + LinkedList.h - V1.1 - Generic LinkedList implementation + Works better with FIFO, because LIFO will need to + search the entire List to find the last one; + + For instructions, go to https://github.com/ivanseidel/LinkedList + + Created by Ivan Seidel Gomes, March, 2013. + Released into the public domain. +*/ + +#ifndef __LINKED_LIST1_H_ +#define __LINKED_LIST1_H_ + + +#include + +template +struct ListNode +{ + T data; + ListNode *next; +}; + +template +class DynamicList{ + +protected: + int _size; + ListNode *root; + ListNode *last; + + // Helps "get" method, by saving last position + ListNode *lastNodeGot; + int lastIndexGot; + // isCached should be set to FALSE + // everytime the list suffer changes + bool isCached; + + ListNode* getNode(int index); + + ListNode* findEndOfSortedString(ListNode *p, int (*cmp)(T &, T &)); + +public: + DynamicList(); + DynamicList(int sizeIndex, T _t); //initiate list size and default value + ~DynamicList(); + + /* + Returns current size of LinkedList + */ + virtual int size(); + /* + Adds a T object in the specified index; + Unlink and link the LinkedList correcly; + Increment _size + */ + virtual bool add(int index, T); + /* + Adds a T object in the end of the LinkedList; + Increment _size; + */ + virtual bool add(T); + /* + Adds a T object in the start of the LinkedList; + Increment _size; + */ + virtual bool unshift(T); + /* + Set the object at index, with T; + */ + virtual bool set(int index, T); + /* + Remove object at index; + If index is not reachable, returns false; + else, decrement _size + */ + virtual T remove(int index); + /* + Remove last object; + */ + virtual T pop(); + /* + Remove first object; + */ + virtual T shift(); + /* + Get the index'th element on the list; + Return Element if accessible, + else, return false; + */ + virtual T get(int index); + + /* + Clear the entire array + */ + virtual void clear(); + + /* + Sort the list, given a comparison function + */ + virtual void sort(int (*cmp)(T &, T &)); + + // add support to array brakets [] operator + inline T& operator[](int index); + inline T& operator[](size_t& i) { return this->get(i); } + inline const T& operator[](const size_t& i) const { return this->get(i); } + +}; + +// Initialize LinkedList with false values +template +DynamicList::DynamicList() +{ + root=NULL; + last=NULL; + _size=0; + + lastNodeGot = root; + lastIndexGot = 0; + isCached = false; +} + +// Clear Nodes and free Memory +template +DynamicList::~DynamicList() +{ + ListNode* tmp; + while(root!=NULL) + { + tmp=root; + root=root->next; + delete tmp; + } + last = NULL; + _size=0; + isCached = false; +} + +/* + Actualy "logic" coding +*/ + +template +ListNode* DynamicList::getNode(int index){ + + int _pos = 0; + ListNode* current = root; + + // Check if the node trying to get is + // immediatly AFTER the previous got one + if(isCached && lastIndexGot <= index){ + _pos = lastIndexGot; + current = lastNodeGot; + } + + while(_pos < index && current){ + current = current->next; + + _pos++; + } + + // Check if the object index got is the same as the required + if(_pos == index){ + isCached = true; + lastIndexGot = index; + lastNodeGot = current; + + return current; + } + + return NULL; +} + +template +int DynamicList::size(){ + return _size; +} + +template +DynamicList::DynamicList(int sizeIndex, T _t){ + for (int i = 0; i < sizeIndex; i++){ + add(_t); + } +} + +template +bool DynamicList::add(int index, T _t){ + + if(index >= _size) + return add(_t); + + if(index == 0) + return unshift(_t); + + ListNode *tmp = new ListNode(), + *_prev = getNode(index-1); + tmp->data = _t; + tmp->next = _prev->next; + _prev->next = tmp; + + _size++; + isCached = false; + + return true; +} + +template +bool DynamicList::add(T _t){ + + ListNode *tmp = new ListNode(); + tmp->data = _t; + tmp->next = NULL; + + if(root){ + // Already have elements inserted + last->next = tmp; + last = tmp; + }else{ + // First element being inserted + root = tmp; + last = tmp; + } + + _size++; + isCached = false; + + return true; +} + +template +bool DynamicList::unshift(T _t){ + + if(_size == 0) + return add(_t); + + ListNode *tmp = new ListNode(); + tmp->next = root; + tmp->data = _t; + root = tmp; + + _size++; + isCached = false; + + return true; +} + + +template +T& DynamicList::operator[](int index) { + return getNode(index)->data; +} + +template +bool DynamicList::set(int index, T _t){ + // Check if index position is in bounds + if(index < 0 || index >= _size) + return false; + + getNode(index)->data = _t; + return true; +} + +template +T DynamicList::pop(){ + if(_size <= 0) + return T(); + + isCached = false; + + if(_size >= 2){ + ListNode *tmp = getNode(_size - 2); + T ret = tmp->next->data; + delete(tmp->next); + tmp->next = NULL; + last = tmp; + _size--; + return ret; + }else{ + // Only one element left on the list + T ret = root->data; + delete(root); + root = NULL; + last = NULL; + _size = 0; + return ret; + } +} + +template +T DynamicList::shift(){ + if(_size <= 0) + return T(); + + if(_size > 1){ + ListNode *_next = root->next; + T ret = root->data; + delete(root); + root = _next; + _size --; + isCached = false; + + return ret; + }else{ + // Only one left, then pop() + return pop(); + } + +} + +template +T DynamicList::remove(int index){ + if (index < 0 || index >= _size) + { + return T(); + } + + if(index == 0) + return shift(); + + if (index == _size-1) + { + return pop(); + } + + ListNode *tmp = getNode(index - 1); + ListNode *toDelete = tmp->next; + T ret = toDelete->data; + tmp->next = tmp->next->next; + delete(toDelete); + _size--; + isCached = false; + return ret; +} + + +template +T DynamicList::get(int index){ + ListNode *tmp = getNode(index); + + return (tmp ? tmp->data : T()); +} + +template +void DynamicList::clear(){ + while(size() > 0) + shift(); +} + +template +void DynamicList::sort(int (*cmp)(T &, T &)){ + if(_size < 2) return; // trivial case; + + for(;;) { + + ListNode **joinPoint = &root; + + while(*joinPoint) { + ListNode *a = *joinPoint; + ListNode *a_end = findEndOfSortedString(a, cmp); + + if(!a_end->next ) { + if(joinPoint == &root) { + last = a_end; + isCached = false; + return; + } + else { + break; + } + } + + ListNode *b = a_end->next; + ListNode *b_end = findEndOfSortedString(b, cmp); + + ListNode *tail = b_end->next; + + a_end->next = NULL; + b_end->next = NULL; + + while(a && b) { + if(cmp(a->data, b->data) <= 0) { + *joinPoint = a; + joinPoint = &a->next; + a = a->next; + } + else { + *joinPoint = b; + joinPoint = &b->next; + b = b->next; + } + } + + if(a) { + *joinPoint = a; + while(a->next) a = a->next; + a->next = tail; + joinPoint = &a->next; + } + else { + *joinPoint = b; + while(b->next) b = b->next; + b->next = tail; + joinPoint = &b->next; + } + } + } +} + +template +ListNode* DynamicList::findEndOfSortedString(ListNode *p, int (*cmp)(T &, T &)) { + while(p->next && cmp(p->data, p->next->data) <= 0) { + p = p->next; + } + + return p; +} + +#endif \ No newline at end of file diff --git a/lib/LED_clock/Modules/SevenSegment/inc/SevenSegment.h b/lib/LED_clock/Modules/SevenSegment/inc/SevenSegment.h index 2c10ed2..63e8813 100644 --- a/lib/LED_clock/Modules/SevenSegment/inc/SevenSegment.h +++ b/lib/LED_clock/Modules/SevenSegment/inc/SevenSegment.h @@ -75,6 +75,7 @@ class SevenSegment bool isConfigComplete(); void DisplayNumberWithoutAnim(uint8_t value); Animator::ComplexAmination* getTransition(uint8_t from, uint8_t to); + void animationDone(); public: diff --git a/lib/LED_clock/Modules/SevenSegment/src/SevenSegment.cpp b/lib/LED_clock/Modules/SevenSegment/src/SevenSegment.cpp index af1e599..311692d 100644 --- a/lib/LED_clock/Modules/SevenSegment/src/SevenSegment.cpp +++ b/lib/LED_clock/Modules/SevenSegment/src/SevenSegment.cpp @@ -257,3 +257,8 @@ void SevenSegment::DisplayChar(char value) DisplayNumber(value - '0'); } } + +void SevenSegment::animationDone() +{ + DisplayNumberWithoutAnim(currentValue); +} \ No newline at end of file diff --git a/lib/LED_clock/Modules/TimeManager/inc/TimeManager.h b/lib/LED_clock/Modules/TimeManager/inc/TimeManager.h index 353e3da..4544ebc 100644 --- a/lib/LED_clock/Modules/TimeManager/inc/TimeManager.h +++ b/lib/LED_clock/Modules/TimeManager/inc/TimeManager.h @@ -10,9 +10,8 @@ #include #include "time.h" #include "Configuration.h" -#if RUN_WITHOUT_WIFI == false - #include "WiFi.h" -#endif +#include "WiFi.h" +#include "ConfigManager.h" /** * \brief The TimeManager is responsible for synchronizing the time to the NTP servers and keeping track of it @@ -55,6 +54,7 @@ class TimeManager TimeInfo currentTime; hw_timer_t* timer; uint32_t offlineTimeCounter; + uint32_t lastWifiConnectionTime; static TimeManager* TimeManagerSingelton; TimeInfo TimerInitialDuration; TimeInfo TimerDuration; diff --git a/lib/LED_clock/Modules/TimeManager/src/TimeManager.cpp b/lib/LED_clock/Modules/TimeManager/src/TimeManager.cpp index bbca0c7..4d9fc75 100644 --- a/lib/LED_clock/Modules/TimeManager/src/TimeManager.cpp +++ b/lib/LED_clock/Modules/TimeManager/src/TimeManager.cpp @@ -30,6 +30,7 @@ TimeManager::TimeManager() AlarmActive = false; AlarmTriggered = false; AlarmCleared = false; + lastWifiConnectionTime = millis(); } TimeManager::~TimeManager() @@ -43,15 +44,14 @@ TimeManager* TimeManager::getInstance() if(TimeManagerSingelton == nullptr) { TimeManagerSingelton = new TimeManager(); + Serial.println("[D]: TimeManager instance created"); } return TimeManagerSingelton; } bool TimeManager::init() { - #if TIME_MANAGER_DEMO_MODE == false - configTzTime(TIMEZONE_INFO, NTP_SERVER); - #endif + configTzTime(TIMEZONE_INFO, NTP_SERVER); // timer 0 divider 80 because 80Mhz and count up timer = timerBegin(0, 80, true); @@ -251,9 +251,11 @@ void TimeManager::clearAlarm() void IRAM_ATTR onTimer() { TimeManager* timeM = TimeManager::getInstance(); + ConfigManager* config = ConfigManager::getInstance(); // Time code, use this for normal operation - #if TIME_MANAGER_DEMO_MODE == false - if(timeM->offlineTimeCounter++ >= TIME_SYNC_INTERVALL && WiFi.status() == WL_CONNECTED) + if(!config->getProperty(ConfigManager::HW_CONFIG, DEMO_MODE_KEY)) + { + if(timeM->offlineTimeCounter++ >= TIME_SYNC_INTERVALL) { if(timeM->synchronize() == true) { @@ -307,7 +309,18 @@ void IRAM_ATTR onTimer() timeM->AlarmCleared = false; } } - #else + //handle wifi lost + if(WiFi.status() != WL_CONNECTED) + { + if(timeM->lastWifiConnectionTime + BROKEN_WIFI_CONNECT_RETRY < millis()) + { + timeM->lastWifiConnectionTime = millis(); + WiFi.begin(); // try to reconnect in a non blocking way + } + } + } + else + { //DEMO CODE: Useful for testing animations timeM->currentTime.minutes++; if(timeM->currentTime.minutes > 59) @@ -319,5 +332,5 @@ void IRAM_ATTR onTimer() { timeM->currentTime.hours = 0; } - #endif + } } \ No newline at end of file diff --git a/lib/LED_clock/Modules/WiFiManager/inc/WiFiManager.h b/lib/LED_clock/Modules/WiFiManager/inc/WiFiManager.h new file mode 100644 index 0000000..40e1cf4 --- /dev/null +++ b/lib/LED_clock/Modules/WiFiManager/inc/WiFiManager.h @@ -0,0 +1,66 @@ +/** + * \file WiFiManager.h + * \author Florian Laschober + * \brief Adaption of the code from https://github.com/Hraph/ESP32WiFiManager + */ + +#ifndef __WIFI_MANAGER_H_ +#define __WIFI_MANAGER_H_ + +#include +#include +#include +#include +#include +#include +#include "ConfigManager.h" +#include "DisplayManager.h" + +#define WEBSERVER_PORT 80 +#define DNS_PORT 53 +#define AP_SSID "LED-Pixel clock fallback AP" +#define AP_PW "0123456789" +#define CONNECTION_TIMEOUT 30000 + +class WiFiManager +{ +private: + AsyncWebServer* server; + DNSServer* dnsServer; + ConfigManager* config; + DisplayManager* ShelfDisplays; + + bool connect; + String WIFI_SSID; + String WIFI_PW; + wifi_event_id_t reconnectEvent; + + static WiFiManager* currentInstance; + WiFiManager(); + + void handleRoot(AsyncWebServerRequest *request); + void handleNotFound(AsyncWebServerRequest *request); + String saveWiFiSettings(ConfigManager::ConfigType type, JsonObject* json); + void handleWifiSubmit(AsyncWebServerRequest *request, JsonVariant &json); + void handleSaveUnavailable(AsyncWebServerRequest *request, JsonVariant &json); + void startWebInterface(); + void stopAnimation(); + void setDisplay(DisplayManager* display); + uint8_t waitForConnectResult(); + int connectWifi(String ssid, String pass); + bool startConfigPortal(); + void setupConfigPortal(); + bool captivePortal(AsyncWebServerRequest *request); + String saveSettings(ConfigManager::ConfigType type, JsonObject* json); + void tryToReconnect(WiFiEvent_t event, WiFiEventInfo_t info); +public: + static WiFiManager* getInstance(); + ~WiFiManager(); + bool autoConnect(); + bool autoConnect(DisplayManager* display); + void startWebUI(); +}; + + + +#endif \ No newline at end of file diff --git a/lib/LED_clock/Modules/WiFiManager/src/WiFiManager.cpp b/lib/LED_clock/Modules/WiFiManager/src/WiFiManager.cpp new file mode 100644 index 0000000..748b85f --- /dev/null +++ b/lib/LED_clock/Modules/WiFiManager/src/WiFiManager.cpp @@ -0,0 +1,387 @@ +#include "WiFiManager.h" + +WiFiManager* WiFiManager::currentInstance = nullptr; + +WiFiManager* WiFiManager::getInstance() +{ + if(currentInstance == nullptr) + { + currentInstance = new WiFiManager(); + Serial.println("[D]: WiFiManager instance created"); + } + return currentInstance; +} + +WiFiManager::WiFiManager() +{ + server = new AsyncWebServer(WEBSERVER_PORT); + dnsServer = new DNSServer(); + config = ConfigManager::getInstance(); + ShelfDisplays = nullptr; +} + +WiFiManager::~WiFiManager() +{ + delete server; + delete dnsServer; +} + +void WiFiManager::setDisplay(DisplayManager* display) +{ + ShelfDisplays = display; +} + +bool WiFiManager::autoConnect(DisplayManager* display) +{ + setDisplay(display); + return autoConnect(); +} + +void WiFiManager::stopAnimation() +{ + if(ShelfDisplays != nullptr) + { + ShelfDisplays->stopLoadingAnimation(); + Serial.println("Waiting for loading animation to finish..."); + ShelfDisplays->waitForLoadingAnimationFinish(); + ShelfDisplays->turnAllSegmentsOff(); + } +} + +bool WiFiManager::autoConnect() +{ + if(ShelfDisplays != nullptr) + { + ShelfDisplays->setAllSegmentColors(config->getProperty(ConfigManager::COLOR_CONFIG, WIFI_CONNECTING_COLOR_KEY)); + ShelfDisplays->showLoadingAnimation(); + } + // attempt to connect; should it fail, fall back to AP + WiFi.mode(WIFI_STA); + + if (connectWifi("", "") == WL_CONNECTED) + { + if(ShelfDisplays != nullptr) + { + ShelfDisplays->setAllSegmentColors(config->getProperty(ConfigManager::COLOR_CONFIG, WIFI_CONNECTED_COLOR_KEY)); + } + Serial.print("IP Address: "); + Serial.println(WiFi.localIP()); + //connected + stopAnimation(); + return true; + } + + return startConfigPortal(); +} + +int WiFiManager::connectWifi(String ssid, String pass) +{ + Serial.println("Connecting as wifi client..."); + + //fix for auto connect racing issue + if (WiFi.status() == WL_CONNECTED) + { + Serial.println("Already connected. Bailing out."); + return WL_CONNECTED; + } + //check if we have ssid and pass and force those, if not, try with last saved values + if (ssid != "") + { + WiFi.begin(ssid.c_str(), pass.c_str()); + } + else + { + if (WiFi.SSID()) + { + Serial.println("Using last saved values"); + //trying to fix connection in progress hanging + WiFi.disconnect(); + + WiFi.begin(); + } + else + { + Serial.println("No saved credentials"); + } + } + + int connRes = waitForConnectResult(); + Serial.print("Connection result: "); + Serial.println(connRes); + return connRes; +} + +uint8_t WiFiManager::waitForConnectResult() +{ + Serial.println("Waiting for connection result with time out"); + unsigned long start = millis(); + bool keepConnecting = true; + uint8_t status; + while (keepConnecting) + { + status = WiFi.status(); + if (millis() - start > CONNECTION_TIMEOUT) + { + keepConnecting = false; + Serial.println("Connection timed out"); + } + if (status == WL_CONNECTED || status == WL_CONNECT_FAILED) + { + keepConnecting = false; + } + if(ShelfDisplays != nullptr) + { + ShelfDisplays->delay(100); + } + else + { + delay(100); + } + } + return status; +} + +void WiFiManager::setupConfigPortal() +{ + server->reset(); + + + #ifdef AP_PW + WiFi.softAP(AP_SSID, AP_PW); + #else + WiFi.softAP(AP_SSID); + #endif + + delay(500); + + /* Setup the DNS server redirecting all the domains to the apIP */ + dnsServer->setErrorReplyCode(DNSReplyCode::NoError); + dnsServer->start(DNS_PORT, "*", WiFi.softAPIP()); + + server->serveStatic("/", SPIFFS, "/").setDefaultFile("index.html"); + // /* Setup web pages: root, wifi config pages, SO captive portal detectors and not found. */ + server->on("/", std::bind(&WiFiManager::handleRoot, this, std::placeholders::_1)); + server->onNotFound(std::bind(&WiFiManager::handleNotFound, this, std::placeholders::_1)); + AsyncCallbackJsonWebHandler* WIFIConfigHandler = new AsyncCallbackJsonWebHandler("/WIFISettings", std::bind(&WiFiManager::handleWifiSubmit, this, std::placeholders::_1, std::placeholders::_2)); + server->addHandler(WIFIConfigHandler); + AsyncCallbackJsonWebHandler* baseConfigHandler = new AsyncCallbackJsonWebHandler("/BaseConfig", std::bind(&WiFiManager::handleSaveUnavailable, this, std::placeholders::_1, std::placeholders::_2)); + server->addHandler(baseConfigHandler); + AsyncCallbackJsonWebHandler* colorConfigHandler = new AsyncCallbackJsonWebHandler("/ColorConfig", std::bind(&WiFiManager::handleSaveUnavailable, this, std::placeholders::_1, std::placeholders::_2)); + server->addHandler(colorConfigHandler); + AsyncCallbackJsonWebHandler* hwConfigHandler = new AsyncCallbackJsonWebHandler("/HWConfig", std::bind(&WiFiManager::handleSaveUnavailable, this, std::placeholders::_1, std::placeholders::_2)); + server->addHandler(hwConfigHandler); + server->begin(); // Web server start + Serial.println("HTTP server started"); +} + +void WiFiManager::handleSaveUnavailable(AsyncWebServerRequest *request, JsonVariant &json) +{ + request->send(200, "text/plain", "ERROR: Changes not saved. Please connect to a WIFI network first."); +} + +void WiFiManager::handleWifiSubmit(AsyncWebServerRequest *request, JsonVariant &json) +{ + JsonObject temp = json.as(); + if(temp.containsKey("SSID") && temp.containsKey("PW")) + { + const char* SSID = temp["SSID"]; + const char* PW = temp["PW"]; + WIFI_SSID = String(SSID); + WIFI_PW = String(PW); + connect = true; + request->send(200, "text/plain", "The device will now attempt to connect to \"" + WIFI_SSID + "\" Connection to the web interface will be lost."); + } + else + { + request->send(200, "text/plain", "ERROR: All two keys (SSID, PW are mandatory but at least one of them was missing."); + } +} + +void WiFiManager::handleRoot(AsyncWebServerRequest *request) +{ + if(captivePortal(request)) + { + return; + } + request->send(SPIFFS, "/index.html", "text/html"); +} + +void WiFiManager::handleNotFound(AsyncWebServerRequest *request) +{ + if(captivePortal(request)) + { + return; + } + request->send(SPIFFS, "/index.html", "text/html"); +} + +bool WiFiManager::startConfigPortal() +{ + if(ShelfDisplays != nullptr) + { + ShelfDisplays->setAllSegmentColors(config->getProperty(ConfigManager::COLOR_CONFIG, WIFI_CONFIG_COLOR_KEY)); + } + //setup AP + WiFi.mode(WIFI_AP_STA); + Serial.println("SET AP STA"); + + connect = false; + setupConfigPortal(); + + while (1) + { + //DNS + dnsServer->processNextRequest(); + + if (connect) + { + connect = false; + delay(2000); + Serial.println("Connecting to new AP"); + + if (connectWifi(WIFI_SSID, WIFI_PW) != WL_CONNECTED) + { + Serial.println("Failed to connect."); + } + else //connected + { + //TODO: Display the new IP in the UI and wait for user confirmation before switching off the fallback AP + WiFi.mode(WIFI_STA); + reconnectEvent = WiFi.onEvent(std::bind(&WiFiManager::tryToReconnect, this, std::placeholders::_1, std::placeholders::_2), SYSTEM_EVENT_STA_DISCONNECTED); + stopAnimation(); + break; + } + } + yield(); + } + + server->reset(); + dnsServer->stop(); + + return WiFi.status() == WL_CONNECTED; +} + +void WiFiManager::tryToReconnect(WiFiEvent_t event, WiFiEventInfo_t info) +{ + Serial.print("WiFi lost connection. Reason: "); + Serial.println(info.disconnected.reason); + Serial.println("Trying to Reconnect"); + WiFi.disconnect(); + connectWifi("",""); +} + +/** Redirect to captive portal if we got a request for another domain. Return true in that case so the page handler do not try to handle the request again. */ +bool WiFiManager::captivePortal(AsyncWebServerRequest *request) +{ + if(request->host() != WiFi.softAPIP().toString()) + { + Serial.printf("Redirecting %s to %s\n\r", request->host().c_str(), WiFi.softAPIP().toString().c_str()); + request->redirect(String("http://") + WiFi.softAPIP().toString() + String("/WIFI")); + return true; + } + return false; +} + +String WiFiManager::saveWiFiSettings(ConfigManager::ConfigType type, JsonObject* json) +{ + String FailedKeys = ""; + bool anyErrors = false; + for (JsonPair kv : *json) + { + if(!config->setProperty(kv, type)) + { + if(anyErrors != false) + { + FailedKeys += ", "; + } + FailedKeys += kv.key().c_str(); + anyErrors = true; + } + } + config->applyChanges(); + config->saveConfigPersistent(type); + if(anyErrors == true) + { + String ErrorMessage = "ERROR: Following base config properties were not set: " + FailedKeys; + Serial.print("[W]: "); + Serial.println(ErrorMessage); + return ErrorMessage; + } + else + { + return "Config Saved without errors"; + } +} + +String WiFiManager::saveSettings(ConfigManager::ConfigType type, JsonObject* json) +{ + String FailedKeys = ""; + bool anyErrors = false; + for (JsonPair kv : *json) + { + if(!config->setProperty(kv, type)) + { + if(anyErrors != false) + { + FailedKeys += ", "; + } + FailedKeys += kv.key().c_str(); + anyErrors = true; + } + } + config->applyChanges(); + config->saveConfigPersistent(type); + if(anyErrors == true) + { + String ErrorMessage = "ERROR: Following base config properties were not set: " + FailedKeys; + Serial.print("[W]: "); + Serial.println(ErrorMessage); + return ErrorMessage; + } + else + { + return "Config Saved without errors"; + } +} + +void WiFiManager::startWebUI() +{ + Serial.println("Starting web UI"); + server->reset(); + server->serveStatic("/", SPIFFS, "/").setDefaultFile("index.html"); + + server->onNotFound([](AsyncWebServerRequest *request) + { + request->send(SPIFFS, "/index.html", "text/html"); + }); + + AsyncCallbackJsonWebHandler* baseConfigHandler = new AsyncCallbackJsonWebHandler("/BaseConfig", [this](AsyncWebServerRequest *request, JsonVariant &json) + { + JsonObject temp = json.as(); + request->send(200, "text/plain", saveSettings(ConfigManager::BASE_CONFIG, &temp)); + config->restartIfRequired(); + }); + server->addHandler(baseConfigHandler); + + AsyncCallbackJsonWebHandler* colorConfigHandler = new AsyncCallbackJsonWebHandler("/ColorConfig", [this](AsyncWebServerRequest *request, JsonVariant &json) + { + JsonObject temp = json.as(); + request->send(200, "text/plain", saveSettings(ConfigManager::COLOR_CONFIG, &temp)); + config->restartIfRequired(); + }); + server->addHandler(colorConfigHandler); + + AsyncCallbackJsonWebHandler* hwConfigHandler = new AsyncCallbackJsonWebHandler("/HWConfig", [this](AsyncWebServerRequest *request, JsonVariant &json) + { + JsonObject temp = json.as(); + request->send(200, "text/plain", saveSettings(ConfigManager::HW_CONFIG, &temp)); + config->restartIfRequired(); + }); + server->addHandler(hwConfigHandler); + + AsyncCallbackJsonWebHandler* WIFIConfigHandler = new AsyncCallbackJsonWebHandler("/WIFISettings", std::bind(&WiFiManager::handleWifiSubmit, this, std::placeholders::_1, std::placeholders::_2)); + server->addHandler(WIFIConfigHandler); + + server->begin(); + Serial.println("Web interface should be up and running"); +} \ No newline at end of file diff --git a/lib/LED_clock/library.json b/lib/LED_clock/library.json new file mode 100644 index 0000000..1675797 --- /dev/null +++ b/lib/LED_clock/library.json @@ -0,0 +1,34 @@ +{ + "name": "LED_Clock-Only", + "version": "1.0.0", + "build": { + "srcFilter": [ + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+", + "+" + ], + "flags": [ + "-I Modules/Animator/inc", + "-I Modules/ClockState/inc", + "-I Modules/DisplayManager/inc", + "-I Modules/Misc/inc", + "-I Modules/SevenSegment/inc", + "-I Modules/TimeManager/inc", + "-I Modules/ConfigManager/inc", + "-I Modules/WiFiManager/inc", + "-I Config/Setup/clock-only", + "-I Config/Blynk/default", + "-I Config/Animations/clock-only", + "-I Config/Transitions/default" + ] + } +} diff --git a/lib/LED_clock/library_clock-only.json b/lib/LED_clock/library_clock-only.json index 87b5471..75db5b7 100644 --- a/lib/LED_clock/library_clock-only.json +++ b/lib/LED_clock/library_clock-only.json @@ -22,6 +22,7 @@ "-I Modules/DisplayManager/inc", "-I Modules/SevenSegment/inc", "-I Modules/TimeManager/inc", + "-I Modules/WiFiManager/inc", "-I Config/Setup/clock-only", "-I Config/Blynk/default", "-I Config/Animations/clock-only", diff --git a/lib/LED_clock/library_default.json b/lib/LED_clock/library_default.json index 866baf5..346798a 100644 --- a/lib/LED_clock/library_default.json +++ b/lib/LED_clock/library_default.json @@ -20,8 +20,11 @@ "-I Modules/Animator/inc", "-I Modules/ClockState/inc", "-I Modules/DisplayManager/inc", + "-I Modules/Misc/inc", "-I Modules/SevenSegment/inc", "-I Modules/TimeManager/inc", + "-I Modules/ConfigManager/inc", + "-I Modules/WiFiManager/inc", "-I Config/Setup/default", "-I Config/Blynk/default", "-I Config/Animations/default", diff --git a/lib/LED_clock/library_diy-machines.json b/lib/LED_clock/library_diy-machines.json index 4327dda..c63660b 100644 --- a/lib/LED_clock/library_diy-machines.json +++ b/lib/LED_clock/library_diy-machines.json @@ -22,6 +22,7 @@ "-I Modules/DisplayManager/inc", "-I Modules/SevenSegment/inc", "-I Modules/TimeManager/inc", + "-I Modules/WiFiManager/inc", "-I Config/Setup/diy-machines", "-I Config/Blynk/default", "-I Config/Animations/diy-machines", diff --git a/partitions_custom.csv b/partitions_custom.csv new file mode 100644 index 0000000..2bf77f5 --- /dev/null +++ b/partitions_custom.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x180000, +app1, app, ota_1, 0x190000,0x180000, +spiffs, data, spiffs, 0x310000,0x90000, \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 78de737..7365ad2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -14,11 +14,19 @@ board = nodemcu-32s framework = arduino upload_speed = 921600 monitor_speed = 115200 -lib_deps = +board_build.partitions = partitions_custom.csv +lib_deps = fastled/FastLED@^3.4.0 - ivanseidel/LinkedList@0.0.0-alpha+sha.dac3874d28 blynkkk/Blynk@^0.6.7 + bblanchon/ArduinoJson@^6.17.3 + me-no-dev/ESP Async WebServer@^1.2.3 + ivanseidel/LinkedList@0.0.0-alpha+sha.dac3874d28 + +; Uncomment these lines to use the debugger. +debug_tool = esp-prog +; upload_protocol = esp-prog +; debug_init_break = tbreak setup ; Uncomment these to use OTA as a flash method. This only works if the ESp already has code running on it where OTA update is enabled. ; upload_protocol = espota -; upload_port = 192.168.0.5 ;LED-Shelf.local \ No newline at end of file +; upload_port = 192.168.1.116 ;LED-Shelf.local \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 425a93c..ce8ecc3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,28 +8,34 @@ #include #include "DisplayManager.h" #include "ClockState.h" - -#if RUN_WITHOUT_WIFI == false - #include "WiFi.h" -#endif -#if ENABLE_OTA_UPLOAD == true - #include -#endif -#if IS_BLYNK_ACTIVE == true - #include "BlynkConfig.h" -#endif - -DisplayManager* ShelfDisplays = DisplayManager::getInstance(); -BlynkConfig* BlynkConfiguration = BlynkConfig::getInstance(); -TimeManager* timeM = TimeManager::getInstance(); -ClockState* states = ClockState::getInstance(); - -#if ENABLE_OTA_UPLOAD == true - void setupOTA(); -#endif -#if RUN_WITHOUT_WIFI == false - void wifiSetup(); -#endif +#include "ConfigManager.h" +#include "WiFiManager.h" +#include +#include "BlynkConfig.h" + +#include "ESPAsyncWebServer.h" +#include "AsyncJson.h" +#include +#include "WiFi.h" +#include + +DisplayManager* ShelfDisplays; +BlynkConfig* BlynkConfiguration; +TimeManager* timeM; +ClockState* states; +ConfigManager* config; +WiFiManager* WIFIManager; + +AsyncWebServer server(80); +bool OTAprogressFirstStep = true; +wifi_event_id_t reconnectEvent; +bool wifiReconnectFlag = false; +String WIFI_SSID; +String WIFI_PW; + +void setupOTA(); +void wifiSetup(); +void connectToWIFI(const char* SSID = nullptr, const char* PW = nullptr); void TimerTick(); void TimerDone(); @@ -83,6 +89,15 @@ void setup() { Serial.begin(115200); + Serial.println("Loading config..."); + config = ConfigManager::getInstance(); + Serial.println("Initializing modules..."); + ShelfDisplays = DisplayManager::getInstance(); + BlynkConfiguration = BlynkConfig::getInstance(); + states = ClockState::getInstance(); + WIFIManager = WiFiManager::getInstance(); + + Serial.println("Initializing LEDs..."); ShelfDisplays->InitSegments(0, NUM_LEDS_PER_SEGMENT, WIFI_CONNECTING_COLOR, 50); ShelfDisplays->setHourSegmentColors(HOUR_COLOR); @@ -90,20 +105,15 @@ void setup() ShelfDisplays->setInternalLEDColor(INTERNAL_COLOR); ShelfDisplays->setDotLEDColor(SEPERATION_DOT_COLOR); - #if RUN_WITHOUT_WIFI == false - wifiSetup(); - #endif - #if ENABLE_OTA_UPLOAD == true - setupOTA(); - #endif - #if ENABLE_OTA_UPLOAD == true - ArduinoOTA.handle(); //give ota the opportunity to update before the main loop starts in case we have a crash in there - #endif + Serial.println("Running wifi setup..."); + WIFIManager->autoConnect(ShelfDisplays); + WIFIManager->startWebUI(); + setupOTA(); + ArduinoOTA.handle(); //give ota the opportunity to update before the main loop starts in case we have a crash in there - #if IS_BLYNK_ACTIVE == true - BlynkConfiguration->setup(); - #endif + BlynkConfiguration->setup(); + timeM = TimeManager::getInstance(); Serial.println("Fetching time from NTP server..."); if(timeM->init() == false) { @@ -113,6 +123,7 @@ void setup() timeM->setTimerDoneCallback(TimerDone); timeM->setAlarmCallback(AlarmTriggered); + Serial.printf("\tMax allocated heap: %d/%d\n\r\tMax allocatedPSram %d/%d\n\r", ESP.getMaxAllocHeap(), ESP.getHeapSize(), ESP.getMaxAllocPsram(), ESP.getPsramSize()); Serial.println("Displaying startup animation..."); startupAnimation(); Serial.println("Setup done. Main Loop starting..."); @@ -120,209 +131,105 @@ void setup() void loop() { - #if ENABLE_OTA_UPLOAD == true - ArduinoOTA.handle(); - #endif + ArduinoOTA.handle(); states->handleStates(); //updates display states, switches between modes etc. - - // Test code: - // if((millis()-last)>= 1500) - // { - // ShelfDisplays->test(); - // last = millis(); - // } ShelfDisplays->handle(); } void AlarmTriggered() { states->switchMode(ClockState::ALARM_NOTIFICATION); - #if IS_BLYNK_ACTIVE == true - BlynkConfiguration->updateUI(); - #endif + BlynkConfiguration->updateUI(); } void TimerTick() { - #if IS_BLYNK_ACTIVE == true - BlynkConfiguration->updateUI(); - #endif + BlynkConfiguration->updateUI(); } void TimerDone() { states->switchMode(ClockState::TIMER_NOTIFICATION); - #if IS_BLYNK_ACTIVE == true - BlynkConfiguration->updateUI(); - #endif + BlynkConfiguration->updateUI(); } -#if RUN_WITHOUT_WIFI == false - void WiFiStationDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) - { - Serial.print("WiFi lost connection. Reason: "); - Serial.println(info.disconnected.reason); - Serial.println("Trying to Reconnect"); - WiFi.disconnect(); - WiFi.reconnect(); - } +void setupOTA() +{ + // Port defaults to 3232 + // ArduinoOTA.setPort(3232); - void wifiSetup() - { - #if USE_ESPTOUCH_SMART_CONFIG == true - WiFi.reconnect(); //try to reconnect - Serial.println("Trying to reconnect to previous wifi network"); - #else - WiFi.begin(WIFI_SSID, WIFI_PW); - #endif - ShelfDisplays->setAllSegmentColors(WIFI_CONNECTING_COLOR); - ShelfDisplays->showLoadingAnimation(); - for (int i = 0; i < NUM_RETRIES; i++) - { - Serial.print("."); - #if USE_ESPTOUCH_SMART_CONFIG == true - if(WiFi.begin() == WL_CONNECTED) - #else - if(WiFi.status() == WL_CONNECTED) - #endif - { - Serial.println("Reconnect successful"); - ShelfDisplays->setAllSegmentColors(WIFI_CONNECTION_SUCCESSFUL_COLOR); - break; - } - ShelfDisplays->delay(500); - } + // Hostname defaults to esp3232-[MAC] + ArduinoOTA.setHostname(OTA_UPDATE_HOST_NAME); - if(WiFi.status() != WL_CONNECTED) - { - #if USE_ESPTOUCH_SMART_CONFIG == true - Serial.println("Reconnect failed. starting smart config"); - WiFi.mode(WIFI_AP_STA); - // start SmartConfig - WiFi.beginSmartConfig(); + // No authentication by default + //ArduinoOTA.setPassword("admin"); - // Wait for SmartConfig packet from mobile - Serial.println("Waiting for SmartConfig."); - ShelfDisplays->setAllSegmentColors(WIFI_SMART_CONFIG_COLOR); - while (!WiFi.smartConfigDone()) - { - Serial.print("."); - ShelfDisplays->delay(500); - } - ShelfDisplays->setAllSegmentColors(WIFI_CONNECTING_COLOR); - Serial.println(""); - Serial.println("SmartConfig done."); + // Password can be set with it's md5 value as well + // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3 + // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3"); - // Wait for WiFi to connect to AP - Serial.println("Waiting for WiFi"); - while (WiFi.status() != WL_CONNECTED) - { - Serial.print("."); - ShelfDisplays->setAllSegmentColors(WIFI_CONNECTION_SUCCESSFUL_COLOR); - ShelfDisplays->delay(500); - } - Serial.println("WiFi Connected."); - Serial.print("IP Address: "); - Serial.println(WiFi.localIP()); - #else - Serial.println("WIFI connection failed"); - ShelfDisplays->setAllSegmentColors(ERROR_COLOR); - #endif - if(WiFi.status() != WL_CONNECTED) - { - Serial.println("WIFI connection failed. Aborting execution."); - abort(); - } + ArduinoOTA.onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) + { + type = "sketch"; } - WiFi.onEvent(WiFiStationDisconnected, SYSTEM_EVENT_STA_DISCONNECTED); - ShelfDisplays->stopLoadingAnimation(); - Serial.println("Waiting for loading animation to finish..."); - ShelfDisplays->waitForLoadingAnimationFinish(); - ShelfDisplays->turnAllSegmentsOff(); - } -#endif - -#if ENABLE_OTA_UPLOAD == true - bool progressFirstStep = true; - void setupOTA() + else // U_SPIFFS + { + type = "filesystem"; + SPIFFS.end(); + } + // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() + Serial.println("Start updating " + type); + timeM->disableTimer(); + BlynkConfiguration->stop(); + ShelfDisplays->setAllSegmentColors(OTA_UPDATE_COLOR); + ShelfDisplays->turnAllLEDsOff(); //instead of the loading animation show a progress bar + ShelfDisplays->setGlobalBrightness(50); + }) + .onEnd([]() { - // Port defaults to 3232 - // ArduinoOTA.setPort(3232); - - // Hostname defaults to esp3232-[MAC] - ArduinoOTA.setHostname(OTA_UPDATE_HOST_NAME); - - // No authentication by default - //ArduinoOTA.setPassword("admin"); - - // Password can be set with it's md5 value as well - // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3 - // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3"); - - ArduinoOTA.onStart([]() { - String type; - if (ArduinoOTA.getCommand() == U_FLASH) - { - type = "sketch"; - } - else // U_SPIFFS - { - type = "filesystem"; - } - // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() - Serial.println("Start updating " + type); - timeM->disableTimer(); - #if IS_BLYNK_ACTIVE == true - BlynkConfiguration->stop(); - #endif - ShelfDisplays->setAllSegmentColors(OTA_UPDATE_COLOR); - ShelfDisplays->turnAllLEDsOff(); //instead of the loading animation show a progress bar - ShelfDisplays->setGlobalBrightness(50); - }) - .onEnd([]() + Serial.println("\nOTA End"); + }) + .onProgress([](unsigned int progress, unsigned int total) + { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + if(OTAprogressFirstStep == true) { - Serial.println("\nOTA End"); - }) - .onProgress([](unsigned int progress, unsigned int total) + ShelfDisplays->displayProgress(total); + OTAprogressFirstStep = false; + } + ShelfDisplays->updateProgress(progress); + }) + .onError([](ota_error_t error) + { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) { - Serial.printf("Progress: %u%%\r", (progress / (total / 100))); - if(progressFirstStep == true) - { - ShelfDisplays->displayProgress(total); - progressFirstStep = false; - } - ShelfDisplays->updateProgress(progress); - }) - .onError([](ota_error_t error) + Serial.println("Auth Failed"); + } + else if (error == OTA_BEGIN_ERROR) { - Serial.printf("Error[%u]: ", error); - if (error == OTA_AUTH_ERROR) - { - Serial.println("Auth Failed"); - } - else if (error == OTA_BEGIN_ERROR) - { - Serial.println("Begin Failed"); - } - else if (error == OTA_CONNECT_ERROR) - { - Serial.println("Connect Failed"); - } - else if (error == OTA_RECEIVE_ERROR) - { - Serial.println("Receive Failed"); - } - else if (error == OTA_END_ERROR) - { - Serial.println("End Failed"); - } - ShelfDisplays->setAllSegmentColors(ERROR_COLOR); - }); + Serial.println("Begin Failed"); + } + else if (error == OTA_CONNECT_ERROR) + { + Serial.println("Connect Failed"); + } + else if (error == OTA_RECEIVE_ERROR) + { + Serial.println("Receive Failed"); + } + else if (error == OTA_END_ERROR) + { + Serial.println("End Failed"); + } + ShelfDisplays->setAllSegmentColors(ERROR_COLOR); + }); - ArduinoOTA.begin(); + ArduinoOTA.begin(); - Serial.println("OTA update functionality is ready"); - Serial.print("IP address: "); - Serial.println(WiFi.localIP()); - } -#endif + Serial.println("OTA update functionality is ready"); + Serial.print("IP address: "); + Serial.println(WiFi.localIP()); +}