From 100f72b7e0f71fa642a19c3dd46e7465dec6877a Mon Sep 17 00:00:00 2001 From: hhvrc Date: Sun, 6 Oct 2024 22:08:17 +0200 Subject: [PATCH 1/4] Update local websocket Sec-WebSocket-Protocol value --- src/CaptivePortalInstance.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CaptivePortalInstance.cpp b/src/CaptivePortalInstance.cpp index 3019ce8e..876729b5 100644 --- a/src/CaptivePortalInstance.cpp +++ b/src/CaptivePortalInstance.cpp @@ -59,7 +59,7 @@ const char* _getPartitionHash() { CaptivePortalInstance::CaptivePortalInstance() : m_webServer(HTTP_PORT) - , m_socketServer(WEBSOCKET_PORT, "/ws", "json") + , m_socketServer(WEBSOCKET_PORT, "/ws", "flatbuffers") // Sec-WebSocket-Protocol = flatbuffers , m_socketDeFragger(std::bind(&CaptivePortalInstance::handleWebSocketEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)) , m_fileSystem() , m_dnsServer() From b486f919481ac6dcdc056fc7dca38eb78999d82f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 08:50:30 +0200 Subject: [PATCH 2/4] build(deps-dev): Bump the frontend group in /frontend with 3 updates (#292) Bumps the frontend group in /frontend with 3 updates: [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit), [eslint](https://github.com/eslint/eslint) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). Updates `@sveltejs/kit` from 2.6.1 to 2.6.2 - [Release notes](https://github.com/sveltejs/kit/releases) - [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md) - [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.6.2/packages/kit) Updates `eslint` from 9.11.1 to 9.12.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v9.11.1...v9.12.0) Updates `vitest` from 2.1.1 to 2.1.2 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v2.1.2/packages/vitest) --- updated-dependencies: - dependency-name: "@sveltejs/kit" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: frontend - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor dependency-group: frontend - dependency-name: vitest dependency-type: direct:development update-type: version-update:semver-patch dependency-group: frontend ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package-lock.json | 193 ++++++++++++++++++++----------------- frontend/package.json | 6 +- 2 files changed, 105 insertions(+), 94 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 07ff5fe4..81582012 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,13 +15,13 @@ "@skeletonlabs/skeleton": "2.10.2", "@skeletonlabs/tw-plugin": "0.4.0", "@sveltejs/adapter-static": "^3.0.5", - "@sveltejs/kit": "2.6.1", + "@sveltejs/kit": "2.6.2", "@sveltejs/vite-plugin-svelte": "^3.1.2", "@tailwindcss/forms": "0.5.9", "@tailwindcss/typography": "0.5.15", "@types/node": "22.7.4", "autoprefixer": "10.4.20", - "eslint": "^9.11.1", + "eslint": "^9.12.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-svelte": "2.44.1", "flatbuffers": "24.3.25", @@ -35,7 +35,7 @@ "typescript": "5.6.2", "vite": "^5.4.8", "vite-plugin-tailwind-purgecss": "^0.3.3", - "vitest": "2.1.1" + "vitest": "2.1.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -540,9 +540,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.11.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz", - "integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==", + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.12.0.tgz", + "integrity": "sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -591,6 +591,28 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" }, + "node_modules/@humanfs/core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", + "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", + "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.0", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -605,9 +627,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", - "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, "engines": { "node": ">=18.18" @@ -1014,14 +1036,14 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.6.1.tgz", - "integrity": "sha512-QFlch3GPGZYidYhdRAub0fONw8UTguPICFHUSPxNkA/jdlU1p6C6yqq19J1QWdxIHS2El/ycDCGrHb3EAiMNqg==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.6.2.tgz", + "integrity": "sha512-ruogrSPXjckn5poUiZU8VYNCSPHq66SFR1AATvOikQxtP6LNI4niAZVX/AWZRe/EPDG3oY2DNJ9c5z7u0t2NAQ==", "dev": true, "hasInstallScript": true, "dependencies": { "@types/cookie": "^0.6.0", - "cookie": "^0.6.0", + "cookie": "^0.7.0", "devalue": "^5.1.0", "esm-env": "^1.0.0", "import-meta-resolve": "^4.1.0", @@ -1139,13 +1161,13 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", - "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.2.tgz", + "integrity": "sha512-FEgtlN8mIUSEAAnlvn7mP8vzaWhEaAEvhSXCqrsijM7K6QqjB11qoRZYEd4AKSCDz8p0/+yH5LzhZ47qt+EyPg==", "dev": true, "dependencies": { - "@vitest/spy": "2.1.1", - "@vitest/utils": "2.1.1", + "@vitest/spy": "2.1.2", + "@vitest/utils": "2.1.2", "chai": "^5.1.1", "tinyrainbow": "^1.2.0" }, @@ -1154,9 +1176,9 @@ } }, "node_modules/@vitest/mocker": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", - "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.2.tgz", + "integrity": "sha512-ExElkCGMS13JAJy+812fw1aCv2QO/LBK6CyO4WOPAzLTmve50gydOlWhgdBJPx2ztbADUq3JVI0C5U+bShaeEA==", "dev": true, "dependencies": { "@vitest/spy": "^2.1.0-beta.1", @@ -1167,7 +1189,7 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/spy": "2.1.1", + "@vitest/spy": "2.1.2", "msw": "^2.3.5", "vite": "^5.0.0" }, @@ -1181,9 +1203,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz", - "integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.2.tgz", + "integrity": "sha512-FIoglbHrSUlOJPDGIrh2bjX1sNars5HbxlcsFKCtKzu4+5lpsRhOCVcuzp0fEhAGHkPZRIXVNzPcpSlkoZ3LuA==", "dev": true, "dependencies": { "tinyrainbow": "^1.2.0" @@ -1193,12 +1215,12 @@ } }, "node_modules/@vitest/runner": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", - "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.2.tgz", + "integrity": "sha512-UCsPtvluHO3u7jdoONGjOSil+uON5SSvU9buQh3lP7GgUXHp78guN1wRmZDX4wGK6J10f9NUtP6pO+SFquoMlw==", "dev": true, "dependencies": { - "@vitest/utils": "2.1.1", + "@vitest/utils": "2.1.2", "pathe": "^1.1.2" }, "funding": { @@ -1206,12 +1228,12 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", - "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.2.tgz", + "integrity": "sha512-xtAeNsZ++aRIYIUsek7VHzry/9AcxeULlegBvsdLncLmNCR6tR8SRjn8BbDP4naxtccvzTqZ+L1ltZlRCfBZFA==", "dev": true, "dependencies": { - "@vitest/pretty-format": "2.1.1", + "@vitest/pretty-format": "2.1.2", "magic-string": "^0.30.11", "pathe": "^1.1.2" }, @@ -1220,9 +1242,9 @@ } }, "node_modules/@vitest/spy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", - "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.2.tgz", + "integrity": "sha512-GSUi5zoy+abNRJwmFhBDC0yRuVUn8WMlQscvnbbXdKLXX9dE59YbfwXxuJ/mth6eeqIzofU8BB5XDo/Ns/qK2A==", "dev": true, "dependencies": { "tinyspy": "^3.0.0" @@ -1232,12 +1254,12 @@ } }, "node_modules/@vitest/utils": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", - "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.2.tgz", + "integrity": "sha512-zMO2KdYy6mx56btx9JvAqAZ6EyS3g49krMPPrgOp1yxGZiA93HumGk+bZ5jIZtOg5/VBYl5eBmGRQHqq4FG6uQ==", "dev": true, "dependencies": { - "@vitest/pretty-format": "2.1.1", + "@vitest/pretty-format": "2.1.2", "loupe": "^3.1.1", "tinyrainbow": "^1.2.0" }, @@ -1644,9 +1666,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "engines": { "node": ">= 0.6" @@ -1876,9 +1898,9 @@ } }, "node_modules/eslint": { - "version": "9.11.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz", - "integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==", + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz", + "integrity": "sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -1886,11 +1908,11 @@ "@eslint/config-array": "^0.18.0", "@eslint/core": "^0.6.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.11.1", + "@eslint/js": "9.12.0", "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.0", - "@nodelib/fs.walk": "^1.2.8", + "@humanwhocodes/retry": "^0.3.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -1898,9 +1920,9 @@ "cross-spawn": "^7.0.2", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.0.2", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.1.0", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -1910,13 +1932,11 @@ "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { @@ -2046,9 +2066,9 @@ "dev": true }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", - "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -2062,9 +2082,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2074,14 +2094,14 @@ } }, "node_modules/eslint/node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", "dev": true, "dependencies": { "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" + "eslint-visitor-keys": "^4.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2531,15 +2551,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-reference": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", @@ -4330,9 +4341,9 @@ } }, "node_modules/vite-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", - "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.2.tgz", + "integrity": "sha512-HPcGNN5g/7I2OtPjLqgOtCRu/qhVvBxTUD3qzitmL0SrG1cWFzxzhMDWussxSbrRYWqnKf8P2jiNhPMSN+ymsQ==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -4408,18 +4419,18 @@ } }, "node_modules/vitest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", - "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", - "dev": true, - "dependencies": { - "@vitest/expect": "2.1.1", - "@vitest/mocker": "2.1.1", - "@vitest/pretty-format": "^2.1.1", - "@vitest/runner": "2.1.1", - "@vitest/snapshot": "2.1.1", - "@vitest/spy": "2.1.1", - "@vitest/utils": "2.1.1", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.2.tgz", + "integrity": "sha512-veNjLizOMkRrJ6xxb+pvxN6/QAWg95mzcRjtmkepXdN87FNfxAss9RKe2far/G9cQpipfgP2taqg0KiWsquj8A==", + "dev": true, + "dependencies": { + "@vitest/expect": "2.1.2", + "@vitest/mocker": "2.1.2", + "@vitest/pretty-format": "^2.1.2", + "@vitest/runner": "2.1.2", + "@vitest/snapshot": "2.1.2", + "@vitest/spy": "2.1.2", + "@vitest/utils": "2.1.2", "chai": "^5.1.1", "debug": "^4.3.6", "magic-string": "^0.30.11", @@ -4430,7 +4441,7 @@ "tinypool": "^1.0.0", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.1.1", + "vite-node": "2.1.2", "why-is-node-running": "^2.3.0" }, "bin": { @@ -4445,8 +4456,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.1", - "@vitest/ui": "2.1.1", + "@vitest/browser": "2.1.2", + "@vitest/ui": "2.1.2", "happy-dom": "*", "jsdom": "*" }, diff --git a/frontend/package.json b/frontend/package.json index 0db0c6e4..892d5c1f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,13 +19,13 @@ "@skeletonlabs/skeleton": "2.10.2", "@skeletonlabs/tw-plugin": "0.4.0", "@sveltejs/adapter-static": "^3.0.5", - "@sveltejs/kit": "2.6.1", + "@sveltejs/kit": "2.6.2", "@sveltejs/vite-plugin-svelte": "^3.1.2", "@tailwindcss/forms": "0.5.9", "@tailwindcss/typography": "0.5.15", "@types/node": "22.7.4", "autoprefixer": "10.4.20", - "eslint": "^9.11.1", + "eslint": "^9.12.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-svelte": "2.44.1", "flatbuffers": "24.3.25", @@ -39,7 +39,7 @@ "typescript": "5.6.2", "vite": "^5.4.8", "vite-plugin-tailwind-purgecss": "^0.3.3", - "vitest": "2.1.1" + "vitest": "2.1.2" }, "type": "module", "dependencies": { From 2ad3e327714973b629201cc87ae2bf3c4f998447 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:20:51 +0200 Subject: [PATCH 3/4] build(deps): Bump @actions/core in /.github/scripts in the ci-cd group (#293) Bumps the ci-cd group in /.github/scripts with 1 update: [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core). Updates `@actions/core` from 1.10.1 to 1.11.1 - [Changelog](https://github.com/actions/toolkit/blob/main/packages/core/RELEASES.md) - [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/core) --- updated-dependencies: - dependency-name: "@actions/core" dependency-type: direct:production update-type: version-update:semver-minor dependency-group: ci-cd ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: HeavenVR --- .github/scripts/package-lock.json | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/.github/scripts/package-lock.json b/.github/scripts/package-lock.json index b2264153..8380d45f 100644 --- a/.github/scripts/package-lock.json +++ b/.github/scripts/package-lock.json @@ -16,12 +16,20 @@ } }, "node_modules/@actions/core": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", - "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", "dependencies": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dependencies": { + "@actions/io": "^1.0.1" } }, "node_modules/@actions/github": { @@ -44,6 +52,11 @@ "undici": "^5.25.4" } }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" + }, "node_modules/@fastify/busboy": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", @@ -231,14 +244,6 @@ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", From a79387b133eceb8ea5ac3bcd0f1d59054f257c07 Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Mon, 7 Oct 2024 23:04:33 +0200 Subject: [PATCH 4/4] Clean up SerilaiInputHandler (#281) * Clean up SerialInputHandler * Pass isAutomated to command handlers * Ignore non-printable characters in command * Implement custom SerialBuffer instead of std::string * Update formatting --- .clang-format | 6 +- include/serial/SerialInputHandler.h | 3 + .../serial/command_handlers/CommandEntry.h | 55 + include/serial/command_handlers/common.h | 14 + include/serial/command_handlers/index.h | 47 + src/main.cpp | 2 +- src/serial/SerialInputHandler.cpp | 1088 ++++------------- src/serial/command_handlers/CommandEntry.cpp | 35 + src/serial/command_handlers/authtoken.cpp | 38 + src/serial/command_handlers/domain.cpp | 72 ++ src/serial/command_handlers/echo.cpp | 41 + src/serial/command_handlers/estoppin.cpp | 50 + src/serial/command_handlers/factoryreset.cpp | 20 + src/serial/command_handlers/hostname.cpp | 39 + src/serial/command_handlers/jsonconfig.cpp | 33 + src/serial/command_handlers/keepalive.cpp | 45 + src/serial/command_handlers/lcgoverride.cpp | 104 ++ src/serial/command_handlers/networks.cpp | 110 ++ src/serial/command_handlers/rawconfig.cpp | 53 + src/serial/command_handlers/restart.cpp | 18 + src/serial/command_handlers/rftransmit.cpp | 70 ++ src/serial/command_handlers/rftxpin.cpp | 56 + src/serial/command_handlers/sysinfo.cpp | 44 + src/serial/command_handlers/validgpios.cpp | 38 + src/serial/command_handlers/version.cpp | 20 + src/serialization/JsonAPI.cpp | 4 +- 26 files changed, 1272 insertions(+), 833 deletions(-) create mode 100644 include/serial/command_handlers/CommandEntry.h create mode 100644 include/serial/command_handlers/common.h create mode 100644 include/serial/command_handlers/index.h create mode 100644 src/serial/command_handlers/CommandEntry.cpp create mode 100644 src/serial/command_handlers/authtoken.cpp create mode 100644 src/serial/command_handlers/domain.cpp create mode 100644 src/serial/command_handlers/echo.cpp create mode 100644 src/serial/command_handlers/estoppin.cpp create mode 100644 src/serial/command_handlers/factoryreset.cpp create mode 100644 src/serial/command_handlers/hostname.cpp create mode 100644 src/serial/command_handlers/jsonconfig.cpp create mode 100644 src/serial/command_handlers/keepalive.cpp create mode 100644 src/serial/command_handlers/lcgoverride.cpp create mode 100644 src/serial/command_handlers/networks.cpp create mode 100644 src/serial/command_handlers/rawconfig.cpp create mode 100644 src/serial/command_handlers/restart.cpp create mode 100644 src/serial/command_handlers/rftransmit.cpp create mode 100644 src/serial/command_handlers/rftxpin.cpp create mode 100644 src/serial/command_handlers/sysinfo.cpp create mode 100644 src/serial/command_handlers/validgpios.cpp create mode 100644 src/serial/command_handlers/version.cpp diff --git a/.clang-format b/.clang-format index 91f7bc2d..22be87b5 100644 --- a/.clang-format +++ b/.clang-format @@ -71,7 +71,7 @@ AllowAllParametersOfDeclarationOnNextLine: false AllowShortBlocksOnASingleLine: Always AllowShortCaseLabelsOnASingleLine: false AllowShortEnumsOnASingleLine: false -AllowShortFunctionsOnASingleLine: Inline +AllowShortFunctionsOnASingleLine: InlineOnly AllowShortIfStatementsOnASingleLine: WithoutElse AllowShortLambdasOnASingleLine: Inline AllowShortLoopsOnASingleLine: true @@ -97,7 +97,7 @@ BraceWrapping: BeforeLambdaBody: false BeforeWhile: false IndentBraces: false - SplitEmptyFunction: false + SplitEmptyFunction: true SplitEmptyRecord: true SplitEmptyNamespace: true BreakAfterAttributes: Never @@ -137,7 +137,7 @@ LambdaBodyIndentation: OuterScope MaxEmptyLinesToKeep: 1 NamespaceIndentation: All PPIndentWidth: -1 -PackConstructorInitializers: NextLine +PackConstructorInitializers: Never PointerAlignment: Left ReferenceAlignment: Left diff --git a/include/serial/SerialInputHandler.h b/include/serial/SerialInputHandler.h index 350ac8de..4587e48a 100644 --- a/include/serial/SerialInputHandler.h +++ b/include/serial/SerialInputHandler.h @@ -6,6 +6,9 @@ namespace OpenShock::SerialInputHandler { [[nodiscard]] bool Init(); void Update(); + bool SerialEchoEnabled(); + void SetSerialEchoEnabled(bool enabled); + void PrintWelcomeHeader(); void PrintVersionInfo(); } // namespace OpenShock::SerialInputHandler diff --git a/include/serial/command_handlers/CommandEntry.h b/include/serial/command_handlers/CommandEntry.h new file mode 100644 index 00000000..ced779ed --- /dev/null +++ b/include/serial/command_handlers/CommandEntry.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include + +namespace OpenShock::Serial { + typedef void (*CommandHandler)(std::string_view arg, bool isAutomated); + + class CommandArgument { + public: + std::string_view name; + std::string_view constraint; + std::string_view exampleValue; + std::vector constraintExtensions; + }; + + class CommandEntry { + public: + CommandEntry(std::string_view description, CommandHandler commandHandler); + CommandEntry(std::string_view name, std::string_view description, CommandHandler commandHandler); + + inline std::string_view name() const { return m_name; } + inline std::string_view description() const { return m_description; } + inline const std::vector& arguments() const { return m_arguments; } + inline const CommandHandler commandHandler() const { return m_commandHandler; } + + CommandArgument& addArgument(std::string_view name, std::string_view constraint, std::string_view exampleValue, std::vector constraintExtensions = {}); + + private: + std::string_view m_name; + std::string_view m_description; + std::vector m_arguments; + CommandHandler m_commandHandler; + }; + + class CommandGroup { + public: + CommandGroup() = default; + CommandGroup(std::string_view name); + CommandGroup(CommandGroup&& other) = default; + CommandGroup(const CommandGroup& other) = default; + CommandGroup& operator=(CommandGroup&& other) = default; + CommandGroup& operator=(const CommandGroup& other) = default; + + inline std::string_view name() const { return m_name; } + inline const std::vector& commands() const { return m_commands; } + + CommandEntry& addCommand(std::string_view description, CommandHandler commandHandler); + CommandEntry& addCommand(std::string_view name, std::string_view description, CommandHandler commandHandler); + + private: + std::string_view m_name; + std::vector m_commands; + }; +} // namespace OpenShock::Serial diff --git a/include/serial/command_handlers/common.h b/include/serial/command_handlers/common.h new file mode 100644 index 00000000..1d0937e0 --- /dev/null +++ b/include/serial/command_handlers/common.h @@ -0,0 +1,14 @@ +#pragma once + +#include "serial/command_handlers/index.h" + +#include "Logging.h" + +#include + +#define SERPR_SYS(format, ...) ::Serial.printf("$SYS$|" format "\n", ##__VA_ARGS__) +#define SERPR_RESPONSE(format, ...) SERPR_SYS("Response|" format, ##__VA_ARGS__) +#define SERPR_SUCCESS(format, ...) SERPR_SYS("Success|" format, ##__VA_ARGS__) +#define SERPR_ERROR(format, ...) SERPR_SYS("Error|" format, ##__VA_ARGS__) + +using namespace std::string_view_literals; diff --git a/include/serial/command_handlers/index.h b/include/serial/command_handlers/index.h new file mode 100644 index 00000000..42b35c41 --- /dev/null +++ b/include/serial/command_handlers/index.h @@ -0,0 +1,47 @@ +#pragma once + +#include "serial/command_handlers/CommandEntry.h" + +#include + +namespace OpenShock::Serial::CommandHandlers { + OpenShock::Serial::CommandGroup VersionHandler(); + OpenShock::Serial::CommandGroup RestartHandler(); + OpenShock::Serial::CommandGroup SysInfoHandler(); + OpenShock::Serial::CommandGroup EchoHandler(); + OpenShock::Serial::CommandGroup ValidGpiosHandler(); + OpenShock::Serial::CommandGroup RfTxPinHandler(); + OpenShock::Serial::CommandGroup ESStopPinHandler(); + OpenShock::Serial::CommandGroup DomainHandler(); + OpenShock::Serial::CommandGroup AuthTokenHandler(); + OpenShock::Serial::CommandGroup LcgOverrideHandler(); + OpenShock::Serial::CommandGroup HostnameHandler(); + OpenShock::Serial::CommandGroup NetworksHandler(); + OpenShock::Serial::CommandGroup KeepAliveHandler(); + OpenShock::Serial::CommandGroup JsonConfigHandler(); + OpenShock::Serial::CommandGroup RawConfigHandler(); + OpenShock::Serial::CommandGroup RfTransmitHandler(); + OpenShock::Serial::CommandGroup FactoryResetHandler(); + + inline std::vector AllCommandHandlers() { + return { + VersionHandler(), + RestartHandler(), + SysInfoHandler(), + EchoHandler(), + ValidGpiosHandler(), + RfTxPinHandler(), + ESStopPinHandler(), + DomainHandler(), + AuthTokenHandler(), + LcgOverrideHandler(), + HostnameHandler(), + NetworksHandler(), + KeepAliveHandler(), + JsonConfigHandler(), + RawConfigHandler(), + RfTransmitHandler(), + FactoryResetHandler(), + }; + } +} // namespace OpenShock::Serial::CommandHandlers diff --git a/src/main.cpp b/src/main.cpp index f3372643..ec3f792d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -83,7 +83,7 @@ void appSetup() { // Arduino setup function void setup() { - Serial.begin(115'200); + ::Serial.begin(115'200); OpenShock::Config::Init(); OpenShock::OtaUpdateManager::Init(); diff --git a/src/serial/SerialInputHandler.cpp b/src/serial/SerialInputHandler.cpp index 46f1f8c5..f523a5a7 100644 --- a/src/serial/SerialInputHandler.cpp +++ b/src/serial/SerialInputHandler.cpp @@ -6,11 +6,14 @@ const char* const TAG = "SerialInputHandler"; #include "CommandHandler.h" #include "config/Config.h" #include "config/SerialInputConfig.h" +#include "Convert.h" #include "EStopManager.h" #include "FormatHelpers.h" #include "http/HTTPRequestManager.h" -#include "Convert.h" #include "Logging.h" +#include "serial/command_handlers/CommandEntry.h" +#include "serial/command_handlers/common.h" +#include "serial/command_handlers/index.h" #include "serialization/JsonAPI.h" #include "serialization/JsonSerial.h" #include "Time.h" @@ -18,6 +21,8 @@ const char* const TAG = "SerialInputHandler"; #include "util/StringUtils.h" #include "wifi/WiFiManager.h" +#include + #include #include @@ -50,864 +55,289 @@ namespace std { using namespace std::string_view_literals; -#define SERPR_SYS(format, ...) Serial.printf("$SYS$|" format "\n", ##__VA_ARGS__) -#define SERPR_RESPONSE(format, ...) SERPR_SYS("Response|" format, ##__VA_ARGS__) -#define SERPR_SUCCESS(format, ...) SERPR_SYS("Success|" format, ##__VA_ARGS__) -#define SERPR_ERROR(format, ...) SERPR_SYS("Error|" format, ##__VA_ARGS__) - using namespace OpenShock; const int64_t PASTE_INTERVAL_THRESHOLD_MS = 20; const std::size_t SERIAL_BUFFER_CLEAR_THRESHOLD = 512; -struct SerialCmdHandler { - std::string_view cmd; - const char* helpResponse; - void (*commandHandler)(std::string_view, bool); -}; - static bool s_echoEnabled = true; -static std::unordered_map s_commandHandlers; - -/// @brief Tries to parse a boolean from a string (case-insensitive) -/// @param str Input string -/// @param strLen Length of input string -/// @param out Output boolean -/// @return True if the argument is a boolean, false otherwise -bool _tryParseBool(std::string_view str, bool& out) { - return OpenShock::Convert::ToBool(OpenShock::StringTrim(str), out); -} - -void _handleVersionCommand(std::string_view arg, bool isAutomated) { - (void)arg; - - Serial.print("\n"); - SerialInputHandler::PrintVersionInfo(); -} - -void _handleRestartCommand(std::string_view arg, bool isAutomated) { - (void)arg; - - Serial.println("Restarting ESP..."); - ESP.restart(); -} - -void _handleFactoryResetCommand(std::string_view arg, bool isAutomated) { - (void)arg; - - Serial.println("Resetting to factory defaults..."); - Config::FactoryReset(); - Serial.println("Restarting..."); - ESP.restart(); -} - -void _handleRfTxPinCommand(std::string_view arg, bool isAutomated) { - if (arg.empty()) { - uint8_t txPin; - if (!Config::GetRFConfigTxPin(txPin)) { - SERPR_ERROR("Failed to get RF TX pin from config"); - return; +static std::vector s_commandGroups; +static std::unordered_map s_commandHandlers; + +void _printCompleteHelp() { + std::size_t commandCount = 0; + std::size_t longestCommand = 0; + std::size_t longestArgument = 0; + std::size_t descriptionSize = 0; + for (const auto& group : s_commandGroups) { + longestCommand = std::max(longestCommand, group.name().size()); + for (const auto& command : group.commands()) { + commandCount++; + + std::size_t argumentSize = 0; + if (command.name().size() > 0) { + argumentSize += command.name().size() + 1; // +1 for space + } + for (const auto& arg : command.arguments()) { + argumentSize += arg.name.size() + 3; // +1 for space, +2 for <> + } + longestArgument = std::max(longestArgument, argumentSize); + descriptionSize += command.description().size(); } - - // Get rmt pin - SERPR_RESPONSE("RmtPin|%u", txPin); - return; - } - - uint8_t pin; - if (!OpenShock::Convert::ToUint8(arg, pin)) { - SERPR_ERROR("Invalid argument (number invalid or out of range)"); } - OpenShock::SetGPIOResultCode result = OpenShock::CommandHandler::SetRfTxPin(static_cast(pin)); - - switch (result) { - case OpenShock::SetGPIOResultCode::InvalidPin: - SERPR_ERROR("Invalid argument (invalid pin)"); - break; - - case OpenShock::SetGPIOResultCode::InternalError: - SERPR_ERROR("Internal error while setting RF TX pin"); - break; - - case OpenShock::SetGPIOResultCode::Success: - SERPR_SUCCESS("Saved config"); - break; - - default: - SERPR_ERROR("Unknown error while setting RF TX pin"); - break; - } -} + std::size_t paddedLength = longestCommand + 1 + longestArgument + 1; // +1 for space, +1 for newline -void _handleEStopPinCommand(std::string_view arg, bool isAutomated) { - if (arg.empty()) { - gpio_num_t estopPin; - if (!Config::GetEStopGpioPin(estopPin)) { - SERPR_ERROR("Failed to get EStop pin from config"); - return; - } + std::string buffer; + buffer.reserve((paddedLength * commandCount) + descriptionSize); // Approximate size - // Get EStop pin - SERPR_RESPONSE("EStopPin|%hhi", static_cast(estopPin)); - return; - } + for (const auto& group : s_commandGroups) { + for (const auto& command : group.commands()) { + buffer.append(group.name()); + buffer.append((longestCommand - group.name().size()) + 1, ' '); - uint8_t pin; - if (!OpenShock::Convert::ToUint8(arg, pin)) { - SERPR_ERROR("Invalid argument (number invalid or out of range)"); - return; - } + std::size_t startSize = buffer.size(); - gpio_num_t estopPin = static_cast(pin); + if (command.name().size() > 0) { + buffer.append(command.name()); + buffer.push_back(' '); + } - if (!OpenShock::EStopManager::SetEStopPin(estopPin)) { - SERPR_ERROR("Failed to set EStop pin"); - return; - } + for (const auto& arg : command.arguments()) { + buffer.push_back('<'); + buffer.append(arg.name); + buffer.push_back('>'); + buffer.push_back(' '); + } - if (!Config::SetEStopGpioPin(estopPin)) { - SERPR_ERROR("Failed to save config"); - return; - } + buffer.append(longestArgument - (buffer.size() - startSize), ' '); - SERPR_SUCCESS("Saved config"); -} + buffer.append(command.description()); -void _handleDomainCommand(std::string_view arg, bool isAutomated) { - if (arg.empty()) { - std::string domain; - if (!Config::GetBackendDomain(domain)) { - SERPR_ERROR("Failed to get domain from config"); - return; + buffer.push_back('\n'); } - - // Get domain - SERPR_RESPONSE("Domain|%s", domain.c_str()); - return; - } - - // Check if the domain is too long - // TODO: Remove magic number - if (arg.length() + 40 >= OPENSHOCK_URI_BUFFER_SIZE) { - SERPR_ERROR("Domain name too long, please try increasing the \"OPENSHOCK_URI_BUFFER_SIZE\" constant in source code"); - return; - } - - char uri[OPENSHOCK_URI_BUFFER_SIZE]; - sprintf(uri, "https://%.*s/1", arg.length(), arg.data()); - - auto resp = HTTP::GetJSON( - uri, - { - {"Accept", "application/json"} - }, - Serialization::JsonAPI::ParseBackendVersionJsonResponse, - {200} - ); - - if (resp.result != HTTP::RequestResult::Success) { - SERPR_ERROR("Tried to connect to \"%.*s\", but failed with status [%d], refusing to save domain to config", arg.length(), arg.data(), resp.code); - return; - } - - OS_LOGI(TAG, "Successfully connected to \"%.*s\", version: %s, commit: %s, current time: %s", arg.length(), arg.data(), resp.data.version.c_str(), resp.data.commit.c_str(), resp.data.currentTime.c_str()); - - bool result = OpenShock::Config::SetBackendDomain(arg); - - if (!result) { - SERPR_ERROR("Failed to save config"); - return; } - SERPR_SUCCESS("Saved config, restarting..."); - - // Restart to use the new domain - ESP.restart(); + SerialInputHandler::PrintWelcomeHeader(); + ::Serial.print(buffer.data()); } -void _handleAuthtokenCommand(std::string_view arg, bool isAutomated) { - if (arg.empty()) { - std::string authToken; - if (!Config::GetBackendAuthToken(authToken)) { - SERPR_ERROR("Failed to get auth token from config"); - return; - } +void _printCommandHelp(Serial::CommandGroup& group) { + std::size_t size = 0; + for (const auto& command : group.commands()) { + size++; // +1 for newline + size += group.name().size(); + size++; // +1 for space - // Get auth token - SERPR_RESPONSE("AuthToken|%s", authToken.c_str()); - return; - } - - bool result = OpenShock::Config::SetBackendAuthToken(arg); - - if (result) { - SERPR_SUCCESS("Saved config"); - } else { - SERPR_ERROR("Failed to save config"); - } -} - -void _handleLcgOverrideCommand(std::string_view arg, bool isAutomated) { - if (arg.empty()) { - std::string lcgOverride; - if (!Config::GetBackendLCGOverride(lcgOverride)) { - SERPR_ERROR("Failed to get LCG override from config"); - return; + if (command.name().size() > 0) { + size += command.name().size() + 1; // +1 for space } - // Get LCG override - SERPR_RESPONSE("LcgOverride|%s", lcgOverride.c_str()); - return; - } - - if (OpenShock::StringStartsWith(arg, "clear"sv)) { - if (arg.size() != 5) { - SERPR_ERROR("Invalid command (clear command should not have any arguments)"); - return; + for (const auto& arg : command.arguments()) { + size += arg.name.size() + 3; // +1 for space, +2 for <> } - bool result = OpenShock::Config::SetBackendLCGOverride(std::string()); - if (result) { - SERPR_SUCCESS("Cleared LCG override"); - } else { - SERPR_ERROR("Failed to clear LCG override"); - } - return; - } + size++; // +1 for newline - if (OpenShock::StringStartsWith(arg, "set "sv)) { - if (arg.size() <= 4) { - SERPR_ERROR("Invalid command (set command should have an argument)"); - return; + if (command.description().size() > 0) { + size = command.description().size() + 3; // +2 for indent, +1 for newline } - std::string_view domain = arg.substr(4); - - if (domain.size() + 40 >= OPENSHOCK_URI_BUFFER_SIZE) { - SERPR_ERROR("Domain name too long, please try increasing the \"OPENSHOCK_URI_BUFFER_SIZE\" constant in source code"); - return; + if (command.arguments().size() > 0) { + size += 13; // +13 for " Arguments:\n" + for (const auto& arg : command.arguments()) { + size += arg.name.size() + 7; // +4 for indent, +2 for <>, +1 for space + size += arg.constraint.size(); + if (arg.constraintExtensions.size() > 0) { + size += 2; // +1 for ':', +1 for newline + for (const auto& ext : arg.constraintExtensions) { + size += ext.size() + 7; // +1 for newline, +6 for indent + } + } else { + size++; // +1 for newline + } + } } - char uri[OPENSHOCK_URI_BUFFER_SIZE]; - sprintf(uri, "https://%.*s/1", static_cast(domain.size()), domain.data()); - - auto resp = HTTP::GetJSON( - uri, - { - {"Accept", "application/json"} - }, - Serialization::JsonAPI::ParseLcgInstanceDetailsJsonResponse, - {200} - ); + size += 11; // +11 for " Example:\n" + size += group.name().size() + 1; // +1 for space - if (resp.result != HTTP::RequestResult::Success) { - SERPR_ERROR("Tried to connect to \"%.*s\", but failed with status [%d], refusing to save domain to config", domain.size(), domain.data(), resp.code); - return; - } - - OS_LOGI( - TAG, - "Successfully connected to \"%.*s\", name: %s, version: %s, current time: %s, country code: %s, FQDN: %s", - domain.size(), - domain.data(), - resp.data.name.c_str(), - resp.data.version.c_str(), - resp.data.currentTime.c_str(), - resp.data.countryCode.c_str(), - resp.data.fqdn.c_str() - ); - - bool result = OpenShock::Config::SetBackendLCGOverride(domain); - - if (result) { - SERPR_SUCCESS("Saved config"); - } else { - SERPR_ERROR("Failed to save config"); + if (command.name().size() > 0) { + size += command.name().size() + 1; // +1 for space } - return; - } - - SERPR_ERROR("Invalid subcommand"); -} -void _handleHostnameCommand(std::string_view arg, bool isAutomated) { - if (arg.empty()) { - std::string hostname; - if (!Config::GetWiFiHostname(hostname)) { - SERPR_ERROR("Failed to get hostname from config"); - return; + for (const auto& arg : command.arguments()) { + size += arg.exampleValue.size() + 1; // +1 for space } - // Get hostname - SERPR_RESPONSE("Hostname|%s", hostname.c_str()); - return; + size++; // +1 for newline } - bool result = OpenShock::Config::SetWiFiHostname(arg); - - if (result) { - SERPR_SUCCESS("Saved config, restarting..."); + size++; // +1 for newline - ESP.restart(); - } else { - SERPR_ERROR("Failed to save config"); - } -} - -void _handleNetworksCommand(std::string_view arg, bool isAutomated) { - cJSON* root; + std::string buffer; + buffer.reserve(size); // TODO: Should be exact size, is 20 bytes off, figure out why - if (arg.empty()) { - root = cJSON_CreateArray(); - if (root == nullptr) { - SERPR_ERROR("Failed to create JSON array"); - return; - } + for (const auto& command : group.commands()) { + buffer.push_back('\n'); + buffer.append(group.name()); + buffer.push_back(' '); - if (!Config::GetWiFiCredentials(root, true)) { - SERPR_ERROR("Failed to get WiFi credentials from config"); - return; + if (command.name().size() > 0) { + buffer.append(command.name()); + buffer.push_back(' '); } - char* out = cJSON_PrintUnformatted(root); - if (out == nullptr) { - SERPR_ERROR("Failed to print JSON"); - return; + for (const auto& arg : command.arguments()) { + buffer.push_back('<'); + buffer.append(arg.name); + buffer.push_back('>'); + buffer.push_back(' '); } - SERPR_RESPONSE("Networks|%s", out); + buffer.push_back('\n'); - cJSON_free(out); - return; - } - - root = cJSON_ParseWithLength(arg.data(), arg.length()); - if (root == nullptr) { - SERPR_ERROR("Failed to parse JSON: %s", cJSON_GetErrorPtr()); - return; - } - - if (cJSON_IsArray(root) == 0) { - SERPR_ERROR("Invalid argument (not an array)"); - return; - } - - std::vector creds; - - uint8_t id = 1; - cJSON* network = nullptr; - cJSON_ArrayForEach(network, root) { - Config::WiFiCredentials cred; - - if (!cred.FromJSON(network)) { - SERPR_ERROR("Failed to parse network"); - return; + if (command.description().size() > 0) { + buffer.append(2, ' '); + buffer.append(command.description()); + buffer.push_back('\n'); } - if (cred.id == 0) { - cred.id = id++; + if (command.arguments().size() > 0) { + buffer.append(" Arguments:\n"sv); + for (const auto& arg : command.arguments()) { + buffer.append(4, ' '); + buffer.push_back('<'); + buffer.append(arg.name); + buffer.push_back('>'); + buffer.push_back(' '); + buffer.append(arg.constraint); + if (arg.constraintExtensions.size() > 0) { + buffer.push_back(':'); + buffer.push_back('\n'); + for (const auto& ext : arg.constraintExtensions) { + buffer.append(6, ' '); + buffer.append(ext); + buffer.push_back('\n'); + } + } else { + buffer.push_back('\n'); + } + } } - OS_LOGI(TAG, "Adding network \"%s\" to config, id=%u", cred.ssid.c_str(), cred.id); - - creds.emplace_back(std::move(cred)); - } - - if (!OpenShock::Config::SetWiFiCredentials(creds)) { - SERPR_ERROR("Failed to save config"); - return; - } - - SERPR_SUCCESS("Saved config"); - - OpenShock::WiFiManager::RefreshNetworkCredentials(); -} - -void _handleKeepAliveCommand(std::string_view arg, bool isAutomated) { - bool keepAliveEnabled; + buffer.append(" Example:\n"sv); + buffer.append(group.name()); + buffer.push_back(' '); - if (arg.empty()) { - // Get keep alive status - if (!Config::GetRFConfigKeepAliveEnabled(keepAliveEnabled)) { - SERPR_ERROR("Failed to get keep-alive status from config"); - return; + if (command.name().size() > 0) { + buffer.append(command.name()); + buffer.push_back(' '); } - SERPR_RESPONSE("KeepAlive|%s", keepAliveEnabled ? "true" : "false"); - return; - } + for (const auto& arg : command.arguments()) { + buffer.append(arg.exampleValue); + buffer.push_back(' '); + } - if (!_tryParseBool(arg, keepAliveEnabled)) { - SERPR_ERROR("Invalid argument (not a boolean)"); - return; + buffer.push_back('\n'); } + buffer.push_back('\n'); - bool result = OpenShock::CommandHandler::SetKeepAliveEnabled(keepAliveEnabled); + OS_LOGI(TAG, "Buffer size: %zu", buffer.size()); - if (result) { - SERPR_SUCCESS("Saved config"); - } else { - SERPR_ERROR("Failed to save config"); - } + ::Serial.print(buffer.data()); } -void _handleSerialEchoCommand(std::string_view arg, bool isAutomated) { +void _handleHelpCommand(std::string_view arg, bool isAutomated) { + arg = OpenShock::StringTrim(arg); if (arg.empty()) { - // Get current serial echo status - SERPR_RESPONSE("SerialEcho|%s", s_echoEnabled ? "true" : "false"); - return; - } - - bool enabled; - if (!_tryParseBool(arg, enabled)) { - SERPR_ERROR("Invalid argument (not a boolean)"); + _printCompleteHelp(); return; } - bool result = Config::SetSerialInputConfigEchoEnabled(enabled); - s_echoEnabled = enabled; - - if (result) { - SERPR_SUCCESS("Saved config"); - } else { - SERPR_ERROR("Failed to save config"); - } -} - -void _handleValidGpiosCommand(std::string_view arg, bool isAutomated) { - if (!arg.empty()) { - SERPR_ERROR("Invalid argument (too many arguments)"); + // Get help for a specific command + auto it = s_commandHandlers.find(arg); + if (it != s_commandHandlers.end()) { + _printCommandHelp(it->second); return; } - auto pins = OpenShock::GetValidGPIOPins(); - - std::string buffer; - buffer.reserve(pins.count() * 4); - - for (std::size_t i = 0; i < pins.size(); i++) { - if (pins[i]) { - buffer.append(std::to_string(i)); - buffer.append(","); - } - } - - if (!buffer.empty()) { - buffer.pop_back(); - } - - SERPR_RESPONSE("ValidGPIOs|%s", buffer.c_str()); + SERPR_ERROR("Command \"%.*s\" not found", arg.length(), arg.data()); } -void _handleJsonConfigCommand(std::string_view arg, bool isAutomated) { - if (arg.empty()) { - // Get raw config - std::string json = Config::GetAsJSON(true); - - SERPR_RESPONSE("JsonConfig|%s", json.c_str()); - return; - } - - if (!Config::SaveFromJSON(arg)) { - SERPR_ERROR("Failed to save config"); - return; - } - - SERPR_SUCCESS("Saved config, restarting..."); - - ESP.restart(); +void RegisterCommandHandler(const OpenShock::Serial::CommandGroup& handler) { + s_commandHandlers[handler.name()] = handler; } -void _handleRawConfigCommand(std::string_view arg, bool isAutomated) { - if (arg.empty()) { - std::vector buffer; +#define CLEAR_LINE "\r\x1B[K" - // Get raw config - if (!Config::GetRaw(buffer)) { - SERPR_ERROR("Failed to get raw config"); +class SerialBuffer { + DISABLE_COPY(SerialBuffer); + DISABLE_MOVE(SerialBuffer); + +public: + constexpr SerialBuffer() + : m_data(nullptr) + , m_size(0) + , m_capacity(0) { } + inline SerialBuffer(std::size_t capacity) + : m_data(new char[capacity]) + , m_size(0) + , m_capacity(capacity) { } + inline ~SerialBuffer() { delete[] m_data; } + + constexpr char* data() { return m_data; } + constexpr std::size_t size() const { return m_size; } + constexpr std::size_t capacity() const { return m_capacity; } + constexpr bool empty() const { return m_size == 0; } + + constexpr void clear() { m_size = 0; } + inline void destroy() { + delete[] m_data; + m_data = nullptr; + m_size = 0; + m_capacity = 0; + } + + inline void reserve(std::size_t size) { + size = (size + 31) & ~31; // Align to 32 bytes + + if (size <= m_capacity) { return; } - std::string base64; - if (!OpenShock::Base64Utils::Encode(buffer.data(), buffer.size(), base64)) { - SERPR_ERROR("Failed to encode raw config to base64"); - return; + char* newData = new char[size]; + if (m_data != nullptr) { + std::memcpy(newData, m_data, m_size); + delete[] m_data; } - SERPR_RESPONSE("RawConfig|%s", base64.c_str()); - return; - } - - std::vector buffer; - if (!OpenShock::Base64Utils::Decode(arg.data(), arg.length(), buffer)) { - SERPR_ERROR("Failed to decode base64"); - return; - } - - if (!Config::SetRaw(buffer.data(), buffer.size())) { - SERPR_ERROR("Failed to save config"); - return; - } - - SERPR_SUCCESS("Saved config, restarting..."); - - ESP.restart(); -} - -void _handleDebugInfoCommand(std::string_view arg, bool isAutomated) { - (void)arg; - - SERPR_RESPONSE("RTOSInfo|Free Heap|%u", xPortGetFreeHeapSize()); - SERPR_RESPONSE("RTOSInfo|Min Free Heap|%u", xPortGetMinimumEverFreeHeapSize()); - - const int64_t now = OpenShock::millis(); - SERPR_RESPONSE("RTOSInfo|UptimeMS|%lli", now); - - const int64_t seconds = now / 1000; - const int64_t minutes = seconds / 60; - const int64_t hours = minutes / 60; - const int64_t days = hours / 24; - SERPR_RESPONSE("RTOSInfo|Uptime|%llid %llih %llim %llis", days, hours % 24, minutes % 60, seconds % 60); - - OpenShock::WiFiNetwork network; - bool connected = OpenShock::WiFiManager::GetConnectedNetwork(network); - SERPR_RESPONSE("WiFiInfo|Connected|%s", connected ? "true" : "false"); - if (connected) { - SERPR_RESPONSE("WiFiInfo|SSID|%s", network.ssid); - SERPR_RESPONSE("WiFiInfo|BSSID|" BSSID_FMT, BSSID_ARG(network.bssid)); - - char ipAddressBuffer[64]; - OpenShock::WiFiManager::GetIPAddress(ipAddressBuffer); - SERPR_RESPONSE("WiFiInfo|IPv4|%s", ipAddressBuffer); - OpenShock::WiFiManager::GetIPv6Address(ipAddressBuffer); - SERPR_RESPONSE("WiFiInfo|IPv6|%s", ipAddressBuffer); - } -} - -void _handleRFTransmitCommand(std::string_view arg, bool isAutomated) { - if (arg.empty()) { - SERPR_ERROR("No command"); - return; - } - cJSON* root = cJSON_ParseWithLength(arg.data(), arg.length()); - if (root == nullptr) { - SERPR_ERROR("Failed to parse JSON: %s", cJSON_GetErrorPtr()); - return; - } - - OpenShock::Serialization::JsonSerial::ShockerCommand cmd; - bool parsed = Serialization::JsonSerial::ParseShockerCommand(root, cmd); - - cJSON_Delete(root); - - if (!parsed) { - SERPR_ERROR("Failed to parse shocker command"); - return; + m_data = newData; + m_capacity = size; } - if (!OpenShock::CommandHandler::HandleCommand(cmd.model, cmd.id, cmd.command, cmd.intensity, cmd.durationMs)) { - SERPR_ERROR("Failed to send command"); - return; - } - - SERPR_SUCCESS("Command sent"); -} + inline void push_back(char c) { + if (m_size >= m_capacity) { + reserve(m_capacity + 16); + } -void _handleHelpCommand(std::string_view arg, bool isAutomated) { - arg = OpenShock::StringTrim(arg); - if (arg.empty()) { - SerialInputHandler::PrintWelcomeHeader(); - - // Raw string literal (1+ to remove the first newline) - Serial.print(1 + R"( -help print this menu -help print help for a command -version print version information -restart restart the board -sysinfo print debug information for various subsystems -echo get serial echo enabled -echo set serial echo enabled -validgpios list all valid GPIO pins -rftxpin get radio transmit pin -rftxpin set radio transmit pin -estoppin get e-stop pin -estoppin set e-stop pin -domain get backend domain -domain set backend domain -authtoken get auth token -authtoken set auth token -hostname get network hostname -hostname set network hostname -networks get all saved networks -networks set all saved networks -keepalive get shocker keep-alive enabled -keepalive set shocker keep-alive enabled -jsonconfig get configuration as JSON -jsonconfig set configuration from JSON -rawconfig get raw configuration as base64 -rawconfig set raw configuration from base64 -rftransmit transmit a RF command -factoryreset reset device to factory defaults and restart -)"); - return; + m_data[m_size++] = c; } - // Get help for a specific command - auto it = s_commandHandlers.find(arg); - if (it != s_commandHandlers.end()) { - Serial.print(it->second.helpResponse); - return; + constexpr void pop_back() { + if (m_size > 0) { + --m_size; + } } - SERPR_ERROR("Command \"%.*s\" not found", arg.length(), arg.data()); -} + constexpr operator std::string_view() const { return std::string_view(m_data, m_size); } -static const SerialCmdHandler kVersionCmdHandler = { - "version"sv, - R"(version - Print version information - Example: - version -)", - _handleVersionCommand, -}; -static const SerialCmdHandler kRestartCmdHandler = { - "restart"sv, - R"(restart - Restart the board - Example: - restart -)", - _handleRestartCommand, -}; -static const SerialCmdHandler kSystemInfoCmdHandler = { - "sysinfo"sv, - R"(sysinfo - Get system information from RTOS, WiFi, etc. - Example: - sysinfo -)", - _handleDebugInfoCommand, -}; -static const SerialCmdHandler kSerialEchoCmdHandler = { - "echo"sv, - R"(echo - Get the serial echo status. - If enabled, typed characters are echoed back to the serial port. - -echo [] - Enable/disable serial echo. - Arguments: - must be a boolean. - Example: - echo true -)", - _handleSerialEchoCommand, -}; -static const SerialCmdHandler kValidGpiosCmdHandler = { - "validgpios"sv, - R"(validgpios - List all valid GPIO pins - Example: - validgpios -)", - _handleValidGpiosCommand, -}; -static const SerialCmdHandler kRfTxPinCmdHandler = { - "rftxpin"sv, - R"(rftxpin - Get the GPIO pin used for the radio transmitter. - -rftxpin [] - Set the GPIO pin used for the radio transmitter. - Arguments: - must be a number. - Example: - rftxpin 15 -)", - _handleRfTxPinCommand, -}; -static const SerialCmdHandler kEStopPinCmdHandler = { - "estoppin"sv, - R"(estoppin - Get the GPIO pin used for the E-Stop. - -estoppin [] - Set the GPIO pin used for the E-Stop. - Arguments: - must be a number. - Example: - estoppin 4 -)", - _handleEStopPinCommand, -}; -static const SerialCmdHandler kDomainCmdHandler = { - "domain"sv, - R"(domain - Get the backend domain. - -domain [] - Set the backend domain. - Arguments: - must be a string. - Example: - domain api.openshock.app -)", - _handleDomainCommand, -}; -static const SerialCmdHandler kAuthTokenCmdHandler = { - "authtoken"sv, - R"(authtoken - Get the backend auth token. - -authtoken [] - Set the auth token. - Arguments: - must be a string. - Example: - authtoken mytoken -)", - _handleAuthtokenCommand, -}; -static const SerialCmdHandler kLcgOverrideCmdHandler = { - "lcgoverride", - R"(lcgoverride - Get the domain overridden for LCG endpoint (if any). - -lcgoverride set - Set a domain to override the LCG endpoint. - Arguments: - must be a string. - Example: - lcgoverride set eu1-gateway.openshock.app - -lcgoverride clear - Clear the overridden LCG endpoint. - Example: - lcgoverride clear -)", - _handleLcgOverrideCommand, -}; -static const SerialCmdHandler kHostnameCmdHandler = { - "hostname"sv, - R"(hostname - Get the network hostname. - -hostname [] - Set the network hostname. - Arguments: - must be a string. - Example: - hostname OpenShock -)", - _handleHostnameCommand, -}; -static const SerialCmdHandler kNetworksCmdHandler = { - "networks"sv, - R"(networks - Get all saved networks. - -networks [] - Set all saved networks. - Arguments: - must be a array of objects with the following fields: - ssid (string) SSID of the network - password (string) Password of the network - id (number) ID of the network (optional) - Example: - networks [{\"ssid\":\"myssid\",\"password\":\"mypassword\"}] -)", - _handleNetworksCommand, -}; -static const SerialCmdHandler kKeepAliveCmdHandler = { - "keepalive"sv, - R"(keepalive - Get the shocker keep-alive status. - -keepalive [] - Enable/disable shocker keep-alive. - Arguments: - must be a boolean. - Example: - keepalive true -)", - _handleKeepAliveCommand, +private: + char* m_data; + std::size_t m_size; + std::size_t m_capacity; }; -static const SerialCmdHandler kJsonConfigCmdHandler = { - "jsonconfig"sv, - R"(jsonconfig - Get the configuration as JSON - Example: - jsonconfig - -jsonconfig - Set the configuration from JSON, and restart - Arguments: - must be a valid JSON object - Example: - jsonconfig { ... } -)", - _handleJsonConfigCommand, -}; -static const SerialCmdHandler kRawConfigCmdHandler = { - "rawconfig"sv, - R"(rawconfig - Get the raw binary config - Example: - rawconfig - -rawconfig - Set the raw binary config, and restart - Arguments: - must be a base64 encoded string - Example: - rawconfig (base64 encoded binary data) -)", - _handleRawConfigCommand, -}; -static const SerialCmdHandler kRfTransmitCmdHandler = { - "rftransmit"sv, - R"(rftransmit - Transmit a RF command - Arguments: - must be a JSON object with the following fields: - model (string) Model of the shocker ("caixianlin", "petrainer", "petrainer998dr") - id (number) ID of the shocker (0-65535) - type (string) Type of the command ("shock", "vibrate", "sound", "stop") - intensity (number) Intensity of the command (0-255) - durationMs (number) Duration of the command in milliseconds (0-65535) - Example: - rftransmit {"model":"caixianlin","id":12345,"type":"vibrate","intensity":99,"durationMs":500} -)", - _handleRFTransmitCommand, -}; -static const SerialCmdHandler kFactoryResetCmdHandler = { - "factoryreset"sv, - R"(factoryreset - Reset the device to factory defaults and restart - Example: - factoryreset -)", - _handleFactoryResetCommand, -}; -static const SerialCmdHandler khelpCmdHandler = { - "help"sv, - R"(help [] - Print help information - Arguments: - (optional) command to print help for - Example: - help -)", - _handleHelpCommand, -}; - -void RegisterCommandHandler(const SerialCmdHandler& handler) { - s_commandHandlers[handler.cmd] = handler; -} - -#define CLEAR_LINE "\r\x1B[K" enum class SerialReadResult { NoData, @@ -916,9 +346,9 @@ enum class SerialReadResult { AutoCompleteRequest, }; -SerialReadResult _tryReadSerialLine(std::string& buffer) { +SerialReadResult _tryReadSerialLine(SerialBuffer& buffer) { // Check if there's any data available - int available = ::Serial.available(); + int available = ::Serial.available(); if (available <= 0) { return SerialReadResult::NoData; } @@ -932,9 +362,7 @@ SerialReadResult _tryReadSerialLine(std::string& buffer) { // Handle backspace if (c == '\b') { - if (!buffer.empty()) { - buffer.pop_back(); - } + buffer.pop_back(); // Remove the last character from the buffer if it exists continue; } @@ -955,14 +383,16 @@ SerialReadResult _tryReadSerialLine(std::string& buffer) { return SerialReadResult::AutoCompleteRequest; } - // Add the character to the buffer - buffer.push_back(c); + // If character is printable, add it to the buffer + if (c > 31 && c < 127) { + buffer.push_back(c); + } } return SerialReadResult::Data; } -void _skipSerialWhitespaces(std::string& buffer) { +void _skipSerialWhitespaces(SerialBuffer& buffer) { int available = ::Serial.available(); while (available-- > 0) { @@ -1025,16 +455,26 @@ void _processSerialLine(std::string_view line) { } auto parts = OpenShock::StringSplit(line, ' ', 1); - std::string_view command = parts[0]; + std::string_view command = OpenShock::StringTrim(parts[0]); std::string_view arguments = parts.size() > 1 ? parts[1] : std::string_view(); + if (command == "help"sv) { + _handleHelpCommand(arguments, isAutomated); + return; + } + auto it = s_commandHandlers.find(command); if (it == s_commandHandlers.end()) { SERPR_ERROR("Command \"%.*s\" not found", command.size(), command.data()); return; } - it->second.commandHandler(arguments, isAutomated); + for (const auto& cmd : it->second.commands()) { + if (cmd.arguments().empty()) { + cmd.commandHandler()(arguments, isAutomated); + return; + } + } } bool SerialInputHandler::Init() { @@ -1046,28 +486,15 @@ bool SerialInputHandler::Init() { s_initialized = true; // Register command handlers - RegisterCommandHandler(kVersionCmdHandler); - RegisterCommandHandler(kRestartCmdHandler); - RegisterCommandHandler(kSystemInfoCmdHandler); - RegisterCommandHandler(kSerialEchoCmdHandler); - RegisterCommandHandler(kValidGpiosCmdHandler); - RegisterCommandHandler(kRfTxPinCmdHandler); - RegisterCommandHandler(kEStopPinCmdHandler); - RegisterCommandHandler(kDomainCmdHandler); - RegisterCommandHandler(kAuthTokenCmdHandler); - RegisterCommandHandler(kLcgOverrideCmdHandler); - RegisterCommandHandler(kHostnameCmdHandler); - RegisterCommandHandler(kNetworksCmdHandler); - RegisterCommandHandler(kKeepAliveCmdHandler); - RegisterCommandHandler(kJsonConfigCmdHandler); - RegisterCommandHandler(kRawConfigCmdHandler); - RegisterCommandHandler(kRfTransmitCmdHandler); - RegisterCommandHandler(kFactoryResetCmdHandler); - RegisterCommandHandler(khelpCmdHandler); + s_commandGroups = OpenShock::Serial::CommandHandlers::AllCommandHandlers(); + for (const auto& handler : s_commandGroups) { + OS_LOGV(TAG, "Registering command handler: %.*s", handler.name().size(), handler.name().data()); + RegisterCommandHandler(handler); + } SerialInputHandler::PrintWelcomeHeader(); SerialInputHandler::PrintVersionInfo(); - Serial.println(); + ::Serial.println(); if (!Config::GetSerialInputConfigEchoEnabled(s_echoEnabled)) { OS_LOGE(TAG, "Failed to get serial echo status from config"); @@ -1078,37 +505,44 @@ bool SerialInputHandler::Init() { } void SerialInputHandler::Update() { - static std::string buffer = ""; + static SerialBuffer buffer(32); switch (_tryReadSerialLine(buffer)) { - case SerialReadResult::LineEnd: - _processSerialLine(buffer); - - // Deallocate memory if the buffer is too large - if (buffer.capacity() > SERIAL_BUFFER_CLEAR_THRESHOLD) { - buffer.clear(); - buffer.shrink_to_fit(); - } else { - buffer.resize(0); // Hopefully doesn't deallocate memory - } + case SerialReadResult::LineEnd: + _processSerialLine(buffer); + + // Deallocate memory if the buffer is too large + if (buffer.capacity() > SERIAL_BUFFER_CLEAR_THRESHOLD) { + buffer.destroy(); + } else { + buffer.clear(); + } - // Skip any remaining trailing whitespaces - _skipSerialWhitespaces(buffer); - break; - case SerialReadResult::AutoCompleteRequest: - ::Serial.printf(CLEAR_LINE "> %.*s [AutoComplete is not implemented]", buffer.size(), buffer.data()); - break; - case SerialReadResult::Data: - _echoHandleSerialInput(buffer, true); - break; - default: - _echoHandleSerialInput(buffer, false); - break; + // Skip any remaining trailing whitespaces + _skipSerialWhitespaces(buffer); + break; + case SerialReadResult::AutoCompleteRequest: + ::Serial.printf(CLEAR_LINE "> %.*s [AutoComplete is not implemented]", buffer.size(), buffer.data()); + break; + case SerialReadResult::Data: + _echoHandleSerialInput(buffer, true); + break; + default: + _echoHandleSerialInput(buffer, false); + break; } } +bool SerialInputHandler::SerialEchoEnabled() { + return s_echoEnabled; +} + +void SerialInputHandler::SetSerialEchoEnabled(bool enabled) { + s_echoEnabled = enabled; +} + void SerialInputHandler::PrintWelcomeHeader() { - Serial.print(R"( + ::Serial.print(R"( ============== OPENSHOCK ============== Contribute @ github.com/OpenShock Discuss @ discord.gg/OpenShock @@ -1118,7 +552,7 @@ void SerialInputHandler::PrintWelcomeHeader() { } void SerialInputHandler::PrintVersionInfo() { - Serial.print("\ + ::Serial.print("\ Version: " OPENSHOCK_FW_VERSION "\n\ Build: " OPENSHOCK_FW_MODE "\n\ Commit: " OPENSHOCK_FW_GIT_COMMIT "\n\ diff --git a/src/serial/command_handlers/CommandEntry.cpp b/src/serial/command_handlers/CommandEntry.cpp new file mode 100644 index 00000000..e3a7da57 --- /dev/null +++ b/src/serial/command_handlers/CommandEntry.cpp @@ -0,0 +1,35 @@ +#include "serial/command_handlers/CommandEntry.h" + +using namespace OpenShock::Serial; + +CommandEntry::CommandEntry(std::string_view description, CommandHandler commandHandler) + : m_description(description) + , m_commandHandler(commandHandler) { +} + +CommandEntry::CommandEntry(std::string_view name, std::string_view description, CommandHandler commandHandler) + : m_name(name) + , m_description(description) + , m_commandHandler(commandHandler) { +} + +CommandArgument& CommandEntry::addArgument(std::string_view name, std::string_view constraint, std::string_view exampleValue, std::vector constraintExtensions) { + m_arguments.push_back(CommandArgument {name, constraint, exampleValue, constraintExtensions}); + return m_arguments.back(); +} + +CommandGroup::CommandGroup(std::string_view name) + : m_name(name) { +} + +CommandEntry& CommandGroup::addCommand(std::string_view description, CommandHandler commandHandler) { + auto cmd = CommandEntry(description, commandHandler); + m_commands.push_back(cmd); + return m_commands.back(); +} + +CommandEntry& CommandGroup::addCommand(std::string_view name, std::string_view description, CommandHandler commandHandler) { + auto cmd = CommandEntry(name, description, commandHandler); + m_commands.push_back(cmd); + return m_commands.back(); +} diff --git a/src/serial/command_handlers/authtoken.cpp b/src/serial/command_handlers/authtoken.cpp new file mode 100644 index 00000000..fad27993 --- /dev/null +++ b/src/serial/command_handlers/authtoken.cpp @@ -0,0 +1,38 @@ +#include "serial/command_handlers/common.h" + +#include "config/Config.h" + +#include + +void _handleAuthtokenCommand(std::string_view arg, bool isAutomated) { + if (arg.empty()) { + std::string authToken; + if (!OpenShock::Config::GetBackendAuthToken(authToken)) { + SERPR_ERROR("Failed to get auth token from config"); + return; + } + + // Get auth token + SERPR_RESPONSE("AuthToken|%s", authToken.c_str()); + return; + } + + bool result = OpenShock::Config::SetBackendAuthToken(arg); + + if (result) { + SERPR_SUCCESS("Saved config"); + } else { + SERPR_ERROR("Failed to save config"); + } +} + +OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::AuthTokenHandler() { + auto group = OpenShock::Serial::CommandGroup("authtoken"sv); + + auto& getCommand = group.addCommand("Get the backend auth token"sv, _handleAuthtokenCommand); + + auto& setCommand = group.addCommand("Set the auth token"sv, _handleAuthtokenCommand); + setCommand.addArgument("token"sv, "must be a string"sv, "mytoken"sv); + + return group; +} diff --git a/src/serial/command_handlers/domain.cpp b/src/serial/command_handlers/domain.cpp new file mode 100644 index 00000000..7b010de2 --- /dev/null +++ b/src/serial/command_handlers/domain.cpp @@ -0,0 +1,72 @@ +#include "serial/command_handlers/common.h" + +#include "config/Config.h" +#include "http/HTTPRequestManager.h" +#include "serialization/JsonAPI.h" + +#include + +const char* const TAG = "Serial::CommandHandlers::Domain"; + +void _handleDomainCommand(std::string_view arg, bool isAutomated) { + if (arg.empty()) { + std::string domain; + if (!OpenShock::Config::GetBackendDomain(domain)) { + SERPR_ERROR("Failed to get domain from config"); + return; + } + + // Get domain + SERPR_RESPONSE("Domain|%s", domain.c_str()); + return; + } + + // Check if the domain is too long + // TODO: Remove magic number + if (arg.length() + 40 >= OPENSHOCK_URI_BUFFER_SIZE) { + SERPR_ERROR("Domain name too long, please try increasing the \"OPENSHOCK_URI_BUFFER_SIZE\" constant in source code"); + return; + } + + char uri[OPENSHOCK_URI_BUFFER_SIZE]; + sprintf(uri, "https://%.*s/1", arg.length(), arg.data()); + + auto resp = OpenShock::HTTP::GetJSON( + uri, + { + {"Accept", "application/json"} + }, + OpenShock::Serialization::JsonAPI::ParseBackendVersionJsonResponse, + {200} + ); + + if (resp.result != OpenShock::HTTP::RequestResult::Success) { + SERPR_ERROR("Tried to connect to \"%.*s\", but failed with status [%d], refusing to save domain to config", arg.length(), arg.data(), resp.code); + return; + } + + OS_LOGI(TAG, "Successfully connected to \"%.*s\", version: %s, commit: %s, current time: %s", arg.length(), arg.data(), resp.data.version.c_str(), resp.data.commit.c_str(), resp.data.currentTime.c_str()); + + bool result = OpenShock::Config::SetBackendDomain(arg); + + if (!result) { + SERPR_ERROR("Failed to save config"); + return; + } + + SERPR_SUCCESS("Saved config, restarting..."); + + // Restart to use the new domain + ESP.restart(); +} + +OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::DomainHandler() { + auto group = OpenShock::Serial::CommandGroup("domain"sv); + + auto& getCommand = group.addCommand("Get the backend domain."sv, _handleDomainCommand); + + auto& setCommand = group.addCommand("Set the backend domain."sv, _handleDomainCommand); + setCommand.addArgument("domain"sv, "must be a string"sv, "api.shocklink.net"sv); + + return group; +} diff --git a/src/serial/command_handlers/echo.cpp b/src/serial/command_handlers/echo.cpp new file mode 100644 index 00000000..0b111dc6 --- /dev/null +++ b/src/serial/command_handlers/echo.cpp @@ -0,0 +1,41 @@ +#include "serial/command_handlers/common.h" + +#include "serial/SerialInputHandler.h" + +#include "config/Config.h" +#include "Convert.h" +#include "util/StringUtils.h" + +void _handleSerialEchoCommand(std::string_view arg, bool isAutomated) { + if (arg.empty()) { + // Get current serial echo status + SERPR_RESPONSE("SerialEcho|%s", OpenShock::SerialInputHandler::SerialEchoEnabled() ? "true" : "false"); + return; + } + + bool enabled; + if (!OpenShock::Convert::ToBool(OpenShock::StringTrim(arg), enabled)) { + SERPR_ERROR("Invalid argument (not a boolean)"); + return; + } + + bool result = OpenShock::Config::SetSerialInputConfigEchoEnabled(enabled); + OpenShock::SerialInputHandler::SetSerialEchoEnabled(enabled); + + if (result) { + SERPR_SUCCESS("Saved config"); + } else { + SERPR_ERROR("Failed to save config"); + } +} + +OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::EchoHandler() { + auto group = OpenShock::Serial::CommandGroup("echo"sv); + + auto& getCommand = group.addCommand("Get the serial echo status"sv, _handleSerialEchoCommand); + + auto& setCommand = group.addCommand("Enable/disable serial echo"sv, _handleSerialEchoCommand); + setCommand.addArgument("enabled"sv, "must be a boolean"sv, "true"sv); + + return group; +} diff --git a/src/serial/command_handlers/estoppin.cpp b/src/serial/command_handlers/estoppin.cpp new file mode 100644 index 00000000..237a9e1f --- /dev/null +++ b/src/serial/command_handlers/estoppin.cpp @@ -0,0 +1,50 @@ +#include "serial/command_handlers/common.h" + +#include "config/Config.h" +#include "Convert.h" +#include "EStopManager.h" + +void _handleEStopPinCommand(std::string_view arg, bool isAutomated) { + if (arg.empty()) { + gpio_num_t estopPin; + if (!OpenShock::Config::GetEStopGpioPin(estopPin)) { + SERPR_ERROR("Failed to get EStop pin from config"); + return; + } + + // Get EStop pin + SERPR_RESPONSE("EStopPin|%hhi", static_cast(estopPin)); + return; + } + + uint8_t pin; + if (!OpenShock::Convert::ToUint8(arg, pin)) { + SERPR_ERROR("Invalid argument (number invalid or out of range)"); + return; + } + + gpio_num_t estopPin = static_cast(pin); + + if (!OpenShock::EStopManager::SetEStopPin(estopPin)) { + SERPR_ERROR("Failed to set EStop pin"); + return; + } + + if (!OpenShock::Config::SetEStopGpioPin(estopPin)) { + SERPR_ERROR("Failed to save config"); + return; + } + + SERPR_SUCCESS("Saved config"); +} + +OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::ESStopPinHandler() { + auto group = OpenShock::Serial::CommandGroup("estoppin"sv); + + auto& getCommand = group.addCommand("Get the GPIO pin used for the E-Stop."sv, _handleEStopPinCommand); + + auto& setCommand = group.addCommand("Set the GPIO pin used for the E-Stop."sv, _handleEStopPinCommand); + setCommand.addArgument("pin"sv, "must be a number"sv, "4"sv); + + return group; +} diff --git a/src/serial/command_handlers/factoryreset.cpp b/src/serial/command_handlers/factoryreset.cpp new file mode 100644 index 00000000..7d69f0c3 --- /dev/null +++ b/src/serial/command_handlers/factoryreset.cpp @@ -0,0 +1,20 @@ +#include "serial/command_handlers/common.h" + +#include "config/Config.h" + +void _handleFactoryResetCommand(std::string_view arg, bool isAutomated) { + (void)arg; + + ::Serial.println("Resetting to factory defaults..."); + OpenShock::Config::FactoryReset(); + ::Serial.println("Restarting..."); + ESP.restart(); +} + +OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::FactoryResetHandler() { + auto group = OpenShock::Serial::CommandGroup("factoryreset"sv); + + auto& cmd = group.addCommand("Reset the device to factory defaults and restart"sv, _handleFactoryResetCommand); + + return group; +} diff --git a/src/serial/command_handlers/hostname.cpp b/src/serial/command_handlers/hostname.cpp new file mode 100644 index 00000000..6c1e9b1c --- /dev/null +++ b/src/serial/command_handlers/hostname.cpp @@ -0,0 +1,39 @@ +#include "serial/command_handlers/common.h" + +#include "config/Config.h" + +#include + +const char* const TAG = "Serial::CommandHandlers::Domain"; + +void _handleHostnameCommand(std::string_view arg, bool isAutomated) { + if (arg.empty()) { + std::string hostname; + if (!OpenShock::Config::GetWiFiHostname(hostname)) { + SERPR_ERROR("Failed to get hostname from config"); + return; + } + // Get hostname + SERPR_RESPONSE("Hostname|%s", hostname.c_str()); + return; + } + + bool result = OpenShock::Config::SetWiFiHostname(arg); + if (result) { + SERPR_SUCCESS("Saved config, restarting..."); + ESP.restart(); + } else { + SERPR_ERROR("Failed to save config"); + } +} + +OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::HostnameHandler() { + auto group = OpenShock::Serial::CommandGroup("hostname"sv); + + auto& getCommand = group.addCommand("Get the network hostname."sv, _handleHostnameCommand); + + auto& setCommand = group.addCommand("Set the network hostname."sv, _handleHostnameCommand); + setCommand.addArgument("hostname"sv, "must be a string"sv, "OpenShock"sv); + + return group; +} diff --git a/src/serial/command_handlers/jsonconfig.cpp b/src/serial/command_handlers/jsonconfig.cpp new file mode 100644 index 00000000..2c1091f6 --- /dev/null +++ b/src/serial/command_handlers/jsonconfig.cpp @@ -0,0 +1,33 @@ +#include "serial/command_handlers/common.h" + +#include "config/Config.h" + +void _handleJsonConfigCommand(std::string_view arg, bool isAutomated) { + if (arg.empty()) { + // Get raw config + std::string json = OpenShock::Config::GetAsJSON(true); + + SERPR_RESPONSE("JsonConfig|%s", json.c_str()); + return; + } + + if (!OpenShock::Config::SaveFromJSON(arg)) { + SERPR_ERROR("Failed to save config"); + return; + } + + SERPR_SUCCESS("Saved config, restarting..."); + + ESP.restart(); +} + +OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::JsonConfigHandler() { + auto group = OpenShock::Serial::CommandGroup("jsonconfig"sv); + + auto& getCommand = group.addCommand("Get the configuration as JSON"sv, _handleJsonConfigCommand); + + auto& setCommand = group.addCommand("Set the configuration from JSON, and restart"sv, _handleJsonConfigCommand); + setCommand.addArgument("json"sv, "must be a valid JSON object"sv, "{ ... }"sv); + + return group; +} diff --git a/src/serial/command_handlers/keepalive.cpp b/src/serial/command_handlers/keepalive.cpp new file mode 100644 index 00000000..eecfe868 --- /dev/null +++ b/src/serial/command_handlers/keepalive.cpp @@ -0,0 +1,45 @@ +#include "serial/command_handlers/common.h" + +#include "CommandHandler.h" +#include "config/Config.h" +#include "Convert.h" +#include "util/StringUtils.h" + +void _handleKeepAliveCommand(std::string_view arg, bool isAutomated) { + bool keepAliveEnabled; + + if (arg.empty()) { + // Get keep alive status + if (!OpenShock::Config::GetRFConfigKeepAliveEnabled(keepAliveEnabled)) { + SERPR_ERROR("Failed to get keep-alive status from config"); + return; + } + + SERPR_RESPONSE("KeepAlive|%s", keepAliveEnabled ? "true" : "false"); + return; + } + + if (!OpenShock::Convert::ToBool(OpenShock::StringTrim(arg), keepAliveEnabled)) { + SERPR_ERROR("Invalid argument (not a boolean)"); + return; + } + + bool result = OpenShock::CommandHandler::SetKeepAliveEnabled(keepAliveEnabled); + + if (result) { + SERPR_SUCCESS("Saved config"); + } else { + SERPR_ERROR("Failed to save config"); + } +} + +OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::KeepAliveHandler() { + auto group = OpenShock::Serial::CommandGroup("keepalive"sv); + + auto& getCommand = group.addCommand("Get the shocker keep-alive status"sv, _handleKeepAliveCommand); + + auto& setCommand = group.addCommand("Enable/disable shocker keep-alive"sv, _handleKeepAliveCommand); + setCommand.addArgument("enabled"sv, "must be a boolean"sv, "true"sv); + + return group; +} diff --git a/src/serial/command_handlers/lcgoverride.cpp b/src/serial/command_handlers/lcgoverride.cpp new file mode 100644 index 00000000..b3878992 --- /dev/null +++ b/src/serial/command_handlers/lcgoverride.cpp @@ -0,0 +1,104 @@ +#include "serial/command_handlers/common.h" + +#include "config/Config.h" +#include "http/HTTPRequestManager.h" +#include "serialization/JsonAPI.h" +#include "util/StringUtils.h" + +const char* TAG = "Serial::CommandHandlers::LcgOverride"; + +void _handleLcgOverrideCommand(std::string_view arg, bool isAutomated) { + if (arg.empty()) { + std::string lcgOverride; + if (!OpenShock::Config::GetBackendLCGOverride(lcgOverride)) { + SERPR_ERROR("Failed to get LCG override from config"); + return; + } + + // Get LCG override + SERPR_RESPONSE("LcgOverride|%s", lcgOverride.c_str()); + return; + } + + if (OpenShock::StringStartsWith(arg, "clear"sv)) { + if (arg.size() != 5) { + SERPR_ERROR("Invalid command (clear command should not have any arguments)"); + return; + } + + bool result = OpenShock::Config::SetBackendLCGOverride(std::string()); + if (result) { + SERPR_SUCCESS("Cleared LCG override"); + } else { + SERPR_ERROR("Failed to clear LCG override"); + } + return; + } + + if (OpenShock::StringStartsWith(arg, "set "sv)) { + if (arg.size() <= 4) { + SERPR_ERROR("Invalid command (set command should have an argument)"); + return; + } + + std::string_view domain = arg.substr(4); + + if (domain.size() + 40 >= OPENSHOCK_URI_BUFFER_SIZE) { + SERPR_ERROR("Domain name too long, please try increasing the \"OPENSHOCK_URI_BUFFER_SIZE\" constant in source code"); + return; + } + + char uri[OPENSHOCK_URI_BUFFER_SIZE]; + sprintf(uri, "https://%.*s/1", static_cast(domain.size()), domain.data()); + + auto resp = OpenShock::HTTP::GetJSON( + uri, + { + {"Accept", "application/json"} + }, + OpenShock::Serialization::JsonAPI::ParseLcgInstanceDetailsJsonResponse, + {200} + ); + + if (resp.result != OpenShock::HTTP::RequestResult::Success) { + SERPR_ERROR("Tried to connect to \"%.*s\", but failed with status [%d], refusing to save domain to config", domain.size(), domain.data(), resp.code); + return; + } + + OS_LOGI( + TAG, + "Successfully connected to \"%.*s\", name: %s, version: %s, current time: %s, country code: %s, FQDN: %s", + domain.size(), + domain.data(), + resp.data.name.c_str(), + resp.data.version.c_str(), + resp.data.currentTime.c_str(), + resp.data.countryCode.c_str(), + resp.data.fqdn.c_str() + ); + + bool result = OpenShock::Config::SetBackendLCGOverride(domain); + + if (result) { + SERPR_SUCCESS("Saved config"); + } else { + SERPR_ERROR("Failed to save config"); + } + return; + } + + SERPR_ERROR("Invalid subcommand"); +} + +OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::LcgOverrideHandler() { + auto group = OpenShock::Serial::CommandGroup("lcgoverride"sv); + + auto& getCommand = group.addCommand("Get the domain overridden for LCG endpoint (if any)."sv, _handleLcgOverrideCommand); + + auto& setCommand = group.addCommand("set"sv, "Set a domain to override the LCG endpoint."sv, _handleLcgOverrideCommand); + setCommand.addArgument("domain"sv, "must be a string"sv, "eu1-gateway.shocklink.net"sv); + + auto& clearCommand = group.addCommand("clear"sv, "Clear the overridden LCG endpoint."sv, _handleLcgOverrideCommand); + + return group; +} diff --git a/src/serial/command_handlers/networks.cpp b/src/serial/command_handlers/networks.cpp new file mode 100644 index 00000000..26ad6cc8 --- /dev/null +++ b/src/serial/command_handlers/networks.cpp @@ -0,0 +1,110 @@ +#include "serial/command_handlers/common.h" + +#include "config/Config.h" +#include "wifi/WiFiManager.h" + +#include + +#include + +const char* const TAG = "Serial::CommandHandlers::Networks"; + +void _handleNetworksCommand(std::string_view arg, bool isAutomated) { + cJSON* root; + + if (arg.empty()) { + root = cJSON_CreateArray(); + if (root == nullptr) { + SERPR_ERROR("Failed to create JSON array"); + return; + } + + if (!OpenShock::Config::GetWiFiCredentials(root, true)) { + SERPR_ERROR("Failed to get WiFi credentials from config"); + return; + } + + char* out = cJSON_PrintUnformatted(root); + if (out == nullptr) { + SERPR_ERROR("Failed to print JSON"); + return; + } + + SERPR_RESPONSE("Networks|%s", out); + + cJSON_free(out); + return; + } + + root = cJSON_ParseWithLength(arg.data(), arg.length()); + if (root == nullptr) { + SERPR_ERROR("Failed to parse JSON: %s", cJSON_GetErrorPtr()); + return; + } + + if (cJSON_IsArray(root) == 0) { + SERPR_ERROR("Invalid argument (not an array)"); + return; + } + + std::vector creds; + + uint8_t id = 1; + cJSON* network = nullptr; + cJSON_ArrayForEach(network, root) { + OpenShock::Config::WiFiCredentials cred; + + if (!cred.FromJSON(network)) { + SERPR_ERROR("Failed to parse network"); + return; + } + + if (cred.id == 0) { + cred.id = id++; + } + + OS_LOGI(TAG, "Adding network \"%s\" to config, id=%u", cred.ssid.c_str(), cred.id); + + creds.emplace_back(std::move(cred)); + } + + if (!OpenShock::Config::SetWiFiCredentials(creds)) { + SERPR_ERROR("Failed to save config"); + return; + } + + SERPR_SUCCESS("Saved config"); + + OpenShock::WiFiManager::RefreshNetworkCredentials(); +} + +OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::NetworksHandler() { + auto group = OpenShock::Serial::CommandGroup("networks"sv); + + auto& getCommand = group.addCommand("Get all saved networks."sv, _handleNetworksCommand); + + auto& setCommand = group.addCommand("Set all saved networks."sv, _handleNetworksCommand); + setCommand.addArgument("json"sv, "must be a array of objects with the following fields:"sv, "[{\"ssid\":\"myssid\",\"password\":\"mypassword\"}]"sv); + + return group; +} + +/* + return OpenShock::Serial::CommandGroup { + "networks"sv, + R"(networks + Get all saved networks. + +networks [] + Set all saved networks. + Arguments: + must be a array of objects with the following fields: + ssid (string) SSID of the network + password (string) Password of the network + id (number) ID of the network (optional) + Example: + networks [{\"ssid\":\"myssid\",\"password\":\"mypassword\"}] +)", + _handleNetworksCommand, + }; +*/ diff --git a/src/serial/command_handlers/rawconfig.cpp b/src/serial/command_handlers/rawconfig.cpp new file mode 100644 index 00000000..baea2fef --- /dev/null +++ b/src/serial/command_handlers/rawconfig.cpp @@ -0,0 +1,53 @@ +#include "serial/command_handlers/common.h" + +#include "config/Config.h" +#include "util/Base64Utils.h" + +#include + +void _handleRawConfigCommand(std::string_view arg, bool isAutomated) { + if (arg.empty()) { + std::vector buffer; + + // Get raw config + if (!OpenShock::Config::GetRaw(buffer)) { + SERPR_ERROR("Failed to get raw config"); + return; + } + + std::string base64; + if (!OpenShock::Base64Utils::Encode(buffer.data(), buffer.size(), base64)) { + SERPR_ERROR("Failed to encode raw config to base64"); + return; + } + + SERPR_RESPONSE("RawConfig|%s", base64.c_str()); + return; + } + + std::vector buffer; + if (!OpenShock::Base64Utils::Decode(arg.data(), arg.length(), buffer)) { + SERPR_ERROR("Failed to decode base64"); + return; + } + + if (!OpenShock::Config::SetRaw(buffer.data(), buffer.size())) { + SERPR_ERROR("Failed to save config"); + return; + } + + SERPR_SUCCESS("Saved config, restarting..."); + + ESP.restart(); +} + +OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::RawConfigHandler() { + auto group = OpenShock::Serial::CommandGroup("rawconfig"sv); + + auto& getCommand = group.addCommand("Get the raw binary config"sv, _handleRawConfigCommand); + + auto& setCommand = group.addCommand("Set the raw binary config, and restart"sv, _handleRawConfigCommand); + setCommand.addArgument("base64"sv, "must be a base64 encoded string"sv, "(base64 encoded binary data)"sv); + + return group; +} diff --git a/src/serial/command_handlers/restart.cpp b/src/serial/command_handlers/restart.cpp new file mode 100644 index 00000000..e9c898b2 --- /dev/null +++ b/src/serial/command_handlers/restart.cpp @@ -0,0 +1,18 @@ +#include "serial/command_handlers/common.h" + +#include + +void _handleRestartCommand(std::string_view arg, bool isAutomated) { + (void)arg; + + ::Serial.println("Restarting ESP..."); + ESP.restart(); +} + +OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::RestartHandler() { + auto group = OpenShock::Serial::CommandGroup("restart"sv); + + auto& cmd = group.addCommand("Restart the board"sv, _handleRestartCommand); + + return group; +} diff --git a/src/serial/command_handlers/rftransmit.cpp b/src/serial/command_handlers/rftransmit.cpp new file mode 100644 index 00000000..a41b7763 --- /dev/null +++ b/src/serial/command_handlers/rftransmit.cpp @@ -0,0 +1,70 @@ +#include "serial/command_handlers/common.h" + +#include "CommandHandler.h" +#include "serialization/JsonSerial.h" + +void _handleRFTransmitCommand(std::string_view arg, bool isAutomated) { + if (arg.empty()) { + SERPR_ERROR("No command"); + return; + } + cJSON* root = cJSON_ParseWithLength(arg.data(), arg.length()); + if (root == nullptr) { + SERPR_ERROR("Failed to parse JSON: %s", cJSON_GetErrorPtr()); + return; + } + + OpenShock::Serialization::JsonSerial::ShockerCommand cmd; + bool parsed = OpenShock::Serialization::JsonSerial::ParseShockerCommand(root, cmd); + + cJSON_Delete(root); + + if (!parsed) { + SERPR_ERROR("Failed to parse shocker command"); + return; + } + + if (!OpenShock::CommandHandler::HandleCommand(cmd.model, cmd.id, cmd.command, cmd.intensity, cmd.durationMs)) { + SERPR_ERROR("Failed to send command"); + return; + } + + SERPR_SUCCESS("Command sent"); +} + +OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::RfTransmitHandler() { + auto group = OpenShock::Serial::CommandGroup("rftransmit"sv); + + auto& cmd = group.addCommand("Transmit a RF command"sv, _handleRFTransmitCommand); + cmd.addArgument( + "json"sv, + "must be a JSON object with the following fields"sv, + "{\"model\":\"caixianlin\",\"id\":12345,\"type\":\"vibrate\",\"intensity\":99,\"durationMs\":500}", + {"model (string) Model of the shocker (\"caixianlin\", \"petrainer\", \"petrainer998dr\")"sv, + "id (number) ID of the shocker (0-65535)"sv, + "type (string) Type of the command (\"shock\", \"vibrate\", \"sound\", \"stop\")"sv, + "intensity (number) Intensity of the command (0-255)"sv, + "durationMs (number) Duration of the command in milliseconds (0-65535)"sv} + ); + + return group; +} + +/* + return OpenShock::Serial::CommandGroup { + "rftransmit"sv, + R"(rftransmit + Transmit a RF command + Arguments: + must be a JSON object with the following fields: + model (string) Model of the shocker ("caixianlin", "petrainer", "petrainer998dr") + id (number) ID of the shocker (0-65535) + type (string) Type of the command ("shock", "vibrate", "sound", "stop") + intensity (number) Intensity of the command (0-255) + durationMs (number) Duration of the command in milliseconds (0-65535) + Example: + rftransmit {"model":"caixianlin","id":12345,"type":"vibrate","intensity":99,"durationMs":500} +)", + _handleRFTransmitCommand, + }; +*/ diff --git a/src/serial/command_handlers/rftxpin.cpp b/src/serial/command_handlers/rftxpin.cpp new file mode 100644 index 00000000..356fc097 --- /dev/null +++ b/src/serial/command_handlers/rftxpin.cpp @@ -0,0 +1,56 @@ +#include "serial/command_handlers/common.h" + +#include "CommandHandler.h" +#include "config/Config.h" +#include "Convert.h" +#include "SetGPIOResultCode.h" + +void _handleRfTxPinCommand(std::string_view arg, bool isAutomated) { + if (arg.empty()) { + uint8_t txPin; + if (!OpenShock::Config::GetRFConfigTxPin(txPin)) { + SERPR_ERROR("Failed to get RF TX pin from config"); + return; + } + + // Get rmt pin + SERPR_RESPONSE("RmtPin|%u", txPin); + return; + } + + uint8_t pin; + if (!OpenShock::Convert::ToUint8(arg, pin)) { + SERPR_ERROR("Invalid argument (number invalid or out of range)"); + } + + OpenShock::SetGPIOResultCode result = OpenShock::CommandHandler::SetRfTxPin(static_cast(pin)); + + switch (result) { + case OpenShock::SetGPIOResultCode::InvalidPin: + SERPR_ERROR("Invalid argument (invalid pin)"); + break; + + case OpenShock::SetGPIOResultCode::InternalError: + SERPR_ERROR("Internal error while setting RF TX pin"); + break; + + case OpenShock::SetGPIOResultCode::Success: + SERPR_SUCCESS("Saved config"); + break; + + default: + SERPR_ERROR("Unknown error while setting RF TX pin"); + break; + } +} + +OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::RfTxPinHandler() { + auto group = OpenShock::Serial::CommandGroup("rftxpin"sv); + + auto& getCommand = group.addCommand("Get the GPIO pin used for the radio transmitter"sv, _handleRfTxPinCommand); + + auto& setCommand = group.addCommand("Set the GPIO pin used for the radio transmitter"sv, _handleRfTxPinCommand); + setCommand.addArgument("pin"sv, "must be a number"sv, "15"sv); + + return group; +} diff --git a/src/serial/command_handlers/sysinfo.cpp b/src/serial/command_handlers/sysinfo.cpp new file mode 100644 index 00000000..052f495d --- /dev/null +++ b/src/serial/command_handlers/sysinfo.cpp @@ -0,0 +1,44 @@ +#include "serial/command_handlers/common.h" + +#include "FormatHelpers.h" +#include "Time.h" +#include "wifi/WiFiManager.h" +#include "wifi/WiFiNetwork.h" + +void _handleDebugInfoCommand(std::string_view arg, bool isAutomated) { + (void)arg; + + SERPR_RESPONSE("RTOSInfo|Free Heap|%u", xPortGetFreeHeapSize()); + SERPR_RESPONSE("RTOSInfo|Min Free Heap|%u", xPortGetMinimumEverFreeHeapSize()); + + const int64_t now = OpenShock::millis(); + SERPR_RESPONSE("RTOSInfo|UptimeMS|%lli", now); + + const int64_t seconds = now / 1000; + const int64_t minutes = seconds / 60; + const int64_t hours = minutes / 60; + const int64_t days = hours / 24; + SERPR_RESPONSE("RTOSInfo|Uptime|%llid %llih %llim %llis", days, hours % 24, minutes % 60, seconds % 60); + + OpenShock::WiFiNetwork network; + bool connected = OpenShock::WiFiManager::GetConnectedNetwork(network); + SERPR_RESPONSE("WiFiInfo|Connected|%s", connected ? "true" : "false"); + if (connected) { + SERPR_RESPONSE("WiFiInfo|SSID|%s", network.ssid); + SERPR_RESPONSE("WiFiInfo|BSSID|" BSSID_FMT, BSSID_ARG(network.bssid)); + + char ipAddressBuffer[64]; + OpenShock::WiFiManager::GetIPAddress(ipAddressBuffer); + SERPR_RESPONSE("WiFiInfo|IPv4|%s", ipAddressBuffer); + OpenShock::WiFiManager::GetIPv6Address(ipAddressBuffer); + SERPR_RESPONSE("WiFiInfo|IPv6|%s", ipAddressBuffer); + } +} + +OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::SysInfoHandler() { + auto group = OpenShock::Serial::CommandGroup("sysinfo"sv); + + auto& cmd = group.addCommand("Get system information from RTOS, WiFi, etc."sv, _handleDebugInfoCommand); + + return group; +} diff --git a/src/serial/command_handlers/validgpios.cpp b/src/serial/command_handlers/validgpios.cpp new file mode 100644 index 00000000..24e16913 --- /dev/null +++ b/src/serial/command_handlers/validgpios.cpp @@ -0,0 +1,38 @@ +#include "serial/command_handlers/common.h" + +#include "Chipset.h" + +#include + +void _handleValidGpiosCommand(std::string_view arg, bool isAutomated) { + if (!arg.empty()) { + SERPR_ERROR("Invalid argument (too many arguments)"); + return; + } + + auto pins = OpenShock::GetValidGPIOPins(); + + std::string buffer; + buffer.reserve(pins.count() * 4); + + for (std::size_t i = 0; i < pins.size(); i++) { + if (pins[i]) { + buffer.append(std::to_string(i)); + buffer.append(","); + } + } + + if (!buffer.empty()) { + buffer.pop_back(); + } + + SERPR_RESPONSE("ValidGPIOs|%s", buffer.c_str()); +} + +OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::ValidGpiosHandler() { + auto group = OpenShock::Serial::CommandGroup("validgpios"sv); + + auto& cmd = group.addCommand("List all valid GPIO pins"sv, _handleValidGpiosCommand); + + return group; +} diff --git a/src/serial/command_handlers/version.cpp b/src/serial/command_handlers/version.cpp new file mode 100644 index 00000000..3b3c5508 --- /dev/null +++ b/src/serial/command_handlers/version.cpp @@ -0,0 +1,20 @@ +#include "serial/command_handlers/common.h" + +#include "serial/SerialInputHandler.h" + +#include + +void _handleVersionCommand(std::string_view arg, bool isAutomated) { + (void)arg; + + ::Serial.print("\n"); + OpenShock::SerialInputHandler::PrintVersionInfo(); +} + +OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::VersionHandler() { + auto group = OpenShock::Serial::CommandGroup("version"sv); + + auto cmd = group.addCommand("Print version information"sv, _handleVersionCommand); + + return group; +} diff --git a/src/serialization/JsonAPI.cpp b/src/serialization/JsonAPI.cpp index 0e96f5b1..6e2d729d 100644 --- a/src/serialization/JsonAPI.cpp +++ b/src/serialization/JsonAPI.cpp @@ -119,7 +119,7 @@ bool JsonAPI::ParseAccountLinkJsonResponse(int code, const cJSON* root, JsonAPI: } bool JsonAPI::ParseDeviceInfoJsonResponse(int code, const cJSON* root, JsonAPI::DeviceInfoResponse& out) { (void)code; - + if (cJSON_IsObject(root) == 0) { ESP_LOGJSONE("not an object", root); return false; @@ -198,7 +198,7 @@ bool JsonAPI::ParseDeviceInfoJsonResponse(int code, const cJSON* root, JsonAPI:: } OpenShock::ShockerModelType shockerModelType; - if (!OpenShock::ShockerModelTypeFromString(shockerModelStr, shockerModelType, true)) { // PetTrainer is a typo in the API, we pass true to allow it + if (!OpenShock::ShockerModelTypeFromString(shockerModelStr, shockerModelType, true)) { // PetTrainer is a typo in the API, we pass true to allow it ESP_LOGJSONE("value at 'shocker.model' is not a valid shocker model", shocker); return false; }