diff --git a/.gitignore b/.gitignore index 84eaebde6..827f0754c 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,4 @@ sdkconfig.* sdkconfig_tasmota_esp32 pnpm-lock.yaml package.json +.cache/ \ No newline at end of file diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index 3245fb989..995aa140f 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -67,3 +67,4 @@ For more details go to [docs.emsesp.org](https://docs.emsesp.org/). - place system message command in side scheduler loop to reduce stack memory usage by 2KB - syslog mark interval set to 1 hour - handle process_telegram in oneloop +- improved GPIO validation in Analog Sensors and System settings diff --git a/interface/package.json b/interface/package.json index d211a7974..5597e723b 100644 --- a/interface/package.json +++ b/interface/package.json @@ -17,7 +17,7 @@ "preview-standalone": "typesafe-i18n --no-watch && vite build && concurrently -c \"auto\" \"pnpm:mock-rest\" \"vite preview\"", "standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite dev\"", "typesafe-i18n": "typesafe-i18n --no-watch", - "webUI": "vite build && node progmem-generator.js", + "build_webUI": "typesafe-i18n --no-watch && vite build && node progmem-generator.js", "format": "prettier -l -w '**/*.{ts,tsx,js,css,json,md}'", "lint": "eslint . --fix", "standalone-devcontainer": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite --host\"" @@ -53,7 +53,7 @@ "@preact/preset-vite": "^2.10.2", "@trivago/prettier-plugin-sort-imports": "^6.0.0", "@types/node": "^24.10.1", - "@types/react": "^19.2.4", + "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "axe-core": "^4.11.0", "concurrently": "^9.2.1", diff --git a/interface/pnpm-lock.yaml b/interface/pnpm-lock.yaml index 03f6f9c14..22cd316b2 100644 --- a/interface/pnpm-lock.yaml +++ b/interface/pnpm-lock.yaml @@ -13,22 +13,22 @@ importers: version: 2.2.1(alova@3.3.4) '@emotion/react': specifier: ^11.14.0 - version: 11.14.0(@types/react@19.2.4)(react@19.2.0) + version: 11.14.0(@types/react@19.2.5)(react@19.2.0) '@emotion/styled': specifier: ^11.14.1 - version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) + version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0) '@mui/icons-material': specifier: ^7.3.5 - version: 7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) + version: 7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.5)(react@19.2.0) '@mui/material': specifier: ^7.3.5 - version: 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@preact/compat': specifier: ^18.3.1 version: 18.3.1(preact@10.27.2) '@table-library/react-table-library': specifier: 4.1.15 - version: 4.1.15(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 4.1.15(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) alova: specifier: 3.3.4 version: 3.3.4 @@ -91,11 +91,11 @@ importers: specifier: ^24.10.1 version: 24.10.1 '@types/react': - specifier: ^19.2.4 - version: 19.2.4 + specifier: ^19.2.5 + version: 19.2.5 '@types/react-dom': specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.4) + version: 19.2.3(@types/react@19.2.5) axe-core: specifier: ^4.11.0 version: 4.11.0 @@ -879,8 +879,8 @@ packages: peerDependencies: '@types/react': '*' - '@types/react@19.2.4': - resolution: {integrity: sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A==} + '@types/react@19.2.5': + resolution: {integrity: sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==} '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} @@ -1225,8 +1225,8 @@ packages: resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} engines: {node: '>=8.0.0'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.1: + resolution: {integrity: sha512-98XGutrXoh75MlgLihlNxAGbUuFQc7l1cqcnEZlLNKc0UrVdPndgmaDmYTDDh929VS/eqTZV0rozmhu2qqT1/g==} currently-unhandled@0.4.1: resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==} @@ -1337,8 +1337,8 @@ packages: duplexer3@0.1.5: resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} - electron-to-chromium@1.5.250: - resolution: {integrity: sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==} + electron-to-chromium@1.5.253: + resolution: {integrity: sha512-O0tpQ/35rrgdiGQ0/OFWhy1itmd9A6TY9uQzlqj3hKSu/aYpe7UIn5d7CU2N9myH6biZiWF3VMZVuup8pw5U9w==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -3268,7 +3268,7 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0)': + '@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 @@ -3280,7 +3280,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 19.2.0 optionalDependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.5 transitivePeerDependencies: - supports-color @@ -3290,22 +3290,22 @@ snapshots: '@emotion/memoize': 0.9.0 '@emotion/unitless': 0.10.0 '@emotion/utils': 1.4.2 - csstype: 3.1.3 + csstype: 3.2.1 '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0)': + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 '@emotion/is-prop-valid': 1.4.0 - '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.5)(react@19.2.0) '@emotion/serialize': 1.3.3 '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.0) '@emotion/utils': 1.4.2 react: 19.2.0 optionalDependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.5 transitivePeerDependencies: - supports-color @@ -3489,90 +3489,90 @@ snapshots: '@mui/core-downloads-tracker@7.3.5': {} - '@mui/icons-material@7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.4)(react@19.2.0)': + '@mui/icons-material@7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.5)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 optionalDependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.5 - '@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@mui/core-downloads-tracker': 7.3.5 - '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) - '@mui/types': 7.4.8(@types/react@19.2.4) - '@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) + '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0) + '@mui/types': 7.4.8(@types/react@19.2.5) + '@mui/utils': 7.3.5(@types/react@19.2.5)(react@19.2.0) '@popperjs/core': 2.11.8 - '@types/react-transition-group': 4.4.12(@types/react@19.2.4) + '@types/react-transition-group': 4.4.12(@types/react@19.2.5) clsx: 2.1.1 - csstype: 3.1.3 + csstype: 3.2.1 prop-types: 15.8.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) react-is: 19.2.0 react-transition-group: 4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) - '@types/react': 19.2.4 + '@emotion/react': 11.14.0(@types/react@19.2.5)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0) + '@types/react': 19.2.5 - '@mui/private-theming@7.3.5(@types/react@19.2.4)(react@19.2.0)': + '@mui/private-theming@7.3.5(@types/react@19.2.5)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) + '@mui/utils': 7.3.5(@types/react@19.2.5)(react@19.2.0) prop-types: 15.8.1 react: 19.2.0 optionalDependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.5 - '@mui/styled-engine@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(react@19.2.0)': + '@mui/styled-engine@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 '@emotion/sheet': 1.4.0 - csstype: 3.1.3 + csstype: 3.2.1 prop-types: 15.8.1 react: 19.2.0 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.5)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0) - '@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0)': + '@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/private-theming': 7.3.5(@types/react@19.2.4)(react@19.2.0) - '@mui/styled-engine': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(react@19.2.0) - '@mui/types': 7.4.8(@types/react@19.2.4) - '@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) + '@mui/private-theming': 7.3.5(@types/react@19.2.5)(react@19.2.0) + '@mui/styled-engine': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(react@19.2.0) + '@mui/types': 7.4.8(@types/react@19.2.5) + '@mui/utils': 7.3.5(@types/react@19.2.5)(react@19.2.0) clsx: 2.1.1 - csstype: 3.1.3 + csstype: 3.2.1 prop-types: 15.8.1 react: 19.2.0 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) - '@types/react': 19.2.4 + '@emotion/react': 11.14.0(@types/react@19.2.5)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0) + '@types/react': 19.2.5 - '@mui/types@7.4.8(@types/react@19.2.4)': + '@mui/types@7.4.8(@types/react@19.2.5)': dependencies: '@babel/runtime': 7.28.4 optionalDependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.5 - '@mui/utils@7.3.5(@types/react@19.2.4)(react@19.2.0)': + '@mui/utils@7.3.5(@types/react@19.2.5)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/types': 7.4.8(@types/react@19.2.4) + '@mui/types': 7.4.8(@types/react@19.2.5) '@types/prop-types': 15.7.15 clsx: 2.1.1 prop-types: 15.8.1 react: 19.2.0 react-is: 19.2.0 optionalDependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.5 '@noble/hashes@1.8.0': {} @@ -3707,9 +3707,9 @@ snapshots: '@sindresorhus/is@0.7.0': {} - '@table-library/react-table-library@4.1.15(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@table-library/react-table-library@4.1.15(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.5)(react@19.2.0) clsx: 1.1.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) @@ -3786,17 +3786,17 @@ snapshots: '@types/prop-types@15.7.15': {} - '@types/react-dom@19.2.3(@types/react@19.2.4)': + '@types/react-dom@19.2.3(@types/react@19.2.5)': dependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.5 - '@types/react-transition-group@4.4.12(@types/react@19.2.4)': + '@types/react-transition-group@4.4.12(@types/react@19.2.5)': dependencies: - '@types/react': 19.2.4 + '@types/react': 19.2.5 - '@types/react@19.2.4': + '@types/react@19.2.5': dependencies: - csstype: 3.1.3 + csstype: 3.2.1 '@types/responselike@1.0.3': dependencies: @@ -4022,7 +4022,7 @@ snapshots: dependencies: baseline-browser-mapping: 2.8.28 caniuse-lite: 1.0.30001754 - electron-to-chromium: 1.5.250 + electron-to-chromium: 1.5.253 node-releases: 2.0.27 update-browserslist-db: 1.1.4(browserslist@4.28.0) @@ -4211,7 +4211,7 @@ snapshots: dependencies: css-tree: 1.1.3 - csstype@3.1.3: {} + csstype@3.2.1: {} currently-unhandled@0.4.1: dependencies: @@ -4294,7 +4294,7 @@ snapshots: dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.28.4 - csstype: 3.1.3 + csstype: 3.2.1 dom-serializer@1.4.1: dependencies: @@ -4367,7 +4367,7 @@ snapshots: duplexer3@0.1.5: {} - electron-to-chromium@1.5.250: {} + electron-to-chromium@1.5.253: {} emoji-regex@8.0.0: {} diff --git a/interface/src/app/main/Sensors.tsx b/interface/src/app/main/Sensors.tsx index bbb8fd636..52fe701b5 100644 --- a/interface/src/app/main/Sensors.tsx +++ b/interface/src/app/main/Sensors.tsx @@ -54,7 +54,7 @@ const MS_PER_SECOND = 1000; const MS_PER_MINUTE = 60 * MS_PER_SECOND; const MS_PER_HOUR = 60 * MS_PER_MINUTE; const MS_PER_DAY = 24 * MS_PER_HOUR; -const DEFAULT_GPIO = 21; // Safe GPIO for all platforms +const DEFAULT_GPIO = 99; // not set const MIN_TEMP_ID = -100; const MAX_TEMP_ID = 100; const GPIO_25 = 25; @@ -134,6 +134,7 @@ const Sensors = () => { ts: [], as: [], analog_enabled: false, + valid_gpio_list: [], platform: 'ESP32' } }); @@ -573,12 +574,8 @@ const Sensors = () => { onSave={onAnalogDialogSave} creating={creating} selectedItem={selectedAnalogSensor} - validator={analogSensorItemValidation( - sensorData.as, - selectedAnalogSensor, - creating, - sensorData.platform - )} + analogGPIOList={sensorData.valid_gpio_list} + validator={analogSensorItemValidation(sensorData.as, selectedAnalogSensor)} /> )} {sensorData?.analog_enabled === true && me.admin && ( diff --git a/interface/src/app/main/SensorsAnalogDialog.tsx b/interface/src/app/main/SensorsAnalogDialog.tsx index ee5087d12..baeb8f2b9 100644 --- a/interface/src/app/main/SensorsAnalogDialog.tsx +++ b/interface/src/app/main/SensorsAnalogDialog.tsx @@ -35,6 +35,7 @@ interface DashboardSensorsAnalogDialogProps { onSave: (as: AnalogSensor) => void; creating: boolean; selectedItem: AnalogSensor; + analogGPIOList: number[]; validator: Schema; } @@ -44,6 +45,7 @@ const SensorsAnalogDialog = ({ onSave, creating, selectedItem, + analogGPIOList, validator }: DashboardSensorsAnalogDialogProps) => { const { LL } = useI18nContext(); @@ -151,33 +153,51 @@ const SensorsAnalogDialog = ({ [creating, LL] ); + // Ensure the current GPIO is in the list when no creating + // note GPIO 99 means not set + const availableGPIOs = useMemo(() => { + const filteredList = analogGPIOList.filter((gpio) => gpio !== 99); + if ( + editItem.g !== undefined && + editItem.g !== 99 && + !filteredList.includes(editItem.g) + ) { + return [...filteredList, editItem.g].sort((a, b) => a - b); + } + return filteredList; + }, [analogGPIOList, editItem.g]); + return ( {dialogTitle} - - + ) : ( + - - {creating && ( - - - {LL.WARN_GPIO()} - - + > + {availableGPIOs?.map((gpio: number) => ( + + {gpio} + + ))} + )} {analogTypeMenuItems} @@ -207,6 +228,7 @@ const SensorsAnalogDialog = ({ sx={{ width: '15ch' }} select onChange={updateFormValue} + disabled={editItem.s} > {uomMenuItems} @@ -222,6 +244,7 @@ const SensorsAnalogDialog = ({ sx={{ width: '11ch' }} variant="outlined" onChange={updateFormValue} + disabled={editItem.s} slotProps={{ input: { startAdornment: ( @@ -243,6 +266,7 @@ const SensorsAnalogDialog = ({ type="number" variant="outlined" onChange={updateFormValue} + disabled={editItem.s} slotProps={{ input: { startAdornment: ( @@ -264,6 +288,7 @@ const SensorsAnalogDialog = ({ sx={{ width: '11ch' }} variant="outlined" onChange={updateFormValue} + disabled={editItem.s} slotProps={{ htmlInput: { step: '0.001' } }} @@ -280,6 +305,7 @@ const SensorsAnalogDialog = ({ sx={{ width: '11ch' }} variant="outlined" onChange={updateFormValue} + disabled={editItem.s} /> )} @@ -293,6 +319,7 @@ const SensorsAnalogDialog = ({ type="number" variant="outlined" onChange={updateFormValue} + disabled={editItem.s} slotProps={{ htmlInput: { step: '0.001' } }} @@ -309,6 +336,7 @@ const SensorsAnalogDialog = ({ type="number" variant="outlined" onChange={updateFormValue} + disabled={editItem.s} slotProps={{ htmlInput: { min: '0', max: '255', step: '1' } }} @@ -325,6 +353,7 @@ const SensorsAnalogDialog = ({ select variant="outlined" onChange={updateFormValue} + disabled={editItem.s} > {LL.OFF()} {LL.ON()} @@ -338,6 +367,7 @@ const SensorsAnalogDialog = ({ sx={{ width: '15ch' }} select onChange={updateFormValue} + disabled={editItem.s} > {LL.ACTIVEHIGH()} {LL.ACTIVELOW()} @@ -351,6 +381,7 @@ const SensorsAnalogDialog = ({ value={editItem.u} select onChange={updateFormValue} + disabled={editItem.s} > {LL.UNCHANGED()} @@ -374,6 +405,7 @@ const SensorsAnalogDialog = ({ variant="outlined" sx={{ width: '11ch' }} onChange={updateFormValue} + disabled={editItem.s} slotProps={{ input: { startAdornment: ( @@ -393,6 +425,7 @@ const SensorsAnalogDialog = ({ sx={{ width: '11ch' }} variant="outlined" onChange={updateFormValue} + disabled={editItem.s} slotProps={{ input: { startAdornment: ( @@ -415,6 +448,7 @@ const SensorsAnalogDialog = ({ sx={{ width: '11ch' }} select onChange={updateFormValue} + disabled={editItem.s} > {LL.ACTIVEHIGH()} {LL.ACTIVELOW()} @@ -429,6 +463,7 @@ const SensorsAnalogDialog = ({ sx={{ width: '15ch' }} variant="outlined" onChange={updateFormValue} + disabled={editItem.s} slotProps={{ input: { startAdornment: ( @@ -442,6 +477,24 @@ const SensorsAnalogDialog = ({ )} + {fieldErrors && Object.keys(fieldErrors).length > 0 && ( + + {Object.values(fieldErrors).map((errArr, idx) => + Array.isArray(errArr) + ? errArr.map((err, j) => ( + + {err.message} + + )) + : null + )} + + )} {editItem.s && ( diff --git a/interface/src/app/main/types.ts b/interface/src/app/main/types.ts index 26dd876c0..280dbc1c3 100644 --- a/interface/src/app/main/types.ts +++ b/interface/src/app/main/types.ts @@ -111,6 +111,7 @@ export interface SensorData { ts: TemperatureSensor[]; as: AnalogSensor[]; analog_enabled: boolean; + valid_gpio_list: number[]; platform: string; } diff --git a/interface/src/app/main/validators.ts b/interface/src/app/main/validators.ts index ad4d6f95b..7ededce0c 100644 --- a/interface/src/app/main/validators.ts +++ b/interface/src/app/main/validators.ts @@ -45,121 +45,15 @@ const VALIDATION_LIMITS = { HEX_BASE: 16 } as const; -// Helper to create GPIO validator from invalid ranges -const createGPIOValidator = ( - invalidRanges: Array, - maxValue: number -) => ({ - validator( - _rule: InternalRuleItem, - value: number, - callback: (error?: string) => void - ) { - if (!value) { - callback(); - return; - } - - if (value < 0 || value > maxValue) { - callback(ERROR_MESSAGES.GPIO_INVALID); - return; - } - - for (const range of invalidRanges) { - if (typeof range === 'number') { - if (value === range) { - callback(ERROR_MESSAGES.GPIO_INVALID); - return; - } - } else { - const [start, end] = range; - if (value >= start && value <= end) { - callback(ERROR_MESSAGES.GPIO_INVALID); - return; - } - } - } - - callback(); - } -}); - -export const GPIO_VALIDATOR = createGPIOValidator( - [[6, 11], 1, 20, 24, [28, 31]], - 40 -); - -export const GPIO_VALIDATORC3 = createGPIOValidator([[11, 19]], 21); - -export const GPIO_VALIDATORS2 = createGPIOValidator( - [ - [19, 20], - [22, 32] - ], - 40 -); - -export const GPIO_VALIDATORS3 = createGPIOValidator( - [ - [19, 20], - [22, 37], - [39, 42] - ], - 48 -); - -const GPIO_FIELD_NAMES = [ - 'led_gpio', - 'dallas_gpio', - 'pbutton_gpio', - 'tx_gpio', - 'rx_gpio' -] as const; - type ValidationRules = Array<{ required?: boolean; message?: string; [key: string]: unknown; }>; -const createGPIOValidations = ( - validator: typeof GPIO_VALIDATOR -): Record => - GPIO_FIELD_NAMES.reduce( - (acc, field) => { - const fieldName = field.replace('_gpio', '').toUpperCase(); - acc[field] = [ - { required: true, message: `${fieldName} GPIO is required` }, - validator - ]; - return acc; - }, - {} as Record - ); - -const PLATFORM_VALIDATORS = { - ESP32: GPIO_VALIDATOR, - ESP32C3: GPIO_VALIDATORC3, - ESP32S2: GPIO_VALIDATORS2, - ESP32S3: GPIO_VALIDATORS3 -} as const; - export const createSettingsValidator = (settings: Settings) => { const schema: Record = {}; - // Add GPIO validations for CUSTOM board profiles - if ( - settings.board_profile === 'CUSTOM' && - settings.platform in PLATFORM_VALIDATORS - ) { - Object.assign( - schema, - createGPIOValidations( - PLATFORM_VALIDATORS[settings.platform as keyof typeof PLATFORM_VALIDATORS] - ) - ); - } - // Syslog validations if (settings.syslog_enabled) { schema.syslog_host = [ @@ -401,52 +295,29 @@ export const temperatureSensorItemValidation = ( n: [NAME_PATTERN, uniqueTemperatureNameValidator(sensors, sensor.o_n)] }); -export const isGPIOUniqueValidator = (sensors: AnalogSensor[]) => ({ - validator( - _rule: InternalRuleItem, - gpio: number, - callback: (error?: string) => void - ) { - if (sensors.some((as) => as.g === gpio)) { - callback(ERROR_MESSAGES.GPIO_DUPLICATE); - return; - } - callback(); - } -}); - export const uniqueAnalogNameValidator = ( sensors: AnalogSensor[], o_name?: string ) => createUniqueFieldNameValidator(sensors, (s) => s.n, o_name); -const getPlatformGPIOValidator = (platform: string) => { - switch (platform) { - case 'ESP32S3': - return GPIO_VALIDATORS3; - case 'ESP32S2': - return GPIO_VALIDATORS2; - case 'ESP32C3': - return GPIO_VALIDATORC3; - default: - return GPIO_VALIDATOR; - } -}; - export const analogSensorItemValidation = ( sensors: AnalogSensor[], - sensor: AnalogSensor, - creating: boolean, - platform: string + sensor: AnalogSensor ) => { - const gpioValidator = getPlatformGPIOValidator(platform); - return new Schema({ - n: [NAME_PATTERN, uniqueAnalogNameValidator(sensors, sensor.o_n)], + // name is required and must be unique + n: [ + { required: true, message: 'Name is required' }, + NAME_PATTERN, + uniqueAnalogNameValidator(sensors, sensor.o_n) + ], g: [ - { required: true, message: 'GPIO is required' }, - gpioValidator, - ...(creating ? [isGPIOUniqueValidator(sensors)] : []) + { + required: true, + type: 'number', + min: 1, + message: 'GPIO is required' + } ] }); }; diff --git a/interface/src/i18n/cz/index.ts b/interface/src/i18n/cz/index.ts index 61b156e6c..704ba83cc 100644 --- a/interface/src/i18n/cz/index.ts +++ b/interface/src/i18n/cz/index.ts @@ -60,7 +60,6 @@ const cz: Translation = { DUTY_CYCLE: 'Pracovní cyklus', UNIT: 'Jednotka', STARTVALUE: 'Počáteční hodnota', - WARN_GPIO: 'Upozornění: buďte opatrní při přiřazování GPIO!', EDIT: 'Upravit', SENSOR: 'Senzor', TEMP_SENSOR: 'Teplotní senzor', diff --git a/interface/src/i18n/de/index.ts b/interface/src/i18n/de/index.ts index cb481cb0e..1e578fc24 100644 --- a/interface/src/i18n/de/index.ts +++ b/interface/src/i18n/de/index.ts @@ -60,7 +60,6 @@ const de: Translation = { DUTY_CYCLE: 'Arbeitszyklus', UNIT: 'Maßeinheit', STARTVALUE: 'Startwert', - WARN_GPIO: 'Warnung: Vorsicht bei der korrekten Wahl des GPIO!', EDIT: 'Editiere', SENSOR: 'Sensor', TEMP_SENSOR: 'Temperatursensor', diff --git a/interface/src/i18n/en/index.ts b/interface/src/i18n/en/index.ts index 81f07294d..38a6e3185 100644 --- a/interface/src/i18n/en/index.ts +++ b/interface/src/i18n/en/index.ts @@ -60,7 +60,6 @@ const en: Translation = { DUTY_CYCLE: 'Duty Cycle', UNIT: 'UoM', STARTVALUE: 'Start Value', - WARN_GPIO: 'Warning: be careful when assigning a GPIO!', EDIT: 'Edit', SENSOR: 'Sensor', TEMP_SENSOR: 'Temperature Sensor', diff --git a/interface/src/i18n/fr/index.ts b/interface/src/i18n/fr/index.ts index cda12b9f0..cfbbaa81d 100644 --- a/interface/src/i18n/fr/index.ts +++ b/interface/src/i18n/fr/index.ts @@ -60,7 +60,6 @@ const fr: Translation = { DUTY_CYCLE: 'Cycle de fonctionnement', UNIT: 'Unité', STARTVALUE: 'Valeur de départ', - WARN_GPIO: 'Attention: soyez vigilant en choisissant un GPIO!', EDIT: 'Éditer', SENSOR: 'Capteur', TEMP_SENSOR: 'Capteur de température', diff --git a/interface/src/i18n/it/index.ts b/interface/src/i18n/it/index.ts index 44cf4f9b6..3dba434f6 100644 --- a/interface/src/i18n/it/index.ts +++ b/interface/src/i18n/it/index.ts @@ -60,7 +60,6 @@ const it: Translation = { DUTY_CYCLE: 'Ciclo di lavoro', UNIT: 'UoM', STARTVALUE: 'Valore di partenza', - WARN_GPIO: 'Avvertimento: prestare attenzione quando si assegna un GPIO!', EDIT: 'Modifica', SENSOR: 'Sensore', TEMP_SENSOR: 'Sensore Temperatura', diff --git a/interface/src/i18n/nl/index.ts b/interface/src/i18n/nl/index.ts index affdddfe2..579202eb6 100644 --- a/interface/src/i18n/nl/index.ts +++ b/interface/src/i18n/nl/index.ts @@ -60,7 +60,6 @@ const nl: Translation = { DUTY_CYCLE: 'Duty Cycle', UNIT: 'UoM', STARTVALUE: 'Startwaarde', - WARN_GPIO: 'Waarschuwing: let op met het koppelen van de juiste GPIO pin!', EDIT: 'Wijzigen', SENSOR: 'Sensor', TEMP_SENSOR: 'Temperatuur sensor', diff --git a/interface/src/i18n/no/index.ts b/interface/src/i18n/no/index.ts index 0c1e88133..a2fbbaa6c 100644 --- a/interface/src/i18n/no/index.ts +++ b/interface/src/i18n/no/index.ts @@ -60,7 +60,6 @@ const no: Translation = { DUTY_CYCLE: 'Duty Cycle', UNIT: 'UoM', STARTVALUE: 'Startverdi', - WARN_GPIO: 'Advarsel: vær forsiktig ved aktivering av GPIO!', EDIT: 'Endre', SENSOR: 'Sensor', TEMP_SENSOR: 'Temperatursensor', diff --git a/interface/src/i18n/pl/index.ts b/interface/src/i18n/pl/index.ts index bbc90b380..6dd874a32 100644 --- a/interface/src/i18n/pl/index.ts +++ b/interface/src/i18n/pl/index.ts @@ -60,7 +60,6 @@ const pl: BaseTranslation = { DUTY_CYCLE: 'Wypełnienie', UNIT: 'J.m.', STARTVALUE: 'Wartość początkowa', - WARN_GPIO: 'Uwaga! Zachowaj ostrożność przypisując GPIO do urządzenia!', EDIT: 'Edycja', SENSOR: '{{c|ustawienia c||ustawień c|}}zujnika', TEMP_SENSOR: 'czujnika temperatury', diff --git a/interface/src/i18n/sk/index.ts b/interface/src/i18n/sk/index.ts index aa22a83cb..52e6f9c75 100644 --- a/interface/src/i18n/sk/index.ts +++ b/interface/src/i18n/sk/index.ts @@ -60,7 +60,6 @@ const sk: Translation = { DUTY_CYCLE: 'Pracovný cyklus', UNIT: 'UoM', STARTVALUE: 'Počiatočná hodnota', - WARN_GPIO: 'Upozornenie: Buďte opatrní pri priraďovaní GPIO!', EDIT: 'Editovať', SENSOR: 'Snímač', TEMP_SENSOR: 'Snímač teploty', diff --git a/interface/src/i18n/sv/index.ts b/interface/src/i18n/sv/index.ts index 9a07450b8..644484269 100644 --- a/interface/src/i18n/sv/index.ts +++ b/interface/src/i18n/sv/index.ts @@ -60,7 +60,6 @@ const sv: Translation = { DUTY_CYCLE: 'Pulskvot', UNIT: 'Måttenhet', STARTVALUE: 'Startvärde', - WARN_GPIO: 'Varning: Var försiktig vid aktivering av GPIO!', EDIT: 'Ändra', SENSOR: 'Sensor', TEMP_SENSOR: 'Temperatursensor', diff --git a/interface/src/i18n/tr/index.ts b/interface/src/i18n/tr/index.ts index e212ea274..79824decd 100644 --- a/interface/src/i18n/tr/index.ts +++ b/interface/src/i18n/tr/index.ts @@ -60,7 +60,6 @@ const tr: Translation = { DUTY_CYCLE: 'Görev Çevrimi', UNIT: 'ÖB', STARTVALUE: 'Başlangıç değeri', - WARN_GPIO: 'Uyarı: bir GPIO atarken dikkatli olun!', EDIT: 'Değiştir', SENSOR: 'Sensör', TEMP_SENSOR: 'Sıcaklık Sensörü', diff --git a/interface/src/utils/binding.ts b/interface/src/utils/binding.ts index b250c04fe..0a4d272ba 100644 --- a/interface/src/utils/binding.ts +++ b/interface/src/utils/binding.ts @@ -46,15 +46,15 @@ type UpdateEntity = (state: (prevState: Readonly) => S) => void; */ export const updateValue = >(updateEntity: UpdateEntity) => - (event: React.ChangeEvent): void => { - const { name } = event.target; - const value = extractEventValue(event); + (event: React.ChangeEvent): void => { + const { name } = event.target; + const value = extractEventValue(event); - updateEntity((prevState) => ({ - ...prevState, - [name]: value - })); - }; + updateEntity((prevState) => ({ + ...prevState, + [name]: value + })); + }; /** * Creates an event handler that tracks dirty flags for modified fields. @@ -67,22 +67,22 @@ export const updateValueDirty = setDirtyFlags: React.Dispatch>, updateDataValue: (updater: (prevState: T) => T) => void ) => - (event: React.ChangeEvent): void => { - const { name } = event.target; - const updatedValue = extractEventValue(event); + (event: React.ChangeEvent): void => { + const { name } = event.target; + const updatedValue = extractEventValue(event); - updateDataValue((prevState) => ({ - ...prevState, - [name]: updatedValue - })); + updateDataValue((prevState) => ({ + ...prevState, + [name]: updatedValue + })); - const isDirty = origData[name] !== updatedValue; - const wasDirty = dirtyFlags.includes(name); + const isDirty = origData[name] !== updatedValue; + const wasDirty = dirtyFlags.includes(name); - // Only update dirty flags if the state changed - if (isDirty !== wasDirty) { - setDirtyFlags( - isDirty ? [...dirtyFlags, name] : dirtyFlags.filter((f) => f !== name) - ); - } - }; + // Only update dirty flags if the state changed + if (isDirty !== wasDirty) { + setDirtyFlags( + isDirty ? [...dirtyFlags, name] : dirtyFlags.filter((f) => f !== name) + ); + } + }; diff --git a/mock-api/restServer.ts b/mock-api/restServer.ts index eb8f18acb..360e36275 100644 --- a/mock-api/restServer.ts +++ b/mock-api/restServer.ts @@ -988,7 +988,7 @@ const emsesp_sensordata = { { id: 2, g: 37, - n: 'External switch', + n: 'External_switch', v: 13, u: 0, o: 17, @@ -999,8 +999,8 @@ const emsesp_sensordata = { }, { id: 3, - g: 39, - n: 'Pulse count', + g: 37, + n: 'Pulse_count', v: 144, u: 0, o: 0, @@ -1011,7 +1011,7 @@ const emsesp_sensordata = { }, { id: 4, - g: 40, + g: 23, n: 'Pressure', v: 16, u: 17, @@ -1046,7 +1046,8 @@ const emsesp_sensordata = { s: true } ], - analog_enabled: true + analog_enabled: true, + valid_gpio_list: [0, 2, 5, 12, 13, 15, 18, 19, 23, 25, 26, 27, 33, 37, 38] }; const activity = { diff --git a/project-words.txt b/project-words.txt index 536581dc1..250a373a7 100644 --- a/project-words.txt +++ b/project-words.txt @@ -1443,4 +1443,5 @@ constlow proplow chimneysweeper pumpopt -intergral \ No newline at end of file +intergral +vchip \ No newline at end of file diff --git a/scripts/build_interface.py b/scripts/build_interface.py index e72afd68d..069e8c774 100755 --- a/scripts/build_interface.py +++ b/scripts/build_interface.py @@ -10,11 +10,11 @@ def get_pnpm_executable(): """Get the appropriate pnpm executable for the current platform.""" # Try different pnpm executable names pnpm_names = ['pnpm', 'pnpm.cmd', 'pnpm.exe'] - + for name in pnpm_names: if shutil.which(name): return name - + # Fallback to pnpm if not found return 'pnpm' @@ -30,14 +30,14 @@ def run_command_in_directory(command, directory): capture_output=True, text=True ) - + if result.stdout: print(result.stdout) if result.stderr: print(result.stderr) - + return True - + except subprocess.CalledProcessError as e: print(f"Command failed: {command}") print(f"Error: {e}") @@ -54,36 +54,34 @@ def run_command_in_directory(command, directory): def buildWeb(): interface_dir = Path("interface") pnpm_exe = get_pnpm_executable() - + # Set CI environment variable to make pnpm use silent mode os.environ['CI'] = 'true' - + print("Building web interface...") - + # Check if interface directory exists if not interface_dir.exists(): print(f"Error: Interface directory '{interface_dir}' not found!") return False - + # Check if pnpm is available if not shutil.which(pnpm_exe): print(f"Error: '{pnpm_exe}' not found in PATH!") return False - + try: # Run pnpm commands in the interface directory commands = [ f"{pnpm_exe} install", - f"{pnpm_exe} typesafe-i18n", - f"{pnpm_exe} build", - f"{pnpm_exe} webUI" + f"{pnpm_exe} build_webUI" ] - + for command in commands: print(f"Running: {command}") if not run_command_in_directory(command, interface_dir): return False - + # Modify i18n-util.ts file i18n_file = interface_dir / "src" / "i18n" / "i18n-util.ts" if i18n_file.exists(): @@ -93,11 +91,12 @@ def buildWeb(): w.write(text) print("Setting WebUI locale to 'en'") else: - print(f"Warning: {i18n_file} not found, skipping locale modification") - + print( + f"Warning: {i18n_file} not found, skipping locale modification") + print("Web interface build completed successfully!") return True - + except Exception as e: print(f"Error building web interface: {e}") return False @@ -108,8 +107,9 @@ def build_webUI(*args, **kwargs): if not success: print("Web interface build failed!") env.Exit(1) - env.Exit(0) - + env.Exit(0) + + # Create custom target that only runs the script and then exits, without continuing with the pio workflow env.AddCustomTarget( name="build", @@ -119,4 +119,3 @@ env.AddCustomTarget( description="installs pnpm packages, updates libraries and builds web UI", always_build=True ) - diff --git a/scripts/update_all.sh b/scripts/update_all.sh index b414f59b3..428301dac 100644 --- a/scripts/update_all.sh +++ b/scripts/update_all.sh @@ -20,7 +20,7 @@ pnpm format cd .. cd interface -pnpm webUI +pnpm build_webUI cd .. npx cspell "**" diff --git a/src/ESP32React/ESP32React.h b/src/ESP32React/ESP32React.h index 875d4d03e..f5af50a8a 100644 --- a/src/ESP32React/ESP32React.h +++ b/src/ESP32React/ESP32React.h @@ -16,7 +16,6 @@ #include #include -#include #include #include diff --git a/src/core/analogsensor.cpp b/src/core/analogsensor.cpp index c385258aa..8345e6b2c 100644 --- a/src/core/analogsensor.cpp +++ b/src/core/analogsensor.cpp @@ -187,8 +187,8 @@ void AnalogSensor::reload(bool get_nvs) { for (auto & sensor : sensors_) { sensor.ha_registered = false; // force HA configs to be re-created - // first check if the GPIO is valid. If not, force set it to disabled - if (!System::is_valid_gpio(sensor.gpio())) { + // first check if the GPIO is valid. If not, force set the sensor to disabled, but don't remove it + if (!EMSESP::system_.is_valid_gpio(sensor.gpio())) { LOG_WARNING("Bad GPIO %d for Sensor %s. Disabling.", sensor.gpio(), sensor.name().c_str()); sensor.set_type(AnalogType::NOTUSED); // set disabled continue; // skip this loop pass @@ -542,7 +542,7 @@ bool AnalogSensor::update(uint8_t gpio, std::string & name, double offset, doubl // return false if it's an invalid GPIO, an error will show in WebUI // and reported as an error in the log - return System::is_valid_gpio(gpio); + return EMSESP::system_.is_valid_gpio(gpio); } // check to see if values have been updated diff --git a/src/core/emsesp.cpp b/src/core/emsesp.cpp index 8e4ddab5d..ad1540a98 100644 --- a/src/core/emsesp.cpp +++ b/src/core/emsesp.cpp @@ -255,7 +255,7 @@ void EMSESP::uart_init() { EMSuart::stop(); // don't start UART if we have invalid GPIOs - if (System::is_valid_gpio(rx_gpio) && System::is_valid_gpio(tx_gpio)) { + if (EMSESP::system_.is_valid_gpio(rx_gpio) && EMSESP::system_.is_valid_gpio(tx_gpio)) { EMSuart::start(tx_mode, rx_gpio, tx_gpio); // start UART } else { LOG_WARNING("Invalid UART Rx/Tx GPIOs. Check config."); diff --git a/src/core/system.cpp b/src/core/system.cpp index ac0ce94ef..0acbd660c 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -19,12 +19,13 @@ #include "system.h" #include "emsesp.h" // for send_raw_telegram() command -#include "shuntingYard.h" #ifndef EMSESP_STANDALONE #include "esp_ota_ops.h" #endif +#include + #include #if defined(EMSESP_TEST) @@ -448,31 +449,9 @@ void System::reload_settings() { }); } -// check for valid ESP32 pins. This is very dependent on which ESP32 board is being used. -// Typically you can't use 1, 6-11, 20, 24, 28-31 and 40+ -// we allow 0 as it has a special function on the NodeMCU apparently -// See https://diyprojects.io/esp32-how-to-use-gpio-digital-io-arduino-code/#.YFpVEq9KhjG -// and https://nodemcu.readthedocs.io/en/dev-esp32/modules/gpio/ -bool System::is_valid_gpio(uint8_t pin, bool has_psram) { -#if CONFIG_IDF_TARGET_ESP32 || defined(EMSESP_STANDALONE) - if ((pin == 1) || (pin >= 6 && pin <= 11) || (pin == 20) || (pin == 24) || (pin >= 28 && pin <= 31) || (pin > 40) - || ((EMSESP::system_.PSram() > 0 || has_psram) && pin >= 16 && pin <= 17)) { -#elif CONFIG_IDF_TARGET_ESP32S2 - if ((pin >= 19 && pin <= 20) || (pin >= 22 && pin <= 32) || (pin > 40)) { -#elif CONFIG_IDF_TARGET_ESP32C3 - // https://www.wemos.cc/en/latest/c3/c3_mini.html - if ((pin >= 11 && pin <= 19) || (pin > 21)) { -#elif CONFIG_IDF_TARGET_ESP32S3 - if ((pin >= 19 && pin <= 20) || (pin >= 22 && pin <= 37) || (pin >= 39 && pin <= 42) || (pin > 48)) { -#endif - return false; // bad pin - } - - // extra check for pins 21 and 22 (I2C) when ethernet is onboard - if ((EMSESP::system_.ethernet_connected() || EMSESP::system_.phy_type_ != PHY_type::PHY_TYPE_NONE) && (pin >= 21 && pin <= 22)) { - return false; // bad pin - } - return true; +// check for valid ESP32 pins +bool System::is_valid_gpio(uint8_t pin) { + return std::find(valid_gpio_list().begin(), valid_gpio_list().end(), pin) != valid_gpio_list().end(); } // Starts up the UART Serial bridge @@ -2320,4 +2299,97 @@ uint8_t System::systemStatus() { return systemStatus_; } +// takes a string range like "6-11, 1, 23, 24-48" which has optional ranges and single values and converts to a vector of ints +std::vector System::string_range_to_vector(const std::string & range) { + std::vector valid_gpios; + std::string::size_type pos = 0; + std::string::size_type prev = 0; + + auto process_part = [&valid_gpios](std::string part) { + // trim whitespace + part.erase(0, part.find_first_not_of(" \t")); + part.erase(part.find_last_not_of(" \t") + 1); + + // check if it's a range (contains '-') + std::string::size_type dash_pos = part.find('-'); + if (dash_pos != std::string::npos) { + // it's a range like "6-11" + int start = std::stoi(part.substr(0, dash_pos)); + int end = std::stoi(part.substr(dash_pos + 1)); + for (int i = start; i <= end; i++) { + valid_gpios.push_back(static_cast(i)); + } + } else { + valid_gpios.push_back(static_cast(std::stoi(part))); + } + }; + + while ((pos = range.find(',', prev)) != std::string::npos) { + process_part(range.substr(prev, pos - prev)); + prev = pos + 1; + } + + // handle the last part + process_part(range.substr(prev)); + + return valid_gpios; +} + +// return a list of valid GPIOs for the ESP32 board +// notes: +// - we allow 0, which is used sometimes to indicate a disabled pin +// - also allow input only pins are accepted (34-39) on some boards +// - and allow pins 33-38 for octal SPI for 32M vchip version on some boards +std::vector System::valid_gpio_list() { + // get free gpios based on board/platform type +#if CONFIG_IDF_TARGET_ESP32C3 + // https://www.wemos.cc/en/latest/c3/c3_mini.html + std::vector valid_gpios = string_range_to_vector("0-10"); +#elif CONFIG_IDF_TARGET_ESP32S2 + std::vector valid_gpios = string_range_to_vector("0-14, 19, 20, 21, 33-38, 45, 46"); +#elif CONFIG_IDF_TARGET_ESP32S3 + std::vector valid_gpios = string_range_to_vector("2, 4-14, 17, 18, 21, 33-38, 45, 46"); +#elif CONFIG_IDF_TARGET_ESP32 || defined(EMSESP_STANDALONE) + std::vector valid_gpios = string_range_to_vector("0, 2, 4, 5, 12-15, 18, 19, 23, 25-27, 32-39"); +#else + std::vector valid_gpios = {}; +#endif + +#if CONFIG_IDF_TARGET_ESP32 + // if psram is enabled remove pins 16 and 17 from the list + if (ESP.getPsramSize() > 0) { + valid_gpios.erase(std::remove(valid_gpios.begin(), valid_gpios.end(), 16), valid_gpios.end()); + valid_gpios.erase(std::remove(valid_gpios.begin(), valid_gpios.end(), 17), valid_gpios.end()); + } +#endif + + // if ethernet is enabled, remove pins 21 and 22 (I2C) + if ((EMSESP::system_.ethernet_connected() || EMSESP::system_.phy_type_ != PHY_type::PHY_TYPE_NONE)) { + valid_gpios.erase(std::remove(valid_gpios.begin(), valid_gpios.end(), 21), valid_gpios.end()); + valid_gpios.erase(std::remove(valid_gpios.begin(), valid_gpios.end(), 22), valid_gpios.end()); + } + + // filter out GPIOs already used in application settings + for (const auto & gpio : valid_gpios) { + if (gpio == EMSESP::system_.pbutton_gpio_ || gpio == EMSESP::system_.led_gpio_ || gpio == EMSESP::system_.dallas_gpio_ + || gpio == EMSESP::system_.rx_gpio_ || gpio == EMSESP::system_.tx_gpio_) { + valid_gpios.erase(std::remove(valid_gpios.begin(), valid_gpios.end(), gpio), valid_gpios.end()); + } + } + + // filter out GPIOs already used in analog sensors, if enabled + if (EMSESP::system_.analog_enabled_) { + for (const auto & sensor : EMSESP::analogsensor_.sensors()) { + if (std::find(valid_gpios.begin(), valid_gpios.end(), sensor.gpio()) != valid_gpios.end()) { + valid_gpios.erase(std::find(valid_gpios.begin(), valid_gpios.end(), sensor.gpio())); + } + } + } + + // sort the list of valid GPIOs + std::sort(valid_gpios.begin(), valid_gpios.end()); + + return valid_gpios; +} + } // namespace emsesp diff --git a/src/core/system.h b/src/core/system.h index 01bf97f88..fb53eb231 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -140,7 +140,7 @@ class System { static void extractSettings(const char * filename, const char * section, JsonObject output); static bool saveSettings(const char * filename, const char * section, JsonObject input); - static bool is_valid_gpio(uint8_t pin, bool has_psram = false); + bool is_valid_gpio(uint8_t pin); static bool load_board_profile(std::vector & data, const std::string & board_profile); static bool readCommand(const char * data); @@ -303,6 +303,7 @@ class System { uint32_t PSram() { return psram_; } + uint32_t appFree() { return appfree_; } @@ -336,6 +337,8 @@ class System { test_set_all_active_ = n; } + static std::vector valid_gpio_list(); + #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 float temperature() { return temperature_; @@ -381,6 +384,8 @@ class System { void led_monitor(); void system_check(); + static std::vector string_range_to_vector(const std::string & range); + int8_t wifi_quality(int8_t dBm); uint8_t healthcheck_ = HEALTHCHECK_NO_NETWORK | HEALTHCHECK_NO_BUS; // start with all flags set, no wifi and no ems bus connection diff --git a/src/devices/boiler.h b/src/devices/boiler.h index 55a6b6a00..38cd3fada 100644 --- a/src/devices/boiler.h +++ b/src/devices/boiler.h @@ -100,7 +100,7 @@ class Boiler : public EMSdevice { uint8_t wwAlternatingOper_; // alternating operation on/off uint8_t wwAltOpPrioHeat_; // alternating operation, prioritize heat time uint8_t wwAltOpPrioWw_; // alternating operation, prioritize dhw time - uint8_t wwPrio_; + uint8_t wwPrio_; // special function uint8_t forceHeatingOff_; diff --git a/src/devices/mixer.cpp b/src/devices/mixer.cpp index 64309d0ec..89dd295f9 100644 --- a/src/devices/mixer.cpp +++ b/src/devices/mixer.cpp @@ -294,7 +294,7 @@ bool Mixer::set_wwprio(const char * value, const int8_t id) { return false; } uint8_t hc = device_id() - 0x20; - write_command(0x2CD + hc, 3, b ? 0xFF: 0, 0x2CD + hc); + write_command(0x2CD + hc, 3, b ? 0xFF : 0, 0x2CD + hc); return true; } diff --git a/src/web/WebDataService.cpp b/src/web/WebDataService.cpp index dc5005bd6..4b78399f4 100644 --- a/src/web/WebDataService.cpp +++ b/src/web/WebDataService.cpp @@ -157,6 +157,11 @@ void WebDataService::sensor_data(AsyncWebServerRequest * request) { root["analog_enabled"] = EMSESP::analog_enabled(); root["platform"] = EMSESP_PLATFORM; + JsonArray valid_gpio_list = root["valid_gpio_list"].to(); + for (const auto & gpio : EMSESP::system_.valid_gpio_list()) { + valid_gpio_list.add(gpio); + } + response->setLength(); request->send(response); } diff --git a/src/web/WebSettingsService.cpp b/src/web/WebSettingsService.cpp index f83475a1a..46135d9b3 100644 --- a/src/web/WebSettingsService.cpp +++ b/src/web/WebSettingsService.cpp @@ -90,12 +90,6 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) { // load the version from the settings config. This can be blank and later used in System::check_upgrade() settings.version = root["version"] | EMSESP_DEFAULT_VERSION; -#ifndef EMSESP_STANDALONE - bool psram = ESP.getPsramSize() > 0; // System::PSram() is initialized later -#else - bool psram = false; -#endif - #if defined(EMSESP_DEBUG) EMSESP::logger().debug("NVS boot value=[%s], board profile=[%s], EMSESP_DEFAULT_BOARD_PROFILE=[%s]", EMSESP::nvs_.getString("boot").c_str(), @@ -143,8 +137,8 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) { #endif } // check valid pins in this board profile - if (!System::is_valid_gpio(data[0], psram) || !System::is_valid_gpio(data[1], psram) || !System::is_valid_gpio(data[2], psram) - || !System::is_valid_gpio(data[3], psram) || !System::is_valid_gpio(data[4], psram) || !System::is_valid_gpio(data[6], psram)) { + if (!EMSESP::system_.is_valid_gpio(data[0]) || !EMSESP::system_.is_valid_gpio(data[1]) || !EMSESP::system_.is_valid_gpio(data[2]) + || !EMSESP::system_.is_valid_gpio(data[3]) || !EMSESP::system_.is_valid_gpio(data[4]) || !EMSESP::system_.is_valid_gpio(data[6])) { settings.board_profile = "default"; // reset to factory default } } else { @@ -161,8 +155,8 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) { #if defined(EMSESP_STANDALONE) settings.board_profile = "S32"; #elif CONFIG_IDF_TARGET_ESP32 - // check for no PSRAM, could be a E32 or S32 - if (!psram) { + // check for no PSRAM, could be a E32 or S32? + if (!ESP.getPsramSize()) { #if ESP_ARDUINO_VERSION_MAJOR < 3 if (ETH.begin(1, 16, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN)) { #else