diff --git a/.env b/.env index 96573fb6..3afb5f60 100644 --- a/.env +++ b/.env @@ -1,4 +1,5 @@ OPENSHOCK_API_DOMAIN=api.shocklink.net +OPENSHOCK_FW_CDN_DOMAIN=firmware.openshock.org OPENSHOCK_FW_VERSION=0.0.0-unknown OPENSHOCK_FW_HOSTNAME=OpenShock OPENSHOCK_FW_AP_PREFIX=OpenShock- diff --git a/.env.development b/.env.development index 2f55eeb9..1cd46e41 100644 --- a/.env.development +++ b/.env.development @@ -1,4 +1,4 @@ -# Included by WebUI/ (DO NOT RENAME THIS FILE!) +# Included by frontend/ (DO NOT RENAME THIS FILE!) # Intended for development builds (locally). LOG_LEVEL=VERBOSE OPENSHOCK_FW_VERSION=0.0.0-local diff --git a/.env.production b/.env.production index bf8f693d..019df3bf 100644 --- a/.env.production +++ b/.env.production @@ -1,3 +1,3 @@ -# Included by WebUI/ (DO NOT RENAME THIS FILE!) +# Included by frontend/ (DO NOT RENAME THIS FILE!) # Intended for all CI firmware builds. LOG_LEVEL=INFO diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..c4c1ab34 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +*.txt text eol=lf diff --git a/.github/ISSUE_TEMPLATE/70-board-test-report.yml b/.github/ISSUE_TEMPLATE/70-board-test-report.yml index 8a71f978..3f5e32a8 100644 --- a/.github/ISSUE_TEMPLATE/70-board-test-report.yml +++ b/.github/ISSUE_TEMPLATE/70-board-test-report.yml @@ -125,8 +125,8 @@ body: attributes: label: 'Gateway checks' options: - - label: 'Can use pairing code to connect board to gateway server' - - label: 'Shows up as online on server board list (depends on API, LiveControlGateway and WebUI)' + - label: 'Can use linking code to link board to account on gateway server' + - label: 'Shows up as online on server board list (depends on API, LiveControlGateway and Captive Portal)' - type: markdown attributes: diff --git a/.github/actions/build-captive-portal/action.yml b/.github/actions/build-captive-portal/action.yml deleted file mode 100644 index 55fe4967..00000000 --- a/.github/actions/build-captive-portal/action.yml +++ /dev/null @@ -1,44 +0,0 @@ - -name: build-captive-portal -description: Builds the captive portal -inputs: - node-version: - description: 'NodeJS runtime version to use' - required: true - skip-checkout: - description: 'If true, skips checkout' - default: false - -runs: - using: composite - steps: - - uses: actions/checkout@v4 - if: ${{ !inputs.skip-checkout }} - with: - sparse-checkout: | - WebUI - path: ${{ github.repository }} - - - uses: actions/setup-node@v3 - with: - node-version: ${{ inputs.node-version }} - cache: 'npm' - cache-dependency-path: ./WebUI/package-lock.json - - - name: Install dependencies - working-directory: ./WebUI - shell: bash - run: npm ci - - - name: Build - working-directory: ./WebUI - shell: bash - run: npm run build - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: captive-portal - path: WebUI/build/* - retention-days: 1 - if-no-files-found: error diff --git a/.github/actions/build-firmware/action.yml b/.github/actions/build-firmware/action.yml index 8a442db8..35029661 100644 --- a/.github/actions/build-firmware/action.yml +++ b/.github/actions/build-firmware/action.yml @@ -1,13 +1,15 @@ - name: build-firmware -description: Builds the firmware +description: Builds the firmware partitions and uploads them as an artifact inputs: python-version: - description: 'Python version to use' required: true + description: 'Python version to use' board: + required: true description: 'Board name to build' + version: required: true + description: 'Current firmware version' skip-checkout: description: 'If true, skips checkout' required: false @@ -30,9 +32,9 @@ runs: - uses: actions/setup-python@v4 with: python-version: ${{ inputs.python-version }} - cache: "pip" + cache: 'pip' - - name: Install dependencies + - name: Install python dependencies shell: bash run: pip install -r requirements.txt @@ -40,11 +42,16 @@ runs: working-directory: . shell: bash run: pio run -e ${{ inputs.board }} + env: + OPENSHOCK_FW_VERSION: ${{ inputs.version }} + OPENSHOCK_FW_GIT_REF: ${{ github.ref }} + OPENSHOCK_FW_GIT_COMMIT: ${{ github.sha }} + OPENSHOCK_FW_BUILD_DATE: ${{ github.event.head_commit.timestamp }} - - name: Upload internal firmware binaries - uses: actions/upload-artifact@v3 + - name: Upload build artifacts + uses: actions/upload-artifact@v4 with: - name: firmware_${{ inputs.board }} + name: firmware_build_${{ inputs.board }} path: .pio/build/${{ inputs.board }}/*.bin retention-days: 1 if-no-files-found: error diff --git a/.github/actions/build-frontend/action.yml b/.github/actions/build-frontend/action.yml new file mode 100644 index 00000000..b7a9be66 --- /dev/null +++ b/.github/actions/build-frontend/action.yml @@ -0,0 +1,43 @@ +name: build-frontend +description: Builds the frontend and uploads it as an artifact +inputs: + node-version: + description: 'NodeJS runtime version to use' + required: true + skip-checkout: + description: 'If true, skips checkout' + default: false + +runs: + using: composite + steps: + - uses: actions/checkout@v4 + if: ${{ !inputs.skip-checkout }} + with: + sparse-checkout: | + frontend + path: ${{ github.repository }} + + - uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: 'npm' + cache-dependency-path: ./frontend/package-lock.json + + - name: Install dependencies + working-directory: ./frontend + shell: bash + run: npm ci + + - name: Build + working-directory: ./frontend + shell: bash + run: npm run build + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: frontend + path: frontend/build/* + retention-days: 1 + if-no-files-found: error diff --git a/.github/actions/build-filesystem/action.yml b/.github/actions/build-staticfs/action.yml similarity index 66% rename from .github/actions/build-filesystem/action.yml rename to .github/actions/build-staticfs/action.yml index 17aab292..7e0b9a55 100644 --- a/.github/actions/build-filesystem/action.yml +++ b/.github/actions/build-staticfs/action.yml @@ -1,6 +1,5 @@ - -name: build-filesystem -description: Builds the filesystem. +name: build-staticfs +description: Builds the static filesystem partition and uploads it as an artifact inputs: python-version: description: 'Python version to use' @@ -27,30 +26,30 @@ runs: - uses: actions/setup-python@v4 with: python-version: ${{ inputs.python-version }} - cache: "pip" + cache: 'pip' - name: Install dependencies shell: bash run: pip install -r requirements.txt - - name: Download built captive portal - uses: actions/download-artifact@v3 + - name: Download built frontend + uses: actions/download-artifact@v4 with: - name: captive-portal - path: WebUI/build/ + name: frontend + path: frontend/build/ - - name: Build filesystem + - name: Build filesystem partition shell: bash run: pio run --target buildfs -e fs - - name: Rename filesystem binary + - name: Rename partition binary shell: bash - run: mv .pio/build/fs/littlefs.bin filesystem.bin + run: mv .pio/build/fs/littlefs.bin staticfs.bin - name: Upload internal filesystem artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: filesystem - path: filesystem.bin + name: firmware_staticfs + path: staticfs.bin retention-days: 1 if-no-files-found: error diff --git a/.github/actions/cdn-bump-version/action.yml b/.github/actions/cdn-bump-version/action.yml index dec90d5f..6f76f067 100644 --- a/.github/actions/cdn-bump-version/action.yml +++ b/.github/actions/cdn-bump-version/action.yml @@ -1,26 +1,22 @@ -name: cnd-upload-firmware -description: Uploads firmware artifacts to CDN +name: cdn-bump-version +description: Uploads version file to CDN inputs: cf-bucket: description: Name of the S3 bucket required: true version: - description: Version to upload + description: 'Version of the release' required: true release-channel: description: 'Release channel that describes this upload' - required: false - default: 'none' + required: true runs: using: composite steps: - - name: Upload version file shell: bash run: | - mkdir upload - cd upload - echo "${{ inputs.version }}" >> version-${{ inputs.release-channel }}.txt - rclone copy . cdn:${{ inputs.cf-bucket }}/ - + mkdir -p upload + echo "${{ inputs.version }}" >> upload/version-${{ inputs.release-channel }}.txt + rclone copy upload cdn:${{ inputs.cf-bucket }}/ diff --git a/.github/actions/cdn-prepare/action.yml b/.github/actions/cdn-prepare/action.yml index ae64a580..3f70b0c1 100644 --- a/.github/actions/cdn-prepare/action.yml +++ b/.github/actions/cdn-prepare/action.yml @@ -1,5 +1,5 @@ -name: cnd-upload -description: Uploads firmware artifacts to CDN +name: cdn-prepare +description: Prepares the CDN for firmware uploads inputs: cf-account-id: description: Cloudflare Account ID diff --git a/.github/actions/cdn-upload-board/action.yml b/.github/actions/cdn-upload-board/action.yml deleted file mode 100644 index d4d0f262..00000000 --- a/.github/actions/cdn-upload-board/action.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: cnd-upload-board -description: Uploads firmware artifacts to CDN -inputs: - cf-bucket: - description: Name of the S3 bucket - required: true - version: - description: Version to upload - required: true - release-channel: - description: 'Release channel that describes this upload' - required: false - default: 'none' - board: - description: 'Board to upload' - required: true - -runs: - using: composite - steps: - - name: Download filesystem image - uses: actions/download-artifact@v3 - with: - name: filesystem - path: . - - - name: Download firmware images - uses: actions/download-artifact@v3 - with: - name: firmware_${{ inputs.board }} - path: . - - - name: Download final artifact - uses: actions/download-artifact@v3 - with: - name: OpenShock_${{ inputs.board }} - path: . - - - name: Upload artifacts to CDN - shell: bash - run: | - mv OpenShock_*.bin OpenShock.bin - mkdir upload - mv *.bin upload/ - rclone copy upload cdn:${{ inputs.cf-bucket }}/${{ inputs.version }}/${{ inputs.board }}/ diff --git a/.github/actions/cdn-upload-firmware/action.yml b/.github/actions/cdn-upload-firmware/action.yml new file mode 100644 index 00000000..eb591ee4 --- /dev/null +++ b/.github/actions/cdn-upload-firmware/action.yml @@ -0,0 +1,52 @@ +name: cdn-upload-firmware +description: Uploads firmware partitions and merged binaries to CDN along with SHA256 checksums +inputs: + cf-bucket: + description: Name of the S3 bucket + required: true + fw-version: + description: Firmware version + required: true + board: + description: 'Board to upload' + required: true + +runs: + using: composite + steps: + - name: Download static filesystem partition + uses: actions/download-artifact@v4 + with: + name: firmware_staticfs + path: . + + - name: Download firmware partitions + uses: actions/download-artifact@v4 + with: + name: firmware_build_${{ inputs.board }} + path: . + + - name: Download merged firmware binary + uses: actions/download-artifact@v4 + with: + name: firmware_merged_${{ inputs.board }} + path: . + + - name: Rename firmware binaries + shell: bash + run: | + mv OpenShock_*.bin firmware.bin + + - name: Generate SHA256 checksums + shell: bash + run: | + find . -type f -name '*.bin' -exec md5sum {} \; > hashes.md5.txt + find . -type f -name '*.bin' -exec sha256sum {} \; > hashes.sha256.txt + + - name: Upload artifacts to CDN + shell: bash + run: | + mkdir -p upload + mv *.bin upload/ + mv hashes.*.txt upload/ + rclone copy upload 'cdn:${{ inputs.cf-bucket }}/${{ inputs.fw-version }}/${{ inputs.board }}/' diff --git a/.github/actions/cdn-upload-version-info/action.yml b/.github/actions/cdn-upload-version-info/action.yml index 1f639004..9c8aa569 100644 --- a/.github/actions/cdn-upload-version-info/action.yml +++ b/.github/actions/cdn-upload-version-info/action.yml @@ -1,11 +1,14 @@ -name: cnd-upload-version-info -description: Uploads firmware artifacts to CDN +name: cdn-upload-version-info +description: Uploads version specific info to CDN inputs: cf-bucket: description: Name of the S3 bucket required: true - version: - description: Version to upload + fw-version: + description: Firmware version + required: true + release-channel: + description: 'Release channel that describes this upload' required: true boards: description: 'List of boards, separated by newlines' @@ -22,6 +25,6 @@ runs: - name: Upload artifacts to CDN shell: bash run: | - mkdir upload + mkdir -p upload mv boards.txt upload/ - rclone copy upload cdn:${{ inputs.cf-bucket }}/${{ inputs.version }}/${{ inputs.board }}/ + rclone copy upload 'cdn:${{ inputs.cf-bucket }}/${{ inputs.fw-version }}/${{ inputs.board }}/' diff --git a/.github/actions/merge-images/action.yml b/.github/actions/merge-partitions/action.yml similarity index 52% rename from .github/actions/merge-images/action.yml rename to .github/actions/merge-partitions/action.yml index 5dcdf75b..6ec3fc03 100644 --- a/.github/actions/merge-images/action.yml +++ b/.github/actions/merge-partitions/action.yml @@ -1,11 +1,14 @@ -name: merge-images -description: Merges multiple images into a single flashable image +name: merge-partitions +description: Merges multiple partitions into a single flashable binary inputs: python-version: description: 'Python version to use' required: true + release-channel: + description: 'Release channel that describes this upload' + required: true board: - description: 'Board name to merge images for' + description: 'Board name to merge partitions for' required: true skip-checkout: description: 'If true, skips checkout' @@ -26,32 +29,32 @@ runs: - uses: actions/setup-python@v4 with: python-version: ${{ inputs.python-version }} - cache: "pip" + cache: 'pip' - name: Install dependencies shell: bash run: pip install esptool - - name: Download filesystem image - uses: actions/download-artifact@v3 + - name: Download static filesystem partition + uses: actions/download-artifact@v4 with: - name: filesystem + name: firmware_staticfs - - name: Download firmware images - uses: actions/download-artifact@v3 + - name: Download firmware partitions + uses: actions/download-artifact@v4 with: - name: firmware_${{ inputs.board }} + name: firmware_build_${{ inputs.board }} - - name: Merge flash image + - name: Merge partitions shell: bash run: | python scripts/merge_image.py ${{ inputs.board }} - mv merged.bin OpenShock_${{ inputs.board }}_${{ !contains(github.ref_name, '/') && github.ref_name || 'unknown' }}.bin + mv merged.bin OpenShock_${{ inputs.board }}_${{ inputs.release-channel }}.bin - - name: Upload merged flashable image - uses: actions/upload-artifact@v3 + - name: Upload merged firmware binary + uses: actions/upload-artifact@v4 with: - name: OpenShock_${{ inputs.board }} + name: firmware_merged_${{ inputs.board }} path: OpenShock_${{ inputs.board }}_*.bin retention-days: 7 if-no-files-found: error diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e7cbd31e..5dfc687e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,7 +14,7 @@ updates: # Check for npm updates - package-ecosystem: 'npm' - directory: '/WebUI' + directory: '/frontend' schedule: interval: 'weekly' day: 'monday' diff --git a/.github/scripts/.gitignore b/.github/scripts/.gitignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/.github/scripts/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/.github/scripts/get-vars.js b/.github/scripts/get-vars.js new file mode 100644 index 00000000..90cb22f2 --- /dev/null +++ b/.github/scripts/get-vars.js @@ -0,0 +1,238 @@ +const fs = require('fs'); +const ini = require('ini'); +const semver = require('semver'); +const core = require('@actions/core'); +const child_process = require('child_process'); + +// Get branch name +const gitRef = process.env.GITHUB_REF; +if (gitRef === undefined) { + core.setFailed('Environment variable "GITHUB_REF" not found'); + process.exit(); +} + +const isGitTag = gitRef.startsWith('refs/tags/'); +const isGitBranch = gitRef.startsWith('refs/heads/'); +const isGitPullRequest = gitRef.startsWith('refs/pull/') && gitRef.endsWith('/merge'); + +if (!isGitTag && !isGitBranch && !isGitPullRequest) { + core.setFailed(`Git ref "${gitRef}" is not a valid branch, tag or pull request`); + process.exit(); +} + +const gitCommitHash = process.env.GITHUB_SHA; +const gitShortCommitHash = child_process.execSync('git rev-parse --short HEAD').toString().trim(); + +if (gitCommitHash === undefined) { + core.setFailed('Environment variable "GITHUB_SHA" not found'); + process.exit(); +} + +const gitHeadRefName = isGitPullRequest ? process.env.GITHUB_HEAD_REF : gitRef.split('/')[2]; +if (gitHeadRefName === undefined) { + core.setFailed('Failed to get git head ref name'); + process.exit(); +} + +const gitTagsList = child_process.execSync('git for-each-ref --sort=-creatordate --format "%(refname:short)" refs/tags').toString().trim(); +if (gitTagsList === undefined) { + core.setFailed('Failed to get latest git tag'); + process.exit(); +} + +function convertGitTagToSemver(tag) { + const parsed = semver.parse(tag === '' ? '0.0.0' : tag); + if (parsed === null || parsed.loose) { + core.setFailed(`Git tag "${tag}" is not a valid semver version`); + process.exit(); + } + + return parsed; +} + +const gitTagsArray = gitTagsList.split('\n').map((tag) => tag.trim()); +const releasesArray = gitTagsArray.map(convertGitTagToSemver); +const latestRelease = isGitTag ? convertGitTagToSemver(gitRef.split('/')[2]) : releasesArray[0]; + +function isStableRelease(release) { + return release.prerelease.length === 0 || release.prerelease[0] === 'stable'; +} +function isBetaRelease(release) { + return release.prerelease.length > 0 && ['rc', 'beta'].includes(release.prerelease[0]); +} +function isDevRelease(release) { + return release.prerelease.length > 0 && ['dev', 'develop'].includes(release.prerelease[0]); +} + +const stableReleasesArray = releasesArray.filter(isStableRelease); +const betaReleasesArray = releasesArray.filter(isBetaRelease); +const devReleasesArray = releasesArray.filter(isDevRelease); + +// Build version string +let currentVersion = `${latestRelease.major}.${latestRelease.minor}.${latestRelease.patch}`; +if (!isGitTag) { + // Get last part of branch name and replace all non-alphanumeric characters with dashes + let sanitizedGitHeadRefName = gitHeadRefName + .split('/') + .pop() + .replace(/[^a-zA-Z0-9-]/g, '-'); + + // Remove leading and trailing dashes + sanitizedGitHeadRefName = sanitizedGitHeadRefName.replace(/^\-+|\-+$/g, ''); + + if (sanitizedGitHeadRefName.length > 0) { + currentVersion += `-${sanitizedGitHeadRefName}`; + } + + // Add the git commit hash to the version string + currentVersion += `+${gitShortCommitHash}`; +} else { + if (latestRelease.prerelease.length > 0) { + currentVersion += `-${latestRelease.prerelease.join('.')}`; + } + if (latestRelease.build.length > 0) { + currentVersion += `+${latestRelease.build.join('.')}`; + } +} + +// Get the channel to deploy to +let currentChannel; +if (gitHeadRefName === 'master' || (isGitTag && isStableRelease(latestRelease))) { + currentChannel = 'stable'; +} else if (gitHeadRefName === 'beta' || (isGitTag && isBetaRelease(latestRelease))) { + currentChannel = 'beta'; +} else if (gitHeadRefName === 'develop' || (isGitTag && isDevRelease(latestRelease))) { + currentChannel = 'develop'; +} else { + currentChannel = gitHeadRefName.replace(/[^a-zA-Z0-9-]/g, '-').replace(/^\-+|\-+$/g, ''); +} + +function getVersionChangeLog(lines) { + const emptyChangelog = lines.length === 0; + + // Enforce that the changelog is not empty if we are on the master branch + if (isGitTag && emptyChangelog) { + core.setFailed('File "CHANGELOG.md" is empty, this must be populated in the master branch'); + process.exit(); + } + + if (emptyChangelog) { + return ''; + } + + // Simple validation of the changelog + if (!lines[0].startsWith('# Version ')) { + core.setFailed('File "CHANGELOG.md" must start with "# Version " followed by a changelog entry'); + process.exit(); + } + + // Get the start of the entry + const changeLogBegin = lines.findIndex((line) => line.startsWith(`# Version ${currentVersion}`)); + if (isGitTag && changeLogBegin === -1) { + core.setFailed(`File "CHANGELOG.md" does not contain a changelog entry for version "${currentVersion}", this must be added in the master branch`); + process.exit(); + } + + // Enforce that the changelog entry is at the top of the file if we are on the master branch + if (isGitTag && changeLogBegin !== 0) { + core.setFailed(`Changelog entry for version "${currentVersion}" is not at the top of the file, you tag is either out of date or you have not updated the changelog`); + process.exit(); + } + + // Get the end of the entry + let changeLogEnd = lines.slice(changeLogBegin + 1).findIndex((line) => line.startsWith('# Version ')); + if (changeLogEnd === -1) { + changeLogEnd = lines.length; + } else { + changeLogEnd += changeLogBegin + 1; + } + + const emptyChangelogEntry = lines.slice(changeLogBegin + 1, changeLogEnd).filter((line) => line.trim() !== '').length === 0; + + // Enforce that the changelog entry is not empty if we are on the master branch + if (isGitTag && emptyChangelogEntry) { + core.setFailed(`Changelog entry for version "${currentVersion}" is empty, this must be populated in the master branch`); + process.exit(); + } + + return lines.slice(changeLogBegin + 1, changeLogEnd).join('\n'); +} + +// Make sure we have all the files we need +for (const file of ['RELEASE.md', 'CHANGELOG.md', 'platformio.ini']) { + if (!fs.existsSync(file)) { + core.setFailed(`File "${file}" not found`); + process.exit(); + } +} + +// Read files +let releaseNotes = fs.readFileSync('RELEASE.md', 'utf8'); +const fullChangelog = fs.readFileSync('CHANGELOG.md', 'utf8').trim(); +const platformioIniStr = fs.readFileSync('platformio.ini', 'utf8').trim(); + +const fullChangelogLines = fullChangelog.split('\n'); + +// Get all versions from the changelog +const changelogVersions = fullChangelogLines.filter((line) => line.startsWith('# Version ')).map((line) => line.substring(10).split(' ')[0].trim()); + +// Get the changelog for the current version +const versionChangeLog = getVersionChangeLog(fullChangelogLines); + +// Enforce that all tags exist in the changelog +let missingTags = []; +for (const tag of gitTagsArray) { + if (!changelogVersions.includes(tag)) { + missingTags.push(tag); + } +} +if (missingTags.length > 0) { + core.setFailed(`Changelog is missing the following tags: ${missingTags.join(', ')}`); + process.exit(); +} + +// Finish building the release string +if (versionChangeLog !== '') { + releaseNotes = `# OpenShock Firmware ${currentVersion}\n\n${versionChangeLog}\n\n${releaseNotes}`.trim(); +} else { + releaseNotes = `# OpenShock Firmware ${currentVersion}\n\n${releaseNotes}`.trim(); +} + +// Parse platformio.ini and extract the different boards +const platformioIni = ini.parse(platformioIniStr); + +// Get every key that starts with "env:", and that isnt "env:fs" (which is the filesystem) +const boards = Object.keys(platformioIni) + .filter((key) => key.startsWith('env:') && key !== 'env:fs') + .reduce((arr, key) => { + arr.push(key.substring(4)); + return arr; + }, []); + +const shouldDeploy = isGitTag || (isGitBranch && gitHeadRefName === 'develop'); + +console.log('Version: ' + currentVersion); +console.log('Channel: ' + currentChannel); +console.log('Boards: ' + boards.join(', ')); +console.log('Deploy: ' + shouldDeploy); +console.log('Tags: ' + gitTagsArray.join(', ')); +console.log('Stable: ' + stableReleasesArray.join(', ')); +console.log('Beta: ' + betaReleasesArray.join(', ')); +console.log('Dev: ' + devReleasesArray.join(', ')); + +// Set outputs +core.setOutput('version', currentVersion); +core.setOutput('changelog', versionChangeLog); +core.setOutput('release-notes', releaseNotes); +core.setOutput('release-channel', currentChannel); +core.setOutput('full-changelog', fullChangelog); +core.setOutput('board-list', boards.join('\n')); +core.setOutput('board-array', JSON.stringify(boards)); +core.setOutput('board-matrix', JSON.stringify({ board: boards })); +core.setOutput('should-deploy', shouldDeploy); +core.setOutput('release-stable-list', stableReleasesArray.join('\n')); +core.setOutput('release-stable-array', JSON.stringify(stableReleasesArray)); +core.setOutput('release-beta-list', betaReleasesArray.join('\n')); +core.setOutput('release-beta-array', JSON.stringify(betaReleasesArray)); +core.setOutput('release-dev-list', devReleasesArray.join('\n')); +core.setOutput('release-dev-array', JSON.stringify(devReleasesArray)); diff --git a/.github/scripts/package-lock.json b/.github/scripts/package-lock.json new file mode 100644 index 00000000..1c71ab3e --- /dev/null +++ b/.github/scripts/package-lock.json @@ -0,0 +1,267 @@ +{ + "name": "get-variables", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "get-variables", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/github": "^6.0.0", + "ini": "^4.1.1", + "semver": "^7.5.4" + } + }, + "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==", + "dependencies": { + "@actions/http-client": "^2.0.1", + "uuid": "^8.3.2" + } + }, + "node_modules/@actions/github": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.0.tgz", + "integrity": "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==", + "dependencies": { + "@actions/http-client": "^2.2.0", + "@octokit/core": "^5.0.1", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-rest-endpoint-methods": "^10.0.0" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.0.tgz", + "integrity": "sha512-q+epW0trjVUUHboliPb4UF9g2msf+w61b32tAkFEwL/IwP0DQWgbCMM0Hbe3e3WXSKz5VcUXbzJQgy8Hkra/Lg==", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.2.tgz", + "integrity": "sha512-cZUy1gUvd4vttMic7C0lwPed8IYXWYp8kHIMatyhY8t8n3Cpw2ILczkV5pGMPqef7v0bLo0pOHrEHarsau2Ydg==", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.0.0", + "@octokit/request": "^8.0.2", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.4.tgz", + "integrity": "sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==", + "dependencies": { + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz", + "integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", + "dependencies": { + "@octokit/request": "^8.0.1", + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", + "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.1.5.tgz", + "integrity": "sha512-WKTQXxK+bu49qzwv4qKbMMRXej1DU2gq017euWyKVudA6MldaSSQuxtz+vGbhxV4CjxpUxjZu6rM2wfc1FiWVg==", + "dependencies": { + "@octokit/types": "^12.4.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.2.0.tgz", + "integrity": "sha512-ePbgBMYtGoRNXDyKGvr9cyHjQ163PbwD0y1MkDJCpkO2YH4OeXX40c4wYHKikHGZcpGPbcRLuy0unPUuafco8Q==", + "dependencies": { + "@octokit/types": "^12.3.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/request": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.6.tgz", + "integrity": "sha512-YhPaGml3ncZC1NfXpP3WZ7iliL1ap6tLkAp6MvbK2fTTPytzVUyUesBBogcdMm86uRYO5rHaM1xIWxigWZ17MQ==", + "dependencies": { + "@octokit/endpoint": "^9.0.0", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz", + "integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==", + "dependencies": { + "@octokit/types": "^12.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz", + "integrity": "sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ==", + "dependencies": { + "@octokit/openapi-types": "^19.1.0" + } + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/undici": { + "version": "5.28.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.2.tgz", + "integrity": "sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "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", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/.github/scripts/package.json b/.github/scripts/package.json new file mode 100644 index 00000000..65b9030e --- /dev/null +++ b/.github/scripts/package.json @@ -0,0 +1,17 @@ +{ + "name": "get-variables", + "version": "1.0.0", + "description": "", + "main": "src/index.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/github": "^6.0.0", + "ini": "^4.1.1", + "semver": "^7.5.4" + } +} diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 4b69b4bc..24921a1e 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -2,9 +2,15 @@ on: push: branches: - master + - beta - develop + tags: + - '[0-9]+.[0-9]+.[0-9]+' + - '[0-9]+.[0-9]+.[0-9]+-*' pull_request: branches: + - master + - beta - develop types: [opened, reopened, synchronize] workflow_dispatch: # Manually invoked by user. @@ -12,22 +18,14 @@ on: name: ci-build env: - NODE_VERSION: 16 + NODE_VERSION: 18 PYTHON_VERSION: 3.12 - OPENSHOCK_API_DOMAIN: api.shocklink.net - # OPENSHOCK_FW_VERSION: - # - If this is branch "master" or "develop", we use "0.0.0-master" or "0.0.0-develop" respectively. - # - All other scenarios we use "0.0.0-unknown", as we cannot guarantee SemVer compliance by accepting any branch name. So this is the safe option. - OPENSHOCK_FW_VERSION: ${{ (contains(fromJSON('["master","develop"]'), github.ref_name) && format('0.0.0-{0}', github.ref_name)) || '0.0.0-unknown' }} - OPENSHOCK_FW_COMMIT: ${{ github.sha }} jobs: + getvars: + uses: ./.github/workflows/get-vars.yml - # Read platformio.ini and extract all specific targets. See the referenced file for more info. - get-targets: - uses: ./.github/workflows/get-targets.yml - - build-captive-portal: + build-frontend: runs-on: ubuntu-latest steps: @@ -35,29 +33,29 @@ jobs: with: sparse-checkout: | .github - WebUI + frontend - - uses: ./.github/actions/build-captive-portal + - uses: ./.github/actions/build-frontend with: node-version: ${{ env.NODE_VERSION }} - build-filesystem: + build-staticfs: runs-on: ubuntu-latest - needs: build-captive-portal + needs: build-frontend steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/build-filesystem + - uses: ./.github/actions/build-staticfs with: python-version: ${{ env.PYTHON_VERSION }} skip-checkout: true build-firmware: + needs: [getvars] runs-on: ubuntu-latest - needs: get-targets strategy: fail-fast: false - matrix: ${{ fromJSON(needs.get-targets.outputs.matrix) }} + matrix: ${{ fromJSON(needs.getvars.outputs.board-matrix) }} steps: - uses: actions/checkout@v4 @@ -66,14 +64,15 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} board: ${{ matrix.board }} + version: ${{ needs.getvars.outputs.version }} skip-checkout: true - merge-images: + merge-partitions: + needs: [getvars, build-staticfs, build-firmware] runs-on: ubuntu-latest - needs: [get-targets, build-filesystem, build-firmware] strategy: fail-fast: false - matrix: ${{ fromJSON(needs.get-targets.outputs.matrix )}} + matrix: ${{ fromJSON(needs.getvars.outputs.board-matrix) }} steps: - uses: actions/checkout@v4 @@ -84,14 +83,123 @@ jobs: boards chips - - uses: ./.github/actions/merge-images + - uses: ./.github/actions/merge-partitions with: python-version: ${{ env.PYTHON_VERSION }} + release-channel: ${{ needs.getvars.outputs.release-channel }} board: ${{ matrix.board }} skip-checkout: true checkpoint-build: runs-on: ubuntu-latest - needs: [build-filesystem, merge-images] + needs: [merge-partitions] steps: - run: echo "Builds checkpoint reached" + + cdn-upload-firmware: + needs: [getvars, checkpoint-build] + if: ${{ needs.getvars.outputs.should-deploy == 'true' }} + runs-on: ubuntu-latest + environment: cdn-firmware-r2 + strategy: + fail-fast: true + matrix: ${{ fromJson(needs.getvars.outputs.board-matrix) }} + + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: | + .github + + # Set up rclone for CDN uploads. + - uses: ./.github/actions/cdn-prepare + with: + cf-account-id: ${{ vars.S3_ACCOUNT_ID }} + cf-access-key-id: ${{ vars.S3_ACCESS_KEY_ID }} + cf-secret-access-key: ${{ secrets.S3_SECRET_ACCESS_KEY }} + + # Upload firmware to CDN. + - uses: ./.github/actions/cdn-upload-firmware + with: + cf-bucket: ${{ vars.S3_BUCKET }} + fw-version: ${{ needs.getvars.outputs.version }} + board: ${{ matrix.board }} + + cdn-upload-version-info: + needs: [getvars, checkpoint-build] + if: ${{ needs.getvars.outputs.should-deploy == 'true' }} + runs-on: ubuntu-latest + environment: cdn-firmware-r2 + + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: | + .github + + # Set up rclone for CDN uploads. + - uses: ./.github/actions/cdn-prepare + with: + cf-account-id: ${{ vars.S3_ACCOUNT_ID }} + cf-access-key-id: ${{ vars.S3_ACCESS_KEY_ID }} + cf-secret-access-key: ${{ secrets.S3_SECRET_ACCESS_KEY }} + + # Upload firmware to CDN. + - uses: ./.github/actions/cdn-upload-version-info + with: + cf-bucket: ${{ vars.S3_BUCKET }} + fw-version: ${{ needs.getvars.outputs.version }} + release-channel: ${{ needs.getvars.outputs.release-channel }} + boards: ${{ needs.getvars.outputs.board-list }} + + cdn-bump-version: + runs-on: ubuntu-latest + needs: [getvars, cdn-upload-firmware] # only after version is complete + environment: cdn-firmware-r2 + + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: | + .github + + # Set up rclone for CDN uploads. + - uses: ./.github/actions/cdn-prepare + with: + cf-account-id: ${{ vars.S3_ACCOUNT_ID }} + cf-access-key-id: ${{ vars.S3_ACCESS_KEY_ID }} + cf-secret-access-key: ${{ secrets.S3_SECRET_ACCESS_KEY }} + + # Upload firmware to CDN. + - uses: ./.github/actions/cdn-bump-version + with: + cf-bucket: ${{ vars.S3_BUCKET }} + version: ${{ needs.getvars.outputs.version }} + release-channel: ${{ needs.getvars.outputs.release-channel }} + + checkpoint-cdn: + runs-on: ubuntu-latest + needs: [cdn-upload-firmware, cdn-upload-version-info, cdn-bump-version] + steps: + - run: echo "CDN checkpoint reached" + + release: + needs: [getvars, checkpoint-cdn] + if: (needs.getvars.outputs.release-channel == 'stable' || needs.getvars.outputs.release-channel == 'beta') + runs-on: ubuntu-latest + + steps: + - name: Download release artifacts + uses: actions/download-artifact@v4 + + - name: Display artifacts + run: ls -R + + - name: Release + uses: ncipollo/release-action@v1 + with: + artifacts: '**/OpenShock_*.bin' + tag: ${{ needs.getvars.outputs.version }} + prerelease: ${{ needs.getvars.outputs.release-channel != 'stable' }} + artifactErrorsFailBuild: true + body: ${{ needs.getvars.outputs.release-notes }} diff --git a/.github/workflows/ci-tag.yml b/.github/workflows/ci-tag.yml deleted file mode 100644 index c68c6bcd..00000000 --- a/.github/workflows/ci-tag.yml +++ /dev/null @@ -1,203 +0,0 @@ -on: - push: - tags: - - '[0-9]+.[0-9]+.[0-9]+' - - '[0-9]+.[0-9]+.[0-9]+-*' - -name: ci-tag -run-name: "ci-tag: ${{ github.ref_name }}" - -env: - NODE_VERSION: 16 - PYTHON_VERSION: 3.12 - OPENSHOCK_API_DOMAIN: api.shocklink.net - # OPENSHOCK_FW_VERSION: - # - Since this is a tag push, we can use ref_name to get the tag name. - OPENSHOCK_FW_VERSION: ${{ github.ref_name }} - OPENSHOCK_FW_COMMIT: ${{ github.sha }} - -jobs: - - # Read platformio.ini and extract all specific targets. See the referenced file for more info. - get-targets: - uses: ./.github/workflows/get-targets.yml - - build-captive-portal: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - sparse-checkout: | - .github - WebUI - - - uses: ./.github/actions/build-captive-portal - with: - node-version: ${{ env.NODE_VERSION }} - skip-checkout: true - - build-filesystem: - runs-on: ubuntu-latest - needs: build-captive-portal - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/build-filesystem - with: - python-version: ${{ env.PYTHON_VERSION }} - skip-checkout: true - - build-firmware: - runs-on: ubuntu-latest - needs: get-targets - strategy: - fail-fast: false - matrix: ${{ fromJSON(needs.get-targets.outputs.matrix) }} - - steps: - - uses: actions/checkout@v4 - - - uses: ./.github/actions/build-firmware - with: - python-version: ${{ env.PYTHON_VERSION }} - board: ${{ matrix.board }} - skip-checkout: true - - merge-images: - runs-on: ubuntu-latest - needs: [get-targets, build-filesystem, build-firmware] - strategy: - fail-fast: false - matrix: ${{ fromJSON(needs.get-targets.outputs.matrix )}} - - steps: - - uses: actions/checkout@v4 - with: - path: . - sparse-checkout: | - .github - scripts - boards - chips - - - name: Merge images - uses: ./.github/actions/merge-images - with: - python-version: ${{ env.PYTHON_VERSION }} - board: ${{ matrix.board }} - skip-checkout: true - - checkpoint-build: - runs-on: ubuntu-latest - needs: [build-filesystem, merge-images] - steps: - - run: echo "Builds checkpoint reached" - - cdn-upload-boards: - runs-on: ubuntu-latest - needs: [get-targets, checkpoint-build] - environment: cdn-firmware-r2 - strategy: - fail-fast: true - matrix: ${{ fromJson(needs.get-targets.outputs.matrix )}} - - steps: - - uses: actions/checkout@v4 - with: - sparse-checkout: | - .github - - # Set up rclone for CDN uploads. - - uses: ./.github/actions/cdn-prepare - with: - cf-account-id: ${{ vars.S3_ACCOUNT_ID }} - cf-access-key-id: ${{ vars.S3_ACCESS_KEY_ID }} - cf-secret-access-key: ${{ secrets.S3_SECRET_ACCESS_KEY }} - - # Upload firmware to CDN. - - uses: ./.github/actions/cdn-upload-board - with: - cf-bucket: ${{ vars.S3_BUCKET }} - board: ${{ matrix.board }} - version: ${{ github.ref_name }} - release-channel: ${{ contains(github.ref_name, '-') && 'stable' || 'beta' }} - - cdn-upload-board-list: - runs-on: ubuntu-latest - needs: [get-targets, checkpoint-build] - environment: cdn-firmware-r2 - - steps: - - uses: actions/checkout@v4 - with: - sparse-checkout: | - .github - - # Set up rclone for CDN uploads. - - uses: ./.github/actions/cdn-prepare - with: - cf-account-id: ${{ vars.S3_ACCOUNT_ID }} - cf-access-key-id: ${{ vars.S3_ACCESS_KEY_ID }} - cf-secret-access-key: ${{ secrets.S3_SECRET_ACCESS_KEY }} - - # Upload firmware to CDN. - - uses: ./.github/actions/cdn-upload-version-info - with: - cf-bucket: ${{ vars.S3_BUCKET }} - version: ${{ github.ref_name }} - boards: ${{ needs.get-targets.outputs.board-list }} - - cdn-bump-version: - runs-on: ubuntu-latest - needs: [cdn-upload-boards] # only after version is complete - environment: cdn-firmware-r2 - - steps: - - uses: actions/checkout@v4 - with: - sparse-checkout: | - .github - - # Set up rclone for CDN uploads. - - uses: ./.github/actions/cdn-prepare - with: - cf-account-id: ${{ vars.S3_ACCOUNT_ID }} - cf-access-key-id: ${{ vars.S3_ACCESS_KEY_ID }} - cf-secret-access-key: ${{ secrets.S3_SECRET_ACCESS_KEY }} - - # Upload firmware to CDN. - - uses: ./.github/actions/cdn-bump-version - with: - cf-bucket: ${{ vars.S3_BUCKET }} - version: ${{ github.ref_name }} - release-channel: ${{ !contains(github.ref_name, '-') && 'stable' || 'beta' }} - - checkpoint-cdn: - runs-on: ubuntu-latest - needs: [checkpoint-build, cdn-upload-boards, cdn-upload-board-list, cdn-bump-version] - steps: - - run: echo "CDN checkpoint reached" - - release: - runs-on: ubuntu-latest - needs: [checkpoint-cdn] - - steps: - - name: Download release notes - uses: actions/checkout@v4 - with: - sparse-checkout: | - RELEASE.md - - - name: Download release artifacts - uses: actions/download-artifact@v3 - - - name: Release - uses: ncipollo/release-action@v1 - with: - artifacts: '**/OpenShock_*.bin' - tag: ${{ github.ref_name }} - prerelease: ${{ contains(github.ref_name, '-') }} - artifactErrorsFailBuild: true - bodyFile: RELEASE.md diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..dccdd083 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,96 @@ +name: 'CodeQL' + +on: + push: + branches: ['master'] + pull_request: + branches: ['master'] + schedule: + - cron: '0 6 * * 1' + +env: + OPENSHOCK_API_DOMAIN: api.shocklink.net + OPENSHOCK_FW_GIT_REF: ${{ github.ref }} + OPENSHOCK_FW_GIT_COMMIT: ${{ github.sha }} + OPENSHOCK_FW_BUILD_DATE: ${{ github.event.head_commit.timestamp }} + +jobs: + get-vars: + uses: ./.github/workflows/get-vars.yml + + analyze-js-py: + name: Analyze JS/PY + runs-on: 'ubuntu-latest' + needs: get-vars + timeout-minutes: 360 + permissions: + actions: read + contents: read + security-events: write + + env: + OPENSHOCK_FW_VERSION: ${{ needs.get-vars.outputs.version }} + + strategy: + fail-fast: false + matrix: + language: ['javascript-typescript', 'python'] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # Build stuff here + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: '/language:${{matrix.language}}' + + analyze-cpp: + name: Analyze C/C++ + runs-on: 'ubuntu-latest' + needs: get-vars + timeout-minutes: 360 + permissions: + actions: read + contents: read + security-events: write + + env: + language: 'c-cpp' + OPENSHOCK_FW_VERSION: ${{ needs.get-vars.outputs.version }} + + strategy: + fail-fast: false + matrix: + board: ${{ fromJson(needs.get-vars.outputs.board-array) }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ env.language }} + + - uses: ./.github/actions/build-firmware + with: + python-version: 3.12 + board: ${{ matrix.board }} + skip-checkout: true + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: '/language:${{ env.language }}' diff --git a/.github/workflows/get-targets.yml b/.github/workflows/get-targets.yml deleted file mode 100644 index 2a432425..00000000 --- a/.github/workflows/get-targets.yml +++ /dev/null @@ -1,73 +0,0 @@ - -# This is a bit of a silly workflow, but Github Workflow definitions -# do not let us easily reuse the strategy matrix used to trigger jobs -# per-board. This is a workaround to define everything in one file, and -# use the output in the multiple places we need it. -# -# Source: https://github.com/orgs/community/discussions/26284#discussioncomment-6701976 - -on: - workflow_call: - outputs: - board-list: - description: "Newline-separated list of boards" - value: ${{ jobs.get-targets.outputs.board-list }} - board-array: - description: "JSON array of boards" - value: ${{ jobs.get-targets.outputs.board-array }} - matrix: - description: "Strategy matrix with a single key 'board', containing a list of all boards." - value: ${{ jobs.get-targets.outputs.matrix }} - -name: targets - -jobs: - get-targets: - runs-on: ubuntu-latest - outputs: - board-array: ${{ steps.board-array.outputs.array }} - board-list: ${{ steps.board-list.outputs.list }} - matrix: ${{ steps.matrix.outputs.matrix }} - - steps: - - uses: actions/checkout@v4 - with: - sparse-checkout: platformio.ini - - # There's a lot going on here, so bear with me. - # - # sed: - # -n Suppresses normal output, - # "s/^\[env:\(.*\)]$/\1 Substitutes "[env:...]" with whatever value is at "..." - # /p" Prints out the substituted value (i.e. the value of "...") - # platformio.ini Reading from this file. - # - # jq: - # "--raw-input --slurp" Takes the previous output, - # -c Output in compact mode (no newlines or unnecessary spaces), - # split("\n") Splits it by line, turning it into an array, - # [ .[] | select(length > 0) Filters out empty lines (there is an empty trailing line usually), - # | select(. != "fs") ] Filters out the "fs" entry since that's only for building the filesystem, - # { board: ... } Wraps the whole thing into a JSON object with only a "board" key - # and the array in question as value. - # - # echo "matrix=$(...)" >> $GITHUB_OUTPUT Sets the value as job output with name "matrix". - # - # Referenced: https://unix.stackexchange.com/a/278377 - # Referenced: https://github.com/jqlang/jq/issues/563 - - name: Extract board array - id: board-array - run: | - echo "array=$(sed -n "s/^\[env:\(.*\)]$/\1/p" platformio.ini | jq --raw-input --slurp -c 'split("\n") | [ .[] | select(length > 0) | select(. != "fs") ]')" >> $GITHUB_OUTPUT - - - name: Build strategy matrix - id: matrix - run: | - echo "matrix=$(echo '${{ steps.board-array.outputs.array }}' | jq -c '{ board: . }')" >> $GITHUB_OUTPUT - - - name: Build board list - id: board-list - run: | - echo "list<> $GITHUB_OUTPUT - echo "$(echo '${{ steps.board-array.outputs.array }}' | jq -r '. | join("\n")' | sed 's|\\n|\n|g')" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT diff --git a/.github/workflows/get-vars.yml b/.github/workflows/get-vars.yml new file mode 100644 index 00000000..857b0452 --- /dev/null +++ b/.github/workflows/get-vars.yml @@ -0,0 +1,105 @@ +# This is a bit of a silly workflow, but Github Workflow definitions +# do not let us easily reuse the strategy matrix used to trigger jobs +# per-board. This is a workaround to define everything in one file, and +# use the output in the multiple places we need it. +# +# Source: https://github.com/orgs/community/discussions/26284#discussioncomment-6701976 + +on: + workflow_call: + inputs: + node-version: + type: string + default: '18' + description: 'The Node.js version to use' + outputs: + version: + description: 'The current version of the project, e.g. 1.0.0-rc.1+build.1' + value: ${{ jobs.get-vars.outputs.version }} + changelog: + description: 'The changelog for the current version' + value: ${{ jobs.get-vars.outputs.changelog }} + release-notes: + description: 'The release notes for the current version (changelog + contents of RELEASE.md)' + value: ${{ jobs.get-vars.outputs.release-notes }} + release-channel: + description: 'The release channel for the current version (stable, beta, dev)' + value: ${{ jobs.get-vars.outputs.release-channel }} + full-changelog: + description: 'The complete changelog for all versions ever released' + value: ${{ jobs.get-vars.outputs.full-changelog }} + board-list: + description: 'Newline-separated list of boards to build' + value: ${{ jobs.get-vars.outputs.board-list }} + board-array: + description: 'JSON array of boards to build' + value: ${{ jobs.get-vars.outputs.board-array }} + board-matrix: + description: 'JSON matrix of boards to build' + value: ${{ jobs.get-vars.outputs.board-matrix }} + should-deploy: + description: 'Whether to deploy the current version' + value: ${{ jobs.get-vars.outputs.should-deploy }} + release-stable-list: + description: 'Newline-separated list of stable releases' + value: ${{ jobs.get-vars.outputs.release-stable-list }} + release-stable-array: + description: 'JSON array of stable releases' + value: ${{ jobs.get-vars.outputs.release-stable-array }} + release-beta-list: + description: 'Newline-separated list of beta releases' + value: ${{ jobs.get-vars.outputs.release-beta-list }} + release-beta-array: + description: 'JSON array of beta releases' + value: ${{ jobs.get-vars.outputs.release-beta-array }} + release-dev-list: + description: 'Newline-separated list of dev releases' + value: ${{ jobs.get-vars.outputs.release-dev-list }} + release-dev-array: + description: 'JSON array of dev releases' + value: ${{ jobs.get-vars.outputs.release-dev-array }} + +name: get-vars + +jobs: + get-vars: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.get-vars.outputs.version }} + changelog: ${{ steps.get-vars.outputs.changelog }} + release-notes: ${{ steps.get-vars.outputs.release-notes }} + release-channel: ${{ steps.get-vars.outputs.release-channel }} + full-changelog: ${{ steps.get-vars.outputs.full-changelog }} + board-list: ${{ steps.get-vars.outputs.board-list }} + board-array: ${{ steps.get-vars.outputs.board-array }} + board-matrix: ${{ steps.get-vars.outputs.board-matrix }} + should-deploy: ${{ steps.get-vars.outputs.should-deploy }} + release-stable-list: ${{ steps.get-vars.outputs.release-stable-list }} + release-stable-array: ${{ steps.get-vars.outputs.release-stable-array }} + release-beta-list: ${{ steps.get-vars.outputs.release-beta-list }} + release-beta-array: ${{ steps.get-vars.outputs.release-beta-array }} + release-dev-list: ${{ steps.get-vars.outputs.release-dev-list }} + release-dev-array: ${{ steps.get-vars.outputs.release-dev-array }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + sparse-checkout: | + .github + + - uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: 'npm' + cache-dependency-path: ./.github/scripts/package-lock.json + + - name: Install dependencies + working-directory: ./.github/scripts + shell: bash + run: npm ci + + - name: Get variables + id: get-vars + shell: bash + run: node ./.github/scripts/get-vars.js diff --git a/.gitignore b/.gitignore index e67a2fd2..daa26996 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .pio .vscode/.browse.c_cpp.db* .vscode/c_cpp_properties.json +.vscode/extensions.json .vscode/launch.json .vscode/ipch *.bin diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 7f9c4c45..00000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "recommendations": [ - "ardenivanov.svelte-intellisense", - "bradlc.vscode-tailwindcss", - "dbaeumer.vscode-eslint", - "editorconfig.editorconfig", - "esbenp.prettier-vscode", - "fivethree.vscode-svelte-snippets", - "gaborv.flatbuffers", - "ms-python.black-formatter", - "ms-vscode.cpptools", - "ms-vscode.cpptools-extension-pack", - "platformio.platformio-ide", - "svelte.svelte-vscode", - "xaver.clang-format" - ] -} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..146213f7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,131 @@ +# Version 1.1.0-rc.6 Release Notes + +Bugfixes: + +- Reduced latency, allocations, and network traffic for reporting wifi network scan results, making the networks instantly available in the frontend and improving the reliability of the captive portal. + +# Version 1.1.0-rc.5 Release Notes + +Bugfixes: + +- Fix what firmware boot type firmware reports to server. + +# Version 1.1.0-rc.4 Release Notes + +Bugfixes: + +- Make OTA update status reporting smoother. + +# Version 1.1.0-rc.3 Release Notes + +Bugfixes: + +- Fixed updateID not being sent with BootStatus message. + +# Version 1.1.0-rc.2 Release Notes + +This is the RC (Release Candidate) 2 for version 1.1.0 + +We did a couple of bugfixes: + +- Fixed User-Agent header not being set on websocket connections. +- Stopped frontend from requesting to connect to a secured network without a password. +- Do sanity checking on pairing code length in firmware to return a proper error message early. +- Fixed some SemVer parsing logic. + +# Version 1.1.0-rc.1 Release Notes + +It's been a while, and we think it's time for another beta release :smile: + +This is the RC (Release Candidate) 1 for version 1.1.0, hence the naming: 1.1.0-rc.1 + +This update is packed with some major enhancements and numerous improvements that we believe will improve your experience using your device. + +From introducing seamless Over-The-Air updates, adding support for new hardware, to enhancing the overall functionality and stability of the system, we've worked hard to improve upon our last release. + +We've also squashed some pesky bugs and made various minor updates to streamline and optimize your experience. + +Here’s what’s new: + +## Major Updates + +- **OTA (Over-The-Air) Updates**: + - Introduced a seamless OTA update capability, device can now be updated with the click of a button. + - Features: + - Updates can be triggered remotely via the OpenShock website. + - Device can automatically check for updates at a configured interval. + - Update state will be streamed back to frontend so you can see the status of your device in real time. + - Provided an option to deactivate each of these features individually through the Captive Portal for users preferring manual control. +- **Support for OpenShock Core V1**: Added support for @nullstalgia custom PCB, which is [fully open-source](https://github.com/nullstalgia/OpenShock-Hardware/tree/main/Core). +- **Captive Behavior Enhancement**: Phones and PC's now prompt the user with the Captive Portal upon WiFi connection. +- **More serial commands**: + - Read/Write configuration in JSON or raw binary format. + - Shocker command execution via serial. + - GPIO pin listing, excluding reserved pins. + - Serial command echo support for terminals lacking this feature. +- **Reworked partitions:** Moved configuration into its own partition, ensuring it persists across updates +- **Reserved pins**: + - Added support for reserved pins so users can no longer use reserved pins that might lead to ESP instability. + - User will now receive an error upon trying to set anything to use these pins. + - The available pins can be listed via the `AvailGPIO` serial command. +- **Shocker keepalive**: + - Improved shocker responsiveness by sending keepalive messages to them at a interval. + - This will prevent the shockers from entering sleep mode. + - Has option to be disabled via the `keepalive` serial command. +- **Config Handler Overhaul**: + - Rewrote config handler to be more modular and make it easier to expand upon the code base. + - Each config section is now seperated into its own file and class. + +## Minor Updates + +- Firmware upload now includes an MD5 sum. +- Enhanced reliability of WiFi scanning. +- Status LEDs: + - Reworked some code for LED pattern and state management. + - Added support for WS2812B RGB (Gamer :sunglasses:) LEDs. +- Improved WiFi connectivity speed post-setup. +- Updated Captive Portal color palette. +- Dependency cleanup: + - Removed ArduinoJSON + - Removed nonstd/span +- Removed Arduino-style loop behaviors, replaced with freeRTOS tasks. +- Improved FreeRTOS task management. +- Implemented self-ratelimiting on httpclient. +- Enhanced error checking in captive portal and firmware. +- CodeQL code quality checks integrated into CI/CD pipeline. +- Utilized filesystem partition hash as ETag for content caching. +- Improved logs to be more consise and verbose. +- Miscellaneous code cleanup, refactoring, and optimizations. + +## Bug Fixes + +- Resolved issue with WiFi scans getting stuck. +- Fixed connection problems with unsecured networks. +- Altered CommandHandler to use a queue kill message, preventing panic when deleting a mid-listening queue. +- Fixed ESP becoming unresponsive when the looptask would get deleted by captive portal deconstructor due to a missing null check. + +# Version 1.0.0 + +- We now support **six different boards**: + - Pishock (2023) + - Pishock Lite (2021) + - Seeed Xiao ESP32S3 + - Wemos D1 Mini ESP32 + - Wemos Lolin S2 Mini + - Wemos Lolin S3 +- All communication is now **websocket based** and using flatbuffers, allowing for even lower latency between the server and the ESP, with lower resource consumption. +- The **Captive Portal** got a MASSIVE overhaul; +- Serial commands have gotten alot better. +- Improved board stability and configurability. +- Added support for having a E-Stop (emergency stop) connected to ESP as a panic button. Thanks to @nullstalgia ❤️ +- And _much, much_ more behind the scenes, including bugfixes and code cleanup. + +# Version 1.0.0-rc.4 + +# Version 1.0.0-rc.3 + +# Version 1.0.0-rc.2 + +# Version 1.0.0-rc.1 + +# Version v0.8.1 diff --git a/README.md b/README.md index a29b5e0f..045fa202 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,34 @@ -# OpenShock (ESP-32) Firmware +# OpenShock Espressif Firmware -ESP-32 Firmware for OpenShock. Controlling shockers via 433 MHz RF. +Espressif Firmware for OpenShock. + +Controlling shockers via Reverse engineered proprietary Sub-1 GHz Protocols. ## Compatible Hardware -You will need a ESP-32 and a 433 MHz antenna attached to it. For more info about buying such hardware see here [OpenShock Wiki - Hardware: Buy](https://docs.shocklink.net/en/Hardware/Buy). -Guide for assembly can be found here [OpenShock Wiki - Hardware: Assembly](https://docs.shocklink.net/en/Hardware/Assembly) +You will need a ESP-32 and a 433 MHz antenna attached to it. + +For more info about buying such hardware see here [OpenShock Wiki - Vendors: Hardware](https://wiki.openshock.org/vendors/hardware/). + +Guide for assembly can be found here [OpenShock Wiki - DIY: Assembling](https://wiki.openshock.org/diy/assembling/) -Confirmed working ESP's: +Confirmed working boards: -- Wemos Mini D1 ESP32-WROOM-32 +- PiShock + - 2021 Q3 + - 2023 +- Seeed + - Xiao ESP32S3 +- Wemos + - D1 Mini + - Lolin S2 Mini + - Lolin S3 +- OpenShock (Legacy) + - Core V1 ## Flashing -Refer to [OpenShock Wiki - Device: Flashing](https://docs.shocklink.net/en/Device/Setup/Flashing) on how to flash your micro controller. +Refer to [OpenShock Wiki - Guides: First time setup](https://wiki.openshock.org/guides/openshock-first-setup/) on how to set up your microcontroller. Other than that, you can just flash via platform io in vscode. More in the contribute section. diff --git a/RELEASE.md b/RELEASE.md index de132a1a..fe4b9ade 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,4 +1,4 @@ -# OpenShock Firmware +## Flashing the firmware Download `OpenShock_[board]_[version].bin` and flash it to your microcontroller: diff --git a/WebUI/package-lock.json b/WebUI/package-lock.json deleted file mode 100644 index 0610f442..00000000 --- a/WebUI/package-lock.json +++ /dev/null @@ -1,7633 +0,0 @@ -{ - "name": "webui", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "webui", - "version": "0.0.1", - "dependencies": { - "@floating-ui/dom": "1.5.3" - }, - "devDependencies": { - "@playwright/test": "^1.39.0", - "@skeletonlabs/skeleton": "2.4.0", - "@skeletonlabs/tw-plugin": "0.2.3", - "@sveltejs/adapter-static": "^2.0.3", - "@sveltejs/kit": "^1.27.3", - "@tailwindcss/forms": "0.5.6", - "@tailwindcss/typography": "0.5.10", - "@types/node": "20.8.10", - "@typescript-eslint/eslint-plugin": "^6.9.1", - "@typescript-eslint/parser": "^6.9.1", - "autoprefixer": "10.4.16", - "eslint": "^8.53.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-svelte": "^2.34.1", - "flatbuffers": "^23.5.26", - "npm-check-updates": "^16.14.6", - "postcss": "8.4.31", - "prettier": "^3.0.3", - "prettier-plugin-svelte": "^3.0.3", - "svelte": "^4.2.2", - "svelte-check": "^3.5.2", - "tailwindcss": "3.3.5", - "tslib": "^2.6.2", - "typescript": "^5.2.2", - "vite": "^4.5.0", - "vite-plugin-tailwind-purgecss": "0.1.3", - "vitest": "^0.34.6" - } - }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.2.tgz", - "integrity": "sha512-0MGxAVt1m/ZK+LTJp/j0qF7Hz97D9O/FH9Ms3ltnyIdDD57cbb1ACIQTkbHvNXtWDv5TPq7w5Kq56+cNukbo7g==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", - "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@fastify/busboy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", - "integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", - "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", - "dependencies": { - "@floating-ui/utils": "^0.1.3" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", - "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", - "dependencies": { - "@floating-ui/core": "^1.4.2", - "@floating-ui/utils": "^0.1.3" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.4.tgz", - "integrity": "sha512-qprfWkn82Iw821mcKofJ5Pk9wgioHicxcQMxx+5zt5GSKoqdWvgG5AxVmpmUUjzTLPVSH5auBrhI93Deayn/DA==" - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", - "dev": true - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@npmcli/fs": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", - "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", - "dev": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/git": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz", - "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==", - "dev": true, - "dependencies": { - "@npmcli/promise-spawn": "^6.0.0", - "lru-cache": "^7.4.4", - "npm-pick-manifest": "^8.0.0", - "proc-log": "^3.0.0", - "promise-inflight": "^1.0.1", - "promise-retry": "^2.0.1", - "semver": "^7.3.5", - "which": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/@npmcli/git/node_modules/which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", - "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/installed-package-contents": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.0.2.tgz", - "integrity": "sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ==", - "dev": true, - "dependencies": { - "npm-bundled": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "bin": { - "installed-package-contents": "lib/index.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/node-gyp": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", - "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/promise-spawn": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz", - "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==", - "dev": true, - "dependencies": { - "which": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/promise-spawn/node_modules/which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", - "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/run-script": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-6.0.2.tgz", - "integrity": "sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==", - "dev": true, - "dependencies": { - "@npmcli/node-gyp": "^3.0.0", - "@npmcli/promise-spawn": "^6.0.0", - "node-gyp": "^9.0.0", - "read-package-json-fast": "^3.0.0", - "which": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/run-script/node_modules/which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", - "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@playwright/test": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", - "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", - "dev": true, - "dependencies": { - "playwright": "1.39.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@pnpm/config.env-replace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", - "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", - "dev": true, - "engines": { - "node": ">=12.22.0" - } - }, - "node_modules/@pnpm/network.ca-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", - "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", - "dev": true, - "dependencies": { - "graceful-fs": "4.2.10" - }, - "engines": { - "node": ">=12.22.0" - } - }, - "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true - }, - "node_modules/@pnpm/npm-conf": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.2.2.tgz", - "integrity": "sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==", - "dev": true, - "dependencies": { - "@pnpm/config.env-replace": "^1.1.0", - "@pnpm/network.ca-file": "^1.0.1", - "config-chain": "^1.1.11" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.23", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", - "integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==", - "dev": true - }, - "node_modules/@sigstore/bundle": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-1.1.0.tgz", - "integrity": "sha512-PFutXEy0SmQxYI4texPw3dd2KewuNqv7OuK1ZFtY2fM754yhvG2KdgwIhRnoEE2uHdtdGNQ8s0lb94dW9sELog==", - "dev": true, - "dependencies": { - "@sigstore/protobuf-specs": "^0.2.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@sigstore/protobuf-specs": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.2.1.tgz", - "integrity": "sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@sigstore/sign": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-1.0.0.tgz", - "integrity": "sha512-INxFVNQteLtcfGmcoldzV6Je0sbbfh9I16DM4yJPw3j5+TFP8X6uIiA18mvpEa9yyeycAKgPmOA3X9hVdVTPUA==", - "dev": true, - "dependencies": { - "@sigstore/bundle": "^1.1.0", - "@sigstore/protobuf-specs": "^0.2.0", - "make-fetch-happen": "^11.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@sigstore/tuf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-1.0.3.tgz", - "integrity": "sha512-2bRovzs0nJZFlCN3rXirE4gwxCn97JNjMmwpecqlbgV9WcxX7WRuIrgzx/X7Ib7MYRbyUTpBYE0s2x6AmZXnlg==", - "dev": true, - "dependencies": { - "@sigstore/protobuf-specs": "^0.2.0", - "tuf-js": "^1.1.7" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, - "node_modules/@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@skeletonlabs/skeleton": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@skeletonlabs/skeleton/-/skeleton-2.4.0.tgz", - "integrity": "sha512-8IVspCMarg1h4yMV6GFSaVqh9jIeQXV3XPNUnVONjkUofHkcKuLYTR4jGlR1ICF5CmIl+z746q3+Cj8rLAtAag==", - "dev": true, - "dependencies": { - "esm-env": "1.0.0" - }, - "peerDependencies": { - "svelte": "^3.56.0 || ^4.0.0" - } - }, - "node_modules/@skeletonlabs/tw-plugin": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@skeletonlabs/tw-plugin/-/tw-plugin-0.2.3.tgz", - "integrity": "sha512-sOavIPm6NlE0h4dmTZ4NDMe8ryqb1wh2hd59V9oP1qf2H1O6cwfKkiBUyLQLedw+suCey/FJ3MHpScTBMejNxQ==", - "dev": true, - "peerDependencies": { - "tailwindcss": ">=3.0.0" - } - }, - "node_modules/@sveltejs/adapter-static": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-2.0.3.tgz", - "integrity": "sha512-VUqTfXsxYGugCpMqQv1U0LIdbR3S5nBkMMDmpjGVJyM6Q2jHVMFtdWJCkeHMySc6mZxJ+0eZK3T7IgmUCDrcUQ==", - "dev": true, - "peerDependencies": { - "@sveltejs/kit": "^1.5.0" - } - }, - "node_modules/@sveltejs/kit": { - "version": "1.27.3", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.27.3.tgz", - "integrity": "sha512-pd7qwX6ww5noA0/FLk45B0aKUeOXWR+pfZsGTrv3dRmj3lTmnki9UTmTdWzHJGrje+BBkGUZHfgGrsSOQQBQpQ==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@sveltejs/vite-plugin-svelte": "^2.4.1", - "@types/cookie": "^0.5.1", - "cookie": "^0.5.0", - "devalue": "^4.3.1", - "esm-env": "^1.0.0", - "kleur": "^4.1.5", - "magic-string": "^0.30.0", - "mrmime": "^1.0.1", - "sade": "^1.8.1", - "set-cookie-parser": "^2.6.0", - "sirv": "^2.0.2", - "tiny-glob": "^0.2.9", - "undici": "~5.26.2" - }, - "bin": { - "svelte-kit": "svelte-kit.js" - }, - "engines": { - "node": "^16.14 || >=18" - }, - "peerDependencies": { - "svelte": "^3.54.0 || ^4.0.0-next.0", - "vite": "^4.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.4.6.tgz", - "integrity": "sha512-zO79p0+DZnXPnF0ltIigWDx/ux7Ni+HRaFOw720Qeivc1azFUrJxTl0OryXVibYNx1hCboGia1NRV3x8RNv4cA==", - "dev": true, - "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^1.0.4", - "debug": "^4.3.4", - "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.3", - "svelte-hmr": "^0.15.3", - "vitefu": "^0.2.4" - }, - "engines": { - "node": "^14.18.0 || >= 16" - }, - "peerDependencies": { - "svelte": "^3.54.0 || ^4.0.0", - "vite": "^4.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-1.0.4.tgz", - "integrity": "sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": "^14.18.0 || >= 16" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^2.2.0", - "svelte": "^3.54.0 || ^4.0.0", - "vite": "^4.0.0" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", - "dev": true, - "dependencies": { - "defer-to-connect": "^2.0.1" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/@tailwindcss/forms": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.6.tgz", - "integrity": "sha512-Fw+2BJ0tmAwK/w01tEFL5TiaJBX1NLT1/YbWgvm7ws3Qcn11kiXxzNTEQDMs5V3mQemhB56l3u0i9dwdzSQldA==", - "dev": true, - "dependencies": { - "mini-svg-data-uri": "^1.2.3" - }, - "peerDependencies": { - "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" - } - }, - "node_modules/@tailwindcss/typography": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz", - "integrity": "sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==", - "dev": true, - "dependencies": { - "lodash.castarray": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.merge": "^4.6.2", - "postcss-selector-parser": "6.0.10" - }, - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" - } - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tufjs/canonical-json": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz", - "integrity": "sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@tufjs/models": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-1.0.4.tgz", - "integrity": "sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A==", - "dev": true, - "dependencies": { - "@tufjs/canonical-json": "1.0.0", - "minimatch": "^9.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@tufjs/models/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@types/chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==", - "dev": true - }, - "node_modules/@types/chai-subset": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", - "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", - "dev": true, - "dependencies": { - "@types/chai": "*" - } - }, - "node_modules/@types/cookie": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.2.tgz", - "integrity": "sha512-DBpRoJGKJZn7RY92dPrgoMew8xCWc2P71beqsjyhEI/Ds9mOyVmBwtekyfhpwFIVt1WrxTonFifiOZ62V8CnNA==", - "dev": true - }, - "node_modules/@types/estree": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz", - "integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==", - "dev": true - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz", - "integrity": "sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw==", - "dev": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", - "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", - "dev": true - }, - "node_modules/@types/node": { - "version": "20.8.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", - "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", - "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/pug": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.7.tgz", - "integrity": "sha512-I469DU0UXNC1aHepwirWhu9YKg5fkxohZD95Ey/5A7lovC+Siu+MCLffva87lnfThaOrw9Vb1DUN5t55oULAAw==", - "dev": true - }, - "node_modules/@types/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==", - "dev": true - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.1.tgz", - "integrity": "sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.9.1", - "@typescript-eslint/type-utils": "6.9.1", - "@typescript-eslint/utils": "6.9.1", - "@typescript-eslint/visitor-keys": "6.9.1", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.1.tgz", - "integrity": "sha512-C7AK2wn43GSaCUZ9do6Ksgi2g3mwFkMO3Cis96kzmgudoVaKyt62yNzJOktP0HDLb/iO2O0n2lBOzJgr6Q/cyg==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "6.9.1", - "@typescript-eslint/types": "6.9.1", - "@typescript-eslint/typescript-estree": "6.9.1", - "@typescript-eslint/visitor-keys": "6.9.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.9.1.tgz", - "integrity": "sha512-38IxvKB6NAne3g/+MyXMs2Cda/Sz+CEpmm+KLGEM8hx/CvnSRuw51i8ukfwB/B/sESdeTGet1NH1Wj7I0YXswg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.9.1", - "@typescript-eslint/visitor-keys": "6.9.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.9.1.tgz", - "integrity": "sha512-eh2oHaUKCK58qIeYp19F5V5TbpM52680sB4zNSz29VBQPTWIlE/hCj5P5B1AChxECe/fmZlspAWFuRniep1Skg==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "6.9.1", - "@typescript-eslint/utils": "6.9.1", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.9.1.tgz", - "integrity": "sha512-BUGslGOb14zUHOUmDB2FfT6SI1CcZEJYfF3qFwBeUrU6srJfzANonwRYHDpLBuzbq3HaoF2XL2hcr01c8f8OaQ==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.1.tgz", - "integrity": "sha512-U+mUylTHfcqeO7mLWVQ5W/tMLXqVpRv61wm9ZtfE5egz7gtnmqVIw9ryh0mgIlkKk9rZLY3UHygsBSdB9/ftyw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.9.1", - "@typescript-eslint/visitor-keys": "6.9.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.9.1.tgz", - "integrity": "sha512-L1T0A5nFdQrMVunpZgzqPL6y2wVreSyHhKGZryS6jrEN7bD9NplVAyMryUhXsQ4TWLnZmxc2ekar/lSGIlprCA==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.9.1", - "@typescript-eslint/types": "6.9.1", - "@typescript-eslint/typescript-estree": "6.9.1", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.1.tgz", - "integrity": "sha512-MUaPUe/QRLEffARsmNfmpghuQkW436DvESW+h+M52w0coICHRfD6Np9/K6PdACwnrq1HmuLl+cSPZaJmeVPkSw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.9.1", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, - "node_modules/@vitest/expect": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", - "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", - "dev": true, - "dependencies": { - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", - "chai": "^4.3.10" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", - "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", - "dev": true, - "dependencies": { - "@vitest/utils": "0.34.6", - "p-limit": "^4.0.0", - "pathe": "^1.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/snapshot": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", - "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", - "dev": true, - "dependencies": { - "magic-string": "^0.30.1", - "pathe": "^1.1.1", - "pretty-format": "^29.5.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", - "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", - "dev": true, - "dependencies": { - "tinyspy": "^2.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", - "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", - "dev": true, - "dependencies": { - "diff-sequences": "^29.4.3", - "loupe": "^2.3.6", - "pretty-format": "^29.5.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agentkeepalive": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", - "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", - "dev": true, - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "dev": true, - "dependencies": { - "string-width": "^4.1.0" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "dev": true - }, - "node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "dev": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.16", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", - "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001538", - "fraction.js": "^4.3.6", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/axobject-query": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", - "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", - "dev": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/boxen": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", - "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", - "dev": true, - "dependencies": { - "ansi-align": "^3.0.1", - "camelcase": "^7.0.1", - "chalk": "^5.2.0", - "cli-boxes": "^3.0.0", - "string-width": "^5.1.2", - "type-fest": "^2.13.0", - "widest-line": "^4.0.1", - "wrap-ansi": "^8.1.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/boxen/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/boxen/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/boxen/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/boxen/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.21.11", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.11.tgz", - "integrity": "sha512-xn1UXOKUz7DjdGlg9RrUr0GGiWzI97UQJnugHtH0OLDfJB7jMgoIkYvRIEO1l9EeEERVqeqLYOcFBW9ldjypbQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001538", - "electron-to-chromium": "^1.4.526", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/builtins": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", - "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", - "dev": true, - "dependencies": { - "semver": "^7.0.0" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacache": { - "version": "17.1.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", - "integrity": "sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==", - "dev": true, - "dependencies": { - "@npmcli/fs": "^3.1.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^7.7.1", - "minipass": "^7.0.3", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/cacheable-lookup": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", - "dev": true, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/cacheable-request": { - "version": "10.2.13", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.13.tgz", - "integrity": "sha512-3SD4rrMu1msNGEtNSt8Od6enwdo//U9s4ykmXfA2TD58kcLkCobtCDiby7kNyj7a/Q7lz/mAesAFI54rTdnvBA==", - "dev": true, - "dependencies": { - "@types/http-cache-semantics": "^4.0.1", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.3", - "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001539", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001539.tgz", - "integrity": "sha512-hfS5tE8bnNiNvEOEkm8HElUHroYwlqMMENEzELymy77+tJ6m+gA2krtHl5hxJaj71OlpC2cHZbdSMX1/YEqEkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chai": { - "version": "4.3.10", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", - "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", - "dev": true, - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-boxes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-table3": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", - "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, - "node_modules/code-red": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", - "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", - "@types/estree": "^1.0.1", - "acorn": "^8.10.0", - "estree-walker": "^3.0.3", - "periscopic": "^3.1.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/config-chain": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", - "dev": true, - "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "node_modules/config-chain/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "node_modules/configstore": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz", - "integrity": "sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==", - "dev": true, - "dependencies": { - "dot-prop": "^6.0.1", - "graceful-fs": "^4.2.6", - "unique-string": "^3.0.0", - "write-file-atomic": "^3.0.3", - "xdg-basedir": "^5.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/yeoman/configstore?sponsor=1" - } - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true - }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypto-random-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", - "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", - "dev": true, - "dependencies": { - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/crypto-random-string/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/devalue": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.2.tgz", - "integrity": "sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg==", - "dev": true - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dot-prop": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", - "dev": true, - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "node_modules/electron-to-chromium": { - "version": "1.4.529", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.529.tgz", - "integrity": "sha512-6uyPyXTo8lkv8SWAmjKFbG42U073TXlzD4R8rW3EzuznhFS2olCIAfjjQtV2dV2ar/vRF55KUd3zQYnCB0dd3A==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true - }, - "node_modules/es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", - "dev": true - }, - "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-goat": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", - "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", - "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.53.0", - "@humanwhocodes/config-array": "^0.11.13", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.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": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-prettier": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", - "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", - "dev": true, - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-svelte": { - "version": "2.34.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.34.1.tgz", - "integrity": "sha512-HnLzYevh9bLL0Rj2d4dmZY9EutN0BL5JsJRHqtJFIyaEmdxxd3ZuY5zNoSjIFhctFMSntsClbd6TwYjgaOY0Xw==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@jridgewell/sourcemap-codec": "^1.4.14", - "debug": "^4.3.1", - "esutils": "^2.0.3", - "known-css-properties": "^0.29.0", - "postcss": "^8.4.5", - "postcss-load-config": "^3.1.4", - "postcss-safe-parser": "^6.0.0", - "postcss-selector-parser": "^6.0.11", - "semver": "^7.5.3", - "svelte-eslint-parser": ">=0.33.0 <1.0.0" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0-0", - "svelte": "^3.37.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "svelte": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-svelte/node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esm-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", - "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", - "dev": true - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", - "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", - "dev": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fast-memoize": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", - "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", - "dev": true, - "dependencies": { - "flatted": "^3.2.7", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/flatbuffers": { - "version": "23.5.26", - "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-23.5.26.tgz", - "integrity": "sha512-vE+SI9vrJDwi1oETtTIFldC/o9GsVKRM+s6EL0nQgxXlYV1Vc4Tk30hj4xGICftInKQKj1F3up2n8UbIVobISQ==", - "dev": true - }, - "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true - }, - "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", - "dev": true, - "engines": { - "node": ">= 14.17" - } - }, - "node_modules/fp-and-or": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/fp-and-or/-/fp-and-or-0.1.4.tgz", - "integrity": "sha512-+yRYRhpnFPWXSly/6V4Lw9IfOV26uu30kynGJ03PW+MnjOEQe45RZ141QcS0aJehYBYA50GfCDnsRbFJdhssRw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/fraction.js": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", - "integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==", - "dev": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", - "dev": true, - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "dev": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/get-stdin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", - "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/global-dirs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", - "dev": true, - "dependencies": { - "ini": "2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/global-dirs/node_modules/ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalyzer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", - "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", - "dev": true - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true - }, - "node_modules/got": { - "version": "12.6.1", - "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", - "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", - "dev": true, - "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true - }, - "node_modules/has-yarn": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", - "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/hosted-git-info": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", - "integrity": "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==", - "dev": true, - "dependencies": { - "lru-cache": "^7.5.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true - }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http2-wrapper": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", - "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", - "dev": true, - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dev": true, - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/ignore-walk": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.3.tgz", - "integrity": "sha512-C7FfFoTA+bI10qfeydT8aZbvr91vAEU+2W5BZUlzPec47oNb07SsOfwYrtxuvOYdUApPP/Qlh4DtAO51Ekk2QA==", - "dev": true, - "dependencies": { - "minimatch": "^9.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/ignore-walk/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/ignore-walk/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-lazy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", - "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", - "dev": true - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-installed-globally": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", - "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", - "dev": true, - "dependencies": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "dev": true - }, - "node_modules/is-npm": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.0.0.tgz", - "integrity": "sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "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", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "dev": true, - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true - }, - "node_modules/is-yarn-global": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz", - "integrity": "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jiti": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", - "integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==", - "dev": true, - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/jju": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", - "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-parse-even-better-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", - "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/json-parse-helpfulerror": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", - "integrity": "sha512-XgP0FGR77+QhUxjXkwOMkC94k3WtqEBfcnjWqhRd82qTat4SWKRE+9kUnynz/shm3I4ea2+qISvTIeGTNU7kJg==", - "dev": true, - "dependencies": { - "jju": "^1.1.0" - } - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, - "node_modules/jsonlines": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsonlines/-/jsonlines-0.1.1.tgz", - "integrity": "sha512-ekDrAGso79Cvf+dtm+mL8OBI2bmAOt3gssYs833De/C9NmIpWDWyUO4zPgB5x2/OhY366dkhgfPMYfwZF7yOZA==", - "dev": true - }, - "node_modules/jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", - "dev": true, - "engines": [ - "node >= 0.2.0" - ] - }, - "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/known-css-properties": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz", - "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==", - "dev": true - }, - "node_modules/latest-version": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", - "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", - "dev": true, - "dependencies": { - "package-json": "^8.1.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/local-pkg": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", - "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lodash.castarray": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", - "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", - "dev": true - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/loupe": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", - "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.0" - } - }, - "node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/make-fetch-happen": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", - "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", - "dev": true, - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^17.0.0", - "http-cache-semantics": "^4.1.1", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^5.0.0", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^10.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/mini-svg-data-uri": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", - "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", - "dev": true, - "bin": { - "mini-svg-data-uri": "cli.js" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-collect/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-fetch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz", - "integrity": "sha512-jHAqnA728uUpIaFm7NWsCnqKT6UqZz7GcI/bDpPATuwYyKwJwW0remxSCxUlKiEty+eopHGa3oc8WxgQ1FFJqg==", - "dev": true, - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-fetch/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-json-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", - "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", - "dev": true, - "dependencies": { - "jsonparse": "^1.3.1", - "minipass": "^3.0.0" - } - }, - "node_modules/minipass-json-stream/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mlly": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", - "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", - "dev": true, - "dependencies": { - "acorn": "^8.10.0", - "pathe": "^1.1.1", - "pkg-types": "^1.0.3", - "ufo": "^1.3.0" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/mrmime": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", - "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-gyp": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.0.tgz", - "integrity": "sha512-dMXsYP6gc9rRbejLXmTbVRYjAHw7ppswsKyMxuxJxxOHzluIO1rGp9TOQgjFJ+2MCqcOcQTOPB/8Xwhr+7s4Eg==", - "dev": true, - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^11.0.3", - "nopt": "^6.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^12.13 || ^14.13 || >=16" - } - }, - "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", - "dev": true - }, - "node_modules/nopt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", - "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", - "dev": true, - "dependencies": { - "abbrev": "^1.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/normalize-package-data": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", - "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", - "dev": true, - "dependencies": { - "hosted-git-info": "^6.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/normalize-package-data/node_modules/hosted-git-info": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", - "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", - "dev": true, - "dependencies": { - "lru-cache": "^7.5.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/normalize-package-data/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-bundled": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.0.tgz", - "integrity": "sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ==", - "dev": true, - "dependencies": { - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-check-updates": { - "version": "16.14.6", - "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.6.tgz", - "integrity": "sha512-sJ6w4AmSDP7YzBXah94Ul2JhiIbjBDfx9XYgib15um2wtiQkOyjE7Lov3MNUSQ84Ry7T81mE4ynMbl/mGbK4HQ==", - "dev": true, - "dependencies": { - "chalk": "^5.3.0", - "cli-table3": "^0.6.3", - "commander": "^10.0.1", - "fast-memoize": "^2.5.2", - "find-up": "5.0.0", - "fp-and-or": "^0.1.4", - "get-stdin": "^8.0.0", - "globby": "^11.0.4", - "hosted-git-info": "^5.1.0", - "ini": "^4.1.1", - "js-yaml": "^4.1.0", - "json-parse-helpfulerror": "^1.0.3", - "jsonlines": "^0.1.1", - "lodash": "^4.17.21", - "make-fetch-happen": "^11.1.1", - "minimatch": "^9.0.3", - "p-map": "^4.0.0", - "pacote": "15.2.0", - "parse-github-url": "^1.0.2", - "progress": "^2.0.3", - "prompts-ncu": "^3.0.0", - "rc-config-loader": "^4.1.3", - "remote-git-tags": "^3.0.0", - "rimraf": "^5.0.5", - "semver": "^7.5.4", - "semver-utils": "^1.1.4", - "source-map-support": "^0.5.21", - "spawn-please": "^2.0.2", - "strip-ansi": "^7.1.0", - "strip-json-comments": "^5.0.1", - "untildify": "^4.0.0", - "update-notifier": "^6.0.2" - }, - "bin": { - "ncu": "build/src/bin/cli.js", - "npm-check-updates": "build/src/bin/cli.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/npm-check-updates/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/npm-check-updates/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/npm-check-updates/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/npm-check-updates/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/npm-check-updates/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm-check-updates/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm-check-updates/node_modules/rimraf": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", - "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", - "dev": true, - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm-check-updates/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/npm-check-updates/node_modules/strip-json-comments": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.1.tgz", - "integrity": "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-install-checks": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.2.0.tgz", - "integrity": "sha512-744wat5wAAHsxa4590mWO0tJ8PKxR8ORZsH9wGpQc3nWTzozMAgBN/XyqYw7mg3yqLM8dLwEnwSfKMmXAjF69g==", - "dev": true, - "dependencies": { - "semver": "^7.1.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-normalize-package-bin": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", - "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-package-arg": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz", - "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^6.0.0", - "proc-log": "^3.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^5.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-package-arg/node_modules/hosted-git-info": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", - "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", - "dev": true, - "dependencies": { - "lru-cache": "^7.5.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-package-arg/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/npm-packlist": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-7.0.4.tgz", - "integrity": "sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==", - "dev": true, - "dependencies": { - "ignore-walk": "^6.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-pick-manifest": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.2.tgz", - "integrity": "sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==", - "dev": true, - "dependencies": { - "npm-install-checks": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0", - "npm-package-arg": "^10.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-registry-fetch": { - "version": "14.0.5", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz", - "integrity": "sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA==", - "dev": true, - "dependencies": { - "make-fetch-happen": "^11.0.0", - "minipass": "^5.0.0", - "minipass-fetch": "^3.0.0", - "minipass-json-stream": "^1.0.1", - "minizlib": "^2.1.2", - "npm-package-arg": "^10.0.0", - "proc-log": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "dev": true, - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-cancelable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", - "dev": true, - "engines": { - "node": ">=12.20" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", - "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", - "dev": true, - "dependencies": { - "got": "^12.1.0", - "registry-auth-token": "^5.0.1", - "registry-url": "^6.0.0", - "semver": "^7.3.7" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pacote": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.2.0.tgz", - "integrity": "sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA==", - "dev": true, - "dependencies": { - "@npmcli/git": "^4.0.0", - "@npmcli/installed-package-contents": "^2.0.1", - "@npmcli/promise-spawn": "^6.0.1", - "@npmcli/run-script": "^6.0.0", - "cacache": "^17.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^5.0.0", - "npm-package-arg": "^10.0.0", - "npm-packlist": "^7.0.0", - "npm-pick-manifest": "^8.0.0", - "npm-registry-fetch": "^14.0.0", - "proc-log": "^3.0.0", - "promise-retry": "^2.0.1", - "read-package-json": "^6.0.0", - "read-package-json-fast": "^3.0.0", - "sigstore": "^1.3.0", - "ssri": "^10.0.0", - "tar": "^6.1.11" - }, - "bin": { - "pacote": "lib/bin.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-github-url": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.2.tgz", - "integrity": "sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==", - "dev": true, - "bin": { - "parse-github-url": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "dev": true, - "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pathe": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", - "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", - "dev": true - }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/periscopic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", - "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^3.0.0", - "is-reference": "^3.0.0" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", - "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", - "dev": true, - "dependencies": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" - } - }, - "node_modules/playwright": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", - "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", - "dev": true, - "dependencies": { - "playwright-core": "1.39.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", - "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", - "dev": true, - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dev": true, - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.11" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-nested/node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-safe-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", - "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", - "dev": true, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.3.3" - } - }, - "node_modules/postcss-scss": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.8.tgz", - "integrity": "sha512-Cr0X8Eu7xMhE96PJck6ses/uVVXDtE5ghUTKNUYgm8ozgP2TkgV3LWs3WgLV1xaSSLq8ZFiXaUrj0LVgG1fGEA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss-scss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.4.29" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-plugin-svelte": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.0.3.tgz", - "integrity": "sha512-dLhieh4obJEK1hnZ6koxF+tMUrZbV5YGvRpf2+OADyanjya5j0z1Llo8iGwiHmFWZVG/hLEw/AJD5chXd9r3XA==", - "dev": true, - "peerDependencies": { - "prettier": "^3.0.0", - "svelte": "^3.2.0 || ^4.0.0-next.0" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/proc-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", - "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prompts-ncu": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/prompts-ncu/-/prompts-ncu-3.0.0.tgz", - "integrity": "sha512-qyz9UxZ5MlPKWVhWrCmSZ1ahm2GVYdjLb8og2sg0IPth1KRuhcggHGuijz0e41dkx35p1t1q3GRISGH7QGALFA==", - "dev": true, - "dependencies": { - "kleur": "^4.0.1", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "dev": true - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/pupa": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz", - "integrity": "sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==", - "dev": true, - "dependencies": { - "escape-goat": "^4.0.0" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/purgecss": { - "version": "6.0.0-alpha.0", - "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-6.0.0-alpha.0.tgz", - "integrity": "sha512-UC7d7uIyZsky+srEsSXny9BkbTcVn3ZtBCNX3rW3DsqJKhvUXFRpufA4ktcHzWF0+JLZgmsqjUm/8R82x9bHpw==", - "dev": true, - "dependencies": { - "commander": "^10.0.0", - "glob": "^8.0.3", - "postcss": "^8.4.4", - "postcss-selector-parser": "^6.0.7" - }, - "bin": { - "purgecss": "bin/purgecss.js" - } - }, - "node_modules/purgecss/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/purgecss/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/purgecss/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/purgecss/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc-config-loader": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.3.tgz", - "integrity": "sha512-kD7FqML7l800i6pS6pvLyIE2ncbk9Du8Q0gp/4hMPhJU6ZxApkoLcGD8ZeqgiAlfwZ6BlETq6qqe+12DUL207w==", - "dev": true, - "dependencies": { - "debug": "^4.3.4", - "js-yaml": "^4.1.0", - "json5": "^2.2.2", - "require-from-string": "^2.0.2" - } - }, - "node_modules/rc/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/read-package-json": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.4.tgz", - "integrity": "sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==", - "dev": true, - "dependencies": { - "glob": "^10.2.2", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^5.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/read-package-json-fast": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", - "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", - "dev": true, - "dependencies": { - "json-parse-even-better-errors": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/read-package-json/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/read-package-json/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/read-package-json/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/registry-auth-token": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz", - "integrity": "sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==", - "dev": true, - "dependencies": { - "@pnpm/npm-conf": "^2.1.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/registry-url": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", - "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", - "dev": true, - "dependencies": { - "rc": "1.2.8" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remote-git-tags": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/remote-git-tags/-/remote-git-tags-3.0.0.tgz", - "integrity": "sha512-C9hAO4eoEsX+OXA4rla66pXZQ+TLQ8T9dttgQj18yuKlPMTVkIkdYXvlMC55IuUsIkV6DpmQYi10JKFLaU+l7w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", - "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/responselike": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", - "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", - "dev": true, - "dependencies": { - "lowercase-keys": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "3.29.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.3.tgz", - "integrity": "sha512-T7du6Hum8jOkSWetjRgbwpM6Sy0nECYrYRSmZjayFcOddtKJWU4d17AC3HNUk7HRuqy4p+G7aEZclSHytqUmEg==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "optional": true - }, - "node_modules/sander": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", - "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", - "dev": true, - "dependencies": { - "es6-promise": "^3.1.2", - "graceful-fs": "^4.1.3", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.2" - } - }, - "node_modules/sander/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", - "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", - "dev": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semver-utils": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/semver-utils/-/semver-utils-1.1.4.tgz", - "integrity": "sha512-EjnoLE5OGmDAVV/8YDoN5KiajNadjzIp9BAHOhYeQHt7j0UWxjmgsx4YD48wp4Ue1Qogq38F1GNUJNqF1kKKxA==", - "dev": true - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true - }, - "node_modules/set-cookie-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", - "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", - "dev": true - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/sigstore": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-1.9.0.tgz", - "integrity": "sha512-0Zjz0oe37d08VeOtBIuB6cRriqXse2e8w+7yIy2XSXjshRKxbc2KkhXjL229jXSxEm7UbcjS76wcJDGQddVI9A==", - "dev": true, - "dependencies": { - "@sigstore/bundle": "^1.1.0", - "@sigstore/protobuf-specs": "^0.2.0", - "@sigstore/sign": "^1.0.0", - "@sigstore/tuf": "^1.0.3", - "make-fetch-happen": "^11.0.1" - }, - "bin": { - "sigstore": "bin/sigstore.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/sirv": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.3.tgz", - "integrity": "sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==", - "dev": true, - "dependencies": { - "@polka/url": "^1.0.0-next.20", - "mrmime": "^1.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", - "dev": true, - "dependencies": { - "ip": "^2.0.0", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.13.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", - "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", - "dev": true, - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/sorcery": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", - "integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.14", - "buffer-crc32": "^0.2.5", - "minimist": "^1.2.0", - "sander": "^0.5.0" - }, - "bin": { - "sorcery": "bin/sorcery" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/spawn-please": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/spawn-please/-/spawn-please-2.0.2.tgz", - "integrity": "sha512-KM8coezO6ISQ89c1BzyWNtcn2V2kAVtwIXd3cN/V5a0xPYc1F/vydrRc01wsKFEQ/p+V1a4sw4z2yMITIXrgGw==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.15.tgz", - "integrity": "sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==", - "dev": true - }, - "node_modules/ssri": { - "version": "10.0.5", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz", - "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==", - "dev": true, - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/ssri/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true - }, - "node_modules/std-env": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.4.3.tgz", - "integrity": "sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==", - "dev": true - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-literal": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", - "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", - "dev": true, - "dependencies": { - "acorn": "^8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/sucrase": { - "version": "3.34.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", - "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "7.1.6", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svelte": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.2.tgz", - "integrity": "sha512-My2tytF2e2NnHSpn2M7/3VdXT4JdTglYVUuSuK/mXL2XtulPYbeBfl8Dm1QiaKRn0zoULRnL+EtfZHHP0k4H3A==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.1", - "@jridgewell/sourcemap-codec": "^1.4.15", - "@jridgewell/trace-mapping": "^0.3.18", - "acorn": "^8.9.0", - "aria-query": "^5.3.0", - "axobject-query": "^3.2.1", - "code-red": "^1.0.3", - "css-tree": "^2.3.1", - "estree-walker": "^3.0.3", - "is-reference": "^3.0.1", - "locate-character": "^3.0.0", - "magic-string": "^0.30.4", - "periscopic": "^3.1.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/svelte-check": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.5.2.tgz", - "integrity": "sha512-5a/YWbiH4c+AqAUP+0VneiV5bP8YOk9JL3jwvN+k2PEPLgpu85bjQc5eE67+eIZBBwUEJzmO3I92OqKcqbp3fw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", - "chokidar": "^3.4.1", - "fast-glob": "^3.2.7", - "import-fresh": "^3.2.1", - "picocolors": "^1.0.0", - "sade": "^1.7.4", - "svelte-preprocess": "^5.0.4", - "typescript": "^5.0.3" - }, - "bin": { - "svelte-check": "bin/svelte-check" - }, - "peerDependencies": { - "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0" - } - }, - "node_modules/svelte-eslint-parser": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.0.tgz", - "integrity": "sha512-5awZ6Bs+Tb/zQwa41PSdcLynAVQTwW0HGyCBjtbAQ59taLZqDgQSMzRlDmapjZdDtzERm0oXDZNE0E+PKJ6ryg==", - "dev": true, - "dependencies": { - "eslint-scope": "^7.0.0", - "eslint-visitor-keys": "^3.0.0", - "espree": "^9.0.0", - "postcss": "^8.4.28", - "postcss-scss": "^4.0.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, - "peerDependencies": { - "svelte": "^3.37.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "svelte": { - "optional": true - } - } - }, - "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/svelte-eslint-parser/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/svelte-hmr": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", - "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", - "dev": true, - "engines": { - "node": "^12.20 || ^14.13.1 || >= 16" - }, - "peerDependencies": { - "svelte": "^3.19.0 || ^4.0.0" - } - }, - "node_modules/svelte-preprocess": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.4.tgz", - "integrity": "sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@types/pug": "^2.0.6", - "detect-indent": "^6.1.0", - "magic-string": "^0.27.0", - "sorcery": "^0.11.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">= 14.10.0" - }, - "peerDependencies": { - "@babel/core": "^7.10.2", - "coffeescript": "^2.5.1", - "less": "^3.11.3 || ^4.0.0", - "postcss": "^7 || ^8", - "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0", - "pug": "^3.0.0", - "sass": "^1.26.8", - "stylus": "^0.55.0", - "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", - "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0", - "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "coffeescript": { - "optional": true - }, - "less": { - "optional": true - }, - "postcss": { - "optional": true - }, - "postcss-load-config": { - "optional": true - }, - "pug": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/svelte-preprocess/node_modules/magic-string": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", - "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/tailwindcss": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", - "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==", - "dev": true, - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss/node_modules/postcss-load-config": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", - "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", - "dev": true, - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^2.1.1" - }, - "engines": { - "node": ">= 14" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/tailwindcss/node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", - "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", - "dev": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tiny-glob": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", - "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", - "dev": true, - "dependencies": { - "globalyzer": "0.1.0", - "globrex": "^0.1.2" - } - }, - "node_modules/tinybench": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.1.tgz", - "integrity": "sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==", - "dev": true - }, - "node_modules/tinypool": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", - "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.1.tgz", - "integrity": "sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/ts-api-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", - "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", - "dev": true, - "engines": { - "node": ">=16.13.0" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true - }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, - "node_modules/tuf-js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-1.1.7.tgz", - "integrity": "sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg==", - "dev": true, - "dependencies": { - "@tufjs/models": "1.0.4", - "debug": "^4.3.4", - "make-fetch-happen": "^11.1.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.1.tgz", - "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==", - "dev": true - }, - "node_modules/undici": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.5.tgz", - "integrity": "sha512-cSb4bPFd5qgR7qr2jYAi0hlX9n5YKK2ONKkLFkxl+v/9BvC0sOpZjBHDBSXc5lWAf5ty9oZdRXytBIHzgUcerw==", - "dev": true, - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, - "engines": { - "node": ">=14.0" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true - }, - "node_modules/unique-filename": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", - "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", - "dev": true, - "dependencies": { - "unique-slug": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/unique-slug": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", - "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/unique-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", - "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", - "dev": true, - "dependencies": { - "crypto-random-string": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/update-notifier": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-6.0.2.tgz", - "integrity": "sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==", - "dev": true, - "dependencies": { - "boxen": "^7.0.0", - "chalk": "^5.0.1", - "configstore": "^6.0.0", - "has-yarn": "^3.0.0", - "import-lazy": "^4.0.0", - "is-ci": "^3.0.1", - "is-installed-globally": "^0.4.0", - "is-npm": "^6.0.0", - "is-yarn-global": "^0.4.0", - "latest-version": "^7.0.0", - "pupa": "^3.1.0", - "semver": "^7.3.7", - "semver-diff": "^4.0.0", - "xdg-basedir": "^5.1.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/yeoman/update-notifier?sponsor=1" - } - }, - "node_modules/update-notifier/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/validate-npm-package-name": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", - "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", - "dev": true, - "dependencies": { - "builtins": "^5.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/vite": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", - "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", - "dev": true, - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", - "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", - "dev": true, - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "mlly": "^1.4.0", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": ">=v14.18.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-plugin-tailwind-purgecss": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/vite-plugin-tailwind-purgecss/-/vite-plugin-tailwind-purgecss-0.1.3.tgz", - "integrity": "sha512-VVz9fwKBEEFSbj/rKxtwtczvoSrIqbzbo6S+MT7gH0CsmKNwlx947VMoV8B085ocxGCuFlddOPRDszNXLi2nTQ==", - "dev": true, - "dependencies": { - "estree-walker": "^3.0.3", - "purgecss": "6.0.0-alpha.0" - }, - "peerDependencies": { - "vite": "^4.1.1" - } - }, - "node_modules/vitefu": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.4.tgz", - "integrity": "sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==", - "dev": true, - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", - "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", - "dev": true, - "dependencies": { - "@types/chai": "^4.3.5", - "@types/chai-subset": "^1.3.3", - "@types/node": "*", - "@vitest/expect": "0.34.6", - "@vitest/runner": "0.34.6", - "@vitest/snapshot": "0.34.6", - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", - "acorn": "^8.9.0", - "acorn-walk": "^8.2.0", - "cac": "^6.7.14", - "chai": "^4.3.10", - "debug": "^4.3.4", - "local-pkg": "^0.4.3", - "magic-string": "^0.30.1", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.3.3", - "strip-literal": "^1.0.1", - "tinybench": "^2.5.0", - "tinypool": "^0.7.0", - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", - "vite-node": "0.34.6", - "why-is-node-running": "^2.2.2" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": ">=v14.18.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@vitest/browser": "*", - "@vitest/ui": "*", - "happy-dom": "*", - "jsdom": "*", - "playwright": "*", - "safaridriver": "*", - "webdriverio": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "playwright": { - "optional": true - }, - "safaridriver": { - "optional": true - }, - "webdriverio": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", - "dev": true, - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/widest-line": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", - "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", - "dev": true, - "dependencies": { - "string-width": "^5.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/widest-line/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/widest-line/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/widest-line/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/widest-line/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "node_modules/xdg-basedir": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", - "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/WebUI/package.json b/WebUI/package.json deleted file mode 100644 index d84ccd0d..00000000 --- a/WebUI/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "webui", - "version": "0.0.1", - "private": true, - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "test": "npm run test:integration && npm run test:unit", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "prettier --plugin-search-dir . --check . && eslint .", - "format": "prettier --plugin-search-dir . --write .", - "test:integration": "playwright test", - "test:unit": "vitest" - }, - "devDependencies": { - "@playwright/test": "^1.39.0", - "@skeletonlabs/skeleton": "2.4.0", - "@skeletonlabs/tw-plugin": "0.2.3", - "@sveltejs/adapter-static": "^2.0.3", - "@sveltejs/kit": "^1.27.3", - "@tailwindcss/forms": "0.5.6", - "@tailwindcss/typography": "0.5.10", - "@types/node": "20.8.10", - "@typescript-eslint/eslint-plugin": "^6.9.1", - "@typescript-eslint/parser": "^6.9.1", - "autoprefixer": "10.4.16", - "eslint": "^8.53.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-svelte": "^2.34.1", - "flatbuffers": "^23.5.26", - "npm-check-updates": "^16.14.6", - "postcss": "8.4.31", - "prettier": "^3.0.3", - "prettier-plugin-svelte": "^3.0.3", - "svelte": "^4.2.2", - "svelte-check": "^3.5.2", - "tailwindcss": "3.3.5", - "tslib": "^2.6.2", - "typescript": "^5.2.2", - "vite": "^4.5.0", - "vite-plugin-tailwind-purgecss": "0.1.3", - "vitest": "^0.34.6" - }, - "type": "module", - "dependencies": { - "@floating-ui/dom": "1.5.3" - } -} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization.ts b/WebUI/src/lib/_fbs/open-shock/serialization.ts deleted file mode 100644 index 0bc9cfe5..00000000 --- a/WebUI/src/lib/_fbs/open-shock/serialization.ts +++ /dev/null @@ -1,8 +0,0 @@ -// automatically generated by the FlatBuffers compiler, do not modify - -export { CaptivePortalConfig } from './serialization/captive-portal-config.js'; -export { ServerToDeviceMessage } from './serialization/server-to-device-message.js'; -export { ServerToDeviceMessagePayload } from './serialization/server-to-device-message-payload.js'; -export { ShockerCommand } from './serialization/shocker-command.js'; -export { ShockerCommandList } from './serialization/shocker-command-list.js'; -export * as Types from './open-shock/serialization/types.js'; diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/configuration.ts b/WebUI/src/lib/_fbs/open-shock/serialization/configuration.ts deleted file mode 100644 index eb922d92..00000000 --- a/WebUI/src/lib/_fbs/open-shock/serialization/configuration.ts +++ /dev/null @@ -1,9 +0,0 @@ -// automatically generated by the FlatBuffers compiler, do not modify - -export { BSSID } from './serialization/configuration/bssid.js'; -export { BackendConfig } from './serialization/configuration/backend-config.js'; -export { CaptivePortalConfig } from './serialization/configuration/captive-portal-config.js'; -export { Config } from './serialization/configuration/config.js'; -export { RFConfig } from './serialization/configuration/rfconfig.js'; -export { WiFiConfig } from './serialization/configuration/wi-fi-config.js'; -export { WiFiCredentials } from './serialization/configuration/wi-fi-credentials.js'; diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/configuration/bssid.ts b/WebUI/src/lib/_fbs/open-shock/serialization/configuration/bssid.ts deleted file mode 100644 index a762a285..00000000 --- a/WebUI/src/lib/_fbs/open-shock/serialization/configuration/bssid.ts +++ /dev/null @@ -1,33 +0,0 @@ -// automatically generated by the FlatBuffers compiler, do not modify - -import * as flatbuffers from 'flatbuffers'; - -export class BSSID { - bb: flatbuffers.ByteBuffer|null = null; - bb_pos = 0; - __init(i:number, bb:flatbuffers.ByteBuffer):BSSID { - this.bb_pos = i; - this.bb = bb; - return this; -} - -array(index: number):number|null { - return this.bb!.readUint8(this.bb_pos + 0 + index); -} - -static sizeOf():number { - return 6; -} - -static createBSSID(builder:flatbuffers.Builder, array: number[]|null):flatbuffers.Offset { - builder.prep(1, 6); - - for (let i = 5; i >= 0; --i) { - builder.writeInt8((array?.[i] ?? 0)); - - } - - return builder.offset(); -} - -} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/configuration/captive-portal-config.ts b/WebUI/src/lib/_fbs/open-shock/serialization/configuration/captive-portal-config.ts deleted file mode 100644 index d9134e50..00000000 --- a/WebUI/src/lib/_fbs/open-shock/serialization/configuration/captive-portal-config.ts +++ /dev/null @@ -1,28 +0,0 @@ -// automatically generated by the FlatBuffers compiler, do not modify - -import * as flatbuffers from 'flatbuffers'; - -export class CaptivePortalConfig { - bb: flatbuffers.ByteBuffer|null = null; - bb_pos = 0; - __init(i:number, bb:flatbuffers.ByteBuffer):CaptivePortalConfig { - this.bb_pos = i; - this.bb = bb; - return this; -} - -alwaysEnabled():boolean { - return !!this.bb!.readInt8(this.bb_pos); -} - -static sizeOf():number { - return 1; -} - -static createCaptivePortalConfig(builder:flatbuffers.Builder, always_enabled: boolean):flatbuffers.Offset { - builder.prep(1, 1); - builder.writeInt8(Number(Boolean(always_enabled))); - return builder.offset(); -} - -} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/configuration/rfconfig.ts b/WebUI/src/lib/_fbs/open-shock/serialization/configuration/rfconfig.ts deleted file mode 100644 index ea360ea5..00000000 --- a/WebUI/src/lib/_fbs/open-shock/serialization/configuration/rfconfig.ts +++ /dev/null @@ -1,28 +0,0 @@ -// automatically generated by the FlatBuffers compiler, do not modify - -import * as flatbuffers from 'flatbuffers'; - -export class RFConfig { - bb: flatbuffers.ByteBuffer|null = null; - bb_pos = 0; - __init(i:number, bb:flatbuffers.ByteBuffer):RFConfig { - this.bb_pos = i; - this.bb = bb; - return this; -} - -txPin():number { - return this.bb!.readUint8(this.bb_pos); -} - -static sizeOf():number { - return 1; -} - -static createRFConfig(builder:flatbuffers.Builder, tx_pin: number):flatbuffers.Offset { - builder.prep(1, 1); - builder.writeInt8(tx_pin); - return builder.offset(); -} - -} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/device-to-server-message-payload.ts b/WebUI/src/lib/_fbs/open-shock/serialization/device-to-server-message-payload.ts deleted file mode 100644 index c1bf52ad..00000000 --- a/WebUI/src/lib/_fbs/open-shock/serialization/device-to-server-message-payload.ts +++ /dev/null @@ -1,32 +0,0 @@ -// automatically generated by the FlatBuffers compiler, do not modify - -import { KeepAlive } from '../../open-shock/serialization/keep-alive.js'; - - -export enum DeviceToServerMessagePayload { - NONE = 0, - KeepAlive = 1 -} - -export function unionToDeviceToServerMessagePayload( - type: DeviceToServerMessagePayload, - accessor: (obj:KeepAlive) => KeepAlive|null -): KeepAlive|null { - switch(DeviceToServerMessagePayload[type]) { - case 'NONE': return null; - case 'KeepAlive': return accessor(new KeepAlive())! as KeepAlive; - default: return null; - } -} - -export function unionListToDeviceToServerMessagePayload( - type: DeviceToServerMessagePayload, - accessor: (index: number, obj:KeepAlive) => KeepAlive|null, - index: number -): KeepAlive|null { - switch(DeviceToServerMessagePayload[type]) { - case 'NONE': return null; - case 'KeepAlive': return accessor(index, new KeepAlive())! as KeepAlive; - default: return null; - } -} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/device-to-server-message.ts b/WebUI/src/lib/_fbs/open-shock/serialization/device-to-server-message.ts deleted file mode 100644 index 641939b5..00000000 --- a/WebUI/src/lib/_fbs/open-shock/serialization/device-to-server-message.ts +++ /dev/null @@ -1,67 +0,0 @@ -// automatically generated by the FlatBuffers compiler, do not modify - -import * as flatbuffers from 'flatbuffers'; - -import { DeviceToServerMessagePayload, unionToDeviceToServerMessagePayload, unionListToDeviceToServerMessagePayload } from '../../open-shock/serialization/device-to-server-message-payload.js'; - - -export class DeviceToServerMessage { - bb: flatbuffers.ByteBuffer|null = null; - bb_pos = 0; - __init(i:number, bb:flatbuffers.ByteBuffer):DeviceToServerMessage { - this.bb_pos = i; - this.bb = bb; - return this; -} - -static getRootAsDeviceToServerMessage(bb:flatbuffers.ByteBuffer, obj?:DeviceToServerMessage):DeviceToServerMessage { - return (obj || new DeviceToServerMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); -} - -static getSizePrefixedRootAsDeviceToServerMessage(bb:flatbuffers.ByteBuffer, obj?:DeviceToServerMessage):DeviceToServerMessage { - bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); - return (obj || new DeviceToServerMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); -} - -payloadType():DeviceToServerMessagePayload { - const offset = this.bb!.__offset(this.bb_pos, 4); - return offset ? this.bb!.readUint8(this.bb_pos + offset) : DeviceToServerMessagePayload.NONE; -} - -payload(obj:any):any|null { - const offset = this.bb!.__offset(this.bb_pos, 6); - return offset ? this.bb!.__union(obj, this.bb_pos + offset) : null; -} - -static startDeviceToServerMessage(builder:flatbuffers.Builder) { - builder.startObject(2); -} - -static addPayloadType(builder:flatbuffers.Builder, payloadType:DeviceToServerMessagePayload) { - builder.addFieldInt8(0, payloadType, DeviceToServerMessagePayload.NONE); -} - -static addPayload(builder:flatbuffers.Builder, payloadOffset:flatbuffers.Offset) { - builder.addFieldOffset(1, payloadOffset, 0); -} - -static endDeviceToServerMessage(builder:flatbuffers.Builder):flatbuffers.Offset { - const offset = builder.endObject(); - return offset; -} - -static finishDeviceToServerMessageBuffer(builder:flatbuffers.Builder, offset:flatbuffers.Offset) { - builder.finish(offset); -} - -static finishSizePrefixedDeviceToServerMessageBuffer(builder:flatbuffers.Builder, offset:flatbuffers.Offset) { - builder.finish(offset, undefined, true); -} - -static createDeviceToServerMessage(builder:flatbuffers.Builder, payloadType:DeviceToServerMessagePayload, payloadOffset:flatbuffers.Offset):flatbuffers.Offset { - DeviceToServerMessage.startDeviceToServerMessage(builder); - DeviceToServerMessage.addPayloadType(builder, payloadType); - DeviceToServerMessage.addPayload(builder, payloadOffset); - return DeviceToServerMessage.endDeviceToServerMessage(builder); -} -} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local.ts b/WebUI/src/lib/_fbs/open-shock/serialization/local.ts deleted file mode 100644 index b7d3085b..00000000 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local.ts +++ /dev/null @@ -1,12 +0,0 @@ -// automatically generated by the FlatBuffers compiler, do not modify - -export { AccountLinkCommand } from './serialization/local/account-link-command.js'; -export { AccountUnlinkCommand } from './serialization/local/account-unlink-command.js'; -export { LocalToDeviceMessage } from './serialization/local/local-to-device-message.js'; -export { LocalToDeviceMessagePayload } from './serialization/local/local-to-device-message-payload.js'; -export { SetRfTxPinCommand } from './serialization/local/set-rf-tx-pin-command.js'; -export { WifiNetworkConnectCommand } from './serialization/local/wifi-network-connect-command.js'; -export { WifiNetworkDisconnectCommand } from './serialization/local/wifi-network-disconnect-command.js'; -export { WifiNetworkForgetCommand } from './serialization/local/wifi-network-forget-command.js'; -export { WifiNetworkSaveCommand } from './serialization/local/wifi-network-save-command.js'; -export { WifiScanCommand } from './serialization/local/wifi-scan-command.js'; diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/local-to-device-message-payload.ts b/WebUI/src/lib/_fbs/open-shock/serialization/local/local-to-device-message-payload.ts deleted file mode 100644 index 5673de4a..00000000 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/local-to-device-message-payload.ts +++ /dev/null @@ -1,60 +0,0 @@ -// automatically generated by the FlatBuffers compiler, do not modify - -import { AccountLinkCommand } from '../../../open-shock/serialization/local/account-link-command.js'; -import { AccountUnlinkCommand } from '../../../open-shock/serialization/local/account-unlink-command.js'; -import { SetRfTxPinCommand } from '../../../open-shock/serialization/local/set-rf-tx-pin-command.js'; -import { WifiNetworkConnectCommand } from '../../../open-shock/serialization/local/wifi-network-connect-command.js'; -import { WifiNetworkDisconnectCommand } from '../../../open-shock/serialization/local/wifi-network-disconnect-command.js'; -import { WifiNetworkForgetCommand } from '../../../open-shock/serialization/local/wifi-network-forget-command.js'; -import { WifiNetworkSaveCommand } from '../../../open-shock/serialization/local/wifi-network-save-command.js'; -import { WifiScanCommand } from '../../../open-shock/serialization/local/wifi-scan-command.js'; - - -export enum LocalToDeviceMessagePayload { - NONE = 0, - WifiScanCommand = 1, - WifiNetworkSaveCommand = 2, - WifiNetworkForgetCommand = 3, - WifiNetworkConnectCommand = 4, - WifiNetworkDisconnectCommand = 5, - AccountLinkCommand = 6, - AccountUnlinkCommand = 7, - SetRfTxPinCommand = 8 -} - -export function unionToLocalToDeviceMessagePayload( - type: LocalToDeviceMessagePayload, - accessor: (obj:AccountLinkCommand|AccountUnlinkCommand|SetRfTxPinCommand|WifiNetworkConnectCommand|WifiNetworkDisconnectCommand|WifiNetworkForgetCommand|WifiNetworkSaveCommand|WifiScanCommand) => AccountLinkCommand|AccountUnlinkCommand|SetRfTxPinCommand|WifiNetworkConnectCommand|WifiNetworkDisconnectCommand|WifiNetworkForgetCommand|WifiNetworkSaveCommand|WifiScanCommand|null -): AccountLinkCommand|AccountUnlinkCommand|SetRfTxPinCommand|WifiNetworkConnectCommand|WifiNetworkDisconnectCommand|WifiNetworkForgetCommand|WifiNetworkSaveCommand|WifiScanCommand|null { - switch(LocalToDeviceMessagePayload[type]) { - case 'NONE': return null; - case 'WifiScanCommand': return accessor(new WifiScanCommand())! as WifiScanCommand; - case 'WifiNetworkSaveCommand': return accessor(new WifiNetworkSaveCommand())! as WifiNetworkSaveCommand; - case 'WifiNetworkForgetCommand': return accessor(new WifiNetworkForgetCommand())! as WifiNetworkForgetCommand; - case 'WifiNetworkConnectCommand': return accessor(new WifiNetworkConnectCommand())! as WifiNetworkConnectCommand; - case 'WifiNetworkDisconnectCommand': return accessor(new WifiNetworkDisconnectCommand())! as WifiNetworkDisconnectCommand; - case 'AccountLinkCommand': return accessor(new AccountLinkCommand())! as AccountLinkCommand; - case 'AccountUnlinkCommand': return accessor(new AccountUnlinkCommand())! as AccountUnlinkCommand; - case 'SetRfTxPinCommand': return accessor(new SetRfTxPinCommand())! as SetRfTxPinCommand; - default: return null; - } -} - -export function unionListToLocalToDeviceMessagePayload( - type: LocalToDeviceMessagePayload, - accessor: (index: number, obj:AccountLinkCommand|AccountUnlinkCommand|SetRfTxPinCommand|WifiNetworkConnectCommand|WifiNetworkDisconnectCommand|WifiNetworkForgetCommand|WifiNetworkSaveCommand|WifiScanCommand) => AccountLinkCommand|AccountUnlinkCommand|SetRfTxPinCommand|WifiNetworkConnectCommand|WifiNetworkDisconnectCommand|WifiNetworkForgetCommand|WifiNetworkSaveCommand|WifiScanCommand|null, - index: number -): AccountLinkCommand|AccountUnlinkCommand|SetRfTxPinCommand|WifiNetworkConnectCommand|WifiNetworkDisconnectCommand|WifiNetworkForgetCommand|WifiNetworkSaveCommand|WifiScanCommand|null { - switch(LocalToDeviceMessagePayload[type]) { - case 'NONE': return null; - case 'WifiScanCommand': return accessor(index, new WifiScanCommand())! as WifiScanCommand; - case 'WifiNetworkSaveCommand': return accessor(index, new WifiNetworkSaveCommand())! as WifiNetworkSaveCommand; - case 'WifiNetworkForgetCommand': return accessor(index, new WifiNetworkForgetCommand())! as WifiNetworkForgetCommand; - case 'WifiNetworkConnectCommand': return accessor(index, new WifiNetworkConnectCommand())! as WifiNetworkConnectCommand; - case 'WifiNetworkDisconnectCommand': return accessor(index, new WifiNetworkDisconnectCommand())! as WifiNetworkDisconnectCommand; - case 'AccountLinkCommand': return accessor(index, new AccountLinkCommand())! as AccountLinkCommand; - case 'AccountUnlinkCommand': return accessor(index, new AccountUnlinkCommand())! as AccountUnlinkCommand; - case 'SetRfTxPinCommand': return accessor(index, new SetRfTxPinCommand())! as SetRfTxPinCommand; - default: return null; - } -} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/server-to-device-message-payload.ts b/WebUI/src/lib/_fbs/open-shock/serialization/server-to-device-message-payload.ts deleted file mode 100644 index 93d6f460..00000000 --- a/WebUI/src/lib/_fbs/open-shock/serialization/server-to-device-message-payload.ts +++ /dev/null @@ -1,36 +0,0 @@ -// automatically generated by the FlatBuffers compiler, do not modify - -import { CaptivePortalConfig } from '../../open-shock/serialization/captive-portal-config.js'; -import { ShockerCommandList } from '../../open-shock/serialization/shocker-command-list.js'; - - -export enum ServerToDeviceMessagePayload { - NONE = 0, - ShockerCommandList = 1, - CaptivePortalConfig = 2 -} - -export function unionToServerToDeviceMessagePayload( - type: ServerToDeviceMessagePayload, - accessor: (obj:CaptivePortalConfig|ShockerCommandList) => CaptivePortalConfig|ShockerCommandList|null -): CaptivePortalConfig|ShockerCommandList|null { - switch(ServerToDeviceMessagePayload[type]) { - case 'NONE': return null; - case 'ShockerCommandList': return accessor(new ShockerCommandList())! as ShockerCommandList; - case 'CaptivePortalConfig': return accessor(new CaptivePortalConfig())! as CaptivePortalConfig; - default: return null; - } -} - -export function unionListToServerToDeviceMessagePayload( - type: ServerToDeviceMessagePayload, - accessor: (index: number, obj:CaptivePortalConfig|ShockerCommandList) => CaptivePortalConfig|ShockerCommandList|null, - index: number -): CaptivePortalConfig|ShockerCommandList|null { - switch(ServerToDeviceMessagePayload[type]) { - case 'NONE': return null; - case 'ShockerCommandList': return accessor(index, new ShockerCommandList())! as ShockerCommandList; - case 'CaptivePortalConfig': return accessor(index, new CaptivePortalConfig())! as CaptivePortalConfig; - default: return null; - } -} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/server-to-device-message.ts b/WebUI/src/lib/_fbs/open-shock/serialization/server-to-device-message.ts deleted file mode 100644 index 60ec936d..00000000 --- a/WebUI/src/lib/_fbs/open-shock/serialization/server-to-device-message.ts +++ /dev/null @@ -1,67 +0,0 @@ -// automatically generated by the FlatBuffers compiler, do not modify - -import * as flatbuffers from 'flatbuffers'; - -import { ServerToDeviceMessagePayload, unionToServerToDeviceMessagePayload, unionListToServerToDeviceMessagePayload } from '../../open-shock/serialization/server-to-device-message-payload.js'; - - -export class ServerToDeviceMessage { - bb: flatbuffers.ByteBuffer|null = null; - bb_pos = 0; - __init(i:number, bb:flatbuffers.ByteBuffer):ServerToDeviceMessage { - this.bb_pos = i; - this.bb = bb; - return this; -} - -static getRootAsServerToDeviceMessage(bb:flatbuffers.ByteBuffer, obj?:ServerToDeviceMessage):ServerToDeviceMessage { - return (obj || new ServerToDeviceMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); -} - -static getSizePrefixedRootAsServerToDeviceMessage(bb:flatbuffers.ByteBuffer, obj?:ServerToDeviceMessage):ServerToDeviceMessage { - bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); - return (obj || new ServerToDeviceMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); -} - -payloadType():ServerToDeviceMessagePayload { - const offset = this.bb!.__offset(this.bb_pos, 4); - return offset ? this.bb!.readUint8(this.bb_pos + offset) : ServerToDeviceMessagePayload.NONE; -} - -payload(obj:any):any|null { - const offset = this.bb!.__offset(this.bb_pos, 6); - return offset ? this.bb!.__union(obj, this.bb_pos + offset) : null; -} - -static startServerToDeviceMessage(builder:flatbuffers.Builder) { - builder.startObject(2); -} - -static addPayloadType(builder:flatbuffers.Builder, payloadType:ServerToDeviceMessagePayload) { - builder.addFieldInt8(0, payloadType, ServerToDeviceMessagePayload.NONE); -} - -static addPayload(builder:flatbuffers.Builder, payloadOffset:flatbuffers.Offset) { - builder.addFieldOffset(1, payloadOffset, 0); -} - -static endServerToDeviceMessage(builder:flatbuffers.Builder):flatbuffers.Offset { - const offset = builder.endObject(); - return offset; -} - -static finishServerToDeviceMessageBuffer(builder:flatbuffers.Builder, offset:flatbuffers.Offset) { - builder.finish(offset); -} - -static finishSizePrefixedServerToDeviceMessageBuffer(builder:flatbuffers.Builder, offset:flatbuffers.Offset) { - builder.finish(offset, undefined, true); -} - -static createServerToDeviceMessage(builder:flatbuffers.Builder, payloadType:ServerToDeviceMessagePayload, payloadOffset:flatbuffers.Offset):flatbuffers.Offset { - ServerToDeviceMessage.startServerToDeviceMessage(builder); - ServerToDeviceMessage.addPayloadType(builder, payloadType); - ServerToDeviceMessage.addPayload(builder, payloadOffset); - return ServerToDeviceMessage.endServerToDeviceMessage(builder); -} -} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/types.ts b/WebUI/src/lib/_fbs/open-shock/serialization/types.ts deleted file mode 100644 index 0bc39a7f..00000000 --- a/WebUI/src/lib/_fbs/open-shock/serialization/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -// automatically generated by the FlatBuffers compiler, do not modify - -export { WifiScanStatus } from './serialization/types/wifi-scan-status.js'; diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/types/shocker-model-type.ts b/WebUI/src/lib/_fbs/open-shock/serialization/types/shocker-model-type.ts deleted file mode 100644 index 90981952..00000000 --- a/WebUI/src/lib/_fbs/open-shock/serialization/types/shocker-model-type.ts +++ /dev/null @@ -1,6 +0,0 @@ -// automatically generated by the FlatBuffers compiler, do not modify - -export enum ShockerModelType { - CaiXianlin = 0, - PetTrainer = 1 -} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/types/wifi-scan-status.ts b/WebUI/src/lib/_fbs/open-shock/serialization/types/wifi-scan-status.ts deleted file mode 100644 index 2ed0ce38..00000000 --- a/WebUI/src/lib/_fbs/open-shock/serialization/types/wifi-scan-status.ts +++ /dev/null @@ -1,9 +0,0 @@ -// automatically generated by the FlatBuffers compiler, do not modify - -export enum WifiScanStatus { - Started = 0, - InProgress = 1, - Completed = 2, - Aborted = 3, - Error = 4 -} diff --git a/WebUI/src/lib/stores/DeviceStateStore.ts b/WebUI/src/lib/stores/DeviceStateStore.ts deleted file mode 100644 index 3d654d92..00000000 --- a/WebUI/src/lib/stores/DeviceStateStore.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { WifiScanStatus } from '$lib/_fbs/open-shock/serialization/types/wifi-scan-status'; -import type { WiFiNetwork } from '$lib/types/WiFiNetwork'; -import type { DeviceState } from '$lib/types/DeviceState'; -import { writable } from 'svelte/store'; - -const { subscribe, update } = writable({ - wifiConnectedBSSID: null, - wifiScanStatus: null, - wifiNetworks: new Map(), - gatewayPaired: false, - rfTxPin: null, -}); - -export const DeviceStateStore = { - subscribe, - update, - setWifiConnectedBSSID(connectedBSSID: string | null) { - update((store) => { - store.wifiConnectedBSSID = connectedBSSID; - return store; - }); - }, - setWifiScanStatus(scanStatus: WifiScanStatus | null) { - update((store) => { - store.wifiScanStatus = scanStatus; - return store; - }); - }, - setWifiNetwork(network: WiFiNetwork) { - update((store) => { - store.wifiNetworks.set(network.bssid, network); - return store; - }); - }, - updateWifiNetwork(bssid: string, updater: (network: WiFiNetwork) => WiFiNetwork) { - update((store) => { - const network = store.wifiNetworks.get(bssid); - if (network) { - store.wifiNetworks.set(bssid, updater(network)); - } - return store; - }); - }, - removeWifiNetwork(bssid: string) { - update((store) => { - store.wifiNetworks.delete(bssid); - return store; - }); - }, - clearWifiNetworks() { - update((store) => { - store.wifiNetworks.clear(); - return store; - }); - }, - setGatewayPaired(paired: boolean) { - update((store) => { - store.gatewayPaired = paired; - return store; - }); - }, - setRfTxPin(pin: number) { - update((store) => { - store.rfTxPin = pin; - return store; - }); - }, -}; diff --git a/chips/ESP32-D0WD/partitions.csv b/chips/ESP32-D0WD/partitions.csv deleted file mode 100644 index 1d00925f..00000000 --- a/chips/ESP32-D0WD/partitions.csv +++ /dev/null @@ -1,6 +0,0 @@ -# Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x5000, -otadata, data, ota, 0xe000, 0x2000, -app0, app, ota_0, 0x10000, 0x300000, -spiffs, data, spiffs, 0x310000,0xE0000, -coredump, data, coredump,0x3F0000,0x10000, diff --git a/chips/ESP32-D0WDQ6/partitions.csv b/chips/ESP32-D0WDQ6/partitions.csv deleted file mode 100644 index 1d00925f..00000000 --- a/chips/ESP32-D0WDQ6/partitions.csv +++ /dev/null @@ -1,6 +0,0 @@ -# Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x5000, -otadata, data, ota, 0xe000, 0x2000, -app0, app, ota_0, 0x10000, 0x300000, -spiffs, data, spiffs, 0x310000,0xE0000, -coredump, data, coredump,0x3F0000,0x10000, diff --git a/chips/ESP32-D0WDQ6/merge-image.py b/chips/ESP32-S2/4MB/merge-image.py similarity index 62% rename from chips/ESP32-D0WDQ6/merge-image.py rename to chips/ESP32-S2/4MB/merge-image.py index 9fbf817c..bfe06d40 100644 --- a/chips/ESP32-D0WDQ6/merge-image.py +++ b/chips/ESP32-S2/4MB/merge-image.py @@ -9,7 +9,7 @@ '--flash_size', '4MB', '0x1000', './bootloader.bin', '0x8000', './partitions.bin', - '0x10000', './firmware.bin', - '0x310000', './filesystem.bin' + '0x10000', './app.bin', + '0x353000', './staticfs.bin' # This is littlefs.bin, the github CI/CD pipeline renames it to staticfs.bin ]) # fmt: on diff --git a/chips/ESP32-S2/N4R2/merge-image.py b/chips/ESP32-S2/N4R2/merge-image.py deleted file mode 100644 index b5f9e741..00000000 --- a/chips/ESP32-S2/N4R2/merge-image.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/python3 - -import esptool - -# fmt: off -esptool.main([ - '--chip', 'esp32s2', - 'merge_bin', '-o', 'merged.bin', - '--flash_size', '4MB', - '0x1000', './bootloader.bin', - '0x8000', './partitions.bin', - '0x10000', './firmware.bin', - '0x310000', './filesystem.bin' -]) -# fmt: on diff --git a/chips/ESP32-S2/N4R2/partitions.csv b/chips/ESP32-S2/N4R2/partitions.csv deleted file mode 100644 index 61254fca..00000000 --- a/chips/ESP32-S2/N4R2/partitions.csv +++ /dev/null @@ -1,6 +0,0 @@ -# Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x5000, -otadata, data, ota, 0xe000, 0x2000, -app0, app, ota_0, 0x10000, 0x300000, -spiffs, data, spiffs, 0x310000,0xE0000, -coredump, data, coredump,0x3F0000,0x10000, \ No newline at end of file diff --git a/chips/ESP32-S3/N16R8/merge-image.py b/chips/ESP32-S3/4MB/merge-image.py similarity index 64% rename from chips/ESP32-S3/N16R8/merge-image.py rename to chips/ESP32-S3/4MB/merge-image.py index c2fcacfb..8e76a390 100644 --- a/chips/ESP32-S3/N16R8/merge-image.py +++ b/chips/ESP32-S3/4MB/merge-image.py @@ -7,10 +7,10 @@ esptool.main([ '--chip', 'esp32s3', 'merge_bin', '-o', 'merged.bin', - '--flash_size', '16MB', # This board has 16MiB of flash. + '--flash_size', '4MB', '0x0', './bootloader.bin', '0x8000', './partitions.bin', - '0x10000', './firmware.bin', - '0x310000', './filesystem.bin' + '0x10000', './app.bin', + '0x353000', './staticfs.bin' # This is littlefs.bin, the github CI/CD pipeline renames it to staticfs.bin ]) # fmt: on diff --git a/chips/ESP32-S3/N16R8/partitions.csv b/chips/ESP32-S3/N16R8/partitions.csv deleted file mode 100644 index 1d00925f..00000000 --- a/chips/ESP32-S3/N16R8/partitions.csv +++ /dev/null @@ -1,6 +0,0 @@ -# Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x5000, -otadata, data, ota, 0xe000, 0x2000, -app0, app, ota_0, 0x10000, 0x300000, -spiffs, data, spiffs, 0x310000,0xE0000, -coredump, data, coredump,0x3F0000,0x10000, diff --git a/chips/ESP32-S3/N8R8/merge-image.py b/chips/ESP32-S3/N8R8/merge-image.py deleted file mode 100644 index e27ec4b3..00000000 --- a/chips/ESP32-S3/N8R8/merge-image.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/python3 - -import esptool - -# fmt: off -# Note: Bootloader for esp32-s3 starts at 0x0000, unlike several other ESP32 variants that start at 0x1000. -esptool.main([ - '--chip', 'esp32s3', - 'merge_bin', '-o', 'merged.bin', - '--flash_size', '8MB', - '0x0', './bootloader.bin', - '0x8000', './partitions.bin', - '0x10000', './firmware.bin', - '0x280000', './filesystem.bin' -]) -# fmt: on diff --git a/chips/ESP32-S3/N8R8/partitions.csv b/chips/ESP32-S3/N8R8/partitions.csv deleted file mode 100644 index 31b1dfac..00000000 --- a/chips/ESP32-S3/N8R8/partitions.csv +++ /dev/null @@ -1,6 +0,0 @@ -# Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x5000, -otadata, data, ota, 0xe000, 0x2000, -app0, app, ota_0, 0x10000, 0x270000, -spiffs, data, spiffs, 0x280000,0xE0000, -coredump, data, coredump,0x3F0000,0x10000, diff --git a/chips/ESP32-D0WD/merge-image.py b/chips/ESP32/4MB/merge-image.py similarity index 62% rename from chips/ESP32-D0WD/merge-image.py rename to chips/ESP32/4MB/merge-image.py index 9fbf817c..bfe06d40 100644 --- a/chips/ESP32-D0WD/merge-image.py +++ b/chips/ESP32/4MB/merge-image.py @@ -9,7 +9,7 @@ '--flash_size', '4MB', '0x1000', './bootloader.bin', '0x8000', './partitions.bin', - '0x10000', './firmware.bin', - '0x310000', './filesystem.bin' + '0x10000', './app.bin', + '0x353000', './staticfs.bin' # This is littlefs.bin, the github CI/CD pipeline renames it to staticfs.bin ]) # fmt: on diff --git a/chips/partitions_4MB.csv b/chips/partitions_4MB.csv new file mode 100644 index 00000000..42a5ccae --- /dev/null +++ b/chips/partitions_4MB.csv @@ -0,0 +1,9 @@ +# CURRENTLY NOT USED - KEPT FOR REFERENCE +# OpenShock 4MB Partition Table - without OTA +# Name, Type, SubType, Offset, Size, Flags +# nvs, data, nvs, 0x009000, 0x005000, +# otadata, data, ota, 0x00e000, 0x002000, +# app0, app, ota_0, 0x010000, 0x340000, +# config, data, spiffs, 0x350000, 0x003000, +# static0, data, spiffs, 0x353000, 0x09D000, +# coredump, data, coredump, 0x3F0000, 0x010000, \ No newline at end of file diff --git a/chips/partitions_4MB_OTA.csv b/chips/partitions_4MB_OTA.csv new file mode 100644 index 00000000..778817c4 --- /dev/null +++ b/chips/partitions_4MB_OTA.csv @@ -0,0 +1,9 @@ +# OpenShock 4MB Partition Table - with OTA +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x009000, 0x005000, +otadata, data, ota, 0x00e000, 0x002000, +app0, app, ota_0, 0x010000, 0x1A0000, +app1, app, ota_1, 0x1B0000, 0x1A0000, +config, data, spiffs, 0x350000, 0x003000, +static0, data, spiffs, 0x353000, 0x09D000, +coredump, data, coredump, 0x3F0000, 0x010000, \ No newline at end of file diff --git a/WebUI/.eslintignore b/frontend/.eslintignore similarity index 100% rename from WebUI/.eslintignore rename to frontend/.eslintignore diff --git a/WebUI/.eslintrc.cjs b/frontend/.eslintrc.cjs similarity index 100% rename from WebUI/.eslintrc.cjs rename to frontend/.eslintrc.cjs diff --git a/WebUI/.gitignore b/frontend/.gitignore similarity index 100% rename from WebUI/.gitignore rename to frontend/.gitignore diff --git a/WebUI/.npmrc b/frontend/.npmrc similarity index 100% rename from WebUI/.npmrc rename to frontend/.npmrc diff --git a/WebUI/.prettierignore b/frontend/.prettierignore similarity index 100% rename from WebUI/.prettierignore rename to frontend/.prettierignore diff --git a/WebUI/.prettierrc b/frontend/.prettierrc similarity index 54% rename from WebUI/.prettierrc rename to frontend/.prettierrc index fedd4864..b4f0a3d9 100644 --- a/WebUI/.prettierrc +++ b/frontend/.prettierrc @@ -7,5 +7,8 @@ "printWidth": 256, "plugins": ["prettier-plugin-svelte"], "pluginSearchDirs": ["."], - "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] + "overrides": [ + { "files": "*.svelte", "options": { "parser": "svelte" } }, + { "files": "package*.json", "options": { "tabWidth": 2, "useTabs": true } } + ] } diff --git a/frontend/openshock-theme.ts b/frontend/openshock-theme.ts new file mode 100644 index 00000000..770004ac --- /dev/null +++ b/frontend/openshock-theme.ts @@ -0,0 +1,101 @@ +import type { CustomThemeConfig } from '@skeletonlabs/tw-plugin'; + +export const openshockTheme: CustomThemeConfig = { + name: 'openshock', + properties: { + // =~= Theme Properties =~= + '--theme-font-family-base': `system-ui`, + '--theme-font-family-heading': `system-ui`, + '--theme-font-color-base': '0 0 0', + '--theme-font-color-dark': '255 255 255', + '--theme-rounded-base': '9999px', + '--theme-rounded-container': '12px', + '--theme-border-base': '1px', + // =~= Theme On-X Colors =~= + '--on-primary': '0 0 0', + '--on-secondary': '255 255 255', + '--on-tertiary': '0 0 0', + '--on-success': '0 0 0', + '--on-warning': '0 0 0', + '--on-error': '255 255 255', + '--on-surface': '255 255 255', + // =~= Theme Colors =~= + // primary | #ff5c82 + '--color-primary-50': '255 231 236', // #ffe7ec + '--color-primary-100': '255 222 230', // #ffdee6 + '--color-primary-200': '255 214 224', // #ffd6e0 + '--color-primary-300': '255 190 205', // #ffbecd + '--color-primary-400': '255 141 168', // #ff8da8 + '--color-primary-500': '255 92 130', // #ff5c82 + '--color-primary-600': '230 83 117', // #e65375 + '--color-primary-700': '191 69 98', // #bf4562 + '--color-primary-800': '153 55 78', // #99374e + '--color-primary-900': '125 45 64', // #7d2d40 + // secondary | #463eda + '--color-secondary-50': '227 226 249', // #e3e2f9 + '--color-secondary-100': '218 216 248', // #dad8f8 + '--color-secondary-200': '209 207 246', // #d1cff6 + '--color-secondary-300': '181 178 240', // #b5b2f0 + '--color-secondary-400': '126 120 229', // #7e78e5 + '--color-secondary-500': '70 62 218', // #463eda + '--color-secondary-600': '63 56 196', // #3f38c4 + '--color-secondary-700': '53 47 164', // #352fa4 + '--color-secondary-800': '42 37 131', // #2a2583 + '--color-secondary-900': '34 30 107', // #221e6b + // tertiary | #33b8ff + '--color-tertiary-50': '224 244 255', // #e0f4ff + '--color-tertiary-100': '214 241 255', // #d6f1ff + '--color-tertiary-200': '204 237 255', // #ccedff + '--color-tertiary-300': '173 227 255', // #ade3ff + '--color-tertiary-400': '112 205 255', // #70cdff + '--color-tertiary-500': '51 184 255', // #33b8ff + '--color-tertiary-600': '46 166 230', // #2ea6e6 + '--color-tertiary-700': '38 138 191', // #268abf + '--color-tertiary-800': '31 110 153', // #1f6e99 + '--color-tertiary-900': '25 90 125', // #195a7d + // success | #03c200 + '--color-success-50': '217 246 217', // #d9f6d9 + '--color-success-100': '205 243 204', // #cdf3cc + '--color-success-200': '192 240 191', // #c0f0bf + '--color-success-300': '154 231 153', // #9ae799 + '--color-success-400': '79 212 77', // #4fd44d + '--color-success-500': '3 194 0', // #03c200 + '--color-success-600': '3 175 0', // #03af00 + '--color-success-700': '2 146 0', // #029200 + '--color-success-800': '2 116 0', // #027400 + '--color-success-900': '1 95 0', // #015f00 + // warning | #ff7300 + '--color-warning-50': '255 234 217', // #ffead9 + '--color-warning-100': '255 227 204', // #ffe3cc + '--color-warning-200': '255 220 191', // #ffdcbf + '--color-warning-300': '255 199 153', // #ffc799 + '--color-warning-400': '255 157 77', // #ff9d4d + '--color-warning-500': '255 115 0', // #ff7300 + '--color-warning-600': '230 104 0', // #e66800 + '--color-warning-700': '191 86 0', // #bf5600 + '--color-warning-800': '153 69 0', // #994500 + '--color-warning-900': '125 56 0', // #7d3800 + // error | #a30000 + '--color-error-50': '241 217 217', // #f1d9d9 + '--color-error-100': '237 204 204', // #edcccc + '--color-error-200': '232 191 191', // #e8bfbf + '--color-error-300': '218 153 153', // #da9999 + '--color-error-400': '191 77 77', // #bf4d4d + '--color-error-500': '163 0 0', // #a30000 + '--color-error-600': '147 0 0', // #930000 + '--color-error-700': '122 0 0', // #7a0000 + '--color-error-800': '98 0 0', // #620000 + '--color-error-900': '80 0 0', // #500000 + // surface | #202325 + '--color-surface-50': '222 222 222', // #dedede + '--color-surface-100': '210 211 211', // #d2d3d3 + '--color-surface-200': '199 200 201', // #c7c8c9 + '--color-surface-300': '166 167 168', // #a6a7a8 + '--color-surface-400': '99 101 102', // #636566 + '--color-surface-500': '32 35 37', // #202325 + '--color-surface-600': '29 32 33', // #1d2021 + '--color-surface-700': '24 26 28', // #181a1c + '--color-surface-800': '19 21 22', // #131516 + '--color-surface-900': '16 17 18', // #101112 + }, +}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 00000000..fa9cfb62 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4802 @@ +{ + "name": "frontend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.1", + "dependencies": { + "@floating-ui/dom": "1.5.4" + }, + "devDependencies": { + "@playwright/test": "1.40.1", + "@skeletonlabs/skeleton": "2.7.0", + "@skeletonlabs/tw-plugin": "0.3.1", + "@sveltejs/adapter-static": "^3.0.1", + "@sveltejs/kit": "2.3.2", + "@sveltejs/vite-plugin-svelte": "^3.0.1", + "@tailwindcss/forms": "0.5.7", + "@tailwindcss/typography": "0.5.10", + "@types/node": "20.11.1", + "@typescript-eslint/eslint-plugin": "6.18.1", + "@typescript-eslint/parser": "6.18.1", + "autoprefixer": "10.4.16", + "eslint": "8.56.0", + "eslint-config-prettier": "9.1.0", + "eslint-plugin-svelte": "2.35.1", + "flatbuffers": "23.5.26", + "postcss": "8.4.33", + "prettier": "3.2.2", + "prettier-plugin-svelte": "3.1.2", + "svelte": "4.2.8", + "svelte-check": "3.6.3", + "tailwindcss": "3.4.1", + "tslib": "2.6.2", + "typescript": "5.3.3", + "vite": "^5.0.11", + "vite-plugin-tailwind-purgecss": "^0.2.0", + "vitest": "1.2.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.9.tgz", + "integrity": "sha512-jkYjjq7SdsWuNI6b5quymW0oC83NN5FdRPuCbs9HZ02mfVdAP8B8eeqLSYU3gb6OJEaY5CQabtTFbqBf26H3GA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.9.tgz", + "integrity": "sha512-q4cR+6ZD0938R19MyEW3jEsMzbb/1rulLXiNAJQADD/XYp7pT+rOS5JGxvpRW8dFDEfjW4wLgC/3FXIw4zYglQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.9.tgz", + "integrity": "sha512-KOqoPntWAH6ZxDwx1D6mRntIgZh9KodzgNOy5Ebt9ghzffOk9X2c1sPwtM9P+0eXbefnDhqYfkh5PLP5ULtWFA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.9.tgz", + "integrity": "sha512-KBJ9S0AFyLVx2E5D8W0vExqRW01WqRtczUZ8NRu+Pi+87opZn5tL4Y0xT0mA4FtHctd0ZgwNoN639fUUGlNIWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.9.tgz", + "integrity": "sha512-vE0VotmNTQaTdX0Q9dOHmMTao6ObjyPm58CHZr1UK7qpNleQyxlFlNCaHsHx6Uqv86VgPmR4o2wdNq3dP1qyDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.9.tgz", + "integrity": "sha512-uFQyd/o1IjiEk3rUHSwUKkqZwqdvuD8GevWF065eqgYfexcVkxh+IJgwTaGZVu59XczZGcN/YMh9uF1fWD8j1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.9.tgz", + "integrity": "sha512-WMLgWAtkdTbTu1AWacY7uoj/YtHthgqrqhf1OaEWnZb7PQgpt8eaA/F3LkV0E6K/Lc0cUr/uaVP/49iE4M4asA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.9.tgz", + "integrity": "sha512-C/ChPohUYoyUaqn1h17m/6yt6OB14hbXvT8EgM1ZWaiiTYz7nWZR0SYmMnB5BzQA4GXl3BgBO1l8MYqL/He3qw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.9.tgz", + "integrity": "sha512-PiPblfe1BjK7WDAKR1Cr9O7VVPqVNpwFcPWgfn4xu0eMemzRp442hXyzF/fSwgrufI66FpHOEJk0yYdPInsmyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.9.tgz", + "integrity": "sha512-f37i/0zE0MjDxijkPSQw1CO/7C27Eojqb+r3BbHVxMLkj8GCa78TrBZzvPyA/FNLUMzP3eyHCVkAopkKVja+6Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.9.tgz", + "integrity": "sha512-t6mN147pUIf3t6wUt3FeumoOTPfmv9Cc6DQlsVBpB7eCpLOqQDyWBP1ymXn1lDw4fNUSb/gBcKAmvTP49oIkaA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.9.tgz", + "integrity": "sha512-jg9fujJTNTQBuDXdmAg1eeJUL4Jds7BklOTkkH80ZgQIoCTdQrDaHYgbFZyeTq8zbY+axgptncko3v9p5hLZtw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.9.tgz", + "integrity": "sha512-tkV0xUX0pUUgY4ha7z5BbDS85uI7ABw3V1d0RNTii7E9lbmV8Z37Pup2tsLV46SQWzjOeyDi1Q7Wx2+QM8WaCQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.9.tgz", + "integrity": "sha512-DfLp8dj91cufgPZDXr9p3FoR++m3ZJ6uIXsXrIvJdOjXVREtXuQCjfMfvmc3LScAVmLjcfloyVtpn43D56JFHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.9.tgz", + "integrity": "sha512-zHbglfEdC88KMgCWpOl/zc6dDYJvWGLiUtmPRsr1OgCViu3z5GncvNVdf+6/56O2Ca8jUU+t1BW261V6kp8qdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.9.tgz", + "integrity": "sha512-JUjpystGFFmNrEHQnIVG8hKwvA2DN5o7RqiO1CVX8EN/F/gkCjkUMgVn6hzScpwnJtl2mPR6I9XV1oW8k9O+0A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.9.tgz", + "integrity": "sha512-GThgZPAwOBOsheA2RUlW5UeroRfESwMq/guy8uEe3wJlAOjpOXuSevLRd70NZ37ZrpO6RHGHgEHvPg1h3S1Jug==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.9.tgz", + "integrity": "sha512-Ki6PlzppaFVbLnD8PtlVQfsYw4S9n3eQl87cqgeIw+O3sRr9IghpfSKY62mggdt1yCSZ8QWvTZ9jo9fjDSg9uw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.9.tgz", + "integrity": "sha512-MLHj7k9hWh4y1ddkBpvRj2b9NCBhfgBt3VpWbHQnXRedVun/hC7sIyTGDGTfsGuXo4ebik2+3ShjcPbhtFwWDw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.9.tgz", + "integrity": "sha512-GQoa6OrQ8G08guMFgeXPH7yE/8Dt0IfOGWJSfSH4uafwdC7rWwrfE6P9N8AtPGIjUzdo2+7bN8Xo3qC578olhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.9.tgz", + "integrity": "sha512-UOozV7Ntykvr5tSOlGCrqU3NBr3d8JqPes0QWN2WOXfvkWVGRajC+Ym0/Wj88fUgecUCLDdJPDF0Nna2UK3Qtg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.9.tgz", + "integrity": "sha512-oxoQgglOP7RH6iasDrhY+R/3cHrfwIDvRlT4CGChflq6twk8iENeVvMJjmvBb94Ik1Z+93iGO27err7w6l54GQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.3.tgz", + "integrity": "sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q==", + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.4.tgz", + "integrity": "sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ==", + "dependencies": { + "@floating-ui/core": "^1.5.3", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.1.tgz", + "integrity": "sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==", + "dev": true, + "dependencies": { + "playwright": "1.40.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.24", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", + "integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.0.tgz", + "integrity": "sha512-+1ge/xmaJpm1KVBuIH38Z94zj9fBD+hp+/5WLaHgyY8XLq1ibxk/zj6dTXaqM2cAbYKq8jYlhHd6k05If1W5xA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.0.tgz", + "integrity": "sha512-im6hUEyQ7ZfoZdNvtwgEJvBWZYauC9KVKq1w58LG2Zfz6zMd8gRrbN+xCVoqA2hv/v6fm9lp5LFGJ3za8EQH3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.0.tgz", + "integrity": "sha512-u7aTMskN6Dmg1lCT0QJ+tINRt+ntUrvVkhbPfFz4bCwRZvjItx2nJtwJnJRlKMMaQCHRjrNqHRDYvE4mBm3DlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.0.tgz", + "integrity": "sha512-8FvEl3w2ExmpcOmX5RJD0yqXcVSOqAJJUJ29Lca29Ik+3zPS1yFimr2fr5JSZ4Z5gt8/d7WqycpgkX9nocijSw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.0.tgz", + "integrity": "sha512-lHoKYaRwd4gge+IpqJHCY+8Vc3hhdJfU6ukFnnrJasEBUvVlydP8PuwndbWfGkdgSvZhHfSEw6urrlBj0TSSfg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.0.tgz", + "integrity": "sha512-JbEPfhndYeWHfOSeh4DOFvNXrj7ls9S/2omijVsao+LBPTPayT1uKcK3dHW3MwDJ7KO11t9m2cVTqXnTKpeaiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.0.tgz", + "integrity": "sha512-ahqcSXLlcV2XUBM3/f/C6cRoh7NxYA/W7Yzuv4bDU1YscTFw7ay4LmD7l6OS8EMhTNvcrWGkEettL1Bhjf+B+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.0.tgz", + "integrity": "sha512-uwvOYNtLw8gVtrExKhdFsYHA/kotURUmZYlinH2VcQxNCQJeJXnkmWgw2hI9Xgzhgu7J9QvWiq9TtTVwWMDa+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.0.tgz", + "integrity": "sha512-m6pkSwcZZD2LCFHZX/zW2aLIISyzWLU3hrLLzQKMI12+OLEzgruTovAxY5sCZJkipklaZqPy/2bEEBNjp+Y7xg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.0.tgz", + "integrity": "sha512-VFAC1RDRSbU3iOF98X42KaVicAfKf0m0OvIu8dbnqhTe26Kh6Ym9JrDulz7Hbk7/9zGc41JkV02g+p3BivOdAg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.0.tgz", + "integrity": "sha512-9jPgMvTKXARz4inw6jezMLA2ihDBvgIU9Ml01hjdVpOcMKyxFBJrn83KVQINnbeqDv0+HdO1c09hgZ8N0s820Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.0.tgz", + "integrity": "sha512-WE4pT2kTXQN2bAv40Uog0AsV7/s9nT9HBWXAou8+++MBCnY51QS02KYtm6dQxxosKi1VIz/wZIrTQO5UP2EW+Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.0.tgz", + "integrity": "sha512-aPP5Q5AqNGuT0tnuEkK/g4mnt3ZhheiXrDIiSVIHN9mcN21OyXDVbEMqmXPE7e2OplNLDkcvV+ZoGJa2ZImFgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@skeletonlabs/skeleton": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@skeletonlabs/skeleton/-/skeleton-2.7.0.tgz", + "integrity": "sha512-XD6aCvEQYp2LtEKM9OelK2HoJZFraM3fvpQpXsnNY82t/QeaZ9WkORCsrVz91puq786FTOktt8beZEe045YLWA==", + "dev": true, + "dependencies": { + "esm-env": "1.0.0" + }, + "peerDependencies": { + "svelte": "^3.56.0 || ^4.0.0" + } + }, + "node_modules/@skeletonlabs/tw-plugin": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@skeletonlabs/tw-plugin/-/tw-plugin-0.3.1.tgz", + "integrity": "sha512-DjjeOHN3HhFQf6gYPT2MUZMkIdw1jeB9mbuKC8etQxUlOR4XitfC7hssRWFJ8RJsvrrN0myCBbdWkVG1JVA96g==", + "dev": true, + "peerDependencies": { + "tailwindcss": ">=3.0.0" + } + }, + "node_modules/@sveltejs/adapter-static": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.1.tgz", + "integrity": "sha512-6lMvf7xYEJ+oGeR5L8DFJJrowkefTK6ZgA4JiMqoClMkKq0s6yvsd3FZfCFvX1fQ0tpCD7fkuRVHsnUVgsHyNg==", + "dev": true, + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.3.2.tgz", + "integrity": "sha512-AzGWV1TyUSkBuciy06E5NegXndIEgTthDtllv80qynEJFh8bZD62ZxLajiQLOsKGqRDilEQyshDARQxjIqiaqg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.6.0", + "devalue": "^4.3.2", + "esm-env": "^1.0.0", + "import-meta-resolve": "^4.0.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^2.0.4", + "tiny-glob": "^0.2.9" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.3" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.0.1.tgz", + "integrity": "sha512-CGURX6Ps+TkOovK6xV+Y2rn8JKa8ZPUHPZ/NKgCxAmgBrXReavzFl8aOSCj3kQ1xqT7yGJj53hjcV/gqwDAaWA==", + "dev": true, + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^2.0.0-next.0 || ^2.0.0", + "debug": "^4.3.4", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "svelte-hmr": "^0.15.3", + "vitefu": "^0.2.5" + }, + "engines": { + "node": "^18.0.0 || >=20" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.0.0.tgz", + "integrity": "sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.0.0 || >=20" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.0" + } + }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz", + "integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==", + "dev": true, + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz", + "integrity": "sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==", + "dev": true, + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.11.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.1.tgz", + "integrity": "sha512-DsXojJUES2M+FE8CpptJTKpg+r54moV9ZEncPstni1WHFmTcCzeFLnMFfyhCVS8XNOy/OQG+8lVxRLRrVHmV5A==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/pug": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", + "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.18.1.tgz", + "integrity": "sha512-nISDRYnnIpk7VCFrGcu1rnZfM1Dh9LRHnfgdkjcbi/l7g16VYRri3TjXi9Ir4lOZSw5N/gnV/3H7jIPQ8Q4daA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.18.1", + "@typescript-eslint/type-utils": "6.18.1", + "@typescript-eslint/utils": "6.18.1", + "@typescript-eslint/visitor-keys": "6.18.1", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.18.1.tgz", + "integrity": "sha512-zct/MdJnVaRRNy9e84XnVtRv9Vf91/qqe+hZJtKanjojud4wAVy/7lXxJmMyX6X6J+xc6c//YEWvpeif8cAhWA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.18.1", + "@typescript-eslint/types": "6.18.1", + "@typescript-eslint/typescript-estree": "6.18.1", + "@typescript-eslint/visitor-keys": "6.18.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.18.1.tgz", + "integrity": "sha512-BgdBwXPFmZzaZUuw6wKiHKIovms97a7eTImjkXCZE04TGHysG+0hDQPmygyvgtkoB/aOQwSM/nWv3LzrOIQOBw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.18.1", + "@typescript-eslint/visitor-keys": "6.18.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.18.1.tgz", + "integrity": "sha512-wyOSKhuzHeU/5pcRDP2G2Ndci+4g653V43gXTpt4nbyoIOAASkGDA9JIAgbQCdCkcr1MvpSYWzxTz0olCn8+/Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.18.1", + "@typescript-eslint/utils": "6.18.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.1.tgz", + "integrity": "sha512-4TuMAe+tc5oA7wwfqMtB0Y5OrREPF1GeJBAjqwgZh1lEMH5PJQgWgHGfYufVB51LtjD+peZylmeyxUXPfENLCw==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.1.tgz", + "integrity": "sha512-fv9B94UAhywPRhUeeV/v+3SBDvcPiLxRZJw/xZeeGgRLQZ6rLMG+8krrJUyIf6s1ecWTzlsbp0rlw7n9sjufHA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.18.1", + "@typescript-eslint/visitor-keys": "6.18.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.18.1.tgz", + "integrity": "sha512-zZmTuVZvD1wpoceHvoQpOiewmWu3uP9FuTWo8vqpy2ffsmfCE8mklRPi+vmnIYAIk9t/4kOThri2QCDgor+OpQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.18.1", + "@typescript-eslint/types": "6.18.1", + "@typescript-eslint/typescript-estree": "6.18.1", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.1.tgz", + "integrity": "sha512-/kvt0C5lRqGoCfsbmm7/CwMqoSkY3zzHLIjdhHZQW3VFrnz7ATecOHR7nb7V+xn4286MBxfnQfQhAmCI0u+bJA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.18.1", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitest/expect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.0.tgz", + "integrity": "sha512-H+2bHzhyvgp32o7Pgj2h9RTHN0pgYaoi26Oo3mE+dCi1PAqV31kIIVfTbqMO3Bvshd5mIrJLc73EwSRrbol9Lw==", + "dev": true, + "dependencies": { + "@vitest/spy": "1.2.0", + "@vitest/utils": "1.2.0", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.0.tgz", + "integrity": "sha512-vaJkDoQaNUTroT70OhM0NPznP7H3WyRwt4LvGwCVYs/llLaqhoSLnlIhUClZpbF5RgAee29KRcNz0FEhYcgxqA==", + "dev": true, + "dependencies": { + "@vitest/utils": "1.2.0", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.0.tgz", + "integrity": "sha512-P33EE7TrVgB3HDLllrjK/GG6WSnmUtWohbwcQqmm7TAk9AVHpdgf7M3F3qRHKm6vhr7x3eGIln7VH052Smo6Kw==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.0.tgz", + "integrity": "sha512-MNxSAfxUaCeowqyyGwC293yZgk7cECZU9wGb8N1pYQ0yOn/SIr8t0l9XnGRdQZvNV/ZHBYu6GO/W3tj5K3VN1Q==", + "dev": true, + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.0.tgz", + "integrity": "sha512-FyD5bpugsXlwVpTcGLDf3wSPYy8g541fQt14qtzo8mJ4LdEpDKZ9mQy2+qdJm2TZRpjY5JLXihXCgIxiRJgi5g==", + "dev": true, + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.16", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", + "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001538", + "fraction.js": "^4.3.6", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axobject-query": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001570", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz", + "integrity": "sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chai": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/code-red": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", + "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1", + "acorn": "^8.10.0", + "estree-walker": "^3.0.3", + "periscopic": "^3.1.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.2.tgz", + "integrity": "sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg==", + "dev": true + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.614", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.614.tgz", + "integrity": "sha512-X4ze/9Sc3QWs6h92yerwqv7aB/uU8vCjZcrMjA8N9R1pjMFRe44dLsck5FzLilOYvcXuDn93B+bpGYyufc70gQ==", + "dev": true + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.9.tgz", + "integrity": "sha512-U9CHtKSy+EpPsEBa+/A2gMs/h3ylBC0H0KSqIg7tpztHerLi6nrrcoUJAkNCEPumx8yJ+Byic4BVwHgRbN0TBg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.19.9", + "@esbuild/android-arm64": "0.19.9", + "@esbuild/android-x64": "0.19.9", + "@esbuild/darwin-arm64": "0.19.9", + "@esbuild/darwin-x64": "0.19.9", + "@esbuild/freebsd-arm64": "0.19.9", + "@esbuild/freebsd-x64": "0.19.9", + "@esbuild/linux-arm": "0.19.9", + "@esbuild/linux-arm64": "0.19.9", + "@esbuild/linux-ia32": "0.19.9", + "@esbuild/linux-loong64": "0.19.9", + "@esbuild/linux-mips64el": "0.19.9", + "@esbuild/linux-ppc64": "0.19.9", + "@esbuild/linux-riscv64": "0.19.9", + "@esbuild/linux-s390x": "0.19.9", + "@esbuild/linux-x64": "0.19.9", + "@esbuild/netbsd-x64": "0.19.9", + "@esbuild/openbsd-x64": "0.19.9", + "@esbuild/sunos-x64": "0.19.9", + "@esbuild/win32-arm64": "0.19.9", + "@esbuild/win32-ia32": "0.19.9", + "@esbuild/win32-x64": "0.19.9" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.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": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.1.2.tgz", + "integrity": "sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "2.35.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.35.1.tgz", + "integrity": "sha512-IF8TpLnROSGy98Z3NrsKXWDSCbNY2ReHDcrYTuXZMbfX7VmESISR78TWgO9zdg4Dht1X8coub5jKwHzP0ExRug==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@jridgewell/sourcemap-codec": "^1.4.14", + "debug": "^4.3.1", + "eslint-compat-utils": "^0.1.2", + "esutils": "^2.0.3", + "known-css-properties": "^0.29.0", + "postcss": "^8.4.5", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^6.0.0", + "postcss-selector-parser": "^6.0.11", + "semver": "^7.5.3", + "svelte-eslint-parser": ">=0.33.0 <1.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0-0", + "svelte": "^3.37.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-svelte/node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esm-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", + "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", + "dev": true + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatbuffers": { + "version": "23.5.26", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-23.5.26.tgz", + "integrity": "sha512-vE+SI9vrJDwi1oETtTIFldC/o9GsVKRM+s6EL0nQgxXlYV1Vc4Tk30hj4xGICftInKQKj1F3up2n8UbIVobISQ==", + "dev": true + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "dev": true + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/ignore": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz", + "integrity": "sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "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", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz", + "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==", + "dev": true + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dev": true, + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mlly": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", + "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", + "dev": true, + "dependencies": { + "acorn": "^8.10.0", + "pathe": "^1.1.1", + "pkg-types": "^1.0.3", + "ufo": "^1.3.0" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, + "node_modules/playwright": { + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.1.tgz", + "integrity": "sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==", + "dev": true, + "dependencies": { + "playwright-core": "1.40.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.1.tgz", + "integrity": "sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/postcss": { + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.2.tgz", + "integrity": "sha512-HTByuKZzw7utPiDO523Tt2pLtEyK7OibUD9suEJQrPUCYQqrHr74GGX6VidMrovbf/I50mPqr8j/II6oBAuc5A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.1.2.tgz", + "integrity": "sha512-7xfMZtwgAWHMT0iZc8jN4o65zgbAQ3+O32V6W7pXrqNvKnHnkoyQCGCbKeUyXKZLbYE0YhFRnamfxfkEGxm8qA==", + "dev": true, + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/purgecss": { + "version": "6.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-6.0.0-alpha.0.tgz", + "integrity": "sha512-UC7d7uIyZsky+srEsSXny9BkbTcVn3ZtBCNX3rW3DsqJKhvUXFRpufA4ktcHzWF0+JLZgmsqjUm/8R82x9bHpw==", + "dev": true, + "dependencies": { + "commander": "^10.0.0", + "glob": "^8.0.3", + "postcss": "^8.4.4", + "postcss-selector-parser": "^6.0.7" + }, + "bin": { + "purgecss": "bin/purgecss.js" + } + }, + "node_modules/purgecss/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/purgecss/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/purgecss/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/purgecss/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.0.tgz", + "integrity": "sha512-bUHW/9N21z64gw8s6tP4c88P382Bq/L5uZDowHlHx6s/QWpjJXivIAbEw6LZthgSvlEizZBfLC4OAvWe7aoF7A==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.9.0", + "@rollup/rollup-android-arm64": "4.9.0", + "@rollup/rollup-darwin-arm64": "4.9.0", + "@rollup/rollup-darwin-x64": "4.9.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.9.0", + "@rollup/rollup-linux-arm64-gnu": "4.9.0", + "@rollup/rollup-linux-arm64-musl": "4.9.0", + "@rollup/rollup-linux-riscv64-gnu": "4.9.0", + "@rollup/rollup-linux-x64-gnu": "4.9.0", + "@rollup/rollup-linux-x64-musl": "4.9.0", + "@rollup/rollup-win32-arm64-msvc": "4.9.0", + "@rollup/rollup-win32-ia32-msvc": "4.9.0", + "@rollup/rollup-win32-x64-msvc": "4.9.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/sander": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", + "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", + "dev": true, + "dependencies": { + "es6-promise": "^3.1.2", + "graceful-fs": "^4.1.3", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2" + } + }, + "node_modules/sander/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sorcery": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", + "integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.14", + "buffer-crc32": "^0.2.5", + "minimist": "^1.2.0", + "sander": "^0.5.0" + }, + "bin": { + "sorcery": "bin/sorcery" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.6.0.tgz", + "integrity": "sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==", + "dev": true + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", + "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "dev": true, + "dependencies": { + "acorn": "^8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/sucrase": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", + "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.8.tgz", + "integrity": "sha512-hU6dh1MPl8gh6klQZwK/n73GiAHiR95IkFsesLPbMeEZi36ydaXL/ZAb4g9sayT0MXzpxyZjR28yderJHxcmYA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/trace-mapping": "^0.3.18", + "acorn": "^8.9.0", + "aria-query": "^5.3.0", + "axobject-query": "^3.2.1", + "code-red": "^1.0.3", + "css-tree": "^2.3.1", + "estree-walker": "^3.0.3", + "is-reference": "^3.0.1", + "locate-character": "^3.0.0", + "magic-string": "^0.30.4", + "periscopic": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/svelte-check": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.3.tgz", + "integrity": "sha512-Q2nGnoysxUnB9KjnjpQLZwdjK62DHyW6nuH/gm2qteFnDk0lCehe/6z8TsIvYeKjC6luKaWxiNGyOcWiLLPSwA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "chokidar": "^3.4.1", + "fast-glob": "^3.2.7", + "import-fresh": "^3.2.1", + "picocolors": "^1.0.0", + "sade": "^1.7.4", + "svelte-preprocess": "^5.1.0", + "typescript": "^5.0.3" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "peerDependencies": { + "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0" + } + }, + "node_modules/svelte-eslint-parser": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.1.tgz", + "integrity": "sha512-vo7xPGTlKBGdLH8T5L64FipvTrqv3OQRx9d2z5X05KKZDlF4rQk8KViZO4flKERY+5BiVdOh7zZ7JGJWo5P0uA==", + "dev": true, + "dependencies": { + "eslint-scope": "^7.0.0", + "eslint-visitor-keys": "^3.0.0", + "espree": "^9.0.0", + "postcss": "^8.4.29", + "postcss-scss": "^4.0.8" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte-hmr": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", + "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", + "dev": true, + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.19.0 || ^4.0.0" + } + }, + "node_modules/svelte-preprocess": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.2.tgz", + "integrity": "sha512-XF0aliMAcYnP4hLETvB6HRAMnaL09ASYT1Z2I1Gwu0nz6xbdg/dSgAEthtFZJA4AKrNhFDFdmUDO+H9d/6xg5g==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@types/pug": "^2.0.6", + "detect-indent": "^6.1.0", + "magic-string": "^0.27.0", + "sorcery": "^0.11.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">= 14.10.0" + }, + "peerDependencies": { + "@babel/core": "^7.10.2", + "coffeescript": "^2.5.1", + "less": "^3.11.3 || ^4.0.0", + "postcss": "^7 || ^8", + "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "pug": "^3.0.0", + "sass": "^1.26.8", + "stylus": "^0.55.0", + "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0", + "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "coffeescript": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "postcss-load-config": { + "optional": true + }, + "pug": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/svelte-preprocess/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", + "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tailwindcss/node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dev": true, + "dependencies": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, + "node_modules/tinybench": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.1.tgz", + "integrity": "sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.1.tgz", + "integrity": "sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", + "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", + "dev": true + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.11.tgz", + "integrity": "sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.32", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.0.tgz", + "integrity": "sha512-ETnQTHeAbbOxl7/pyBck9oAPZZZo+kYnFt1uQDD+hPReOc+wCjXw4r4jHriBRuVDB5isHmPXxrfc1yJnfBERqg==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-plugin-tailwind-purgecss": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-tailwind-purgecss/-/vite-plugin-tailwind-purgecss-0.2.0.tgz", + "integrity": "sha512-6Q+SaalUd0t3BOIIiCQPlbZQuYARVgjoC78X+fLbQJqIEy/9fC58aQgHMgi+CmYfVfZmJToA8YiLueSGEo2mng==", + "dev": true, + "dependencies": { + "estree-walker": "^3.0.3", + "purgecss": "6.0.0-alpha.0" + }, + "peerDependencies": { + "vite": "^4.1.1 || ^5.0.0" + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitefu": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "dev": true, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.0.tgz", + "integrity": "sha512-Ixs5m7BjqvLHXcibkzKRQUvD/XLw0E3rvqaCMlrm/0LMsA0309ZqYvTlPzkhh81VlEyVZXFlwWnkhb6/UMtcaQ==", + "dev": true, + "dependencies": { + "@vitest/expect": "1.2.0", + "@vitest/runner": "1.2.0", + "@vitest/snapshot": "1.2.0", + "@vitest/spy": "1.2.0", + "@vitest/utils": "1.2.0", + "acorn-walk": "^8.3.1", + "cac": "^6.7.14", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^1.3.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.1", + "vite": "^5.0.0", + "vite-node": "1.2.0", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "^1.0.0", + "@vitest/ui": "^1.0.0", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..4d6c42a5 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,50 @@ +{ + "name": "frontend", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "test": "npm run test:integration && npm run test:unit", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --plugin-search-dir . --check . && eslint .", + "format": "prettier --plugin-search-dir . --write .", + "test:integration": "playwright test", + "test:unit": "vitest" + }, + "devDependencies": { + "@playwright/test": "1.40.1", + "@skeletonlabs/skeleton": "2.7.0", + "@skeletonlabs/tw-plugin": "0.3.1", + "@sveltejs/adapter-static": "^3.0.1", + "@sveltejs/kit": "2.3.2", + "@sveltejs/vite-plugin-svelte": "^3.0.1", + "@tailwindcss/forms": "0.5.7", + "@tailwindcss/typography": "0.5.10", + "@types/node": "20.11.1", + "@typescript-eslint/eslint-plugin": "6.18.1", + "@typescript-eslint/parser": "6.18.1", + "autoprefixer": "10.4.16", + "eslint": "8.56.0", + "eslint-config-prettier": "9.1.0", + "eslint-plugin-svelte": "2.35.1", + "flatbuffers": "23.5.26", + "postcss": "8.4.33", + "prettier": "3.2.2", + "prettier-plugin-svelte": "3.1.2", + "svelte": "4.2.8", + "svelte-check": "3.6.3", + "tailwindcss": "3.4.1", + "tslib": "2.6.2", + "typescript": "5.3.3", + "vite": "^5.0.11", + "vite-plugin-tailwind-purgecss": "^0.2.0", + "vitest": "1.2.0" + }, + "type": "module", + "dependencies": { + "@floating-ui/dom": "1.5.4" + } +} diff --git a/WebUI/playwright.config.ts b/frontend/playwright.config.ts similarity index 100% rename from WebUI/playwright.config.ts rename to frontend/playwright.config.ts diff --git a/WebUI/postcss.config.cjs b/frontend/postcss.config.cjs similarity index 100% rename from WebUI/postcss.config.cjs rename to frontend/postcss.config.cjs diff --git a/WebUI/purgecss.config.js b/frontend/purgecss.config.js similarity index 100% rename from WebUI/purgecss.config.js rename to frontend/purgecss.config.js diff --git a/WebUI/src/app.d.ts b/frontend/src/app.d.ts similarity index 100% rename from WebUI/src/app.d.ts rename to frontend/src/app.d.ts diff --git a/WebUI/src/app.html b/frontend/src/app.html similarity index 85% rename from WebUI/src/app.html rename to frontend/src/app.html index b76ca170..e4cbcd5c 100644 --- a/WebUI/src/app.html +++ b/frontend/src/app.html @@ -8,7 +8,7 @@ OpenShock Captive Portal %sveltekit.head% - +
%sveltekit.body%
diff --git a/WebUI/src/app.postcss b/frontend/src/app.postcss similarity index 100% rename from WebUI/src/app.postcss rename to frontend/src/app.postcss diff --git a/WebUI/src/index.test.ts b/frontend/src/index.test.ts similarity index 100% rename from WebUI/src/index.test.ts rename to frontend/src/index.test.ts diff --git a/WebUI/src/lib/MessageHandlers/WifiNetworkEventHandler.ts b/frontend/src/lib/MessageHandlers/WifiNetworkEventHandler.ts similarity index 93% rename from WebUI/src/lib/MessageHandlers/WifiNetworkEventHandler.ts rename to frontend/src/lib/MessageHandlers/WifiNetworkEventHandler.ts index 5f5d42c7..9b5268c5 100644 --- a/WebUI/src/lib/MessageHandlers/WifiNetworkEventHandler.ts +++ b/frontend/src/lib/MessageHandlers/WifiNetworkEventHandler.ts @@ -147,9 +147,6 @@ export const WifiNetworkEventHandler: MessageHandler = (cli, msg) => { const payload = new WifiNetworkEvent(); msg.payload(payload); - const fbsNetwork = new FbsWifiNetwork(); - payload.network(fbsNetwork); - const eventType = payload.eventType(); if (eventType < 0 || eventType >= EventHandlers.length) { @@ -157,5 +154,13 @@ export const WifiNetworkEventHandler: MessageHandler = (cli, msg) => { return; } - EventHandlers[eventType](fbsNetwork); -}; + const networksLength = payload.networksLength(); + for (let i = 0; i < networksLength; i++) { + const fbsNetwork = payload.networks(i); + if (!fbsNetwork) { + console.warn('[WS] Received invalid wifi network event (null network)'); + continue; + } + EventHandlers[eventType](fbsNetwork); + } +}; \ No newline at end of file diff --git a/WebUI/src/lib/MessageHandlers/index.ts b/frontend/src/lib/MessageHandlers/index.ts similarity index 94% rename from WebUI/src/lib/MessageHandlers/index.ts rename to frontend/src/lib/MessageHandlers/index.ts index 061e883b..b848153a 100644 --- a/WebUI/src/lib/MessageHandlers/index.ts +++ b/frontend/src/lib/MessageHandlers/index.ts @@ -13,6 +13,7 @@ import { AccountLinkCommandResult } from '$lib/_fbs/open-shock/serialization/loc import { AccountLinkResultCode } from '$lib/_fbs/open-shock/serialization/local/account-link-result-code'; import { ErrorMessage } from '$lib/_fbs/open-shock/serialization/local/error-message'; import { WifiNetworkEventHandler } from './WifiNetworkEventHandler'; +import { mapConfig } from '$lib/mappers/ConfigMapper'; export type MessageHandler = (wsClient: WebSocketClient, message: DeviceToLocalMessage) => void; @@ -31,8 +32,11 @@ PayloadHandlers[DeviceToLocalMessagePayload.ReadyMessage] = (cli, msg) => { DeviceStateStore.update((store) => { store.wifiConnectedBSSID = payload.connectedWifi()?.bssid() || null; - store.gatewayPaired = payload.gatewayPaired(); - store.rfTxPin = payload.rftxPin(); + store.accountLinked = payload.accountLinked(); + store.config = mapConfig(payload.config()); + + console.log('[WS] Updated device state store: ', store); + return store; }); @@ -74,7 +78,7 @@ PayloadHandlers[DeviceToLocalMessagePayload.AccountLinkCommandResult] = (cli, ms if (result == AccountLinkResultCode.Success) { toastDelegator.trigger({ - message: 'Gateway paired successfully', + message: 'Account linked successfully', background: 'bg-green-500', }); } else { @@ -100,7 +104,7 @@ PayloadHandlers[DeviceToLocalMessagePayload.AccountLinkCommandResult] = (cli, ms break; } toastDelegator.trigger({ - message: 'Failed to pair gateway: ' + reason, + message: 'Failed to link account: ' + reason, background: 'bg-red-500', }); } diff --git a/WebUI/src/lib/Serializers/AccountLinkCommand.ts b/frontend/src/lib/Serializers/AccountLinkCommand.ts similarity index 83% rename from WebUI/src/lib/Serializers/AccountLinkCommand.ts rename to frontend/src/lib/Serializers/AccountLinkCommand.ts index 7fa466bb..5a51e0ea 100644 --- a/WebUI/src/lib/Serializers/AccountLinkCommand.ts +++ b/frontend/src/lib/Serializers/AccountLinkCommand.ts @@ -3,12 +3,12 @@ import { LocalToDeviceMessage } from '$lib/_fbs/open-shock/serialization/local/l import { LocalToDeviceMessagePayload } from '$lib/_fbs/open-shock/serialization/local/local-to-device-message-payload'; import { AccountLinkCommand } from '$lib/_fbs/open-shock/serialization/local/account-link-command'; -export function SerializeAccountLinkCommand(pairCode: string): Uint8Array { +export function SerializeAccountLinkCommand(linkCode: string): Uint8Array { const fbb = new FlatbufferBuilder(64); - const pairCodeOffset = fbb.createString(pairCode); + const linkCodeOffset = fbb.createString(linkCode); - const cmdOffset = AccountLinkCommand.createAccountLinkCommand(fbb, pairCodeOffset); + const cmdOffset = AccountLinkCommand.createAccountLinkCommand(fbb, linkCodeOffset); const payloadOffset = LocalToDeviceMessage.createLocalToDeviceMessage(fbb, LocalToDeviceMessagePayload.AccountLinkCommand, cmdOffset); diff --git a/WebUI/src/lib/Serializers/SetRfTxPinCommand.ts b/frontend/src/lib/Serializers/SetRfTxPinCommand.ts similarity index 100% rename from WebUI/src/lib/Serializers/SetRfTxPinCommand.ts rename to frontend/src/lib/Serializers/SetRfTxPinCommand.ts diff --git a/WebUI/src/lib/Serializers/WifiNetworkConnectCommand.ts b/frontend/src/lib/Serializers/WifiNetworkConnectCommand.ts similarity index 100% rename from WebUI/src/lib/Serializers/WifiNetworkConnectCommand.ts rename to frontend/src/lib/Serializers/WifiNetworkConnectCommand.ts diff --git a/WebUI/src/lib/Serializers/WifiNetworkDisconnectCommand.ts b/frontend/src/lib/Serializers/WifiNetworkDisconnectCommand.ts similarity index 100% rename from WebUI/src/lib/Serializers/WifiNetworkDisconnectCommand.ts rename to frontend/src/lib/Serializers/WifiNetworkDisconnectCommand.ts diff --git a/WebUI/src/lib/Serializers/WifiNetworkForgetCommand.ts b/frontend/src/lib/Serializers/WifiNetworkForgetCommand.ts similarity index 100% rename from WebUI/src/lib/Serializers/WifiNetworkForgetCommand.ts rename to frontend/src/lib/Serializers/WifiNetworkForgetCommand.ts diff --git a/WebUI/src/lib/Serializers/WifiNetworkSaveCommand.ts b/frontend/src/lib/Serializers/WifiNetworkSaveCommand.ts similarity index 100% rename from WebUI/src/lib/Serializers/WifiNetworkSaveCommand.ts rename to frontend/src/lib/Serializers/WifiNetworkSaveCommand.ts diff --git a/WebUI/src/lib/Serializers/WifiScanCommand.ts b/frontend/src/lib/Serializers/WifiScanCommand.ts similarity index 100% rename from WebUI/src/lib/Serializers/WifiScanCommand.ts rename to frontend/src/lib/Serializers/WifiScanCommand.ts diff --git a/WebUI/src/lib/TypeGuards/BasicGuards.ts b/frontend/src/lib/TypeGuards/BasicGuards.ts similarity index 100% rename from WebUI/src/lib/TypeGuards/BasicGuards.ts rename to frontend/src/lib/TypeGuards/BasicGuards.ts diff --git a/WebUI/src/lib/WebSocketClient.ts b/frontend/src/lib/WebSocketClient.ts similarity index 100% rename from WebUI/src/lib/WebSocketClient.ts rename to frontend/src/lib/WebSocketClient.ts diff --git a/frontend/src/lib/_fbs/open-shock/serialization/configuration.ts b/frontend/src/lib/_fbs/open-shock/serialization/configuration.ts new file mode 100644 index 00000000..b7193bf5 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/configuration.ts @@ -0,0 +1,14 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +export { BackendConfig } from './configuration/backend-config'; +export { CaptivePortalConfig } from './configuration/captive-portal-config'; +export { Config } from './configuration/config'; +export { OtaUpdateChannel } from './configuration/ota-update-channel'; +export { OtaUpdateConfig } from './configuration/ota-update-config'; +export { OtaUpdateStep } from './configuration/ota-update-step'; +export { RFConfig } from './configuration/rfconfig'; +export { SerialInputConfig } from './configuration/serial-input-config'; +export { WiFiConfig } from './configuration/wi-fi-config'; +export { WiFiCredentials } from './configuration/wi-fi-credentials'; diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/configuration/backend-config.ts b/frontend/src/lib/_fbs/open-shock/serialization/configuration/backend-config.ts similarity index 68% rename from WebUI/src/lib/_fbs/open-shock/serialization/configuration/backend-config.ts rename to frontend/src/lib/_fbs/open-shock/serialization/configuration/backend-config.ts index 66490f3e..54d691c1 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/configuration/backend-config.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/configuration/backend-config.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; export class BackendConfig { @@ -20,13 +22,19 @@ static getSizePrefixedRootAsBackendConfig(bb:flatbuffers.ByteBuffer, obj?:Backen return (obj || new BackendConfig()).__init(bb.readInt32(bb.position()) + bb.position(), bb); } -host():string|null -host(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null -host(optionalEncoding?:any):string|Uint8Array|null { +/** + * Domain name of the backend server, e.g. "api.shocklink.net" + */ +domain():string|null +domain(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +domain(optionalEncoding?:any):string|Uint8Array|null { const offset = this.bb!.__offset(this.bb_pos, 4); return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; } +/** + * Authentication token for the backend server + */ authToken():string|null authToken(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null authToken(optionalEncoding?:any):string|Uint8Array|null { @@ -38,8 +46,8 @@ static startBackendConfig(builder:flatbuffers.Builder) { builder.startObject(2); } -static addHost(builder:flatbuffers.Builder, hostOffset:flatbuffers.Offset) { - builder.addFieldOffset(0, hostOffset, 0); +static addDomain(builder:flatbuffers.Builder, domainOffset:flatbuffers.Offset) { + builder.addFieldOffset(0, domainOffset, 0); } static addAuthToken(builder:flatbuffers.Builder, authTokenOffset:flatbuffers.Offset) { @@ -51,9 +59,9 @@ static endBackendConfig(builder:flatbuffers.Builder):flatbuffers.Offset { return offset; } -static createBackendConfig(builder:flatbuffers.Builder, hostOffset:flatbuffers.Offset, authTokenOffset:flatbuffers.Offset):flatbuffers.Offset { +static createBackendConfig(builder:flatbuffers.Builder, domainOffset:flatbuffers.Offset, authTokenOffset:flatbuffers.Offset):flatbuffers.Offset { BackendConfig.startBackendConfig(builder); - BackendConfig.addHost(builder, hostOffset); + BackendConfig.addDomain(builder, domainOffset); BackendConfig.addAuthToken(builder, authTokenOffset); return BackendConfig.endBackendConfig(builder); } diff --git a/frontend/src/lib/_fbs/open-shock/serialization/configuration/captive-portal-config.ts b/frontend/src/lib/_fbs/open-shock/serialization/configuration/captive-portal-config.ts new file mode 100644 index 00000000..52f1c8eb --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/configuration/captive-portal-config.ts @@ -0,0 +1,52 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +export class CaptivePortalConfig { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):CaptivePortalConfig { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsCaptivePortalConfig(bb:flatbuffers.ByteBuffer, obj?:CaptivePortalConfig):CaptivePortalConfig { + return (obj || new CaptivePortalConfig()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsCaptivePortalConfig(bb:flatbuffers.ByteBuffer, obj?:CaptivePortalConfig):CaptivePortalConfig { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new CaptivePortalConfig()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +/** + * Whether the captive portal is forced to be enabled + * The captive portal will otherwise shut down when a gateway connection is established + */ +alwaysEnabled():boolean { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false; +} + +static startCaptivePortalConfig(builder:flatbuffers.Builder) { + builder.startObject(1); +} + +static addAlwaysEnabled(builder:flatbuffers.Builder, alwaysEnabled:boolean) { + builder.addFieldInt8(0, +alwaysEnabled, +false); +} + +static endCaptivePortalConfig(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createCaptivePortalConfig(builder:flatbuffers.Builder, alwaysEnabled:boolean):flatbuffers.Offset { + CaptivePortalConfig.startCaptivePortalConfig(builder); + CaptivePortalConfig.addAlwaysEnabled(builder, alwaysEnabled); + return CaptivePortalConfig.endCaptivePortalConfig(builder); +} +} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/configuration/config.ts b/frontend/src/lib/_fbs/open-shock/serialization/configuration/config.ts similarity index 55% rename from WebUI/src/lib/_fbs/open-shock/serialization/configuration/config.ts rename to frontend/src/lib/_fbs/open-shock/serialization/configuration/config.ts index 36d11e38..452f7dfa 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/configuration/config.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/configuration/config.ts @@ -1,11 +1,15 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; -import { BackendConfig } from '../../../open-shock/serialization/configuration/backend-config.js'; -import { CaptivePortalConfig } from '../../../open-shock/serialization/configuration/captive-portal-config.js'; -import { RFConfig } from '../../../open-shock/serialization/configuration/rfconfig.js'; -import { WiFiConfig } from '../../../open-shock/serialization/configuration/wi-fi-config.js'; +import { BackendConfig } from '../../../open-shock/serialization/configuration/backend-config'; +import { CaptivePortalConfig } from '../../../open-shock/serialization/configuration/captive-portal-config'; +import { OtaUpdateConfig } from '../../../open-shock/serialization/configuration/ota-update-config'; +import { RFConfig } from '../../../open-shock/serialization/configuration/rfconfig'; +import { SerialInputConfig } from '../../../open-shock/serialization/configuration/serial-input-config'; +import { WiFiConfig } from '../../../open-shock/serialization/configuration/wi-fi-config'; export class Config { @@ -26,32 +30,60 @@ static getSizePrefixedRootAsConfig(bb:flatbuffers.ByteBuffer, obj?:Config):Confi return (obj || new Config()).__init(bb.readInt32(bb.position()) + bb.position(), bb); } +/** + * RF Transmitter configuration + */ rf(obj?:RFConfig):RFConfig|null { const offset = this.bb!.__offset(this.bb_pos, 4); - return offset ? (obj || new RFConfig()).__init(this.bb_pos + offset, this.bb!) : null; + return offset ? (obj || new RFConfig()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; } +/** + * WiFi configuration + */ wifi(obj?:WiFiConfig):WiFiConfig|null { const offset = this.bb!.__offset(this.bb_pos, 6); return offset ? (obj || new WiFiConfig()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; } +/** + * Captive portal configuration + */ captivePortal(obj?:CaptivePortalConfig):CaptivePortalConfig|null { const offset = this.bb!.__offset(this.bb_pos, 8); - return offset ? (obj || new CaptivePortalConfig()).__init(this.bb_pos + offset, this.bb!) : null; + return offset ? (obj || new CaptivePortalConfig()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; } +/** + * Backend configuration + */ backend(obj?:BackendConfig):BackendConfig|null { const offset = this.bb!.__offset(this.bb_pos, 10); return offset ? (obj || new BackendConfig()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; } +/** + * Serial input configuration + */ +serialInput(obj?:SerialInputConfig):SerialInputConfig|null { + const offset = this.bb!.__offset(this.bb_pos, 12); + return offset ? (obj || new SerialInputConfig()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +/** + * OTA update configuration + */ +otaUpdate(obj?:OtaUpdateConfig):OtaUpdateConfig|null { + const offset = this.bb!.__offset(this.bb_pos, 14); + return offset ? (obj || new OtaUpdateConfig()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + static startConfig(builder:flatbuffers.Builder) { - builder.startObject(4); + builder.startObject(6); } static addRf(builder:flatbuffers.Builder, rfOffset:flatbuffers.Offset) { - builder.addFieldStruct(0, rfOffset, 0); + builder.addFieldOffset(0, rfOffset, 0); } static addWifi(builder:flatbuffers.Builder, wifiOffset:flatbuffers.Offset) { @@ -59,13 +91,21 @@ static addWifi(builder:flatbuffers.Builder, wifiOffset:flatbuffers.Offset) { } static addCaptivePortal(builder:flatbuffers.Builder, captivePortalOffset:flatbuffers.Offset) { - builder.addFieldStruct(2, captivePortalOffset, 0); + builder.addFieldOffset(2, captivePortalOffset, 0); } static addBackend(builder:flatbuffers.Builder, backendOffset:flatbuffers.Offset) { builder.addFieldOffset(3, backendOffset, 0); } +static addSerialInput(builder:flatbuffers.Builder, serialInputOffset:flatbuffers.Offset) { + builder.addFieldOffset(4, serialInputOffset, 0); +} + +static addOtaUpdate(builder:flatbuffers.Builder, otaUpdateOffset:flatbuffers.Offset) { + builder.addFieldOffset(5, otaUpdateOffset, 0); +} + static endConfig(builder:flatbuffers.Builder):flatbuffers.Offset { const offset = builder.endObject(); return offset; diff --git a/frontend/src/lib/_fbs/open-shock/serialization/configuration/ota-update-channel.ts b/frontend/src/lib/_fbs/open-shock/serialization/configuration/ota-update-channel.ts new file mode 100644 index 00000000..535cfb3b --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/configuration/ota-update-channel.ts @@ -0,0 +1,9 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +export enum OtaUpdateChannel { + Stable = 0, + Beta = 1, + Develop = 2 +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/configuration/ota-update-config.ts b/frontend/src/lib/_fbs/open-shock/serialization/configuration/ota-update-config.ts new file mode 100644 index 00000000..b3a29758 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/configuration/ota-update-config.ts @@ -0,0 +1,174 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { OtaUpdateChannel } from '../../../open-shock/serialization/configuration/ota-update-channel'; +import { OtaUpdateStep } from '../../../open-shock/serialization/configuration/ota-update-step'; + + +export class OtaUpdateConfig { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):OtaUpdateConfig { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsOtaUpdateConfig(bb:flatbuffers.ByteBuffer, obj?:OtaUpdateConfig):OtaUpdateConfig { + return (obj || new OtaUpdateConfig()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsOtaUpdateConfig(bb:flatbuffers.ByteBuffer, obj?:OtaUpdateConfig):OtaUpdateConfig { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new OtaUpdateConfig()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +/** + * Indicates whether OTA updates are enabled. + */ +isEnabled():boolean { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false; +} + +/** + * The domain name of the OTA Content Delivery Network (CDN). + */ +cdnDomain():string|null +cdnDomain(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +cdnDomain(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +/** + * The update channel to use. + */ +updateChannel():OtaUpdateChannel { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : OtaUpdateChannel.Stable; +} + +/** + * Indicates whether to check for updates on startup. + */ +checkOnStartup():boolean { + const offset = this.bb!.__offset(this.bb_pos, 10); + return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false; +} + +/** + * Indicates whether to check for updates periodically. + */ +checkPeriodically():boolean { + const offset = this.bb!.__offset(this.bb_pos, 12); + return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false; +} + +/** + * The interval in minutes between periodic update checks. + */ +checkInterval():number { + const offset = this.bb!.__offset(this.bb_pos, 14); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +/** + * Indicates if the backend is authorized to manage the device's update version on behalf of the user. + */ +allowBackendManagement():boolean { + const offset = this.bb!.__offset(this.bb_pos, 16); + return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false; +} + +/** + * Indicates if manual approval via serial input or captive portal is required before installing updates. + */ +requireManualApproval():boolean { + const offset = this.bb!.__offset(this.bb_pos, 18); + return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false; +} + +/** + * Update process ID, used to track the update process server-side across reboots. + */ +updateId():number { + const offset = this.bb!.__offset(this.bb_pos, 20); + return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0; +} + +/** + * Indicates what step of the update process the device is currently in, used to detect failed updates for status reporting. + */ +updateStep():OtaUpdateStep { + const offset = this.bb!.__offset(this.bb_pos, 22); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : OtaUpdateStep.None; +} + +static startOtaUpdateConfig(builder:flatbuffers.Builder) { + builder.startObject(10); +} + +static addIsEnabled(builder:flatbuffers.Builder, isEnabled:boolean) { + builder.addFieldInt8(0, +isEnabled, +false); +} + +static addCdnDomain(builder:flatbuffers.Builder, cdnDomainOffset:flatbuffers.Offset) { + builder.addFieldOffset(1, cdnDomainOffset, 0); +} + +static addUpdateChannel(builder:flatbuffers.Builder, updateChannel:OtaUpdateChannel) { + builder.addFieldInt8(2, updateChannel, OtaUpdateChannel.Stable); +} + +static addCheckOnStartup(builder:flatbuffers.Builder, checkOnStartup:boolean) { + builder.addFieldInt8(3, +checkOnStartup, +false); +} + +static addCheckPeriodically(builder:flatbuffers.Builder, checkPeriodically:boolean) { + builder.addFieldInt8(4, +checkPeriodically, +false); +} + +static addCheckInterval(builder:flatbuffers.Builder, checkInterval:number) { + builder.addFieldInt16(5, checkInterval, 0); +} + +static addAllowBackendManagement(builder:flatbuffers.Builder, allowBackendManagement:boolean) { + builder.addFieldInt8(6, +allowBackendManagement, +false); +} + +static addRequireManualApproval(builder:flatbuffers.Builder, requireManualApproval:boolean) { + builder.addFieldInt8(7, +requireManualApproval, +false); +} + +static addUpdateId(builder:flatbuffers.Builder, updateId:number) { + builder.addFieldInt32(8, updateId, 0); +} + +static addUpdateStep(builder:flatbuffers.Builder, updateStep:OtaUpdateStep) { + builder.addFieldInt8(9, updateStep, OtaUpdateStep.None); +} + +static endOtaUpdateConfig(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createOtaUpdateConfig(builder:flatbuffers.Builder, isEnabled:boolean, cdnDomainOffset:flatbuffers.Offset, updateChannel:OtaUpdateChannel, checkOnStartup:boolean, checkPeriodically:boolean, checkInterval:number, allowBackendManagement:boolean, requireManualApproval:boolean, updateId:number, updateStep:OtaUpdateStep):flatbuffers.Offset { + OtaUpdateConfig.startOtaUpdateConfig(builder); + OtaUpdateConfig.addIsEnabled(builder, isEnabled); + OtaUpdateConfig.addCdnDomain(builder, cdnDomainOffset); + OtaUpdateConfig.addUpdateChannel(builder, updateChannel); + OtaUpdateConfig.addCheckOnStartup(builder, checkOnStartup); + OtaUpdateConfig.addCheckPeriodically(builder, checkPeriodically); + OtaUpdateConfig.addCheckInterval(builder, checkInterval); + OtaUpdateConfig.addAllowBackendManagement(builder, allowBackendManagement); + OtaUpdateConfig.addRequireManualApproval(builder, requireManualApproval); + OtaUpdateConfig.addUpdateId(builder, updateId); + OtaUpdateConfig.addUpdateStep(builder, updateStep); + return OtaUpdateConfig.endOtaUpdateConfig(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/configuration/ota-update-step.ts b/frontend/src/lib/_fbs/open-shock/serialization/configuration/ota-update-step.ts new file mode 100644 index 00000000..2e1c4c61 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/configuration/ota-update-step.ts @@ -0,0 +1,12 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +export enum OtaUpdateStep { + None = 0, + Updating = 1, + Updated = 2, + Validating = 3, + Validated = 4, + RollingBack = 5 +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/configuration/rfconfig.ts b/frontend/src/lib/_fbs/open-shock/serialization/configuration/rfconfig.ts new file mode 100644 index 00000000..b34fb2a4 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/configuration/rfconfig.ts @@ -0,0 +1,64 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +export class RFConfig { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):RFConfig { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsRFConfig(bb:flatbuffers.ByteBuffer, obj?:RFConfig):RFConfig { + return (obj || new RFConfig()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsRFConfig(bb:flatbuffers.ByteBuffer, obj?:RFConfig):RFConfig { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new RFConfig()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +/** + * The GPIO pin connected to the RF modulator's data pin for transmitting (TX) + */ +txPin():number { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : 0; +} + +/** + * Whether to transmit keepalive messages to keep the devices from entering sleep mode + */ +keepaliveEnabled():boolean { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false; +} + +static startRFConfig(builder:flatbuffers.Builder) { + builder.startObject(2); +} + +static addTxPin(builder:flatbuffers.Builder, txPin:number) { + builder.addFieldInt8(0, txPin, 0); +} + +static addKeepaliveEnabled(builder:flatbuffers.Builder, keepaliveEnabled:boolean) { + builder.addFieldInt8(1, +keepaliveEnabled, +false); +} + +static endRFConfig(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createRFConfig(builder:flatbuffers.Builder, txPin:number, keepaliveEnabled:boolean):flatbuffers.Offset { + RFConfig.startRFConfig(builder); + RFConfig.addTxPin(builder, txPin); + RFConfig.addKeepaliveEnabled(builder, keepaliveEnabled); + return RFConfig.endRFConfig(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/configuration/serial-input-config.ts b/frontend/src/lib/_fbs/open-shock/serialization/configuration/serial-input-config.ts new file mode 100644 index 00000000..28d823e7 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/configuration/serial-input-config.ts @@ -0,0 +1,51 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +export class SerialInputConfig { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):SerialInputConfig { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsSerialInputConfig(bb:flatbuffers.ByteBuffer, obj?:SerialInputConfig):SerialInputConfig { + return (obj || new SerialInputConfig()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsSerialInputConfig(bb:flatbuffers.ByteBuffer, obj?:SerialInputConfig):SerialInputConfig { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new SerialInputConfig()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +/** + * Whether to echo typed characters back to the serial console + */ +echoEnabled():boolean { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : true; +} + +static startSerialInputConfig(builder:flatbuffers.Builder) { + builder.startObject(1); +} + +static addEchoEnabled(builder:flatbuffers.Builder, echoEnabled:boolean) { + builder.addFieldInt8(0, +echoEnabled, +true); +} + +static endSerialInputConfig(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createSerialInputConfig(builder:flatbuffers.Builder, echoEnabled:boolean):flatbuffers.Offset { + SerialInputConfig.startSerialInputConfig(builder); + SerialInputConfig.addEchoEnabled(builder, echoEnabled); + return SerialInputConfig.endSerialInputConfig(builder); +} +} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/configuration/wi-fi-config.ts b/frontend/src/lib/_fbs/open-shock/serialization/configuration/wi-fi-config.ts similarity index 89% rename from WebUI/src/lib/_fbs/open-shock/serialization/configuration/wi-fi-config.ts rename to frontend/src/lib/_fbs/open-shock/serialization/configuration/wi-fi-config.ts index 3b8fe4b5..126b275f 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/configuration/wi-fi-config.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/configuration/wi-fi-config.ts @@ -1,8 +1,10 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; -import { WiFiCredentials } from '../../../open-shock/serialization/configuration/wi-fi-credentials.js'; +import { WiFiCredentials } from '../../../open-shock/serialization/configuration/wi-fi-credentials'; export class WiFiConfig { @@ -23,6 +25,9 @@ static getSizePrefixedRootAsWiFiConfig(bb:flatbuffers.ByteBuffer, obj?:WiFiConfi return (obj || new WiFiConfig()).__init(bb.readInt32(bb.position()) + bb.position(), bb); } +/** + * Access point SSID + */ apSsid():string|null apSsid(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null apSsid(optionalEncoding?:any):string|Uint8Array|null { @@ -30,6 +35,9 @@ apSsid(optionalEncoding?:any):string|Uint8Array|null { return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; } +/** + * Device hostname + */ hostname():string|null hostname(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null hostname(optionalEncoding?:any):string|Uint8Array|null { @@ -37,6 +45,9 @@ hostname(optionalEncoding?:any):string|Uint8Array|null { return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; } +/** + * WiFi network credentials + */ credentials(index: number, obj?:WiFiCredentials):WiFiCredentials|null { const offset = this.bb!.__offset(this.bb_pos, 8); return offset ? (obj || new WiFiCredentials()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null; diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/configuration/wi-fi-credentials.ts b/frontend/src/lib/_fbs/open-shock/serialization/configuration/wi-fi-credentials.ts similarity index 69% rename from WebUI/src/lib/_fbs/open-shock/serialization/configuration/wi-fi-credentials.ts rename to frontend/src/lib/_fbs/open-shock/serialization/configuration/wi-fi-credentials.ts index 8c2f1320..5c894bbc 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/configuration/wi-fi-credentials.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/configuration/wi-fi-credentials.ts @@ -1,9 +1,8 @@ // automatically generated by the FlatBuffers compiler, do not modify -import * as flatbuffers from 'flatbuffers'; - -import { BSSID } from '../../../open-shock/serialization/configuration/bssid.js'; +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ +import * as flatbuffers from 'flatbuffers'; export class WiFiCredentials { bb: flatbuffers.ByteBuffer|null = null; @@ -23,11 +22,17 @@ static getSizePrefixedRootAsWiFiCredentials(bb:flatbuffers.ByteBuffer, obj?:WiFi return (obj || new WiFiCredentials()).__init(bb.readInt32(bb.position()) + bb.position(), bb); } +/** + * ID of the WiFi network credentials, used for referencing the credentials with a low memory footprint + */ id():number { const offset = this.bb!.__offset(this.bb_pos, 4); return offset ? this.bb!.readUint8(this.bb_pos + offset) : 0; } +/** + * SSID of the WiFi network + */ ssid():string|null ssid(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null ssid(optionalEncoding?:any):string|Uint8Array|null { @@ -35,20 +40,18 @@ ssid(optionalEncoding?:any):string|Uint8Array|null { return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; } -bssid(obj?:BSSID):BSSID|null { - const offset = this.bb!.__offset(this.bb_pos, 8); - return offset ? (obj || new BSSID()).__init(this.bb_pos + offset, this.bb!) : null; -} - +/** + * Password of the WiFi network + */ password():string|null password(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null password(optionalEncoding?:any):string|Uint8Array|null { - const offset = this.bb!.__offset(this.bb_pos, 10); + const offset = this.bb!.__offset(this.bb_pos, 8); return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; } static startWiFiCredentials(builder:flatbuffers.Builder) { - builder.startObject(4); + builder.startObject(3); } static addId(builder:flatbuffers.Builder, id:number) { @@ -59,12 +62,8 @@ static addSsid(builder:flatbuffers.Builder, ssidOffset:flatbuffers.Offset) { builder.addFieldOffset(1, ssidOffset, 0); } -static addBssid(builder:flatbuffers.Builder, bssidOffset:flatbuffers.Offset) { - builder.addFieldStruct(2, bssidOffset, 0); -} - static addPassword(builder:flatbuffers.Builder, passwordOffset:flatbuffers.Offset) { - builder.addFieldOffset(3, passwordOffset, 0); + builder.addFieldOffset(2, passwordOffset, 0); } static endWiFiCredentials(builder:flatbuffers.Builder):flatbuffers.Offset { @@ -72,4 +71,11 @@ static endWiFiCredentials(builder:flatbuffers.Builder):flatbuffers.Offset { return offset; } +static createWiFiCredentials(builder:flatbuffers.Builder, id:number, ssidOffset:flatbuffers.Offset, passwordOffset:flatbuffers.Offset):flatbuffers.Offset { + WiFiCredentials.startWiFiCredentials(builder); + WiFiCredentials.addId(builder, id); + WiFiCredentials.addSsid(builder, ssidOffset); + WiFiCredentials.addPassword(builder, passwordOffset); + return WiFiCredentials.endWiFiCredentials(builder); +} } diff --git a/frontend/src/lib/_fbs/open-shock/serialization/gateway.ts b/frontend/src/lib/_fbs/open-shock/serialization/gateway.ts new file mode 100644 index 00000000..655b96d7 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/gateway.ts @@ -0,0 +1,10 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +export { CaptivePortalConfig } from './gateway/captive-portal-config'; +export { GatewayToDeviceMessage } from './gateway/gateway-to-device-message'; +export { GatewayToDeviceMessagePayload } from './gateway/gateway-to-device-message-payload'; +export { OtaInstall } from './gateway/ota-install'; +export { ShockerCommand } from './gateway/shocker-command'; +export { ShockerCommandList } from './gateway/shocker-command-list'; diff --git a/frontend/src/lib/_fbs/open-shock/serialization/gateway/boot-status.ts b/frontend/src/lib/_fbs/open-shock/serialization/gateway/boot-status.ts new file mode 100644 index 00000000..f9fe5e9f --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/gateway/boot-status.ts @@ -0,0 +1,65 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { FirmwareBootType } from '../../../open-shock/serialization/types/firmware-boot-type'; +import { SemVer } from '../../../open-shock/serialization/types/sem-ver'; + + +export class BootStatus { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):BootStatus { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsBootStatus(bb:flatbuffers.ByteBuffer, obj?:BootStatus):BootStatus { + return (obj || new BootStatus()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsBootStatus(bb:flatbuffers.ByteBuffer, obj?:BootStatus):BootStatus { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new BootStatus()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +bootType():FirmwareBootType { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : FirmwareBootType.Normal; +} + +firmwareVersion(obj?:SemVer):SemVer|null { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? (obj || new SemVer()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +otaUpdateId():number { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0; +} + +static startBootStatus(builder:flatbuffers.Builder) { + builder.startObject(3); +} + +static addBootType(builder:flatbuffers.Builder, bootType:FirmwareBootType) { + builder.addFieldInt8(0, bootType, FirmwareBootType.Normal); +} + +static addFirmwareVersion(builder:flatbuffers.Builder, firmwareVersionOffset:flatbuffers.Offset) { + builder.addFieldOffset(1, firmwareVersionOffset, 0); +} + +static addOtaUpdateId(builder:flatbuffers.Builder, otaUpdateId:number) { + builder.addFieldInt32(2, otaUpdateId, 0); +} + +static endBootStatus(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/captive-portal-config.ts b/frontend/src/lib/_fbs/open-shock/serialization/gateway/captive-portal-config.ts similarity index 79% rename from WebUI/src/lib/_fbs/open-shock/serialization/captive-portal-config.ts rename to frontend/src/lib/_fbs/open-shock/serialization/gateway/captive-portal-config.ts index e09ee61c..14d15463 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/captive-portal-config.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/gateway/captive-portal-config.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; export class CaptivePortalConfig { diff --git a/frontend/src/lib/_fbs/open-shock/serialization/gateway/device-to-gateway-message-payload.ts b/frontend/src/lib/_fbs/open-shock/serialization/gateway/device-to-gateway-message-payload.ts new file mode 100644 index 00000000..2da46cc9 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/gateway/device-to-gateway-message-payload.ts @@ -0,0 +1,50 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import { BootStatus } from '../../../open-shock/serialization/gateway/boot-status'; +import { KeepAlive } from '../../../open-shock/serialization/gateway/keep-alive'; +import { OtaInstallFailed } from '../../../open-shock/serialization/gateway/ota-install-failed'; +import { OtaInstallProgress } from '../../../open-shock/serialization/gateway/ota-install-progress'; +import { OtaInstallStarted } from '../../../open-shock/serialization/gateway/ota-install-started'; + + +export enum DeviceToGatewayMessagePayload { + NONE = 0, + KeepAlive = 1, + BootStatus = 2, + OtaInstallStarted = 3, + OtaInstallProgress = 4, + OtaInstallFailed = 5 +} + +export function unionToDeviceToGatewayMessagePayload( + type: DeviceToGatewayMessagePayload, + accessor: (obj:BootStatus|KeepAlive|OtaInstallFailed|OtaInstallProgress|OtaInstallStarted) => BootStatus|KeepAlive|OtaInstallFailed|OtaInstallProgress|OtaInstallStarted|null +): BootStatus|KeepAlive|OtaInstallFailed|OtaInstallProgress|OtaInstallStarted|null { + switch(DeviceToGatewayMessagePayload[type]) { + case 'NONE': return null; + case 'KeepAlive': return accessor(new KeepAlive())! as KeepAlive; + case 'BootStatus': return accessor(new BootStatus())! as BootStatus; + case 'OtaInstallStarted': return accessor(new OtaInstallStarted())! as OtaInstallStarted; + case 'OtaInstallProgress': return accessor(new OtaInstallProgress())! as OtaInstallProgress; + case 'OtaInstallFailed': return accessor(new OtaInstallFailed())! as OtaInstallFailed; + default: return null; + } +} + +export function unionListToDeviceToGatewayMessagePayload( + type: DeviceToGatewayMessagePayload, + accessor: (index: number, obj:BootStatus|KeepAlive|OtaInstallFailed|OtaInstallProgress|OtaInstallStarted) => BootStatus|KeepAlive|OtaInstallFailed|OtaInstallProgress|OtaInstallStarted|null, + index: number +): BootStatus|KeepAlive|OtaInstallFailed|OtaInstallProgress|OtaInstallStarted|null { + switch(DeviceToGatewayMessagePayload[type]) { + case 'NONE': return null; + case 'KeepAlive': return accessor(index, new KeepAlive())! as KeepAlive; + case 'BootStatus': return accessor(index, new BootStatus())! as BootStatus; + case 'OtaInstallStarted': return accessor(index, new OtaInstallStarted())! as OtaInstallStarted; + case 'OtaInstallProgress': return accessor(index, new OtaInstallProgress())! as OtaInstallProgress; + case 'OtaInstallFailed': return accessor(index, new OtaInstallFailed())! as OtaInstallFailed; + default: return null; + } +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/gateway/device-to-gateway-message.ts b/frontend/src/lib/_fbs/open-shock/serialization/gateway/device-to-gateway-message.ts new file mode 100644 index 00000000..1f0048d6 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/gateway/device-to-gateway-message.ts @@ -0,0 +1,69 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { DeviceToGatewayMessagePayload, unionToDeviceToGatewayMessagePayload, unionListToDeviceToGatewayMessagePayload } from '../../../open-shock/serialization/gateway/device-to-gateway-message-payload'; + + +export class DeviceToGatewayMessage { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):DeviceToGatewayMessage { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsDeviceToGatewayMessage(bb:flatbuffers.ByteBuffer, obj?:DeviceToGatewayMessage):DeviceToGatewayMessage { + return (obj || new DeviceToGatewayMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsDeviceToGatewayMessage(bb:flatbuffers.ByteBuffer, obj?:DeviceToGatewayMessage):DeviceToGatewayMessage { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new DeviceToGatewayMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +payloadType():DeviceToGatewayMessagePayload { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : DeviceToGatewayMessagePayload.NONE; +} + +payload(obj:any):any|null { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.__union(obj, this.bb_pos + offset) : null; +} + +static startDeviceToGatewayMessage(builder:flatbuffers.Builder) { + builder.startObject(2); +} + +static addPayloadType(builder:flatbuffers.Builder, payloadType:DeviceToGatewayMessagePayload) { + builder.addFieldInt8(0, payloadType, DeviceToGatewayMessagePayload.NONE); +} + +static addPayload(builder:flatbuffers.Builder, payloadOffset:flatbuffers.Offset) { + builder.addFieldOffset(1, payloadOffset, 0); +} + +static endDeviceToGatewayMessage(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static finishDeviceToGatewayMessageBuffer(builder:flatbuffers.Builder, offset:flatbuffers.Offset) { + builder.finish(offset); +} + +static finishSizePrefixedDeviceToGatewayMessageBuffer(builder:flatbuffers.Builder, offset:flatbuffers.Offset) { + builder.finish(offset, undefined, true); +} + +static createDeviceToGatewayMessage(builder:flatbuffers.Builder, payloadType:DeviceToGatewayMessagePayload, payloadOffset:flatbuffers.Offset):flatbuffers.Offset { + DeviceToGatewayMessage.startDeviceToGatewayMessage(builder); + DeviceToGatewayMessage.addPayloadType(builder, payloadType); + DeviceToGatewayMessage.addPayload(builder, payloadOffset); + return DeviceToGatewayMessage.endDeviceToGatewayMessage(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/gateway/gateway-to-device-message-payload.ts b/frontend/src/lib/_fbs/open-shock/serialization/gateway/gateway-to-device-message-payload.ts new file mode 100644 index 00000000..0b12d57b --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/gateway/gateway-to-device-message-payload.ts @@ -0,0 +1,42 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import { CaptivePortalConfig } from '../../../open-shock/serialization/gateway/captive-portal-config'; +import { OtaInstall } from '../../../open-shock/serialization/gateway/ota-install'; +import { ShockerCommandList } from '../../../open-shock/serialization/gateway/shocker-command-list'; + + +export enum GatewayToDeviceMessagePayload { + NONE = 0, + ShockerCommandList = 1, + CaptivePortalConfig = 2, + OtaInstall = 3 +} + +export function unionToGatewayToDeviceMessagePayload( + type: GatewayToDeviceMessagePayload, + accessor: (obj:CaptivePortalConfig|OtaInstall|ShockerCommandList) => CaptivePortalConfig|OtaInstall|ShockerCommandList|null +): CaptivePortalConfig|OtaInstall|ShockerCommandList|null { + switch(GatewayToDeviceMessagePayload[type]) { + case 'NONE': return null; + case 'ShockerCommandList': return accessor(new ShockerCommandList())! as ShockerCommandList; + case 'CaptivePortalConfig': return accessor(new CaptivePortalConfig())! as CaptivePortalConfig; + case 'OtaInstall': return accessor(new OtaInstall())! as OtaInstall; + default: return null; + } +} + +export function unionListToGatewayToDeviceMessagePayload( + type: GatewayToDeviceMessagePayload, + accessor: (index: number, obj:CaptivePortalConfig|OtaInstall|ShockerCommandList) => CaptivePortalConfig|OtaInstall|ShockerCommandList|null, + index: number +): CaptivePortalConfig|OtaInstall|ShockerCommandList|null { + switch(GatewayToDeviceMessagePayload[type]) { + case 'NONE': return null; + case 'ShockerCommandList': return accessor(index, new ShockerCommandList())! as ShockerCommandList; + case 'CaptivePortalConfig': return accessor(index, new CaptivePortalConfig())! as CaptivePortalConfig; + case 'OtaInstall': return accessor(index, new OtaInstall())! as OtaInstall; + default: return null; + } +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/gateway/gateway-to-device-message.ts b/frontend/src/lib/_fbs/open-shock/serialization/gateway/gateway-to-device-message.ts new file mode 100644 index 00000000..85a7ecc1 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/gateway/gateway-to-device-message.ts @@ -0,0 +1,69 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { GatewayToDeviceMessagePayload, unionToGatewayToDeviceMessagePayload, unionListToGatewayToDeviceMessagePayload } from '../../../open-shock/serialization/gateway/gateway-to-device-message-payload'; + + +export class GatewayToDeviceMessage { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):GatewayToDeviceMessage { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsGatewayToDeviceMessage(bb:flatbuffers.ByteBuffer, obj?:GatewayToDeviceMessage):GatewayToDeviceMessage { + return (obj || new GatewayToDeviceMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsGatewayToDeviceMessage(bb:flatbuffers.ByteBuffer, obj?:GatewayToDeviceMessage):GatewayToDeviceMessage { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new GatewayToDeviceMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +payloadType():GatewayToDeviceMessagePayload { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readUint8(this.bb_pos + offset) : GatewayToDeviceMessagePayload.NONE; +} + +payload(obj:any):any|null { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.__union(obj, this.bb_pos + offset) : null; +} + +static startGatewayToDeviceMessage(builder:flatbuffers.Builder) { + builder.startObject(2); +} + +static addPayloadType(builder:flatbuffers.Builder, payloadType:GatewayToDeviceMessagePayload) { + builder.addFieldInt8(0, payloadType, GatewayToDeviceMessagePayload.NONE); +} + +static addPayload(builder:flatbuffers.Builder, payloadOffset:flatbuffers.Offset) { + builder.addFieldOffset(1, payloadOffset, 0); +} + +static endGatewayToDeviceMessage(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static finishGatewayToDeviceMessageBuffer(builder:flatbuffers.Builder, offset:flatbuffers.Offset) { + builder.finish(offset); +} + +static finishSizePrefixedGatewayToDeviceMessageBuffer(builder:flatbuffers.Builder, offset:flatbuffers.Offset) { + builder.finish(offset, undefined, true); +} + +static createGatewayToDeviceMessage(builder:flatbuffers.Builder, payloadType:GatewayToDeviceMessagePayload, payloadOffset:flatbuffers.Offset):flatbuffers.Offset { + GatewayToDeviceMessage.startGatewayToDeviceMessage(builder); + GatewayToDeviceMessage.addPayloadType(builder, payloadType); + GatewayToDeviceMessage.addPayload(builder, payloadOffset); + return GatewayToDeviceMessage.endGatewayToDeviceMessage(builder); +} +} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/keep-alive.ts b/frontend/src/lib/_fbs/open-shock/serialization/gateway/keep-alive.ts similarity index 78% rename from WebUI/src/lib/_fbs/open-shock/serialization/keep-alive.ts rename to frontend/src/lib/_fbs/open-shock/serialization/gateway/keep-alive.ts index 3a720863..3a19cbe7 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/keep-alive.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/gateway/keep-alive.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; export class KeepAlive { diff --git a/frontend/src/lib/_fbs/open-shock/serialization/gateway/ota-install-failed.ts b/frontend/src/lib/_fbs/open-shock/serialization/gateway/ota-install-failed.ts new file mode 100644 index 00000000..81571d1d --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/gateway/ota-install-failed.ts @@ -0,0 +1,70 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +export class OtaInstallFailed { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):OtaInstallFailed { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsOtaInstallFailed(bb:flatbuffers.ByteBuffer, obj?:OtaInstallFailed):OtaInstallFailed { + return (obj || new OtaInstallFailed()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsOtaInstallFailed(bb:flatbuffers.ByteBuffer, obj?:OtaInstallFailed):OtaInstallFailed { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new OtaInstallFailed()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +updateId():number { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0; +} + +message():string|null +message(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +message(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +fatal():boolean { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false; +} + +static startOtaInstallFailed(builder:flatbuffers.Builder) { + builder.startObject(3); +} + +static addUpdateId(builder:flatbuffers.Builder, updateId:number) { + builder.addFieldInt32(0, updateId, 0); +} + +static addMessage(builder:flatbuffers.Builder, messageOffset:flatbuffers.Offset) { + builder.addFieldOffset(1, messageOffset, 0); +} + +static addFatal(builder:flatbuffers.Builder, fatal:boolean) { + builder.addFieldInt8(2, +fatal, +false); +} + +static endOtaInstallFailed(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createOtaInstallFailed(builder:flatbuffers.Builder, updateId:number, messageOffset:flatbuffers.Offset, fatal:boolean):flatbuffers.Offset { + OtaInstallFailed.startOtaInstallFailed(builder); + OtaInstallFailed.addUpdateId(builder, updateId); + OtaInstallFailed.addMessage(builder, messageOffset); + OtaInstallFailed.addFatal(builder, fatal); + return OtaInstallFailed.endOtaInstallFailed(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/gateway/ota-install-progress-task.ts b/frontend/src/lib/_fbs/open-shock/serialization/gateway/ota-install-progress-task.ts new file mode 100644 index 00000000..e9a25149 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/gateway/ota-install-progress-task.ts @@ -0,0 +1,13 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +export enum OtaInstallProgressTask { + FetchingMetadata = 0, + PreparingForInstall = 1, + FlashingFilesystem = 2, + VerifyingFilesystem = 3, + FlashingApplication = 4, + MarkingApplicationBootable = 5, + Rebooting = 6 +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/gateway/ota-install-progress.ts b/frontend/src/lib/_fbs/open-shock/serialization/gateway/ota-install-progress.ts new file mode 100644 index 00000000..70f756a0 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/gateway/ota-install-progress.ts @@ -0,0 +1,71 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { OtaInstallProgressTask } from '../../../open-shock/serialization/gateway/ota-install-progress-task'; + + +export class OtaInstallProgress { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):OtaInstallProgress { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsOtaInstallProgress(bb:flatbuffers.ByteBuffer, obj?:OtaInstallProgress):OtaInstallProgress { + return (obj || new OtaInstallProgress()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsOtaInstallProgress(bb:flatbuffers.ByteBuffer, obj?:OtaInstallProgress):OtaInstallProgress { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new OtaInstallProgress()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +updateId():number { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0; +} + +task():OtaInstallProgressTask { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.readInt8(this.bb_pos + offset) : OtaInstallProgressTask.FetchingMetadata; +} + +progress():number { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? this.bb!.readFloat32(this.bb_pos + offset) : 0.0; +} + +static startOtaInstallProgress(builder:flatbuffers.Builder) { + builder.startObject(3); +} + +static addUpdateId(builder:flatbuffers.Builder, updateId:number) { + builder.addFieldInt32(0, updateId, 0); +} + +static addTask(builder:flatbuffers.Builder, task:OtaInstallProgressTask) { + builder.addFieldInt8(1, task, OtaInstallProgressTask.FetchingMetadata); +} + +static addProgress(builder:flatbuffers.Builder, progress:number) { + builder.addFieldFloat32(2, progress, 0.0); +} + +static endOtaInstallProgress(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createOtaInstallProgress(builder:flatbuffers.Builder, updateId:number, task:OtaInstallProgressTask, progress:number):flatbuffers.Offset { + OtaInstallProgress.startOtaInstallProgress(builder); + OtaInstallProgress.addUpdateId(builder, updateId); + OtaInstallProgress.addTask(builder, task); + OtaInstallProgress.addProgress(builder, progress); + return OtaInstallProgress.endOtaInstallProgress(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/gateway/ota-install-started.ts b/frontend/src/lib/_fbs/open-shock/serialization/gateway/ota-install-started.ts new file mode 100644 index 00000000..bd82a2bd --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/gateway/ota-install-started.ts @@ -0,0 +1,55 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { SemVer } from '../../../open-shock/serialization/types/sem-ver'; + + +export class OtaInstallStarted { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):OtaInstallStarted { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsOtaInstallStarted(bb:flatbuffers.ByteBuffer, obj?:OtaInstallStarted):OtaInstallStarted { + return (obj || new OtaInstallStarted()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsOtaInstallStarted(bb:flatbuffers.ByteBuffer, obj?:OtaInstallStarted):OtaInstallStarted { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new OtaInstallStarted()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +updateId():number { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0; +} + +version(obj?:SemVer):SemVer|null { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? (obj || new SemVer()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +static startOtaInstallStarted(builder:flatbuffers.Builder) { + builder.startObject(2); +} + +static addUpdateId(builder:flatbuffers.Builder, updateId:number) { + builder.addFieldInt32(0, updateId, 0); +} + +static addVersion(builder:flatbuffers.Builder, versionOffset:flatbuffers.Offset) { + builder.addFieldOffset(1, versionOffset, 0); +} + +static endOtaInstallStarted(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/gateway/ota-install.ts b/frontend/src/lib/_fbs/open-shock/serialization/gateway/ota-install.ts new file mode 100644 index 00000000..d3d17fc5 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/gateway/ota-install.ts @@ -0,0 +1,51 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { SemVer } from '../../../open-shock/serialization/types/sem-ver'; + + +export class OtaInstall { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):OtaInstall { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsOtaInstall(bb:flatbuffers.ByteBuffer, obj?:OtaInstall):OtaInstall { + return (obj || new OtaInstall()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsOtaInstall(bb:flatbuffers.ByteBuffer, obj?:OtaInstall):OtaInstall { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new OtaInstall()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +version(obj?:SemVer):SemVer|null { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? (obj || new SemVer()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +static startOtaInstall(builder:flatbuffers.Builder) { + builder.startObject(1); +} + +static addVersion(builder:flatbuffers.Builder, versionOffset:flatbuffers.Offset) { + builder.addFieldOffset(0, versionOffset, 0); +} + +static endOtaInstall(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createOtaInstall(builder:flatbuffers.Builder, versionOffset:flatbuffers.Offset):flatbuffers.Offset { + OtaInstall.startOtaInstall(builder); + OtaInstall.addVersion(builder, versionOffset); + return OtaInstall.endOtaInstall(builder); +} +} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/shocker-command-list.ts b/frontend/src/lib/_fbs/open-shock/serialization/gateway/shocker-command-list.ts similarity index 87% rename from WebUI/src/lib/_fbs/open-shock/serialization/shocker-command-list.ts rename to frontend/src/lib/_fbs/open-shock/serialization/gateway/shocker-command-list.ts index 5d4090fb..efdbdc61 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/shocker-command-list.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/gateway/shocker-command-list.ts @@ -1,8 +1,10 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; -import { ShockerCommand } from '../../open-shock/serialization/shocker-command.js'; +import { ShockerCommand } from '../../../open-shock/serialization/gateway/shocker-command'; export class ShockerCommandList { diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/shocker-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/gateway/shocker-command.ts similarity index 74% rename from WebUI/src/lib/_fbs/open-shock/serialization/shocker-command.ts rename to frontend/src/lib/_fbs/open-shock/serialization/gateway/shocker-command.ts index 9f99c9d6..abf14f61 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/shocker-command.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/gateway/shocker-command.ts @@ -1,9 +1,11 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; -import { ShockerCommandType } from '../../open-shock/serialization/types/shocker-command-type.js'; -import { ShockerModelType } from '../../open-shock/serialization/types/shocker-model-type.js'; +import { ShockerCommandType } from '../../../open-shock/serialization/types/shocker-command-type'; +import { ShockerModelType } from '../../../open-shock/serialization/types/shocker-model-type'; export class ShockerCommand { diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local.ts b/frontend/src/lib/_fbs/open-shock/serialization/local.ts new file mode 100644 index 00000000..f7b55800 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local.ts @@ -0,0 +1,23 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +export { AccountLinkCommand } from './local/account-link-command'; +export { AccountUnlinkCommand } from './local/account-unlink-command'; +export { LocalToDeviceMessage } from './local/local-to-device-message'; +export { LocalToDeviceMessagePayload } from './local/local-to-device-message-payload'; +export { OtaUpdateCheckForUpdatesCommand } from './local/ota-update-check-for-updates-command'; +export { OtaUpdateHandleUpdateRequestCommand } from './local/ota-update-handle-update-request-command'; +export { OtaUpdateSetAllowBackendManagementCommand } from './local/ota-update-set-allow-backend-management-command'; +export { OtaUpdateSetCheckIntervalCommand } from './local/ota-update-set-check-interval-command'; +export { OtaUpdateSetDomainCommand } from './local/ota-update-set-domain-command'; +export { OtaUpdateSetIsEnabledCommand } from './local/ota-update-set-is-enabled-command'; +export { OtaUpdateSetRequireManualApprovalCommand } from './local/ota-update-set-require-manual-approval-command'; +export { OtaUpdateSetUpdateChannelCommand } from './local/ota-update-set-update-channel-command'; +export { OtaUpdateStartUpdateCommand } from './local/ota-update-start-update-command'; +export { SetRfTxPinCommand } from './local/set-rf-tx-pin-command'; +export { WifiNetworkConnectCommand } from './local/wifi-network-connect-command'; +export { WifiNetworkDisconnectCommand } from './local/wifi-network-disconnect-command'; +export { WifiNetworkForgetCommand } from './local/wifi-network-forget-command'; +export { WifiNetworkSaveCommand } from './local/wifi-network-save-command'; +export { WifiScanCommand } from './local/wifi-scan-command'; diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/account-link-command-result.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/account-link-command-result.ts similarity index 77% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/account-link-command-result.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/account-link-command-result.ts index 9f56f273..491d0809 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/account-link-command-result.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/account-link-command-result.ts @@ -1,8 +1,10 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; -import { AccountLinkResultCode } from '../../../open-shock/serialization/local/account-link-result-code.js'; +import { AccountLinkResultCode } from '../../../open-shock/serialization/local/account-link-result-code'; export class AccountLinkCommandResult { diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/account-link-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/account-link-command.ts similarity index 90% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/account-link-command.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/account-link-command.ts index 1f03301f..1c0188d3 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/account-link-command.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/account-link-command.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; export class AccountLinkCommand { diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/account-link-result-code.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/account-link-result-code.ts similarity index 61% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/account-link-result-code.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/account-link-result-code.ts index 766b8b4b..48872d5b 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/account-link-result-code.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/account-link-result-code.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + export enum AccountLinkResultCode { Success = 0, CodeRequired = 1, diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/account-unlink-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/account-unlink-command.ts similarity index 79% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/account-unlink-command.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/account-unlink-command.ts index 408a903a..936e2ecf 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/account-unlink-command.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/account-unlink-command.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; export class AccountUnlinkCommand { diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/device-to-local-message-payload.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/device-to-local-message-payload.ts similarity index 89% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/device-to-local-message-payload.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/device-to-local-message-payload.ts index d843397b..5e5dd9e0 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/device-to-local-message-payload.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/device-to-local-message-payload.ts @@ -1,13 +1,15 @@ // automatically generated by the FlatBuffers compiler, do not modify -import { AccountLinkCommandResult } from '../../../open-shock/serialization/local/account-link-command-result.js'; -import { ErrorMessage } from '../../../open-shock/serialization/local/error-message.js'; -import { ReadyMessage } from '../../../open-shock/serialization/local/ready-message.js'; -import { SetRfTxPinCommandResult } from '../../../open-shock/serialization/local/set-rf-tx-pin-command-result.js'; -import { WifiGotIpEvent } from '../../../open-shock/serialization/local/wifi-got-ip-event.js'; -import { WifiLostIpEvent } from '../../../open-shock/serialization/local/wifi-lost-ip-event.js'; -import { WifiNetworkEvent } from '../../../open-shock/serialization/local/wifi-network-event.js'; -import { WifiScanStatusMessage } from '../../../open-shock/serialization/local/wifi-scan-status-message.js'; +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import { AccountLinkCommandResult } from '../../../open-shock/serialization/local/account-link-command-result'; +import { ErrorMessage } from '../../../open-shock/serialization/local/error-message'; +import { ReadyMessage } from '../../../open-shock/serialization/local/ready-message'; +import { SetRfTxPinCommandResult } from '../../../open-shock/serialization/local/set-rf-tx-pin-command-result'; +import { WifiGotIpEvent } from '../../../open-shock/serialization/local/wifi-got-ip-event'; +import { WifiLostIpEvent } from '../../../open-shock/serialization/local/wifi-lost-ip-event'; +import { WifiNetworkEvent } from '../../../open-shock/serialization/local/wifi-network-event'; +import { WifiScanStatusMessage } from '../../../open-shock/serialization/local/wifi-scan-status-message'; export enum DeviceToLocalMessagePayload { diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/device-to-local-message.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/device-to-local-message.ts similarity index 92% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/device-to-local-message.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/device-to-local-message.ts index 0e651810..953e2207 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/device-to-local-message.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/device-to-local-message.ts @@ -1,8 +1,10 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; -import { DeviceToLocalMessagePayload, unionToDeviceToLocalMessagePayload, unionListToDeviceToLocalMessagePayload } from '../../../open-shock/serialization/local/device-to-local-message-payload.js'; +import { DeviceToLocalMessagePayload, unionToDeviceToLocalMessagePayload, unionListToDeviceToLocalMessagePayload } from '../../../open-shock/serialization/local/device-to-local-message-payload'; export class DeviceToLocalMessage { diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/error-message.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/error-message.ts similarity index 90% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/error-message.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/error-message.ts index 9a9c7be4..9b62f56a 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/error-message.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/error-message.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; export class ErrorMessage { diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/local-to-device-message-payload.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/local-to-device-message-payload.ts new file mode 100644 index 00000000..fc1999fb --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/local-to-device-message-payload.ts @@ -0,0 +1,98 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import { AccountLinkCommand } from '../../../open-shock/serialization/local/account-link-command'; +import { AccountUnlinkCommand } from '../../../open-shock/serialization/local/account-unlink-command'; +import { OtaUpdateCheckForUpdatesCommand } from '../../../open-shock/serialization/local/ota-update-check-for-updates-command'; +import { OtaUpdateHandleUpdateRequestCommand } from '../../../open-shock/serialization/local/ota-update-handle-update-request-command'; +import { OtaUpdateSetAllowBackendManagementCommand } from '../../../open-shock/serialization/local/ota-update-set-allow-backend-management-command'; +import { OtaUpdateSetCheckIntervalCommand } from '../../../open-shock/serialization/local/ota-update-set-check-interval-command'; +import { OtaUpdateSetDomainCommand } from '../../../open-shock/serialization/local/ota-update-set-domain-command'; +import { OtaUpdateSetIsEnabledCommand } from '../../../open-shock/serialization/local/ota-update-set-is-enabled-command'; +import { OtaUpdateSetRequireManualApprovalCommand } from '../../../open-shock/serialization/local/ota-update-set-require-manual-approval-command'; +import { OtaUpdateSetUpdateChannelCommand } from '../../../open-shock/serialization/local/ota-update-set-update-channel-command'; +import { OtaUpdateStartUpdateCommand } from '../../../open-shock/serialization/local/ota-update-start-update-command'; +import { SetRfTxPinCommand } from '../../../open-shock/serialization/local/set-rf-tx-pin-command'; +import { WifiNetworkConnectCommand } from '../../../open-shock/serialization/local/wifi-network-connect-command'; +import { WifiNetworkDisconnectCommand } from '../../../open-shock/serialization/local/wifi-network-disconnect-command'; +import { WifiNetworkForgetCommand } from '../../../open-shock/serialization/local/wifi-network-forget-command'; +import { WifiNetworkSaveCommand } from '../../../open-shock/serialization/local/wifi-network-save-command'; +import { WifiScanCommand } from '../../../open-shock/serialization/local/wifi-scan-command'; + + +export enum LocalToDeviceMessagePayload { + NONE = 0, + WifiScanCommand = 1, + WifiNetworkSaveCommand = 2, + WifiNetworkForgetCommand = 3, + WifiNetworkConnectCommand = 4, + WifiNetworkDisconnectCommand = 5, + OtaUpdateSetIsEnabledCommand = 6, + OtaUpdateSetDomainCommand = 7, + OtaUpdateSetUpdateChannelCommand = 8, + OtaUpdateSetCheckIntervalCommand = 9, + OtaUpdateSetAllowBackendManagementCommand = 10, + OtaUpdateSetRequireManualApprovalCommand = 11, + OtaUpdateHandleUpdateRequestCommand = 12, + OtaUpdateCheckForUpdatesCommand = 13, + OtaUpdateStartUpdateCommand = 14, + AccountLinkCommand = 15, + AccountUnlinkCommand = 16, + SetRfTxPinCommand = 17 +} + +export function unionToLocalToDeviceMessagePayload( + type: LocalToDeviceMessagePayload, + accessor: (obj:AccountLinkCommand|AccountUnlinkCommand|OtaUpdateCheckForUpdatesCommand|OtaUpdateHandleUpdateRequestCommand|OtaUpdateSetAllowBackendManagementCommand|OtaUpdateSetCheckIntervalCommand|OtaUpdateSetDomainCommand|OtaUpdateSetIsEnabledCommand|OtaUpdateSetRequireManualApprovalCommand|OtaUpdateSetUpdateChannelCommand|OtaUpdateStartUpdateCommand|SetRfTxPinCommand|WifiNetworkConnectCommand|WifiNetworkDisconnectCommand|WifiNetworkForgetCommand|WifiNetworkSaveCommand|WifiScanCommand) => AccountLinkCommand|AccountUnlinkCommand|OtaUpdateCheckForUpdatesCommand|OtaUpdateHandleUpdateRequestCommand|OtaUpdateSetAllowBackendManagementCommand|OtaUpdateSetCheckIntervalCommand|OtaUpdateSetDomainCommand|OtaUpdateSetIsEnabledCommand|OtaUpdateSetRequireManualApprovalCommand|OtaUpdateSetUpdateChannelCommand|OtaUpdateStartUpdateCommand|SetRfTxPinCommand|WifiNetworkConnectCommand|WifiNetworkDisconnectCommand|WifiNetworkForgetCommand|WifiNetworkSaveCommand|WifiScanCommand|null +): AccountLinkCommand|AccountUnlinkCommand|OtaUpdateCheckForUpdatesCommand|OtaUpdateHandleUpdateRequestCommand|OtaUpdateSetAllowBackendManagementCommand|OtaUpdateSetCheckIntervalCommand|OtaUpdateSetDomainCommand|OtaUpdateSetIsEnabledCommand|OtaUpdateSetRequireManualApprovalCommand|OtaUpdateSetUpdateChannelCommand|OtaUpdateStartUpdateCommand|SetRfTxPinCommand|WifiNetworkConnectCommand|WifiNetworkDisconnectCommand|WifiNetworkForgetCommand|WifiNetworkSaveCommand|WifiScanCommand|null { + switch(LocalToDeviceMessagePayload[type]) { + case 'NONE': return null; + case 'WifiScanCommand': return accessor(new WifiScanCommand())! as WifiScanCommand; + case 'WifiNetworkSaveCommand': return accessor(new WifiNetworkSaveCommand())! as WifiNetworkSaveCommand; + case 'WifiNetworkForgetCommand': return accessor(new WifiNetworkForgetCommand())! as WifiNetworkForgetCommand; + case 'WifiNetworkConnectCommand': return accessor(new WifiNetworkConnectCommand())! as WifiNetworkConnectCommand; + case 'WifiNetworkDisconnectCommand': return accessor(new WifiNetworkDisconnectCommand())! as WifiNetworkDisconnectCommand; + case 'OtaUpdateSetIsEnabledCommand': return accessor(new OtaUpdateSetIsEnabledCommand())! as OtaUpdateSetIsEnabledCommand; + case 'OtaUpdateSetDomainCommand': return accessor(new OtaUpdateSetDomainCommand())! as OtaUpdateSetDomainCommand; + case 'OtaUpdateSetUpdateChannelCommand': return accessor(new OtaUpdateSetUpdateChannelCommand())! as OtaUpdateSetUpdateChannelCommand; + case 'OtaUpdateSetCheckIntervalCommand': return accessor(new OtaUpdateSetCheckIntervalCommand())! as OtaUpdateSetCheckIntervalCommand; + case 'OtaUpdateSetAllowBackendManagementCommand': return accessor(new OtaUpdateSetAllowBackendManagementCommand())! as OtaUpdateSetAllowBackendManagementCommand; + case 'OtaUpdateSetRequireManualApprovalCommand': return accessor(new OtaUpdateSetRequireManualApprovalCommand())! as OtaUpdateSetRequireManualApprovalCommand; + case 'OtaUpdateHandleUpdateRequestCommand': return accessor(new OtaUpdateHandleUpdateRequestCommand())! as OtaUpdateHandleUpdateRequestCommand; + case 'OtaUpdateCheckForUpdatesCommand': return accessor(new OtaUpdateCheckForUpdatesCommand())! as OtaUpdateCheckForUpdatesCommand; + case 'OtaUpdateStartUpdateCommand': return accessor(new OtaUpdateStartUpdateCommand())! as OtaUpdateStartUpdateCommand; + case 'AccountLinkCommand': return accessor(new AccountLinkCommand())! as AccountLinkCommand; + case 'AccountUnlinkCommand': return accessor(new AccountUnlinkCommand())! as AccountUnlinkCommand; + case 'SetRfTxPinCommand': return accessor(new SetRfTxPinCommand())! as SetRfTxPinCommand; + default: return null; + } +} + +export function unionListToLocalToDeviceMessagePayload( + type: LocalToDeviceMessagePayload, + accessor: (index: number, obj:AccountLinkCommand|AccountUnlinkCommand|OtaUpdateCheckForUpdatesCommand|OtaUpdateHandleUpdateRequestCommand|OtaUpdateSetAllowBackendManagementCommand|OtaUpdateSetCheckIntervalCommand|OtaUpdateSetDomainCommand|OtaUpdateSetIsEnabledCommand|OtaUpdateSetRequireManualApprovalCommand|OtaUpdateSetUpdateChannelCommand|OtaUpdateStartUpdateCommand|SetRfTxPinCommand|WifiNetworkConnectCommand|WifiNetworkDisconnectCommand|WifiNetworkForgetCommand|WifiNetworkSaveCommand|WifiScanCommand) => AccountLinkCommand|AccountUnlinkCommand|OtaUpdateCheckForUpdatesCommand|OtaUpdateHandleUpdateRequestCommand|OtaUpdateSetAllowBackendManagementCommand|OtaUpdateSetCheckIntervalCommand|OtaUpdateSetDomainCommand|OtaUpdateSetIsEnabledCommand|OtaUpdateSetRequireManualApprovalCommand|OtaUpdateSetUpdateChannelCommand|OtaUpdateStartUpdateCommand|SetRfTxPinCommand|WifiNetworkConnectCommand|WifiNetworkDisconnectCommand|WifiNetworkForgetCommand|WifiNetworkSaveCommand|WifiScanCommand|null, + index: number +): AccountLinkCommand|AccountUnlinkCommand|OtaUpdateCheckForUpdatesCommand|OtaUpdateHandleUpdateRequestCommand|OtaUpdateSetAllowBackendManagementCommand|OtaUpdateSetCheckIntervalCommand|OtaUpdateSetDomainCommand|OtaUpdateSetIsEnabledCommand|OtaUpdateSetRequireManualApprovalCommand|OtaUpdateSetUpdateChannelCommand|OtaUpdateStartUpdateCommand|SetRfTxPinCommand|WifiNetworkConnectCommand|WifiNetworkDisconnectCommand|WifiNetworkForgetCommand|WifiNetworkSaveCommand|WifiScanCommand|null { + switch(LocalToDeviceMessagePayload[type]) { + case 'NONE': return null; + case 'WifiScanCommand': return accessor(index, new WifiScanCommand())! as WifiScanCommand; + case 'WifiNetworkSaveCommand': return accessor(index, new WifiNetworkSaveCommand())! as WifiNetworkSaveCommand; + case 'WifiNetworkForgetCommand': return accessor(index, new WifiNetworkForgetCommand())! as WifiNetworkForgetCommand; + case 'WifiNetworkConnectCommand': return accessor(index, new WifiNetworkConnectCommand())! as WifiNetworkConnectCommand; + case 'WifiNetworkDisconnectCommand': return accessor(index, new WifiNetworkDisconnectCommand())! as WifiNetworkDisconnectCommand; + case 'OtaUpdateSetIsEnabledCommand': return accessor(index, new OtaUpdateSetIsEnabledCommand())! as OtaUpdateSetIsEnabledCommand; + case 'OtaUpdateSetDomainCommand': return accessor(index, new OtaUpdateSetDomainCommand())! as OtaUpdateSetDomainCommand; + case 'OtaUpdateSetUpdateChannelCommand': return accessor(index, new OtaUpdateSetUpdateChannelCommand())! as OtaUpdateSetUpdateChannelCommand; + case 'OtaUpdateSetCheckIntervalCommand': return accessor(index, new OtaUpdateSetCheckIntervalCommand())! as OtaUpdateSetCheckIntervalCommand; + case 'OtaUpdateSetAllowBackendManagementCommand': return accessor(index, new OtaUpdateSetAllowBackendManagementCommand())! as OtaUpdateSetAllowBackendManagementCommand; + case 'OtaUpdateSetRequireManualApprovalCommand': return accessor(index, new OtaUpdateSetRequireManualApprovalCommand())! as OtaUpdateSetRequireManualApprovalCommand; + case 'OtaUpdateHandleUpdateRequestCommand': return accessor(index, new OtaUpdateHandleUpdateRequestCommand())! as OtaUpdateHandleUpdateRequestCommand; + case 'OtaUpdateCheckForUpdatesCommand': return accessor(index, new OtaUpdateCheckForUpdatesCommand())! as OtaUpdateCheckForUpdatesCommand; + case 'OtaUpdateStartUpdateCommand': return accessor(index, new OtaUpdateStartUpdateCommand())! as OtaUpdateStartUpdateCommand; + case 'AccountLinkCommand': return accessor(index, new AccountLinkCommand())! as AccountLinkCommand; + case 'AccountUnlinkCommand': return accessor(index, new AccountUnlinkCommand())! as AccountUnlinkCommand; + case 'SetRfTxPinCommand': return accessor(index, new SetRfTxPinCommand())! as SetRfTxPinCommand; + default: return null; + } +} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/local-to-device-message.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/local-to-device-message.ts similarity index 92% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/local-to-device-message.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/local-to-device-message.ts index 11d03739..91a2a4a8 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/local-to-device-message.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/local-to-device-message.ts @@ -1,8 +1,10 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; -import { LocalToDeviceMessagePayload, unionToLocalToDeviceMessagePayload, unionListToLocalToDeviceMessagePayload } from '../../../open-shock/serialization/local/local-to-device-message-payload.js'; +import { LocalToDeviceMessagePayload, unionToLocalToDeviceMessagePayload, unionListToLocalToDeviceMessagePayload } from '../../../open-shock/serialization/local/local-to-device-message-payload'; export class LocalToDeviceMessage { diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-check-for-updates-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-check-for-updates-command.ts new file mode 100644 index 00000000..d44738a4 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-check-for-updates-command.ts @@ -0,0 +1,50 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +export class OtaUpdateCheckForUpdatesCommand { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):OtaUpdateCheckForUpdatesCommand { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsOtaUpdateCheckForUpdatesCommand(bb:flatbuffers.ByteBuffer, obj?:OtaUpdateCheckForUpdatesCommand):OtaUpdateCheckForUpdatesCommand { + return (obj || new OtaUpdateCheckForUpdatesCommand()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsOtaUpdateCheckForUpdatesCommand(bb:flatbuffers.ByteBuffer, obj?:OtaUpdateCheckForUpdatesCommand):OtaUpdateCheckForUpdatesCommand { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new OtaUpdateCheckForUpdatesCommand()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +channel():string|null +channel(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +channel(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +static startOtaUpdateCheckForUpdatesCommand(builder:flatbuffers.Builder) { + builder.startObject(1); +} + +static addChannel(builder:flatbuffers.Builder, channelOffset:flatbuffers.Offset) { + builder.addFieldOffset(0, channelOffset, 0); +} + +static endOtaUpdateCheckForUpdatesCommand(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createOtaUpdateCheckForUpdatesCommand(builder:flatbuffers.Builder, channelOffset:flatbuffers.Offset):flatbuffers.Offset { + OtaUpdateCheckForUpdatesCommand.startOtaUpdateCheckForUpdatesCommand(builder); + OtaUpdateCheckForUpdatesCommand.addChannel(builder, channelOffset); + return OtaUpdateCheckForUpdatesCommand.endOtaUpdateCheckForUpdatesCommand(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-handle-update-request-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-handle-update-request-command.ts new file mode 100644 index 00000000..82e5cd43 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-handle-update-request-command.ts @@ -0,0 +1,30 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +export class OtaUpdateHandleUpdateRequestCommand { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):OtaUpdateHandleUpdateRequestCommand { + this.bb_pos = i; + this.bb = bb; + return this; +} + +accept():boolean { + return !!this.bb!.readInt8(this.bb_pos); +} + +static sizeOf():number { + return 1; +} + +static createOtaUpdateHandleUpdateRequestCommand(builder:flatbuffers.Builder, accept: boolean):flatbuffers.Offset { + builder.prep(1, 1); + builder.writeInt8(Number(Boolean(accept))); + return builder.offset(); +} + +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-allow-backend-management-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-allow-backend-management-command.ts new file mode 100644 index 00000000..239cb149 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-allow-backend-management-command.ts @@ -0,0 +1,30 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +export class OtaUpdateSetAllowBackendManagementCommand { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):OtaUpdateSetAllowBackendManagementCommand { + this.bb_pos = i; + this.bb = bb; + return this; +} + +allow():boolean { + return !!this.bb!.readInt8(this.bb_pos); +} + +static sizeOf():number { + return 1; +} + +static createOtaUpdateSetAllowBackendManagementCommand(builder:flatbuffers.Builder, allow: boolean):flatbuffers.Offset { + builder.prep(1, 1); + builder.writeInt8(Number(Boolean(allow))); + return builder.offset(); +} + +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-check-interval-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-check-interval-command.ts new file mode 100644 index 00000000..b7c57545 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-check-interval-command.ts @@ -0,0 +1,30 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +export class OtaUpdateSetCheckIntervalCommand { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):OtaUpdateSetCheckIntervalCommand { + this.bb_pos = i; + this.bb = bb; + return this; +} + +interval():number { + return this.bb!.readUint16(this.bb_pos); +} + +static sizeOf():number { + return 2; +} + +static createOtaUpdateSetCheckIntervalCommand(builder:flatbuffers.Builder, interval: number):flatbuffers.Offset { + builder.prep(2, 2); + builder.writeInt16(interval); + return builder.offset(); +} + +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-domain-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-domain-command.ts new file mode 100644 index 00000000..f4475a98 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-domain-command.ts @@ -0,0 +1,50 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +export class OtaUpdateSetDomainCommand { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):OtaUpdateSetDomainCommand { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsOtaUpdateSetDomainCommand(bb:flatbuffers.ByteBuffer, obj?:OtaUpdateSetDomainCommand):OtaUpdateSetDomainCommand { + return (obj || new OtaUpdateSetDomainCommand()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsOtaUpdateSetDomainCommand(bb:flatbuffers.ByteBuffer, obj?:OtaUpdateSetDomainCommand):OtaUpdateSetDomainCommand { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new OtaUpdateSetDomainCommand()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +domain():string|null +domain(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +domain(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +static startOtaUpdateSetDomainCommand(builder:flatbuffers.Builder) { + builder.startObject(1); +} + +static addDomain(builder:flatbuffers.Builder, domainOffset:flatbuffers.Offset) { + builder.addFieldOffset(0, domainOffset, 0); +} + +static endOtaUpdateSetDomainCommand(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createOtaUpdateSetDomainCommand(builder:flatbuffers.Builder, domainOffset:flatbuffers.Offset):flatbuffers.Offset { + OtaUpdateSetDomainCommand.startOtaUpdateSetDomainCommand(builder); + OtaUpdateSetDomainCommand.addDomain(builder, domainOffset); + return OtaUpdateSetDomainCommand.endOtaUpdateSetDomainCommand(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-is-enabled-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-is-enabled-command.ts new file mode 100644 index 00000000..eccce2ea --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-is-enabled-command.ts @@ -0,0 +1,30 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +export class OtaUpdateSetIsEnabledCommand { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):OtaUpdateSetIsEnabledCommand { + this.bb_pos = i; + this.bb = bb; + return this; +} + +enabled():boolean { + return !!this.bb!.readInt8(this.bb_pos); +} + +static sizeOf():number { + return 1; +} + +static createOtaUpdateSetIsEnabledCommand(builder:flatbuffers.Builder, enabled: boolean):flatbuffers.Offset { + builder.prep(1, 1); + builder.writeInt8(Number(Boolean(enabled))); + return builder.offset(); +} + +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-require-manual-approval-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-require-manual-approval-command.ts new file mode 100644 index 00000000..4df4af6a --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-require-manual-approval-command.ts @@ -0,0 +1,30 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +export class OtaUpdateSetRequireManualApprovalCommand { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):OtaUpdateSetRequireManualApprovalCommand { + this.bb_pos = i; + this.bb = bb; + return this; +} + +require():boolean { + return !!this.bb!.readInt8(this.bb_pos); +} + +static sizeOf():number { + return 1; +} + +static createOtaUpdateSetRequireManualApprovalCommand(builder:flatbuffers.Builder, require: boolean):flatbuffers.Offset { + builder.prep(1, 1); + builder.writeInt8(Number(Boolean(require))); + return builder.offset(); +} + +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-update-channel-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-update-channel-command.ts new file mode 100644 index 00000000..039aa734 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-set-update-channel-command.ts @@ -0,0 +1,50 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +export class OtaUpdateSetUpdateChannelCommand { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):OtaUpdateSetUpdateChannelCommand { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsOtaUpdateSetUpdateChannelCommand(bb:flatbuffers.ByteBuffer, obj?:OtaUpdateSetUpdateChannelCommand):OtaUpdateSetUpdateChannelCommand { + return (obj || new OtaUpdateSetUpdateChannelCommand()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsOtaUpdateSetUpdateChannelCommand(bb:flatbuffers.ByteBuffer, obj?:OtaUpdateSetUpdateChannelCommand):OtaUpdateSetUpdateChannelCommand { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new OtaUpdateSetUpdateChannelCommand()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +channel():string|null +channel(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +channel(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +static startOtaUpdateSetUpdateChannelCommand(builder:flatbuffers.Builder) { + builder.startObject(1); +} + +static addChannel(builder:flatbuffers.Builder, channelOffset:flatbuffers.Offset) { + builder.addFieldOffset(0, channelOffset, 0); +} + +static endOtaUpdateSetUpdateChannelCommand(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createOtaUpdateSetUpdateChannelCommand(builder:flatbuffers.Builder, channelOffset:flatbuffers.Offset):flatbuffers.Offset { + OtaUpdateSetUpdateChannelCommand.startOtaUpdateSetUpdateChannelCommand(builder); + OtaUpdateSetUpdateChannelCommand.addChannel(builder, channelOffset); + return OtaUpdateSetUpdateChannelCommand.endOtaUpdateSetUpdateChannelCommand(builder); +} +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-start-update-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-start-update-command.ts new file mode 100644 index 00000000..447711ec --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/ota-update-start-update-command.ts @@ -0,0 +1,62 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +export class OtaUpdateStartUpdateCommand { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):OtaUpdateStartUpdateCommand { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsOtaUpdateStartUpdateCommand(bb:flatbuffers.ByteBuffer, obj?:OtaUpdateStartUpdateCommand):OtaUpdateStartUpdateCommand { + return (obj || new OtaUpdateStartUpdateCommand()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsOtaUpdateStartUpdateCommand(bb:flatbuffers.ByteBuffer, obj?:OtaUpdateStartUpdateCommand):OtaUpdateStartUpdateCommand { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new OtaUpdateStartUpdateCommand()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +channel():string|null +channel(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +channel(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +version():string|null +version(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +version(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +static startOtaUpdateStartUpdateCommand(builder:flatbuffers.Builder) { + builder.startObject(2); +} + +static addChannel(builder:flatbuffers.Builder, channelOffset:flatbuffers.Offset) { + builder.addFieldOffset(0, channelOffset, 0); +} + +static addVersion(builder:flatbuffers.Builder, versionOffset:flatbuffers.Offset) { + builder.addFieldOffset(1, versionOffset, 0); +} + +static endOtaUpdateStartUpdateCommand(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createOtaUpdateStartUpdateCommand(builder:flatbuffers.Builder, channelOffset:flatbuffers.Offset, versionOffset:flatbuffers.Offset):flatbuffers.Offset { + OtaUpdateStartUpdateCommand.startOtaUpdateStartUpdateCommand(builder); + OtaUpdateStartUpdateCommand.addChannel(builder, channelOffset); + OtaUpdateStartUpdateCommand.addVersion(builder, versionOffset); + return OtaUpdateStartUpdateCommand.endOtaUpdateStartUpdateCommand(builder); +} +} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/ready-message.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/ready-message.ts similarity index 72% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/ready-message.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/ready-message.ts index 4e1d79f7..a4402724 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/ready-message.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/ready-message.ts @@ -1,8 +1,11 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; -import { WifiNetwork } from '../../../open-shock/serialization/types/wifi-network.js'; +import { Config } from '../../../open-shock/serialization/configuration/config'; +import { WifiNetwork } from '../../../open-shock/serialization/types/wifi-network'; export class ReadyMessage { @@ -33,14 +36,14 @@ connectedWifi(obj?:WifiNetwork):WifiNetwork|null { return offset ? (obj || new WifiNetwork()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; } -gatewayPaired():boolean { +accountLinked():boolean { const offset = this.bb!.__offset(this.bb_pos, 8); return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false; } -rftxPin():number { +config(obj?:Config):Config|null { const offset = this.bb!.__offset(this.bb_pos, 10); - return offset ? this.bb!.readUint8(this.bb_pos + offset) : 0; + return offset ? (obj || new Config()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; } static startReadyMessage(builder:flatbuffers.Builder) { @@ -55,12 +58,12 @@ static addConnectedWifi(builder:flatbuffers.Builder, connectedWifiOffset:flatbuf builder.addFieldOffset(1, connectedWifiOffset, 0); } -static addGatewayPaired(builder:flatbuffers.Builder, gatewayPaired:boolean) { - builder.addFieldInt8(2, +gatewayPaired, +false); +static addAccountLinked(builder:flatbuffers.Builder, accountLinked:boolean) { + builder.addFieldInt8(2, +accountLinked, +false); } -static addRftxPin(builder:flatbuffers.Builder, rftxPin:number) { - builder.addFieldInt8(3, rftxPin, 0); +static addConfig(builder:flatbuffers.Builder, configOffset:flatbuffers.Offset) { + builder.addFieldOffset(3, configOffset, 0); } static endReadyMessage(builder:flatbuffers.Builder):flatbuffers.Offset { diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/set-rf-pin-result-code.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/set-rf-pin-result-code.ts similarity index 52% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/set-rf-pin-result-code.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/set-rf-pin-result-code.ts index 0963ae4e..4e608b12 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/set-rf-pin-result-code.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/set-rf-pin-result-code.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + export enum SetRfPinResultCode { Success = 0, InvalidPin = 1, diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/set-rf-tx-pin-command-result.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/set-rf-tx-pin-command-result.ts similarity index 79% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/set-rf-tx-pin-command-result.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/set-rf-tx-pin-command-result.ts index 9f676ad2..94ea4a2a 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/set-rf-tx-pin-command-result.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/set-rf-tx-pin-command-result.ts @@ -1,8 +1,10 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; -import { SetRfPinResultCode } from '../../../open-shock/serialization/local/set-rf-pin-result-code.js'; +import { SetRfPinResultCode } from '../../../open-shock/serialization/local/set-rf-pin-result-code'; export class SetRfTxPinCommandResult { diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/set-rf-tx-pin-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/set-rf-tx-pin-command.ts similarity index 78% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/set-rf-tx-pin-command.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/set-rf-tx-pin-command.ts index 6a339ba8..7d1e9892 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/set-rf-tx-pin-command.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/set-rf-tx-pin-command.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; export class SetRfTxPinCommand { diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-got-ip-event.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-got-ip-event.ts similarity index 89% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-got-ip-event.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/wifi-got-ip-event.ts index b2cb409a..bc8dc682 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-got-ip-event.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-got-ip-event.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; export class WifiGotIpEvent { diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-lost-ip-event.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-lost-ip-event.ts similarity index 90% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-lost-ip-event.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/wifi-lost-ip-event.ts index d65f3662..a6309328 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-lost-ip-event.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-lost-ip-event.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; export class WifiLostIpEvent { diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-network-connect-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-network-connect-command.ts similarity index 91% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-network-connect-command.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/wifi-network-connect-command.ts index 3c936407..45a66c20 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-network-connect-command.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-network-connect-command.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; export class WifiNetworkConnectCommand { diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-network-disconnect-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-network-disconnect-command.ts similarity index 80% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-network-disconnect-command.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/wifi-network-disconnect-command.ts index cf824ab8..563b9e98 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-network-disconnect-command.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-network-disconnect-command.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; export class WifiNetworkDisconnectCommand { diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-network-event.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-network-event.ts similarity index 52% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-network-event.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/wifi-network-event.ts index 1a279fad..d5670352 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-network-event.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-network-event.ts @@ -1,9 +1,11 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; -import { WifiNetwork } from '../../../open-shock/serialization/types/wifi-network.js'; -import { WifiNetworkEventType } from '../../../open-shock/serialization/types/wifi-network-event-type.js'; +import { WifiNetwork } from '../../../open-shock/serialization/types/wifi-network'; +import { WifiNetworkEventType } from '../../../open-shock/serialization/types/wifi-network-event-type'; export class WifiNetworkEvent { @@ -29,9 +31,14 @@ eventType():WifiNetworkEventType { return offset ? this.bb!.readUint8(this.bb_pos + offset) : WifiNetworkEventType.Discovered; } -network(obj?:WifiNetwork):WifiNetwork|null { +networks(index: number, obj?:WifiNetwork):WifiNetwork|null { const offset = this.bb!.__offset(this.bb_pos, 6); - return offset ? (obj || new WifiNetwork()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; + return offset ? (obj || new WifiNetwork()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null; +} + +networksLength():number { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0; } static startWifiNetworkEvent(builder:flatbuffers.Builder) { @@ -42,8 +49,20 @@ static addEventType(builder:flatbuffers.Builder, eventType:WifiNetworkEventType) builder.addFieldInt8(0, eventType, WifiNetworkEventType.Discovered); } -static addNetwork(builder:flatbuffers.Builder, networkOffset:flatbuffers.Offset) { - builder.addFieldOffset(1, networkOffset, 0); +static addNetworks(builder:flatbuffers.Builder, networksOffset:flatbuffers.Offset) { + builder.addFieldOffset(1, networksOffset, 0); +} + +static createNetworksVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset { + builder.startVector(4, data.length, 4); + for (let i = data.length - 1; i >= 0; i--) { + builder.addOffset(data[i]!); + } + return builder.endVector(); +} + +static startNetworksVector(builder:flatbuffers.Builder, numElems:number) { + builder.startVector(4, numElems, 4); } static endWifiNetworkEvent(builder:flatbuffers.Builder):flatbuffers.Offset { @@ -51,4 +70,10 @@ static endWifiNetworkEvent(builder:flatbuffers.Builder):flatbuffers.Offset { return offset; } +static createWifiNetworkEvent(builder:flatbuffers.Builder, eventType:WifiNetworkEventType, networksOffset:flatbuffers.Offset):flatbuffers.Offset { + WifiNetworkEvent.startWifiNetworkEvent(builder); + WifiNetworkEvent.addEventType(builder, eventType); + WifiNetworkEvent.addNetworks(builder, networksOffset); + return WifiNetworkEvent.endWifiNetworkEvent(builder); +} } diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-network-forget-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-network-forget-command.ts similarity index 90% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-network-forget-command.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/wifi-network-forget-command.ts index a20cd458..b8bd4bf9 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-network-forget-command.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-network-forget-command.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; export class WifiNetworkForgetCommand { diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-network-save-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-network-save-command.ts similarity index 92% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-network-save-command.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/wifi-network-save-command.ts index a8b3aaf7..1742227b 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-network-save-command.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-network-save-command.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; export class WifiNetworkSaveCommand { diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-scan-command.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-scan-command.ts similarity index 78% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-scan-command.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/wifi-scan-command.ts index 97f16782..ab5b0b95 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-scan-command.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-scan-command.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; export class WifiScanCommand { diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-scan-status-message.ts b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-scan-status-message.ts similarity index 78% rename from WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-scan-status-message.ts rename to frontend/src/lib/_fbs/open-shock/serialization/local/wifi-scan-status-message.ts index d4eb8ae9..38547d89 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/local/wifi-scan-status-message.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/local/wifi-scan-status-message.ts @@ -1,8 +1,10 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; -import { WifiScanStatus } from '../../../open-shock/serialization/types/wifi-scan-status.js'; +import { WifiScanStatus } from '../../../open-shock/serialization/types/wifi-scan-status'; export class WifiScanStatusMessage { diff --git a/frontend/src/lib/_fbs/open-shock/serialization/types.ts b/frontend/src/lib/_fbs/open-shock/serialization/types.ts new file mode 100644 index 00000000..6e7cf011 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/types.ts @@ -0,0 +1,5 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +export { WifiScanStatus } from './types/wifi-scan-status'; diff --git a/frontend/src/lib/_fbs/open-shock/serialization/types/firmware-boot-type.ts b/frontend/src/lib/_fbs/open-shock/serialization/types/firmware-boot-type.ts new file mode 100644 index 00000000..fea6e2a8 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/types/firmware-boot-type.ts @@ -0,0 +1,9 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +export enum FirmwareBootType { + Normal = 0, + NewFirmware = 1, + Rollback = 2 +} diff --git a/frontend/src/lib/_fbs/open-shock/serialization/types/sem-ver.ts b/frontend/src/lib/_fbs/open-shock/serialization/types/sem-ver.ts new file mode 100644 index 00000000..f141fcb2 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/types/sem-ver.ts @@ -0,0 +1,92 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +export class SemVer { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):SemVer { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsSemVer(bb:flatbuffers.ByteBuffer, obj?:SemVer):SemVer { + return (obj || new SemVer()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsSemVer(bb:flatbuffers.ByteBuffer, obj?:SemVer):SemVer { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new SemVer()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +major():number { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +minor():number { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +patch():number { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? this.bb!.readUint16(this.bb_pos + offset) : 0; +} + +prerelease():string|null +prerelease(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +prerelease(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 10); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +build():string|null +build(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +build(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 12); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +static startSemVer(builder:flatbuffers.Builder) { + builder.startObject(5); +} + +static addMajor(builder:flatbuffers.Builder, major:number) { + builder.addFieldInt16(0, major, 0); +} + +static addMinor(builder:flatbuffers.Builder, minor:number) { + builder.addFieldInt16(1, minor, 0); +} + +static addPatch(builder:flatbuffers.Builder, patch:number) { + builder.addFieldInt16(2, patch, 0); +} + +static addPrerelease(builder:flatbuffers.Builder, prereleaseOffset:flatbuffers.Offset) { + builder.addFieldOffset(3, prereleaseOffset, 0); +} + +static addBuild(builder:flatbuffers.Builder, buildOffset:flatbuffers.Offset) { + builder.addFieldOffset(4, buildOffset, 0); +} + +static endSemVer(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createSemVer(builder:flatbuffers.Builder, major:number, minor:number, patch:number, prereleaseOffset:flatbuffers.Offset, buildOffset:flatbuffers.Offset):flatbuffers.Offset { + SemVer.startSemVer(builder); + SemVer.addMajor(builder, major); + SemVer.addMinor(builder, minor); + SemVer.addPatch(builder, patch); + SemVer.addPrerelease(builder, prereleaseOffset); + SemVer.addBuild(builder, buildOffset); + return SemVer.endSemVer(builder); +} +} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/types/shocker-command-type.ts b/frontend/src/lib/_fbs/open-shock/serialization/types/shocker-command-type.ts similarity index 52% rename from WebUI/src/lib/_fbs/open-shock/serialization/types/shocker-command-type.ts rename to frontend/src/lib/_fbs/open-shock/serialization/types/shocker-command-type.ts index 76cfb60e..a6c77901 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/types/shocker-command-type.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/types/shocker-command-type.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + export enum ShockerCommandType { Stop = 0, Shock = 1, diff --git a/frontend/src/lib/_fbs/open-shock/serialization/types/shocker-model-type.ts b/frontend/src/lib/_fbs/open-shock/serialization/types/shocker-model-type.ts new file mode 100644 index 00000000..e41c5b9d --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/types/shocker-model-type.ts @@ -0,0 +1,8 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +export enum ShockerModelType { + CaiXianlin = 0, + Petrainer = 1 +} diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/types/wifi-auth-mode.ts b/frontend/src/lib/_fbs/open-shock/serialization/types/wifi-auth-mode.ts similarity index 63% rename from WebUI/src/lib/_fbs/open-shock/serialization/types/wifi-auth-mode.ts rename to frontend/src/lib/_fbs/open-shock/serialization/types/wifi-auth-mode.ts index 7d426cc3..e048d0e3 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/types/wifi-auth-mode.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/types/wifi-auth-mode.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + export enum WifiAuthMode { Open = 0, WEP = 1, diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/types/wifi-network-event-type.ts b/frontend/src/lib/_fbs/open-shock/serialization/types/wifi-network-event-type.ts similarity index 59% rename from WebUI/src/lib/_fbs/open-shock/serialization/types/wifi-network-event-type.ts rename to frontend/src/lib/_fbs/open-shock/serialization/types/wifi-network-event-type.ts index 8192dcc4..cd7b1ee0 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/types/wifi-network-event-type.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/types/wifi-network-event-type.ts @@ -1,5 +1,7 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + export enum WifiNetworkEventType { Discovered = 0, Updated = 1, diff --git a/WebUI/src/lib/_fbs/open-shock/serialization/types/wifi-network.ts b/frontend/src/lib/_fbs/open-shock/serialization/types/wifi-network.ts similarity index 92% rename from WebUI/src/lib/_fbs/open-shock/serialization/types/wifi-network.ts rename to frontend/src/lib/_fbs/open-shock/serialization/types/wifi-network.ts index feab5592..2c2a7b23 100644 --- a/WebUI/src/lib/_fbs/open-shock/serialization/types/wifi-network.ts +++ b/frontend/src/lib/_fbs/open-shock/serialization/types/wifi-network.ts @@ -1,8 +1,10 @@ // automatically generated by the FlatBuffers compiler, do not modify +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + import * as flatbuffers from 'flatbuffers'; -import { WifiAuthMode } from '../../../open-shock/serialization/types/wifi-auth-mode.js'; +import { WifiAuthMode } from '../../../open-shock/serialization/types/wifi-auth-mode'; export class WifiNetwork { diff --git a/frontend/src/lib/_fbs/open-shock/serialization/types/wifi-scan-status.ts b/frontend/src/lib/_fbs/open-shock/serialization/types/wifi-scan-status.ts new file mode 100644 index 00000000..0037d639 --- /dev/null +++ b/frontend/src/lib/_fbs/open-shock/serialization/types/wifi-scan-status.ts @@ -0,0 +1,12 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +export enum WifiScanStatus { + Started = 0, + InProgress = 1, + Completed = 2, + TimedOut = 3, + Aborted = 4, + Error = 5 +} diff --git a/WebUI/src/lib/components/Layout/Footer.svelte b/frontend/src/lib/components/Layout/Footer.svelte similarity index 100% rename from WebUI/src/lib/components/Layout/Footer.svelte rename to frontend/src/lib/components/Layout/Footer.svelte diff --git a/WebUI/src/lib/components/Layout/Header.svelte b/frontend/src/lib/components/Layout/Header.svelte similarity index 100% rename from WebUI/src/lib/components/Layout/Header.svelte rename to frontend/src/lib/components/Layout/Header.svelte diff --git a/WebUI/src/lib/components/WiFiList.svelte b/frontend/src/lib/components/WiFiList.svelte similarity index 66% rename from WebUI/src/lib/components/WiFiList.svelte rename to frontend/src/lib/components/WiFiList.svelte index a072781c..0a625955 100644 --- a/WebUI/src/lib/components/WiFiList.svelte +++ b/frontend/src/lib/components/WiFiList.svelte @@ -1,6 +1,5 @@
- {#if item} + {#if group}
-

WiFi Info

+

Network Info

{#each rows as row (row.key)} - {row.key}:{row.value} + {row.key}:{row.value} {/each}
+ +
+

Access Points

+ +
+ {#each group.networks as network} +
+ {network.bssid} + {network.rssi} dBm + Channel {network.channel} +
+ {/each} +
+
+ {#if showPasswordPrompt} + + {/if}
- {#if item.saved} - + {#if showPasswordPrompt} + + {/if} + + {#if group.saved} - {:else} - {/if}
diff --git a/WebUI/src/lib/index.ts b/frontend/src/lib/index.ts similarity index 100% rename from WebUI/src/lib/index.ts rename to frontend/src/lib/index.ts diff --git a/frontend/src/lib/mappers/ConfigMapper.ts b/frontend/src/lib/mappers/ConfigMapper.ts new file mode 100644 index 00000000..45e4f773 --- /dev/null +++ b/frontend/src/lib/mappers/ConfigMapper.ts @@ -0,0 +1,179 @@ +import type { OtaUpdateChannel } from '$lib/_fbs/open-shock/serialization/configuration'; +import { Config as FbsConfig } from '$lib/_fbs/open-shock/serialization/configuration/config'; + +// TODO: Update these configs and ensure that typescript enforces them to be up to date + +export interface RFConfig { + txPin: number; + keepaliveEnabled: boolean; +} + +export interface WifiCredentials { + id: number; + ssid: string; + password: string | null; +} + +export interface WifiConfig { + apSsid: string; + hostname: string; + credentials: WifiCredentials[]; +} + +export interface CaptivePortalConfig { + alwaysEnabled: boolean; +} + +export interface BackendConfig { + domain: string; + authToken: string | null; +} + +export interface SerialInputConfig { + echoEnabled: boolean; +} + +export interface OtaUpdateConfig { + isEnabled: boolean; + cdnDomain: string; + updateChannel: OtaUpdateChannel; + checkOnStartup: boolean; + checkInterval: number; + allowBackendManagement: boolean; + requireManualApproval: boolean; +} + +export interface Config { + rf: RFConfig; + wifi: WifiConfig; + captivePortal: CaptivePortalConfig; + backend: BackendConfig; + serialInput: SerialInputConfig; + otaUpdate: OtaUpdateConfig; +} + +function mapRfConfig(fbsConfig: FbsConfig): RFConfig { + const rf = fbsConfig.rf(); + if (!rf) throw new Error('fbsConfig.rf is null'); + + const txPin = rf.txPin(); + const keepaliveEnabled = rf.keepaliveEnabled(); + + return { + txPin, + keepaliveEnabled, + }; +} + +function mapWifiConfig(fbsConfig: FbsConfig): WifiConfig { + const wifi = fbsConfig.wifi(); + if (!wifi) throw new Error('fbsConfig.wifi is null'); + + const apSsid = wifi.apSsid(); + const hostname = wifi.hostname(); + + if (!apSsid) throw new Error('wifi.apSsid is null'); + if (!hostname) throw new Error('wifi.hostname is null'); + + const credentials: WifiCredentials[] = []; + const credentialsLength = wifi.credentialsLength(); + for (let i = 0; i < credentialsLength; i++) { + const cred = wifi.credentials(i); + if (!cred) throw new Error('wifi.credentials is null'); + + const id = cred.id(); + const ssid = cred.ssid(); + const password = cred.password(); + + if (!id) throw new Error('cred.id is null'); + if (!ssid) throw new Error('cred.ssid is null'); + if (!password) throw new Error('cred.password is null'); + + credentials.push({ + id, + ssid, + password, + }); + } + + return { + apSsid, + hostname, + credentials, + }; +} + +function mapCaptivePortalConfig(fbsConfig: FbsConfig): CaptivePortalConfig { + const captivePortal = fbsConfig.captivePortal(); + if (!captivePortal) throw new Error('fbsConfig.captivePortal is null'); + + const alwaysEnabled = captivePortal.alwaysEnabled(); + + return { + alwaysEnabled, + }; +} + +function mapBackendConfig(fbsConfig: FbsConfig): BackendConfig { + const backend = fbsConfig.backend(); + if (!backend) throw new Error('fbsConfig.backend is null'); + + const domain = backend.domain(); + const authToken = backend.authToken(); + + if (!domain) throw new Error('backend.domain is null'); + + return { + domain, + authToken, + }; +} + +function mapSerialInputConfig(fbsConfig: FbsConfig): SerialInputConfig { + const serialInput = fbsConfig.serialInput(); + if (!serialInput) throw new Error('fbsConfig.serialInput is null'); + + const echoEnabled = serialInput.echoEnabled(); + + return { + echoEnabled, + }; +} + +function mapOtaUpdateConfig(fbsConfig: FbsConfig): OtaUpdateConfig { + const otaUpdate = fbsConfig.otaUpdate(); + if (!otaUpdate) throw new Error('fbsConfig.otaUpdate is null'); + + const isEnabled = otaUpdate.isEnabled(); + const cdnDomain = otaUpdate.cdnDomain(); + const updateChannel = otaUpdate.updateChannel(); + const checkOnStartup = otaUpdate.checkOnStartup(); + const checkInterval = otaUpdate.checkInterval(); + const allowBackendManagement = otaUpdate.allowBackendManagement(); + const requireManualApproval = otaUpdate.requireManualApproval(); + + if (!cdnDomain) throw new Error('otaUpdate.cdnDomain is null'); + + return { + isEnabled, + cdnDomain, + updateChannel, + checkOnStartup, + checkInterval, + allowBackendManagement, + requireManualApproval, + }; +} + +export function mapConfig(fbsConfig: FbsConfig | null): Config | null { + if (!fbsConfig) return null; + + return { + rf: mapRfConfig(fbsConfig), + wifi: mapWifiConfig(fbsConfig), + captivePortal: mapCaptivePortalConfig(fbsConfig), + backend: mapBackendConfig(fbsConfig), + serialInput: mapSerialInputConfig(fbsConfig), + otaUpdate: mapOtaUpdateConfig(fbsConfig), + }; +} diff --git a/frontend/src/lib/stores/DeviceStateStore.ts b/frontend/src/lib/stores/DeviceStateStore.ts new file mode 100644 index 00000000..6d0f7cd1 --- /dev/null +++ b/frontend/src/lib/stores/DeviceStateStore.ts @@ -0,0 +1,112 @@ +import type { WifiScanStatus } from '$lib/_fbs/open-shock/serialization/types/wifi-scan-status'; +import type { WiFiNetwork, WiFiNetworkGroup, DeviceState } from '$lib/types'; +import { writable } from 'svelte/store'; +import { WifiAuthMode } from '$lib/_fbs/open-shock/serialization/types/wifi-auth-mode'; + +const { subscribe, update } = writable({ + wifiConnectedBSSID: null, + wifiScanStatus: null, + wifiNetworks: new Map(), + wifiNetworkGroups: new Map(), + accountLinked: false, + config: null, +}); + +function insertSorted(array: T[], value: T, compare: (a: T, b: T) => number) { + let low = 0, + high = array.length; + while (low < high) { + const mid = (low + high) >>> 1; + if (compare(array[mid], value) < 0) { + low = mid + 1; + } else { + high = mid; + } + } + array.splice(low, 0, value); +} + +function SsidMapReducer(groups: Map, [, value]: [string, WiFiNetwork]): Map { + const key = `${value.ssid || value.bssid}_${WifiAuthMode[value.security]}`; + + // Get the group for this SSID, or create a new one + const group = groups.get(key) ?? ({ ssid: value.ssid, saved: false, security: value.security, networks: [] } as WiFiNetworkGroup); + + // Update the group's saved status + group.saved = group.saved || value.saved; + + // Add the network to the group, sorted by signal strength (RSSI, higher is stronger) + insertSorted(group.networks, value, (a, b) => b.rssi - a.rssi); + + // Update the group in the map + groups.set(key, group); + + // Return the updated groups object + return groups; +} + +function updateWifiNetworkGroups(store: DeviceState) { + store.wifiNetworkGroups = Array.from(store.wifiNetworks.entries()).reduce(SsidMapReducer, new Map()); +} + +export const DeviceStateStore = { + subscribe, + update, + setWifiConnectedBSSID(connectedBSSID: string | null) { + update((store) => { + store.wifiConnectedBSSID = connectedBSSID; + return store; + }); + }, + setWifiScanStatus(scanStatus: WifiScanStatus | null) { + update((store) => { + store.wifiScanStatus = scanStatus; + return store; + }); + }, + setWifiNetwork(network: WiFiNetwork) { + update((store) => { + store.wifiNetworks.set(network.bssid, network); + updateWifiNetworkGroups(store); + return store; + }); + }, + updateWifiNetwork(bssid: string, updater: (network: WiFiNetwork) => WiFiNetwork) { + update((store) => { + const network = store.wifiNetworks.get(bssid); + if (network) { + store.wifiNetworks.set(bssid, updater(network)); + updateWifiNetworkGroups(store); + } + return store; + }); + }, + removeWifiNetwork(bssid: string) { + update((store) => { + store.wifiNetworks.delete(bssid); + updateWifiNetworkGroups(store); + return store; + }); + }, + clearWifiNetworks() { + update((store) => { + store.wifiNetworks.clear(); + store.wifiNetworkGroups.clear(); + return store; + }); + }, + setAccountLinked(linked: boolean) { + update((store) => { + store.accountLinked = linked; + return store; + }); + }, + setRfTxPin(pin: number) { + update((store) => { + if (store.config) { + store.config.rf.txPin = pin; + } + return store; + }); + }, +}; diff --git a/WebUI/src/lib/stores/ToastDelegator.ts b/frontend/src/lib/stores/ToastDelegator.ts similarity index 100% rename from WebUI/src/lib/stores/ToastDelegator.ts rename to frontend/src/lib/stores/ToastDelegator.ts diff --git a/WebUI/src/lib/stores/index.ts b/frontend/src/lib/stores/index.ts similarity index 100% rename from WebUI/src/lib/stores/index.ts rename to frontend/src/lib/stores/index.ts diff --git a/WebUI/src/lib/types/DeviceState.ts b/frontend/src/lib/types/DeviceState.ts similarity index 53% rename from WebUI/src/lib/types/DeviceState.ts rename to frontend/src/lib/types/DeviceState.ts index 6fd1ee4a..6a698394 100644 --- a/WebUI/src/lib/types/DeviceState.ts +++ b/frontend/src/lib/types/DeviceState.ts @@ -1,10 +1,12 @@ import type { WifiScanStatus } from '$lib/_fbs/open-shock/serialization/types/wifi-scan-status'; -import type { WiFiNetwork } from './WiFiNetwork'; +import type { Config } from '$lib/mappers/ConfigMapper'; +import type { WiFiNetwork, WiFiNetworkGroup } from './'; export type DeviceState = { wifiConnectedBSSID: string | null; wifiScanStatus: WifiScanStatus | null; wifiNetworks: Map; - gatewayPaired: boolean; - rfTxPin: number | null; + wifiNetworkGroups: Map; + accountLinked: boolean; + config: Config | null; }; diff --git a/WebUI/src/lib/types/WiFiNetwork.ts b/frontend/src/lib/types/WiFiNetwork.ts similarity index 100% rename from WebUI/src/lib/types/WiFiNetwork.ts rename to frontend/src/lib/types/WiFiNetwork.ts diff --git a/frontend/src/lib/types/WiFiNetworkGroup.ts b/frontend/src/lib/types/WiFiNetworkGroup.ts new file mode 100644 index 00000000..d68320a6 --- /dev/null +++ b/frontend/src/lib/types/WiFiNetworkGroup.ts @@ -0,0 +1,9 @@ +import type { WifiAuthMode } from '$lib/_fbs/open-shock/serialization/types/wifi-auth-mode'; +import type { WiFiNetwork } from './WiFiNetwork'; + +export type WiFiNetworkGroup = { + ssid: string; + saved: boolean; + security: WifiAuthMode; + networks: WiFiNetwork[]; +}; diff --git a/frontend/src/lib/types/index.ts b/frontend/src/lib/types/index.ts new file mode 100644 index 00000000..3cbfd44e --- /dev/null +++ b/frontend/src/lib/types/index.ts @@ -0,0 +1,3 @@ +export * from './DeviceState'; +export * from './WiFiNetwork'; +export * from './WiFiNetworkGroup'; diff --git a/WebUI/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte similarity index 100% rename from WebUI/src/routes/+layout.svelte rename to frontend/src/routes/+layout.svelte diff --git a/WebUI/src/routes/+layout.ts b/frontend/src/routes/+layout.ts similarity index 100% rename from WebUI/src/routes/+layout.ts rename to frontend/src/routes/+layout.ts diff --git a/WebUI/src/routes/+page.svelte b/frontend/src/routes/+page.svelte similarity index 85% rename from WebUI/src/routes/+page.svelte rename to frontend/src/routes/+page.svelte index a00c5251..719bce77 100644 --- a/WebUI/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -19,7 +19,7 @@ let linkCode: string = ''; $: linkCodeValid = isValidLinkCode(linkCode); - let rfTxPin: number | null = $DeviceStateStore.rfTxPin; + let rfTxPin: number | null = $DeviceStateStore.config?.rf.txPin ?? null; $: rfTxPinValid = rfTxPin !== null && rfTxPin >= 0 && rfTxPin < 255; function linkAccount() { @@ -42,15 +42,15 @@

Account Linking

- - + +

RF TX Pin

- (Currently {$DeviceStateStore.rfTxPin == null ? ' not set' : $DeviceStateStore.rfTxPin}) + (Currently {$DeviceStateStore.config == null ? ' not set' : $DeviceStateStore.config.rf.txPin})
diff --git a/WebUI/static/fa/fa-all.css b/frontend/static/fa/fa-all.css similarity index 100% rename from WebUI/static/fa/fa-all.css rename to frontend/static/fa/fa-all.css diff --git a/WebUI/static/fa/fa-solid-900.woff2 b/frontend/static/fa/fa-solid-900.woff2 similarity index 100% rename from WebUI/static/fa/fa-solid-900.woff2 rename to frontend/static/fa/fa-solid-900.woff2 diff --git a/WebUI/static/favicon.ico b/frontend/static/favicon.ico similarity index 100% rename from WebUI/static/favicon.ico rename to frontend/static/favicon.ico diff --git a/WebUI/static/logo.svg b/frontend/static/logo.svg similarity index 100% rename from WebUI/static/logo.svg rename to frontend/static/logo.svg diff --git a/WebUI/svelte.config.js b/frontend/svelte.config.js similarity index 80% rename from WebUI/svelte.config.js rename to frontend/svelte.config.js index 1cc7fe65..7a9af602 100644 --- a/WebUI/svelte.config.js +++ b/frontend/svelte.config.js @@ -1,5 +1,5 @@ import adapter from '@sveltejs/adapter-static'; -import { vitePreprocess } from '@sveltejs/kit/vite'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ const config = { diff --git a/WebUI/tailwind.config.ts b/frontend/tailwind.config.ts similarity index 85% rename from WebUI/tailwind.config.ts rename to frontend/tailwind.config.ts index 10dfb253..11ff74b7 100644 --- a/WebUI/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -3,6 +3,7 @@ import type { Config } from 'tailwindcss' import forms from '@tailwindcss/forms'; import typography from '@tailwindcss/typography'; import { skeleton } from '@skeletonlabs/tw-plugin' +import { openshockTheme } from './openshock-theme'; export default { darkMode: 'class', @@ -15,12 +16,7 @@ export default { typography, skeleton({ themes: { - preset: [ - { - name: 'skeleton', - enhancements: true, - }, - ], + custom: [openshockTheme], }, }), ], diff --git a/WebUI/tests/test.ts b/frontend/tests/test.ts similarity index 100% rename from WebUI/tests/test.ts rename to frontend/tests/test.ts diff --git a/WebUI/tsconfig.json b/frontend/tsconfig.json similarity index 100% rename from WebUI/tsconfig.json rename to frontend/tsconfig.json diff --git a/WebUI/vite.config.ts b/frontend/vite.config.ts similarity index 100% rename from WebUI/vite.config.ts rename to frontend/vite.config.ts diff --git a/include/Board.h b/include/Board.h deleted file mode 100644 index e75b9865..00000000 --- a/include/Board.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include - -namespace OpenShock { - constexpr bool IsValidGPIOPin(std::uint8_t pin) { - if (pin >= GPIO_NUM_MAX) { - return false; - } - - // Normally used for UART0 TXD and RXD. - // See: ESP32 Series Datasheet Version 4.3 Section 2.2 Pin Overview - if (pin == GPIO_NUM_1 || pin == GPIO_NUM_3) { - return false; - } - - // Used to control the boot mode of the chip. - // See: ESP32 Series Datasheet Version 4.3 Section 2.4 Strapping Pins - if (pin == GPIO_NUM_0 || pin == GPIO_NUM_2) { - return false; - } - - // Note: 5 and 15 are used for SDIO slave timing selection, but 15 is already widely used for RF, so we'll let these slide. - - // Allocated for communication with in-package flash/PSRAM and NOT recommended for other uses. - // See: ESP32 Series Datasheet Version 4.3 Section 2.2.1 Restrictions for GPIOs and RTC_GPIOs - if (pin == GPIO_NUM_6 || pin == GPIO_NUM_7 || pin == GPIO_NUM_8 || pin == GPIO_NUM_9 || pin == GPIO_NUM_10 || pin == GPIO_NUM_11 || pin == GPIO_NUM_16 || pin == GPIO_NUM_17) { - return false; - } - - return true; - } - constexpr bool IsValidInputPin(std::uint8_t pin) { - if (!IsValidGPIOPin(pin)) { - return false; - } - - return GPIO_IS_VALID_GPIO(pin) && !GPIO_IS_VALID_OUTPUT_GPIO(pin); - } - constexpr bool IsValidOutputPin(std::uint8_t pin) { - if (!IsValidGPIOPin(pin)) { - return false; - } - - return GPIO_IS_VALID_OUTPUT_GPIO(pin); - } -} // namespace OpenShock diff --git a/include/CaptivePortal.h b/include/CaptivePortal.h index 1803aeea..392e996e 100644 --- a/include/CaptivePortal.h +++ b/include/CaptivePortal.h @@ -1,24 +1,23 @@ #pragma once +#include "StringView.h" + +#include + #include -#include namespace OpenShock::CaptivePortal { void SetAlwaysEnabled(bool alwaysEnabled); bool IsAlwaysEnabled(); + bool ForceClose(std::uint32_t timeoutMs); + bool IsRunning(); void Update(); - bool SendMessageTXT(std::uint8_t socketId, const char* data, std::size_t len); + bool SendMessageTXT(std::uint8_t socketId, StringView data); bool SendMessageBIN(std::uint8_t socketId, const std::uint8_t* data, std::size_t len); - inline bool SendMessageTXT(std::uint8_t socketId, const std::string& message) { - return SendMessageTXT(socketId, message.c_str(), message.length()); - } - bool BroadcastMessageTXT(const char* data, std::size_t len); + bool BroadcastMessageTXT(StringView data); bool BroadcastMessageBIN(const std::uint8_t* data, std::size_t len); - inline bool BroadcastMessageTXT(const std::string& message) { - return BroadcastMessageTXT(message.c_str(), message.length()); - } } // namespace OpenShock::CaptivePortal diff --git a/include/CaptivePortalInstance.h b/include/CaptivePortalInstance.h index d0794126..7397c579 100644 --- a/include/CaptivePortalInstance.h +++ b/include/CaptivePortalInstance.h @@ -1,10 +1,16 @@ #pragma once +#include "StringView.h" #include "WebSocketDeFragger.h" +#include #include +#include #include +#include +#include + #include namespace OpenShock { @@ -13,14 +19,13 @@ namespace OpenShock { CaptivePortalInstance(); ~CaptivePortalInstance(); - bool sendMessageTXT(std::uint8_t socketId, const char* data, std::size_t len) { return m_socketServer.sendTXT(socketId, data, len); } + bool sendMessageTXT(std::uint8_t socketId, StringView data) { return m_socketServer.sendTXT(socketId, data.data(), data.length()); } bool sendMessageBIN(std::uint8_t socketId, const std::uint8_t* data, std::size_t len) { return m_socketServer.sendBIN(socketId, data, len); } - bool broadcastMessageTXT(const char* data, std::size_t len) { return m_socketServer.broadcastTXT(data, len); } + bool broadcastMessageTXT(StringView data) { return m_socketServer.broadcastTXT(data.data(), data.length()); } bool broadcastMessageBIN(const std::uint8_t* data, std::size_t len) { return m_socketServer.broadcastBIN(data, len); } - void loop() { m_socketServer.loop(); } - private: + static void task(void* arg); void handleWebSocketClientConnected(std::uint8_t socketId); void handleWebSocketClientDisconnected(std::uint8_t socketId); void handleWebSocketClientError(std::uint8_t socketId, std::uint16_t code, const char* message); @@ -29,5 +34,8 @@ namespace OpenShock { AsyncWebServer m_webServer; WebSocketsServer m_socketServer; WebSocketDeFragger m_socketDeFragger; + fs::LittleFSFS m_fileSystem; + DNSServer m_dnsServer; + TaskHandle_t m_taskHandle; }; } // namespace OpenShock diff --git a/include/Chipset.h b/include/Chipset.h new file mode 100644 index 00000000..9ec604e5 --- /dev/null +++ b/include/Chipset.h @@ -0,0 +1,256 @@ +#pragma once + +#include + +#include + +// The following chipsets are supported by the OpenShock firmware. +// To find documentation for a specific chipset, see the docs link. +// You need to navigate to the datasheets pin's section, the unsafe pins are usually listed under the "Strapping Pins" section. +// We want to avoid using these pins as they are used for boot mode and SDIO slave timing selection, and its easy to encounter issues if we use them. +#pragma region Chipset Definitions +// ESP8266EX +// +// Chips: ESP8266EX +// +// Docs: https://www.espressif.com/sites/default/files/documentation/0a-esp8266ex_datasheet_en.pdf +#ifdef OPENSHOCK_FW_CHIP_ESP8266EX +#define OPENSHOCK_FW_CHIP_DEFINED +// GPIO3, GPIO1 is used for UART0 RXD/TXD. +// GPIO0, GPIO2, and GPIO15 are used for boot mode and SDIO slave timing selection. +#define CHIP_UNSAFE_GPIO(pin) ((pin) == GPIO_NUM_0 || (pin) == GPIO_NUM_1 || (pin) == GPIO_NUM_2 || (pin) == GPIO_NUM_3 || (pin) == GPIO_NUM_15) +#endif + +// ESP8285 +// +// Chips: ESP8285N08, ESP8285H16 +// +// Docs: https://www.espressif.com/sites/default/files/documentation/0a-esp8285_datasheet_en.pdf +#ifdef OPENSHOCK_FW_CHIP_ESP8285 +#define OPENSHOCK_FW_CHIP_DEFINED +// GPIO3, GPIO1 is used for UART0 RXD/TXD. +// GPIO0, GPIO2, and GPIO15 are used for boot mode and SDIO slave timing selection. +#define CHIP_UNSAFE_GPIO(pin) ((pin) == GPIO_NUM_0 || (pin) == GPIO_NUM_1 || (pin) == GPIO_NUM_2 || (pin) == GPIO_NUM_3 || (pin) == GPIO_NUM_15) +#endif + +// ESP8684 +// +// Chips: ESP8684H1, ESP8684H2, ESP8684H4 +// +// Docs: https://www.espressif.com/sites/default/files/documentation/esp8684_datasheet_en.pdf +// Docs: https://www.espressif.com/sites/default/files/documentation/esp8684_technical_reference_manual_en.pdf#bootctrl +#ifdef OPENSHOCK_FW_CHIP_ESP8684 +#define OPENSHOCK_FW_CHIP_DEFINED +// GPIO3, GPIO1 is used for UART0 RXD/TXD. +// GPIO8, GPIO9 are strapping pins used to control the boot mode adn ROM code printing +#define CHIP_UNSAFE_GPIO(pin) ((pin) == GPIO_NUM_1 || (pin) == GPIO_NUM_3 || (pin) == GPIO_NUM_8 || (pin) == GPIO_NUM_9) +#endif + +// ESP8685 +// +// Chips: ESP8685H2, ESP8685H4 +// +// Docs: https://www.espressif.com/sites/default/files/documentation/esp8685_datasheet_en.pdf +#ifdef OPENSHOCK_FW_CHIP_ESP8685 +#define OPENSHOCK_FW_CHIP_DEFINED +// GPIO20, GPIO21 is used for UART0 RXD/TXD. +// GPIO2, GPIO8, GPIO9 are strapping pins. +#define CHIP_UNSAFE_GPIO(pin) ((pin) == GPIO_NUM_2 || (pin) == GPIO_NUM_8 || (pin) == GPIO_NUM_9 || (pin) == GPIO_NUM_20 || (pin) == GPIO_NUM_21) +#endif + +// ESP32 +// +// Chips: ESP32-D0WD-V3, ESP32-D0WDR2-V3, ESP32-U4WDH, ESP32-S0WD, ESP32-D0WD, ESP32-D0WDQ6, ESP32-D0WDQ6-V3 +// +// Docs: https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf +#ifdef OPENSHOCK_FW_CHIP_ESP32 +#define OPENSHOCK_FW_CHIP_DEFINED + +// GPIO1, GPIO3 is used for UART0 RXD/TXD. +// GPIO0, GPIO2 is used to control the boot mode of the chip. +// GPIO5, GPIO15 is used for SDIO slave timing selection. +// GPIO6, GPIO7, GPIO8, GPIO9, GPIO11, GPIO16, GPIO17 is used for SPI flash connection. (DO NOT TOUCH) +// +// See: ESP32 Series Datasheet Version 4.3 Section 2.2 Pin Overview +// See: ESP32 Series Datasheet Version 4.3 Section 2.4 Strapping Pins +#define CHIP_UNSAFE_GPIO(pin) \ + ((pin) == GPIO_NUM_1 || (pin) == GPIO_NUM_3 || (pin) == GPIO_NUM_0 || (pin) == GPIO_NUM_2 || (pin) == GPIO_NUM_5 || (pin) == GPIO_NUM_15 || (pin) == GPIO_NUM_6 || (pin) == GPIO_NUM_7 || (pin) == GPIO_NUM_8 || (pin) == GPIO_NUM_9 || (pin) == GPIO_NUM_11 || (pin) == GPIO_NUM_16 \ + || (pin) == GPIO_NUM_17) +#endif + +// ESP32-PICO-D4 +// +// Chips: ESP32-PICO-D4 +// +// Docs: https://www.espressif.com/sites/default/files/documentation/esp32-pico_series_datasheet_en.pdf (Section 2.1.2 - 2.1.3 Pin Description and Pin Mapping between ESP and Flash/PSRAM) +#ifdef OPENSHOCK_FW_CHIP_ESP32PICOD4 +#define OPENSHOCK_FW_CHIP_DEFINED +// GPIO3, GPIO1 is used for UART0 RXD/TXD. +// GPIO25, GPIO27, GPIO29, GPIO30, GPIO31, GPIO32, GPIO33 is used for SPI flash connection. (DO NOT TOUCH) +// GPIO12, GPIO0, GPIO2, GPIO15, and GPIO5 are used for boot mode and SDIO slave timing selection. +#define CHIP_UNSAFE_GPIO(pin) ((pin) == GPIO_NUM_3 || (pin) == GPIO_NUM_1 || (pin) == GPIO_NUM_25 || (pin) == GPIO_NUM_27 || (pin) == GPIO_NUM_29 || (pin) == GPIO_NUM_30 || (pin) == GPIO_NUM_31 || (pin) == GPIO_NUM_32 || (pin) == GPIO_NUM_33 || (pin) == GPIO_NUM_12 || (pin) == GPIO_NUM_0 || (pin) == GPIO_NUM_2 || (pin) == GPIO_NUM_15 || (pin) == GPIO_NUM_5) +#endif + +// ESP32-PICO-V3 +// +// Chips:, ESP32-PICO-V3, ESP32-PICO-V3-02 +// +// Docs: https://www.espressif.com/sites/default/files/documentation/esp32-pico_series_datasheet_en.pdf +#ifdef OPENSHOCK_FW_CHIP_ESP32PICOV3 +#define OPENSHOCK_FW_CHIP_DEFINED +// GPIO3, GPIO1 is used for UART0 RXD/TXD. +// GPIO6, GPIO11, GPIO9, GPIO10 is used for SPI flash connection. (DO NOT TOUCH) +// GPIO12, GPIO0, GPIO2, GPIO15, and GPIO5 are used for boot mode and SDIO slave timing selection. +#define CHIP_UNSAFE_GPIO(pin) ((pin) == GPIO_NUM_3 || (pin) == GPIO_NUM_1 || (pin) == GPIO_NUM_6 || (pin) == GPIO_NUM_11 || (pin) == GPIO_NUM_9 || (pin) == GPIO_NUM_10 || (pin) == GPIO_NUM_12 || (pin) == GPIO_NUM_0 || (pin) == GPIO_NUM_2 || (pin) == GPIO_NUM_15 || (pin) == GPIO_NUM_5) +#endif + +// ESP32-S2 +// +// Chips: ESP32-S2, ESP32-S2FH2, ESP32-S2FH4, ESP32-S2FN4R2, ESP32-S2R2 +// +// Docs: https://www.espressif.com/sites/default/files/documentation/esp32-s2_datasheet_en.pdf +#ifdef OPENSHOCK_FW_CHIP_ESP32S2 +#define OPENSHOCK_FW_CHIP_DEFINED +// GPIO44, GPIO43 is used for UART0 RXD/TXD. +// GPIO29, GPIO26, GPIO32, GPIO31, GPIO30, GPIO28, GPIO27 is used for SPI flash connection. (DO NOT TOUCH) +// GPIO0, GPIO45, GPIO46 is strapping pins used to control the boot mode and misc. functions. +#define CHIP_UNSAFE_GPIO(pin) ((pin) == GPIO_NUM_44 || (pin) == GPIO_NUM_43 || (pin) == GPIO_NUM_29 || (pin) == GPIO_NUM_26 || (pin) == GPIO_NUM_32 || (pin) == GPIO_NUM_31 || (pin) == GPIO_NUM_30 || (pin) == GPIO_NUM_28 || (pin) == GPIO_NUM_27 || (pin) == GPIO_NUM_0 || (pin) == GPIO_NUM_45 || (pin) == GPIO_NUM_46) +#endif + +// ESP32-S3 +// +// Chips: ESP32-S3, ESP32-S3FN8, ESP32-S3R2, ESP32-S3R8, ESP32-S3R8V, ESP32-S3R16V, ESP32-S3FH4R2 +// +// Docs: https://www.espressif.com/sites/default/files/documentation/esp32-s3_datasheet_en.pdf +#ifdef OPENSHOCK_FW_CHIP_ESP32S3 +#define OPENSHOCK_FW_CHIP_DEFINED +// GPIO44, GPIO43 is used for UART0 RXD/TXD. +// GPIO19, GPIO20 is used for USB serial, flashing, and debugging. +// GPIO30, GPIO29, GPIO26, GPIO32, GPIO31, GPIO28, GPIO27, GPIO33, GPIO34, GPIO35, GPIO36, GPIO37 is used for SPI flash connection. (DO NOT TOUCH) +// GPIO0, GPIO3, GPIO45, GPIO46 is strapping pins used to control the boot mode and misc. functions. +#define CHIP_UNSAFE_GPIO(pin) ((pin) == GPIO_NUM_44 || (pin) == GPIO_NUM_43 || (pin) == GPIO_NUM_19 || (pin) == GPIO_NUM_20 || (pin) == GPIO_NUM_30 || (pin) == GPIO_NUM_29 || (pin) == GPIO_NUM_26 || (pin) == GPIO_NUM_32 || (pin) == GPIO_NUM_31 || (pin) == GPIO_NUM_28 || (pin) == GPIO_NUM_27 || (pin) == GPIO_NUM_33 || (pin) == GPIO_NUM_34 || (pin) == GPIO_NUM_35 || (pin) == GPIO_NUM_36 || (pin) == GPIO_NUM_37 || (pin) == GPIO_NUM_0 || (pin) == GPIO_NUM_3 || (pin) == GPIO_NUM_45 || (pin) == GPIO_NUM_46) +#endif + +// ESP32-S3-PICO-1 +// +// Chips: ESP32-S3-PICO-1-N8R2, ESP32-S3-PICO-1-N8R8 +// +// Docs: https://www.espressif.com/sites/default/files/documentation/esp32-s3-pico-1_datasheet_en.pdf +#ifdef OPENSHOCK_FW_CHIP_ESP32S3PICO1 +#define OPENSHOCK_FW_CHIP_DEFINED +#error "ESP32-S3-PICO-1 is not supported yet." +#endif + +// ESP32-C3 +// +// Chips: ESP32-C3, ESP32-C3FN4, ESP32-C3FH4, ESP32-C3FH4AZ +// +// Docs: https://www.espressif.com/sites/default/files/documentation/esp32-c3_datasheet_en.pdf +#ifdef OPENSHOCK_FW_CHIP_ESP32C3 +#define OPENSHOCK_FW_CHIP_DEFINED +#error "ESP32-C3 is not supported yet." +#endif + +// ESP32-C6 +// +// Chips: ESP32-C6, ESP32-C6FH4 +// +// Docs: https://www.espressif.com/sites/default/files/documentation/esp32-c6_datasheet_en.pdf +#ifdef OPENSHOCK_FW_CHIP_ESP32C6 +#define OPENSHOCK_FW_CHIP_DEFINED +#error "ESP32-C6 is not supported yet." +#endif + +#ifndef OPENSHOCK_FW_CHIP_DEFINED +#error "Selected chipset is misspelled or not supported by OpenShock." +#endif + +#pragma endregion + +/// Board specific bad-pin bypasses for compatibility reasons. +/// To be clear, these pins are still unsafe, but we need to use them for compatibility reasons. +#pragma region Board Specific Bypasses + +#ifdef OPENSHOCK_FW_BOARD_WEMOSD1MINIESP32 +#define OPENSHOCK_BYPASSED_GPIO(pin) ((pin) == 15 || (pin) == 2) +#endif +#ifdef OPENSHOCK_FW_BOARD_WEMOSLOLINS2MINI +#define OPENSHOCK_BYPASSED_GPIO(pin) ((pin) == 15) +#endif +#ifdef OPENSHOCK_FW_BOARD_PISHOCK2023 +#define OPENSHOCK_BYPASSED_GPIO(pin) ((pin) == 12 || (pin) == 2) +#endif +#ifdef OPENSHOCK_FW_BOARD_PISHOCKLITE2021 +#define OPENSHOCK_BYPASSED_GPIO(pin) ((pin) == 15 || (pin) == 2) +#endif +#ifdef OPENSHOCK_FW_BOARD_OPENSHOCKCOREV1 +#define OPENSHOCK_BYPASSED_GPIO(pin) ((pin) == 15 || (pin) == 35) +#endif +#ifndef OPENSHOCK_BYPASSED_GPIO +#define OPENSHOCK_BYPASSED_GPIO(pin) (false) +#endif + +#pragma endregion + +namespace OpenShock { + constexpr bool IsValidGPIOPin(std::uint8_t pin) { + if (pin >= GPIO_NUM_MAX) { + return false; + } + + if (!GPIO_IS_VALID_GPIO(pin)) { + return false; + } + + if (OPENSHOCK_BYPASSED_GPIO(pin)) { + return true; + } + + if (CHIP_UNSAFE_GPIO(pin)) { + return false; + } + + return true; + } + constexpr bool IsValidInputPin(std::uint8_t pin) { + return IsValidGPIOPin(pin); + } + constexpr bool IsValidOutputPin(std::uint8_t pin) { + if (!IsValidGPIOPin(pin)) { + return false; + } + + if (!GPIO_IS_VALID_OUTPUT_GPIO(pin)) { + return false; + } + + return true; + } + constexpr std::bitset GetValidGPIOPins() { + std::bitset pins; + for (std::uint8_t i = 0; i < GPIO_NUM_MAX; i++) { + if (IsValidGPIOPin(i)) { + pins.set(i); + } + } + return pins; + } + constexpr std::bitset GetValidInputPins() { + std::bitset pins; + for (std::uint8_t i = 0; i < GPIO_NUM_MAX; i++) { + if (IsValidInputPin(i)) { + pins.set(i); + } + } + return pins; + } + constexpr std::bitset GetValidOutputPins() { + std::bitset pins; + for (std::uint8_t i = 0; i < GPIO_NUM_MAX; i++) { + if (IsValidOutputPin(i)) { + pins.set(i); + } + } + return pins; + } +} // namespace OpenShock diff --git a/include/CommandHandler.h b/include/CommandHandler.h index 7ba2867a..294a0ac1 100644 --- a/include/CommandHandler.h +++ b/include/CommandHandler.h @@ -15,5 +15,8 @@ namespace OpenShock::CommandHandler { SetRfPinResultCode SetRfTxPin(std::uint8_t txPin); std::uint8_t GetRfTxPin(); + bool SetKeepAliveEnabled(bool enabled); + bool SetKeepAlivePaused(bool paused); + bool HandleCommand(ShockerModelType shockerModel, std::uint16_t shockerId, ShockerCommandType type, std::uint8_t intensity, std::uint16_t durationMs); } // namespace OpenShock::CommandHandler diff --git a/include/Common.h b/include/Common.h new file mode 100644 index 00000000..6ef50aad --- /dev/null +++ b/include/Common.h @@ -0,0 +1,47 @@ +#pragma once + +#include + +#define DISABLE_COPY(TypeName) \ + TypeName(const TypeName&) = delete; \ + void operator=(const TypeName&) = delete +#define DISABLE_MOVE(TypeName) \ + TypeName(TypeName&&) = delete; \ + void operator=(TypeName&&) = delete + + +#ifndef OPENSHOCK_API_DOMAIN +#error "OPENSHOCK_API_DOMAIN must be defined" +#endif +#ifndef OPENSHOCK_FW_CDN_DOMAIN +#error "OPENSHOCK_FW_CDN_DOMAIN must be defined" +#endif +#ifndef OPENSHOCK_FW_VERSION +#error "OPENSHOCK_FW_VERSION must be defined" +#endif + +#define OPENSHOCK_API_URL(path) "https://" OPENSHOCK_API_DOMAIN path +#define OPENSHOCK_FW_CDN_URL(path) "https://" OPENSHOCK_FW_CDN_DOMAIN path + +#define OPENSHOCK_GPIO_INVALID 0 + +#ifndef OPENSHOCK_RF_TX_GPIO +#warning "OPENSHOCK_RF_TX_GPIO is not defined, setting to OPENSHOCK_GPIO_INVALID" +#define OPENSHOCK_RF_TX_GPIO OPENSHOCK_GPIO_INVALID +#endif + +// Check if OPENSHOCK_FW_USERAGENT is overridden trough compiler flags, if not, generate a default useragent. +#ifndef OPENSHOCK_FW_USERAGENT +#define OPENSHOCK_FW_USERAGENT OPENSHOCK_FW_HOSTNAME "/" OPENSHOCK_FW_VERSION " (arduino-esp32; " OPENSHOCK_FW_BOARD "; " OPENSHOCK_FW_CHIP "; Espressif)" +#endif + +// Check if Arduino.h exists, if not instruct the developer to remove "arduino-esp32" from the useragent and replace it with "ESP-IDF", after which the developer may remove this warning. +#if defined(__has_include) && !__has_include("Arduino.h") +#warning "Let it be known that Arduino hath finally been cast aside in favor of the noble ESP-IDF! I beseech thee, kind sir or madam, wouldst thou kindly partake in the honors of expunging 'arduino-esp32' from yonder useragent aloft, and in its stead, bestow the illustrious 'ESP-IDF'?" +#endif + +namespace OpenShock::Constants { + const std::uint8_t GPIO_INVALID = OPENSHOCK_GPIO_INVALID; + const std::uint8_t GPIO_RF_TX = OPENSHOCK_RF_TX_GPIO; + const char* const FW_USERAGENT = OPENSHOCK_FW_USERAGENT; +} // namespace OpenShock::Constants diff --git a/include/Config.h b/include/Config.h deleted file mode 100644 index bf03bc6c..00000000 --- a/include/Config.h +++ /dev/null @@ -1,69 +0,0 @@ -#pragma once - -#include "serialization/_fbs/ConfigFile_generated.h" - -#include -#include -#include - -namespace OpenShock::Config { - // This is a copy of the flatbuffers schema defined in schemas/ConfigFile.fbs - struct RFConfig { - std::uint8_t txPin; - }; - struct WiFiCredentials { - std::uint8_t id; - std::string ssid; - std::uint8_t bssid[6]; - std::string password; - }; - struct WiFiConfig { - std::string apSsid; - std::string hostname; - std::vector credentials; - }; - struct CaptivePortalConfig { - bool alwaysEnabled; - }; - struct BackendConfig { - std::string authToken; - }; - - void Init(); - - /** - * @brief Resets the config file to the factory default values. - * - * @note A reboot after calling this function is HIGHLY recommended. - */ - void FactoryReset(); - - const RFConfig& GetRFConfig(); - const WiFiConfig& GetWiFiConfig(); - const std::vector& GetWiFiCredentials(); - const CaptivePortalConfig& GetCaptivePortalConfig(); - const BackendConfig& GetBackendConfig(); - - bool SetRFConfig(const RFConfig& config); - bool SetWiFiConfig(const WiFiConfig& config); - bool SetWiFiCredentials(const std::vector& credentials); - bool SetCaptivePortalConfig(const CaptivePortalConfig& config); - bool SetBackendConfig(const BackendConfig& config); - - bool SetRFConfigTxPin(std::uint8_t txPin); - - std::uint8_t AddWiFiCredentials(const std::string& ssid, const std::uint8_t (&bssid)[6], const std::string& password); - bool TryGetWiFiCredentialsByID(std::uint8_t id, WiFiCredentials& out); - bool TryGetWiFiCredentialsBySSID(const char* ssid, WiFiCredentials& out); - bool TryGetWiFiCredentialsByBSSID(const std::uint8_t (&bssid)[6], WiFiCredentials& out); - std::uint8_t GetWiFiCredentialsIDbySSID(const char* ssid); - std::uint8_t GetWiFiCredentialsIDbyBSSID(const std::uint8_t (&bssid)[6]); - std::uint8_t GetWiFiCredentialsIDbyBSSIDorSSID(const std::uint8_t (&bssid)[6], const char* ssid); - bool RemoveWiFiCredentials(std::uint8_t id); - void ClearWiFiCredentials(); - - bool HasBackendAuthToken(); - const std::string& GetBackendAuthToken(); - bool SetBackendAuthToken(const std::string& token); - bool ClearBackendAuthToken(); -} // namespace OpenShock::Config diff --git a/include/Constants.h b/include/Constants.h deleted file mode 100644 index 01be0ced..00000000 --- a/include/Constants.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include - -#ifndef OPENSHOCK_API_DOMAIN -#error "OPENSHOCK_API_DOMAIN must be defined" -#endif - -#ifndef OPENSHOCK_API_BASE_URL -#define OPENSHOCK_API_BASE_URL "https://" OPENSHOCK_API_DOMAIN -#endif - -#define OPENSHOCK_API_URL(path) OPENSHOCK_API_BASE_URL path - -#ifndef OPENSHOCK_FW_VERSION -#error "OPENSHOCK_FW_VERSION must be defined" -#endif - -namespace OpenShock::Constants { - constexpr std::uint8_t GPIO_INVALID = UINT8_MAX; -} // namespace OpenShock::Constants diff --git a/include/EStopManager.h b/include/EStopManager.h index 167a7f4c..fd89b4ed 100644 --- a/include/EStopManager.h +++ b/include/EStopManager.h @@ -10,8 +10,7 @@ namespace OpenShock::EStopManager { ESTOPPED_CLEARED // The EStop has been cleared by the user, but we're waiting for the user to release the button (to avoid incidental estops) }; - void Init(); - EStopStatus Update(); + void Init(std::uint16_t updateIntervalMs); bool IsEStopped(); std::int64_t WhenEStopped(); } // namespace OpenShock::EStopManager diff --git a/include/FirmwareBootType.h b/include/FirmwareBootType.h new file mode 100644 index 00000000..f9f48896 --- /dev/null +++ b/include/FirmwareBootType.h @@ -0,0 +1,29 @@ +#pragma once + +#include "serialization/_fbs/FirmwareBootType_generated.h" + +#include +#include + +namespace OpenShock { + typedef OpenShock::Serialization::Types::FirmwareBootType FirmwareBootType; + + inline bool TryParseFirmwareBootType(FirmwareBootType& bootType, const char* str) { + if (strcasecmp(str, "normal") == 0) { + bootType = FirmwareBootType::Normal; + return true; + } + + if (strcasecmp(str, "newfirmware") == 0 || strcasecmp(str, "new_firmware") == 0) { + bootType = FirmwareBootType::NewFirmware; + return true; + } + + if (strcasecmp(str, "rollback") == 0) { + bootType = FirmwareBootType::Rollback; + return true; + } + + return false; + } +} // namespace OpenShock diff --git a/include/FormatHelpers.h b/include/FormatHelpers.h index abc74a3a..be5035b2 100644 --- a/include/FormatHelpers.h +++ b/include/FormatHelpers.h @@ -2,3 +2,12 @@ #define BSSID_FMT "%02X:%02X:%02X:%02X:%02X:%02X" #define BSSID_ARG(bssid) bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5] +#define BSSID_FMT_LEN 18 + +#define IPV4ADDR_FMT "%u.%u.%u.%u" +#define IPV4ADDR_ARG(addr) addr[0], addr[1], addr[2], addr[3] +#define IPV4ADDR_FMT_LEN 15 + +#define IPV6ADDR_FMT "%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x" +#define IPV6ADDR_ARG(addr) addr[0], addr[1], addr[2], addr[3], addr[4], addr[5], addr[6], addr[7], addr[8], addr[9], addr[10], addr[11], addr[12], addr[13], addr[14], addr[15] +#define IPV6ADDR_FMT_LEN 39 diff --git a/include/GatewayClient.h b/include/GatewayClient.h index 1b4e2002..64c799f5 100644 --- a/include/GatewayClient.h +++ b/include/GatewayClient.h @@ -1,10 +1,12 @@ #pragma once +#include "StringView.h" + #include +#include #include #include -#include namespace OpenShock { class GatewayClient { @@ -24,10 +26,14 @@ namespace OpenShock { void connect(const char* lcgFqdn); void disconnect(); + bool sendMessageTXT(StringView data); + bool sendMessageBIN(const std::uint8_t* data, std::size_t length); + bool loop(); private: void _sendKeepAlive(); + void _sendBootStatus(); void _handleEvent(WStype_t type, std::uint8_t* payload, std::size_t length); WebSocketsClient m_webSocket; diff --git a/include/GatewayConnectionManager.h b/include/GatewayConnectionManager.h index f26913dc..8bd1a9bd 100644 --- a/include/GatewayConnectionManager.h +++ b/include/GatewayConnectionManager.h @@ -1,6 +1,7 @@ #pragma once #include "AccountLinkResultCode.h" +#include "StringView.h" #include #include @@ -10,9 +11,12 @@ namespace OpenShock::GatewayConnectionManager { bool IsConnected(); - bool IsPaired(); - AccountLinkResultCode Pair(const char* pairCode); - void UnPair(); + bool IsLinked(); + AccountLinkResultCode Link(const char* linkCode); + void UnLink(); + + bool SendMessageTXT(StringView data); + bool SendMessageBIN(const std::uint8_t* data, std::size_t length); void Update(); } // namespace OpenShock::GatewayConnectionManager diff --git a/include/Hashing.h b/include/Hashing.h new file mode 100644 index 00000000..15d880ed --- /dev/null +++ b/include/Hashing.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +#include + +namespace OpenShock { +struct MD5 { + MD5() { mbedtls_md5_init(&ctx); } + ~MD5() { mbedtls_md5_free(&ctx); } + + bool begin() { return mbedtls_md5_starts_ret(&ctx) == 0; } + bool update(const std::uint8_t* data, std::size_t dataLen) { return mbedtls_md5_update_ret(&ctx, data, dataLen) == 0; } + bool finish(std::array& hash) { return mbedtls_md5_finish_ret(&ctx, hash.data()) == 0; } + + mbedtls_md5_context ctx; +}; +struct SHA1 { + SHA1() { mbedtls_sha1_init(&ctx); } + ~SHA1() { mbedtls_sha1_free(&ctx); } + + bool begin() { return mbedtls_sha1_starts_ret(&ctx) == 0; } + bool update(const std::uint8_t* data, std::size_t dataLen) { return mbedtls_sha1_update_ret(&ctx, data, dataLen) == 0; } + bool finish(std::array& hash) { return mbedtls_sha1_finish_ret(&ctx, hash.data()) == 0; } + + mbedtls_sha1_context ctx; +}; +struct SHA256 { + SHA256() { mbedtls_sha256_init(&ctx); } + ~SHA256() { mbedtls_sha256_free(&ctx); } + + bool begin() { return mbedtls_sha256_starts_ret(&ctx, 0) == 0; } + bool update(const std::uint8_t* data, std::size_t dataLen) { return mbedtls_sha256_update_ret(&ctx, data, dataLen) == 0; } + bool finish(std::array& hash) { return mbedtls_sha256_finish_ret(&ctx, hash.data()) == 0; } + + mbedtls_sha256_context ctx; +}; +} // namespace OpenShock diff --git a/include/Logging.h b/include/Logging.h index aa87e3b9..422f1a7e 100644 --- a/include/Logging.h +++ b/include/Logging.h @@ -2,12 +2,22 @@ #include #include +#include +#include -#define ESP_PANIC(TAG, format, ...) \ - ESP_LOGE(TAG, "PANIC: " format ", restarting in 5 seconds...", ##__VA_ARGS__); \ - vTaskDelay(pdMS_TO_TICKS(5000)); \ - esp_restart(); +#define ESP_PANIC_PRINT(TAG, format, ...) ESP_LOGE(TAG, "PANIC: " format, ##__VA_ARGS__) -#define ESP_PANIC_INSTANT(TAG, format, ...) \ - ESP_LOGE(TAG, "PANIC: " format ", restarting now...", ##__VA_ARGS__); \ - esp_restart(); +#define ESP_PANIC(TAG, format, ...) \ + ESP_PANIC_PRINT(TAG, format ", restarting in 5 seconds...", ##__VA_ARGS__); \ + vTaskDelay(pdMS_TO_TICKS(5000)); \ + esp_restart() + +#define ESP_PANIC_OTA(TAG, format, ...) \ + ESP_PANIC_PRINT(TAG, format ", invalidating update partition and restarting in 5 seconds...", ##__VA_ARGS__); \ + vTaskDelay(pdMS_TO_TICKS(5000)); \ + esp_ota_mark_app_invalid_rollback_and_reboot(); \ + esp_restart() + +#define ESP_PANIC_INSTANT(TAG, format, ...) \ + ESP_PANIC_PRINT(TAG, format, ##__VA_ARGS__); \ + esp_restart() diff --git a/include/OtaUpdateChannel.h b/include/OtaUpdateChannel.h new file mode 100644 index 00000000..a44ef376 --- /dev/null +++ b/include/OtaUpdateChannel.h @@ -0,0 +1,29 @@ +#pragma once + +#include "serialization/_fbs/ConfigFile_generated.h" + +#include +#include + +namespace OpenShock { + typedef OpenShock::Serialization::Configuration::OtaUpdateChannel OtaUpdateChannel; + + inline bool TryParseOtaUpdateChannel(OtaUpdateChannel& channel, const char* str) { + if (strcasecmp(str, "stable") == 0) { + channel = OtaUpdateChannel::Stable; + return true; + } + + if (strcasecmp(str, "beta") == 0) { + channel = OtaUpdateChannel::Beta; + return true; + } + + if (strcasecmp(str, "develop") == 0 || strcasecmp(str, "dev") == 0) { + channel = OtaUpdateChannel::Develop; + return true; + } + + return false; + } +} // namespace OpenShock diff --git a/include/OtaUpdateManager.h b/include/OtaUpdateManager.h new file mode 100644 index 00000000..f12c894b --- /dev/null +++ b/include/OtaUpdateManager.h @@ -0,0 +1,34 @@ +#pragma once + +#include "FirmwareBootType.h" +#include "OtaUpdateChannel.h" +#include "SemVer.h" +#include "StringView.h" + +#include +#include +#include +#include + +namespace OpenShock::OtaUpdateManager { + bool Init(); + + struct FirmwareRelease { + std::string appBinaryUrl; + std::uint8_t appBinaryHash[32]; + std::string filesystemBinaryUrl; + std::uint8_t filesystemBinaryHash[32]; + }; + + bool TryGetFirmwareVersion(OtaUpdateChannel channel, OpenShock::SemVer& version); + bool TryGetFirmwareBoards(const OpenShock::SemVer& version, std::vector& boards); + bool TryGetFirmwareRelease(const OpenShock::SemVer& version, FirmwareRelease& release); + + bool TryStartFirmwareInstallation(const OpenShock::SemVer& version); + + FirmwareBootType GetFirmwareBootType(); + bool IsValidatingApp(); + + void InvalidateAndRollback(); + void ValidateApp(); +} // namespace OpenShock::OtaUpdateManager diff --git a/include/OtaUpdateStep.h b/include/OtaUpdateStep.h new file mode 100644 index 00000000..343ea3cc --- /dev/null +++ b/include/OtaUpdateStep.h @@ -0,0 +1,44 @@ +#pragma once + +#include "serialization/_fbs/ConfigFile_generated.h" + +#include +#include + +namespace OpenShock { + typedef OpenShock::Serialization::Configuration::OtaUpdateStep OtaUpdateStep; + + inline bool TryParseOtaUpdateStep(OtaUpdateStep& channel, const char* str) { + if (strcasecmp(str, "none") == 0) { + channel = OtaUpdateStep::None; + return true; + } + + if (strcasecmp(str, "updating") == 0) { + channel = OtaUpdateStep::Updating; + return true; + } + + if (strcasecmp(str, "updated") == 0) { + channel = OtaUpdateStep::Updated; + return true; + } + + if (strcasecmp(str, "validating") == 0) { + channel = OtaUpdateStep::Validating; + return true; + } + + if (strcasecmp(str, "validated") == 0) { + channel = OtaUpdateStep::Validated; + return true; + } + + if (strcasecmp(str, "rollingback") == 0) { + channel = OtaUpdateStep::RollingBack; + return true; + } + + return false; + } +} // namespace OpenShock diff --git a/include/PinPatternManager.h b/include/PinPatternManager.h index ceb8cf53..a61550fe 100644 --- a/include/PinPatternManager.h +++ b/include/PinPatternManager.h @@ -1,15 +1,17 @@ #pragma once -#include +#include "Common.h" #include #include #include +#include #include namespace OpenShock { class PinPatternManager { + DISABLE_COPY(PinPatternManager); public: PinPatternManager(std::uint8_t gpioPin); ~PinPatternManager(); @@ -19,7 +21,11 @@ namespace OpenShock { std::uint32_t duration; }; - void SetPattern(nonstd::span pattern); + void SetPattern(const State* pattern, std::size_t patternLength); + template + inline void SetPattern(const State (&pattern)[N]) { + SetPattern(pattern, N); + } void ClearPattern(); private: @@ -30,6 +36,6 @@ namespace OpenShock { State* m_pattern; std::size_t m_patternLength; TaskHandle_t m_taskHandle; - SemaphoreHandle_t m_taskSemaphore; + SemaphoreHandle_t m_taskMutex; }; } // namespace OpenShock diff --git a/include/RGBPatternManager.h b/include/RGBPatternManager.h new file mode 100644 index 00000000..81231677 --- /dev/null +++ b/include/RGBPatternManager.h @@ -0,0 +1,49 @@ +#pragma once + +#include "Common.h" + +#include +#include +#include + +#include + +#include +#include + +namespace OpenShock { + class RGBPatternManager { + DISABLE_COPY(RGBPatternManager); + public: + RGBPatternManager(std::uint8_t gpioPin); + ~RGBPatternManager(); + + struct RGBState { + std::uint8_t red; + std::uint8_t green; + std::uint8_t blue; + std::uint32_t duration; + }; + + void SetPattern(const RGBState* pattern, std::size_t patternLength); + template + inline void SetPattern(const RGBState (&pattern)[N]) { + SetPattern(pattern, N); + } + void SetBrightness(std::uint8_t brightness); + void ClearPattern(); + + private: + void ClearPatternInternal(); + void SendRGB(const RGBState& state); + static void RunPattern(void* arg); + + std::uint8_t m_rgbPin; + std::uint8_t m_rgbBrightness; // 0-255 + rmt_obj_t* m_rmtHandle; + RGBState* m_pattern; + std::size_t m_patternLength; + TaskHandle_t m_taskHandle; + SemaphoreHandle_t m_taskMutex; + }; +} // namespace OpenShock diff --git a/include/ReadWriteMutex.h b/include/ReadWriteMutex.h new file mode 100644 index 00000000..6f3ac8ae --- /dev/null +++ b/include/ReadWriteMutex.h @@ -0,0 +1,102 @@ +#pragma once + +#include +#include + +namespace OpenShock { + class ReadWriteMutex { + public: + ReadWriteMutex(); + ~ReadWriteMutex(); + + bool lockRead(TickType_t xTicksToWait); + void unlockRead(); + + bool lockWrite(TickType_t xTicksToWait); + void unlockWrite(); + private: + SemaphoreHandle_t m_mutex; + SemaphoreHandle_t m_readSem; + int m_readers; + }; + + class ScopedReadLock { + public: + ScopedReadLock(ReadWriteMutex* mutex, TickType_t xTicksToWait = portMAX_DELAY) : m_mutex(mutex) { + bool result = false; + if (m_mutex != nullptr) { + result = m_mutex->lockRead(xTicksToWait); + } + + if (!result) { + m_mutex = nullptr; + } + } + + ~ScopedReadLock() { + if (m_mutex != nullptr) { + m_mutex->unlockRead(); + } + } + + bool isLocked() const { + return m_mutex != nullptr; + } + + bool unlock() { + if (m_mutex != nullptr) { + m_mutex->unlockRead(); + m_mutex = nullptr; + return true; + } + + return false; + } + + ReadWriteMutex* getMutex() const { + return m_mutex; + } + private: + ReadWriteMutex* m_mutex; + }; + + class ScopedWriteLock { + public: + ScopedWriteLock(ReadWriteMutex* mutex, TickType_t xTicksToWait = portMAX_DELAY) : m_mutex(mutex) { + bool result = false; + if (m_mutex != nullptr) { + result = m_mutex->lockWrite(xTicksToWait); + } + + if (!result) { + m_mutex = nullptr; + } + } + + ~ScopedWriteLock() { + if (m_mutex != nullptr) { + m_mutex->unlockWrite(); + } + } + + bool isLocked() const { + return m_mutex != nullptr; + } + + bool unlock() { + if (m_mutex != nullptr) { + m_mutex->unlockWrite(); + m_mutex = nullptr; + return true; + } + + return false; + } + + ReadWriteMutex* getMutex() const { + return m_mutex; + } + private: + ReadWriteMutex* m_mutex; + }; +} // namespace OpenShock diff --git a/include/SemVer.h b/include/SemVer.h new file mode 100644 index 00000000..dad22c14 --- /dev/null +++ b/include/SemVer.h @@ -0,0 +1,76 @@ +#pragma once + +#include "StringView.h" + +#include + +namespace OpenShock { + struct SemVer { + std::uint16_t major; + std::uint16_t minor; + std::uint16_t patch; + std::string prerelease; + std::string build; + + SemVer() : major(0), minor(0), patch(0), prerelease(), build() {} + SemVer(std::uint16_t major, std::uint16_t minor, std::uint16_t patch) + : major(major), minor(minor), patch(patch), prerelease(), build() + {} + SemVer(std::uint16_t major, std::uint16_t minor, std::uint16_t patch, StringView prerelease, StringView build) + : major(major), minor(minor), patch(patch), prerelease(prerelease.data(), prerelease.length()), build(build.data(), build.length()) + {} + + bool operator==(const SemVer& other) const { + return major == other.major && minor == other.minor && patch == other.patch && prerelease == other.prerelease && build == other.build; + } + bool operator!=(const SemVer& other) const { + return !(*this == other); + } + bool operator<(const SemVer& other) const { + if (major < other.major) { + return true; + } + if (major > other.major) { + return false; + } + + if (minor < other.minor) { + return true; + } + if (minor > other.minor) { + return false; + } + + if (patch < other.patch) { + return true; + } + if (patch > other.patch) { + return false; + } + + if (prerelease < other.prerelease) { + return true; + } + if (prerelease > other.prerelease) { + return false; + } + + return build < other.build; + } + bool operator<=(const SemVer& other) const { + return *this < other || *this == other; + } + bool operator>(const SemVer& other) const { + return !(*this <= other); + } + bool operator>=(const SemVer& other) const { + return !(*this < other); + } + + bool isValid() const; + + std::string toString() const; + }; + + bool TryParseSemVer(StringView str, SemVer& out); +} // namespace OpenShock diff --git a/include/ShockerCommandType.h b/include/ShockerCommandType.h index 03fce70e..862d180f 100644 --- a/include/ShockerCommandType.h +++ b/include/ShockerCommandType.h @@ -6,4 +6,22 @@ namespace OpenShock { typedef OpenShock::Serialization::Types::ShockerCommandType ShockerCommandType; + + inline bool ShockerCommandTypeFromString(const char* str, ShockerCommandType& out) { + if (strcasecmp(str, "stop") == 0) { + out = ShockerCommandType::Stop; + return true; + } else if (strcasecmp(str, "shock") == 0) { + out = ShockerCommandType::Shock; + return true; + } else if (strcasecmp(str, "vibrate") == 0) { + out = ShockerCommandType::Vibrate; + return true; + } else if (strcasecmp(str, "sound") == 0) { + out = ShockerCommandType::Sound; + return true; + } else { + return false; + } + } } // namespace OpenShock diff --git a/include/ShockerModelType.h b/include/ShockerModelType.h index 1c89876a..aa2e7f61 100644 --- a/include/ShockerModelType.h +++ b/include/ShockerModelType.h @@ -3,7 +3,27 @@ #include "serialization/_fbs/ShockerModelType_generated.h" #include +#include namespace OpenShock { typedef OpenShock::Serialization::Types::ShockerModelType ShockerModelType; + + inline bool ShockerModelTypeFromString(const char* str, ShockerModelType& out, bool allowTypo = false) { + if (strcasecmp(str, "caixianlin") == 0 || strcasecmp(str, "cai-xianlin") == 0) { + out = ShockerModelType::CaiXianlin; + return true; + } + + if (strcasecmp(str, "petrainer") == 0) { + out = ShockerModelType::Petrainer; + return true; + } + + if (allowTypo && strcasecmp(str, "pettrainer") == 0) { + out = ShockerModelType::Petrainer; + return true; + } + + return false; + } } // namespace OpenShock diff --git a/include/StringView.h b/include/StringView.h new file mode 100644 index 00000000..959afe06 --- /dev/null +++ b/include/StringView.h @@ -0,0 +1,394 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace OpenShock { + struct StringView { + using value_type = char; + using const_iterator = const value_type*; + using const_reverse_iterator = std::reverse_iterator; + + static const std::size_t npos = std::numeric_limits::max(); + + static constexpr StringView Null() { return StringView(nullptr); } + static constexpr StringView Empty() { return StringView(""); } + + constexpr StringView() : _ptrBeg(nullptr), _ptrEnd(nullptr) { } + constexpr StringView(const char* const ptr) : _ptrBeg(ptr), _ptrEnd(_getStringEnd(ptr)) { } + constexpr StringView(const char* const ptr, std::size_t len) : _ptrBeg(ptr), _ptrEnd(ptr + len) { } + constexpr StringView(const char* const ptrBeg, const char* const ptrEnd) : _ptrBeg(ptrBeg), _ptrEnd(ptrEnd) { } + constexpr StringView(const StringView& str) : _ptrBeg(str._ptrBeg), _ptrEnd(str._ptrEnd) { } + StringView(const String& str) : _ptrBeg(str.c_str()), _ptrEnd(str.c_str() + str.length()) { } + StringView(const std::string& str) : _ptrBeg(str.c_str()), _ptrEnd(str.c_str() + str.size()) { } + + constexpr bool isNull() const { return _ptrBeg == nullptr || _ptrEnd == nullptr; } + + constexpr const char* data() const { return _ptrBeg; } + + constexpr const_iterator begin() const { return _ptrBeg; } + const_reverse_iterator rbegin() const { return std::make_reverse_iterator(end()); } + + constexpr const_iterator end() const { return _ptrEnd; } + const_reverse_iterator rend() const { return std::make_reverse_iterator(begin()); } + + constexpr char front() const { return *_ptrBeg; } + constexpr char back() const { return *(_ptrEnd - 1); } + + constexpr std::size_t size() const { + if (isNull()) return 0; + + return _ptrEnd - _ptrBeg; + } + constexpr std::size_t length() const { return size(); } + + constexpr bool isNullOrEmpty() const { return size() == 0; } + + constexpr StringView substr(std::size_t pos, std::size_t count = StringView::npos) const { + if (isNullOrEmpty()) { + return *this; + } + + if (pos > size()) { + return Null(); + } + + if (count == StringView::npos) { + count = size() - pos; + } else if (pos + count > size()) { + return Null(); + } + + return StringView(_ptrBeg + pos, _ptrBeg + pos + count); + } + + constexpr std::size_t find(char needle, std::size_t pos = 0) const { + std::size_t _size = this->size(); + + for (std::size_t i = pos; i < _size; ++i) { + if (_ptrBeg[i] == needle) { + return i; + } + } + + return StringView::npos; + } + std::size_t find(StringView needle, std::size_t pos = 0) const { + if (isNullOrEmpty() || pos + needle.size() >= size()) { + return StringView::npos; + } + + const char* ptr = std::search(_ptrBeg + pos, _ptrEnd, needle._ptrBeg, needle._ptrEnd); + if (ptr == _ptrEnd) { + return StringView::npos; + } + + return ptr - _ptrBeg; + } + + std::size_t rfind(char needle, std::size_t pos = StringView::npos) const { + std::size_t _size = this->size(); + + if (pos == StringView::npos) { + pos = _size - 1; + } else if (pos >= _size) { + return StringView::npos; + } + + for (std::size_t i = pos; i > 0; --i) { + if (_ptrBeg[i] == needle) { + return i; + } + } + + return StringView::npos; + } + std::size_t rfind(StringView needle, std::size_t pos = StringView::npos) const { + if (isNullOrEmpty()) { + return StringView::npos; + } + + if (pos == StringView::npos) { + pos = size() - 1; + } else if (pos + needle.size() >= size()) { + return StringView::npos; + } + + const char* ptr = std::find_end(_ptrBeg, _ptrBeg + pos, needle._ptrBeg, needle._ptrEnd); + if (ptr == _ptrBeg + pos) { + return StringView::npos; + } + + return ptr - _ptrBeg; + } + + StringView beforeDelimiter(char delimiter) const { + std::size_t pos = find(delimiter); + if (pos != StringView::npos) { + return substr(0, pos); + } + + return *this; + } + StringView beforeDelimiter(StringView delimiter) const { + std::size_t pos = find(delimiter); + if (pos != StringView::npos) { + return substr(0, pos); + } + + return *this; + } + + StringView afterDelimiter(char delimiter) const { + std::size_t pos = find(delimiter); + if (pos != StringView::npos) { + return substr(pos + 1); + } + + return *this; + } + StringView afterDelimiter(StringView delimiter) const { + std::size_t pos = find(delimiter); + if (pos != StringView::npos) { + return substr(pos + delimiter.size()); + } + + return *this; + } + + std::vector split(char delimiter) const { + std::vector result; + + std::size_t pos = 0; + while (pos < size()) { + std::size_t nextPos = find(delimiter, pos); + if (nextPos == StringView::npos) { + nextPos = size(); + } + + result.push_back(substr(pos, nextPos - pos)); + pos = nextPos + 1; + } + + return result; + } + std::vector split(StringView delimiter) const { + std::vector result; + + std::size_t pos = 0; + while (pos < size()) { + std::size_t nextPos = find(delimiter, pos); + if (nextPos == StringView::npos) { + nextPos = size(); + } + + result.push_back(substr(pos, nextPos - pos)); + pos = nextPos + delimiter.size(); + } + + return result; + } + std::vector split(std::function predicate) const { + std::vector result; + + const char* start = nullptr; + for (const char* ptr = _ptrBeg; ptr < _ptrEnd; ++ptr) { + if (predicate(*ptr)) { + if (start != nullptr) { + result.emplace_back(StringView(start, ptr)); + start = nullptr; + } + } else if (start == nullptr) { + start = ptr; + } + } + + if (start != nullptr) { + result.emplace_back(StringView(start, _ptrEnd)); + } + + return result; + } + + std::vector splitLines() const { + return split([](char c) { return c == '\r' || c == '\n'; }); + } + std::vector splitWhitespace() const { return split(isspace); } + + constexpr bool startsWith(char needle) const { + if (isNull()) { + return false; + } + + return _ptrBeg[0] == needle; + } + constexpr bool startsWith(StringView needle) const { + if (isNull()) { + return false; + } + + return _strEquals(_ptrBeg, _ptrBeg + needle.size(), needle._ptrBeg, needle._ptrEnd); + } + + constexpr bool endsWith(char needle) const { + if (isNull()) { + return false; + } + + return _ptrEnd[-1] == needle; + } + constexpr bool endsWith(StringView needle) const { + if (isNull()) { + return false; + } + + return _strEquals(_ptrEnd - needle.size(), _ptrEnd, needle._ptrBeg, needle._ptrEnd); + } + + constexpr StringView& trimLeft() { + if (isNull()) { + return *this; + } + + while (_ptrBeg < _ptrEnd && isspace(*_ptrBeg)) { + ++_ptrBeg; + } + + return *this; + } + + constexpr StringView& trimRight() { + if (isNull()) { + return *this; + } + + while (_ptrBeg < _ptrEnd && isspace(_ptrEnd[-1])) { + --_ptrEnd; + } + + return *this; + } + + constexpr StringView& trim() { + trimLeft(); + trimRight(); + return *this; + } + + constexpr void clear() { + _ptrBeg = nullptr; + _ptrEnd = nullptr; + } + + String toArduinoString() const { + if (isNull()) { + return String(); + } + + return String(_ptrBeg, size()); + } + + std::string toString() const { + if (isNull()) { + return std::string(); + } + + return std::string(_ptrBeg, _ptrEnd); + } + + constexpr operator const char*() const { return _ptrBeg; } + + explicit operator String() const { return toArduinoString(); } + + explicit operator std::string() const { return toString(); } + + /// Returns a reference to the character at the specified index, Going out of bounds is undefined behavior + constexpr char const& operator[](int index) const { + return _ptrBeg[index]; + } + /// Returns a const reference to the character at the specified index, Going out of bounds is undefined behavior + constexpr char const& operator[](std::size_t index) const { + return _ptrBeg[index]; + } + + constexpr bool operator==(const StringView& other) { + if (this == &other) return true; + + return _strEquals(_ptrBeg, _ptrEnd, other._ptrBeg, other._ptrEnd); + } + constexpr bool operator!=(const StringView& other) { return !(*this == other); } + constexpr bool operator==(const char* const other) { return *this == StringView(other); } + constexpr bool operator!=(const char* const other) { return !(*this == other); } + + bool operator<(const StringView& other) const { + if (this == &other) return false; + + return std::lexicographical_compare(_ptrBeg, _ptrEnd, other._ptrBeg, other._ptrEnd); + } + bool operator<=(const StringView& other) const { return *this < other || *this == other; } + bool operator>(const StringView& other) const { return !(*this <= other); } + bool operator>=(const StringView& other) const { return !(*this < other); } + + constexpr StringView& operator=(const char* const ptr) { + _ptrBeg = ptr; + _ptrEnd = _getStringEnd(ptr); + return *this; + } + constexpr StringView& operator=(const StringView& str) { + _ptrBeg = str._ptrBeg; + _ptrEnd = str._ptrEnd; + return *this; + } + StringView& operator=(const std::string& str) { + _ptrBeg = str.c_str(); + _ptrEnd = str.c_str() + str.size(); + return *this; + } + + private: + static constexpr bool _strEquals(const char* aStart, const char* aEnd, const char* bStart, const char* bEnd) { + if (aStart == bStart && aEnd == bEnd) { + return true; + } + if (aStart == nullptr || aEnd == nullptr || bStart == nullptr || bEnd == nullptr) { + return false; + } + + std::size_t aLen = aEnd - aStart; + std::size_t bLen = bEnd - bStart; + if (aLen != bLen) { + return false; + } + + while (aStart < aEnd) { + if (*aStart != *bStart) { + return false; + } + ++aStart; + ++bStart; + } + + return true; + } + static constexpr const char* _getStringEnd(const char* ptr) { + if (ptr == nullptr) { + return nullptr; + } + + while (*ptr != '\0') { + ++ptr; + } + + return ptr; + } + + const char* _ptrBeg; + const char* _ptrEnd; + }; +} // namespace OpenShock diff --git a/include/VisualStateManager.h b/include/VisualStateManager.h index 9148dff6..c6174fdd 100644 --- a/include/VisualStateManager.h +++ b/include/VisualStateManager.h @@ -1,5 +1,6 @@ #pragma once +#include "EStopManager.h" #include namespace OpenShock::VisualStateManager { @@ -7,6 +8,6 @@ namespace OpenShock::VisualStateManager { void SetCriticalError(); void SetScanningStarted(); - void SetEmergencyStop(bool isStopped); + void SetEmergencyStop(OpenShock::EStopManager::EStopStatus status); void SetWebSocketConnected(bool isConnected); } // namespace OpenShock::VisualStateManager diff --git a/include/WebSocketDeFragger.h b/include/WebSocketDeFragger.h index 8de56268..85d464af 100644 --- a/include/WebSocketDeFragger.h +++ b/include/WebSocketDeFragger.h @@ -1,5 +1,6 @@ #pragma once +#include "Common.h" #include "WebSocketMessageType.h" #include @@ -10,152 +11,22 @@ namespace OpenShock { class WebSocketDeFragger { + DISABLE_COPY(WebSocketDeFragger); public: typedef std::function EventCallback; - WebSocketDeFragger(EventCallback callback) : m_messages(), m_callback(callback) {} - WebSocketDeFragger(const WebSocketDeFragger&) = delete; - ~WebSocketDeFragger() { - clear(); - } + WebSocketDeFragger(EventCallback callback); + ~WebSocketDeFragger(); - void handler(std::uint8_t socketId, WStype_t type, const std::uint8_t* payload, std::size_t length) { - switch (type) { - case WStype_FRAGMENT_BIN_START: - start(socketId, WebSocketMessageType::Binary, payload, length); - return; - case WStype_FRAGMENT_TEXT_START: - start(socketId, WebSocketMessageType::Text, payload, length); - return; - case WStype_FRAGMENT: - append(socketId, payload, length); - return; - case WStype_FRAGMENT_FIN: - finish(socketId, payload, length); - return; - [[likely]] default: - clear(socketId); - break; - } - - WebSocketMessageType messageType; - switch (type) { - [[unlikely]] case WStype_ERROR: - messageType = WebSocketMessageType::Error; - break; - case WStype_DISCONNECTED: - messageType = WebSocketMessageType::Disconnected; - break; - case WStype_CONNECTED: - messageType = WebSocketMessageType::Connected; - break; - case WStype_TEXT: - messageType = WebSocketMessageType::Text; - break; - [[likely]] case WStype_BIN: - messageType = WebSocketMessageType::Binary; - break; - case WStype_PING: - messageType = WebSocketMessageType::Ping; - break; - case WStype_PONG: - messageType = WebSocketMessageType::Pong; - break; - [[unlikely]] default: - const char* const errorMessage = "Unknown WebSocket event type"; - m_callback(socketId, WebSocketMessageType::Error, reinterpret_cast(errorMessage), strlen(errorMessage)); - return; - } - - m_callback(socketId, messageType, payload, length); - } - - void onEvent(const EventCallback& callback) { - m_callback = callback; - } - void clear(std::uint8_t socketId) { - auto it = m_messages.find(socketId); - if (it != m_messages.end()) { - free(it->second.data); - m_messages.erase(it); - } - } - void clear() { - for (auto it = m_messages.begin(); it != m_messages.end(); ++it) { - free(it->second.data); - } - m_messages.clear(); - } - - WebSocketDeFragger& operator=(const WebSocketDeFragger&) = delete; + void handler(std::uint8_t socketId, WStype_t type, const std::uint8_t* payload, std::size_t length); + void onEvent(const EventCallback& callback); + void clear(std::uint8_t socketId); + void clear(); private: - void start(std::uint8_t socketId, WebSocketMessageType type, const std::uint8_t* data, std::uint32_t length) { - auto it = m_messages.find(socketId); - if (it != m_messages.end()) { - auto& message = it->second; - - if (message.capacity < length) { - message.data = reinterpret_cast(realloc(message.data, length)); - message.capacity = length; - } - - memcpy(message.data, data, length); - message.size = length; - - return; - } - - Message message { - .data = reinterpret_cast(malloc(length)), - .size = length, - .capacity = length, - .type = type - }; - - memcpy(message.data, data, length); - - m_messages.insert(std::make_pair(socketId, message)); - } - void append(std::uint8_t socketId, const std::uint8_t* data, std::uint32_t length) { - auto it = m_messages.find(socketId); - if (it == m_messages.end()) { - return; - } - - auto& message = it->second; - - std::uint32_t newLength = message.size + length; - if (message.capacity < newLength) { - message.data = reinterpret_cast(realloc(message.data, newLength)); - message.capacity = newLength; - } - - memcpy(message.data + message.size, data, length); - message.size = newLength; - } - void finish(std::uint8_t socketId, const std::uint8_t* data, std::uint32_t length) { - auto it = m_messages.find(socketId); - if (it == m_messages.end()) { - return; - } - - auto& message = it->second; - - std::uint32_t newLength = message.size + length; - if (message.capacity < newLength) { - message.data = reinterpret_cast(realloc(message.data, newLength)); - message.capacity = newLength; - } - - memcpy(message.data + message.size, data, length); - message.size = newLength; - - m_callback(socketId, message.type, message.data, message.size); - - free(message.data); - m_messages.erase(it); - } + void start(std::uint8_t socketId, WebSocketMessageType type, const std::uint8_t* data, std::uint32_t length); + void append(std::uint8_t socketId, const std::uint8_t* data, std::uint32_t length); + void finish(std::uint8_t socketId, const std::uint8_t* data, std::uint32_t length); struct Message { std::uint8_t* data; diff --git a/include/config/BackendConfig.h b/include/config/BackendConfig.h new file mode 100644 index 00000000..f3dc2827 --- /dev/null +++ b/include/config/BackendConfig.h @@ -0,0 +1,23 @@ +#pragma once + +#include "config/ConfigBase.h" + +#include + +namespace OpenShock::Config { + struct BackendConfig : public ConfigBase { + BackendConfig(); + BackendConfig(const std::string& domain, const std::string& authToken); + + std::string domain; + std::string authToken; + + void ToDefault() override; + + bool FromFlatbuffers(const Serialization::Configuration::BackendConfig* config) override; + flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override; + + bool FromJSON(const cJSON* json) override; + cJSON* ToJSON(bool withSensitiveData) const override; + }; +} // namespace OpenShock::Config diff --git a/include/config/CaptivePortalConfig.h b/include/config/CaptivePortalConfig.h new file mode 100644 index 00000000..fd33edc9 --- /dev/null +++ b/include/config/CaptivePortalConfig.h @@ -0,0 +1,20 @@ +#pragma once + +#include "config/ConfigBase.h" + +namespace OpenShock::Config { + struct CaptivePortalConfig : public ConfigBase { + CaptivePortalConfig(); + CaptivePortalConfig(bool alwaysEnabled); + + bool alwaysEnabled; + + void ToDefault() override; + + bool FromFlatbuffers(const Serialization::Configuration::CaptivePortalConfig* config) override; + flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override; + + bool FromJSON(const cJSON* json) override; + cJSON* ToJSON(bool withSensitiveData) const override; + }; +} // namespace OpenShock::Config diff --git a/include/config/Config.h b/include/config/Config.h new file mode 100644 index 00000000..c5b52431 --- /dev/null +++ b/include/config/Config.h @@ -0,0 +1,76 @@ +#pragma once + +#include "config/BackendConfig.h" +#include "config/CaptivePortalConfig.h" +#include "config/OtaUpdateConfig.h" +#include "config/RFConfig.h" +#include "config/SerialInputConfig.h" +#include "config/WiFiConfig.h" +#include "config/WiFiCredentials.h" + +#include +#include +#include + +namespace OpenShock::Config { + void Init(); + + /* GetAsJSON and SaveFromJSON are used for Reading/Writing the config file in its human-readable form. */ + std::string GetAsJSON(bool withSensitiveData); + bool SaveFromJSON(const std::string& json); + + /* GetAsFlatBuffer and SaveFromFlatBuffer are used for Reading/Writing the config file in its binary form. */ + flatbuffers::Offset GetAsFlatBuffer(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData); + bool SaveFromFlatBuffer(const Serialization::Configuration::Config* config); + + /* GetRaw and SetRaw are used for Reading/Writing the config file in its binary form. */ + bool GetRaw(std::vector& buffer); + bool SetRaw(const std::uint8_t* buffer, std::size_t size); + + /** + * @brief Resets the config file to the factory default values. + * + * @note A restart after calling this function is HIGHLY recommended. + */ + void FactoryReset(); + + bool GetRFConfig(RFConfig& out); + bool GetWiFiConfig(WiFiConfig& out); + bool GetOtaUpdateConfig(OtaUpdateConfig& out); + bool GetWiFiCredentials(cJSON* array, bool withSensitiveData); + bool GetWiFiCredentials(std::vector& out); + + bool SetRFConfig(const RFConfig& config); + bool SetWiFiConfig(const WiFiConfig& config); + bool SetWiFiCredentials(const std::vector& credentials); + bool SetCaptivePortalConfig(const CaptivePortalConfig& config); + bool SetSerialInputConfig(const SerialInputConfig& config); + bool SetBackendConfig(const BackendConfig& config); + + bool GetRFConfigTxPin(std::uint8_t& out); + bool SetRFConfigTxPin(std::uint8_t txPin); + bool GetRFConfigKeepAliveEnabled(bool& out); + bool SetRFConfigKeepAliveEnabled(bool enabled); + + bool GetSerialInputConfigEchoEnabled(bool& out); + bool SetSerialInputConfigEchoEnabled(bool enabled); + + bool AnyWiFiCredentials(std::function predicate); + + std::uint8_t AddWiFiCredentials(const std::string& ssid, const std::string& password); + bool TryGetWiFiCredentialsByID(std::uint8_t id, WiFiCredentials& out); + bool TryGetWiFiCredentialsBySSID(const char* ssid, WiFiCredentials& out); + std::uint8_t GetWiFiCredentialsIDbySSID(const char* ssid); + bool RemoveWiFiCredentials(std::uint8_t id); + bool ClearWiFiCredentials(); + + bool GetOtaUpdateId(std::int32_t& out); + bool SetOtaUpdateId(std::int32_t updateId); + bool GetOtaUpdateStep(OtaUpdateStep& out); + bool SetOtaUpdateStep(OtaUpdateStep updateStep); + + bool HasBackendAuthToken(); + bool GetBackendAuthToken(std::string& out); + bool SetBackendAuthToken(const std::string& token); + bool ClearBackendAuthToken(); +} // namespace OpenShock::Config diff --git a/include/config/ConfigBase.h b/include/config/ConfigBase.h new file mode 100644 index 00000000..d1301830 --- /dev/null +++ b/include/config/ConfigBase.h @@ -0,0 +1,19 @@ +#pragma once + +#include "serialization/_fbs/ConfigFile_generated.h" + +#include + +namespace OpenShock::Config { + template + struct ConfigBase { + virtual void ToDefault() = 0; + + virtual bool FromFlatbuffers(const T* config) = 0; + virtual flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const = 0; + + virtual bool FromJSON(const cJSON* json) = 0; + virtual cJSON* ToJSON(bool withSensitiveData) const = 0; + }; + +} // namespace OpenShock::Config diff --git a/include/config/OtaUpdateConfig.h b/include/config/OtaUpdateConfig.h new file mode 100644 index 00000000..446cc402 --- /dev/null +++ b/include/config/OtaUpdateConfig.h @@ -0,0 +1,45 @@ +#pragma once + +#include "config/ConfigBase.h" +#include "FirmwareBootType.h" +#include "OtaUpdateChannel.h" +#include "OtaUpdateStep.h" + +#include + +namespace OpenShock::Config { + struct OtaUpdateConfig : public ConfigBase { + OtaUpdateConfig(); + OtaUpdateConfig( + bool isEnabled, + std::string cdnDomain, + OtaUpdateChannel updateChannel, + bool checkOnStartup, + bool checkPeriodically, + std::uint16_t checkInterval, + bool allowBackendManagement, + bool requireManualApproval, + std::int32_t updateId, + OtaUpdateStep updateStep + ); + + bool isEnabled; + std::string cdnDomain; + OtaUpdateChannel updateChannel; + bool checkOnStartup; + bool checkPeriodically; + std::uint16_t checkInterval; + bool allowBackendManagement; + bool requireManualApproval; + std::int32_t updateId; + OtaUpdateStep updateStep; + + void ToDefault() override; + + bool FromFlatbuffers(const Serialization::Configuration::OtaUpdateConfig* config) override; + flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override; + + bool FromJSON(const cJSON* json) override; + cJSON* ToJSON(bool withSensitiveData) const override; + }; +} // namespace OpenShock::Config diff --git a/include/config/RFConfig.h b/include/config/RFConfig.h new file mode 100644 index 00000000..55e80721 --- /dev/null +++ b/include/config/RFConfig.h @@ -0,0 +1,21 @@ +#pragma once + +#include "config/ConfigBase.h" + +namespace OpenShock::Config { + struct RFConfig : public ConfigBase { + RFConfig(); + RFConfig(std::uint8_t txPin, bool keepAliveEnabled); + + std::uint8_t txPin; + bool keepAliveEnabled; + + void ToDefault() override; + + bool FromFlatbuffers(const Serialization::Configuration::RFConfig* config) override; + flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override; + + bool FromJSON(const cJSON* json) override; + cJSON* ToJSON(bool withSensitiveData) const override; + }; +} // namespace OpenShock::Config diff --git a/include/config/RootConfig.h b/include/config/RootConfig.h new file mode 100644 index 00000000..8a23e179 --- /dev/null +++ b/include/config/RootConfig.h @@ -0,0 +1,28 @@ +#pragma once + +#include "config/BackendConfig.h" +#include "config/CaptivePortalConfig.h" +#include "config/ConfigBase.h" +#include "config/OtaUpdateConfig.h" +#include "config/RFConfig.h" +#include "config/SerialInputConfig.h" +#include "config/WiFiConfig.h" + +namespace OpenShock::Config { + struct RootConfig : public ConfigBase { + OpenShock::Config::RFConfig rf; + OpenShock::Config::WiFiConfig wifi; + OpenShock::Config::CaptivePortalConfig captivePortal; + OpenShock::Config::BackendConfig backend; + OpenShock::Config::SerialInputConfig serialInput; + OpenShock::Config::OtaUpdateConfig otaUpdate; + + void ToDefault() override; + + bool FromFlatbuffers(const Serialization::Configuration::Config* config) override; + flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override; + + bool FromJSON(const cJSON* json) override; + cJSON* ToJSON(bool withSensitiveData) const override; + }; +} // namespace OpenShock::Config diff --git a/include/config/SerialInputConfig.h b/include/config/SerialInputConfig.h new file mode 100644 index 00000000..44c27aaf --- /dev/null +++ b/include/config/SerialInputConfig.h @@ -0,0 +1,20 @@ +#pragma once + +#include "config/ConfigBase.h" + +namespace OpenShock::Config { + struct SerialInputConfig : public ConfigBase { + SerialInputConfig(); + SerialInputConfig(bool echoEnabled); + + bool echoEnabled; + + void ToDefault() override; + + bool FromFlatbuffers(const Serialization::Configuration::SerialInputConfig* config) override; + flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override; + + bool FromJSON(const cJSON* json) override; + cJSON* ToJSON(bool withSensitiveData) const override; + }; +} // namespace OpenShock::Config diff --git a/include/config/WiFiConfig.h b/include/config/WiFiConfig.h new file mode 100644 index 00000000..1f657d78 --- /dev/null +++ b/include/config/WiFiConfig.h @@ -0,0 +1,26 @@ +#pragma once + +#include "config/ConfigBase.h" +#include "config/WiFiCredentials.h" + +#include +#include + +namespace OpenShock::Config { + struct WiFiConfig : public ConfigBase { + WiFiConfig(); + WiFiConfig(const std::string& accessPointSSID, const std::string& hostname, const std::vector& credentialsList); + + std::string accessPointSSID; + std::string hostname; + std::vector credentialsList; + + void ToDefault() override; + + bool FromFlatbuffers(const Serialization::Configuration::WiFiConfig* config) override; + flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override; + + bool FromJSON(const cJSON* json) override; + cJSON* ToJSON(bool withSensitiveData) const override; + }; +} // namespace OpenShock::Config diff --git a/include/config/WiFiCredentials.h b/include/config/WiFiCredentials.h new file mode 100644 index 00000000..10a9d03a --- /dev/null +++ b/include/config/WiFiCredentials.h @@ -0,0 +1,24 @@ +#pragma once + +#include "config/ConfigBase.h" + +#include + +namespace OpenShock::Config { + struct WiFiCredentials : public ConfigBase { + WiFiCredentials(); + WiFiCredentials(std::uint8_t id, const std::string& ssid, const std::string& password); + + std::uint8_t id; + std::string ssid; + std::string password; + + void ToDefault() override; + + bool FromFlatbuffers(const Serialization::Configuration::WiFiCredentials* config) override; + flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override; + + bool FromJSON(const cJSON* json) override; + cJSON* ToJSON(bool withSensitiveData) const override; + }; +} // namespace OpenShock::Config diff --git a/include/config/internal/utils.h b/include/config/internal/utils.h new file mode 100644 index 00000000..1051c3be --- /dev/null +++ b/include/config/internal/utils.h @@ -0,0 +1,69 @@ +#pragma once + +#include "config/ConfigBase.h" +#include "Logging.h" + +#include + +#include +#include + +namespace OpenShock::Config::Internal::Utils { + void FromFbsStr(std::string& str, const flatbuffers::String* fbsStr, const char* defaultStr); + bool FromJsonBool(bool& val, const cJSON* json, const char* name, bool defaultVal); + bool FromJsonU8(std::uint8_t& val, const cJSON* json, const char* name, std::uint8_t defaultVal); + bool FromJsonU16(std::uint16_t& val, const cJSON* json, const char* name, std::uint16_t defaultVal); + bool FromJsonI32(std::int32_t& val, const cJSON* json, const char* name, std::int32_t defaultVal); + bool FromJsonStr(std::string& str, const cJSON* json, const char* name, const char* defaultStr); + + template // T inherits from ConfigBase + void FromFbsVec(std::vector& vec, const flatbuffers::Vector>* fbsVec) { + vec.clear(); + + if (fbsVec == nullptr) { + return; + } + + for (auto fbsItem : *fbsVec) { + T item; + if (item.FromFlatbuffers(fbsItem)) { + vec.emplace_back(std::move(item)); + } + } + } + template // T inherits from ConfigBase + bool FromJsonStrParsed(T& val, const cJSON* json, const char* name, bool (*StringParser)(T&, const char*), T defaultVal) { + const cJSON* jsonVal = cJSON_GetObjectItemCaseSensitive(json, name); + if (jsonVal == nullptr) { + val = defaultVal; + return true; + } + + if (cJSON_IsString(jsonVal) == 0) { + return false; + } + + if (!StringParser(val, jsonVal->valuestring)) { + return false; + } + + return true; + } + template // T inherits from ConfigBase + bool FromJsonArray(std::vector& vec, const cJSON* jsonArray) { + vec.clear(); + if (jsonArray == nullptr) { + return true; + } + + const cJSON* jsonItem = nullptr; + cJSON_ArrayForEach(jsonItem, jsonArray) { + T item; + if (item.FromJSON(jsonItem)) { + vec.emplace_back(std::move(item)); + } + } + + return true; + } +} // namespace OpenShock::Config::Internal::Utils diff --git a/include/event_handlers/Init.h b/include/event_handlers/Init.h index aea03a97..2f382854 100644 --- a/include/event_handlers/Init.h +++ b/include/event_handlers/Init.h @@ -1,14 +1,9 @@ #pragma once #include "event_handlers/WebSocket.h" -#include "event_handlers/WiFiScan.h" namespace OpenShock::EventHandlers { - void Init() { - WiFiScan::Init(); - } + void Init() { } - void Deinit() { - WiFiScan::Deinit(); - } + void Deinit() { } } // namespace OpenShock::EventHandlers diff --git a/include/event_handlers/WiFiScan.h b/include/event_handlers/WiFiScan.h deleted file mode 100644 index a48aab95..00000000 --- a/include/event_handlers/WiFiScan.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -namespace OpenShock::EventHandlers::WiFiScan { - void Init(); - void Deinit(); -} // namespace OpenShock::EventHandlers::WiFiScan diff --git a/include/event_handlers/impl/WSGateway.h b/include/event_handlers/impl/WSGateway.h index 23009c55..cf0419fd 100644 --- a/include/event_handlers/impl/WSGateway.h +++ b/include/event_handlers/impl/WSGateway.h @@ -1,14 +1,15 @@ #pragma once -#include "serialization/_fbs/ServerToDeviceMessage_generated.h" +#include "serialization/_fbs/GatewayToDeviceMessage_generated.h" #include -#define _HANDLER_SIGNATURE(NAME) void NAME(const OpenShock::Serialization::ServerToDeviceMessage* msg) +#define WS_EVENT_HANDLER_SIGNATURE(NAME) void NAME(const OpenShock::Serialization::Gateway::GatewayToDeviceMessage* msg) namespace OpenShock::MessageHandlers::Server::_Private { - typedef _HANDLER_SIGNATURE((*HandlerType)); - _HANDLER_SIGNATURE(HandleInvalidMessage); - _HANDLER_SIGNATURE(HandleShockerCommandList); - _HANDLER_SIGNATURE(HandleCaptivePortalConfig); + typedef WS_EVENT_HANDLER_SIGNATURE((*HandlerType)); + WS_EVENT_HANDLER_SIGNATURE(HandleInvalidMessage); + WS_EVENT_HANDLER_SIGNATURE(HandleShockerCommandList); + WS_EVENT_HANDLER_SIGNATURE(HandleCaptivePortalConfig); + WS_EVENT_HANDLER_SIGNATURE(HandleOtaInstall); } // namespace OpenShock::MessageHandlers::Server::_Private diff --git a/include/event_handlers/impl/WSLocal.h b/include/event_handlers/impl/WSLocal.h index 5e1ddcdc..cf68ed59 100644 --- a/include/event_handlers/impl/WSLocal.h +++ b/include/event_handlers/impl/WSLocal.h @@ -5,17 +5,17 @@ #include -#define _HANDLER_SIGNATURE(NAME) void NAME(std::uint8_t socketId, const OpenShock::Serialization::Local::LocalToDeviceMessage* msg) +#define WS_EVENT_HANDLER_SIGNATURE(NAME) void NAME(std::uint8_t socketId, const OpenShock::Serialization::Local::LocalToDeviceMessage* msg) namespace OpenShock::MessageHandlers::Local::_Private { - typedef _HANDLER_SIGNATURE((*HandlerType)); - _HANDLER_SIGNATURE(HandleInvalidMessage); - _HANDLER_SIGNATURE(HandleWiFiScanCommand); - _HANDLER_SIGNATURE(HandleWiFiNetworkSaveCommand); - _HANDLER_SIGNATURE(HandleWiFiNetworkForgetCommand); - _HANDLER_SIGNATURE(HandleWiFiNetworkConnectCommand); - _HANDLER_SIGNATURE(HandleWiFiNetworkDisconnectCommand); - _HANDLER_SIGNATURE(HandleAccountLinkCommand); - _HANDLER_SIGNATURE(HandleAccountUnlinkCommand); - _HANDLER_SIGNATURE(HandleSetRfTxPinCommand); + typedef WS_EVENT_HANDLER_SIGNATURE((*HandlerType)); + WS_EVENT_HANDLER_SIGNATURE(HandleInvalidMessage); + WS_EVENT_HANDLER_SIGNATURE(HandleWiFiScanCommand); + WS_EVENT_HANDLER_SIGNATURE(HandleWiFiNetworkSaveCommand); + WS_EVENT_HANDLER_SIGNATURE(HandleWiFiNetworkForgetCommand); + WS_EVENT_HANDLER_SIGNATURE(HandleWiFiNetworkConnectCommand); + WS_EVENT_HANDLER_SIGNATURE(HandleWiFiNetworkDisconnectCommand); + WS_EVENT_HANDLER_SIGNATURE(HandleAccountLinkCommand); + WS_EVENT_HANDLER_SIGNATURE(HandleAccountUnlinkCommand); + WS_EVENT_HANDLER_SIGNATURE(HandleSetRfTxPinCommand); } // namespace OpenShock::MessageHandlers::Local::_Private diff --git a/include/http/HTTPRequestManager.h b/include/http/HTTPRequestManager.h new file mode 100644 index 00000000..5ac1700e --- /dev/null +++ b/include/http/HTTPRequestManager.h @@ -0,0 +1,59 @@ +#pragma once + +#include "StringView.h" + +#include + +#include +#include +#include + +namespace OpenShock::HTTP { + enum class RequestResult : std::uint8_t { + InvalidURL, // Invalid URL + RequestFailed, // Failed to start request + TimedOut, // Request timed out + RateLimited, // Rate limited (can be both local and global) + CodeRejected, // Request completed, but response code was not OK + ParseFailed, // Request completed, but JSON parsing failed + Cancelled, // Request was cancelled + Success, // Request completed successfully + }; + + template + struct Response { + RequestResult result; + int code; + T data; + }; + + template + using JsonParser = std::function; + using GotContentLengthCallback = std::function; + using DownloadCallback = std::function; + + Response Download(StringView url, const std::map& headers, GotContentLengthCallback contentLengthCallback, DownloadCallback downloadCallback, const std::vector& acceptedCodes = {200}, std::uint32_t timeoutMs = 10'000); + Response GetString(StringView url, const std::map& headers, const std::vector& acceptedCodes = {200}, std::uint32_t timeoutMs = 10'000); + + template + Response GetJSON(StringView url, const std::map& headers, JsonParser jsonParser, const std::vector& acceptedCodes = {200}, std::uint32_t timeoutMs = 10'000) { + auto response = GetString(url, headers, acceptedCodes, timeoutMs); + if (response.result != RequestResult::Success) { + return {response.result, response.code, {}}; + } + + cJSON* json = cJSON_ParseWithLength(response.data.c_str(), response.data.length()); + if (json == nullptr) { + return {RequestResult::ParseFailed, response.code, {}}; + } + + T data; + if (!jsonParser(response.code, json, data)) { + return {RequestResult::ParseFailed, response.code, {}}; + } + + cJSON_Delete(json); + + return {response.result, response.code, data}; + } +} // namespace OpenShock::HTTP diff --git a/include/http/JsonAPI.h b/include/http/JsonAPI.h new file mode 100644 index 00000000..d58ec35d --- /dev/null +++ b/include/http/JsonAPI.h @@ -0,0 +1,21 @@ +#pragma once + +#include "http/HTTPRequestManager.h" +#include "serialization/JsonAPI.h" + +namespace OpenShock::HTTP::JsonAPI { + /// @brief Links the device to the account with the given account link code, returns the device token. Valid response codes: 200, 404 + /// @param deviceToken + /// @return + HTTP::Response LinkAccount(const char* accountLinkCode); + + /// @brief Gets the device info for the given device token. Valid response codes: 200, 401 + /// @param deviceToken + /// @return + HTTP::Response GetDeviceInfo(const String& deviceToken); + + /// @brief Requests a Live Control Gateway to connect to. Valid response codes: 200, 401 + /// @param deviceToken + /// @return + HTTP::Response AssignLcg(const String& deviceToken); +} // namespace OpenShock::HTTP::JsonAPI diff --git a/include/radio/RFTransmitter.h b/include/radio/RFTransmitter.h index 819a1e2e..7bcb7e36 100644 --- a/include/radio/RFTransmitter.h +++ b/include/radio/RFTransmitter.h @@ -15,14 +15,14 @@ typedef void* TaskHandle_t; namespace OpenShock { class RFTransmitter { public: - RFTransmitter(std::uint8_t gpioPin, int queueSize = 32); + RFTransmitter(std::uint8_t gpioPin); ~RFTransmitter(); inline std::uint8_t GetTxPin() const { return m_txPin; } inline bool ok() const { return m_rmtHandle != nullptr && m_queueHandle != nullptr && m_taskHandle != nullptr; } - bool SendCommand(ShockerModelType model, std::uint16_t shockerId, ShockerCommandType type, std::uint8_t intensity, std::uint16_t durationMs); + bool SendCommand(ShockerModelType model, std::uint16_t shockerId, ShockerCommandType type, std::uint8_t intensity, std::uint16_t durationMs, bool overwriteExisting = true); void ClearPendingCommands(); private: diff --git a/include/radio/rmt/CaiXianlinEncoder.h b/include/radio/rmt/CaiXianlinEncoder.h new file mode 100644 index 00000000..bf7f4e26 --- /dev/null +++ b/include/radio/rmt/CaiXianlinEncoder.h @@ -0,0 +1,12 @@ +#pragma once + +#include "ShockerCommandType.h" + +#include + +#include +#include + +namespace OpenShock::Rmt::CaiXianlinEncoder { + std::vector GetSequence(std::uint16_t transmitterId, std::uint8_t channelId, OpenShock::ShockerCommandType type, std::uint8_t intensity); +} diff --git a/include/radio/rmt/PetTrainerEncoder.h b/include/radio/rmt/PetrainerEncoder.h similarity index 83% rename from include/radio/rmt/PetTrainerEncoder.h rename to include/radio/rmt/PetrainerEncoder.h index 52b354a8..1ccaace8 100644 --- a/include/radio/rmt/PetTrainerEncoder.h +++ b/include/radio/rmt/PetrainerEncoder.h @@ -7,6 +7,6 @@ #include #include -namespace OpenShock::Rmt::PetTrainerEncoder { +namespace OpenShock::Rmt::PetrainerEncoder { std::vector GetSequence(std::uint16_t shockerId, OpenShock::ShockerCommandType type, std::uint8_t intensity); } diff --git a/include/radio/rmt/XlcEncoder.h b/include/radio/rmt/XlcEncoder.h deleted file mode 100644 index 968e9aec..00000000 --- a/include/radio/rmt/XlcEncoder.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include "ShockerCommandType.h" - -#include - -#include -#include - -namespace OpenShock::Rmt::XlcEncoder { - std::vector GetSequence(std::uint16_t transmitterId, - std::uint8_t channelId, - OpenShock::ShockerCommandType type, - std::uint8_t intensity); -} diff --git a/include/radio/rmt/internal/Shared.h b/include/radio/rmt/internal/Shared.h new file mode 100644 index 00000000..7969b158 --- /dev/null +++ b/include/radio/rmt/internal/Shared.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace OpenShock::Rmt::Internal { + template + inline void EncodeBits(std::vector& pulses, T data, const rmt_data_t& rmtOne, const rmt_data_t& rmtZero) { + static_assert(std::is_unsigned::value, "T must be an unsigned integer"); + static_assert(N > 0, "N must be greater than 0"); + static_assert(N < std::numeric_limits::digits, "N must be less or equal to the number of bits in T"); + + pulses.reserve(pulses.size() + N); + for (std::int64_t bit_pos = N - 1; bit_pos >= 0; --bit_pos) { + pulses.push_back((data >> bit_pos) & 1 ? rmtOne : rmtZero); + } + } +} diff --git a/include/SerialInputHandler.h b/include/serial/SerialInputHandler.h similarity index 92% rename from include/SerialInputHandler.h rename to include/serial/SerialInputHandler.h index e6d52e00..448d6475 100644 --- a/include/SerialInputHandler.h +++ b/include/serial/SerialInputHandler.h @@ -3,6 +3,7 @@ #include namespace OpenShock::SerialInputHandler { + bool Init(); void Update(); void PrintWelcomeHeader(); diff --git a/include/serialization/JsonAPI.h b/include/serialization/JsonAPI.h new file mode 100644 index 00000000..04d48282 --- /dev/null +++ b/include/serialization/JsonAPI.h @@ -0,0 +1,33 @@ +#pragma once + +#include "ShockerModelType.h" + +#include + +#include +#include +#include + +namespace OpenShock::Serialization::JsonAPI { + struct AccountLinkResponse { + std::string authToken; + }; + struct DeviceInfoResponse { + std::string deviceId; + std::string deviceName; + struct ShockerInfo { + std::string id; + std::uint16_t rfId; + OpenShock::ShockerModelType model; + }; + std::vector shockers; + }; + struct AssignLcgResponse { + std::string fqdn; + std::string country; + }; + + bool ParseAccountLinkJsonResponse(int code, const cJSON* root, AccountLinkResponse& out); + bool ParseDeviceInfoJsonResponse(int code, const cJSON* root, DeviceInfoResponse& out); + bool ParseAssignLcgJsonResponse(int code, const cJSON* root, AssignLcgResponse& out); +} // namespace OpenShock::Serialization::JsonAPI diff --git a/include/serialization/JsonSerial.h b/include/serialization/JsonSerial.h new file mode 100644 index 00000000..06e8baae --- /dev/null +++ b/include/serialization/JsonSerial.h @@ -0,0 +1,20 @@ +#pragma once + +#include "ShockerModelType.h" +#include "ShockerCommandType.h" + +#include + +#include + +namespace OpenShock::Serialization::JsonSerial { + struct ShockerCommand { + OpenShock::ShockerModelType model; + std::uint16_t id; + OpenShock::ShockerCommandType command; + std::uint8_t intensity; + std::uint16_t durationMs; + }; + + bool ParseShockerCommand(const cJSON* root, ShockerCommand& out); +} // namespace OpenShock::Serialization::JsonAPI diff --git a/include/serialization/WSGateway.h b/include/serialization/WSGateway.h index f5269b29..81a304c0 100644 --- a/include/serialization/WSGateway.h +++ b/include/serialization/WSGateway.h @@ -1,5 +1,16 @@ #pragma once +#include "FirmwareBootType.h" +#include "SemVer.h" #include "serialization/CallbackFn.h" +#include "StringView.h" -namespace OpenShock::Serialization::Gateway { } +#include "serialization/_fbs/DeviceToGatewayMessage_generated.h" + +namespace OpenShock::Serialization::Gateway { + bool SerializeKeepAliveMessage(Common::SerializationCallbackFn callback); + bool SerializeBootStatusMessage(std::int32_t otaUpdateId, OpenShock::FirmwareBootType bootType, const OpenShock::SemVer& version, Common::SerializationCallbackFn callback); + bool SerializeOtaInstallStartedMessage(std::int32_t updateId, const OpenShock::SemVer& version, Common::SerializationCallbackFn callback); + bool SerializeOtaInstallProgressMessage(std::int32_t updateId, Gateway::OtaInstallProgressTask task, float progress, Common::SerializationCallbackFn callback); + bool SerializeOtaInstallFailedMessage(std::int32_t updateId, StringView message, bool fatal, Common::SerializationCallbackFn callback); +} // namespace OpenShock::Serialization::Gateway diff --git a/include/serialization/WSLocal.h b/include/serialization/WSLocal.h index c8abc76e..50961f34 100644 --- a/include/serialization/WSLocal.h +++ b/include/serialization/WSLocal.h @@ -13,7 +13,8 @@ namespace OpenShock { namespace OpenShock::Serialization::Local { bool SerializeErrorMessage(const char* message, Common::SerializationCallbackFn callback); - bool SerializeReadyMessage(const WiFiNetwork* connectedNetwork, bool gatewayPaired, std::uint8_t radioTxPin, Common::SerializationCallbackFn callback); + bool SerializeReadyMessage(const WiFiNetwork* connectedNetwork, bool accountLinked, Common::SerializationCallbackFn callback); bool SerializeWiFiScanStatusChangedEvent(OpenShock::WiFiScanStatus status, Common::SerializationCallbackFn callback); bool SerializeWiFiNetworkEvent(Types::WifiNetworkEventType eventType, const WiFiNetwork& network, Common::SerializationCallbackFn callback); + bool SerializeWiFiNetworksEvent(Types::WifiNetworkEventType eventType, const std::vector& networks, Common::SerializationCallbackFn callback); } // namespace OpenShock::Serialization::Local diff --git a/include/serialization/_fbs/ConfigFile_generated.h b/include/serialization/_fbs/ConfigFile_generated.h index e5148ba4..434539c5 100644 --- a/include/serialization/_fbs/ConfigFile_generated.h +++ b/include/serialization/_fbs/ConfigFile_generated.h @@ -17,9 +17,8 @@ namespace OpenShock { namespace Serialization { namespace Configuration { -struct BSSID; - struct RFConfig; +struct RFConfigBuilder; struct WiFiCredentials; struct WiFiCredentialsBuilder; @@ -28,86 +27,155 @@ struct WiFiConfig; struct WiFiConfigBuilder; struct CaptivePortalConfig; +struct CaptivePortalConfigBuilder; struct BackendConfig; struct BackendConfigBuilder; +struct SerialInputConfig; +struct SerialInputConfigBuilder; + +struct OtaUpdateConfig; +struct OtaUpdateConfigBuilder; + struct Config; struct ConfigBuilder; -FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(1) BSSID FLATBUFFERS_FINAL_CLASS { - private: - uint8_t array_[6]; - - public: - struct Traits; - static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { - return "OpenShock.Serialization.Configuration.BSSID"; - } - BSSID() - : array_() { - } - BSSID(::flatbuffers::span _array) { - ::flatbuffers::CastToArray(array_).CopyFromSpan(_array); - } - const ::flatbuffers::Array *array() const { - return &::flatbuffers::CastToArray(array_); - } +enum class OtaUpdateChannel : uint8_t { + Stable = 0, + Beta = 1, + Develop = 2, + MIN = Stable, + MAX = Develop }; -FLATBUFFERS_STRUCT_END(BSSID, 6); -struct BSSID::Traits { - using type = BSSID; +inline const OtaUpdateChannel (&EnumValuesOtaUpdateChannel())[3] { + static const OtaUpdateChannel values[] = { + OtaUpdateChannel::Stable, + OtaUpdateChannel::Beta, + OtaUpdateChannel::Develop + }; + return values; +} + +inline const char * const *EnumNamesOtaUpdateChannel() { + static const char * const names[4] = { + "Stable", + "Beta", + "Develop", + nullptr + }; + return names; +} + +inline const char *EnumNameOtaUpdateChannel(OtaUpdateChannel e) { + if (::flatbuffers::IsOutRange(e, OtaUpdateChannel::Stable, OtaUpdateChannel::Develop)) return ""; + const size_t index = static_cast(e); + return EnumNamesOtaUpdateChannel()[index]; +} + +enum class OtaUpdateStep : uint8_t { + None = 0, + Updating = 1, + Updated = 2, + Validating = 3, + Validated = 4, + RollingBack = 5, + MIN = None, + MAX = RollingBack }; -FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(1) RFConfig FLATBUFFERS_FINAL_CLASS { - private: - uint8_t tx_pin_; +inline const OtaUpdateStep (&EnumValuesOtaUpdateStep())[6] { + static const OtaUpdateStep values[] = { + OtaUpdateStep::None, + OtaUpdateStep::Updating, + OtaUpdateStep::Updated, + OtaUpdateStep::Validating, + OtaUpdateStep::Validated, + OtaUpdateStep::RollingBack + }; + return values; +} + +inline const char * const *EnumNamesOtaUpdateStep() { + static const char * const names[7] = { + "None", + "Updating", + "Updated", + "Validating", + "Validated", + "RollingBack", + nullptr + }; + return names; +} + +inline const char *EnumNameOtaUpdateStep(OtaUpdateStep e) { + if (::flatbuffers::IsOutRange(e, OtaUpdateStep::None, OtaUpdateStep::RollingBack)) return ""; + const size_t index = static_cast(e); + return EnumNamesOtaUpdateStep()[index]; +} - public: +struct RFConfig FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef RFConfigBuilder Builder; struct Traits; static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { return "OpenShock.Serialization.Configuration.RFConfig"; } - RFConfig() - : tx_pin_(0) { + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_TX_PIN = 4, + VT_KEEPALIVE_ENABLED = 6 + }; + /// The GPIO pin connected to the RF modulator's data pin for transmitting (TX) + uint8_t tx_pin() const { + return GetField(VT_TX_PIN, 0); } - RFConfig(uint8_t _tx_pin) - : tx_pin_(::flatbuffers::EndianScalar(_tx_pin)) { + /// Whether to transmit keepalive messages to keep the devices from entering sleep mode + bool keepalive_enabled() const { + return GetField(VT_KEEPALIVE_ENABLED, 0) != 0; } - uint8_t tx_pin() const { - return ::flatbuffers::EndianScalar(tx_pin_); + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_TX_PIN, 1) && + VerifyField(verifier, VT_KEEPALIVE_ENABLED, 1) && + verifier.EndTable(); } }; -FLATBUFFERS_STRUCT_END(RFConfig, 1); - -struct RFConfig::Traits { - using type = RFConfig; -}; -FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(1) CaptivePortalConfig FLATBUFFERS_FINAL_CLASS { - private: - uint8_t always_enabled_; - - public: - struct Traits; - static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { - return "OpenShock.Serialization.Configuration.CaptivePortalConfig"; +struct RFConfigBuilder { + typedef RFConfig Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_tx_pin(uint8_t tx_pin) { + fbb_.AddElement(RFConfig::VT_TX_PIN, tx_pin, 0); } - CaptivePortalConfig() - : always_enabled_(0) { + void add_keepalive_enabled(bool keepalive_enabled) { + fbb_.AddElement(RFConfig::VT_KEEPALIVE_ENABLED, static_cast(keepalive_enabled), 0); } - CaptivePortalConfig(bool _always_enabled) - : always_enabled_(::flatbuffers::EndianScalar(static_cast(_always_enabled))) { + explicit RFConfigBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); } - bool always_enabled() const { - return ::flatbuffers::EndianScalar(always_enabled_) != 0; + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; } }; -FLATBUFFERS_STRUCT_END(CaptivePortalConfig, 1); -struct CaptivePortalConfig::Traits { - using type = CaptivePortalConfig; +inline ::flatbuffers::Offset CreateRFConfig( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint8_t tx_pin = 0, + bool keepalive_enabled = false) { + RFConfigBuilder builder_(_fbb); + builder_.add_keepalive_enabled(keepalive_enabled); + builder_.add_tx_pin(tx_pin); + return builder_.Finish(); +} + +struct RFConfig::Traits { + using type = RFConfig; + static auto constexpr Create = CreateRFConfig; }; struct WiFiCredentials FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { @@ -119,18 +187,17 @@ struct WiFiCredentials FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { VT_ID = 4, VT_SSID = 6, - VT_BSSID = 8, - VT_PASSWORD = 10 + VT_PASSWORD = 8 }; + /// ID of the WiFi network credentials, used for referencing the credentials with a low memory footprint uint8_t id() const { return GetField(VT_ID, 0); } + /// SSID of the WiFi network const ::flatbuffers::String *ssid() const { return GetPointer(VT_SSID); } - const OpenShock::Serialization::Configuration::BSSID *bssid() const { - return GetStruct(VT_BSSID); - } + /// Password of the WiFi network const ::flatbuffers::String *password() const { return GetPointer(VT_PASSWORD); } @@ -139,7 +206,6 @@ struct WiFiCredentials FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { VerifyField(verifier, VT_ID, 1) && VerifyOffset(verifier, VT_SSID) && verifier.VerifyString(ssid()) && - VerifyField(verifier, VT_BSSID, 1) && VerifyOffset(verifier, VT_PASSWORD) && verifier.VerifyString(password()) && verifier.EndTable(); @@ -156,9 +222,6 @@ struct WiFiCredentialsBuilder { void add_ssid(::flatbuffers::Offset<::flatbuffers::String> ssid) { fbb_.AddOffset(WiFiCredentials::VT_SSID, ssid); } - void add_bssid(const OpenShock::Serialization::Configuration::BSSID *bssid) { - fbb_.AddStruct(WiFiCredentials::VT_BSSID, bssid); - } void add_password(::flatbuffers::Offset<::flatbuffers::String> password) { fbb_.AddOffset(WiFiCredentials::VT_PASSWORD, password); } @@ -177,11 +240,9 @@ inline ::flatbuffers::Offset CreateWiFiCredentials( ::flatbuffers::FlatBufferBuilder &_fbb, uint8_t id = 0, ::flatbuffers::Offset<::flatbuffers::String> ssid = 0, - const OpenShock::Serialization::Configuration::BSSID *bssid = nullptr, ::flatbuffers::Offset<::flatbuffers::String> password = 0) { WiFiCredentialsBuilder builder_(_fbb); builder_.add_password(password); - builder_.add_bssid(bssid); builder_.add_ssid(ssid); builder_.add_id(id); return builder_.Finish(); @@ -196,7 +257,6 @@ inline ::flatbuffers::Offset CreateWiFiCredentialsDirect( ::flatbuffers::FlatBufferBuilder &_fbb, uint8_t id = 0, const char *ssid = nullptr, - const OpenShock::Serialization::Configuration::BSSID *bssid = nullptr, const char *password = nullptr) { auto ssid__ = ssid ? _fbb.CreateString(ssid) : 0; auto password__ = password ? _fbb.CreateString(password) : 0; @@ -204,7 +264,6 @@ inline ::flatbuffers::Offset CreateWiFiCredentialsDirect( _fbb, id, ssid__, - bssid, password__); } @@ -219,12 +278,15 @@ struct WiFiConfig FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { VT_HOSTNAME = 6, VT_CREDENTIALS = 8 }; + /// Access point SSID const ::flatbuffers::String *ap_ssid() const { return GetPointer(VT_AP_SSID); } + /// Device hostname const ::flatbuffers::String *hostname() const { return GetPointer(VT_HOSTNAME); } + /// WiFi network credentials const ::flatbuffers::Vector<::flatbuffers::Offset> *credentials() const { return GetPointer> *>(VT_CREDENTIALS); } @@ -297,6 +359,58 @@ inline ::flatbuffers::Offset CreateWiFiConfigDirect( credentials__); } +struct CaptivePortalConfig FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef CaptivePortalConfigBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Configuration.CaptivePortalConfig"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_ALWAYS_ENABLED = 4 + }; + /// Whether the captive portal is forced to be enabled + /// The captive portal will otherwise shut down when a gateway connection is established + bool always_enabled() const { + return GetField(VT_ALWAYS_ENABLED, 0) != 0; + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_ALWAYS_ENABLED, 1) && + verifier.EndTable(); + } +}; + +struct CaptivePortalConfigBuilder { + typedef CaptivePortalConfig Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_always_enabled(bool always_enabled) { + fbb_.AddElement(CaptivePortalConfig::VT_ALWAYS_ENABLED, static_cast(always_enabled), 0); + } + explicit CaptivePortalConfigBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateCaptivePortalConfig( + ::flatbuffers::FlatBufferBuilder &_fbb, + bool always_enabled = false) { + CaptivePortalConfigBuilder builder_(_fbb); + builder_.add_always_enabled(always_enabled); + return builder_.Finish(); +} + +struct CaptivePortalConfig::Traits { + using type = CaptivePortalConfig; + static auto constexpr Create = CreateCaptivePortalConfig; +}; + struct BackendConfig FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { typedef BackendConfigBuilder Builder; struct Traits; @@ -304,19 +418,21 @@ struct BackendConfig FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { return "OpenShock.Serialization.Configuration.BackendConfig"; } enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { - VT_HOST = 4, + VT_DOMAIN = 4, VT_AUTH_TOKEN = 6 }; - const ::flatbuffers::String *host() const { - return GetPointer(VT_HOST); + /// Domain name of the backend server, e.g. "api.shocklink.net" + const ::flatbuffers::String *domain() const { + return GetPointer(VT_DOMAIN); } + /// Authentication token for the backend server const ::flatbuffers::String *auth_token() const { return GetPointer(VT_AUTH_TOKEN); } bool Verify(::flatbuffers::Verifier &verifier) const { return VerifyTableStart(verifier) && - VerifyOffset(verifier, VT_HOST) && - verifier.VerifyString(host()) && + VerifyOffset(verifier, VT_DOMAIN) && + verifier.VerifyString(domain()) && VerifyOffset(verifier, VT_AUTH_TOKEN) && verifier.VerifyString(auth_token()) && verifier.EndTable(); @@ -327,8 +443,8 @@ struct BackendConfigBuilder { typedef BackendConfig Table; ::flatbuffers::FlatBufferBuilder &fbb_; ::flatbuffers::uoffset_t start_; - void add_host(::flatbuffers::Offset<::flatbuffers::String> host) { - fbb_.AddOffset(BackendConfig::VT_HOST, host); + void add_domain(::flatbuffers::Offset<::flatbuffers::String> domain) { + fbb_.AddOffset(BackendConfig::VT_DOMAIN, domain); } void add_auth_token(::flatbuffers::Offset<::flatbuffers::String> auth_token) { fbb_.AddOffset(BackendConfig::VT_AUTH_TOKEN, auth_token); @@ -346,11 +462,11 @@ struct BackendConfigBuilder { inline ::flatbuffers::Offset CreateBackendConfig( ::flatbuffers::FlatBufferBuilder &_fbb, - ::flatbuffers::Offset<::flatbuffers::String> host = 0, + ::flatbuffers::Offset<::flatbuffers::String> domain = 0, ::flatbuffers::Offset<::flatbuffers::String> auth_token = 0) { BackendConfigBuilder builder_(_fbb); builder_.add_auth_token(auth_token); - builder_.add_host(host); + builder_.add_domain(domain); return builder_.Finish(); } @@ -361,16 +477,245 @@ struct BackendConfig::Traits { inline ::flatbuffers::Offset CreateBackendConfigDirect( ::flatbuffers::FlatBufferBuilder &_fbb, - const char *host = nullptr, + const char *domain = nullptr, const char *auth_token = nullptr) { - auto host__ = host ? _fbb.CreateString(host) : 0; + auto domain__ = domain ? _fbb.CreateString(domain) : 0; auto auth_token__ = auth_token ? _fbb.CreateString(auth_token) : 0; return OpenShock::Serialization::Configuration::CreateBackendConfig( _fbb, - host__, + domain__, auth_token__); } +struct SerialInputConfig FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef SerialInputConfigBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Configuration.SerialInputConfig"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_ECHO_ENABLED = 4 + }; + /// Whether to echo typed characters back to the serial console + bool echo_enabled() const { + return GetField(VT_ECHO_ENABLED, 1) != 0; + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_ECHO_ENABLED, 1) && + verifier.EndTable(); + } +}; + +struct SerialInputConfigBuilder { + typedef SerialInputConfig Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_echo_enabled(bool echo_enabled) { + fbb_.AddElement(SerialInputConfig::VT_ECHO_ENABLED, static_cast(echo_enabled), 1); + } + explicit SerialInputConfigBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateSerialInputConfig( + ::flatbuffers::FlatBufferBuilder &_fbb, + bool echo_enabled = true) { + SerialInputConfigBuilder builder_(_fbb); + builder_.add_echo_enabled(echo_enabled); + return builder_.Finish(); +} + +struct SerialInputConfig::Traits { + using type = SerialInputConfig; + static auto constexpr Create = CreateSerialInputConfig; +}; + +struct OtaUpdateConfig FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef OtaUpdateConfigBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Configuration.OtaUpdateConfig"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_IS_ENABLED = 4, + VT_CDN_DOMAIN = 6, + VT_UPDATE_CHANNEL = 8, + VT_CHECK_ON_STARTUP = 10, + VT_CHECK_PERIODICALLY = 12, + VT_CHECK_INTERVAL = 14, + VT_ALLOW_BACKEND_MANAGEMENT = 16, + VT_REQUIRE_MANUAL_APPROVAL = 18, + VT_UPDATE_ID = 20, + VT_UPDATE_STEP = 22 + }; + /// Indicates whether OTA updates are enabled. + bool is_enabled() const { + return GetField(VT_IS_ENABLED, 0) != 0; + } + /// The domain name of the OTA Content Delivery Network (CDN). + const ::flatbuffers::String *cdn_domain() const { + return GetPointer(VT_CDN_DOMAIN); + } + /// The update channel to use. + OpenShock::Serialization::Configuration::OtaUpdateChannel update_channel() const { + return static_cast(GetField(VT_UPDATE_CHANNEL, 0)); + } + /// Indicates whether to check for updates on startup. + bool check_on_startup() const { + return GetField(VT_CHECK_ON_STARTUP, 0) != 0; + } + /// Indicates whether to check for updates periodically. + bool check_periodically() const { + return GetField(VT_CHECK_PERIODICALLY, 0) != 0; + } + /// The interval in minutes between periodic update checks. + uint16_t check_interval() const { + return GetField(VT_CHECK_INTERVAL, 0); + } + /// Indicates if the backend is authorized to manage the device's update version on behalf of the user. + bool allow_backend_management() const { + return GetField(VT_ALLOW_BACKEND_MANAGEMENT, 0) != 0; + } + /// Indicates if manual approval via serial input or captive portal is required before installing updates. + bool require_manual_approval() const { + return GetField(VT_REQUIRE_MANUAL_APPROVAL, 0) != 0; + } + /// Update process ID, used to track the update process server-side across reboots. + int32_t update_id() const { + return GetField(VT_UPDATE_ID, 0); + } + /// Indicates what step of the update process the device is currently in, used to detect failed updates for status reporting. + OpenShock::Serialization::Configuration::OtaUpdateStep update_step() const { + return static_cast(GetField(VT_UPDATE_STEP, 0)); + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_IS_ENABLED, 1) && + VerifyOffset(verifier, VT_CDN_DOMAIN) && + verifier.VerifyString(cdn_domain()) && + VerifyField(verifier, VT_UPDATE_CHANNEL, 1) && + VerifyField(verifier, VT_CHECK_ON_STARTUP, 1) && + VerifyField(verifier, VT_CHECK_PERIODICALLY, 1) && + VerifyField(verifier, VT_CHECK_INTERVAL, 2) && + VerifyField(verifier, VT_ALLOW_BACKEND_MANAGEMENT, 1) && + VerifyField(verifier, VT_REQUIRE_MANUAL_APPROVAL, 1) && + VerifyField(verifier, VT_UPDATE_ID, 4) && + VerifyField(verifier, VT_UPDATE_STEP, 1) && + verifier.EndTable(); + } +}; + +struct OtaUpdateConfigBuilder { + typedef OtaUpdateConfig Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_is_enabled(bool is_enabled) { + fbb_.AddElement(OtaUpdateConfig::VT_IS_ENABLED, static_cast(is_enabled), 0); + } + void add_cdn_domain(::flatbuffers::Offset<::flatbuffers::String> cdn_domain) { + fbb_.AddOffset(OtaUpdateConfig::VT_CDN_DOMAIN, cdn_domain); + } + void add_update_channel(OpenShock::Serialization::Configuration::OtaUpdateChannel update_channel) { + fbb_.AddElement(OtaUpdateConfig::VT_UPDATE_CHANNEL, static_cast(update_channel), 0); + } + void add_check_on_startup(bool check_on_startup) { + fbb_.AddElement(OtaUpdateConfig::VT_CHECK_ON_STARTUP, static_cast(check_on_startup), 0); + } + void add_check_periodically(bool check_periodically) { + fbb_.AddElement(OtaUpdateConfig::VT_CHECK_PERIODICALLY, static_cast(check_periodically), 0); + } + void add_check_interval(uint16_t check_interval) { + fbb_.AddElement(OtaUpdateConfig::VT_CHECK_INTERVAL, check_interval, 0); + } + void add_allow_backend_management(bool allow_backend_management) { + fbb_.AddElement(OtaUpdateConfig::VT_ALLOW_BACKEND_MANAGEMENT, static_cast(allow_backend_management), 0); + } + void add_require_manual_approval(bool require_manual_approval) { + fbb_.AddElement(OtaUpdateConfig::VT_REQUIRE_MANUAL_APPROVAL, static_cast(require_manual_approval), 0); + } + void add_update_id(int32_t update_id) { + fbb_.AddElement(OtaUpdateConfig::VT_UPDATE_ID, update_id, 0); + } + void add_update_step(OpenShock::Serialization::Configuration::OtaUpdateStep update_step) { + fbb_.AddElement(OtaUpdateConfig::VT_UPDATE_STEP, static_cast(update_step), 0); + } + explicit OtaUpdateConfigBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateOtaUpdateConfig( + ::flatbuffers::FlatBufferBuilder &_fbb, + bool is_enabled = false, + ::flatbuffers::Offset<::flatbuffers::String> cdn_domain = 0, + OpenShock::Serialization::Configuration::OtaUpdateChannel update_channel = OpenShock::Serialization::Configuration::OtaUpdateChannel::Stable, + bool check_on_startup = false, + bool check_periodically = false, + uint16_t check_interval = 0, + bool allow_backend_management = false, + bool require_manual_approval = false, + int32_t update_id = 0, + OpenShock::Serialization::Configuration::OtaUpdateStep update_step = OpenShock::Serialization::Configuration::OtaUpdateStep::None) { + OtaUpdateConfigBuilder builder_(_fbb); + builder_.add_update_id(update_id); + builder_.add_cdn_domain(cdn_domain); + builder_.add_check_interval(check_interval); + builder_.add_update_step(update_step); + builder_.add_require_manual_approval(require_manual_approval); + builder_.add_allow_backend_management(allow_backend_management); + builder_.add_check_periodically(check_periodically); + builder_.add_check_on_startup(check_on_startup); + builder_.add_update_channel(update_channel); + builder_.add_is_enabled(is_enabled); + return builder_.Finish(); +} + +struct OtaUpdateConfig::Traits { + using type = OtaUpdateConfig; + static auto constexpr Create = CreateOtaUpdateConfig; +}; + +inline ::flatbuffers::Offset CreateOtaUpdateConfigDirect( + ::flatbuffers::FlatBufferBuilder &_fbb, + bool is_enabled = false, + const char *cdn_domain = nullptr, + OpenShock::Serialization::Configuration::OtaUpdateChannel update_channel = OpenShock::Serialization::Configuration::OtaUpdateChannel::Stable, + bool check_on_startup = false, + bool check_periodically = false, + uint16_t check_interval = 0, + bool allow_backend_management = false, + bool require_manual_approval = false, + int32_t update_id = 0, + OpenShock::Serialization::Configuration::OtaUpdateStep update_step = OpenShock::Serialization::Configuration::OtaUpdateStep::None) { + auto cdn_domain__ = cdn_domain ? _fbb.CreateString(cdn_domain) : 0; + return OpenShock::Serialization::Configuration::CreateOtaUpdateConfig( + _fbb, + is_enabled, + cdn_domain__, + update_channel, + check_on_startup, + check_periodically, + check_interval, + allow_backend_management, + require_manual_approval, + update_id, + update_step); +} + struct Config FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { typedef ConfigBuilder Builder; struct Traits; @@ -381,28 +726,48 @@ struct Config FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { VT_RF = 4, VT_WIFI = 6, VT_CAPTIVE_PORTAL = 8, - VT_BACKEND = 10 + VT_BACKEND = 10, + VT_SERIAL_INPUT = 12, + VT_OTA_UPDATE = 14 }; + /// RF Transmitter configuration const OpenShock::Serialization::Configuration::RFConfig *rf() const { - return GetStruct(VT_RF); + return GetPointer(VT_RF); } + /// WiFi configuration const OpenShock::Serialization::Configuration::WiFiConfig *wifi() const { return GetPointer(VT_WIFI); } + /// Captive portal configuration const OpenShock::Serialization::Configuration::CaptivePortalConfig *captive_portal() const { - return GetStruct(VT_CAPTIVE_PORTAL); + return GetPointer(VT_CAPTIVE_PORTAL); } + /// Backend configuration const OpenShock::Serialization::Configuration::BackendConfig *backend() const { return GetPointer(VT_BACKEND); } + /// Serial input configuration + const OpenShock::Serialization::Configuration::SerialInputConfig *serial_input() const { + return GetPointer(VT_SERIAL_INPUT); + } + /// OTA update configuration + const OpenShock::Serialization::Configuration::OtaUpdateConfig *ota_update() const { + return GetPointer(VT_OTA_UPDATE); + } bool Verify(::flatbuffers::Verifier &verifier) const { return VerifyTableStart(verifier) && - VerifyField(verifier, VT_RF, 1) && + VerifyOffset(verifier, VT_RF) && + verifier.VerifyTable(rf()) && VerifyOffset(verifier, VT_WIFI) && verifier.VerifyTable(wifi()) && - VerifyField(verifier, VT_CAPTIVE_PORTAL, 1) && + VerifyOffset(verifier, VT_CAPTIVE_PORTAL) && + verifier.VerifyTable(captive_portal()) && VerifyOffset(verifier, VT_BACKEND) && verifier.VerifyTable(backend()) && + VerifyOffset(verifier, VT_SERIAL_INPUT) && + verifier.VerifyTable(serial_input()) && + VerifyOffset(verifier, VT_OTA_UPDATE) && + verifier.VerifyTable(ota_update()) && verifier.EndTable(); } }; @@ -411,18 +776,24 @@ struct ConfigBuilder { typedef Config Table; ::flatbuffers::FlatBufferBuilder &fbb_; ::flatbuffers::uoffset_t start_; - void add_rf(const OpenShock::Serialization::Configuration::RFConfig *rf) { - fbb_.AddStruct(Config::VT_RF, rf); + void add_rf(::flatbuffers::Offset rf) { + fbb_.AddOffset(Config::VT_RF, rf); } void add_wifi(::flatbuffers::Offset wifi) { fbb_.AddOffset(Config::VT_WIFI, wifi); } - void add_captive_portal(const OpenShock::Serialization::Configuration::CaptivePortalConfig *captive_portal) { - fbb_.AddStruct(Config::VT_CAPTIVE_PORTAL, captive_portal); + void add_captive_portal(::flatbuffers::Offset captive_portal) { + fbb_.AddOffset(Config::VT_CAPTIVE_PORTAL, captive_portal); } void add_backend(::flatbuffers::Offset backend) { fbb_.AddOffset(Config::VT_BACKEND, backend); } + void add_serial_input(::flatbuffers::Offset serial_input) { + fbb_.AddOffset(Config::VT_SERIAL_INPUT, serial_input); + } + void add_ota_update(::flatbuffers::Offset ota_update) { + fbb_.AddOffset(Config::VT_OTA_UPDATE, ota_update); + } explicit ConfigBuilder(::flatbuffers::FlatBufferBuilder &_fbb) : fbb_(_fbb) { start_ = fbb_.StartTable(); @@ -436,11 +807,15 @@ struct ConfigBuilder { inline ::flatbuffers::Offset CreateConfig( ::flatbuffers::FlatBufferBuilder &_fbb, - const OpenShock::Serialization::Configuration::RFConfig *rf = nullptr, + ::flatbuffers::Offset rf = 0, ::flatbuffers::Offset wifi = 0, - const OpenShock::Serialization::Configuration::CaptivePortalConfig *captive_portal = nullptr, - ::flatbuffers::Offset backend = 0) { + ::flatbuffers::Offset captive_portal = 0, + ::flatbuffers::Offset backend = 0, + ::flatbuffers::Offset serial_input = 0, + ::flatbuffers::Offset ota_update = 0) { ConfigBuilder builder_(_fbb); + builder_.add_ota_update(ota_update); + builder_.add_serial_input(serial_input); builder_.add_backend(backend); builder_.add_captive_portal(captive_portal); builder_.add_wifi(wifi); diff --git a/include/serialization/_fbs/DeviceToGatewayMessage_generated.h b/include/serialization/_fbs/DeviceToGatewayMessage_generated.h new file mode 100644 index 00000000..a853d7f2 --- /dev/null +++ b/include/serialization/_fbs/DeviceToGatewayMessage_generated.h @@ -0,0 +1,636 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_DEVICETOGATEWAYMESSAGE_OPENSHOCK_SERIALIZATION_GATEWAY_H_ +#define FLATBUFFERS_GENERATED_DEVICETOGATEWAYMESSAGE_OPENSHOCK_SERIALIZATION_GATEWAY_H_ + +#include "flatbuffers/flatbuffers.h" + +// Ensure the included flatbuffers.h is the same version as when this file was +// generated, otherwise it may not be compatible. +static_assert(FLATBUFFERS_VERSION_MAJOR == 23 && + FLATBUFFERS_VERSION_MINOR == 5 && + FLATBUFFERS_VERSION_REVISION == 26, + "Non-compatible flatbuffers version included"); + +#include "FirmwareBootType_generated.h" +#include "SemVer_generated.h" + +namespace OpenShock { +namespace Serialization { +namespace Gateway { + +struct KeepAlive; + +struct BootStatus; +struct BootStatusBuilder; + +struct OtaInstallStarted; +struct OtaInstallStartedBuilder; + +struct OtaInstallProgress; +struct OtaInstallProgressBuilder; + +struct OtaInstallFailed; +struct OtaInstallFailedBuilder; + +struct DeviceToGatewayMessage; +struct DeviceToGatewayMessageBuilder; + +enum class OtaInstallProgressTask : int8_t { + FetchingMetadata = 0, + PreparingForInstall = 1, + FlashingFilesystem = 2, + VerifyingFilesystem = 3, + FlashingApplication = 4, + MarkingApplicationBootable = 5, + Rebooting = 6, + MIN = FetchingMetadata, + MAX = Rebooting +}; + +inline const OtaInstallProgressTask (&EnumValuesOtaInstallProgressTask())[7] { + static const OtaInstallProgressTask values[] = { + OtaInstallProgressTask::FetchingMetadata, + OtaInstallProgressTask::PreparingForInstall, + OtaInstallProgressTask::FlashingFilesystem, + OtaInstallProgressTask::VerifyingFilesystem, + OtaInstallProgressTask::FlashingApplication, + OtaInstallProgressTask::MarkingApplicationBootable, + OtaInstallProgressTask::Rebooting + }; + return values; +} + +inline const char * const *EnumNamesOtaInstallProgressTask() { + static const char * const names[8] = { + "FetchingMetadata", + "PreparingForInstall", + "FlashingFilesystem", + "VerifyingFilesystem", + "FlashingApplication", + "MarkingApplicationBootable", + "Rebooting", + nullptr + }; + return names; +} + +inline const char *EnumNameOtaInstallProgressTask(OtaInstallProgressTask e) { + if (::flatbuffers::IsOutRange(e, OtaInstallProgressTask::FetchingMetadata, OtaInstallProgressTask::Rebooting)) return ""; + const size_t index = static_cast(e); + return EnumNamesOtaInstallProgressTask()[index]; +} + +enum class DeviceToGatewayMessagePayload : uint8_t { + NONE = 0, + KeepAlive = 1, + BootStatus = 2, + OtaInstallStarted = 3, + OtaInstallProgress = 4, + OtaInstallFailed = 5, + MIN = NONE, + MAX = OtaInstallFailed +}; + +inline const DeviceToGatewayMessagePayload (&EnumValuesDeviceToGatewayMessagePayload())[6] { + static const DeviceToGatewayMessagePayload values[] = { + DeviceToGatewayMessagePayload::NONE, + DeviceToGatewayMessagePayload::KeepAlive, + DeviceToGatewayMessagePayload::BootStatus, + DeviceToGatewayMessagePayload::OtaInstallStarted, + DeviceToGatewayMessagePayload::OtaInstallProgress, + DeviceToGatewayMessagePayload::OtaInstallFailed + }; + return values; +} + +inline const char * const *EnumNamesDeviceToGatewayMessagePayload() { + static const char * const names[7] = { + "NONE", + "KeepAlive", + "BootStatus", + "OtaInstallStarted", + "OtaInstallProgress", + "OtaInstallFailed", + nullptr + }; + return names; +} + +inline const char *EnumNameDeviceToGatewayMessagePayload(DeviceToGatewayMessagePayload e) { + if (::flatbuffers::IsOutRange(e, DeviceToGatewayMessagePayload::NONE, DeviceToGatewayMessagePayload::OtaInstallFailed)) return ""; + const size_t index = static_cast(e); + return EnumNamesDeviceToGatewayMessagePayload()[index]; +} + +template struct DeviceToGatewayMessagePayloadTraits { + static const DeviceToGatewayMessagePayload enum_value = DeviceToGatewayMessagePayload::NONE; +}; + +template<> struct DeviceToGatewayMessagePayloadTraits { + static const DeviceToGatewayMessagePayload enum_value = DeviceToGatewayMessagePayload::KeepAlive; +}; + +template<> struct DeviceToGatewayMessagePayloadTraits { + static const DeviceToGatewayMessagePayload enum_value = DeviceToGatewayMessagePayload::BootStatus; +}; + +template<> struct DeviceToGatewayMessagePayloadTraits { + static const DeviceToGatewayMessagePayload enum_value = DeviceToGatewayMessagePayload::OtaInstallStarted; +}; + +template<> struct DeviceToGatewayMessagePayloadTraits { + static const DeviceToGatewayMessagePayload enum_value = DeviceToGatewayMessagePayload::OtaInstallProgress; +}; + +template<> struct DeviceToGatewayMessagePayloadTraits { + static const DeviceToGatewayMessagePayload enum_value = DeviceToGatewayMessagePayload::OtaInstallFailed; +}; + +bool VerifyDeviceToGatewayMessagePayload(::flatbuffers::Verifier &verifier, const void *obj, DeviceToGatewayMessagePayload type); +bool VerifyDeviceToGatewayMessagePayloadVector(::flatbuffers::Verifier &verifier, const ::flatbuffers::Vector<::flatbuffers::Offset> *values, const ::flatbuffers::Vector *types); + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(8) KeepAlive FLATBUFFERS_FINAL_CLASS { + private: + uint64_t uptime_; + + public: + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Gateway.KeepAlive"; + } + KeepAlive() + : uptime_(0) { + } + KeepAlive(uint64_t _uptime) + : uptime_(::flatbuffers::EndianScalar(_uptime)) { + } + uint64_t uptime() const { + return ::flatbuffers::EndianScalar(uptime_); + } +}; +FLATBUFFERS_STRUCT_END(KeepAlive, 8); + +struct KeepAlive::Traits { + using type = KeepAlive; +}; + +struct BootStatus FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef BootStatusBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Gateway.BootStatus"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_BOOT_TYPE = 4, + VT_FIRMWARE_VERSION = 6, + VT_OTA_UPDATE_ID = 8 + }; + OpenShock::Serialization::Types::FirmwareBootType boot_type() const { + return static_cast(GetField(VT_BOOT_TYPE, 0)); + } + const OpenShock::Serialization::Types::SemVer *firmware_version() const { + return GetPointer(VT_FIRMWARE_VERSION); + } + int32_t ota_update_id() const { + return GetField(VT_OTA_UPDATE_ID, 0); + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_BOOT_TYPE, 1) && + VerifyOffset(verifier, VT_FIRMWARE_VERSION) && + verifier.VerifyTable(firmware_version()) && + VerifyField(verifier, VT_OTA_UPDATE_ID, 4) && + verifier.EndTable(); + } +}; + +struct BootStatusBuilder { + typedef BootStatus Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_boot_type(OpenShock::Serialization::Types::FirmwareBootType boot_type) { + fbb_.AddElement(BootStatus::VT_BOOT_TYPE, static_cast(boot_type), 0); + } + void add_firmware_version(::flatbuffers::Offset firmware_version) { + fbb_.AddOffset(BootStatus::VT_FIRMWARE_VERSION, firmware_version); + } + void add_ota_update_id(int32_t ota_update_id) { + fbb_.AddElement(BootStatus::VT_OTA_UPDATE_ID, ota_update_id, 0); + } + explicit BootStatusBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateBootStatus( + ::flatbuffers::FlatBufferBuilder &_fbb, + OpenShock::Serialization::Types::FirmwareBootType boot_type = OpenShock::Serialization::Types::FirmwareBootType::Normal, + ::flatbuffers::Offset firmware_version = 0, + int32_t ota_update_id = 0) { + BootStatusBuilder builder_(_fbb); + builder_.add_ota_update_id(ota_update_id); + builder_.add_firmware_version(firmware_version); + builder_.add_boot_type(boot_type); + return builder_.Finish(); +} + +struct BootStatus::Traits { + using type = BootStatus; + static auto constexpr Create = CreateBootStatus; +}; + +struct OtaInstallStarted FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef OtaInstallStartedBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Gateway.OtaInstallStarted"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_UPDATE_ID = 4, + VT_VERSION = 6 + }; + int32_t update_id() const { + return GetField(VT_UPDATE_ID, 0); + } + const OpenShock::Serialization::Types::SemVer *version() const { + return GetPointer(VT_VERSION); + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_UPDATE_ID, 4) && + VerifyOffset(verifier, VT_VERSION) && + verifier.VerifyTable(version()) && + verifier.EndTable(); + } +}; + +struct OtaInstallStartedBuilder { + typedef OtaInstallStarted Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_update_id(int32_t update_id) { + fbb_.AddElement(OtaInstallStarted::VT_UPDATE_ID, update_id, 0); + } + void add_version(::flatbuffers::Offset version) { + fbb_.AddOffset(OtaInstallStarted::VT_VERSION, version); + } + explicit OtaInstallStartedBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateOtaInstallStarted( + ::flatbuffers::FlatBufferBuilder &_fbb, + int32_t update_id = 0, + ::flatbuffers::Offset version = 0) { + OtaInstallStartedBuilder builder_(_fbb); + builder_.add_version(version); + builder_.add_update_id(update_id); + return builder_.Finish(); +} + +struct OtaInstallStarted::Traits { + using type = OtaInstallStarted; + static auto constexpr Create = CreateOtaInstallStarted; +}; + +struct OtaInstallProgress FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef OtaInstallProgressBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Gateway.OtaInstallProgress"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_UPDATE_ID = 4, + VT_TASK = 6, + VT_PROGRESS = 8 + }; + int32_t update_id() const { + return GetField(VT_UPDATE_ID, 0); + } + OpenShock::Serialization::Gateway::OtaInstallProgressTask task() const { + return static_cast(GetField(VT_TASK, 0)); + } + float progress() const { + return GetField(VT_PROGRESS, 0.0f); + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_UPDATE_ID, 4) && + VerifyField(verifier, VT_TASK, 1) && + VerifyField(verifier, VT_PROGRESS, 4) && + verifier.EndTable(); + } +}; + +struct OtaInstallProgressBuilder { + typedef OtaInstallProgress Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_update_id(int32_t update_id) { + fbb_.AddElement(OtaInstallProgress::VT_UPDATE_ID, update_id, 0); + } + void add_task(OpenShock::Serialization::Gateway::OtaInstallProgressTask task) { + fbb_.AddElement(OtaInstallProgress::VT_TASK, static_cast(task), 0); + } + void add_progress(float progress) { + fbb_.AddElement(OtaInstallProgress::VT_PROGRESS, progress, 0.0f); + } + explicit OtaInstallProgressBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateOtaInstallProgress( + ::flatbuffers::FlatBufferBuilder &_fbb, + int32_t update_id = 0, + OpenShock::Serialization::Gateway::OtaInstallProgressTask task = OpenShock::Serialization::Gateway::OtaInstallProgressTask::FetchingMetadata, + float progress = 0.0f) { + OtaInstallProgressBuilder builder_(_fbb); + builder_.add_progress(progress); + builder_.add_update_id(update_id); + builder_.add_task(task); + return builder_.Finish(); +} + +struct OtaInstallProgress::Traits { + using type = OtaInstallProgress; + static auto constexpr Create = CreateOtaInstallProgress; +}; + +struct OtaInstallFailed FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef OtaInstallFailedBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Gateway.OtaInstallFailed"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_UPDATE_ID = 4, + VT_MESSAGE = 6, + VT_FATAL = 8 + }; + int32_t update_id() const { + return GetField(VT_UPDATE_ID, 0); + } + const ::flatbuffers::String *message() const { + return GetPointer(VT_MESSAGE); + } + bool fatal() const { + return GetField(VT_FATAL, 0) != 0; + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_UPDATE_ID, 4) && + VerifyOffset(verifier, VT_MESSAGE) && + verifier.VerifyString(message()) && + VerifyField(verifier, VT_FATAL, 1) && + verifier.EndTable(); + } +}; + +struct OtaInstallFailedBuilder { + typedef OtaInstallFailed Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_update_id(int32_t update_id) { + fbb_.AddElement(OtaInstallFailed::VT_UPDATE_ID, update_id, 0); + } + void add_message(::flatbuffers::Offset<::flatbuffers::String> message) { + fbb_.AddOffset(OtaInstallFailed::VT_MESSAGE, message); + } + void add_fatal(bool fatal) { + fbb_.AddElement(OtaInstallFailed::VT_FATAL, static_cast(fatal), 0); + } + explicit OtaInstallFailedBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateOtaInstallFailed( + ::flatbuffers::FlatBufferBuilder &_fbb, + int32_t update_id = 0, + ::flatbuffers::Offset<::flatbuffers::String> message = 0, + bool fatal = false) { + OtaInstallFailedBuilder builder_(_fbb); + builder_.add_message(message); + builder_.add_update_id(update_id); + builder_.add_fatal(fatal); + return builder_.Finish(); +} + +struct OtaInstallFailed::Traits { + using type = OtaInstallFailed; + static auto constexpr Create = CreateOtaInstallFailed; +}; + +inline ::flatbuffers::Offset CreateOtaInstallFailedDirect( + ::flatbuffers::FlatBufferBuilder &_fbb, + int32_t update_id = 0, + const char *message = nullptr, + bool fatal = false) { + auto message__ = message ? _fbb.CreateString(message) : 0; + return OpenShock::Serialization::Gateway::CreateOtaInstallFailed( + _fbb, + update_id, + message__, + fatal); +} + +struct DeviceToGatewayMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef DeviceToGatewayMessageBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Gateway.DeviceToGatewayMessage"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_PAYLOAD_TYPE = 4, + VT_PAYLOAD = 6 + }; + OpenShock::Serialization::Gateway::DeviceToGatewayMessagePayload payload_type() const { + return static_cast(GetField(VT_PAYLOAD_TYPE, 0)); + } + const void *payload() const { + return GetPointer(VT_PAYLOAD); + } + template const T *payload_as() const; + const OpenShock::Serialization::Gateway::KeepAlive *payload_as_KeepAlive() const { + return payload_type() == OpenShock::Serialization::Gateway::DeviceToGatewayMessagePayload::KeepAlive ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Gateway::BootStatus *payload_as_BootStatus() const { + return payload_type() == OpenShock::Serialization::Gateway::DeviceToGatewayMessagePayload::BootStatus ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Gateway::OtaInstallStarted *payload_as_OtaInstallStarted() const { + return payload_type() == OpenShock::Serialization::Gateway::DeviceToGatewayMessagePayload::OtaInstallStarted ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Gateway::OtaInstallProgress *payload_as_OtaInstallProgress() const { + return payload_type() == OpenShock::Serialization::Gateway::DeviceToGatewayMessagePayload::OtaInstallProgress ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Gateway::OtaInstallFailed *payload_as_OtaInstallFailed() const { + return payload_type() == OpenShock::Serialization::Gateway::DeviceToGatewayMessagePayload::OtaInstallFailed ? static_cast(payload()) : nullptr; + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_PAYLOAD_TYPE, 1) && + VerifyOffset(verifier, VT_PAYLOAD) && + VerifyDeviceToGatewayMessagePayload(verifier, payload(), payload_type()) && + verifier.EndTable(); + } +}; + +template<> inline const OpenShock::Serialization::Gateway::KeepAlive *DeviceToGatewayMessage::payload_as() const { + return payload_as_KeepAlive(); +} + +template<> inline const OpenShock::Serialization::Gateway::BootStatus *DeviceToGatewayMessage::payload_as() const { + return payload_as_BootStatus(); +} + +template<> inline const OpenShock::Serialization::Gateway::OtaInstallStarted *DeviceToGatewayMessage::payload_as() const { + return payload_as_OtaInstallStarted(); +} + +template<> inline const OpenShock::Serialization::Gateway::OtaInstallProgress *DeviceToGatewayMessage::payload_as() const { + return payload_as_OtaInstallProgress(); +} + +template<> inline const OpenShock::Serialization::Gateway::OtaInstallFailed *DeviceToGatewayMessage::payload_as() const { + return payload_as_OtaInstallFailed(); +} + +struct DeviceToGatewayMessageBuilder { + typedef DeviceToGatewayMessage Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_payload_type(OpenShock::Serialization::Gateway::DeviceToGatewayMessagePayload payload_type) { + fbb_.AddElement(DeviceToGatewayMessage::VT_PAYLOAD_TYPE, static_cast(payload_type), 0); + } + void add_payload(::flatbuffers::Offset payload) { + fbb_.AddOffset(DeviceToGatewayMessage::VT_PAYLOAD, payload); + } + explicit DeviceToGatewayMessageBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateDeviceToGatewayMessage( + ::flatbuffers::FlatBufferBuilder &_fbb, + OpenShock::Serialization::Gateway::DeviceToGatewayMessagePayload payload_type = OpenShock::Serialization::Gateway::DeviceToGatewayMessagePayload::NONE, + ::flatbuffers::Offset payload = 0) { + DeviceToGatewayMessageBuilder builder_(_fbb); + builder_.add_payload(payload); + builder_.add_payload_type(payload_type); + return builder_.Finish(); +} + +struct DeviceToGatewayMessage::Traits { + using type = DeviceToGatewayMessage; + static auto constexpr Create = CreateDeviceToGatewayMessage; +}; + +inline bool VerifyDeviceToGatewayMessagePayload(::flatbuffers::Verifier &verifier, const void *obj, DeviceToGatewayMessagePayload type) { + switch (type) { + case DeviceToGatewayMessagePayload::NONE: { + return true; + } + case DeviceToGatewayMessagePayload::KeepAlive: { + return verifier.VerifyField(static_cast(obj), 0, 8); + } + case DeviceToGatewayMessagePayload::BootStatus: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + case DeviceToGatewayMessagePayload::OtaInstallStarted: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + case DeviceToGatewayMessagePayload::OtaInstallProgress: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + case DeviceToGatewayMessagePayload::OtaInstallFailed: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + default: return true; + } +} + +inline bool VerifyDeviceToGatewayMessagePayloadVector(::flatbuffers::Verifier &verifier, const ::flatbuffers::Vector<::flatbuffers::Offset> *values, const ::flatbuffers::Vector *types) { + if (!values || !types) return !values && !types; + if (values->size() != types->size()) return false; + for (::flatbuffers::uoffset_t i = 0; i < values->size(); ++i) { + if (!VerifyDeviceToGatewayMessagePayload( + verifier, values->Get(i), types->GetEnum(i))) { + return false; + } + } + return true; +} + +inline const OpenShock::Serialization::Gateway::DeviceToGatewayMessage *GetDeviceToGatewayMessage(const void *buf) { + return ::flatbuffers::GetRoot(buf); +} + +inline const OpenShock::Serialization::Gateway::DeviceToGatewayMessage *GetSizePrefixedDeviceToGatewayMessage(const void *buf) { + return ::flatbuffers::GetSizePrefixedRoot(buf); +} + +inline bool VerifyDeviceToGatewayMessageBuffer( + ::flatbuffers::Verifier &verifier) { + return verifier.VerifyBuffer(nullptr); +} + +inline bool VerifySizePrefixedDeviceToGatewayMessageBuffer( + ::flatbuffers::Verifier &verifier) { + return verifier.VerifySizePrefixedBuffer(nullptr); +} + +inline void FinishDeviceToGatewayMessageBuffer( + ::flatbuffers::FlatBufferBuilder &fbb, + ::flatbuffers::Offset root) { + fbb.Finish(root); +} + +inline void FinishSizePrefixedDeviceToGatewayMessageBuffer( + ::flatbuffers::FlatBufferBuilder &fbb, + ::flatbuffers::Offset root) { + fbb.FinishSizePrefixed(root); +} + +} // namespace Gateway +} // namespace Serialization +} // namespace OpenShock + +#endif // FLATBUFFERS_GENERATED_DEVICETOGATEWAYMESSAGE_OPENSHOCK_SERIALIZATION_GATEWAY_H_ diff --git a/include/serialization/_fbs/DeviceToLocalMessage_generated.h b/include/serialization/_fbs/DeviceToLocalMessage_generated.h index 9811b1da..58e04b48 100644 --- a/include/serialization/_fbs/DeviceToLocalMessage_generated.h +++ b/include/serialization/_fbs/DeviceToLocalMessage_generated.h @@ -13,6 +13,7 @@ static_assert(FLATBUFFERS_VERSION_MAJOR == 23 && FLATBUFFERS_VERSION_REVISION == 26, "Non-compatible flatbuffers version included"); +#include "ConfigFile_generated.h" #include "WifiNetwork_generated.h" #include "WifiNetworkEventType_generated.h" #include "WifiScanStatus_generated.h" @@ -300,8 +301,8 @@ struct ReadyMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { VT_POGGIES = 4, VT_CONNECTED_WIFI = 6, - VT_GATEWAY_PAIRED = 8, - VT_RFTX_PIN = 10 + VT_ACCOUNT_LINKED = 8, + VT_CONFIG = 10 }; bool poggies() const { return GetField(VT_POGGIES, 0) != 0; @@ -309,19 +310,20 @@ struct ReadyMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { const OpenShock::Serialization::Types::WifiNetwork *connected_wifi() const { return GetPointer(VT_CONNECTED_WIFI); } - bool gateway_paired() const { - return GetField(VT_GATEWAY_PAIRED, 0) != 0; + bool account_linked() const { + return GetField(VT_ACCOUNT_LINKED, 0) != 0; } - uint8_t rftx_pin() const { - return GetField(VT_RFTX_PIN, 0); + const OpenShock::Serialization::Configuration::Config *config() const { + return GetPointer(VT_CONFIG); } bool Verify(::flatbuffers::Verifier &verifier) const { return VerifyTableStart(verifier) && VerifyField(verifier, VT_POGGIES, 1) && VerifyOffset(verifier, VT_CONNECTED_WIFI) && verifier.VerifyTable(connected_wifi()) && - VerifyField(verifier, VT_GATEWAY_PAIRED, 1) && - VerifyField(verifier, VT_RFTX_PIN, 1) && + VerifyField(verifier, VT_ACCOUNT_LINKED, 1) && + VerifyOffset(verifier, VT_CONFIG) && + verifier.VerifyTable(config()) && verifier.EndTable(); } }; @@ -336,11 +338,11 @@ struct ReadyMessageBuilder { void add_connected_wifi(::flatbuffers::Offset connected_wifi) { fbb_.AddOffset(ReadyMessage::VT_CONNECTED_WIFI, connected_wifi); } - void add_gateway_paired(bool gateway_paired) { - fbb_.AddElement(ReadyMessage::VT_GATEWAY_PAIRED, static_cast(gateway_paired), 0); + void add_account_linked(bool account_linked) { + fbb_.AddElement(ReadyMessage::VT_ACCOUNT_LINKED, static_cast(account_linked), 0); } - void add_rftx_pin(uint8_t rftx_pin) { - fbb_.AddElement(ReadyMessage::VT_RFTX_PIN, rftx_pin, 0); + void add_config(::flatbuffers::Offset config) { + fbb_.AddOffset(ReadyMessage::VT_CONFIG, config); } explicit ReadyMessageBuilder(::flatbuffers::FlatBufferBuilder &_fbb) : fbb_(_fbb) { @@ -357,12 +359,12 @@ inline ::flatbuffers::Offset CreateReadyMessage( ::flatbuffers::FlatBufferBuilder &_fbb, bool poggies = false, ::flatbuffers::Offset connected_wifi = 0, - bool gateway_paired = false, - uint8_t rftx_pin = 0) { + bool account_linked = false, + ::flatbuffers::Offset config = 0) { ReadyMessageBuilder builder_(_fbb); + builder_.add_config(config); builder_.add_connected_wifi(connected_wifi); - builder_.add_rftx_pin(rftx_pin); - builder_.add_gateway_paired(gateway_paired); + builder_.add_account_linked(account_linked); builder_.add_poggies(poggies); return builder_.Finish(); } @@ -440,19 +442,20 @@ struct WifiNetworkEvent FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { } enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { VT_EVENT_TYPE = 4, - VT_NETWORK = 6 + VT_NETWORKS = 6 }; OpenShock::Serialization::Types::WifiNetworkEventType event_type() const { return static_cast(GetField(VT_EVENT_TYPE, 0)); } - const OpenShock::Serialization::Types::WifiNetwork *network() const { - return GetPointer(VT_NETWORK); + const ::flatbuffers::Vector<::flatbuffers::Offset> *networks() const { + return GetPointer> *>(VT_NETWORKS); } bool Verify(::flatbuffers::Verifier &verifier) const { return VerifyTableStart(verifier) && VerifyField(verifier, VT_EVENT_TYPE, 1) && - VerifyOffset(verifier, VT_NETWORK) && - verifier.VerifyTable(network()) && + VerifyOffset(verifier, VT_NETWORKS) && + verifier.VerifyVector(networks()) && + verifier.VerifyVectorOfTables(networks()) && verifier.EndTable(); } }; @@ -464,8 +467,8 @@ struct WifiNetworkEventBuilder { void add_event_type(OpenShock::Serialization::Types::WifiNetworkEventType event_type) { fbb_.AddElement(WifiNetworkEvent::VT_EVENT_TYPE, static_cast(event_type), 0); } - void add_network(::flatbuffers::Offset network) { - fbb_.AddOffset(WifiNetworkEvent::VT_NETWORK, network); + void add_networks(::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> networks) { + fbb_.AddOffset(WifiNetworkEvent::VT_NETWORKS, networks); } explicit WifiNetworkEventBuilder(::flatbuffers::FlatBufferBuilder &_fbb) : fbb_(_fbb) { @@ -481,9 +484,9 @@ struct WifiNetworkEventBuilder { inline ::flatbuffers::Offset CreateWifiNetworkEvent( ::flatbuffers::FlatBufferBuilder &_fbb, OpenShock::Serialization::Types::WifiNetworkEventType event_type = OpenShock::Serialization::Types::WifiNetworkEventType::Discovered, - ::flatbuffers::Offset network = 0) { + ::flatbuffers::Offset<::flatbuffers::Vector<::flatbuffers::Offset>> networks = 0) { WifiNetworkEventBuilder builder_(_fbb); - builder_.add_network(network); + builder_.add_networks(networks); builder_.add_event_type(event_type); return builder_.Finish(); } @@ -493,6 +496,17 @@ struct WifiNetworkEvent::Traits { static auto constexpr Create = CreateWifiNetworkEvent; }; +inline ::flatbuffers::Offset CreateWifiNetworkEventDirect( + ::flatbuffers::FlatBufferBuilder &_fbb, + OpenShock::Serialization::Types::WifiNetworkEventType event_type = OpenShock::Serialization::Types::WifiNetworkEventType::Discovered, + const std::vector<::flatbuffers::Offset> *networks = nullptr) { + auto networks__ = networks ? _fbb.CreateVector<::flatbuffers::Offset>(*networks) : 0; + return OpenShock::Serialization::Local::CreateWifiNetworkEvent( + _fbb, + event_type, + networks__); +} + struct WifiGotIpEvent FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { typedef WifiGotIpEventBuilder Builder; struct Traits; diff --git a/include/serialization/_fbs/DeviceToServerMessage_generated.h b/include/serialization/_fbs/DeviceToServerMessage_generated.h deleted file mode 100644 index 7dc92459..00000000 --- a/include/serialization/_fbs/DeviceToServerMessage_generated.h +++ /dev/null @@ -1,216 +0,0 @@ -// automatically generated by the FlatBuffers compiler, do not modify - - -#ifndef FLATBUFFERS_GENERATED_DEVICETOSERVERMESSAGE_OPENSHOCK_SERIALIZATION_H_ -#define FLATBUFFERS_GENERATED_DEVICETOSERVERMESSAGE_OPENSHOCK_SERIALIZATION_H_ - -#include "flatbuffers/flatbuffers.h" - -// Ensure the included flatbuffers.h is the same version as when this file was -// generated, otherwise it may not be compatible. -static_assert(FLATBUFFERS_VERSION_MAJOR == 23 && - FLATBUFFERS_VERSION_MINOR == 5 && - FLATBUFFERS_VERSION_REVISION == 26, - "Non-compatible flatbuffers version included"); - -namespace OpenShock { -namespace Serialization { - -struct KeepAlive; - -struct DeviceToServerMessage; -struct DeviceToServerMessageBuilder; - -enum class DeviceToServerMessagePayload : uint8_t { - NONE = 0, - KeepAlive = 1, - MIN = NONE, - MAX = KeepAlive -}; - -inline const DeviceToServerMessagePayload (&EnumValuesDeviceToServerMessagePayload())[2] { - static const DeviceToServerMessagePayload values[] = { - DeviceToServerMessagePayload::NONE, - DeviceToServerMessagePayload::KeepAlive - }; - return values; -} - -inline const char * const *EnumNamesDeviceToServerMessagePayload() { - static const char * const names[3] = { - "NONE", - "KeepAlive", - nullptr - }; - return names; -} - -inline const char *EnumNameDeviceToServerMessagePayload(DeviceToServerMessagePayload e) { - if (::flatbuffers::IsOutRange(e, DeviceToServerMessagePayload::NONE, DeviceToServerMessagePayload::KeepAlive)) return ""; - const size_t index = static_cast(e); - return EnumNamesDeviceToServerMessagePayload()[index]; -} - -template struct DeviceToServerMessagePayloadTraits { - static const DeviceToServerMessagePayload enum_value = DeviceToServerMessagePayload::NONE; -}; - -template<> struct DeviceToServerMessagePayloadTraits { - static const DeviceToServerMessagePayload enum_value = DeviceToServerMessagePayload::KeepAlive; -}; - -bool VerifyDeviceToServerMessagePayload(::flatbuffers::Verifier &verifier, const void *obj, DeviceToServerMessagePayload type); -bool VerifyDeviceToServerMessagePayloadVector(::flatbuffers::Verifier &verifier, const ::flatbuffers::Vector<::flatbuffers::Offset> *values, const ::flatbuffers::Vector *types); - -FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(8) KeepAlive FLATBUFFERS_FINAL_CLASS { - private: - uint64_t uptime_; - - public: - struct Traits; - static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { - return "OpenShock.Serialization.KeepAlive"; - } - KeepAlive() - : uptime_(0) { - } - KeepAlive(uint64_t _uptime) - : uptime_(::flatbuffers::EndianScalar(_uptime)) { - } - uint64_t uptime() const { - return ::flatbuffers::EndianScalar(uptime_); - } -}; -FLATBUFFERS_STRUCT_END(KeepAlive, 8); - -struct KeepAlive::Traits { - using type = KeepAlive; -}; - -struct DeviceToServerMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { - typedef DeviceToServerMessageBuilder Builder; - struct Traits; - static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { - return "OpenShock.Serialization.DeviceToServerMessage"; - } - enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { - VT_PAYLOAD_TYPE = 4, - VT_PAYLOAD = 6 - }; - OpenShock::Serialization::DeviceToServerMessagePayload payload_type() const { - return static_cast(GetField(VT_PAYLOAD_TYPE, 0)); - } - const void *payload() const { - return GetPointer(VT_PAYLOAD); - } - template const T *payload_as() const; - const OpenShock::Serialization::KeepAlive *payload_as_KeepAlive() const { - return payload_type() == OpenShock::Serialization::DeviceToServerMessagePayload::KeepAlive ? static_cast(payload()) : nullptr; - } - bool Verify(::flatbuffers::Verifier &verifier) const { - return VerifyTableStart(verifier) && - VerifyField(verifier, VT_PAYLOAD_TYPE, 1) && - VerifyOffset(verifier, VT_PAYLOAD) && - VerifyDeviceToServerMessagePayload(verifier, payload(), payload_type()) && - verifier.EndTable(); - } -}; - -template<> inline const OpenShock::Serialization::KeepAlive *DeviceToServerMessage::payload_as() const { - return payload_as_KeepAlive(); -} - -struct DeviceToServerMessageBuilder { - typedef DeviceToServerMessage Table; - ::flatbuffers::FlatBufferBuilder &fbb_; - ::flatbuffers::uoffset_t start_; - void add_payload_type(OpenShock::Serialization::DeviceToServerMessagePayload payload_type) { - fbb_.AddElement(DeviceToServerMessage::VT_PAYLOAD_TYPE, static_cast(payload_type), 0); - } - void add_payload(::flatbuffers::Offset payload) { - fbb_.AddOffset(DeviceToServerMessage::VT_PAYLOAD, payload); - } - explicit DeviceToServerMessageBuilder(::flatbuffers::FlatBufferBuilder &_fbb) - : fbb_(_fbb) { - start_ = fbb_.StartTable(); - } - ::flatbuffers::Offset Finish() { - const auto end = fbb_.EndTable(start_); - auto o = ::flatbuffers::Offset(end); - return o; - } -}; - -inline ::flatbuffers::Offset CreateDeviceToServerMessage( - ::flatbuffers::FlatBufferBuilder &_fbb, - OpenShock::Serialization::DeviceToServerMessagePayload payload_type = OpenShock::Serialization::DeviceToServerMessagePayload::NONE, - ::flatbuffers::Offset payload = 0) { - DeviceToServerMessageBuilder builder_(_fbb); - builder_.add_payload(payload); - builder_.add_payload_type(payload_type); - return builder_.Finish(); -} - -struct DeviceToServerMessage::Traits { - using type = DeviceToServerMessage; - static auto constexpr Create = CreateDeviceToServerMessage; -}; - -inline bool VerifyDeviceToServerMessagePayload(::flatbuffers::Verifier &verifier, const void *obj, DeviceToServerMessagePayload type) { - switch (type) { - case DeviceToServerMessagePayload::NONE: { - return true; - } - case DeviceToServerMessagePayload::KeepAlive: { - return verifier.VerifyField(static_cast(obj), 0, 8); - } - default: return true; - } -} - -inline bool VerifyDeviceToServerMessagePayloadVector(::flatbuffers::Verifier &verifier, const ::flatbuffers::Vector<::flatbuffers::Offset> *values, const ::flatbuffers::Vector *types) { - if (!values || !types) return !values && !types; - if (values->size() != types->size()) return false; - for (::flatbuffers::uoffset_t i = 0; i < values->size(); ++i) { - if (!VerifyDeviceToServerMessagePayload( - verifier, values->Get(i), types->GetEnum(i))) { - return false; - } - } - return true; -} - -inline const OpenShock::Serialization::DeviceToServerMessage *GetDeviceToServerMessage(const void *buf) { - return ::flatbuffers::GetRoot(buf); -} - -inline const OpenShock::Serialization::DeviceToServerMessage *GetSizePrefixedDeviceToServerMessage(const void *buf) { - return ::flatbuffers::GetSizePrefixedRoot(buf); -} - -inline bool VerifyDeviceToServerMessageBuffer( - ::flatbuffers::Verifier &verifier) { - return verifier.VerifyBuffer(nullptr); -} - -inline bool VerifySizePrefixedDeviceToServerMessageBuffer( - ::flatbuffers::Verifier &verifier) { - return verifier.VerifySizePrefixedBuffer(nullptr); -} - -inline void FinishDeviceToServerMessageBuffer( - ::flatbuffers::FlatBufferBuilder &fbb, - ::flatbuffers::Offset root) { - fbb.Finish(root); -} - -inline void FinishSizePrefixedDeviceToServerMessageBuffer( - ::flatbuffers::FlatBufferBuilder &fbb, - ::flatbuffers::Offset root) { - fbb.FinishSizePrefixed(root); -} - -} // namespace Serialization -} // namespace OpenShock - -#endif // FLATBUFFERS_GENERATED_DEVICETOSERVERMESSAGE_OPENSHOCK_SERIALIZATION_H_ diff --git a/include/serialization/_fbs/FirmwareBootType_generated.h b/include/serialization/_fbs/FirmwareBootType_generated.h new file mode 100644 index 00000000..331c57a0 --- /dev/null +++ b/include/serialization/_fbs/FirmwareBootType_generated.h @@ -0,0 +1,57 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_FIRMWAREBOOTTYPE_OPENSHOCK_SERIALIZATION_TYPES_H_ +#define FLATBUFFERS_GENERATED_FIRMWAREBOOTTYPE_OPENSHOCK_SERIALIZATION_TYPES_H_ + +#include "flatbuffers/flatbuffers.h" + +// Ensure the included flatbuffers.h is the same version as when this file was +// generated, otherwise it may not be compatible. +static_assert(FLATBUFFERS_VERSION_MAJOR == 23 && + FLATBUFFERS_VERSION_MINOR == 5 && + FLATBUFFERS_VERSION_REVISION == 26, + "Non-compatible flatbuffers version included"); + +namespace OpenShock { +namespace Serialization { +namespace Types { + +enum class FirmwareBootType : uint8_t { + Normal = 0, + NewFirmware = 1, + Rollback = 2, + MIN = Normal, + MAX = Rollback +}; + +inline const FirmwareBootType (&EnumValuesFirmwareBootType())[3] { + static const FirmwareBootType values[] = { + FirmwareBootType::Normal, + FirmwareBootType::NewFirmware, + FirmwareBootType::Rollback + }; + return values; +} + +inline const char * const *EnumNamesFirmwareBootType() { + static const char * const names[4] = { + "Normal", + "NewFirmware", + "Rollback", + nullptr + }; + return names; +} + +inline const char *EnumNameFirmwareBootType(FirmwareBootType e) { + if (::flatbuffers::IsOutRange(e, FirmwareBootType::Normal, FirmwareBootType::Rollback)) return ""; + const size_t index = static_cast(e); + return EnumNamesFirmwareBootType()[index]; +} + +} // namespace Types +} // namespace Serialization +} // namespace OpenShock + +#endif // FLATBUFFERS_GENERATED_FIRMWAREBOOTTYPE_OPENSHOCK_SERIALIZATION_TYPES_H_ diff --git a/include/serialization/_fbs/GatewayToDeviceMessage_generated.h b/include/serialization/_fbs/GatewayToDeviceMessage_generated.h new file mode 100644 index 00000000..2f79f58a --- /dev/null +++ b/include/serialization/_fbs/GatewayToDeviceMessage_generated.h @@ -0,0 +1,432 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_GATEWAYTODEVICEMESSAGE_OPENSHOCK_SERIALIZATION_GATEWAY_H_ +#define FLATBUFFERS_GENERATED_GATEWAYTODEVICEMESSAGE_OPENSHOCK_SERIALIZATION_GATEWAY_H_ + +#include "flatbuffers/flatbuffers.h" + +// Ensure the included flatbuffers.h is the same version as when this file was +// generated, otherwise it may not be compatible. +static_assert(FLATBUFFERS_VERSION_MAJOR == 23 && + FLATBUFFERS_VERSION_MINOR == 5 && + FLATBUFFERS_VERSION_REVISION == 26, + "Non-compatible flatbuffers version included"); + +#include "SemVer_generated.h" +#include "ShockerCommandType_generated.h" +#include "ShockerModelType_generated.h" + +namespace OpenShock { +namespace Serialization { +namespace Gateway { + +struct ShockerCommand; + +struct ShockerCommandList; +struct ShockerCommandListBuilder; + +struct CaptivePortalConfig; + +struct OtaInstall; +struct OtaInstallBuilder; + +struct GatewayToDeviceMessage; +struct GatewayToDeviceMessageBuilder; + +enum class GatewayToDeviceMessagePayload : uint8_t { + NONE = 0, + ShockerCommandList = 1, + CaptivePortalConfig = 2, + OtaInstall = 3, + MIN = NONE, + MAX = OtaInstall +}; + +inline const GatewayToDeviceMessagePayload (&EnumValuesGatewayToDeviceMessagePayload())[4] { + static const GatewayToDeviceMessagePayload values[] = { + GatewayToDeviceMessagePayload::NONE, + GatewayToDeviceMessagePayload::ShockerCommandList, + GatewayToDeviceMessagePayload::CaptivePortalConfig, + GatewayToDeviceMessagePayload::OtaInstall + }; + return values; +} + +inline const char * const *EnumNamesGatewayToDeviceMessagePayload() { + static const char * const names[5] = { + "NONE", + "ShockerCommandList", + "CaptivePortalConfig", + "OtaInstall", + nullptr + }; + return names; +} + +inline const char *EnumNameGatewayToDeviceMessagePayload(GatewayToDeviceMessagePayload e) { + if (::flatbuffers::IsOutRange(e, GatewayToDeviceMessagePayload::NONE, GatewayToDeviceMessagePayload::OtaInstall)) return ""; + const size_t index = static_cast(e); + return EnumNamesGatewayToDeviceMessagePayload()[index]; +} + +template struct GatewayToDeviceMessagePayloadTraits { + static const GatewayToDeviceMessagePayload enum_value = GatewayToDeviceMessagePayload::NONE; +}; + +template<> struct GatewayToDeviceMessagePayloadTraits { + static const GatewayToDeviceMessagePayload enum_value = GatewayToDeviceMessagePayload::ShockerCommandList; +}; + +template<> struct GatewayToDeviceMessagePayloadTraits { + static const GatewayToDeviceMessagePayload enum_value = GatewayToDeviceMessagePayload::CaptivePortalConfig; +}; + +template<> struct GatewayToDeviceMessagePayloadTraits { + static const GatewayToDeviceMessagePayload enum_value = GatewayToDeviceMessagePayload::OtaInstall; +}; + +bool VerifyGatewayToDeviceMessagePayload(::flatbuffers::Verifier &verifier, const void *obj, GatewayToDeviceMessagePayload type); +bool VerifyGatewayToDeviceMessagePayloadVector(::flatbuffers::Verifier &verifier, const ::flatbuffers::Vector<::flatbuffers::Offset> *values, const ::flatbuffers::Vector *types); + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(2) ShockerCommand FLATBUFFERS_FINAL_CLASS { + private: + uint8_t model_; + int8_t padding0__; + uint16_t id_; + uint8_t type_; + uint8_t intensity_; + uint16_t duration_; + + public: + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Gateway.ShockerCommand"; + } + ShockerCommand() + : model_(0), + padding0__(0), + id_(0), + type_(0), + intensity_(0), + duration_(0) { + (void)padding0__; + } + ShockerCommand(OpenShock::Serialization::Types::ShockerModelType _model, uint16_t _id, OpenShock::Serialization::Types::ShockerCommandType _type, uint8_t _intensity, uint16_t _duration) + : model_(::flatbuffers::EndianScalar(static_cast(_model))), + padding0__(0), + id_(::flatbuffers::EndianScalar(_id)), + type_(::flatbuffers::EndianScalar(static_cast(_type))), + intensity_(::flatbuffers::EndianScalar(_intensity)), + duration_(::flatbuffers::EndianScalar(_duration)) { + (void)padding0__; + } + OpenShock::Serialization::Types::ShockerModelType model() const { + return static_cast(::flatbuffers::EndianScalar(model_)); + } + uint16_t id() const { + return ::flatbuffers::EndianScalar(id_); + } + OpenShock::Serialization::Types::ShockerCommandType type() const { + return static_cast(::flatbuffers::EndianScalar(type_)); + } + uint8_t intensity() const { + return ::flatbuffers::EndianScalar(intensity_); + } + uint16_t duration() const { + return ::flatbuffers::EndianScalar(duration_); + } +}; +FLATBUFFERS_STRUCT_END(ShockerCommand, 8); + +struct ShockerCommand::Traits { + using type = ShockerCommand; +}; + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(1) CaptivePortalConfig FLATBUFFERS_FINAL_CLASS { + private: + uint8_t enabled_; + + public: + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Gateway.CaptivePortalConfig"; + } + CaptivePortalConfig() + : enabled_(0) { + } + CaptivePortalConfig(bool _enabled) + : enabled_(::flatbuffers::EndianScalar(static_cast(_enabled))) { + } + bool enabled() const { + return ::flatbuffers::EndianScalar(enabled_) != 0; + } +}; +FLATBUFFERS_STRUCT_END(CaptivePortalConfig, 1); + +struct CaptivePortalConfig::Traits { + using type = CaptivePortalConfig; +}; + +struct ShockerCommandList FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef ShockerCommandListBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Gateway.ShockerCommandList"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_COMMANDS = 4 + }; + const ::flatbuffers::Vector *commands() const { + return GetPointer *>(VT_COMMANDS); + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffsetRequired(verifier, VT_COMMANDS) && + verifier.VerifyVector(commands()) && + verifier.EndTable(); + } +}; + +struct ShockerCommandListBuilder { + typedef ShockerCommandList Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_commands(::flatbuffers::Offset<::flatbuffers::Vector> commands) { + fbb_.AddOffset(ShockerCommandList::VT_COMMANDS, commands); + } + explicit ShockerCommandListBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + fbb_.Required(o, ShockerCommandList::VT_COMMANDS); + return o; + } +}; + +inline ::flatbuffers::Offset CreateShockerCommandList( + ::flatbuffers::FlatBufferBuilder &_fbb, + ::flatbuffers::Offset<::flatbuffers::Vector> commands = 0) { + ShockerCommandListBuilder builder_(_fbb); + builder_.add_commands(commands); + return builder_.Finish(); +} + +struct ShockerCommandList::Traits { + using type = ShockerCommandList; + static auto constexpr Create = CreateShockerCommandList; +}; + +inline ::flatbuffers::Offset CreateShockerCommandListDirect( + ::flatbuffers::FlatBufferBuilder &_fbb, + const std::vector *commands = nullptr) { + auto commands__ = commands ? _fbb.CreateVectorOfStructs(*commands) : 0; + return OpenShock::Serialization::Gateway::CreateShockerCommandList( + _fbb, + commands__); +} + +struct OtaInstall FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef OtaInstallBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Gateway.OtaInstall"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_VERSION = 4 + }; + const OpenShock::Serialization::Types::SemVer *version() const { + return GetPointer(VT_VERSION); + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_VERSION) && + verifier.VerifyTable(version()) && + verifier.EndTable(); + } +}; + +struct OtaInstallBuilder { + typedef OtaInstall Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_version(::flatbuffers::Offset version) { + fbb_.AddOffset(OtaInstall::VT_VERSION, version); + } + explicit OtaInstallBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateOtaInstall( + ::flatbuffers::FlatBufferBuilder &_fbb, + ::flatbuffers::Offset version = 0) { + OtaInstallBuilder builder_(_fbb); + builder_.add_version(version); + return builder_.Finish(); +} + +struct OtaInstall::Traits { + using type = OtaInstall; + static auto constexpr Create = CreateOtaInstall; +}; + +struct GatewayToDeviceMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef GatewayToDeviceMessageBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Gateway.GatewayToDeviceMessage"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_PAYLOAD_TYPE = 4, + VT_PAYLOAD = 6 + }; + OpenShock::Serialization::Gateway::GatewayToDeviceMessagePayload payload_type() const { + return static_cast(GetField(VT_PAYLOAD_TYPE, 0)); + } + const void *payload() const { + return GetPointer(VT_PAYLOAD); + } + template const T *payload_as() const; + const OpenShock::Serialization::Gateway::ShockerCommandList *payload_as_ShockerCommandList() const { + return payload_type() == OpenShock::Serialization::Gateway::GatewayToDeviceMessagePayload::ShockerCommandList ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Gateway::CaptivePortalConfig *payload_as_CaptivePortalConfig() const { + return payload_type() == OpenShock::Serialization::Gateway::GatewayToDeviceMessagePayload::CaptivePortalConfig ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Gateway::OtaInstall *payload_as_OtaInstall() const { + return payload_type() == OpenShock::Serialization::Gateway::GatewayToDeviceMessagePayload::OtaInstall ? static_cast(payload()) : nullptr; + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_PAYLOAD_TYPE, 1) && + VerifyOffset(verifier, VT_PAYLOAD) && + VerifyGatewayToDeviceMessagePayload(verifier, payload(), payload_type()) && + verifier.EndTable(); + } +}; + +template<> inline const OpenShock::Serialization::Gateway::ShockerCommandList *GatewayToDeviceMessage::payload_as() const { + return payload_as_ShockerCommandList(); +} + +template<> inline const OpenShock::Serialization::Gateway::CaptivePortalConfig *GatewayToDeviceMessage::payload_as() const { + return payload_as_CaptivePortalConfig(); +} + +template<> inline const OpenShock::Serialization::Gateway::OtaInstall *GatewayToDeviceMessage::payload_as() const { + return payload_as_OtaInstall(); +} + +struct GatewayToDeviceMessageBuilder { + typedef GatewayToDeviceMessage Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_payload_type(OpenShock::Serialization::Gateway::GatewayToDeviceMessagePayload payload_type) { + fbb_.AddElement(GatewayToDeviceMessage::VT_PAYLOAD_TYPE, static_cast(payload_type), 0); + } + void add_payload(::flatbuffers::Offset payload) { + fbb_.AddOffset(GatewayToDeviceMessage::VT_PAYLOAD, payload); + } + explicit GatewayToDeviceMessageBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateGatewayToDeviceMessage( + ::flatbuffers::FlatBufferBuilder &_fbb, + OpenShock::Serialization::Gateway::GatewayToDeviceMessagePayload payload_type = OpenShock::Serialization::Gateway::GatewayToDeviceMessagePayload::NONE, + ::flatbuffers::Offset payload = 0) { + GatewayToDeviceMessageBuilder builder_(_fbb); + builder_.add_payload(payload); + builder_.add_payload_type(payload_type); + return builder_.Finish(); +} + +struct GatewayToDeviceMessage::Traits { + using type = GatewayToDeviceMessage; + static auto constexpr Create = CreateGatewayToDeviceMessage; +}; + +inline bool VerifyGatewayToDeviceMessagePayload(::flatbuffers::Verifier &verifier, const void *obj, GatewayToDeviceMessagePayload type) { + switch (type) { + case GatewayToDeviceMessagePayload::NONE: { + return true; + } + case GatewayToDeviceMessagePayload::ShockerCommandList: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + case GatewayToDeviceMessagePayload::CaptivePortalConfig: { + return verifier.VerifyField(static_cast(obj), 0, 1); + } + case GatewayToDeviceMessagePayload::OtaInstall: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + default: return true; + } +} + +inline bool VerifyGatewayToDeviceMessagePayloadVector(::flatbuffers::Verifier &verifier, const ::flatbuffers::Vector<::flatbuffers::Offset> *values, const ::flatbuffers::Vector *types) { + if (!values || !types) return !values && !types; + if (values->size() != types->size()) return false; + for (::flatbuffers::uoffset_t i = 0; i < values->size(); ++i) { + if (!VerifyGatewayToDeviceMessagePayload( + verifier, values->Get(i), types->GetEnum(i))) { + return false; + } + } + return true; +} + +inline const OpenShock::Serialization::Gateway::GatewayToDeviceMessage *GetGatewayToDeviceMessage(const void *buf) { + return ::flatbuffers::GetRoot(buf); +} + +inline const OpenShock::Serialization::Gateway::GatewayToDeviceMessage *GetSizePrefixedGatewayToDeviceMessage(const void *buf) { + return ::flatbuffers::GetSizePrefixedRoot(buf); +} + +inline bool VerifyGatewayToDeviceMessageBuffer( + ::flatbuffers::Verifier &verifier) { + return verifier.VerifyBuffer(nullptr); +} + +inline bool VerifySizePrefixedGatewayToDeviceMessageBuffer( + ::flatbuffers::Verifier &verifier) { + return verifier.VerifySizePrefixedBuffer(nullptr); +} + +inline void FinishGatewayToDeviceMessageBuffer( + ::flatbuffers::FlatBufferBuilder &fbb, + ::flatbuffers::Offset root) { + fbb.Finish(root); +} + +inline void FinishSizePrefixedGatewayToDeviceMessageBuffer( + ::flatbuffers::FlatBufferBuilder &fbb, + ::flatbuffers::Offset root) { + fbb.FinishSizePrefixed(root); +} + +} // namespace Gateway +} // namespace Serialization +} // namespace OpenShock + +#endif // FLATBUFFERS_GENERATED_GATEWAYTODEVICEMESSAGE_OPENSHOCK_SERIALIZATION_GATEWAY_H_ diff --git a/include/serialization/_fbs/LocalToDeviceMessage_generated.h b/include/serialization/_fbs/LocalToDeviceMessage_generated.h index 6f4e0d55..24afa8d6 100644 --- a/include/serialization/_fbs/LocalToDeviceMessage_generated.h +++ b/include/serialization/_fbs/LocalToDeviceMessage_generated.h @@ -30,6 +30,28 @@ struct WifiNetworkConnectCommandBuilder; struct WifiNetworkDisconnectCommand; +struct OtaUpdateSetIsEnabledCommand; + +struct OtaUpdateSetDomainCommand; +struct OtaUpdateSetDomainCommandBuilder; + +struct OtaUpdateSetUpdateChannelCommand; +struct OtaUpdateSetUpdateChannelCommandBuilder; + +struct OtaUpdateSetCheckIntervalCommand; + +struct OtaUpdateSetAllowBackendManagementCommand; + +struct OtaUpdateSetRequireManualApprovalCommand; + +struct OtaUpdateHandleUpdateRequestCommand; + +struct OtaUpdateCheckForUpdatesCommand; +struct OtaUpdateCheckForUpdatesCommandBuilder; + +struct OtaUpdateStartUpdateCommand; +struct OtaUpdateStartUpdateCommandBuilder; + struct AccountLinkCommand; struct AccountLinkCommandBuilder; @@ -47,14 +69,23 @@ enum class LocalToDeviceMessagePayload : uint8_t { WifiNetworkForgetCommand = 3, WifiNetworkConnectCommand = 4, WifiNetworkDisconnectCommand = 5, - AccountLinkCommand = 6, - AccountUnlinkCommand = 7, - SetRfTxPinCommand = 8, + OtaUpdateSetIsEnabledCommand = 6, + OtaUpdateSetDomainCommand = 7, + OtaUpdateSetUpdateChannelCommand = 8, + OtaUpdateSetCheckIntervalCommand = 9, + OtaUpdateSetAllowBackendManagementCommand = 10, + OtaUpdateSetRequireManualApprovalCommand = 11, + OtaUpdateHandleUpdateRequestCommand = 12, + OtaUpdateCheckForUpdatesCommand = 13, + OtaUpdateStartUpdateCommand = 14, + AccountLinkCommand = 15, + AccountUnlinkCommand = 16, + SetRfTxPinCommand = 17, MIN = NONE, MAX = SetRfTxPinCommand }; -inline const LocalToDeviceMessagePayload (&EnumValuesLocalToDeviceMessagePayload())[9] { +inline const LocalToDeviceMessagePayload (&EnumValuesLocalToDeviceMessagePayload())[18] { static const LocalToDeviceMessagePayload values[] = { LocalToDeviceMessagePayload::NONE, LocalToDeviceMessagePayload::WifiScanCommand, @@ -62,6 +93,15 @@ inline const LocalToDeviceMessagePayload (&EnumValuesLocalToDeviceMessagePayload LocalToDeviceMessagePayload::WifiNetworkForgetCommand, LocalToDeviceMessagePayload::WifiNetworkConnectCommand, LocalToDeviceMessagePayload::WifiNetworkDisconnectCommand, + LocalToDeviceMessagePayload::OtaUpdateSetIsEnabledCommand, + LocalToDeviceMessagePayload::OtaUpdateSetDomainCommand, + LocalToDeviceMessagePayload::OtaUpdateSetUpdateChannelCommand, + LocalToDeviceMessagePayload::OtaUpdateSetCheckIntervalCommand, + LocalToDeviceMessagePayload::OtaUpdateSetAllowBackendManagementCommand, + LocalToDeviceMessagePayload::OtaUpdateSetRequireManualApprovalCommand, + LocalToDeviceMessagePayload::OtaUpdateHandleUpdateRequestCommand, + LocalToDeviceMessagePayload::OtaUpdateCheckForUpdatesCommand, + LocalToDeviceMessagePayload::OtaUpdateStartUpdateCommand, LocalToDeviceMessagePayload::AccountLinkCommand, LocalToDeviceMessagePayload::AccountUnlinkCommand, LocalToDeviceMessagePayload::SetRfTxPinCommand @@ -70,13 +110,22 @@ inline const LocalToDeviceMessagePayload (&EnumValuesLocalToDeviceMessagePayload } inline const char * const *EnumNamesLocalToDeviceMessagePayload() { - static const char * const names[10] = { + static const char * const names[19] = { "NONE", "WifiScanCommand", "WifiNetworkSaveCommand", "WifiNetworkForgetCommand", "WifiNetworkConnectCommand", "WifiNetworkDisconnectCommand", + "OtaUpdateSetIsEnabledCommand", + "OtaUpdateSetDomainCommand", + "OtaUpdateSetUpdateChannelCommand", + "OtaUpdateSetCheckIntervalCommand", + "OtaUpdateSetAllowBackendManagementCommand", + "OtaUpdateSetRequireManualApprovalCommand", + "OtaUpdateHandleUpdateRequestCommand", + "OtaUpdateCheckForUpdatesCommand", + "OtaUpdateStartUpdateCommand", "AccountLinkCommand", "AccountUnlinkCommand", "SetRfTxPinCommand", @@ -115,6 +164,42 @@ template<> struct LocalToDeviceMessagePayloadTraits struct LocalToDeviceMessagePayloadTraits { + static const LocalToDeviceMessagePayload enum_value = LocalToDeviceMessagePayload::OtaUpdateSetIsEnabledCommand; +}; + +template<> struct LocalToDeviceMessagePayloadTraits { + static const LocalToDeviceMessagePayload enum_value = LocalToDeviceMessagePayload::OtaUpdateSetDomainCommand; +}; + +template<> struct LocalToDeviceMessagePayloadTraits { + static const LocalToDeviceMessagePayload enum_value = LocalToDeviceMessagePayload::OtaUpdateSetUpdateChannelCommand; +}; + +template<> struct LocalToDeviceMessagePayloadTraits { + static const LocalToDeviceMessagePayload enum_value = LocalToDeviceMessagePayload::OtaUpdateSetCheckIntervalCommand; +}; + +template<> struct LocalToDeviceMessagePayloadTraits { + static const LocalToDeviceMessagePayload enum_value = LocalToDeviceMessagePayload::OtaUpdateSetAllowBackendManagementCommand; +}; + +template<> struct LocalToDeviceMessagePayloadTraits { + static const LocalToDeviceMessagePayload enum_value = LocalToDeviceMessagePayload::OtaUpdateSetRequireManualApprovalCommand; +}; + +template<> struct LocalToDeviceMessagePayloadTraits { + static const LocalToDeviceMessagePayload enum_value = LocalToDeviceMessagePayload::OtaUpdateHandleUpdateRequestCommand; +}; + +template<> struct LocalToDeviceMessagePayloadTraits { + static const LocalToDeviceMessagePayload enum_value = LocalToDeviceMessagePayload::OtaUpdateCheckForUpdatesCommand; +}; + +template<> struct LocalToDeviceMessagePayloadTraits { + static const LocalToDeviceMessagePayload enum_value = LocalToDeviceMessagePayload::OtaUpdateStartUpdateCommand; +}; + template<> struct LocalToDeviceMessagePayloadTraits { static const LocalToDeviceMessagePayload enum_value = LocalToDeviceMessagePayload::AccountLinkCommand; }; @@ -180,6 +265,131 @@ struct WifiNetworkDisconnectCommand::Traits { using type = WifiNetworkDisconnectCommand; }; +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(1) OtaUpdateSetIsEnabledCommand FLATBUFFERS_FINAL_CLASS { + private: + uint8_t enabled_; + + public: + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.OtaUpdateSetIsEnabledCommand"; + } + OtaUpdateSetIsEnabledCommand() + : enabled_(0) { + } + OtaUpdateSetIsEnabledCommand(bool _enabled) + : enabled_(::flatbuffers::EndianScalar(static_cast(_enabled))) { + } + bool enabled() const { + return ::flatbuffers::EndianScalar(enabled_) != 0; + } +}; +FLATBUFFERS_STRUCT_END(OtaUpdateSetIsEnabledCommand, 1); + +struct OtaUpdateSetIsEnabledCommand::Traits { + using type = OtaUpdateSetIsEnabledCommand; +}; + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(2) OtaUpdateSetCheckIntervalCommand FLATBUFFERS_FINAL_CLASS { + private: + uint16_t interval_; + + public: + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.OtaUpdateSetCheckIntervalCommand"; + } + OtaUpdateSetCheckIntervalCommand() + : interval_(0) { + } + OtaUpdateSetCheckIntervalCommand(uint16_t _interval) + : interval_(::flatbuffers::EndianScalar(_interval)) { + } + uint16_t interval() const { + return ::flatbuffers::EndianScalar(interval_); + } +}; +FLATBUFFERS_STRUCT_END(OtaUpdateSetCheckIntervalCommand, 2); + +struct OtaUpdateSetCheckIntervalCommand::Traits { + using type = OtaUpdateSetCheckIntervalCommand; +}; + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(1) OtaUpdateSetAllowBackendManagementCommand FLATBUFFERS_FINAL_CLASS { + private: + uint8_t allow_; + + public: + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.OtaUpdateSetAllowBackendManagementCommand"; + } + OtaUpdateSetAllowBackendManagementCommand() + : allow_(0) { + } + OtaUpdateSetAllowBackendManagementCommand(bool _allow) + : allow_(::flatbuffers::EndianScalar(static_cast(_allow))) { + } + bool allow() const { + return ::flatbuffers::EndianScalar(allow_) != 0; + } +}; +FLATBUFFERS_STRUCT_END(OtaUpdateSetAllowBackendManagementCommand, 1); + +struct OtaUpdateSetAllowBackendManagementCommand::Traits { + using type = OtaUpdateSetAllowBackendManagementCommand; +}; + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(1) OtaUpdateSetRequireManualApprovalCommand FLATBUFFERS_FINAL_CLASS { + private: + uint8_t require_; + + public: + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.OtaUpdateSetRequireManualApprovalCommand"; + } + OtaUpdateSetRequireManualApprovalCommand() + : require_(0) { + } + OtaUpdateSetRequireManualApprovalCommand(bool _require) + : require_(::flatbuffers::EndianScalar(static_cast(_require))) { + } + bool require() const { + return ::flatbuffers::EndianScalar(require_) != 0; + } +}; +FLATBUFFERS_STRUCT_END(OtaUpdateSetRequireManualApprovalCommand, 1); + +struct OtaUpdateSetRequireManualApprovalCommand::Traits { + using type = OtaUpdateSetRequireManualApprovalCommand; +}; + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(1) OtaUpdateHandleUpdateRequestCommand FLATBUFFERS_FINAL_CLASS { + private: + uint8_t accept_; + + public: + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.OtaUpdateHandleUpdateRequestCommand"; + } + OtaUpdateHandleUpdateRequestCommand() + : accept_(0) { + } + OtaUpdateHandleUpdateRequestCommand(bool _accept) + : accept_(::flatbuffers::EndianScalar(static_cast(_accept))) { + } + bool accept() const { + return ::flatbuffers::EndianScalar(accept_) != 0; + } +}; +FLATBUFFERS_STRUCT_END(OtaUpdateHandleUpdateRequestCommand, 1); + +struct OtaUpdateHandleUpdateRequestCommand::Traits { + using type = OtaUpdateHandleUpdateRequestCommand; +}; + FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(1) AccountUnlinkCommand FLATBUFFERS_FINAL_CLASS { private: uint8_t placeholder_; @@ -436,6 +646,260 @@ inline ::flatbuffers::Offset CreateWifiNetworkConnect ssid__); } +struct OtaUpdateSetDomainCommand FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef OtaUpdateSetDomainCommandBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.OtaUpdateSetDomainCommand"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_DOMAIN = 4 + }; + const ::flatbuffers::String *domain() const { + return GetPointer(VT_DOMAIN); + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_DOMAIN) && + verifier.VerifyString(domain()) && + verifier.EndTable(); + } +}; + +struct OtaUpdateSetDomainCommandBuilder { + typedef OtaUpdateSetDomainCommand Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_domain(::flatbuffers::Offset<::flatbuffers::String> domain) { + fbb_.AddOffset(OtaUpdateSetDomainCommand::VT_DOMAIN, domain); + } + explicit OtaUpdateSetDomainCommandBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateOtaUpdateSetDomainCommand( + ::flatbuffers::FlatBufferBuilder &_fbb, + ::flatbuffers::Offset<::flatbuffers::String> domain = 0) { + OtaUpdateSetDomainCommandBuilder builder_(_fbb); + builder_.add_domain(domain); + return builder_.Finish(); +} + +struct OtaUpdateSetDomainCommand::Traits { + using type = OtaUpdateSetDomainCommand; + static auto constexpr Create = CreateOtaUpdateSetDomainCommand; +}; + +inline ::flatbuffers::Offset CreateOtaUpdateSetDomainCommandDirect( + ::flatbuffers::FlatBufferBuilder &_fbb, + const char *domain = nullptr) { + auto domain__ = domain ? _fbb.CreateString(domain) : 0; + return OpenShock::Serialization::Local::CreateOtaUpdateSetDomainCommand( + _fbb, + domain__); +} + +struct OtaUpdateSetUpdateChannelCommand FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef OtaUpdateSetUpdateChannelCommandBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.OtaUpdateSetUpdateChannelCommand"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_CHANNEL = 4 + }; + const ::flatbuffers::String *channel() const { + return GetPointer(VT_CHANNEL); + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_CHANNEL) && + verifier.VerifyString(channel()) && + verifier.EndTable(); + } +}; + +struct OtaUpdateSetUpdateChannelCommandBuilder { + typedef OtaUpdateSetUpdateChannelCommand Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_channel(::flatbuffers::Offset<::flatbuffers::String> channel) { + fbb_.AddOffset(OtaUpdateSetUpdateChannelCommand::VT_CHANNEL, channel); + } + explicit OtaUpdateSetUpdateChannelCommandBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateOtaUpdateSetUpdateChannelCommand( + ::flatbuffers::FlatBufferBuilder &_fbb, + ::flatbuffers::Offset<::flatbuffers::String> channel = 0) { + OtaUpdateSetUpdateChannelCommandBuilder builder_(_fbb); + builder_.add_channel(channel); + return builder_.Finish(); +} + +struct OtaUpdateSetUpdateChannelCommand::Traits { + using type = OtaUpdateSetUpdateChannelCommand; + static auto constexpr Create = CreateOtaUpdateSetUpdateChannelCommand; +}; + +inline ::flatbuffers::Offset CreateOtaUpdateSetUpdateChannelCommandDirect( + ::flatbuffers::FlatBufferBuilder &_fbb, + const char *channel = nullptr) { + auto channel__ = channel ? _fbb.CreateString(channel) : 0; + return OpenShock::Serialization::Local::CreateOtaUpdateSetUpdateChannelCommand( + _fbb, + channel__); +} + +struct OtaUpdateCheckForUpdatesCommand FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef OtaUpdateCheckForUpdatesCommandBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.OtaUpdateCheckForUpdatesCommand"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_CHANNEL = 4 + }; + const ::flatbuffers::String *channel() const { + return GetPointer(VT_CHANNEL); + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_CHANNEL) && + verifier.VerifyString(channel()) && + verifier.EndTable(); + } +}; + +struct OtaUpdateCheckForUpdatesCommandBuilder { + typedef OtaUpdateCheckForUpdatesCommand Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_channel(::flatbuffers::Offset<::flatbuffers::String> channel) { + fbb_.AddOffset(OtaUpdateCheckForUpdatesCommand::VT_CHANNEL, channel); + } + explicit OtaUpdateCheckForUpdatesCommandBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateOtaUpdateCheckForUpdatesCommand( + ::flatbuffers::FlatBufferBuilder &_fbb, + ::flatbuffers::Offset<::flatbuffers::String> channel = 0) { + OtaUpdateCheckForUpdatesCommandBuilder builder_(_fbb); + builder_.add_channel(channel); + return builder_.Finish(); +} + +struct OtaUpdateCheckForUpdatesCommand::Traits { + using type = OtaUpdateCheckForUpdatesCommand; + static auto constexpr Create = CreateOtaUpdateCheckForUpdatesCommand; +}; + +inline ::flatbuffers::Offset CreateOtaUpdateCheckForUpdatesCommandDirect( + ::flatbuffers::FlatBufferBuilder &_fbb, + const char *channel = nullptr) { + auto channel__ = channel ? _fbb.CreateString(channel) : 0; + return OpenShock::Serialization::Local::CreateOtaUpdateCheckForUpdatesCommand( + _fbb, + channel__); +} + +struct OtaUpdateStartUpdateCommand FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef OtaUpdateStartUpdateCommandBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Local.OtaUpdateStartUpdateCommand"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_CHANNEL = 4, + VT_VERSION = 6 + }; + const ::flatbuffers::String *channel() const { + return GetPointer(VT_CHANNEL); + } + const ::flatbuffers::String *version() const { + return GetPointer(VT_VERSION); + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_CHANNEL) && + verifier.VerifyString(channel()) && + VerifyOffset(verifier, VT_VERSION) && + verifier.VerifyString(version()) && + verifier.EndTable(); + } +}; + +struct OtaUpdateStartUpdateCommandBuilder { + typedef OtaUpdateStartUpdateCommand Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_channel(::flatbuffers::Offset<::flatbuffers::String> channel) { + fbb_.AddOffset(OtaUpdateStartUpdateCommand::VT_CHANNEL, channel); + } + void add_version(::flatbuffers::Offset<::flatbuffers::String> version) { + fbb_.AddOffset(OtaUpdateStartUpdateCommand::VT_VERSION, version); + } + explicit OtaUpdateStartUpdateCommandBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateOtaUpdateStartUpdateCommand( + ::flatbuffers::FlatBufferBuilder &_fbb, + ::flatbuffers::Offset<::flatbuffers::String> channel = 0, + ::flatbuffers::Offset<::flatbuffers::String> version = 0) { + OtaUpdateStartUpdateCommandBuilder builder_(_fbb); + builder_.add_version(version); + builder_.add_channel(channel); + return builder_.Finish(); +} + +struct OtaUpdateStartUpdateCommand::Traits { + using type = OtaUpdateStartUpdateCommand; + static auto constexpr Create = CreateOtaUpdateStartUpdateCommand; +}; + +inline ::flatbuffers::Offset CreateOtaUpdateStartUpdateCommandDirect( + ::flatbuffers::FlatBufferBuilder &_fbb, + const char *channel = nullptr, + const char *version = nullptr) { + auto channel__ = channel ? _fbb.CreateString(channel) : 0; + auto version__ = version ? _fbb.CreateString(version) : 0; + return OpenShock::Serialization::Local::CreateOtaUpdateStartUpdateCommand( + _fbb, + channel__, + version__); +} + struct AccountLinkCommand FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { typedef AccountLinkCommandBuilder Builder; struct Traits; @@ -528,6 +992,33 @@ struct LocalToDeviceMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Tab const OpenShock::Serialization::Local::WifiNetworkDisconnectCommand *payload_as_WifiNetworkDisconnectCommand() const { return payload_type() == OpenShock::Serialization::Local::LocalToDeviceMessagePayload::WifiNetworkDisconnectCommand ? static_cast(payload()) : nullptr; } + const OpenShock::Serialization::Local::OtaUpdateSetIsEnabledCommand *payload_as_OtaUpdateSetIsEnabledCommand() const { + return payload_type() == OpenShock::Serialization::Local::LocalToDeviceMessagePayload::OtaUpdateSetIsEnabledCommand ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Local::OtaUpdateSetDomainCommand *payload_as_OtaUpdateSetDomainCommand() const { + return payload_type() == OpenShock::Serialization::Local::LocalToDeviceMessagePayload::OtaUpdateSetDomainCommand ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Local::OtaUpdateSetUpdateChannelCommand *payload_as_OtaUpdateSetUpdateChannelCommand() const { + return payload_type() == OpenShock::Serialization::Local::LocalToDeviceMessagePayload::OtaUpdateSetUpdateChannelCommand ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Local::OtaUpdateSetCheckIntervalCommand *payload_as_OtaUpdateSetCheckIntervalCommand() const { + return payload_type() == OpenShock::Serialization::Local::LocalToDeviceMessagePayload::OtaUpdateSetCheckIntervalCommand ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Local::OtaUpdateSetAllowBackendManagementCommand *payload_as_OtaUpdateSetAllowBackendManagementCommand() const { + return payload_type() == OpenShock::Serialization::Local::LocalToDeviceMessagePayload::OtaUpdateSetAllowBackendManagementCommand ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Local::OtaUpdateSetRequireManualApprovalCommand *payload_as_OtaUpdateSetRequireManualApprovalCommand() const { + return payload_type() == OpenShock::Serialization::Local::LocalToDeviceMessagePayload::OtaUpdateSetRequireManualApprovalCommand ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Local::OtaUpdateHandleUpdateRequestCommand *payload_as_OtaUpdateHandleUpdateRequestCommand() const { + return payload_type() == OpenShock::Serialization::Local::LocalToDeviceMessagePayload::OtaUpdateHandleUpdateRequestCommand ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Local::OtaUpdateCheckForUpdatesCommand *payload_as_OtaUpdateCheckForUpdatesCommand() const { + return payload_type() == OpenShock::Serialization::Local::LocalToDeviceMessagePayload::OtaUpdateCheckForUpdatesCommand ? static_cast(payload()) : nullptr; + } + const OpenShock::Serialization::Local::OtaUpdateStartUpdateCommand *payload_as_OtaUpdateStartUpdateCommand() const { + return payload_type() == OpenShock::Serialization::Local::LocalToDeviceMessagePayload::OtaUpdateStartUpdateCommand ? static_cast(payload()) : nullptr; + } const OpenShock::Serialization::Local::AccountLinkCommand *payload_as_AccountLinkCommand() const { return payload_type() == OpenShock::Serialization::Local::LocalToDeviceMessagePayload::AccountLinkCommand ? static_cast(payload()) : nullptr; } @@ -566,6 +1057,42 @@ template<> inline const OpenShock::Serialization::Local::WifiNetworkDisconnectCo return payload_as_WifiNetworkDisconnectCommand(); } +template<> inline const OpenShock::Serialization::Local::OtaUpdateSetIsEnabledCommand *LocalToDeviceMessage::payload_as() const { + return payload_as_OtaUpdateSetIsEnabledCommand(); +} + +template<> inline const OpenShock::Serialization::Local::OtaUpdateSetDomainCommand *LocalToDeviceMessage::payload_as() const { + return payload_as_OtaUpdateSetDomainCommand(); +} + +template<> inline const OpenShock::Serialization::Local::OtaUpdateSetUpdateChannelCommand *LocalToDeviceMessage::payload_as() const { + return payload_as_OtaUpdateSetUpdateChannelCommand(); +} + +template<> inline const OpenShock::Serialization::Local::OtaUpdateSetCheckIntervalCommand *LocalToDeviceMessage::payload_as() const { + return payload_as_OtaUpdateSetCheckIntervalCommand(); +} + +template<> inline const OpenShock::Serialization::Local::OtaUpdateSetAllowBackendManagementCommand *LocalToDeviceMessage::payload_as() const { + return payload_as_OtaUpdateSetAllowBackendManagementCommand(); +} + +template<> inline const OpenShock::Serialization::Local::OtaUpdateSetRequireManualApprovalCommand *LocalToDeviceMessage::payload_as() const { + return payload_as_OtaUpdateSetRequireManualApprovalCommand(); +} + +template<> inline const OpenShock::Serialization::Local::OtaUpdateHandleUpdateRequestCommand *LocalToDeviceMessage::payload_as() const { + return payload_as_OtaUpdateHandleUpdateRequestCommand(); +} + +template<> inline const OpenShock::Serialization::Local::OtaUpdateCheckForUpdatesCommand *LocalToDeviceMessage::payload_as() const { + return payload_as_OtaUpdateCheckForUpdatesCommand(); +} + +template<> inline const OpenShock::Serialization::Local::OtaUpdateStartUpdateCommand *LocalToDeviceMessage::payload_as() const { + return payload_as_OtaUpdateStartUpdateCommand(); +} + template<> inline const OpenShock::Serialization::Local::AccountLinkCommand *LocalToDeviceMessage::payload_as() const { return payload_as_AccountLinkCommand(); } @@ -637,6 +1164,37 @@ inline bool VerifyLocalToDeviceMessagePayload(::flatbuffers::Verifier &verifier, case LocalToDeviceMessagePayload::WifiNetworkDisconnectCommand: { return verifier.VerifyField(static_cast(obj), 0, 1); } + case LocalToDeviceMessagePayload::OtaUpdateSetIsEnabledCommand: { + return verifier.VerifyField(static_cast(obj), 0, 1); + } + case LocalToDeviceMessagePayload::OtaUpdateSetDomainCommand: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + case LocalToDeviceMessagePayload::OtaUpdateSetUpdateChannelCommand: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + case LocalToDeviceMessagePayload::OtaUpdateSetCheckIntervalCommand: { + return verifier.VerifyField(static_cast(obj), 0, 2); + } + case LocalToDeviceMessagePayload::OtaUpdateSetAllowBackendManagementCommand: { + return verifier.VerifyField(static_cast(obj), 0, 1); + } + case LocalToDeviceMessagePayload::OtaUpdateSetRequireManualApprovalCommand: { + return verifier.VerifyField(static_cast(obj), 0, 1); + } + case LocalToDeviceMessagePayload::OtaUpdateHandleUpdateRequestCommand: { + return verifier.VerifyField(static_cast(obj), 0, 1); + } + case LocalToDeviceMessagePayload::OtaUpdateCheckForUpdatesCommand: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + case LocalToDeviceMessagePayload::OtaUpdateStartUpdateCommand: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } case LocalToDeviceMessagePayload::AccountLinkCommand: { auto ptr = reinterpret_cast(obj); return verifier.VerifyTable(ptr); diff --git a/include/serialization/_fbs/SemVer_generated.h b/include/serialization/_fbs/SemVer_generated.h new file mode 100644 index 00000000..f26346fb --- /dev/null +++ b/include/serialization/_fbs/SemVer_generated.h @@ -0,0 +1,137 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_SEMVER_OPENSHOCK_SERIALIZATION_TYPES_H_ +#define FLATBUFFERS_GENERATED_SEMVER_OPENSHOCK_SERIALIZATION_TYPES_H_ + +#include "flatbuffers/flatbuffers.h" + +// Ensure the included flatbuffers.h is the same version as when this file was +// generated, otherwise it may not be compatible. +static_assert(FLATBUFFERS_VERSION_MAJOR == 23 && + FLATBUFFERS_VERSION_MINOR == 5 && + FLATBUFFERS_VERSION_REVISION == 26, + "Non-compatible flatbuffers version included"); + +namespace OpenShock { +namespace Serialization { +namespace Types { + +struct SemVer; +struct SemVerBuilder; + +struct SemVer FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { + typedef SemVerBuilder Builder; + struct Traits; + static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { + return "OpenShock.Serialization.Types.SemVer"; + } + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_MAJOR = 4, + VT_MINOR = 6, + VT_PATCH = 8, + VT_PRERELEASE = 10, + VT_BUILD = 12 + }; + uint16_t major() const { + return GetField(VT_MAJOR, 0); + } + uint16_t minor() const { + return GetField(VT_MINOR, 0); + } + uint16_t patch() const { + return GetField(VT_PATCH, 0); + } + const ::flatbuffers::String *prerelease() const { + return GetPointer(VT_PRERELEASE); + } + const ::flatbuffers::String *build() const { + return GetPointer(VT_BUILD); + } + bool Verify(::flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_MAJOR, 2) && + VerifyField(verifier, VT_MINOR, 2) && + VerifyField(verifier, VT_PATCH, 2) && + VerifyOffset(verifier, VT_PRERELEASE) && + verifier.VerifyString(prerelease()) && + VerifyOffset(verifier, VT_BUILD) && + verifier.VerifyString(build()) && + verifier.EndTable(); + } +}; + +struct SemVerBuilder { + typedef SemVer Table; + ::flatbuffers::FlatBufferBuilder &fbb_; + ::flatbuffers::uoffset_t start_; + void add_major(uint16_t major) { + fbb_.AddElement(SemVer::VT_MAJOR, major, 0); + } + void add_minor(uint16_t minor) { + fbb_.AddElement(SemVer::VT_MINOR, minor, 0); + } + void add_patch(uint16_t patch) { + fbb_.AddElement(SemVer::VT_PATCH, patch, 0); + } + void add_prerelease(::flatbuffers::Offset<::flatbuffers::String> prerelease) { + fbb_.AddOffset(SemVer::VT_PRERELEASE, prerelease); + } + void add_build(::flatbuffers::Offset<::flatbuffers::String> build) { + fbb_.AddOffset(SemVer::VT_BUILD, build); + } + explicit SemVerBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset CreateSemVer( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint16_t major = 0, + uint16_t minor = 0, + uint16_t patch = 0, + ::flatbuffers::Offset<::flatbuffers::String> prerelease = 0, + ::flatbuffers::Offset<::flatbuffers::String> build = 0) { + SemVerBuilder builder_(_fbb); + builder_.add_build(build); + builder_.add_prerelease(prerelease); + builder_.add_patch(patch); + builder_.add_minor(minor); + builder_.add_major(major); + return builder_.Finish(); +} + +struct SemVer::Traits { + using type = SemVer; + static auto constexpr Create = CreateSemVer; +}; + +inline ::flatbuffers::Offset CreateSemVerDirect( + ::flatbuffers::FlatBufferBuilder &_fbb, + uint16_t major = 0, + uint16_t minor = 0, + uint16_t patch = 0, + const char *prerelease = nullptr, + const char *build = nullptr) { + auto prerelease__ = prerelease ? _fbb.CreateString(prerelease) : 0; + auto build__ = build ? _fbb.CreateString(build) : 0; + return OpenShock::Serialization::Types::CreateSemVer( + _fbb, + major, + minor, + patch, + prerelease__, + build__); +} + +} // namespace Types +} // namespace Serialization +} // namespace OpenShock + +#endif // FLATBUFFERS_GENERATED_SEMVER_OPENSHOCK_SERIALIZATION_TYPES_H_ diff --git a/include/serialization/_fbs/ServerToDeviceMessage_generated.h b/include/serialization/_fbs/ServerToDeviceMessage_generated.h deleted file mode 100644 index 7c66c420..00000000 --- a/include/serialization/_fbs/ServerToDeviceMessage_generated.h +++ /dev/null @@ -1,357 +0,0 @@ -// automatically generated by the FlatBuffers compiler, do not modify - - -#ifndef FLATBUFFERS_GENERATED_SERVERTODEVICEMESSAGE_OPENSHOCK_SERIALIZATION_H_ -#define FLATBUFFERS_GENERATED_SERVERTODEVICEMESSAGE_OPENSHOCK_SERIALIZATION_H_ - -#include "flatbuffers/flatbuffers.h" - -// Ensure the included flatbuffers.h is the same version as when this file was -// generated, otherwise it may not be compatible. -static_assert(FLATBUFFERS_VERSION_MAJOR == 23 && - FLATBUFFERS_VERSION_MINOR == 5 && - FLATBUFFERS_VERSION_REVISION == 26, - "Non-compatible flatbuffers version included"); - -#include "ShockerCommandType_generated.h" -#include "ShockerModelType_generated.h" - -namespace OpenShock { -namespace Serialization { - -struct ShockerCommand; - -struct ShockerCommandList; -struct ShockerCommandListBuilder; - -struct CaptivePortalConfig; - -struct ServerToDeviceMessage; -struct ServerToDeviceMessageBuilder; - -enum class ServerToDeviceMessagePayload : uint8_t { - NONE = 0, - ShockerCommandList = 1, - CaptivePortalConfig = 2, - MIN = NONE, - MAX = CaptivePortalConfig -}; - -inline const ServerToDeviceMessagePayload (&EnumValuesServerToDeviceMessagePayload())[3] { - static const ServerToDeviceMessagePayload values[] = { - ServerToDeviceMessagePayload::NONE, - ServerToDeviceMessagePayload::ShockerCommandList, - ServerToDeviceMessagePayload::CaptivePortalConfig - }; - return values; -} - -inline const char * const *EnumNamesServerToDeviceMessagePayload() { - static const char * const names[4] = { - "NONE", - "ShockerCommandList", - "CaptivePortalConfig", - nullptr - }; - return names; -} - -inline const char *EnumNameServerToDeviceMessagePayload(ServerToDeviceMessagePayload e) { - if (::flatbuffers::IsOutRange(e, ServerToDeviceMessagePayload::NONE, ServerToDeviceMessagePayload::CaptivePortalConfig)) return ""; - const size_t index = static_cast(e); - return EnumNamesServerToDeviceMessagePayload()[index]; -} - -template struct ServerToDeviceMessagePayloadTraits { - static const ServerToDeviceMessagePayload enum_value = ServerToDeviceMessagePayload::NONE; -}; - -template<> struct ServerToDeviceMessagePayloadTraits { - static const ServerToDeviceMessagePayload enum_value = ServerToDeviceMessagePayload::ShockerCommandList; -}; - -template<> struct ServerToDeviceMessagePayloadTraits { - static const ServerToDeviceMessagePayload enum_value = ServerToDeviceMessagePayload::CaptivePortalConfig; -}; - -bool VerifyServerToDeviceMessagePayload(::flatbuffers::Verifier &verifier, const void *obj, ServerToDeviceMessagePayload type); -bool VerifyServerToDeviceMessagePayloadVector(::flatbuffers::Verifier &verifier, const ::flatbuffers::Vector<::flatbuffers::Offset> *values, const ::flatbuffers::Vector *types); - -FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(2) ShockerCommand FLATBUFFERS_FINAL_CLASS { - private: - uint8_t model_; - int8_t padding0__; - uint16_t id_; - uint8_t type_; - uint8_t intensity_; - uint16_t duration_; - - public: - struct Traits; - static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { - return "OpenShock.Serialization.ShockerCommand"; - } - ShockerCommand() - : model_(0), - padding0__(0), - id_(0), - type_(0), - intensity_(0), - duration_(0) { - (void)padding0__; - } - ShockerCommand(OpenShock::Serialization::Types::ShockerModelType _model, uint16_t _id, OpenShock::Serialization::Types::ShockerCommandType _type, uint8_t _intensity, uint16_t _duration) - : model_(::flatbuffers::EndianScalar(static_cast(_model))), - padding0__(0), - id_(::flatbuffers::EndianScalar(_id)), - type_(::flatbuffers::EndianScalar(static_cast(_type))), - intensity_(::flatbuffers::EndianScalar(_intensity)), - duration_(::flatbuffers::EndianScalar(_duration)) { - (void)padding0__; - } - OpenShock::Serialization::Types::ShockerModelType model() const { - return static_cast(::flatbuffers::EndianScalar(model_)); - } - uint16_t id() const { - return ::flatbuffers::EndianScalar(id_); - } - OpenShock::Serialization::Types::ShockerCommandType type() const { - return static_cast(::flatbuffers::EndianScalar(type_)); - } - uint8_t intensity() const { - return ::flatbuffers::EndianScalar(intensity_); - } - uint16_t duration() const { - return ::flatbuffers::EndianScalar(duration_); - } -}; -FLATBUFFERS_STRUCT_END(ShockerCommand, 8); - -struct ShockerCommand::Traits { - using type = ShockerCommand; -}; - -FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(1) CaptivePortalConfig FLATBUFFERS_FINAL_CLASS { - private: - uint8_t enabled_; - - public: - struct Traits; - static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { - return "OpenShock.Serialization.CaptivePortalConfig"; - } - CaptivePortalConfig() - : enabled_(0) { - } - CaptivePortalConfig(bool _enabled) - : enabled_(::flatbuffers::EndianScalar(static_cast(_enabled))) { - } - bool enabled() const { - return ::flatbuffers::EndianScalar(enabled_) != 0; - } -}; -FLATBUFFERS_STRUCT_END(CaptivePortalConfig, 1); - -struct CaptivePortalConfig::Traits { - using type = CaptivePortalConfig; -}; - -struct ShockerCommandList FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { - typedef ShockerCommandListBuilder Builder; - struct Traits; - static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { - return "OpenShock.Serialization.ShockerCommandList"; - } - enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { - VT_COMMANDS = 4 - }; - const ::flatbuffers::Vector *commands() const { - return GetPointer *>(VT_COMMANDS); - } - bool Verify(::flatbuffers::Verifier &verifier) const { - return VerifyTableStart(verifier) && - VerifyOffsetRequired(verifier, VT_COMMANDS) && - verifier.VerifyVector(commands()) && - verifier.EndTable(); - } -}; - -struct ShockerCommandListBuilder { - typedef ShockerCommandList Table; - ::flatbuffers::FlatBufferBuilder &fbb_; - ::flatbuffers::uoffset_t start_; - void add_commands(::flatbuffers::Offset<::flatbuffers::Vector> commands) { - fbb_.AddOffset(ShockerCommandList::VT_COMMANDS, commands); - } - explicit ShockerCommandListBuilder(::flatbuffers::FlatBufferBuilder &_fbb) - : fbb_(_fbb) { - start_ = fbb_.StartTable(); - } - ::flatbuffers::Offset Finish() { - const auto end = fbb_.EndTable(start_); - auto o = ::flatbuffers::Offset(end); - fbb_.Required(o, ShockerCommandList::VT_COMMANDS); - return o; - } -}; - -inline ::flatbuffers::Offset CreateShockerCommandList( - ::flatbuffers::FlatBufferBuilder &_fbb, - ::flatbuffers::Offset<::flatbuffers::Vector> commands = 0) { - ShockerCommandListBuilder builder_(_fbb); - builder_.add_commands(commands); - return builder_.Finish(); -} - -struct ShockerCommandList::Traits { - using type = ShockerCommandList; - static auto constexpr Create = CreateShockerCommandList; -}; - -inline ::flatbuffers::Offset CreateShockerCommandListDirect( - ::flatbuffers::FlatBufferBuilder &_fbb, - const std::vector *commands = nullptr) { - auto commands__ = commands ? _fbb.CreateVectorOfStructs(*commands) : 0; - return OpenShock::Serialization::CreateShockerCommandList( - _fbb, - commands__); -} - -struct ServerToDeviceMessage FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { - typedef ServerToDeviceMessageBuilder Builder; - struct Traits; - static FLATBUFFERS_CONSTEXPR_CPP11 const char *GetFullyQualifiedName() { - return "OpenShock.Serialization.ServerToDeviceMessage"; - } - enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { - VT_PAYLOAD_TYPE = 4, - VT_PAYLOAD = 6 - }; - OpenShock::Serialization::ServerToDeviceMessagePayload payload_type() const { - return static_cast(GetField(VT_PAYLOAD_TYPE, 0)); - } - const void *payload() const { - return GetPointer(VT_PAYLOAD); - } - template const T *payload_as() const; - const OpenShock::Serialization::ShockerCommandList *payload_as_ShockerCommandList() const { - return payload_type() == OpenShock::Serialization::ServerToDeviceMessagePayload::ShockerCommandList ? static_cast(payload()) : nullptr; - } - const OpenShock::Serialization::CaptivePortalConfig *payload_as_CaptivePortalConfig() const { - return payload_type() == OpenShock::Serialization::ServerToDeviceMessagePayload::CaptivePortalConfig ? static_cast(payload()) : nullptr; - } - bool Verify(::flatbuffers::Verifier &verifier) const { - return VerifyTableStart(verifier) && - VerifyField(verifier, VT_PAYLOAD_TYPE, 1) && - VerifyOffset(verifier, VT_PAYLOAD) && - VerifyServerToDeviceMessagePayload(verifier, payload(), payload_type()) && - verifier.EndTable(); - } -}; - -template<> inline const OpenShock::Serialization::ShockerCommandList *ServerToDeviceMessage::payload_as() const { - return payload_as_ShockerCommandList(); -} - -template<> inline const OpenShock::Serialization::CaptivePortalConfig *ServerToDeviceMessage::payload_as() const { - return payload_as_CaptivePortalConfig(); -} - -struct ServerToDeviceMessageBuilder { - typedef ServerToDeviceMessage Table; - ::flatbuffers::FlatBufferBuilder &fbb_; - ::flatbuffers::uoffset_t start_; - void add_payload_type(OpenShock::Serialization::ServerToDeviceMessagePayload payload_type) { - fbb_.AddElement(ServerToDeviceMessage::VT_PAYLOAD_TYPE, static_cast(payload_type), 0); - } - void add_payload(::flatbuffers::Offset payload) { - fbb_.AddOffset(ServerToDeviceMessage::VT_PAYLOAD, payload); - } - explicit ServerToDeviceMessageBuilder(::flatbuffers::FlatBufferBuilder &_fbb) - : fbb_(_fbb) { - start_ = fbb_.StartTable(); - } - ::flatbuffers::Offset Finish() { - const auto end = fbb_.EndTable(start_); - auto o = ::flatbuffers::Offset(end); - return o; - } -}; - -inline ::flatbuffers::Offset CreateServerToDeviceMessage( - ::flatbuffers::FlatBufferBuilder &_fbb, - OpenShock::Serialization::ServerToDeviceMessagePayload payload_type = OpenShock::Serialization::ServerToDeviceMessagePayload::NONE, - ::flatbuffers::Offset payload = 0) { - ServerToDeviceMessageBuilder builder_(_fbb); - builder_.add_payload(payload); - builder_.add_payload_type(payload_type); - return builder_.Finish(); -} - -struct ServerToDeviceMessage::Traits { - using type = ServerToDeviceMessage; - static auto constexpr Create = CreateServerToDeviceMessage; -}; - -inline bool VerifyServerToDeviceMessagePayload(::flatbuffers::Verifier &verifier, const void *obj, ServerToDeviceMessagePayload type) { - switch (type) { - case ServerToDeviceMessagePayload::NONE: { - return true; - } - case ServerToDeviceMessagePayload::ShockerCommandList: { - auto ptr = reinterpret_cast(obj); - return verifier.VerifyTable(ptr); - } - case ServerToDeviceMessagePayload::CaptivePortalConfig: { - return verifier.VerifyField(static_cast(obj), 0, 1); - } - default: return true; - } -} - -inline bool VerifyServerToDeviceMessagePayloadVector(::flatbuffers::Verifier &verifier, const ::flatbuffers::Vector<::flatbuffers::Offset> *values, const ::flatbuffers::Vector *types) { - if (!values || !types) return !values && !types; - if (values->size() != types->size()) return false; - for (::flatbuffers::uoffset_t i = 0; i < values->size(); ++i) { - if (!VerifyServerToDeviceMessagePayload( - verifier, values->Get(i), types->GetEnum(i))) { - return false; - } - } - return true; -} - -inline const OpenShock::Serialization::ServerToDeviceMessage *GetServerToDeviceMessage(const void *buf) { - return ::flatbuffers::GetRoot(buf); -} - -inline const OpenShock::Serialization::ServerToDeviceMessage *GetSizePrefixedServerToDeviceMessage(const void *buf) { - return ::flatbuffers::GetSizePrefixedRoot(buf); -} - -inline bool VerifyServerToDeviceMessageBuffer( - ::flatbuffers::Verifier &verifier) { - return verifier.VerifyBuffer(nullptr); -} - -inline bool VerifySizePrefixedServerToDeviceMessageBuffer( - ::flatbuffers::Verifier &verifier) { - return verifier.VerifySizePrefixedBuffer(nullptr); -} - -inline void FinishServerToDeviceMessageBuffer( - ::flatbuffers::FlatBufferBuilder &fbb, - ::flatbuffers::Offset root) { - fbb.Finish(root); -} - -inline void FinishSizePrefixedServerToDeviceMessageBuffer( - ::flatbuffers::FlatBufferBuilder &fbb, - ::flatbuffers::Offset root) { - fbb.FinishSizePrefixed(root); -} - -} // namespace Serialization -} // namespace OpenShock - -#endif // FLATBUFFERS_GENERATED_SERVERTODEVICEMESSAGE_OPENSHOCK_SERIALIZATION_H_ diff --git a/include/serialization/_fbs/ShockerModelType_generated.h b/include/serialization/_fbs/ShockerModelType_generated.h index a8a826c9..f55fc512 100644 --- a/include/serialization/_fbs/ShockerModelType_generated.h +++ b/include/serialization/_fbs/ShockerModelType_generated.h @@ -19,15 +19,15 @@ namespace Types { enum class ShockerModelType : uint8_t { CaiXianlin = 0, - PetTrainer = 1, + Petrainer = 1, MIN = CaiXianlin, - MAX = PetTrainer + MAX = Petrainer }; inline const ShockerModelType (&EnumValuesShockerModelType())[2] { static const ShockerModelType values[] = { ShockerModelType::CaiXianlin, - ShockerModelType::PetTrainer + ShockerModelType::Petrainer }; return values; } @@ -35,14 +35,14 @@ inline const ShockerModelType (&EnumValuesShockerModelType())[2] { inline const char * const *EnumNamesShockerModelType() { static const char * const names[3] = { "CaiXianlin", - "PetTrainer", + "Petrainer", nullptr }; return names; } inline const char *EnumNameShockerModelType(ShockerModelType e) { - if (::flatbuffers::IsOutRange(e, ShockerModelType::CaiXianlin, ShockerModelType::PetTrainer)) return ""; + if (::flatbuffers::IsOutRange(e, ShockerModelType::CaiXianlin, ShockerModelType::Petrainer)) return ""; const size_t index = static_cast(e); return EnumNamesShockerModelType()[index]; } diff --git a/include/serialization/_fbs/WifiScanStatus_generated.h b/include/serialization/_fbs/WifiScanStatus_generated.h index 2976861e..473682f0 100644 --- a/include/serialization/_fbs/WifiScanStatus_generated.h +++ b/include/serialization/_fbs/WifiScanStatus_generated.h @@ -21,17 +21,19 @@ enum class WifiScanStatus : uint8_t { Started = 0, InProgress = 1, Completed = 2, - Aborted = 3, - Error = 4, + TimedOut = 3, + Aborted = 4, + Error = 5, MIN = Started, MAX = Error }; -inline const WifiScanStatus (&EnumValuesWifiScanStatus())[5] { +inline const WifiScanStatus (&EnumValuesWifiScanStatus())[6] { static const WifiScanStatus values[] = { WifiScanStatus::Started, WifiScanStatus::InProgress, WifiScanStatus::Completed, + WifiScanStatus::TimedOut, WifiScanStatus::Aborted, WifiScanStatus::Error }; @@ -39,10 +41,11 @@ inline const WifiScanStatus (&EnumValuesWifiScanStatus())[5] { } inline const char * const *EnumNamesWifiScanStatus() { - static const char * const names[6] = { + static const char * const names[7] = { "Started", "InProgress", "Completed", + "TimedOut", "Aborted", "Error", nullptr diff --git a/include/util/Base64Utils.h b/include/util/Base64Utils.h new file mode 100644 index 00000000..cc0706c0 --- /dev/null +++ b/include/util/Base64Utils.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include + +namespace OpenShock::Base64Utils { + /// @brief Calculates the size of the buffer required to hold the base64 encoded data. + /// @param size The size of the data to encode. + /// @return The size of the buffer required to hold the base64 encoded data. + constexpr std::size_t CalculateEncodedSize(std::size_t size) noexcept { + return (4 * (size / 3)) + 4; // TODO: This is wrong, but what mbedtls requires??? + } + + /// @brief Calculates the size of the buffer required to hold the decoded data. + /// @param size The size of the data to decode. + /// @return The size of the buffer required to hold the decoded data. + constexpr std::size_t CalculateDecodedSize(std::size_t size) noexcept { + return 3 * size / 4; + } + + /// @brief Encodes a byte array to base64. + /// @param data The data to encode. + /// @param dataLen The size of the data to encode. + /// @param output + /// @param outputLen + /// @return The amount of bytes written to the output buffer. + std::size_t Encode(const std::uint8_t* data, std::size_t dataLen, char* output, std::size_t outputLen) noexcept; + + /// @brief Encodes a byte array to base64. + /// @param data The data to encode. + /// @param dataLen The size of the data to encode. + /// @param output The output string to write to. + /// @return The amount of bytes written to the output buffer. + bool Encode(const std::uint8_t* data, std::size_t dataLen, std::string& output); + + /// @brief Decodes a base64 string. + /// @param data The data to decode. + /// @param dataLen The size of the data to decode. + /// @param output The output buffer to write to. + /// @param outputLen The size of the output buffer. + /// @return The amount of bytes written to the output buffer. + std::size_t Decode(const char* data, std::size_t dataLen, std::uint8_t* output, std::size_t outputLen) noexcept; + + /// @brief Decodes a base64 string. + /// @param data The data to decode. + /// @param dataLen The size of the data to decode. + /// @param output The output buffer to write to. + /// @return The amount of bytes written to the output buffer. + bool Decode(const char* data, std::size_t dataLen, std::vector& output) noexcept; +} // namespace OpenShock::Base64Utils diff --git a/include/CertificateUtils.h b/include/util/CertificateUtils.h similarity index 100% rename from include/CertificateUtils.h rename to include/util/CertificateUtils.h diff --git a/include/Utils/HexUtils.h b/include/util/HexUtils.h similarity index 56% rename from include/Utils/HexUtils.h rename to include/util/HexUtils.h index 8760dedf..d723fa91 100644 --- a/include/Utils/HexUtils.h +++ b/include/util/HexUtils.h @@ -1,9 +1,8 @@ #pragma once -#include - #include #include +#include namespace OpenShock::HexUtils { /// @brief Converts a single byte to a hex pair, and writes it to the output buffer. @@ -22,10 +21,24 @@ namespace OpenShock::HexUtils { /// @param upper Whether to use uppercase hex characters. /// @remark To use this you must specify the size of the array in the template parameter. (e.g. ToHexMac<6>(...)) template - constexpr void ToHex(nonstd::span data, nonstd::span output, bool upper = true) noexcept { - for (std::size_t i = 0; i < data.size(); ++i) { + constexpr void ToHex(const std::uint8_t (&data)[N], char (&output)[(N * 2) + 1], bool upper = true) noexcept { + for (std::size_t i = 0; i < N; ++i) { ToHex(data[i], &output[i * 2], upper); } + output[N * 2] = '\0'; + } + + /// @brief Converts a byte array to a hex string. + /// @param data The byte array to convert. + /// @param output The output buffer to write to. + /// @param upper Whether to use uppercase hex characters. + /// @remark To use this you must specify the size of the array in the template parameter. (e.g. ToHexMac<6>(...)) + template + constexpr void ToHex(const std::uint8_t (&data)[N], std::array& output, bool upper = true) noexcept { + for (std::size_t i = 0; i < N; ++i) { + ToHex(data[i], &output[i * 2], upper); + } + output[N * 2] = '\0'; } /// @brief Converts a byte array to a hex string. @@ -34,10 +47,9 @@ namespace OpenShock::HexUtils { /// @return The hex string. /// @remark To use this you must specify the size of the array in the template parameter. (e.g. ToHexMac<6>(...)) template - constexpr std::array ToHex(nonstd::span data, bool upper = true) noexcept { + constexpr std::array ToHex(const std::uint8_t (&data)[N], bool upper = true) noexcept { std::array output {}; - ToHex(data, output, upper); - output[N * 2] = '\0'; + ToHex(data, output, upper); return output; } @@ -47,13 +59,14 @@ namespace OpenShock::HexUtils { /// @param upper Whether to use uppercase hex characters. /// @remark To use this you must specify the size of the array in the template parameter. (e.g. ToHexMac<6>(...)) template - constexpr void ToHexMac(nonstd::span data, nonstd::span output, bool upper = true) noexcept { + constexpr void ToHexMac(const std::uint8_t (&data)[N], std::array& output, bool upper = true) noexcept { const std::size_t Last = N - 1; for (std::size_t i = 0; i < Last; ++i) { ToHex(data[i], &output[i * 3], upper); output[i * 3 + 2] = ':'; } ToHex(data[Last], &output[Last * 3], upper); + output[(N * 3) - 1] = '\0'; } /// @brief Converts a byte array to a MAC address string. (hex pairs separated by colons) @@ -62,10 +75,9 @@ namespace OpenShock::HexUtils { /// @return The hex string in a MAC address format. /// @remark To use this you must specify the size of the array in the template parameter. (e.g. ToHexMac<6>(...)) template - constexpr std::array ToHexMac(nonstd::span data, bool upper = true) noexcept { + constexpr std::array ToHexMac(const std::uint8_t (&data)[N], bool upper = true) noexcept { std::array output {}; - ToHexMac(data, nonstd::span(output.data(), output.size() - 1), upper); - output[(N * 3) - 1] = '\0'; + ToHexMac(data, output, upper); return output; } @@ -98,19 +110,57 @@ namespace OpenShock::HexUtils { return true; } - /// @brief Converts a hex string to a byte array. - /// @param data The hex string to convert. - /// @param output The output buffer to write to. - /// @return Whether the conversion was successful. - /// @remark To use this you must specify the size of the array in the template parameter. - template - constexpr bool TryParseHexMac(nonstd::span data, nonstd::span output) noexcept { - static_assert((N + 1) % 3 == 0, "Invalid MAC-Style hex string length."); - for (std::size_t i = 0; i < output.size(); ++i) { - if (!TryParseHexPair(data[i * 3], data[i * 3 + 1], output[i])) { - return false; + constexpr std::size_t TryParseHexMac(const char* str, std::size_t strLen, std::uint8_t* out, std::size_t outLen) noexcept { + std::size_t parsedLength = (strLen + 1) / 3; + + if ((parsedLength * 3) - 1 != strLen) { + return 0; // Invalid MAC-Style hex string length. + } + + if (parsedLength > outLen) { + return 0; // Output buffer is too small. + } + + for (std::size_t i = 0; i < parsedLength - 1; ++i) { + if (!TryParseHexPair(str[i * 3], str[i * 3 + 1], out[i])) { + return 0; // Invalid hex pair. + } + if (str[i * 3 + 2] != ':') { + return 0; // Invalid separator. } } - return true; + + if (!TryParseHexPair(str[(parsedLength - 1) * 3], str[(parsedLength - 1) * 3 + 1], out[parsedLength - 1])) { + return 0; // Invalid hex pair. + } + + return parsedLength; + } + + inline std::size_t TryParseHexMac(const char* str, std::uint8_t* out, std::size_t outLen) noexcept { + return TryParseHexMac(str, strlen(str), out, outLen); + } + + constexpr std::size_t TryParseHex(const char* str, std::size_t strLen, std::uint8_t* out, std::size_t outLen) noexcept { + std::size_t parsedLength = strLen / 2; + + if (parsedLength * 2 != strLen) { + return 0; // Invalid hex string length. + } + + if (parsedLength > outLen) { + return 0; // Output buffer is too small. + } + + for (std::size_t i = 0; i < parsedLength; ++i) { + if (!TryParseHexPair(str[i * 2], str[i * 2 + 1], out[i])) { + return 0; // Invalid hex pair. + } + } + + return parsedLength; + } + inline std::size_t TryParseHex(const char* str, std::uint8_t* out, std::size_t outLen) noexcept { + return TryParseHex(str, strlen(str), out, outLen); } } // namespace OpenShock::HexUtils diff --git a/include/util/PartitionUtils.h b/include/util/PartitionUtils.h new file mode 100644 index 00000000..0ff32a5a --- /dev/null +++ b/include/util/PartitionUtils.h @@ -0,0 +1,13 @@ +#pragma once + +#include "StringView.h" + +#include + +#include +#include + +namespace OpenShock { + bool TryGetPartitionHash(const esp_partition_t* partition, char (&hash)[65]); + bool FlashPartitionFromUrl(const esp_partition_t* partition, StringView remoteUrl, const std::uint8_t (&remoteHash)[32], std::function progressCallback = nullptr); +} diff --git a/include/util/StringUtils.h b/include/util/StringUtils.h new file mode 100644 index 00000000..1b2cc8c0 --- /dev/null +++ b/include/util/StringUtils.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +namespace OpenShock { + bool FormatToString(std::string& out, const char* format, ...); +} // namespace OpenShock diff --git a/include/util/TaskUtils.h b/include/util/TaskUtils.h new file mode 100644 index 00000000..ca00675d --- /dev/null +++ b/include/util/TaskUtils.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +namespace OpenShock::TaskUtils { + /// @brief Create a task on the specified core, or the default core if the specified core is invalid + inline esp_err_t TaskCreateUniversal(TaskFunction_t pvTaskCode, const char* const pcName, const uint32_t usStackDepth, void * const pvParameters, UBaseType_t uxPriority, TaskHandle_t * const pvCreatedTask, const BaseType_t xCoreID) { +#ifndef CONFIG_FREERTOS_UNICORE + if (xCoreID >= 0 && xCoreID < portNUM_PROCESSORS) { + return xTaskCreatePinnedToCore(pvTaskCode, pcName, usStackDepth, pvParameters, uxPriority, pvCreatedTask, xCoreID); + } +#endif + return xTaskCreate(pvTaskCode, pcName, usStackDepth, pvParameters, uxPriority, pvCreatedTask); + } + + /// @brief Create a task on the core that does expensive work, this should not run on the core that handles WiFi + inline esp_err_t TaskCreateExpensive(TaskFunction_t pvTaskCode, const char* const pcName, const uint32_t usStackDepth, void * const pvParameters, UBaseType_t uxPriority, TaskHandle_t * const pvCreatedTask) { +#if portNUM_PROCESSORS > 2 +#warning "This chip has more than 2 cores. Please update this code to use the correct core." +#endif + // Run on core 1 (0 handles WiFi and should be minimally used) + return TaskCreateUniversal(pvTaskCode, pcName, usStackDepth, pvParameters, uxPriority, pvCreatedTask, 1); + } +} // namespace OpenShock diff --git a/include/wifi/WiFiManager.h b/include/wifi/WiFiManager.h index 46c55b5f..efab76d4 100644 --- a/include/wifi/WiFiManager.h +++ b/include/wifi/WiFiManager.h @@ -4,6 +4,7 @@ #include #include +#include namespace OpenShock::WiFiManager { /// @brief Initializes the WiFiManager @@ -16,22 +17,11 @@ namespace OpenShock::WiFiManager { /// @return True if the network was saved successfully bool Save(const char* ssid, const std::string& password); - /// @brief Saves a network to the config by it's BSSID (supports hidden networks) - /// @param bssid BSSID of the network - /// @param password Password of the network - /// @return True if the network was saved successfully - bool Save(const std::uint8_t (&bssid)[6], const std::string& password); - /// @brief Removes a network from the config by it's SSID /// @param ssid SSID of the network /// @return True if the network was removed successfully bool Forget(const char* ssid); - /// @brief Removes a network from the config by it's BSSID - /// @param bssid BSSID of the network - /// @return True if the network was removed successfully - bool Forget(const std::uint8_t (&bssid)[6]); - /// @brief Refreshes all the networks with updated credential IDs from the config /// @return True if the networks were refreshed successfully bool RefreshNetworkCredentials(); @@ -41,17 +31,6 @@ namespace OpenShock::WiFiManager { /// @return True if a saved network matches the given SSID bool IsSaved(const char* ssid); - /// @brief Checks if a network is saved in the config by it's BSSID - /// @param bssid BSSID of the network - /// @return True if a saved network matches the given BSSID - bool IsSaved(const std::uint8_t (&bssid)[6]); - - /// @brief Checks if a network is saved in the config by it's SSID and BSSID - /// @param ssid SSID of the network - /// @param bssid BSSID of the network - /// @return True if a saved network matches both the given SSID and BSSID - bool IsSaved(const char* ssid, const std::uint8_t (&bssid)[6]); - /// @brief Connects to a saved network by it's SSID /// @param ssid SSID of the network /// @return True if the saved network was found and the connection process was started successfully @@ -74,6 +53,20 @@ namespace OpenShock::WiFiManager { /// @return True if the device is connected to a network bool GetConnectedNetwork(OpenShock::WiFiNetwork& network); + /// @brief Gets the devices IP address if it's connected to a network (IPv4) + /// @param ipAddress Variable to store the IP address in + /// @return True if the device is connected to a network + bool GetIPAddress(char* ipAddress); + + /// @brief Gets the devices IP address if it's connected to a network (IPv6) + /// @param ipAddress Variable to store the IP address in + /// @return True if the device is connected to a network + bool GetIPv6Address(char* ipAddress); + /// @brief Runs the WiFiManager loop void Update(); + + /// @brief Gets a copy of the vector of discovered WiFi networks + /// @return Vector of discovered WiFiNetworks + std::vector GetDiscoveredWiFiNetworks(); } // namespace OpenShock::WiFiManager diff --git a/include/wifi/WiFiScanManager.h b/include/wifi/WiFiScanManager.h index ff165444..4c1173c1 100644 --- a/include/wifi/WiFiScanManager.h +++ b/include/wifi/WiFiScanManager.h @@ -13,16 +13,14 @@ namespace OpenShock::WiFiScanManager { bool IsScanning(); bool StartScan(); - void AbortScan(); + bool AbortScan(); typedef std::function StatusChangedHandler; - typedef std::function NetworkDiscoveryHandler; + typedef std::function& networkRecords)> NetworksDiscoveredHandler; std::uint64_t RegisterStatusChangedHandler(const StatusChangedHandler& handler); void UnregisterStatusChangedHandler(std::uint64_t id); - std::uint64_t RegisterNetworkDiscoveryHandler(const NetworkDiscoveryHandler& handler); - void UnregisterNetworkDiscoveredHandler(std::uint64_t id); - - void Update(); + std::uint64_t RegisterNetworksDiscoveredHandler(const NetworksDiscoveredHandler& handler); + void UnregisterNetworksDiscoveredHandler(std::uint64_t id); } // namespace OpenShock::WiFiScanManager diff --git a/platformio.ini b/platformio.ini index d449af1e..103e50f4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,7 +9,7 @@ ; https://docs.platformio.org/page/projectconf.html [env] -platform = espressif32 +platform = espressif32 @ ^6.5.0 board = az-delivery-devkit-v4 ; Overridden per board framework = arduino build_flags = @@ -17,12 +17,10 @@ build_flags = build_unflags = -std=gnu++11 lib_deps = - bblanchon/ArduinoJson@^6.21.3 https://github.com/OpenShock/flatbuffers - https://github.com/martinmoene/span-lite https://github.com/OpenShock/ESPAsyncWebServer - https://github.com/Links2004/arduinoWebSockets -board_build.partitions = huge_app.csv ; Overridden per board + https://github.com/OpenShock/BadWebSockets +custom_openshock.flash_size = 4MB; Can be overridden per board board_build.filesystem = littlefs board_build.embed_files = certificates/x509_crt_bundle extra_scripts = @@ -38,23 +36,22 @@ monitor_filters = esp32_exception_decoder ; Static code analysis check_tool = cppcheck - -[env:fs] -; This exists so we don't build a filesystem per board. +check_skip_packages = true +check_flags = + cppcheck: --std=c++20 -j 8 --suppress=*:*/.pio/* --suppress=*:*/include/serialization/_fbs/* ; https://docs.platformio.org/en/stable/boards/espressif32/wemos_d1_mini32.html [env:Wemos-D1-Mini-ESP32] board = Wemos-D1-Mini-ESP32 -custom_openshock.chip = ESP32-D0WDQ6 +custom_openshock.chip = ESP32 build_flags = -DOPENSHOCK_LED_GPIO=2 - -DOPENSHOCK_TX_PIN=12 + -DOPENSHOCK_RF_TX_GPIO=15 ; https://docs.platformio.org/en/latest/boards/espressif32/lolin_s2_mini.html [env:Wemos-Lolin-S2-Mini] board = Wemos-Lolin-S2-Mini ; override custom_openshock.chip = ESP32-S2 -custom_openshock.chip_variant = N4R2 build_flags = -DOPENSHOCK_LED_GPIO=15 @@ -62,31 +59,49 @@ build_flags = [env:Wemos-Lolin-S3] board = Wemos-Lolin-S3 ; override custom_openshock.chip = ESP32-S3 -custom_openshock.chip_variant = N16R8 +custom_openshock.flash_size = 16MB build_flags = -DOPENSHOCK_LED_WS2812B=38 [env:Pishock-2023] board = Wemos-D1-Mini-ESP32 ; override -custom_openshock.chip = ESP32-D0WD +custom_openshock.chip = ESP32 build_flags = -DOPENSHOCK_LED_GPIO=2 - -DOPENSHOCK_TX_PIN=12 + -DOPENSHOCK_RF_TX_GPIO=12 [env:Pishock-Lite-2021] board = Wemos-D1-Mini-ESP32 ; override -custom_openshock.chip = ESP32-D0WDQ6 +custom_openshock.chip = ESP32 build_flags = -DOPENSHOCK_LED_GPIO=2 - -DOPENSHOCK_TX_PIN=15 + -DOPENSHOCK_RF_TX_GPIO=15 ; https://docs.platformio.org/en/latest//boards/espressif32/seeed_xiao_esp32s3.html [env:Seeed-Xiao-ESP32S3] board = seeed_xiao_esp32s3 ; builtin custom_openshock.chip = ESP32-S3 -custom_openshock.chip_variant = N8R8 +custom_openshock.flash_size = 8MB build_flags = -DOPENSHOCK_LED_GPIO=21 +; https://github.com/nullstalgia/OpenShock-Hardware/tree/main/Core +; 8MB Flash, assume no PSRAM. +[env:OpenShock-Core-V1] +board = esp32-s3-devkitc-1 ; builtin +custom_openshock.chip = ESP32-S3 +custom_openshock.flash_size = 8MB +build_flags = + -DOPENSHOCK_LED_WS2812B=48 + -DOPENSHOCK_LED_GPIO=35 + -DOPENSHOCK_RF_TX_GPIO=15 + -DOPENSHOCK_ESTOP_PIN=13 + -DARDUINO_USB_CDC_ON_BOOT=1 + ; TODO: ; https://docs.platformio.org/en/latest/boards/espressif32/upesy_wroom.html;upesy-esp32-wroom-devkit + +[env:fs] +custom_openshock.chip = ESP32 +custom_openshock.flash_size = 4MB +; This exists so we don't build individual filesystems per board. diff --git a/requirements.txt b/requirements.txt index 65ecf745..556c1cba 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/schemas/ConfigFile.fbs b/schemas/ConfigFile.fbs index 7fe1f346..d7e88d8f 100644 --- a/schemas/ConfigFile.fbs +++ b/schemas/ConfigFile.fbs @@ -1,38 +1,118 @@ namespace OpenShock.Serialization.Configuration; -struct BSSID { - array:[ubyte:6]; -} - -struct RFConfig { +table RFConfig { + /// The GPIO pin connected to the RF modulator's data pin for transmitting (TX) tx_pin:uint8; + + /// Whether to transmit keepalive messages to keep the devices from entering sleep mode + keepalive_enabled:bool; } table WiFiCredentials { + /// ID of the WiFi network credentials, used for referencing the credentials with a low memory footprint id:uint8; + + /// SSID of the WiFi network ssid:string; - bssid:BSSID; + + /// Password of the WiFi network password:string; } table WiFiConfig { + /// Access point SSID ap_ssid:string; + + /// Device hostname hostname:string; + + /// WiFi network credentials credentials:[WiFiCredentials]; } -struct CaptivePortalConfig { +table CaptivePortalConfig { + /// Whether the captive portal is forced to be enabled + /// The captive portal will otherwise shut down when a gateway connection is established always_enabled:bool; } table BackendConfig { - host:string; + /// Domain name of the backend server, e.g. "api.shocklink.net" + domain:string; + + /// Authentication token for the backend server auth_token:string; } +table SerialInputConfig { + /// Whether to echo typed characters back to the serial console + echo_enabled:bool = true; +} + +enum OtaUpdateChannel:uint8 { + Stable = 0, + Beta = 1, + Develop = 2 +} + +enum OtaUpdateStep:uint8 { + None = 0, + Updating = 1, + Updated = 2, + Validating = 3, + Validated = 4, + RollingBack = 5, +} + +// Represents configuration for Over-The-Air (OTA) updates. +table OtaUpdateConfig { + /// Indicates whether OTA updates are enabled. + is_enabled:bool; + + /// The domain name of the OTA Content Delivery Network (CDN). + cdn_domain:string; + + /// The update channel to use. + update_channel:OtaUpdateChannel; + + /// Indicates whether to check for updates on startup. + check_on_startup:bool; + + /// Indicates whether to check for updates periodically. + check_periodically:bool; + + /// The interval in minutes between periodic update checks. + check_interval:uint16; + + /// Indicates if the backend is authorized to manage the device's update version on behalf of the user. + allow_backend_management:bool; + + /// Indicates if manual approval via serial input or captive portal is required before installing updates. + require_manual_approval:bool; + + /// Update process ID, used to track the update process server-side across reboots. + update_id:int32; + + /// Indicates what step of the update process the device is currently in, used to detect failed updates for status reporting. + update_step:OtaUpdateStep; +} + table Config { + /// RF Transmitter configuration rf:RFConfig; + + /// WiFi configuration wifi:WiFiConfig; + + /// Captive portal configuration captive_portal:CaptivePortalConfig; + + /// Backend configuration backend:BackendConfig; + + /// Serial input configuration + serial_input:SerialInputConfig; + + /// OTA update configuration + ota_update:OtaUpdateConfig; } diff --git a/schemas/DeviceToGatewayMessage.fbs b/schemas/DeviceToGatewayMessage.fbs new file mode 100644 index 00000000..e2e7e252 --- /dev/null +++ b/schemas/DeviceToGatewayMessage.fbs @@ -0,0 +1,57 @@ +include "./Types/SemVer.fbs"; +include "./Types/FirmwareBootType.fbs"; + +attribute "fs_serializer"; + +namespace OpenShock.Serialization.Gateway; + +struct KeepAlive { + uptime:ulong; +} + +table BootStatus { + boot_type:Types.FirmwareBootType; + firmware_version:Types.SemVer; + ota_update_id:int32; +} + +table OtaInstallStarted { + update_id:int32; + version:Types.SemVer; +} + +enum OtaInstallProgressTask:byte { + FetchingMetadata, + PreparingForInstall, + FlashingFilesystem, + VerifyingFilesystem, + FlashingApplication, + MarkingApplicationBootable, + Rebooting +} + +table OtaInstallProgress { + update_id:int32; + task:OtaInstallProgressTask; + progress:float; +} + +table OtaInstallFailed { + update_id:int32; + message:string; + fatal:bool; +} + +union DeviceToGatewayMessagePayload { + KeepAlive, + BootStatus, + OtaInstallStarted, + OtaInstallProgress, + OtaInstallFailed +} + +table DeviceToGatewayMessage (fs_serializer) { + payload:DeviceToGatewayMessagePayload; +} + +root_type DeviceToGatewayMessage; diff --git a/schemas/DeviceToLocalMessage.fbs b/schemas/DeviceToLocalMessage.fbs index fa51ed40..50cdb999 100644 --- a/schemas/DeviceToLocalMessage.fbs +++ b/schemas/DeviceToLocalMessage.fbs @@ -1,6 +1,7 @@ include "./Types/WifiScanStatus.fbs"; include "./Types/WifiNetwork.fbs"; include "./Types/WifiNetworkEventType.fbs"; +include "./ConfigFile.fbs"; attribute "fs_serializer"; @@ -9,8 +10,9 @@ namespace OpenShock.Serialization.Local; table ReadyMessage { poggies:bool; connected_wifi:Types.WifiNetwork; - gateway_paired:bool; - rftx_pin:uint8; + account_linked:bool; + + config:Configuration.Config; } table ErrorMessage { @@ -22,7 +24,7 @@ struct WifiScanStatusMessage { } table WifiNetworkEvent { event_type:Types.WifiNetworkEventType; - network:Types.WifiNetwork; + networks:[Types.WifiNetwork]; } table WifiGotIpEvent { ip:string; @@ -55,13 +57,21 @@ struct SetRfTxPinCommandResult { } union DeviceToLocalMessagePayload { + + // General stuff ReadyMessage, ErrorMessage, + + // Wifi stuff WifiScanStatusMessage, WifiNetworkEvent, WifiGotIpEvent, WifiLostIpEvent, + + // Account linking stuff AccountLinkCommandResult, + + // RF transmitter stuff SetRfTxPinCommandResult } diff --git a/schemas/DeviceToServerMessage.fbs b/schemas/DeviceToServerMessage.fbs deleted file mode 100644 index 5d42b1a8..00000000 --- a/schemas/DeviceToServerMessage.fbs +++ /dev/null @@ -1,17 +0,0 @@ -attribute "fs_serializer"; - -namespace OpenShock.Serialization; - -struct KeepAlive { - uptime:ulong; -} - -union DeviceToServerMessagePayload { - KeepAlive -} - -table DeviceToServerMessage (fs_serializer) { - payload:DeviceToServerMessagePayload; -} - -root_type DeviceToServerMessage; diff --git a/schemas/ServerToDeviceMessage.fbs b/schemas/GatewayToDeviceMessage.fbs similarity index 53% rename from schemas/ServerToDeviceMessage.fbs rename to schemas/GatewayToDeviceMessage.fbs index 0936c673..141d483d 100644 --- a/schemas/ServerToDeviceMessage.fbs +++ b/schemas/GatewayToDeviceMessage.fbs @@ -1,9 +1,10 @@ include "./Types/ShockerCommandType.fbs"; include "./Types/ShockerModelType.fbs"; +include "./Types/SemVer.fbs"; attribute "fs_serializer"; -namespace OpenShock.Serialization; +namespace OpenShock.Serialization.Gateway; struct ShockerCommand { model:Types.ShockerModelType; @@ -21,13 +22,19 @@ struct CaptivePortalConfig { enabled:bool; } -union ServerToDeviceMessagePayload { +// Begin installing an OTA update +table OtaInstall { + version:Types.SemVer; +} + +union GatewayToDeviceMessagePayload { ShockerCommandList, - CaptivePortalConfig + CaptivePortalConfig, + OtaInstall } -table ServerToDeviceMessage (fs_serializer) { - payload:ServerToDeviceMessagePayload; +table GatewayToDeviceMessage (fs_serializer) { + payload:GatewayToDeviceMessagePayload; } -root_type ServerToDeviceMessage; +root_type GatewayToDeviceMessage; diff --git a/schemas/LocalToDeviceMessage.fbs b/schemas/LocalToDeviceMessage.fbs index d676b5e0..12964936 100644 --- a/schemas/LocalToDeviceMessage.fbs +++ b/schemas/LocalToDeviceMessage.fbs @@ -20,6 +20,35 @@ struct WifiNetworkDisconnectCommand { placeholder:bool; } +struct OtaUpdateSetIsEnabledCommand { + enabled:bool; +} +table OtaUpdateSetDomainCommand { + domain:string; +} +table OtaUpdateSetUpdateChannelCommand { + channel:string; +} +struct OtaUpdateSetCheckIntervalCommand { + interval:uint16; +} +struct OtaUpdateSetAllowBackendManagementCommand { + allow:bool; +} +struct OtaUpdateSetRequireManualApprovalCommand { + require:bool; +} +struct OtaUpdateHandleUpdateRequestCommand { + accept:bool; +} +table OtaUpdateCheckForUpdatesCommand { + channel:string; +} +table OtaUpdateStartUpdateCommand { + channel:string; + version:string; +} + table AccountLinkCommand { code:string; } @@ -31,13 +60,30 @@ struct SetRfTxPinCommand { } union LocalToDeviceMessagePayload { + + // Wifi stuff WifiScanCommand, WifiNetworkSaveCommand, WifiNetworkForgetCommand, WifiNetworkConnectCommand, WifiNetworkDisconnectCommand, + + // OTA stuff + OtaUpdateSetIsEnabledCommand, + OtaUpdateSetDomainCommand, + OtaUpdateSetUpdateChannelCommand, + OtaUpdateSetCheckIntervalCommand, + OtaUpdateSetAllowBackendManagementCommand, + OtaUpdateSetRequireManualApprovalCommand, + OtaUpdateHandleUpdateRequestCommand, + OtaUpdateCheckForUpdatesCommand, + OtaUpdateStartUpdateCommand, + + // Account linking stuff AccountLinkCommand, AccountUnlinkCommand, + + // RF Transmitter stuff SetRfTxPinCommand } diff --git a/schemas/Types/FirmwareBootType.fbs b/schemas/Types/FirmwareBootType.fbs new file mode 100644 index 00000000..2055b6dc --- /dev/null +++ b/schemas/Types/FirmwareBootType.fbs @@ -0,0 +1,7 @@ +namespace OpenShock.Serialization.Types; + +enum FirmwareBootType : uint8 { + Normal = 0, + NewFirmware = 1, + Rollback = 2, +} diff --git a/schemas/Types/SemVer.fbs b/schemas/Types/SemVer.fbs new file mode 100644 index 00000000..733f952e --- /dev/null +++ b/schemas/Types/SemVer.fbs @@ -0,0 +1,9 @@ +namespace OpenShock.Serialization.Types; + +table SemVer { + major: uint16; + minor: uint16; + patch: uint16; + prerelease: string; + build: string; +} diff --git a/schemas/Types/ShockerModelType.fbs b/schemas/Types/ShockerModelType.fbs index 5e03e7d3..606e3df4 100644 --- a/schemas/Types/ShockerModelType.fbs +++ b/schemas/Types/ShockerModelType.fbs @@ -2,5 +2,5 @@ namespace OpenShock.Serialization.Types; enum ShockerModelType : uint8 { CaiXianlin = 0, - PetTrainer = 1 + Petrainer = 1 } diff --git a/schemas/Types/WifiScanStatus.fbs b/schemas/Types/WifiScanStatus.fbs index 94b0a212..06325ceb 100644 --- a/schemas/Types/WifiScanStatus.fbs +++ b/schemas/Types/WifiScanStatus.fbs @@ -4,6 +4,7 @@ enum WifiScanStatus : uint8 { Started, InProgress, Completed, + TimedOut, Aborted, Error } diff --git a/scripts/build_frontend.py b/scripts/build_frontend.py index e8eed1f3..4bd82331 100644 --- a/scripts/build_frontend.py +++ b/scripts/build_frontend.py @@ -165,11 +165,11 @@ def minify_fa_font(font_path, icon_map): def build_frontend(source, target, env): # Change working directory to frontend. - os.chdir('WebUI') + os.chdir('frontend') - # Build the captive portal only if it wasn't already built. - # This is to avoid rebuilding the captive portal every time the firmware is built. - # This could also lead to some annoying behaviour where the captive portal is not updated when the firmware is built. + # Build the frontend only if it wasn't already built. + # This is to avoid rebuilding the frontend every time the firmware is built. + # This could also lead to some annoying behaviour where the frontend is not updated when the firmware is built. if not sysenv.get_bool('CI', False): print('Building frontend...') os.system('npm i') @@ -179,11 +179,11 @@ def build_frontend(source, target, env): # Change working directory back to root. os.chdir('..') - fa_css = 'WebUI/build/fa/fa-all.css' - fa_woff2 = 'WebUI/build/fa/fa-solid-900.woff2' + fa_css = 'frontend/build/fa/fa-all.css' + fa_woff2 = 'frontend/build/fa/fa-solid-900.woff2' # Analyze the frontend to find all the font awesome icons in use and which css selectors from fa-all.css are unused. - (icon_map, unused_css_selectors) = get_fa_icon_map('WebUI/src', fa_css) + (icon_map, unused_css_selectors) = get_fa_icon_map('frontend/src', fa_css) print('Found ' + str(len(icon_map)) + ' font awesome icons.') # Write a minified css and font file to the static directory. @@ -195,9 +195,9 @@ def build_frontend(source, target, env): copy_actions = [] rename_actions = [] renamed_filenames = [] - for root, dirs, files in os.walk('WebUI/build'): + for root, dirs, files in os.walk('frontend/build'): root = root.replace('\\', '/') - newroot = root.replace('WebUI/build', 'data/www') + newroot = root.replace('frontend/build', 'data/www') isImmutable = '_app/immutable' in root for filename in files: @@ -263,35 +263,49 @@ def build_frontend(source, target, env): with gzip.open(dst_path + '.gz', 'wb') as f_out: f_out.write(s) - # Hash the data/www directory. + +def hash_file_update(md5, sha1, sha256, filepath): + with open(filepath, 'rb') as f: + while True: + chunk = f.read(65536) + if not chunk: + break + md5.update(chunk) + sha1.update(chunk) + sha256.update(chunk) + + +def hash_file(filepath): hashMd5 = hashlib.md5() hashSha1 = hashlib.sha1() hashSha256 = hashlib.sha256() - for root, _, files in os.walk('data/www'): - root = root.replace('\\', '/') - for filename in files: - filepath = os.path.join(root, filename) - with open(filepath, 'rb') as f: - while True: - chunk = f.read(65536) - if not chunk: - break - hashMd5.update(chunk) - hashSha1.update(chunk) - hashSha256.update(chunk) - hashMd5 = hashMd5.hexdigest() - hashSha1 = hashSha1.hexdigest() - hashSha256 = hashSha256.hexdigest() - - print('Build hash:') - print(' MD5: ' + hashMd5) - print(' SHA1: ' + hashSha1) - print(' SHA256: ' + hashSha256) - - # Write the hashes to data/www files - file_write_text('data/www/hash.md5', hashMd5, None) - file_write_text('data/www/hash.sha1', hashSha1, None) - file_write_text('data/www/hash.sha256', hashSha256, None) + + hash_file_update(hashMd5, hashSha1, hashSha256, filepath) + + return { + 'MD5': hashMd5.hexdigest(), + 'SHA1': hashSha1.hexdigest(), + 'SHA256': hashSha256.hexdigest(), + } + + +def process_littlefs_binary(source, target, env): + nTargets = len(target) + if nTargets != 1: + raise Exception('Expected 1 target, got ' + str(nTargets)) + + # Get the path to the binary and its directory. + littlefs_path = target[0].get_abspath() + + bin_size = os.path.getsize(littlefs_path) + bin_hashes = hash_file(littlefs_path) + + print('FileSystem Size: ' + str(bin_size) + ' bytes') + print('FileSystem Hashes:') + print('MD5: ' + bin_hashes['MD5']) + print('SHA1: ' + bin_hashes['SHA1']) + print('SHA256: ' + bin_hashes['SHA256']) env.AddPreAction('$BUILD_DIR/littlefs.bin', build_frontend) +env.AddPostAction('$BUILD_DIR/littlefs.bin', process_littlefs_binary) diff --git a/scripts/embed_env_vars.py b/scripts/embed_env_vars.py index a999506e..9d8564be 100644 --- a/scripts/embed_env_vars.py +++ b/scripts/embed_env_vars.py @@ -45,13 +45,21 @@ def get_git_commit() -> str | None: # Find env variables based on only the pioenv and sysenv. def get_pio_firmware_vars() -> dict[str, str | int | bool]: + fw_board = pio.get_string('PIOENV') + fw_chip = pio.get_string('BOARD_MCU') + + def macroify(s: str) -> str: + return s.upper().replace('-', '').replace('_', '') + vars = {} - vars['OPENSHOCK_FW_BOARD'] = pio.get_string('PIOENV') - vars['OPENSHOCK_FW_CHIP'] = pio.get_string('BOARD_MCU') + vars['OPENSHOCK_FW_BOARD'] = fw_board + vars['OPENSHOCK_FW_BOARD_' + macroify(fw_board)] = True # Used for conditional compilation. + vars['OPENSHOCK_FW_CHIP'] = fw_chip.upper() + vars['OPENSHOCK_FW_CHIP_' + macroify(fw_chip)] = True # Used for conditional compilation. vars['OPENSHOCK_FW_MODE'] = pio_build_type git_commit = get_git_commit() if git_commit is not None: - vars['OPENSHOCK_FW_COMMIT'] = git_commit + vars['OPENSHOCK_FW_GIT_COMMIT'] = git_commit return vars @@ -84,16 +92,16 @@ def transform_cpp_define_string(k: str, v: str) -> str: if v.startswith('"') and v.endswith('"'): v = v[1:-1] - # Special case for OPENSHOCK_FW_COMMIT. - if k == 'OPENSHOCK_FW_COMMIT' and len(v) > 7: + # Special case for OPENSHOCK_FW_GIT_COMMIT. + if k == 'OPENSHOCK_FW_GIT_COMMIT' and len(v) > 7: v = v[0:7] return env.StringifyMacro(v) def serialize_cpp_define(k: str, v: str | int | bool) -> str | int: - # Special case for OPENSHOCK_FW_COMMIT. - if k == 'OPENSHOCK_FW_COMMIT': + # Special case for OPENSHOCK_FW_GIT_COMMIT. + if k == 'OPENSHOCK_FW_GIT_COMMIT': return transform_cpp_define_string(k, str(v)) try: @@ -170,4 +178,7 @@ def print_dump(name: str, map: Mapping[str, str | int | bool]) -> None: env['BUILD_FLAGS'] = remaining_build_flags env.Append(CPPDEFINES=list(cpp_defines.items())) +# Rename the firmware.bin to app.bin. +env.Replace(PROGNAME='app') + print(env.Dump()) diff --git a/scripts/flatc.exe b/scripts/flatc.exe index 79fd9065..7dd09fc9 100644 Binary files a/scripts/flatc.exe and b/scripts/flatc.exe differ diff --git a/scripts/generate_schemas.py b/scripts/generate_schemas.py index de835ffe..a2820e7b 100644 --- a/scripts/generate_schemas.py +++ b/scripts/generate_schemas.py @@ -1,13 +1,50 @@ import os -import sys import shutil +import subprocess -# Check if flatc is installed. -if os.system('flatc --version') != 0: + +def flatc_test(path: str): + return os.system(path + ' --version') == 0 + + +def exec_silent(args: list[str]): + return subprocess.check_output(args, shell=True).decode('utf-8').strip() + + +def get_flatc_path(): + # Check if there is a flatc.exe in the working directory. + path = os.path.join(os.getcwd(), 'flatc.exe') + if os.path.exists(path): + # Test if the executable is working. + if flatc_test(path): + return path + + # Check if there is a flatc.exe in the scripts directory. + script_dir = os.path.dirname(os.path.realpath(__file__)) + path = os.path.join(script_dir, 'flatc.exe') + if os.path.exists(path): + # Test if the executable is working. + if flatc_test(path): + return path + + # Check if flatc is installed globally. + if flatc_test('flatc'): + return 'flatc' + + return None + + +flatc_path = get_flatc_path() +if flatc_path is None: print('flatc not found. Please install flatc and make sure it is in your PATH.') exit(1) +def run_flatc(args): + # Run flatc. + return os.system(flatc_path + ' ' + args) + + def resolve_path(path): cwd = os.getcwd() script_path = os.path.dirname(os.path.realpath(__file__)) @@ -16,7 +53,7 @@ def resolve_path(path): # resolve paths schemas_path = resolve_path('../schemas') -ts_output_path = resolve_path('../WebUI/src/lib/_fbs') +ts_output_path = resolve_path('../frontend/src/lib/_fbs') cpp_output_path = resolve_path('../include/serialization/_fbs') # Get all the schema files. @@ -32,6 +69,8 @@ def resolve_path(path): flatc_args_ts = [ # Compile for TypeScript. '--ts', + # Don't add .ts extension to imports. + '--ts-no-import-ext', # Output directory. '-o ' + ts_output_path, ] + schema_files @@ -56,10 +95,10 @@ def resolve_path(path): print('Deleting old TypeScript schemas') shutil.rmtree(ts_output_path) print('Compiling schemas for TypeScript') -os.system('flatc ' + ' '.join(flatc_args_ts)) +run_flatc(' '.join(flatc_args_ts)) if os.path.exists(cpp_output_path): print('Deleting old C++ schemas') shutil.rmtree(cpp_output_path) print('Compiling schemas for C++') -os.system('flatc ' + ' '.join(flatc_args_cpp)) +run_flatc(' '.join(flatc_args_cpp)) diff --git a/scripts/use_openshock_params.py b/scripts/use_openshock_params.py index fed0cfe5..18066721 100644 --- a/scripts/use_openshock_params.py +++ b/scripts/use_openshock_params.py @@ -1,11 +1,15 @@ import os from pathlib import Path -from utils.boardconf import BoardConf, from_pio_env, validate, print_header, print_footer +from utils.boardconf import BoardConf, from_pio_env, validate, print_header, print_footer, fallback_flash_size +import csv # # This file is responsible for processing the "custom_openshock.*" # parameters declared in `platformio.ini`. # +# And for updating the `partitions.csv` file to include the +# `config` partition. +# # This file is invoked by PlatformIO during build. # See 'extra_scripts' in 'platformio.ini'. # @@ -16,21 +20,34 @@ # Parse the board/chip specific configuration from the current pio `env` variable. boardconf: BoardConf = from_pio_env(env) +# In case we need to blacklist certain flash sizes from OTA support. +blacklisted_OTA_sizes = [ + # '4MB' + ] def use_openshock_params(): if not validate(boardconf): + print('Failed to validate OpenShock board configuration.') return # Handle partitions file not existing - if not boardconf.does_partitions_file_exists(): - print('WARNING: PARTITIONS FILE DOES NOT EXIST.') - print('Not overriding default value.') - return + if not boardconf.do_partitions_files_exists(boardconf.get_flash_size()): + if boardconf.do_partitions_files_exists(fallback_flash_size): + print(f'WARNING: PARTITIONS FILES DON\'T EXIST FOR THIS FLASH SIZE, FALLING BACK TO {fallback_flash_size}.') + boardconf.override_flash_size(fallback_flash_size) # Set partition file to chip dir. # https://docs.platformio.org/en/latest/scripting/examples/override_board_configuration.html board_config = env.BoardConfig() - board_config.update('build.partitions', str(boardconf.get_partitions_file())) + + partitions, partitions_ota = boardconf.get_partitions_files(boardconf.get_flash_size()) + + if boardconf.get_flash_size() in blacklisted_OTA_sizes: + print('WARNING: OTA NOT SUPPORTED FOR THIS FLASH SIZE.') + print('Using Non-OTA partition layout.') + board_config.update('build.partitions', str(partitions)) + else: + board_config.update('build.partitions', str(partitions_ota)) print_header() diff --git a/scripts/utils/boardconf.py b/scripts/utils/boardconf.py index f416a25a..6fe35397 100644 --- a/scripts/utils/boardconf.py +++ b/scripts/utils/boardconf.py @@ -2,25 +2,28 @@ from pathlib import Path from configparser import ConfigParser +fallback_flash_size = '4MB' class BoardConf: - def __init__(self, chip: str, chip_variant: str): + def __init__(self, chip: str, flash_size: str): self.chip = chip - self.chip_variant = chip_variant + self.flash_size = flash_size + if flash_size == '': + self.flash_size = fallback_flash_size # Set dirs self.root_dir = Path().absolute() self.chips_dir = self.root_dir / 'chips' self.chip_base_dir = self.chips_dir / self.chip - self.chip_variant_dir = ( - (self.chip_base_dir / self.chip_variant) if self.chip_variant != '' else self.chip_base_dir + self.flash_size_dir = ( + (self.chip_base_dir / self.flash_size) if self.flash_size != '' else self.chip_base_dir ) def get_chip(self) -> str: return self.chip - def get_chip_variant(self) -> str: - return self.chip_variant + def get_flash_size(self) -> str: + return self.flash_size def get_chips_dir(self) -> Path: return self.chips_dir @@ -28,36 +31,44 @@ def get_chips_dir(self) -> Path: def get_chip_dir(self) -> Path: return self.chip_base_dir - def get_chip_variant_dir(self) -> Path: - return self.chip_variant_dir + def get_flash_size_dir(self) -> Path: + return self.flash_size_dir + + def override_flash_size(self, flash_size: str): + self.flash_size = flash_size + self.flash_size_dir = self.chip_base_dir / self.flash_size def get_merge_script(self) -> Path: - return self.chip_variant_dir / 'merge-image.py' + return self.flash_size_dir / 'merge-image.py' - def get_partitions_file(self) -> Path: - return self.chip_variant_dir / 'partitions.csv' + def get_partitions_files(self, size: str) -> tuple[Path, Path]: + return self.chips_dir / f'partitions_{size}.csv', self.chips_dir / f'partitions_{size}_OTA.csv', def is_chip_specified(self) -> bool: return self.chip != '' - def is_chip_variant_specified(self) -> bool: - return self.chip_variant != '' + def is_flash_size_specified(self) -> bool: + return self.flash_size != '' # Whether the chip folder exists. def does_chip_exist(self) -> bool: return os.path.exists(self.get_chip_dir()) - # Whether the chip variant folder exists. - def does_chip_variant_exist(self) -> bool: - return os.path.exists(self.get_chip_variant_dir()) + # Whether the flash size folder exists. + def does_flash_size_exist(self, size: str = "") -> bool: + if size == "": + return os.path.exists(self.get_flash_size_dir()) + else: + return os.path.exists(self.chip_base_dir / size) - # Whether the merge script for the current chip variant exists. + # Whether the merge script for the current flash size exists. def does_merge_script_exist(self) -> bool: return os.path.exists(self.get_merge_script()) - # Whether the partitiosn file for the current chip variant exists. - def does_partitions_file_exists(self) -> bool: - return os.path.exists(self.get_partitions_file()) + # Whether the partitions file for the current flash size exists. + def do_partitions_files_exists(self, size: str) -> bool: + partitions, partitions_ota = self.get_partitions_files(size) + return os.path.exists(partitions) and os.path.exists(partitions_ota) def from_pio_file(env_name: str) -> BoardConf: @@ -68,18 +79,23 @@ def from_pio_file(env_name: str) -> BoardConf: env = config['env:' + env_name] # Grab strings from platformio.ini env declaration + # TODO: Simplify platformio.ini to only have one env (Flash Size) + # Need to either pass in or figure out chip chip = env.get('custom_openshock.chip', '') - chip_variant = env.get('custom_openshock.chip_variant', '') + flash_size = env.get('custom_openshock.flash_size', '') - return BoardConf(chip, chip_variant) + return BoardConf(chip, flash_size) def from_pio_env(env): # Grab strings from current pio env + # TODO: Simplify platformio.ini to only have one env (Flash Size) + # env.BoardConfig().get('build.mcu') + # can be used to get chip chip = env.GetProjectOption('custom_openshock.chip', '') - chip_variant = env.GetProjectOption('custom_openshock.chip_variant', '') + flash_size = env.GetProjectOption('custom_openshock.flash_size', '') - return BoardConf(chip, chip_variant) + return BoardConf(chip, flash_size) def print_header(): @@ -98,12 +114,12 @@ def validate(boardconf) -> bool: # Handle missing chip declaration. if not boardconf.is_chip_specified(): print('No chip specified.') - print('Skipping further chip/chip-variant specific initialization.') + print('Skipping further chip/flash size-specific initialization.') return False - # Print info about determined chip/variant. - print('Chip: %s' % boardconf.get_chip()) - print('Variant: %s' % boardconf.get_chip_variant()) + # Print info about determined chip/flash size. + print('Chip Variant: %s' % boardconf.get_chip()) + print('Flash Size: %s' % boardconf.get_flash_size()) print('') # Handle chip not existing. @@ -112,15 +128,21 @@ def validate(boardconf) -> bool: print('Did you make a typo?') return False - # Handle specified variant not existing. - if not boardconf.does_chip_variant_exist(): - print('Chip VARIANT directory does not exist: %s' % boardconf.get_chip_variant_dir()) - print('Did you make a typo?') - return False + # Handle specified flash size not existing. + if not boardconf.does_flash_size_exist(): + print('Flash size directory does not exist: %s' % boardconf.get_flash_size_dir()) + # Attempt to try fallback flash size. + if boardconf.does_flash_size_exist(fallback_flash_size): + print(f'Falling back to {fallback_flash_size} flash size.') + boardconf.override_flash_size(fallback_flash_size) + else: + print(f'Failed to fallback to {fallback_flash_size} flash size.') + print('Did you make a typo?') + return False # Print expected location of merge script and partitions file print('Determined merge script: %s' % boardconf.get_merge_script()) - print('Determined partitions file: %s' % boardconf.get_partitions_file()) + print('Potential partition files: %s' % str(boardconf.get_partitions_files(boardconf.get_flash_size()))) print('') return True diff --git a/src/CaptivePortal.cpp b/src/CaptivePortal.cpp index 7422e6c1..b26a2d7b 100644 --- a/src/CaptivePortal.cpp +++ b/src/CaptivePortal.cpp @@ -2,9 +2,10 @@ #include "CaptivePortalInstance.h" #include "CommandHandler.h" -#include "Config.h" +#include "config/Config.h" #include "GatewayConnectionManager.h" #include "Logging.h" +#include "Time.h" #include #include @@ -19,6 +20,7 @@ static const char* TAG = "CaptivePortal"; using namespace OpenShock; static bool s_alwaysEnabled = false; +static bool s_forceClosed = false; static std::unique_ptr s_instance = nullptr; bool _startCaptive() { @@ -92,18 +94,38 @@ bool CaptivePortal::IsAlwaysEnabled() { return s_alwaysEnabled; } +bool CaptivePortal::ForceClose(std::uint32_t timeoutMs) { + s_forceClosed = true; + + if (s_instance == nullptr) return true; + + while (timeoutMs > 0) { + std::uint32_t delay = std::min(timeoutMs, 10U); + + vTaskDelay(pdMS_TO_TICKS(delay)); + + timeoutMs -= delay; + + if (s_instance == nullptr) return true; + } + + return false; +} + bool CaptivePortal::IsRunning() { return s_instance != nullptr; } + void CaptivePortal::Update() { bool gatewayConnected = GatewayConnectionManager::IsConnected(); bool commandHandlerOk = CommandHandler::Ok(); - bool shouldBeRunning = s_alwaysEnabled || !gatewayConnected || !commandHandlerOk; + bool shouldBeRunning = (s_alwaysEnabled || !gatewayConnected || !commandHandlerOk) && !s_forceClosed; if (s_instance == nullptr) { if (shouldBeRunning) { ESP_LOGD(TAG, "Starting captive portal"); ESP_LOGD(TAG, " alwaysEnabled: %s", s_alwaysEnabled ? "true" : "false"); + ESP_LOGD(TAG, " forceClosed: %s", s_forceClosed ? "true" : "false"); ESP_LOGD(TAG, " isConnected: %s", gatewayConnected ? "true" : "false"); ESP_LOGD(TAG, " commandHandlerOk: %s", commandHandlerOk ? "true" : "false"); _startCaptive(); @@ -114,19 +136,18 @@ void CaptivePortal::Update() { if (!shouldBeRunning) { ESP_LOGD(TAG, "Stopping captive portal"); ESP_LOGD(TAG, " alwaysEnabled: %s", s_alwaysEnabled ? "true" : "false"); + ESP_LOGD(TAG, " forceClosed: %s", s_forceClosed ? "true" : "false"); ESP_LOGD(TAG, " isConnected: %s", gatewayConnected ? "true" : "false"); ESP_LOGD(TAG, " commandHandlerOk: %s", commandHandlerOk ? "true" : "false"); _stopCaptive(); return; } - - s_instance->loop(); } -bool CaptivePortal::SendMessageTXT(std::uint8_t socketId, const char* data, std::size_t len) { +bool CaptivePortal::SendMessageTXT(std::uint8_t socketId, StringView data) { if (s_instance == nullptr) return false; - s_instance->sendMessageTXT(socketId, data, len); + s_instance->sendMessageTXT(socketId, data); return true; } @@ -138,10 +159,10 @@ bool CaptivePortal::SendMessageBIN(std::uint8_t socketId, const std::uint8_t* da return true; } -bool CaptivePortal::BroadcastMessageTXT(const char* data, std::size_t len) { +bool CaptivePortal::BroadcastMessageTXT(StringView data) { if (s_instance == nullptr) return false; - s_instance->broadcastMessageTXT(data, len); + s_instance->broadcastMessageTXT(data); return true; } diff --git a/src/CaptivePortalInstance.cpp b/src/CaptivePortalInstance.cpp index 14229fd8..13e8ed4a 100644 --- a/src/CaptivePortalInstance.cpp +++ b/src/CaptivePortalInstance.cpp @@ -5,79 +5,100 @@ #include "GatewayConnectionManager.h" #include "Logging.h" #include "serialization/WSLocal.h" - +#include "util/HexUtils.h" +#include "util/PartitionUtils.h" +#include "util/TaskUtils.h" #include "wifi/WiFiManager.h" #include "serialization/_fbs/DeviceToLocalMessage_generated.h" -#include #include static const char* TAG = "CaptivePortalInstance"; -constexpr std::uint16_t HTTP_PORT = 80; -constexpr std::uint16_t WEBSOCKET_PORT = 81; -constexpr std::uint32_t WEBSOCKET_PING_INTERVAL = 10'000; -constexpr std::uint32_t WEBSOCKET_PING_TIMEOUT = 1000; -constexpr std::uint8_t WEBSOCKET_PING_RETRIES = 3; +constexpr std::uint16_t HTTP_PORT = 80; +constexpr std::uint16_t WEBSOCKET_PORT = 81; +constexpr std::uint16_t DNS_PORT = 53; +constexpr std::uint32_t WEBSOCKET_PING_INTERVAL = 10'000; +constexpr std::uint32_t WEBSOCKET_PING_TIMEOUT = 1000; +constexpr std::uint8_t WEBSOCKET_PING_RETRIES = 3; +constexpr std::uint32_t WEBSOCKET_UPDATE_INTERVAL = 10; // 10ms / 100Hz using namespace OpenShock; -bool TryReadFile(const char* path, char* buffer, std::size_t& bufferSize) { - File file = LittleFS.open(path, "r"); - if (!file) { - ESP_LOGE(TAG, "Failed to open file %s for reading", path); - return false; +const esp_partition_t* _getStaticPartition() { + const esp_partition_t* partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "static0"); + if (partition != nullptr) { + return partition; } - std::size_t fileSize = file.size(); - if (fileSize > bufferSize) { - ESP_LOGE(TAG, "File %s is too large to fit in buffer", path); - file.close(); - return false; + partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "static1"); + if (partition != nullptr) { + return partition; } - file.readBytes(buffer, fileSize); - - bufferSize = fileSize; - - file.close(); - - return true; + return nullptr; } -bool TryGetFsHash(char (&buffer)[65]) { - std::size_t bufferSize = sizeof(buffer); - if (TryReadFile("/www/hash.sha1", buffer, bufferSize)) { - buffer[bufferSize] = '\0'; - return true; - } - if (TryReadFile("/www/hash.sha256", buffer, bufferSize)) { - buffer[bufferSize] = '\0'; - return true; +const char* _getPartitionHash() { + const esp_partition_t* partition = _getStaticPartition(); + if (partition == nullptr) { + return nullptr; } - if (TryReadFile("/www/hash.md5", buffer, bufferSize)) { - buffer[bufferSize] = '\0'; - return true; + + static char hash[65]; + if (!OpenShock::TryGetPartitionHash(partition, hash)) { + return nullptr; } - return false; + + return hash; } CaptivePortalInstance::CaptivePortalInstance() - : m_webServer(HTTP_PORT), m_socketServer(WEBSOCKET_PORT, "/ws", "json"), m_socketDeFragger(std::bind(&CaptivePortalInstance::handleWebSocketEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)) { + : m_webServer(HTTP_PORT) + , m_socketServer(WEBSOCKET_PORT, "/ws", "json") + , m_socketDeFragger(std::bind(&CaptivePortalInstance::handleWebSocketEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)) + , m_fileSystem() + , m_dnsServer() + , m_taskHandle(nullptr) { m_socketServer.onEvent(std::bind(&WebSocketDeFragger::handler, &m_socketDeFragger, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); m_socketServer.begin(); m_socketServer.enableHeartbeat(WEBSOCKET_PING_INTERVAL, WEBSOCKET_PING_TIMEOUT, WEBSOCKET_PING_RETRIES); - // Check if the www folder exists and is populated - char fsHash[65]; - if (LittleFS.exists("/www/index.html.gz") && TryGetFsHash(fsHash)) { + ESP_LOGI(TAG, "Setting up DNS server"); + m_dnsServer.start(DNS_PORT, "*", WiFi.softAPIP()); + + bool fsOk = true; + + // Get the hash of the filesystem + const char* fsHash = _getPartitionHash(); + if (fsHash == nullptr) { + ESP_LOGE(TAG, "Failed to get filesystem hash"); + fsOk = false; + } + + if (fsOk) { + // Mounting LittleFS + if (!m_fileSystem.begin(false, "/static", 10U, "static0")) { + ESP_LOGE(TAG, "Failed to mount LittleFS"); + fsOk = false; + } else { + fsOk = m_fileSystem.exists("/www/index.html.gz"); + } + } + + if (fsOk) { ESP_LOGI(TAG, "Serving files from LittleFS"); ESP_LOGI(TAG, "Filesystem hash: %s", fsHash); - m_webServer.serveStatic("/", LittleFS, "/www/", "max-age=3600").setDefaultFile("index.html").setSharedEtag(fsHash); + char softAPURL[64]; + snprintf(softAPURL, sizeof(softAPURL), "http://%s", WiFi.softAPIP().toString().c_str()); + + // Serving the captive portal files from LittleFS + m_webServer.serveStatic("/", m_fileSystem, "/www/", "max-age=3600").setDefaultFile("index.html").setSharedEtag(fsHash); - m_webServer.onNotFound([](AsyncWebServerRequest* request) { request->send(404, "text/plain", "Not found"); }); + // Redirecting connection tests to the captive portal, triggering the "login to network" prompt + m_webServer.onNotFound([softAPURL](AsyncWebServerRequest* request) { request->redirect(softAPURL); }); } else { ESP_LOGE(TAG, "/www/index.html or hash files not found, serving error page"); @@ -85,17 +106,46 @@ CaptivePortalInstance::CaptivePortalInstance() request->send( 200, "text/plain", - "You probably forgot to upload the Filesystem with PlatformIO!\nGo to PlatformIO -> Platform -> Upload Filesystem Image!\nIf this happened with a file we provided or you just need help, come to the Discord!\n\ndiscord.gg/openshock" + // Raw string literal (1+ to remove the first newline) + 1 + R"( +You probably forgot to upload the Filesystem with PlatformIO! +Go to PlatformIO -> Platform -> Upload Filesystem Image! +If this happened with a file we provided or you just need help, come to the Discord! + +discord.gg/OpenShock +)" ); }); } m_webServer.begin(); + + if (fsOk) { + if (TaskUtils::TaskCreateExpensive(CaptivePortalInstance::task, TAG, 8192, this, 1, &m_taskHandle) != pdPASS) { + ESP_LOGE(TAG, "Failed to create task"); + } + } } CaptivePortalInstance::~CaptivePortalInstance() { + if (m_taskHandle != nullptr) { + vTaskDelete(m_taskHandle); + m_taskHandle = nullptr; + } m_webServer.end(); m_socketServer.close(); + m_fileSystem.end(); + m_dnsServer.stop(); +} + +void CaptivePortalInstance::task(void* arg) { + CaptivePortalInstance* instance = reinterpret_cast(arg); + + while (true) { + instance->m_socketServer.loop(); + // instance->m_dnsServer.processNextRequest(); + vTaskDelay(pdMS_TO_TICKS(WEBSOCKET_UPDATE_INTERVAL)); + } } void CaptivePortalInstance::handleWebSocketClientConnected(std::uint8_t socketId) { @@ -107,7 +157,12 @@ void CaptivePortalInstance::handleWebSocketClientConnected(std::uint8_t socketId connectedNetworkPtr = &connectedNetwork; } - Serialization::Local::SerializeReadyMessage(connectedNetworkPtr, GatewayConnectionManager::IsPaired(), CommandHandler::GetRfTxPin(), std::bind(&CaptivePortalInstance::sendMessageBIN, this, socketId, std::placeholders::_1, std::placeholders::_2)); + Serialization::Local::SerializeReadyMessage(connectedNetworkPtr, GatewayConnectionManager::IsLinked(), std::bind(&CaptivePortalInstance::sendMessageBIN, this, socketId, std::placeholders::_1, std::placeholders::_2)); + + // Send all previously scanned wifi networks + auto networks = OpenShock::WiFiManager::GetDiscoveredWiFiNetworks(); + + Serialization::Local::SerializeWiFiNetworksEvent(Serialization::Types::WifiNetworkEventType::Discovered, networks, std::bind(&CaptivePortalInstance::sendMessageBIN, this, socketId, std::placeholders::_1, std::placeholders::_2)); } void CaptivePortalInstance::handleWebSocketClientDisconnected(std::uint8_t socketId) { diff --git a/src/CommandHandler.cpp b/src/CommandHandler.cpp index ecff4559..059c8c42 100644 --- a/src/CommandHandler.cpp +++ b/src/CommandHandler.cpp @@ -1,34 +1,199 @@ #include "CommandHandler.h" -#include "Board.h" -#include "Config.h" -#include "Constants.h" +#include "Chipset.h" +#include "config/Config.h" +#include "Common.h" #include "Logging.h" #include "radio/RFTransmitter.h" +#include "Time.h" +#include "util/TaskUtils.h" + +#include +#include +#include #include +#include const char* const TAG = "CommandHandler"; +const std::int64_t KEEP_ALIVE_INTERVAL = 60'000; +const std::uint16_t KEEP_ALIVE_DURATION = 300; + using namespace OpenShock; +template +constexpr T saturate(T value, T min, T max) { + return std::min(std::max(value, min), max); +} +std::uint32_t calculateEepyTime(std::int64_t timeToKeepAlive) { + std::int64_t now = OpenShock::millis(); + return static_cast(saturate(timeToKeepAlive - now, 0LL, KEEP_ALIVE_INTERVAL)); +} + +struct KnownShocker { + bool killTask; + ShockerModelType model; + std::uint16_t shockerId; + std::int64_t lastActivityTimestamp; +}; + +static SemaphoreHandle_t s_rfTransmitterMutex = nullptr; static std::unique_ptr s_rfTransmitter = nullptr; +static SemaphoreHandle_t s_keepAliveMutex = nullptr; +static QueueHandle_t s_keepAliveQueue = nullptr; +static TaskHandle_t s_keepAliveTaskHandle = nullptr; + +void _keepAliveTask(void* arg) { + (void)arg; + + std::int64_t timeToKeepAlive = KEEP_ALIVE_INTERVAL; + + // Map of shocker IDs to time of next keep-alive + std::unordered_map activityMap; + + while (true) { + // Calculate eepyTime based on the timeToKeepAlive + std::uint32_t eepyTime = calculateEepyTime(timeToKeepAlive); + + KnownShocker cmd; + while (xQueueReceive(s_keepAliveQueue, &cmd, pdMS_TO_TICKS(eepyTime)) == pdTRUE) { + if (cmd.killTask) { + ESP_LOGI(TAG, "Received kill command, exiting keep-alive task"); + vTaskDelete(nullptr); + break; // This should never be reached + } + + activityMap[cmd.shockerId] = cmd; + + eepyTime = calculateEepyTime(std::min(timeToKeepAlive, cmd.lastActivityTimestamp + KEEP_ALIVE_INTERVAL)); + } + + // Update the time to now + std::int64_t now = OpenShock::millis(); + + // Keep track of the minimum activity time, so we know when to wake up + timeToKeepAlive = now + KEEP_ALIVE_INTERVAL; + + // For every entry that has a keep-alive time less than now, send a keep-alive + for (auto it = activityMap.begin(); it != activityMap.end(); ++it) { + auto& cmdRef = it->second; + + if (cmdRef.lastActivityTimestamp + KEEP_ALIVE_INTERVAL < now) { + ESP_LOGV(TAG, "Sending keep-alive for shocker %u", cmdRef.shockerId); + + if (s_rfTransmitter == nullptr) { + ESP_LOGW(TAG, "RF Transmitter is not initialized, ignoring keep-alive"); + break; + } + + if (!s_rfTransmitter->SendCommand(cmdRef.model, cmdRef.shockerId, ShockerCommandType::Vibrate, 0, KEEP_ALIVE_DURATION, false)) { + ESP_LOGW(TAG, "Failed to send keep-alive for shocker %u", cmdRef.shockerId); + } + + cmdRef.lastActivityTimestamp = now; + } + + timeToKeepAlive = std::min(timeToKeepAlive, cmdRef.lastActivityTimestamp + KEEP_ALIVE_INTERVAL); + } + } +} + +bool _internalSetKeepAliveEnabled(bool enabled) { + bool wasEnabled = s_keepAliveQueue != nullptr && s_keepAliveTaskHandle != nullptr; + + if (enabled == wasEnabled) { + ESP_LOGV(TAG, "keep-alive task is already %s", enabled ? "enabled" : "disabled"); + return true; + } + + xSemaphoreTake(s_keepAliveMutex, portMAX_DELAY); + + if (enabled) { + ESP_LOGV(TAG, "Enabling keep-alive task"); + + s_keepAliveQueue = xQueueCreate(32, sizeof(KnownShocker)); + if (s_keepAliveQueue == nullptr) { + ESP_LOGE(TAG, "Failed to create keep-alive task"); + + xSemaphoreGive(s_keepAliveMutex); + return false; + } + + if (TaskUtils::TaskCreateExpensive(_keepAliveTask, "KeepAliveTask", 4096, nullptr, 1, &s_keepAliveTaskHandle) != pdPASS) { + ESP_LOGE(TAG, "Failed to create keep-alive task"); + + vQueueDelete(s_keepAliveQueue); + s_keepAliveQueue = nullptr; + + xSemaphoreGive(s_keepAliveMutex); + return false; + } + } else { + ESP_LOGV(TAG, "Disabling keep-alive task"); + if (s_keepAliveTaskHandle != nullptr && s_keepAliveQueue != nullptr) { + // Wait for the task to stop + KnownShocker cmd {.killTask = true}; + while (eTaskGetState(s_keepAliveTaskHandle) != eDeleted) { + vTaskDelay(pdMS_TO_TICKS(10)); + + // Send nullptr to stop the task gracefully + xQueueSend(s_keepAliveQueue, &cmd, pdMS_TO_TICKS(10)); + } + vQueueDelete(s_keepAliveQueue); + s_keepAliveQueue = nullptr; + } else { + ESP_LOGW(TAG, "keep-alive task is already disabled? Something might be wrong."); + } + } + + xSemaphoreGive(s_keepAliveMutex); + + return true; +} + bool CommandHandler::Init() { - std::uint8_t txPin = Config::GetRFConfig().txPin; - if (!OpenShock::IsValidOutputPin(txPin)) { - ESP_LOGW(TAG, "Clearing invalid RF TX pin"); - Config::SetRFConfigTxPin(Constants::GPIO_INVALID); + if (s_rfTransmitterMutex != nullptr) { + ESP_LOGW(TAG, "RF Transmitter is already initialized"); + return true; + } + + // Initialize mutexes + s_rfTransmitterMutex = xSemaphoreCreateMutex(); + s_keepAliveMutex = xSemaphoreCreateMutex(); + + Config::RFConfig rfConfig; + if (!Config::GetRFConfig(rfConfig)) { + ESP_LOGE(TAG, "Failed to get RF config"); return false; } - s_rfTransmitter = std::make_unique(txPin, 32); + std::uint8_t txPin = rfConfig.txPin; + if (!OpenShock::IsValidOutputPin(txPin)) { + if (!OpenShock::IsValidOutputPin(Constants::GPIO_RF_TX)) { + ESP_LOGE(TAG, "Configured RF TX pin (%u) is invalid, and default pin (%u) is invalid. Unable to initialize RF transmitter", txPin, Constants::GPIO_RF_TX); + + ESP_LOGD(TAG, "Setting RF TX pin to GPIO_INVALID"); + return Config::SetRFConfigTxPin(Constants::GPIO_INVALID); // This is not a error yet, unless we are unable to save the RF TX Pin as invalid + } + + ESP_LOGW(TAG, "Configured RF TX pin (%u) is invalid, using default pin (%u)", txPin, Constants::GPIO_RF_TX); + txPin = Constants::GPIO_RF_TX; + Config::SetRFConfigTxPin(txPin); + } + + s_rfTransmitter = std::make_unique(txPin); if (!s_rfTransmitter->ok()) { - ESP_LOGE(TAG, "Failed to initialize RF transmitter"); + ESP_LOGE(TAG, "Failed to initialize RF Transmitter"); s_rfTransmitter = nullptr; return false; } + if (rfConfig.keepAliveEnabled) { + _internalSetKeepAliveEnabled(true); + } + return true; } @@ -41,35 +206,83 @@ SetRfPinResultCode CommandHandler::SetRfTxPin(std::uint8_t txPin) { return SetRfPinResultCode::InvalidPin; } + xSemaphoreTake(s_rfTransmitterMutex, portMAX_DELAY); + if (s_rfTransmitter != nullptr) { ESP_LOGV(TAG, "Destroying existing RF transmitter"); s_rfTransmitter = nullptr; } ESP_LOGV(TAG, "Creating new RF transmitter"); - auto rfxmit = std::make_unique(txPin, 32); + auto rfxmit = std::make_unique(txPin); if (!rfxmit->ok()) { ESP_LOGE(TAG, "Failed to initialize RF transmitter"); + + xSemaphoreGive(s_rfTransmitterMutex); return SetRfPinResultCode::InternalError; } if (!Config::SetRFConfigTxPin(txPin)) { ESP_LOGE(TAG, "Failed to set RF TX pin in config"); + + xSemaphoreGive(s_rfTransmitterMutex); return SetRfPinResultCode::InternalError; } s_rfTransmitter = std::move(rfxmit); + xSemaphoreGive(s_rfTransmitterMutex); return SetRfPinResultCode::Success; } +bool CommandHandler::SetKeepAliveEnabled(bool enabled) { + if (!_internalSetKeepAliveEnabled(enabled)) { + return false; + } + + if (!Config::SetRFConfigKeepAliveEnabled(enabled)) { + ESP_LOGE(TAG, "Failed to set keep-alive enabled in config"); + return false; + } + + return true; +} + +bool CommandHandler::SetKeepAlivePaused(bool paused) { + bool keepAliveEnabled = false; + if (!Config::GetRFConfigKeepAliveEnabled(keepAliveEnabled)) { + ESP_LOGE(TAG, "Failed to get keep-alive enabled from config"); + return false; + } + + if (keepAliveEnabled == false && paused == false) { + ESP_LOGW(TAG, "Keep-alive is disabled in config, ignoring unpause command"); + return false; + } + if (!_internalSetKeepAliveEnabled(!paused)) { + return false; + } + + return true; +} + std::uint8_t CommandHandler::GetRfTxPin() { - return Config::GetRFConfig().txPin; + std::uint8_t txPin; + if (!Config::GetRFConfigTxPin(txPin)) { + ESP_LOGE(TAG, "Failed to get RF TX pin from config"); + txPin = Constants::GPIO_INVALID; + } + + return txPin; } bool CommandHandler::HandleCommand(ShockerModelType model, std::uint16_t shockerId, ShockerCommandType type, std::uint8_t intensity, std::uint16_t durationMs) { + xSemaphoreTake(s_rfTransmitterMutex, portMAX_DELAY); + if (s_rfTransmitter == nullptr) { ESP_LOGW(TAG, "RF Transmitter is not initialized, ignoring command"); + + xSemaphoreGive(s_rfTransmitterMutex); return false; } @@ -83,8 +296,22 @@ bool CommandHandler::HandleCommand(ShockerModelType model, std::uint16_t shocker s_rfTransmitter->ClearPendingCommands(); } else { - ESP_LOGV(TAG, "Command received: %u %u %u %u", model, shockerId, type, intensity); + ESP_LOGD(TAG, "Command received: %u %u %u %u", model, shockerId, type, intensity); } - return s_rfTransmitter->SendCommand(model, shockerId, type, intensity, durationMs); + bool ok = s_rfTransmitter->SendCommand(model, shockerId, type, intensity, durationMs); + + xSemaphoreGive(s_rfTransmitterMutex); + xSemaphoreTake(s_keepAliveMutex, portMAX_DELAY); + + if (ok && s_keepAliveQueue != nullptr) { + KnownShocker cmd {.model = model, .shockerId = shockerId, .lastActivityTimestamp = OpenShock::millis() + durationMs}; + if (xQueueSend(s_keepAliveQueue, &cmd, pdMS_TO_TICKS(10)) != pdTRUE) { + ESP_LOGE(TAG, "Failed to send keep-alive command to queue"); + } + } + + xSemaphoreGive(s_keepAliveMutex); + + return ok; } diff --git a/src/CompatibilityChecks.cpp b/src/CompatibilityChecks.cpp new file mode 100644 index 00000000..87ae9385 --- /dev/null +++ b/src/CompatibilityChecks.cpp @@ -0,0 +1,17 @@ +#include "Common.h" +#include "Chipset.h" + +constexpr bool kIsValidOrUndefinedRfTxPin = OpenShock::IsValidOutputPin(OpenShock::Constants::GPIO_RF_TX) || OpenShock::Constants::GPIO_RF_TX == OpenShock::Constants::GPIO_INVALID; +static_assert(kIsValidOrUndefinedRfTxPin , "OPENSHOCK_RF_TX_GPIO is not a valid output GPIO, and is not declared as bypassed by board specific definitions, refusing to compile"); + +#ifdef OPENSHOCK_ESTOP_PIN +static_assert(OpenShock::IsValidInputPin(OPENSHOCK_ESTOP_PIN), "OPENSHOCK_ESTOP_PIN is not a valid input GPIO, and is not declared as bypassed by board specific definitions, refusing to compile"); +#endif + +#ifdef OPENSHOCK_LED_GPIO +static_assert(OpenShock::IsValidOutputPin(OPENSHOCK_LED_GPIO), "OPENSHOCK_LED_GPIO is not a valid output GPIO, and is not declared as bypassed by board specific definitions, refusing to compile"); +#endif + +#ifdef OPENSHOCK_LED_WS2812B +static_assert(OpenShock::IsValidOutputPin(OPENSHOCK_LED_WS2812B), "OPENSHOCK_LED_WS2812B is not a valid output GPIO, and is not declared as bypassed by board specific definitions, refusing to compile"); +#endif diff --git a/src/Config.cpp b/src/Config.cpp deleted file mode 100644 index 618212ef..00000000 --- a/src/Config.cpp +++ /dev/null @@ -1,476 +0,0 @@ -#include "Config.h" - -#include "Constants.h" -#include "Logging.h" -#include "Utils/HexUtils.h" - -#include - -const char* const TAG = "Config"; - -using namespace OpenShock; - -struct MainConfig { - Config::RFConfig rf; - Config::WiFiConfig wifi; - Config::CaptivePortalConfig captivePortal; - Config::BackendConfig backend; -}; - -static MainConfig _mainConfig; - -bool ReadFbsConfig(const Serialization::Configuration::RFConfig* fbsConfig) { - if (fbsConfig == nullptr) { - ESP_LOGE(TAG, "Config::RF is null"); - return false; - } - - _mainConfig.rf = { - .txPin = fbsConfig->tx_pin(), - }; - - return true; -} -bool ReadFbsConfig(const Serialization::Configuration::WiFiConfig* cfg) { - if (cfg == nullptr) { - ESP_LOGE(TAG, "Config::WiFi is null"); - return false; - } - - auto fbsApSsid = cfg->ap_ssid(); - auto fbsHostname = cfg->hostname(); - auto fbsCredsVec = cfg->credentials(); - - if (fbsApSsid == nullptr || fbsHostname == nullptr || fbsCredsVec == nullptr) { - ESP_LOGE(TAG, "Config::WiFi::apSsid, Config::WiFi::hostname or Config::WiFi::credentials is null"); - return false; - } - - std::vector credentials; - credentials.reserve(fbsCredsVec->size()); - - for (auto it = fbsCredsVec->begin(); it != fbsCredsVec->end(); ++it) { - auto fbsCreds = *it; - - if (fbsCreds == nullptr) { - ESP_LOGE(TAG, "Config::WiFi::credentials contains null entry"); - continue; - } - - auto id = fbsCreds->id(); - if (id == 0 || id > 32) { - ESP_LOGE(TAG, "Config::WiFi::credentials entry %u has invalid ID (must be 1-32)", id); - continue; - } - - auto ssid = fbsCreds->ssid(); - if (ssid == nullptr) { - ESP_LOGE(TAG, "Config::WiFi::credentials entry has null SSID"); - continue; - } - - auto bssid = fbsCreds->bssid(); - if (bssid == nullptr) { - ESP_LOGE(TAG, "Config::WiFi::credentials entry has null BSSID"); - continue; - } - - auto password = fbsCreds->password(); - if (password == nullptr) { - ESP_LOGE(TAG, "Config::WiFi::credentials entry has null password"); - continue; - } - - auto bssidArray = bssid->array(); - if (bssidArray == nullptr) { - ESP_LOGE(TAG, "Config::WiFi::credentials entry has null BSSID array"); - continue; - } - - Config::WiFiCredentials creds { - .id = id, - .ssid = ssid->str(), - .password = password->str(), - }; - - std::memcpy(creds.bssid, bssidArray->data(), 6); - - credentials.push_back(std::move(creds)); - } - - _mainConfig.wifi = { - .apSsid = cfg->ap_ssid()->str(), - .hostname = cfg->hostname()->str(), - .credentials = std::move(credentials), - }; - - return true; -} -bool ReadFbsConfig(const Serialization::Configuration::CaptivePortalConfig* cfg) { - if (cfg == nullptr) { - ESP_LOGE(TAG, "Config::CaptivePortal is null"); - return false; - } - - _mainConfig.captivePortal = { - .alwaysEnabled = cfg->always_enabled(), - }; - - return true; -} -bool ReadFbsConfig(const Serialization::Configuration::BackendConfig* cfg) { - if (cfg == nullptr) { - ESP_LOGE(TAG, "Config::Backend is null"); - return false; - } - - auto authToken = cfg->auth_token(); - - if (authToken == nullptr) { - ESP_LOGE(TAG, "Config::Backend::authToken is null"); - return false; - } - - _mainConfig.backend = { - .authToken = authToken->str(), - }; - - return true; -} -bool ReadFbsConfig(const Serialization::Configuration::Config* cfg) { - if (cfg == nullptr) { - ESP_LOGE(TAG, "Config is null"); - return false; - } - - return ReadFbsConfig(cfg->rf()) && ReadFbsConfig(cfg->wifi()) && ReadFbsConfig(cfg->captive_portal()) && ReadFbsConfig(cfg->backend()); -} - -bool _tryLoadConfig() { - File file = LittleFS.open("/config", "rb"); - if (!file) { - ESP_LOGE(TAG, "Failed to open config file for reading"); - return false; - } - - // Get file size - std::size_t size = file.size(); - - // Allocate buffer - std::vector buffer(size); - - // Read file - if (file.read(buffer.data(), buffer.size()) != buffer.size()) { - ESP_LOGE(TAG, "Failed to read config file, size mismatch"); - return false; - } - - file.close(); - - // Deserialize - auto fbsConfig = flatbuffers::GetRoot(buffer.data()); - if (fbsConfig == nullptr) { - ESP_LOGE(TAG, "Failed to get deserialization root for config file"); - return false; - } - - // Validate buffer - flatbuffers::Verifier::Options verifierOptions { - .max_size = 4096, // Should be enough - }; - flatbuffers::Verifier verifier(buffer.data(), buffer.size(), verifierOptions); - if (!fbsConfig->Verify(verifier)) { - ESP_LOGE(TAG, "Failed to verify config file integrity"); - return false; - } - - // Read config - if (!ReadFbsConfig(fbsConfig)) { - ESP_LOGE(TAG, "Failed to read config file"); - return false; - } - - return true; -} -bool _trySaveConfig() { - File file = LittleFS.open("/config", "wb"); - if (!file) { - ESP_LOGE(TAG, "Failed to open config file for writing"); - return false; - } - - auto& _rf = _mainConfig.rf; - auto& _wifi = _mainConfig.wifi; - auto& _backend = _mainConfig.backend; - auto& _captivePortal = _mainConfig.captivePortal; - - // Serialize - flatbuffers::FlatBufferBuilder builder(1024); - - auto rfConfig = Serialization::Configuration::RFConfig(_rf.txPin); - - std::vector> wifiCredentials; - for (const auto& cred : _wifi.credentials) { - auto bssid = Serialization::Configuration::BSSID(cred.bssid); - - wifiCredentials.push_back(Serialization::Configuration::CreateWiFiCredentials(builder, cred.id, builder.CreateString(cred.ssid), &bssid, builder.CreateString(cred.password))); - } - - auto wifiConfig = Serialization::Configuration::CreateWiFiConfig(builder, builder.CreateString(_wifi.apSsid), builder.CreateString(_wifi.hostname), builder.CreateVector(wifiCredentials)); - - auto backendConfig = Serialization::Configuration::CreateBackendConfig(builder, builder.CreateString(""), builder.CreateString(_backend.authToken)); - - auto captivePortalConfig = Serialization::Configuration::CaptivePortalConfig(_captivePortal.alwaysEnabled); - - auto fbsConfig = Serialization::Configuration::CreateConfig(builder, &rfConfig, wifiConfig, &captivePortalConfig, backendConfig); - - builder.Finish(fbsConfig); - - // Write file - if (file.write(builder.GetBufferPointer(), builder.GetSize()) != builder.GetSize()) { - ESP_LOGE(TAG, "Failed to write config file"); - return false; - } - - file.close(); - - return true; -} - -void Config::Init() { - if (_tryLoadConfig()) { - return; - } - - ESP_LOGW(TAG, "Failed to load config, writing default config"); - - _mainConfig = { - .rf = { -#ifdef OPENSHOCK_TX_PIN - .txPin = OPENSHOCK_TX_PIN, -#else - .txPin = Constants::GPIO_INVALID, -#endif - }, - .wifi = { - .apSsid = "", - .hostname = "", - .credentials = {}, - }, - .captivePortal = { - .alwaysEnabled = false, - }, - .backend = { - .authToken = "", - }, - }; - - if (!_trySaveConfig()) { - ESP_PANIC(TAG, "Failed to save default config. Recommend formatting microcontroller and re-flashing firmware"); - } -} - -void Config::FactoryReset() { - if (!LittleFS.remove("/config") && LittleFS.exists("/config")) { - ESP_PANIC(TAG, "Failed to remove existing config file for factory reset. Reccomend formatting microcontroller and re-flashing firmware"); - } -} - -const Config::RFConfig& Config::GetRFConfig() { - return _mainConfig.rf; -} - -const Config::WiFiConfig& Config::GetWiFiConfig() { - return _mainConfig.wifi; -} - -const std::vector& Config::GetWiFiCredentials() { - return _mainConfig.wifi.credentials; -} - -const Config::CaptivePortalConfig& Config::GetCaptivePortalConfig() { - return _mainConfig.captivePortal; -} - -const Config::BackendConfig& Config::GetBackendConfig() { - return _mainConfig.backend; -} - -bool Config::SetRFConfig(const RFConfig& config) { - _mainConfig.rf = config; - return _trySaveConfig(); -} - -bool Config::SetWiFiConfig(const WiFiConfig& config) { - _mainConfig.wifi = config; - return _trySaveConfig(); -} - -bool Config::SetWiFiCredentials(const std::vector& credentials) { - for (auto& cred : credentials) { - if (cred.id == 0 || cred.id > 32) { - ESP_LOGE(TAG, "Cannot set WiFi credentials: credential ID %u is invalid (must be 1-32)", cred.id); - return false; - } - } - - _mainConfig.wifi.credentials = credentials; - return _trySaveConfig(); -} - -bool Config::SetCaptivePortalConfig(const CaptivePortalConfig& config) { - _mainConfig.captivePortal = config; - return _trySaveConfig(); -} - -bool Config::SetBackendConfig(const BackendConfig& config) { - _mainConfig.backend = config; - return _trySaveConfig(); -} - -bool Config::SetRFConfigTxPin(std::uint8_t txPin) { - _mainConfig.rf.txPin = txPin; - return _trySaveConfig(); -} - -std::uint8_t Config::AddWiFiCredentials(const std::string& ssid, const std::uint8_t (&bssid)[6], const std::string& password) { - std::uint8_t id = 0; - - // Bitmask representing available credential IDs (0-31) - std::uint32_t bits = 0; - for (auto& creds : _mainConfig.wifi.credentials) { - if (creds.ssid == ssid) { - creds.password = password; - - id = creds.id; - - break; - } - - // Mark the credential ID as used - bits |= 1u << creds.id; - } - - if (id == 0) { - id = 1; - while (bits & (1u << (id - 1)) && id <= 32) { - id++; - } - - if (id > 32) { - ESP_LOGE(TAG, "Cannot add WiFi credentials: too many credentials"); - return 0; - } - - WiFiCredentials creds { - .id = id, - .ssid = ssid, - .password = password, - }; - - memcpy(creds.bssid, bssid, 6); - - _mainConfig.wifi.credentials.push_back(creds); - } - - _trySaveConfig(); - - return id; -} - -bool Config::TryGetWiFiCredentialsByID(std::uint8_t id, Config::WiFiCredentials& credentials) { - for (auto& creds : _mainConfig.wifi.credentials) { - if (creds.id == id) { - credentials = creds; - return true; - } - } - - return false; -} - -bool Config::TryGetWiFiCredentialsBySSID(const char* ssid, Config::WiFiCredentials& credentials) { - for (auto& creds : _mainConfig.wifi.credentials) { - if (creds.ssid == ssid) { - credentials = creds; - return true; - } - } - - return false; -} - -bool Config::TryGetWiFiCredentialsByBSSID(const std::uint8_t (&bssid)[6], Config::WiFiCredentials& credentials) { - for (auto& creds : _mainConfig.wifi.credentials) { - if (memcmp(creds.bssid, bssid, 6) == 0) { - credentials = creds; - return true; - } - } - - return false; -} - -std::uint8_t Config::GetWiFiCredentialsIDbySSID(const char* ssid) { - for (auto& creds : _mainConfig.wifi.credentials) { - if (creds.ssid == ssid) { - return creds.id; - } - } - - return 0; -} - -std::uint8_t Config::GetWiFiCredentialsIDbyBSSID(const std::uint8_t (&bssid)[6]) { - for (auto& creds : _mainConfig.wifi.credentials) { - if (memcmp(creds.bssid, bssid, 6) == 0) { - return creds.id; - } - } - - return 0; -} - -std::uint8_t Config::GetWiFiCredentialsIDbyBSSIDorSSID(const std::uint8_t (&bssid)[6], const char* ssid) { - std::uint8_t id = GetWiFiCredentialsIDbyBSSID(bssid); - if (id != 0) { - return id; - } - - return GetWiFiCredentialsIDbySSID(ssid); -} - -bool Config::RemoveWiFiCredentials(std::uint8_t id) { - for (auto it = _mainConfig.wifi.credentials.begin(); it != _mainConfig.wifi.credentials.end(); ++it) { - if (it->id == id) { - _mainConfig.wifi.credentials.erase(it); - _trySaveConfig(); - return true; - } - } - - return false; -} - -void Config::ClearWiFiCredentials() { - _mainConfig.wifi.credentials.clear(); - _trySaveConfig(); -} - -bool Config::HasBackendAuthToken() { - return !_mainConfig.backend.authToken.empty(); -} - -const std::string& Config::GetBackendAuthToken() { - return _mainConfig.backend.authToken; -} - -bool Config::SetBackendAuthToken(const std::string& token) { - _mainConfig.backend.authToken = token; - return _trySaveConfig(); -} - -bool Config::ClearBackendAuthToken() { - _mainConfig.backend.authToken = ""; - return _trySaveConfig(); -} diff --git a/src/EStopManager.cpp b/src/EStopManager.cpp index 39da390e..22e0d7fa 100644 --- a/src/EStopManager.cpp +++ b/src/EStopManager.cpp @@ -1,7 +1,9 @@ #include "EStopManager.h" -#include "Time.h" +#include "CommandHandler.h" +#include "config/Config.h" #include "Logging.h" +#include "Time.h" #include "VisualStateManager.h" #include @@ -10,48 +12,18 @@ const char* const TAG = "EStopManager"; using namespace OpenShock; -static EStopManager::EStopStatus s_estopStatus = EStopManager::EStopStatus::ALL_CLEAR; -static std::uint32_t s_estopHoldToClearTime = 5000; -static std::int64_t s_lastEStopButtonStateChange = 0; -static std::int64_t s_estoppedAt = 0; -static bool s_lastEStopButtonState = HIGH; +static EStopManager::EStopStatus s_estopStatus = EStopManager::EStopStatus::ALL_CLEAR; +static std::uint32_t s_estopHoldToClearTime = 5000; +static std::int64_t s_lastEStopButtonStateChange = 0; +static std::int64_t s_estoppedAt = 0; +static int s_lastEStopButtonState = HIGH; static std::uint8_t s_estopPin; -// TODO?: Allow active HIGH EStops? -void EStopManager::Init() { -#ifdef OPENSHOCK_ESTOP_PIN - s_estopPin = OPENSHOCK_ESTOP_PIN; - pinMode(s_estopPin, INPUT_PULLUP); - ESP_LOGI(TAG, "Initializing on pin %u", s_estopPin); -#else - ESP_LOGI(TAG, "EStopManager disabled, no pin defined"); -#endif -} - -bool EStopManager::IsEStopped() { -#ifdef OPENSHOCK_ESTOP_PIN - return (s_estopStatus != EStopManager::EStopStatus::ALL_CLEAR); -#else - return false; -#endif -} - -std::int64_t EStopManager::WhenEStopped() { -#ifdef OPENSHOCK_ESTOP_PIN - if (IsEStopped()) { - return s_estoppedAt; - } else { - return 0; - } -#else - return 0; -#endif -} +void _estopManagerTask(TimerHandle_t xTimer) { + configASSERT(xTimer); -EStopManager::EStopStatus EStopManager::Update() { -#ifdef OPENSHOCK_ESTOP_PIN - bool buttonState = digitalRead(s_estopPin); + int buttonState = digitalRead(s_estopPin); if (buttonState != s_lastEStopButtonState) { s_lastEStopButtonStateChange = OpenShock::millis(); } @@ -60,29 +32,33 @@ EStopManager::EStopStatus EStopManager::Update() { if (buttonState == LOW) { s_estopStatus = EStopManager::EStopStatus::ESTOPPED_AND_HELD; s_estoppedAt = s_lastEStopButtonStateChange; - ESP_LOGI(TAG, "Emergency Stopped!!!"); - OpenShock::VisualStateManager::SetEmergencyStop(true); + ESP_LOGI(TAG, "EStop triggered"); + OpenShock::VisualStateManager::SetEmergencyStop(s_estopStatus); + OpenShock::CommandHandler::SetKeepAlivePaused(true); } break; case EStopManager::EStopStatus::ESTOPPED_AND_HELD: if (buttonState == HIGH) { // User has released the button, now we can trust them holding to clear it. s_estopStatus = EStopManager::EStopStatus::ESTOPPED; + OpenShock::VisualStateManager::SetEmergencyStop(s_estopStatus); } break; case EStopManager::EStopStatus::ESTOPPED: // If the button is held again for the specified time after being released, clear the EStop if (buttonState == LOW && s_lastEStopButtonState == LOW && s_lastEStopButtonStateChange + s_estopHoldToClearTime <= OpenShock::millis()) { s_estopStatus = EStopManager::EStopStatus::ESTOPPED_CLEARED; - ESP_LOGI(TAG, "Clearing EStop on button release!"); - OpenShock::VisualStateManager::SetEmergencyStop(false); + ESP_LOGI(TAG, "EStop cleared"); + OpenShock::VisualStateManager::SetEmergencyStop(s_estopStatus); } break; case EStopManager::EStopStatus::ESTOPPED_CLEARED: // If the button is released, report as ALL_CLEAR if (buttonState == HIGH) { s_estopStatus = EStopManager::EStopStatus::ALL_CLEAR; - ESP_LOGI(TAG, "All clear!"); + ESP_LOGI(TAG, "EStop cleared, all clear"); + OpenShock::VisualStateManager::SetEmergencyStop(s_estopStatus); + OpenShock::CommandHandler::SetKeepAlivePaused(false); } break; @@ -91,9 +67,39 @@ EStopManager::EStopStatus EStopManager::Update() { } s_lastEStopButtonState = buttonState; +} + +// TODO?: Allow active HIGH EStops? +void EStopManager::Init(std::uint16_t updateIntervalMs) { +#ifdef OPENSHOCK_ESTOP_PIN + s_estopPin = OPENSHOCK_ESTOP_PIN; + pinMode(s_estopPin, INPUT_PULLUP); + ESP_LOGI(TAG, "Initializing on pin %u", s_estopPin); + + // Start the repeating task, 10Hz may seem slow, but it's plenty fast for an EStop + TimerHandle_t timer = xTimerCreate(TAG, pdMS_TO_TICKS(updateIntervalMs), pdTRUE, nullptr, _estopManagerTask); + if (timer == nullptr) { + ESP_LOGE(TAG, "Failed to create timer!!! Triggering EStop."); + s_estopStatus = EStopManager::EStopStatus::ESTOPPED; + } else { + xTimerStart(timer, 0); + } - return s_estopStatus; #else - return EStopManager::EStopStatus::ALL_CLEAR; + (void)updateIntervalMs; + + ESP_LOGI(TAG, "EStopManager disabled, no pin defined"); #endif } + +bool EStopManager::IsEStopped() { + return (s_estopStatus != EStopManager::EStopStatus::ALL_CLEAR); +} + +std::int64_t EStopManager::WhenEStopped() { + if (IsEStopped()) { + return s_estoppedAt; + } + + return 0; +} diff --git a/src/GatewayClient.cpp b/src/GatewayClient.cpp index 317b27c3..5e330371 100644 --- a/src/GatewayClient.cpp +++ b/src/GatewayClient.cpp @@ -1,16 +1,20 @@ #include "GatewayClient.h" -#include "CertificateUtils.h" +#include "Common.h" +#include "config/Config.h" #include "event_handlers/WebSocket.h" #include "Logging.h" +#include "OtaUpdateManager.h" +#include "serialization/WSGateway.h" #include "Time.h" - -#include "serialization/_fbs/DeviceToServerMessage_generated.h" +#include "util/CertificateUtils.h" const char* const TAG = "GatewayClient"; using namespace OpenShock; +static bool s_bootStatusSent = false; + GatewayClient::GatewayClient(const std::string& authToken) : m_webSocket(), m_lastKeepAlive(0), m_state(State::Disconnected) { ESP_LOGD(TAG, "Creating GatewayClient"); @@ -18,6 +22,7 @@ GatewayClient::GatewayClient(const std::string& authToken) : m_webSocket(), m_la "Device-Token: " + authToken; + m_webSocket.setUserAgent(OpenShock::Constants::FW_USERAGENT); m_webSocket.setExtraHeaders(headers.c_str()); m_webSocket.onEvent(std::bind(&GatewayClient::_handleEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); } @@ -58,6 +63,22 @@ void GatewayClient::disconnect() { m_webSocket.disconnect(); } +bool GatewayClient::sendMessageTXT(StringView data) { + if (m_state != State::Connected) { + return false; + } + + return m_webSocket.sendTXT(data.data(), data.length()); +} + +bool GatewayClient::sendMessageBIN(const std::uint8_t* data, std::size_t length) { + if (m_state != State::Connected) { + return false; + } + + return m_webSocket.sendBIN(data, length); +} + bool GatewayClient::loop() { if (m_state == State::Disconnected) { return false; @@ -84,23 +105,45 @@ bool GatewayClient::loop() { } void GatewayClient::_sendKeepAlive() { - ESP_LOGV(TAG, "Sending keep alive message"); + ESP_LOGV(TAG, "Sending Gateway keep-alive message"); + Serialization::Gateway::SerializeKeepAliveMessage([this](const std::uint8_t* data, std::size_t len) { return m_webSocket.sendBIN(data, len); }); +} - // Casting to uint64 here is safe since millis is guaranteed to return a positive value - OpenShock::Serialization::KeepAlive keepAlive((std::uint64_t)OpenShock::millis()); +void GatewayClient::_sendBootStatus() { + if (s_bootStatusSent) return; - flatbuffers::FlatBufferBuilder builder(64); + ESP_LOGV(TAG, "Sending Gateway boot status message"); + + std::int32_t updateId; + if (!Config::GetOtaUpdateId(updateId)) { + ESP_LOGE(TAG, "Failed to get OTA update ID"); + return; + } - auto keepAliveOffset = builder.CreateStruct(keepAlive); + OpenShock::OtaUpdateStep updateStep; + if (!Config::GetOtaUpdateStep(updateStep)) { + ESP_LOGE(TAG, "Failed to get OTA firmware boot type"); + return; + } - auto msg = OpenShock::Serialization::CreateDeviceToServerMessage(builder, OpenShock::Serialization::DeviceToServerMessagePayload::KeepAlive, keepAliveOffset.Union()); + OpenShock::SemVer version; + if (!OpenShock::TryParseSemVer(OPENSHOCK_FW_VERSION, version)) { + ESP_LOGE(TAG, "Failed to parse firmware version"); + return; + } - builder.Finish(msg); + s_bootStatusSent = Serialization::Gateway::SerializeBootStatusMessage(updateId, OtaUpdateManager::GetFirmwareBootType(), version, [this](const std::uint8_t* data, std::size_t len) { return m_webSocket.sendBIN(data, len); }); - m_webSocket.sendBIN(builder.GetBufferPointer(), builder.GetSize()); + if (s_bootStatusSent && updateStep != OpenShock::OtaUpdateStep::None) { + if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::None)) { + ESP_LOGE(TAG, "Failed to reset firmware boot type to normal"); + } + } } void GatewayClient::_handleEvent(WStype_t type, std::uint8_t* payload, std::size_t length) { + (void)payload; + switch (type) { case WStype_DISCONNECTED: ESP_LOGI(TAG, "Disconnected from API"); @@ -110,6 +153,7 @@ void GatewayClient::_handleEvent(WStype_t type, std::uint8_t* payload, std::size ESP_LOGI(TAG, "Connected to API"); m_state = State::Connected; _sendKeepAlive(); + _sendBootStatus(); break; case WStype_TEXT: ESP_LOGW(TAG, "Received text from API, JSON parsing is not supported anymore :D"); diff --git a/src/GatewayConnectionManager.cpp b/src/GatewayConnectionManager.cpp index 392f2e79..5a0ee78a 100644 --- a/src/GatewayConnectionManager.cpp +++ b/src/GatewayConnectionManager.cpp @@ -2,16 +2,12 @@ #include "VisualStateManager.h" -#include "Config.h" -#include "Constants.h" +#include "config/Config.h" #include "GatewayClient.h" +#include "http/JsonAPI.h" #include "Logging.h" #include "Time.h" -#include -#include - -#include #include // @@ -31,19 +27,25 @@ static const char* const TAG = "GatewayConnectionManager"; static const char* const AUTH_TOKEN_FILE = "/authToken"; -constexpr std::uint8_t FLAG_NONE = 0; -constexpr std::uint8_t FLAG_HAS_IP = 1 << 0; -constexpr std::uint8_t FLAG_AUTHENTICATED = 1 << 1; +constexpr std::uint8_t FLAG_NONE = 0; +constexpr std::uint8_t FLAG_HAS_IP = 1 << 0; +constexpr std::uint8_t FLAG_LINKED = 1 << 1; + +constexpr std::uint8_t LINK_CODE_LENGTH = 6; static std::uint8_t s_flags = 0; static std::unique_ptr s_wsClient = nullptr; void _evGotIPHandler(arduino_event_t* event) { + (void)event; + s_flags |= FLAG_HAS_IP; ESP_LOGD(TAG, "Got IP address"); } void _evWiFiDisconnectedHandler(arduino_event_t* event) { + (void)event; + s_flags = FLAG_NONE; s_wsClient = nullptr; ESP_LOGD(TAG, "Lost IP address"); @@ -51,6 +53,7 @@ void _evWiFiDisconnectedHandler(arduino_event_t* event) { } using namespace OpenShock; +namespace JsonAPI = OpenShock::Serialization::JsonAPI; bool GatewayConnectionManager::Init() { WiFi.onEvent(_evGotIPHandler, ARDUINO_EVENT_WIFI_STA_GOT_IP); @@ -68,70 +71,44 @@ bool GatewayConnectionManager::IsConnected() { return s_wsClient->state() == GatewayClient::State::Connected; } -void GetDeviceInfoFromJsonResponse(HTTPClient& client) { - ArduinoJson::DynamicJsonDocument doc(1024); // TODO: profile the normal message size and adjust this accordingly - deserializeJson(doc, client.getString()); - - auto data = doc["data"]; - String id = data["id"]; - String name = data["name"]; - - ESP_LOGD(TAG, "Device ID: %s", id.c_str()); - ESP_LOGD(TAG, "Device name: %s", name.c_str()); - - auto shockers = data["shockers"]; - for (int i = 0; i < shockers.size(); i++) { - auto shocker = shockers[i]; - String shockerId = shocker["id"]; - std::uint16_t shockerRfId = shocker["rfId"]; - std::uint8_t shockerModel = shocker["model"]; - - ESP_LOGD(TAG, "Found shocker %s with RF ID %u and model %u", shockerId.c_str(), shockerRfId, shockerModel); - } -} - -bool GatewayConnectionManager::IsPaired() { - return (s_flags & FLAG_AUTHENTICATED) != 0; -} - -// This method is here to heap usage -std::string GetAuthTokenFromJsonResponse(HTTPClient& client) { - ArduinoJson::DynamicJsonDocument doc(1024); // TODO: profile the normal message size and adjust this accordingly - deserializeJson(doc, client.getString()); - - String str = doc["data"]; - - return std::string(str.c_str(), str.length()); +bool GatewayConnectionManager::IsLinked() { + return (s_flags & FLAG_LINKED) != 0; } -AccountLinkResultCode GatewayConnectionManager::Pair(const char* pairCode) { +AccountLinkResultCode GatewayConnectionManager::Link(const char* linkCode) { if ((s_flags & FLAG_HAS_IP) == 0) { return AccountLinkResultCode::NoInternetConnection; } s_wsClient = nullptr; - ESP_LOGD(TAG, "Attempting to pair with pair code %s", pairCode); + ESP_LOGD(TAG, "Attempting to link to account using code %s", linkCode); - HTTPClient client; + if (std::strlen(linkCode) != LINK_CODE_LENGTH) { + ESP_LOGE(TAG, "Invalid link code length"); + return AccountLinkResultCode::InvalidCode; + } - char uri[256]; - sprintf(uri, OPENSHOCK_API_URL("/1/device/pair/%s"), pairCode); + auto response = HTTP::JsonAPI::LinkAccount(linkCode); - client.begin(uri); + if (response.result == HTTP::RequestResult::RateLimited) { + return AccountLinkResultCode::InternalError; // Just return false, don't spam the console with errors + } + if (response.result != HTTP::RequestResult::Success) { + ESP_LOGE(TAG, "Error while getting auth token: %d %d", response.result, response.code); - int responseCode = client.GET(); + return AccountLinkResultCode::InternalError; + } - if (responseCode == 404) { + if (response.code == 404) { return AccountLinkResultCode::InvalidCode; } - if (responseCode != 200) { - ESP_LOGE(TAG, "Error while getting auth token: [%d] %s", responseCode, client.getString().c_str()); + + if (response.code != 200) { + ESP_LOGE(TAG, "Unexpected response code: %d", response.code); return AccountLinkResultCode::InternalError; } - std::string authToken = GetAuthTokenFromJsonResponse(client); - - client.end(); + const std::string& authToken = response.data.authToken; if (authToken.empty()) { ESP_LOGE(TAG, "Received empty auth token"); @@ -143,47 +120,68 @@ AccountLinkResultCode GatewayConnectionManager::Pair(const char* pairCode) { return AccountLinkResultCode::InternalError; } - s_flags |= FLAG_AUTHENTICATED; - ESP_LOGD(TAG, "Successfully paired with pair code %u", pairCode); + s_flags |= FLAG_LINKED; + ESP_LOGD(TAG, "Successfully linked to account"); return AccountLinkResultCode::Success; } -void GatewayConnectionManager::UnPair() { +void GatewayConnectionManager::UnLink() { s_flags &= FLAG_HAS_IP; s_wsClient = nullptr; Config::ClearBackendAuthToken(); } -bool FetchDeviceInfo(const std::string& authToken) { - // TODO: this function is very slow, should be optimized! - if ((s_flags & FLAG_HAS_IP) == 0) { +bool GatewayConnectionManager::SendMessageTXT(StringView data) { + if (s_wsClient == nullptr) { return false; } - HTTPClient client; + return s_wsClient->sendMessageTXT(data); +} - client.begin(OPENSHOCK_API_URL("/1/device/self")); +bool GatewayConnectionManager::SendMessageBIN(const std::uint8_t* data, std::size_t length) { + if (s_wsClient == nullptr) { + return false; + } + + return s_wsClient->sendMessageBIN(data, length); +} + +bool FetchDeviceInfo(const String& authToken) { + // TODO: this function is very slow, should be optimized! + if ((s_flags & FLAG_HAS_IP) == 0) { + return false; + } - client.addHeader("DeviceToken", authToken.c_str()); + auto response = HTTP::JsonAPI::GetDeviceInfo(authToken); - int responseCode = client.GET(); + if (response.result == HTTP::RequestResult::RateLimited) { + return false; // Just return false, don't spam the console with errors + } + if (response.result != HTTP::RequestResult::Success) { + ESP_LOGE(TAG, "Error while fetching device info: %d %d", response.result, response.code); + return false; + } - if (responseCode == 401) { + if (response.code == 401) { ESP_LOGD(TAG, "Auth token is invalid, clearing it"); Config::ClearBackendAuthToken(); return false; } - if (responseCode != 200) { - ESP_LOGE(TAG, "Error while verifying auth token: [%d] %s", responseCode, client.getString().c_str()); + if (response.code != 200) { + ESP_LOGE(TAG, "Unexpected response code: %d", response.code); return false; } - GetDeviceInfoFromJsonResponse(client); - - client.end(); + ESP_LOGI(TAG, "Device ID: %s", response.data.deviceId.c_str()); + ESP_LOGI(TAG, "Device Name: %s", response.data.deviceName.c_str()); + ESP_LOGI(TAG, "Shockers:"); + for (auto& shocker : response.data.shockers) { + ESP_LOGI(TAG, " [%s] rf=%u model=%u", shocker.id.c_str(), shocker.rfId, shocker.model); + } - s_flags |= FLAG_AUTHENTICATED; + s_flags |= FLAG_LINKED; return true; } @@ -214,37 +212,35 @@ bool ConnectToLCG() { return false; } - std::string authToken = Config::GetBackendAuthToken(); - - HTTPClient client; - - client.begin(OPENSHOCK_API_URL("/1/device/assignLCG")); - - client.addHeader("DeviceToken", authToken.c_str()); - - int responseCode = client.GET(); - - if (responseCode != 200) { - ESP_LOGE(TAG, "Error while fetching LCG endpoint: [%d] %s", responseCode, client.getString().c_str()); + std::string authToken; + if (!Config::GetBackendAuthToken(authToken)) { + ESP_LOGE(TAG, "Failed to get auth token"); return false; } - ArduinoJson::DynamicJsonDocument doc(1024); // TODO: profile the normal message size and adjust this accordingly - deserializeJson(doc, client.getString()); + auto response = HTTP::JsonAPI::AssignLcg(authToken.c_str()); - auto data = doc["data"]; - const char* fqdn = data["fqdn"]; - const char* country = data["country"]; + if (response.result == HTTP::RequestResult::RateLimited) { + return false; // Just return false, don't spam the console with errors + } + if (response.result != HTTP::RequestResult::Success) { + ESP_LOGE(TAG, "Error while fetching LCG endpoint: %d %d", response.result, response.code); + return false; + } - client.end(); + if (response.code == 401) { + ESP_LOGD(TAG, "Auth token is invalid, clearing it"); + Config::ClearBackendAuthToken(); + return false; + } - if (fqdn == nullptr || country == nullptr) { - ESP_LOGE(TAG, "Received invalid response from LCG endpoint"); + if (response.code != 200) { + ESP_LOGE(TAG, "Unexpected response code: %d", response.code); return false; } - ESP_LOGD(TAG, "Connecting to LCG endpoint %s in country %s", fqdn, country); - s_wsClient->connect(fqdn); + ESP_LOGD(TAG, "Connecting to LCG endpoint %s in country %s", response.data.fqdn.c_str(), response.data.country.c_str()); + s_wsClient->connect(response.data.fqdn.c_str()); return true; } @@ -256,14 +252,18 @@ void GatewayConnectionManager::Update() { return; } - std::string authToken = Config::GetBackendAuthToken(); + std::string authToken; + if (!Config::GetBackendAuthToken(authToken)) { + ESP_LOGE(TAG, "Failed to get auth token"); + return; + } // Fetch device info - if (!FetchDeviceInfo(authToken)) { + if (!FetchDeviceInfo(authToken.c_str())) { return; } - s_flags |= FLAG_AUTHENTICATED; + s_flags |= FLAG_LINKED; ESP_LOGD(TAG, "Successfully verified auth token"); s_wsClient = std::make_unique(authToken); diff --git a/src/OtaUpdateManager.cpp b/src/OtaUpdateManager.cpp new file mode 100644 index 00000000..c95e9e13 --- /dev/null +++ b/src/OtaUpdateManager.cpp @@ -0,0 +1,678 @@ +#include "OtaUpdateManager.h" + +#include "CaptivePortal.h" +#include "Common.h" +#include "config/Config.h" +#include "GatewayConnectionManager.h" +#include "Hashing.h" +#include "http/HTTPRequestManager.h" +#include "Logging.h" +#include "SemVer.h" +#include "serialization/WSGateway.h" +#include "StringView.h" +#include "Time.h" +#include "util/HexUtils.h" +#include "util/PartitionUtils.h" +#include "util/StringUtils.h" +#include "util/TaskUtils.h" +#include "wifi/WiFiManager.h" + +#include + +#include +#include + +#include + +#define OPENSHOCK_FW_CDN_CHANNEL_URL(ch) OPENSHOCK_FW_CDN_URL("/version-" ch ".txt") + +#define OPENSHOCK_FW_CDN_STABLE_URL OPENSHOCK_FW_CDN_CHANNEL_URL("stable") +#define OPENSHOCK_FW_CDN_BETA_URL OPENSHOCK_FW_CDN_CHANNEL_URL("beta") +#define OPENSHOCK_FW_CDN_DEVELOP_URL OPENSHOCK_FW_CDN_CHANNEL_URL("develop") + +#define OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT OPENSHOCK_FW_CDN_URL("/%s") +#define OPENSHOCK_FW_CDN_BOARDS_INDEX_URL_FORMAT OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT "/boards.txt" + +#define OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT "/" OPENSHOCK_FW_BOARD + +#define OPENSHOCK_FW_CDN_APP_URL_FORMAT OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT "/app.bin" +#define OPENSHOCK_FW_CDN_FILESYSTEM_URL_FORMAT OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT "/staticfs.bin" +#define OPENSHOCK_FW_CDN_SHA256_HASHES_URL_FORMAT OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT "/hashes.sha256.txt" + +const char* const TAG = "OtaUpdateManager"; + +/// @brief Stops initArduino() from handling OTA rollbacks +/// @todo Get rid of Arduino entirely. >:( +/// +/// @see .platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-misc.c +/// @return true +bool verifyRollbackLater() { + return true; +} + +using namespace OpenShock; + +enum OtaTaskEventFlag : std::uint32_t { + OTA_TASK_EVENT_UPDATE_REQUESTED = 1 << 0, + OTA_TASK_EVENT_WIFI_DISCONNECTED = 1 << 1, // If both connected and disconnected are set, disconnected takes priority. + OTA_TASK_EVENT_WIFI_CONNECTED = 1 << 2, +}; + +static esp_ota_img_states_t _otaImageState; +static OpenShock::FirmwareBootType _bootType; +static TaskHandle_t _taskHandle; +static OpenShock::SemVer _requestedVersion; +static SemaphoreHandle_t _requestedVersionMutex = xSemaphoreCreateMutex(); + +bool _tryQueueUpdateRequest(const OpenShock::SemVer& version) { + if (xSemaphoreTake(_requestedVersionMutex, pdMS_TO_TICKS(1000)) != pdTRUE) { + ESP_LOGE(TAG, "Failed to take requested version mutex"); + return false; + } + + _requestedVersion = version; + + xSemaphoreGive(_requestedVersionMutex); + + xTaskNotify(_taskHandle, OTA_TASK_EVENT_UPDATE_REQUESTED, eSetBits); + + return true; +} + +bool _tryGetRequestedVersion(OpenShock::SemVer& version) { + if (xSemaphoreTake(_requestedVersionMutex, pdMS_TO_TICKS(1000)) != pdTRUE) { + ESP_LOGE(TAG, "Failed to take requested version mutex"); + return false; + } + + version = _requestedVersion; + + xSemaphoreGive(_requestedVersionMutex); + + return true; +} + +void _otaEvGotIPHandler(arduino_event_t* event) { + (void)event; + xTaskNotify(_taskHandle, OTA_TASK_EVENT_WIFI_CONNECTED, eSetBits); +} +void _otaEvWiFiDisconnectedHandler(arduino_event_t* event) { + (void)event; + xTaskNotify(_taskHandle, OTA_TASK_EVENT_WIFI_DISCONNECTED, eSetBits); +} + +bool _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask task, float progress) { + std::int32_t updateId; + if (!Config::GetOtaUpdateId(updateId)) { + ESP_LOGE(TAG, "Failed to get OTA update ID"); + return false; + } + + if (!Serialization::Gateway::SerializeOtaInstallProgressMessage(updateId, task, progress, GatewayConnectionManager::SendMessageBIN)) { + ESP_LOGE(TAG, "Failed to send OTA install progress message"); + return false; + } + + return true; +} +bool _sendFailureMessage(StringView message, bool fatal = false) { + std::int32_t updateId; + if (!Config::GetOtaUpdateId(updateId)) { + ESP_LOGE(TAG, "Failed to get OTA update ID"); + return false; + } + + if (!Serialization::Gateway::SerializeOtaInstallFailedMessage(updateId, message, fatal, GatewayConnectionManager::SendMessageBIN)) { + ESP_LOGE(TAG, "Failed to send OTA install failed message"); + return false; + } + + return true; +} + +bool _flashAppPartition(const esp_partition_t* partition, StringView remoteUrl, const std::uint8_t (&remoteHash)[32]) { + ESP_LOGD(TAG, "Flashing app partition"); + + if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FlashingApplication, 0.0f)) { + return false; + } + + auto onProgress = [](std::size_t current, std::size_t total, float progress) -> bool { + ESP_LOGD(TAG, "Flashing app partition: %u / %u (%.2f%%)", current, total, progress * 100.0f); + + _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FlashingApplication, progress); + + return true; + }; + + if (!OpenShock::FlashPartitionFromUrl(partition, remoteUrl, remoteHash, onProgress)) { + ESP_LOGE(TAG, "Failed to flash app partition"); + _sendFailureMessage("Failed to flash app partition"); + return false; + } + + if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::MarkingApplicationBootable, 0.0f)) { + return false; + } + + // Set app partition bootable. + if (esp_ota_set_boot_partition(partition) != ESP_OK) { + ESP_LOGE(TAG, "Failed to set app partition bootable"); + _sendFailureMessage("Failed to set app partition bootable"); + return false; + } + + return true; +} + +bool _flashFilesystemPartition(const esp_partition_t* parition, StringView remoteUrl, const std::uint8_t (&remoteHash)[32]) { + if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::PreparingForInstall, 0.0f)) { + return false; + } + + // Make sure captive portal is stopped, timeout after 5 seconds. + if (!CaptivePortal::ForceClose(5000U)) { + ESP_LOGE(TAG, "Failed to force close captive portal (timed out)"); + _sendFailureMessage("Failed to force close captive portal (timed out)"); + return false; + } + + ESP_LOGD(TAG, "Flashing filesystem partition"); + + if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FlashingFilesystem, 0.0f)) { + return false; + } + + auto onProgress = [](std::size_t current, std::size_t total, float progress) -> bool { + ESP_LOGD(TAG, "Flashing filesystem partition: %u / %u (%.2f%%)", current, total, progress * 100.0f); + + _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FlashingFilesystem, progress); + + return true; + }; + + if (!OpenShock::FlashPartitionFromUrl(parition, remoteUrl, remoteHash, onProgress)) { + ESP_LOGE(TAG, "Failed to flash filesystem partition"); + _sendFailureMessage("Failed to flash filesystem partition"); + return false; + } + + if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::VerifyingFilesystem, 0.0f)) { + return false; + } + + // Attempt to mount filesystem. + fs::LittleFSFS test; + if (!test.begin(false, "/static", 10, "static0")) { + ESP_LOGE(TAG, "Failed to mount filesystem"); + _sendFailureMessage("Failed to mount filesystem"); + return false; + } + test.end(); + + OpenShock::CaptivePortal::ForceClose(false); + + return true; +} + +void _otaUpdateTask(void* arg) { + (void)arg; + + ESP_LOGD(TAG, "OTA update task started"); + + bool connected = false; + bool updateRequested = false; + std::int64_t lastUpdateCheck = 0; + + // Update task loop. + while (true) { + // Wait for event. + uint32_t eventBits = 0; + xTaskNotifyWait(0, UINT32_MAX, &eventBits, pdMS_TO_TICKS(5000)); // TODO: wait for rest time + + updateRequested |= (eventBits & OTA_TASK_EVENT_UPDATE_REQUESTED) != 0; + + if ((eventBits & OTA_TASK_EVENT_WIFI_DISCONNECTED) != 0) { + ESP_LOGD(TAG, "WiFi disconnected"); + connected = false; + continue; // No further processing needed. + } + + if ((eventBits & OTA_TASK_EVENT_WIFI_CONNECTED) != 0 && !connected) { + ESP_LOGD(TAG, "WiFi connected"); + connected = true; + } + + // If we're not connected, continue. + if (!connected) { + continue; + } + + std::int64_t now = OpenShock::millis(); + + Config::OtaUpdateConfig config; + if (!Config::GetOtaUpdateConfig(config)) { + ESP_LOGE(TAG, "Failed to get OTA update config"); + continue; + } + + if (!config.isEnabled) { + ESP_LOGD(TAG, "OTA updates are disabled, skipping update check"); + continue; + } + + bool firstCheck = lastUpdateCheck == 0; + std::int64_t diff = now - lastUpdateCheck; + std::int64_t diffMins = diff / 60'000LL; + + bool check = false; + check |= config.checkOnStartup && firstCheck; // On startup + check |= config.checkPeriodically && diffMins >= config.checkInterval; // Periodically + check |= updateRequested && (firstCheck || diffMins >= 1); // Update requested + + if (!check) { + continue; + } + + lastUpdateCheck = now; + + if (config.requireManualApproval) { + ESP_LOGD(TAG, "Manual approval required, skipping update check"); + // TODO: IMPLEMENT + continue; + } + + OpenShock::SemVer version; + if (updateRequested) { + updateRequested = false; + + if (!_tryGetRequestedVersion(version)) { + ESP_LOGE(TAG, "Failed to get requested version"); + continue; + } + + ESP_LOGD(TAG, "Update requested for version %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + } else { + ESP_LOGD(TAG, "Checking for updates"); + + // Fetch current version. + if (!OtaUpdateManager::TryGetFirmwareVersion(config.updateChannel, version)) { + ESP_LOGE(TAG, "Failed to fetch firmware version"); + continue; + } + + ESP_LOGD(TAG, "Remote version: %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + } + + if (version.toString() == OPENSHOCK_FW_VERSION) { // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + ESP_LOGI(TAG, "Requested version is already installed"); + continue; + } + + // Generate random int32_t for this update. + std::int32_t updateId = static_cast(esp_random()); + if (!Config::SetOtaUpdateId(updateId)) { + ESP_LOGE(TAG, "Failed to set OTA update ID"); + continue; + } + if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::Updating)) { + ESP_LOGE(TAG, "Failed to set OTA update step"); + continue; + } + + if (!Serialization::Gateway::SerializeOtaInstallStartedMessage(updateId, version, GatewayConnectionManager::SendMessageBIN)) { + ESP_LOGE(TAG, "Failed to serialize OTA install started message"); + continue; + } + + if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FetchingMetadata, 0.0f)) { + continue; + } + + // Fetch current release. + OtaUpdateManager::FirmwareRelease release; + if (!OtaUpdateManager::TryGetFirmwareRelease(version, release)) { + ESP_LOGE(TAG, "Failed to fetch firmware release"); // TODO: Send error message to server + _sendFailureMessage("Failed to fetch firmware release"); + continue; + } + + // Print release. + ESP_LOGD(TAG, "Firmware release:"); + ESP_LOGD(TAG, " Version: %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + ESP_LOGD(TAG, " App binary URL: %s", release.appBinaryUrl.c_str()); + ESP_LOGD(TAG, " App binary hash: %s", HexUtils::ToHex<32>(release.appBinaryHash).data()); + ESP_LOGD(TAG, " Filesystem binary URL: %s", release.filesystemBinaryUrl.c_str()); + ESP_LOGD(TAG, " Filesystem binary hash: %s", HexUtils::ToHex<32>(release.filesystemBinaryHash).data()); + + // Get available app update partition. + const esp_partition_t* appPartition = esp_ota_get_next_update_partition(nullptr); + if (appPartition == nullptr) { + ESP_LOGE(TAG, "Failed to get app update partition"); // TODO: Send error message to server + _sendFailureMessage("Failed to get app update partition"); + continue; + } + + // Get filesystem partition. + const esp_partition_t* filesystemPartition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "static0"); + if (filesystemPartition == nullptr) { + ESP_LOGE(TAG, "Failed to find filesystem partition"); // TODO: Send error message to server + _sendFailureMessage("Failed to find filesystem partition"); + continue; + } + + // Flash app and filesystem partitions. + if (!_flashFilesystemPartition(filesystemPartition, release.filesystemBinaryUrl, release.filesystemBinaryHash)) continue; + if (!_flashAppPartition(appPartition, release.appBinaryUrl, release.appBinaryHash)) continue; + + // Set OTA boot type in config. + if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::Updated)) { + ESP_LOGE(TAG, "Failed to set OTA update step"); + _sendFailureMessage("Failed to set OTA update step"); + continue; + } + + // Send reboot message. + _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::Rebooting, 0.0f); + + // Reboot into new firmware. + ESP_LOGI(TAG, "Restarting into new firmware..."); + vTaskDelay(pdMS_TO_TICKS(200)); + break; + } + + // Restart. + esp_restart(); +} + +bool _tryGetStringList(StringView url, std::vector& list) { + auto response = OpenShock::HTTP::GetString( + url, + { + {"Accept", "text/plain"} + }, + {200, 304} + ); + if (response.result != OpenShock::HTTP::RequestResult::Success) { + ESP_LOGE(TAG, "Failed to fetch list: [%u] %s", response.code, response.data.c_str()); + return false; + } + + list.clear(); + + OpenShock::StringView data = response.data; + + auto lines = data.splitLines(); + list.reserve(lines.size()); + + for (auto line : lines) { + line = line.trim(); + + if (line.isNullOrEmpty()) { + continue; + } + + list.push_back(line.toString()); + } + + return true; +} + +bool OtaUpdateManager::Init() { + ESP_LOGD(TAG, "Fetching current partition"); + + // Fetch current partition info. + const esp_partition_t* partition = esp_ota_get_running_partition(); + if (partition == nullptr) { + ESP_PANIC(TAG, "Failed to get currently running partition"); + return false; // This will never be reached, but the compiler doesn't know that. + } + + ESP_LOGD(TAG, "Fetching partition state"); + + // Get OTA state for said partition. + esp_err_t err = esp_ota_get_state_partition(partition, &_otaImageState); + if (err != ESP_OK) { + ESP_PANIC(TAG, "Failed to get partition state: %s", esp_err_to_name(err)); + return false; // This will never be reached, but the compiler doesn't know that. + } + + ESP_LOGD(TAG, "Fetching previous update step"); + OtaUpdateStep updateStep; + if (!Config::GetOtaUpdateStep(updateStep)) { + ESP_LOGE(TAG, "Failed to get OTA update step"); + return false; + } + + // Infer boot type from update step. + switch (updateStep) { + case OtaUpdateStep::Updated: + _bootType = FirmwareBootType::NewFirmware; + break; + case OtaUpdateStep::Validating: // If the update step is validating, we have failed in the middle of validating the new firmware, meaning this is a rollback. + case OtaUpdateStep::RollingBack: + _bootType = FirmwareBootType::Rollback; + break; + default: + _bootType = FirmwareBootType::Normal; + break; + } + + if (updateStep == OtaUpdateStep::Updated) { + if (!Config::SetOtaUpdateStep(OtaUpdateStep::Validating)) { + ESP_PANIC(TAG, "Failed to set OTA update step in critical section"); // TODO: THIS IS A CRITICAL SECTION, WHAT DO WE DO? + } + } + + WiFi.onEvent(_otaEvGotIPHandler, ARDUINO_EVENT_WIFI_STA_GOT_IP); + WiFi.onEvent(_otaEvGotIPHandler, ARDUINO_EVENT_WIFI_STA_GOT_IP6); + WiFi.onEvent(_otaEvWiFiDisconnectedHandler, ARDUINO_EVENT_WIFI_STA_DISCONNECTED); + + // Start OTA update task. + TaskUtils::TaskCreateExpensive(_otaUpdateTask, "OTA Update", 8192, nullptr, 1, &_taskHandle); + + return true; +} + +bool OtaUpdateManager::TryGetFirmwareVersion(OtaUpdateChannel channel, OpenShock::SemVer& version) { + const char* channelIndexUrl = nullptr; + switch (channel) { + case OtaUpdateChannel::Stable: + channelIndexUrl = OPENSHOCK_FW_CDN_STABLE_URL; + break; + case OtaUpdateChannel::Beta: + channelIndexUrl = OPENSHOCK_FW_CDN_BETA_URL; + break; + case OtaUpdateChannel::Develop: + channelIndexUrl = OPENSHOCK_FW_CDN_DEVELOP_URL; + break; + default: + ESP_LOGE(TAG, "Unknown channel: %u", channel); + return false; + } + + ESP_LOGD(TAG, "Fetching firmware version from %s", channelIndexUrl); + + auto response = OpenShock::HTTP::GetString( + channelIndexUrl, + { + {"Accept", "text/plain"} + }, + {200, 304} + ); + if (response.result != OpenShock::HTTP::RequestResult::Success) { + ESP_LOGE(TAG, "Failed to fetch firmware version: [%u] %s", response.code, response.data.c_str()); + return false; + } + + if (!OpenShock::TryParseSemVer(response.data, version)) { + ESP_LOGE(TAG, "Failed to parse firmware version: %.*s", response.data.size(), response.data.data()); + return false; + } + + return true; +} + +bool OtaUpdateManager::TryGetFirmwareBoards(const OpenShock::SemVer& version, std::vector& boards) { + std::string channelIndexUrl; + if (!FormatToString(channelIndexUrl, OPENSHOCK_FW_CDN_BOARDS_INDEX_URL_FORMAT, version.toString().c_str())) { // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + ESP_LOGE(TAG, "Failed to format URL"); + return false; + } + + ESP_LOGD(TAG, "Fetching firmware boards from %s", channelIndexUrl.c_str()); + + if (!_tryGetStringList(channelIndexUrl.c_str(), boards)) { + ESP_LOGE(TAG, "Failed to fetch firmware boards"); + return false; + } + + return true; +} + +bool _tryParseIntoHash(const std::string& hash, std::uint8_t (&hashBytes)[32]) { + if (!HexUtils::TryParseHex(hash.data(), hash.size(), hashBytes, 32)) { + ESP_LOGE(TAG, "Failed to parse hash: %.*s", hash.size(), hash.data()); + return false; + } + + return true; +} + +bool OtaUpdateManager::TryGetFirmwareRelease(const OpenShock::SemVer& version, FirmwareRelease& release) { + auto versionStr = version.toString(); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + + if (!FormatToString(release.appBinaryUrl, OPENSHOCK_FW_CDN_APP_URL_FORMAT, versionStr.c_str())) { + ESP_LOGE(TAG, "Failed to format URL"); + return false; + } + + if (!FormatToString(release.filesystemBinaryUrl, OPENSHOCK_FW_CDN_FILESYSTEM_URL_FORMAT, versionStr.c_str())) { + ESP_LOGE(TAG, "Failed to format URL"); + return false; + } + + // Construct hash URLs. + std::string sha256HashesUrl; + if (!FormatToString(sha256HashesUrl, OPENSHOCK_FW_CDN_SHA256_HASHES_URL_FORMAT, versionStr.c_str())) { + ESP_LOGE(TAG, "Failed to format URL"); + return false; + } + + // Fetch hashes. + auto sha256HashesResponse = OpenShock::HTTP::GetString( + sha256HashesUrl.c_str(), + { + {"Accept", "text/plain"} + }, + {200, 304} + ); + if (sha256HashesResponse.result != OpenShock::HTTP::RequestResult::Success) { + ESP_LOGE(TAG, "Failed to fetch hashes: [%u] %s", sha256HashesResponse.code, sha256HashesResponse.data.c_str()); + return false; + } + + auto hashesLines = OpenShock::StringView(sha256HashesResponse.data).splitLines(); + + // Parse hashes. + bool foundAppHash = false, foundFilesystemHash = false; + for (auto line : hashesLines) { + auto parts = line.splitWhitespace(); + if (parts.size() != 2) { + ESP_LOGE(TAG, "Invalid hashes entry: %.*s", line.size(), line.data()); + return false; + } + + auto hash = parts[0].trim(); + auto file = parts[1].trim(); + + if (file.startsWith("./")) { + file = file.substr(2); + } + + if (hash.size() != 64) { + ESP_LOGE(TAG, "Invalid hash: %.*s", hash.size(), hash.data()); + return false; + } + + if (file == "app.bin") { + if (foundAppHash) { + ESP_LOGE(TAG, "Duplicate hash for app.bin"); + return false; + } + + if (!_tryParseIntoHash(hash.toString(), release.appBinaryHash)) { + return false; + } + + foundAppHash = true; + } else if (file == "staticfs.bin") { + if (foundFilesystemHash) { + ESP_LOGE(TAG, "Duplicate hash for staticfs.bin"); + return false; + } + + if (!_tryParseIntoHash(hash.toString(), release.filesystemBinaryHash)) { + return false; + } + + foundFilesystemHash = true; + } + } + + return true; +} + +bool OtaUpdateManager::TryStartFirmwareInstallation(const OpenShock::SemVer& version) { + ESP_LOGD(TAG, "Requesting firmware version %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + + return _tryQueueUpdateRequest(version); +} + +FirmwareBootType OtaUpdateManager::GetFirmwareBootType() { + return _bootType; +} + +bool OtaUpdateManager::IsValidatingApp() { + return _otaImageState == ESP_OTA_IMG_PENDING_VERIFY; +} + +void OtaUpdateManager::InvalidateAndRollback() { + // Set OTA boot type in config. + if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::RollingBack)) { + ESP_PANIC(TAG, "Failed to set OTA firmware boot type in critical section"); // TODO: THIS IS A CRITICAL SECTION, WHAT DO WE DO? + return; + } + + switch (esp_ota_mark_app_invalid_rollback_and_reboot()) { + case ESP_FAIL: + ESP_LOGE(TAG, "Rollback failed (ESP_FAIL)"); + break; + case ESP_ERR_OTA_ROLLBACK_FAILED: + ESP_LOGE(TAG, "Rollback failed (ESP_ERR_OTA_ROLLBACK_FAILED)"); + break; + default: + ESP_LOGE(TAG, "Rollback failed (Unknown)"); + break; + } + + // Set OTA boot type in config. + if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::None)) { + ESP_LOGE(TAG, "Failed to set OTA firmware boot type"); + } + + esp_restart(); +} + +void OtaUpdateManager::ValidateApp() { + if (esp_ota_mark_app_valid_cancel_rollback() != ESP_OK) { + ESP_PANIC(TAG, "Unable to mark app as valid, WTF?"); // TODO: Wtf do we do here? + } + + // Set OTA boot type in config. + if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::Validated)) { + ESP_PANIC(TAG, "Failed to set OTA firmware boot type in critical section"); // TODO: THIS IS A CRITICAL SECTION, WHAT DO WE DO? + } + + _otaImageState = ESP_OTA_IMG_VALID; +} diff --git a/src/PinPatternManager.cpp b/src/PinPatternManager.cpp index 4db7c2fc..fab848cd 100644 --- a/src/PinPatternManager.cpp +++ b/src/PinPatternManager.cpp @@ -8,24 +8,23 @@ const char* const TAG = "PinPatternManager"; using namespace OpenShock; -PinPatternManager::PinPatternManager(std::uint8_t gpioPin) : m_gpioPin(gpioPin), m_pattern(nullptr), m_patternLength(0), m_taskHandle(nullptr), m_taskSemaphore(xSemaphoreCreateBinary()) { +PinPatternManager::PinPatternManager(std::uint8_t gpioPin) : m_gpioPin(gpioPin), m_pattern(nullptr), m_patternLength(0), m_taskHandle(nullptr), m_taskMutex(xSemaphoreCreateMutex()) { pinMode(gpioPin, OUTPUT); - xSemaphoreGive(m_taskSemaphore); } PinPatternManager::~PinPatternManager() { ClearPattern(); - vSemaphoreDelete(m_taskSemaphore); + vSemaphoreDelete(m_taskMutex); } -void PinPatternManager::SetPattern(nonstd::span pattern) { +void PinPatternManager::SetPattern(const State* pattern, std::size_t patternLength) { ClearPatternInternal(); // Set new values - m_patternLength = pattern.size(); + m_patternLength = patternLength; m_pattern = new State[m_patternLength]; - std::copy(pattern.begin(), pattern.end(), m_pattern); + std::copy(pattern, pattern + m_patternLength, m_pattern); char name[32]; snprintf(name, sizeof(name), "PinPatternManager-%u", m_gpioPin); @@ -45,16 +44,16 @@ void PinPatternManager::SetPattern(nonstd::span pattern) { } // Give the semaphore back - xSemaphoreGive(m_taskSemaphore); + xSemaphoreGive(m_taskMutex); } void PinPatternManager::ClearPattern() { ClearPatternInternal(); - xSemaphoreGive(m_taskSemaphore); + xSemaphoreGive(m_taskMutex); } void PinPatternManager::ClearPatternInternal() { - xSemaphoreTake(m_taskSemaphore, portMAX_DELAY); + xSemaphoreTake(m_taskMutex, portMAX_DELAY); if (m_taskHandle != nullptr) { vTaskDelete(m_taskHandle); diff --git a/src/RGBPatternManager.cpp b/src/RGBPatternManager.cpp new file mode 100644 index 00000000..3e9e5d93 --- /dev/null +++ b/src/RGBPatternManager.cpp @@ -0,0 +1,129 @@ +#include "RGBPatternManager.h" + +#include "Logging.h" +#include "util/TaskUtils.h" + +const char* const TAG = "RGBPatternManager"; + +using namespace OpenShock; + +// Currently this assumes a single WS2812B LED +// TODO: Support multiple LEDs ? +// TODO: Support other LED types ? + +RGBPatternManager::RGBPatternManager(std::uint8_t rgbPin) : m_rgbPin(rgbPin), m_rgbBrightness(255), m_pattern(nullptr), m_patternLength(0), m_taskHandle(nullptr), m_taskMutex(xSemaphoreCreateMutex()) { + m_rmtHandle = rmtInit(m_rgbPin, RMT_TX_MODE, RMT_MEM_64); + if (m_rmtHandle == NULL) { + ESP_LOGE(TAG, "[pin-%u] Failed to create rgb rmt object", m_rgbPin); + } + + float realTick = rmtSetTick(m_rmtHandle, 100.F); + ESP_LOGD(TAG, "[pin-%u] real tick set to: %fns", m_rgbPin, realTick); + + SetBrightness(20); +} + +RGBPatternManager::~RGBPatternManager() { + ClearPattern(); + + vSemaphoreDelete(m_taskMutex); +} + +void RGBPatternManager::SetPattern(const RGBState* pattern, std::size_t patternLength) { + ClearPatternInternal(); + + // Set new values + m_patternLength = patternLength; + m_pattern = new RGBState[m_patternLength]; + std::copy(pattern, pattern + m_patternLength, m_pattern); + + // Start the task + BaseType_t result = TaskUtils::TaskCreateExpensive(RunPattern, TAG, 4096, this, 1, &m_taskHandle); + if (result != pdPASS) { + ESP_LOGE(TAG, "[pin-%u] Failed to create task: %d", m_rgbPin, result); + + m_taskHandle = nullptr; + + if (m_pattern != nullptr) { + delete[] m_pattern; + m_pattern = nullptr; + } + m_patternLength = 0; + } + + // Give the semaphore back + xSemaphoreGive(m_taskMutex); +} + +void RGBPatternManager::ClearPattern() { + ClearPatternInternal(); + xSemaphoreGive(m_taskMutex); +} + +void RGBPatternManager::ClearPatternInternal() { + xSemaphoreTake(m_taskMutex, portMAX_DELAY); + + if (m_taskHandle != nullptr) { + vTaskDelete(m_taskHandle); + m_taskHandle = nullptr; + } + + if (m_pattern != nullptr) { + delete[] m_pattern; + m_pattern = nullptr; + } + m_patternLength = 0; +} + +// Range: 0-255 +void RGBPatternManager::SetBrightness(std::uint8_t brightness) { + m_rgbBrightness = brightness; +} + +void RGBPatternManager::SendRGB(const RGBState& state) { + const std::uint16_t bitCount = (8 * 3) * (1); // 8 bits per color * 3 colors * 1 LED + + rmt_data_t led_data[bitCount]; + + std::uint8_t r = static_cast(static_cast(state.red) * m_rgbBrightness / 255); + std::uint8_t g = static_cast(static_cast(state.green) * m_rgbBrightness / 255); + std::uint8_t b = static_cast(static_cast(state.blue) * m_rgbBrightness / 255); + + std::uint8_t i = 0; + // WS2812B takes commands in GRB order + // https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf - Page 5 + const std::uint8_t colors[3] = {g, r, b}; + for (std::uint8_t col = 0; col < 3; col++) { + for (std::uint8_t bit = 0; bit < 8; bit++) { + if ((colors[col] & (1 << (7 - bit)))) { + led_data[i].level0 = 1; + led_data[i].duration0 = 8; + led_data[i].level1 = 0; + led_data[i].duration1 = 4; + } else { + led_data[i].level0 = 1; + led_data[i].duration0 = 4; + led_data[i].level1 = 0; + led_data[i].duration1 = 8; + } + i++; + } + } + + // Send the data + rmtWriteBlocking(m_rmtHandle, led_data, bitCount); +} + +void RGBPatternManager::RunPattern(void* arg) { + RGBPatternManager* thisPtr = reinterpret_cast(arg); + + RGBPatternManager::RGBState* pattern = thisPtr->m_pattern; + std::size_t patternLength = thisPtr->m_patternLength; + + while (true) { + for (std::size_t i = 0; i < patternLength; ++i) { + thisPtr->SendRGB(pattern[i]); + vTaskDelay(pdMS_TO_TICKS(pattern[i].duration)); + } + } +} diff --git a/src/ReadWriteMutex.cpp b/src/ReadWriteMutex.cpp new file mode 100644 index 00000000..2fadf901 --- /dev/null +++ b/src/ReadWriteMutex.cpp @@ -0,0 +1,58 @@ +#include "ReadWriteMutex.h" + +#include "Logging.h" + +const char* const TAG = "ReadWriteMutex"; + +OpenShock::ReadWriteMutex::ReadWriteMutex() : m_mutex(xSemaphoreCreateMutex()), m_readSem(xSemaphoreCreateBinary()), m_readers(0) { + xSemaphoreGive(m_readSem); +} + +OpenShock::ReadWriteMutex::~ReadWriteMutex() { + vSemaphoreDelete(m_mutex); + vSemaphoreDelete(m_readSem); +} + +bool OpenShock::ReadWriteMutex::lockRead(TickType_t xTicksToWait) { + if (xSemaphoreTake(m_readSem, xTicksToWait) == pdFALSE) { + ESP_LOGE(TAG, "Failed to take read semaphore"); + return false; + } + + if (++m_readers == 1) { + if (xSemaphoreTake(m_mutex, xTicksToWait) == pdFALSE) { + xSemaphoreGive(m_readSem); + return false; + } + } + + xSemaphoreGive(m_readSem); + + return true; +} + +void OpenShock::ReadWriteMutex::unlockRead() { + if (xSemaphoreTake(m_readSem, portMAX_DELAY) == pdFALSE) { + ESP_LOGE(TAG, "Failed to take read semaphore"); + return; + } + + if (--m_readers == 0) { + xSemaphoreGive(m_mutex); + } + + xSemaphoreGive(m_readSem); +} + +bool OpenShock::ReadWriteMutex::lockWrite(TickType_t xTicksToWait) { + if (xSemaphoreTake(m_mutex, xTicksToWait) == pdFALSE) { + ESP_LOGE(TAG, "Failed to take mutex"); + return false; + } + + return true; +} + +void OpenShock::ReadWriteMutex::unlockWrite() { + xSemaphoreGive(m_mutex); +} diff --git a/src/SemVer.cpp b/src/SemVer.cpp new file mode 100644 index 00000000..f90eee57 --- /dev/null +++ b/src/SemVer.cpp @@ -0,0 +1,300 @@ +#include "SemVer.h" + +const char* const TAG = "SemVer"; + +using namespace OpenShock; + +constexpr bool _semverIsLetter(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} +constexpr bool _semverIsPositiveDigit(char c) { + return c >= '1' && c <= '9'; +} +constexpr bool _semverIsDigit(char c) { + return c == '0' || _semverIsPositiveDigit(c); +} +constexpr bool _semverIsDigits(StringView str) { + if (str.isNullOrEmpty()) { + return false; + } + + for (auto c : str) { + if (!_semverIsDigit(c)) { + return false; + } + } + + return true; +} +constexpr bool _semverIsNonDigit(char c) { + return _semverIsLetter(c) || c == '-'; +} +constexpr bool _semverIsIdentifierChararacter(char c) { + return _semverIsDigit(c) || _semverIsNonDigit(c); +} +constexpr bool _semverIsIdentifierChararacters(StringView str) { + if (str.isNullOrEmpty()) { + return false; + } + + for (auto c : str) { + if (!_semverIsIdentifierChararacter(c)) { + return false; + } + } + + return true; +} +constexpr bool _semverIsNumericIdentifier(StringView str) { + if (str.isNullOrEmpty()) { + return false; + } + + if (str.length() == 1) { + return _semverIsDigit(str[0]); + } + + return _semverIsPositiveDigit(str[0]) && _semverIsDigits(str.substr(1)); +} +constexpr bool _semverIsAlphanumericIdentifier(StringView str) { + if (str.isNullOrEmpty()) { + return false; + } + + if (str.length() == 1) { + return _semverIsNonDigit(str[0]); + } + + std::size_t nonDigitPos = StringView::npos; + for (std::size_t i = 0; i < str.length(); ++i) { + if (_semverIsNonDigit(str[i])) { + nonDigitPos = i; + break; + } + } + + if (nonDigitPos == StringView::npos) { + return false; + } + + auto after = str.substr(nonDigitPos + 1); + + if (nonDigitPos == 0) { + return _semverIsIdentifierChararacters(after); + } + + auto before = str.substr(0, nonDigitPos); + + if (nonDigitPos == str.length() - 1) { + return _semverIsIdentifierChararacters(before); + } + + return _semverIsIdentifierChararacters(before) && _semverIsIdentifierChararacters(after); +} +constexpr bool _semverIsBuildIdentifier(StringView str) { + return _semverIsAlphanumericIdentifier(str) || _semverIsDigits(str); +} +constexpr bool _semverIsPrereleaseIdentifier(StringView str) { + return _semverIsAlphanumericIdentifier(str) || _semverIsNumericIdentifier(str); +} +constexpr bool _semverIsDotSeperatedBuildIdentifiers(StringView str) { + if (str.isNullOrEmpty()) { + return false; + } + + auto dotIdx = str.find('.'); + while (dotIdx != StringView::npos) { + auto part = str.substr(0, dotIdx); + if (!_semverIsBuildIdentifier(part)) { + return false; + } + + str = str.substr(dotIdx + 1); + dotIdx = str.find('.'); + } + + return _semverIsBuildIdentifier(str); +} +constexpr bool _semverIsDotSeperatedPreleaseIdentifiers(StringView str) { + if (str.isNullOrEmpty()) { + return false; + } + + auto dotIdx = str.find('.'); + while (dotIdx != StringView::npos) { + auto part = str.substr(0, dotIdx); + if (!_semverIsPrereleaseIdentifier(part)) { + return false; + } + + str = str.substr(dotIdx + 1); + dotIdx = str.find('.'); + } + + return _semverIsPrereleaseIdentifier(str); +} +const auto _semverIsPatch = _semverIsNumericIdentifier; +const auto _semverIsMinor = _semverIsNumericIdentifier; +const auto _semverIsMajor = _semverIsNumericIdentifier; +const auto _semverIsPrerelease = _semverIsDotSeperatedPreleaseIdentifiers; +const auto _semverIsBuild = _semverIsDotSeperatedBuildIdentifiers; +bool _semverIsVersionCore(StringView str) { + if (str.isNullOrEmpty()) { + return false; + } + + auto parts = str.split('.'); + if (parts.size() != 3) { + return false; + } + + return _semverIsMajor(parts[0]) && _semverIsMinor(parts[1]) && _semverIsPatch(parts[2]); +} +bool _semverIsSemver(StringView str) { + if (str.isNullOrEmpty()) { + return false; + } + + auto dashPos = str.find('-'); + auto plusPos = str.find('+'); + + if (dashPos == StringView::npos && plusPos == StringView::npos) { + return _semverIsVersionCore(str); + } + + if (dashPos != StringView::npos && plusPos != StringView::npos) { + if (dashPos > plusPos) { + return false; + } + + auto core = str.substr(0, dashPos); + auto prerelease = str.substr(dashPos + 1, plusPos - dashPos - 1); + auto build = str.substr(plusPos + 1); + + return _semverIsVersionCore(core) && _semverIsPrerelease(prerelease) && _semverIsBuild(build); + } + + if (dashPos != StringView::npos) { + auto core = str.substr(0, dashPos); + auto prerelease = str.substr(dashPos + 1); + + return _semverIsVersionCore(core) && _semverIsPrerelease(prerelease); + } + + if (plusPos != StringView::npos) { + auto core = str.substr(0, plusPos); + auto build = str.substr(plusPos + 1); + + return _semverIsVersionCore(core) && _semverIsBuild(build); + } + + return false; +} + +bool _tryParseU16(OpenShock::StringView str, std::uint16_t& out) { + if (str.isNullOrEmpty()) { + return false; + } + + std::uint32_t u32 = 0; + for (auto c : str) { + if (c < '0' || c > '9') { + return false; + } + + u32 *= 10; + u32 += c - '0'; + + if (u32 > std::numeric_limits::max()) { + return false; + } + } + + out = static_cast(u32); + + return true; +} + +bool SemVer::isValid() const { + if (!this->prerelease.empty() && !_semverIsPrereleaseIdentifier(this->prerelease)) { + return false; + } + + if (!this->build.empty() && !_semverIsBuildIdentifier(this->build)) { + return false; + } + + return true; +} + +std::string SemVer::toString() const { + std::string str; + str.reserve(32); + + str += std::to_string(major); + str += '.'; + str += std::to_string(minor); + str += '.'; + str += std::to_string(patch); + + if (!prerelease.empty()) { + str += '-'; + str.append(prerelease.data(), prerelease.length()); + } + + if (!build.empty()) { + str += '+'; + str.append(build.data(), build.length()); + } + + return str; +} + +bool OpenShock::TryParseSemVer(StringView semverStr, SemVer& semver) { + auto parts = semverStr.split('.'); + if (parts.size() < 3) { + ESP_LOGE(TAG, "Must have at least 3 parts: %s", semverStr.data()); + return false; + } + + StringView majorStr = parts[0], minorStr = parts[1], patchStr = parts[2]; + + auto dashIdx = patchStr.find('-'); + if (dashIdx != StringView::npos) { + semver.prerelease = patchStr.substr(dashIdx + 1); + patchStr = patchStr.substr(0, dashIdx); + } + + auto plusIdx = semver.prerelease.find('+'); + if (plusIdx != StringView::npos) { + semver.build = semver.prerelease.substr(plusIdx + 1); + semver.prerelease = semver.prerelease.substr(0, plusIdx); + } + + if (!_tryParseU16(majorStr, semver.major)) { + ESP_LOGE(TAG, "Invalid major version: %s", majorStr.data()); + return false; + } + + if (!_tryParseU16(minorStr, semver.minor)) { + ESP_LOGE(TAG, "Invalid minor version: %s", minorStr.data()); + return false; + } + + if (!_tryParseU16(patchStr, semver.patch)) { + ESP_LOGE(TAG, "Invalid patch version: %s", patchStr.data()); + return false; + } + + if (!semver.prerelease.empty() && !_semverIsPrerelease(semver.prerelease)) { + ESP_LOGE(TAG, "Invalid prerelease: %s", semver.prerelease.data()); + return false; + } + + if (!semver.build.empty() && !_semverIsBuild(semver.build)) { + ESP_LOGE(TAG, "Invalid build: %s", semver.build.data()); + return false; + } + + return true; +} diff --git a/src/SerialInputHandler.cpp b/src/SerialInputHandler.cpp deleted file mode 100644 index 9291a51f..00000000 --- a/src/SerialInputHandler.cpp +++ /dev/null @@ -1,353 +0,0 @@ -#include "SerialInputHandler.h" - -#include "CommandHandler.h" -#include "Config.h" -#include "Logging.h" -#include "wifi/WiFiManager.h" - -#include -#include - -#include - -const char* const TAG = "SerialInputHandler"; - -using namespace OpenShock; - -const char* const kCommandHelp = "help"; -const char* const kCommandVersion = "version"; -const char* const kCommandRestart = "restart"; -const char* const kCommandRmtpin = "rmtpin"; -const char* const kCommandAuthToken = "authtoken"; -const char* const kCommandNetworks = "networks"; -const char* const kCommandFactoryReset = "factoryreset"; - -void _handleHelpCommand(char* arg, std::size_t argLength) { - SerialInputHandler::PrintWelcomeHeader(); - if (arg == nullptr || argLength <= 0) { - Serial.println("help print this menu"); - Serial.println("help print help for a command"); - Serial.println("version print version information"); - Serial.println("restart restart the board"); - Serial.println("rmtpin get radio pin"); - Serial.println("rmtpin set radio pin"); - Serial.println("authtoken set auth token"); - Serial.println("networks get all saved networks"); - Serial.println("networks set all saved networks"); - Serial.println("factoryreset reset device to factory defaults and reboot"); - return; - } - - if (strcmp(arg, kCommandRmtpin) == 0) { - Serial.println("rmtpin"); - Serial.println(" Get the GPIO pin used for the radio transmitter."); - Serial.println(); - Serial.println("rmtpin []"); - Serial.println(" Set the GPIO pin used for the radio transmitter."); - Serial.println(" Arguments:"); - Serial.println(" must be a number."); - Serial.println(" Example:"); - Serial.println(" rmtpin 15"); - return; - } - - if (strcmp(arg, kCommandAuthToken) == 0) { - Serial.println("authtoken "); - Serial.println(" Set the auth token."); - Serial.println(" Arguments:"); - Serial.println(" must be a string."); - Serial.println(" Example:"); - Serial.println(" authtoken mytoken"); - return; - } - - if (strcmp(arg, kCommandNetworks) == 0) { - Serial.println("networks"); - Serial.println(" Get all saved networks."); - Serial.println(); - Serial.println("networks []"); - Serial.println(" Set all saved networks."); - Serial.println(" Arguments:"); - Serial.println(" must be a array of objects with the following fields:"); - Serial.println(" ssid (string) SSID of the network"); - Serial.println(" password (string) Password of the network"); - Serial.println(" Example:"); - Serial.println(" networks [{\"ssid\":\"myssid\",\"password\":\"mypassword\"}]"); - return; - } - - if (strcmp(arg, kCommandRestart) == 0) { - Serial.println(kCommandRestart); - Serial.println(" Restart the board"); - Serial.println(" Example:"); - Serial.println(" restart"); - return; - } - - if (strcmp(arg, kCommandFactoryReset) == 0) { - Serial.println(kCommandFactoryReset); - Serial.println(" Reset the device to factory defaults and reboot"); - Serial.println(" Example:"); - Serial.println(" factoryreset"); - return; - } - - if (strcmp(arg, kCommandVersion) == 0) { - Serial.println(kCommandVersion); - Serial.println(" Print version information"); - Serial.println(" Example:"); - Serial.println(" version"); - return; - } - - if (strcmp(arg, kCommandHelp) == 0) { - Serial.println(kCommandHelp); - Serial.println(" Print help information"); - Serial.println(" Arguments:"); - Serial.println(" (optional) command to print help for"); - Serial.println(" Example:"); - Serial.println(" help"); - return; - } - - Serial.println("Command not found"); -} - -void _handleVersionCommand(char* arg, std::size_t argLength) { - Serial.print("\n"); - SerialInputHandler::PrintVersionInfo(); -} - -void _handleRestartCommand(char* arg, std::size_t argLength) { - Serial.println("Restarting ESP..."); - ESP.restart(); -} - -void _handleFactoryResetCommand(char* arg, std::size_t argLength) { - Serial.println("Resetting to factory defaults..."); - Config::FactoryReset(); - Serial.println("Rebooting..."); - ESP.restart(); -} - -void _handleRmtpinCommand(char* arg, std::size_t argLength) { - if (arg == nullptr || argLength <= 0) { - // Get rmt pin - Serial.print("$SYS$|Response|RmtPin|"); - Serial.println(Config::GetRFConfig().txPin); - return; - } - - unsigned int pin; - if (sscanf(arg, "%u", &pin) != 1) { - Serial.println("$SYS$|Error|Invalid argument (not a number)"); - return; - } - - if (pin > UINT8_MAX) { - Serial.println("$SYS$|Error|Invalid argument (out of range)"); - return; - } - - OpenShock::CommandHandler::SetRfTxPin(static_cast(pin)); - - Serial.println("$SYS$|Success|Saved config"); -} - -void _handleAuthtokenCommand(char* arg, std::size_t argLength) { - if (arg == nullptr || argLength <= 0) { - Serial.println("$SYS$|Error|Invalid argument"); - return; - } - - OpenShock::Config::SetBackendAuthToken(std::string(arg, argLength)); - - Serial.println("$SYS$|Success|Saved config"); -} - -void _handleNetworksCommand(char* arg, std::size_t argLength) { - if (arg == nullptr || argLength <= 0) { - // Get networks - StaticJsonDocument<1024> outDoc; - JsonArray outNetworks = outDoc.to(); - - for (auto& creds : Config::GetWiFiCredentials()) { - JsonObject network = outNetworks.createNestedObject(); - network["ssid"] = creds.ssid; - network["password"] = creds.password; - } - - Serial.print("$SYS$|Response|Networks|"); - serializeJson(outDoc, Serial); - Serial.println(); - return; - } - - DynamicJsonDocument doc(1024); - deserializeJson(doc, arg, argLength); - - JsonArray networks = doc.as(); - - std::uint8_t id = 1; - std::vector creds; - for (JsonObject network : networks) { - std::string ssid = network["ssid"].as(); - std::string password = network["password"].as(); - - if (ssid.empty() || password.empty()) { - Serial.println("$SYS$|Error|Invalid argument (missing ssid or password)"); - return; - } - - Config::WiFiCredentials cred { - .id = id++, - .ssid = ssid, - .bssid = {0, 0, 0, 0, 0, 0}, - .password = password, - }; - ESP_LOGI(TAG, "Adding network to config %s", ssid.c_str()); - - creds.push_back(std::move(cred)); - } - - if (!OpenShock::Config::SetWiFiCredentials(creds)) { - Serial.println("$SYS$|Error|Failed to save config"); - return; - } - - Serial.println("$SYS$|Success|Saved config"); - - OpenShock::WiFiManager::RefreshNetworkCredentials(); -} - -static std::unordered_map s_commandHandlers = { - { kCommandHelp, _handleHelpCommand}, - { kCommandVersion, _handleVersionCommand}, - { kCommandRestart, _handleRestartCommand}, - { kCommandRmtpin, _handleRmtpinCommand}, - { kCommandAuthToken, _handleAuthtokenCommand}, - { kCommandNetworks, _handleNetworksCommand}, - {kCommandFactoryReset, _handleFactoryResetCommand}, -}; - -int findChar(const char* buffer, std::size_t bufferSize, char c) { - for (int i = 0; i < bufferSize; i++) { - if (buffer[i] == c) { - return i; - } - } - - return -1; -} - -int findLineEnd(const char* buffer, int bufferSize) { - for (int i = 0; i < bufferSize; i++) { - if (buffer[i] == '\r' || buffer[i] == '\n' || buffer[i] == '\0') { - return i; - } - } - - return -1; -} - -int findLineStart(const char* buffer, int bufferSize, int lineEnd) { - if (lineEnd < 0) return -1; - - for (int i = lineEnd; i < bufferSize - lineEnd; i++) { - if (buffer[i] != '\r' && buffer[i] != '\n' && buffer[i] != '\0') { - return i; - } - } - - return -1; -} - -void processSerialLine(char* data, std::size_t length) { - int delimiter = findChar(data, length, ' '); - if (delimiter == 0) { - Serial.println("$SYS$|Error|Command cannot start with a space"); - return; - } - - char* command = data; - std::size_t commandLength = length; - char* arg = nullptr; - std::size_t argLength = 0; - - // Handle arg-less commands - if (delimiter > 0) { - data[delimiter] = '\0'; - commandLength = delimiter; - arg = data + delimiter + 1; - argLength = length - delimiter - 1; - } - - // TODO: Clean this up, test this - auto it = s_commandHandlers.find(std::string(command, commandLength)); - if (it != s_commandHandlers.end()) { - it->second(arg, argLength); - return; - } - - Serial.println("$SYS$|Error|Command not found"); -} - -void SerialInputHandler::Update() { - static char* buffer = nullptr; // TODO: Clean up this buffer every once in a while - static std::size_t bufferSize = 0; - static std::size_t bufferIndex = 0; - - while (true) { - int available = Serial.available(); - if (available <= 0) { - break; - } - - if (bufferIndex + available > bufferSize) { - bufferSize = bufferIndex + available; - buffer = static_cast(realloc(buffer, bufferSize)); - } - - bufferIndex += Serial.readBytes(buffer + bufferIndex, available); - - int lineEnd = findLineEnd(buffer, bufferIndex); - if (lineEnd == -1) { - break; - } - - buffer[lineEnd] = '\0'; - Serial.printf("> %s\n", buffer); - - processSerialLine(buffer, lineEnd); - - int nextLine = findLineStart(buffer, bufferIndex, lineEnd + 1); - if (nextLine < 0) { - bufferIndex = 0; - break; - } - - int remaining = bufferIndex - nextLine; - if (remaining > 0) { - memmove(buffer, buffer + nextLine, remaining); - bufferIndex = remaining; - } else { - bufferIndex = 0; - } - } -} - -void SerialInputHandler::PrintWelcomeHeader() { - Serial.println("\n============== OPENSHOCK =============="); - Serial.println(" Contribute @ github.com/OpenShock"); - Serial.println(" Discuss @ discord.gg/AHcCbXbEcF"); - Serial.println(" Type 'help' for available commands"); - Serial.println("=======================================\n"); -} - -void SerialInputHandler::PrintVersionInfo() { - Serial.println(" Version: " OPENSHOCK_FW_VERSION); - Serial.println(" Build: " OPENSHOCK_FW_MODE); - Serial.println(" Commit: " OPENSHOCK_FW_COMMIT); - Serial.println(" Board: " OPENSHOCK_FW_BOARD); - Serial.println(" Chip: " OPENSHOCK_FW_CHIP); -} diff --git a/src/VisualStateManager.cpp b/src/VisualStateManager.cpp index 6f859c67..d72cc245 100644 --- a/src/VisualStateManager.cpp +++ b/src/VisualStateManager.cpp @@ -1,23 +1,24 @@ #include "VisualStateManager.h" #ifdef OPENSHOCK_LED_GPIO -#ifdef OPENSHOCK_LED_IMPLEMENTATION -#error "Only one LED type can be defined at a time" -#else -#define OPENSHOCK_LED_IMPLEMENTATION #include "PinPatternManager.h" #include +#ifndef OPENSHOCK_LED_IMPLEMENTATION +#define OPENSHOCK_LED_IMPLEMENTATION #endif // OPENSHOCK_LED_IMPLEMENTATION #endif // OPENSHOCK_LED_GPIO #ifdef OPENSHOCK_LED_WS2812B -#ifdef OPENSHOCK_LED_IMPLEMENTATION -#error "Only one LED type can be defined at a time" -#else +#include "RGBPatternManager.h" +#ifndef OPENSHOCK_LED_IMPLEMENTATION #define OPENSHOCK_LED_IMPLEMENTATION #endif // OPENSHOCK_LED_IMPLEMENTATION #endif // OPENSHOCK_LED_WS2812B +#ifndef OPENSHOCK_LED_IMPLEMENTATION +#warning "No LED implementation selected, board will not be able to indicate its status visually" +#endif // OPENSHOCK_LED_IMPLEMENTATION + #include "Logging.h" #include @@ -30,14 +31,19 @@ const char* const TAG = "VisualStateManager"; -constexpr std::uint64_t kCriticalErrorFlag = 1 << 0; -constexpr std::uint64_t kEmergencyStoppedFlag = 1 << 1; -constexpr std::uint64_t kWebSocketConnectedFlag = 1 << 2; -constexpr std::uint64_t kWiFiConnectedWithoutWSFlag = 1 << 3; -constexpr std::uint64_t kWiFiScanningFlag = 1 << 4; +constexpr std::uint64_t kCriticalErrorFlag = 1 << 0; +constexpr std::uint64_t kEmergencyStoppedFlag = 1 << 1; +constexpr std::uint64_t kEmergencyStopClearedFlag = 1 << 2; +constexpr std::uint64_t kWebSocketConnectedFlag = 1 << 3; +constexpr std::uint64_t kWiFiConnectedFlag = 1 << 4; +constexpr std::uint64_t kWiFiScanningFlag = 1 << 5; static std::uint64_t s_stateFlags = 0; +// Bitmask of when the system is in a "all clear" state. + +constexpr std::uint64_t kStatusOKMask = kWebSocketConnectedFlag | kWiFiConnectedFlag; + using namespace OpenShock; #ifdef OPENSHOCK_LED_GPIO @@ -52,7 +58,14 @@ constexpr PinPatternManager::State kEmergencyStoppedPattern[] = { {false, 500} }; +constexpr PinPatternManager::State kEmergencyStopClearedPattern[] = { + { true, 150}, + {false, 150} +}; + constexpr PinPatternManager::State kWiFiDisconnectedPattern[] = { + { true, 100}, + {false, 100}, { true, 100}, {false, 100}, { true, 100}, @@ -60,8 +73,6 @@ constexpr PinPatternManager::State kWiFiDisconnectedPattern[] = { }; constexpr PinPatternManager::State kWiFiConnectedWithoutWSPattern[] = { - { true, 100}, - {false, 100}, { true, 100}, {false, 100}, { true, 100}, @@ -97,9 +108,22 @@ constexpr PinPatternManager::State kWebSocketConnectedPattern[] = { {false, 10'000} }; +constexpr PinPatternManager::State kSolidOnPattern[] = { + {true, 100'000} +}; + +constexpr PinPatternManager::State kSolidOffPattern[] = { + {false, 100'000} +}; + PinPatternManager s_builtInLedManager(OPENSHOCK_LED_GPIO); -void _updateVisualState() { +template +inline void _updateVisualStateGPIO(const PinPatternManager::State (&override)[N]) { + s_builtInLedManager.SetPattern(override); +} + +void _updateVisualStateGPIO() { if (s_stateFlags & kCriticalErrorFlag) { s_builtInLedManager.SetPattern(kCriticalErrorPattern); return; @@ -110,12 +134,17 @@ void _updateVisualState() { return; } + if (s_stateFlags & kEmergencyStopClearedFlag) { + s_builtInLedManager.SetPattern(kEmergencyStopClearedPattern); + return; + } + if (s_stateFlags & kWebSocketConnectedFlag) { s_builtInLedManager.SetPattern(kWebSocketConnectedPattern); return; } - if (s_stateFlags & kWiFiConnectedWithoutWSFlag) { + if (s_stateFlags & kWiFiConnectedFlag) { s_builtInLedManager.SetPattern(kWiFiConnectedWithoutWSPattern); return; } @@ -132,39 +161,151 @@ void _updateVisualState() { #ifdef OPENSHOCK_LED_WS2812B -void _updateVisualState() { - ESP_LOGE(TAG, "_updateVisualState: (but WS2812B is not implemented yet)"); +constexpr RGBPatternManager::RGBState kCriticalErrorRGBPattern[] = { + {255, 0, 0, 100}, // Red ON for 0.1 seconds + { 0, 0, 0, 100} // OFF for 0.1 seconds +}; + +constexpr RGBPatternManager::RGBState kEmergencyStoppedRGBPattern[] = { + {255, 0, 0, 500}, + { 0, 0, 0, 500} +}; + +constexpr RGBPatternManager::RGBState kEmergencyStopClearedRGBPattern[] = { + {0, 255, 0, 150}, + {0, 0, 0, 150} +}; + +constexpr RGBPatternManager::RGBState kWiFiDisconnectedRGBPattern[] = { + {0, 0, 255, 100}, + {0, 0, 0, 100}, + {0, 0, 255, 100}, + {0, 0, 0, 100}, + {0, 0, 255, 100}, + {0, 0, 0, 700} +}; + +constexpr RGBPatternManager::RGBState kWiFiConnectedWithoutWSRGBPattern[] = { + {255, 165, 0, 100}, + { 0, 0, 0, 100}, + {255, 165, 0, 100}, + { 0, 0, 0, 700} +}; + +constexpr RGBPatternManager::RGBState kPingNoResponseRGBPattern[] = { + {0, 50, 255, 100}, + {0, 0, 0, 100}, + {0, 50, 255, 100}, + {0, 0, 0, 100}, + {0, 50, 255, 100}, + {0, 0, 0, 100}, + {0, 50, 255, 100}, + {0, 0, 0, 700} +}; + +constexpr RGBPatternManager::RGBState kWebSocketCantConnectRGBPattern[] = { + {255, 0, 0, 100}, + { 0, 0, 0, 100}, + {255, 0, 0, 100}, + { 0, 0, 0, 100}, + {255, 0, 0, 100}, + { 0, 0, 0, 100}, + {255, 0, 0, 100}, + { 0, 0, 0, 100}, + {255, 0, 0, 100}, + { 0, 0, 0, 700} +}; + +constexpr RGBPatternManager::RGBState kWebSocketConnectedRGBPattern[] = { + {0, 255, 0, 100}, + {0, 0, 0, 10'000}, +}; + +RGBPatternManager s_RGBLedManager(OPENSHOCK_LED_WS2812B); + +void _updateVisualStateRGB() { + if (s_stateFlags & kCriticalErrorFlag) { + s_RGBLedManager.SetPattern(kCriticalErrorRGBPattern); + return; + } + + if (s_stateFlags & kEmergencyStoppedFlag) { + s_RGBLedManager.SetPattern(kEmergencyStoppedRGBPattern); + return; + } + + if (s_stateFlags & kEmergencyStopClearedFlag) { + s_RGBLedManager.SetPattern(kEmergencyStopClearedRGBPattern); + return; + } + + if (s_stateFlags & kWebSocketConnectedFlag) { + s_RGBLedManager.SetPattern(kWebSocketConnectedRGBPattern); + return; + } + + if (s_stateFlags & kWiFiConnectedFlag) { + s_RGBLedManager.SetPattern(kWiFiConnectedWithoutWSRGBPattern); + return; + } + + if (s_stateFlags & kWiFiScanningFlag) { + s_RGBLedManager.SetPattern(kPingNoResponseRGBPattern); + return; + } + + s_RGBLedManager.SetPattern(kWiFiDisconnectedRGBPattern); } #endif // OPENSHOCK_LED_WS2812B -#ifndef OPENSHOCK_LED_IMPLEMENTATION - void _updateVisualState() { +#ifdef OPENSHOCK_LED_IMPLEMENTATION +#if defined(OPENSHOCK_LED_GPIO) && defined(OPENSHOCK_LED_WS2812B) + if (s_stateFlags == kStatusOKMask) { + _updateVisualStateGPIO(kSolidOnPattern); + } else { + _updateVisualStateGPIO(kSolidOffPattern); + } + _updateVisualStateRGB(); +#elif defined(OPENSHOCK_LED_GPIO) + _updateVisualStateGPIO(); +#elif defined(OPENSHOCK_LED_WS2812B) + _updateVisualStateRGB(); +#else +#error "No LED implementation selected but OPENSHOCK_LED_IMPLEMENTATION is defined" +#endif +#else ESP_LOGE(TAG, "_updateVisualState: (but no LED implementation is selected)"); + vTaskDelay(10); +#endif } -#endif // OPENSHOCK_LED_NONE - void _handleWiFiConnected(arduino_event_t* event) { + (void)event; + std::uint64_t oldState = s_stateFlags; - s_stateFlags |= kWiFiConnectedWithoutWSFlag; + s_stateFlags |= kWiFiConnectedFlag; if (oldState != s_stateFlags) { _updateVisualState(); } } void _handleWiFiDisconnected(arduino_event_t* event) { + (void)event; + std::uint64_t oldState = s_stateFlags; - s_stateFlags &= ~kWiFiConnectedWithoutWSFlag; + s_stateFlags &= ~kWiFiConnectedFlag; if (oldState != s_stateFlags) { _updateVisualState(); } } void _handleWiFiScanDone(arduino_event_t* event) { + (void)event; + std::uint64_t oldState = s_stateFlags; s_stateFlags &= ~kWiFiScanningFlag; @@ -204,13 +345,30 @@ void VisualStateManager::SetScanningStarted() { } } -void VisualStateManager::SetEmergencyStop(bool isStopped) { +void VisualStateManager::SetEmergencyStop(OpenShock::EStopManager::EStopStatus status) { std::uint64_t oldState = s_stateFlags; - if (isStopped) { - s_stateFlags |= kEmergencyStoppedFlag; - } else { - s_stateFlags &= ~kEmergencyStoppedFlag; + switch (status) { + // When there is no EStop currently active. + case OpenShock::EStopManager::EStopStatus::ALL_CLEAR: + s_stateFlags &= ~kEmergencyStoppedFlag; + s_stateFlags &= ~kEmergencyStopClearedFlag; + break; + // EStop has been triggered! + case OpenShock::EStopManager::EStopStatus::ESTOPPED_AND_HELD: + // EStop still active, and user has released the button from the initial trigger. + case OpenShock::EStopManager::EStopStatus::ESTOPPED: + s_stateFlags |= kEmergencyStoppedFlag; + s_stateFlags &= ~kEmergencyStopClearedFlag; + break; + // User has held and cleared the EStop, now we're waiting for them to release the button. + case OpenShock::EStopManager::EStopStatus::ESTOPPED_CLEARED: + s_stateFlags &= ~kEmergencyStoppedFlag; + s_stateFlags |= kEmergencyStopClearedFlag; + break; + default: + ESP_LOGE(TAG, "Unhandled EStop status: %d", status); + break; } if (oldState != s_stateFlags) { diff --git a/src/WebSocketDeFragger.cpp b/src/WebSocketDeFragger.cpp new file mode 100644 index 00000000..adb8a1b1 --- /dev/null +++ b/src/WebSocketDeFragger.cpp @@ -0,0 +1,159 @@ +#include "WebSocketDeFragger.h" + +#include "Logging.h" + +#include + +const char* const TAG = "WebSocketDeFragger"; + +using namespace OpenShock; + +std::uint8_t* _reallocOrFree(std::uint8_t* ptr, std::size_t size) { + void* newPtr = realloc(ptr, size); + if (newPtr == nullptr) { + free(ptr); + ESP_PANIC(TAG, "Failed to allocate memory"); + } + + return reinterpret_cast(newPtr); +} + +WebSocketDeFragger::WebSocketDeFragger(EventCallback callback) : m_messages(), m_callback(callback) { } + +WebSocketDeFragger::~WebSocketDeFragger() { + clear(); +} + +void WebSocketDeFragger::handler(std::uint8_t socketId, WStype_t type, const std::uint8_t* payload, std::size_t length) { + switch (type) { + case WStype_FRAGMENT_BIN_START: + start(socketId, WebSocketMessageType::Binary, payload, length); + return; + case WStype_FRAGMENT_TEXT_START: + start(socketId, WebSocketMessageType::Text, payload, length); + return; + case WStype_FRAGMENT: + append(socketId, payload, length); + return; + case WStype_FRAGMENT_FIN: + finish(socketId, payload, length); + return; + [[likely]] default: + clear(socketId); + break; + } + + WebSocketMessageType messageType; + switch (type) { + [[unlikely]] case WStype_ERROR: + messageType = WebSocketMessageType::Error; + break; + case WStype_DISCONNECTED: + messageType = WebSocketMessageType::Disconnected; + break; + case WStype_CONNECTED: + messageType = WebSocketMessageType::Connected; + break; + case WStype_TEXT: + messageType = WebSocketMessageType::Text; + break; + [[likely]] case WStype_BIN: + messageType = WebSocketMessageType::Binary; + break; + case WStype_PING: + messageType = WebSocketMessageType::Ping; + break; + case WStype_PONG: + messageType = WebSocketMessageType::Pong; + break; + [[unlikely]] default: + const char* const errorMessage = "Unknown WebSocket event type"; + m_callback(socketId, WebSocketMessageType::Error, reinterpret_cast(errorMessage), strlen(errorMessage)); + return; + } + + m_callback(socketId, messageType, payload, length); +} + +void WebSocketDeFragger::onEvent(const EventCallback& callback) { + m_callback = callback; +} + +void WebSocketDeFragger::clear(std::uint8_t socketId) { + auto it = m_messages.find(socketId); + if (it != m_messages.end()) { + free(it->second.data); + m_messages.erase(it); + } +} + +void WebSocketDeFragger::clear() { + for (auto it = m_messages.begin(); it != m_messages.end(); ++it) { + free(it->second.data); + } + m_messages.clear(); +} + +void WebSocketDeFragger::start(std::uint8_t socketId, WebSocketMessageType type, const std::uint8_t* data, std::uint32_t length) { + auto it = m_messages.find(socketId); + if (it != m_messages.end()) { + auto& message = it->second; + + if (message.capacity < length) { + message.data = _reallocOrFree(message.data, length); + message.capacity = length; + } + + memcpy(message.data, data, length); + message.size = length; + + return; + } + + Message message {.data = reinterpret_cast(malloc(length)), .size = length, .capacity = length, .type = type}; + + memcpy(message.data, data, length); + + m_messages.insert(std::make_pair(socketId, message)); +} + +void WebSocketDeFragger::append(std::uint8_t socketId, const std::uint8_t* data, std::uint32_t length) { + auto it = m_messages.find(socketId); + if (it == m_messages.end()) { + return; + } + + auto& message = it->second; + + std::uint32_t newLength = message.size + length; + if (message.capacity < newLength) { + message.data = _reallocOrFree(message.data, newLength); + message.capacity = newLength; + } + + memcpy(message.data + message.size, data, length); + message.size = newLength; +} + +void WebSocketDeFragger::finish(std::uint8_t socketId, const std::uint8_t* data, std::uint32_t length) { + auto it = m_messages.find(socketId); + if (it == m_messages.end()) { + return; + } + + auto& message = it->second; + + std::uint32_t newLength = message.size + length; + if (message.capacity < newLength) { + message.data = _reallocOrFree(message.data, newLength); + message.capacity = newLength; + } + + memcpy(message.data + message.size, data, length); + message.size = newLength; + + m_callback(socketId, message.type, message.data, message.size); + + free(message.data); + m_messages.erase(it); +} diff --git a/src/config/BackendConfig.cpp b/src/config/BackendConfig.cpp new file mode 100644 index 00000000..af560633 --- /dev/null +++ b/src/config/BackendConfig.cpp @@ -0,0 +1,68 @@ +#include "config/BackendConfig.h" + +#include "config/internal/utils.h" +#include "Logging.h" + +const char* const TAG = "Config::BackendConfig"; + +using namespace OpenShock::Config; + +BackendConfig::BackendConfig() : domain(OPENSHOCK_API_DOMAIN), authToken() { } + +void BackendConfig::ToDefault() { + domain = OPENSHOCK_API_DOMAIN; + authToken.clear(); +} + +bool BackendConfig::FromFlatbuffers(const Serialization::Configuration::BackendConfig* config) { + if (config == nullptr) { + ESP_LOGE(TAG, "config is null"); + return false; + } + + Internal::Utils::FromFbsStr(domain, config->domain(), OPENSHOCK_API_DOMAIN); + Internal::Utils::FromFbsStr(authToken, config->auth_token(), ""); + + return true; +} + +flatbuffers::Offset BackendConfig::ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const { + auto domainOffset = builder.CreateString(domain); + + flatbuffers::Offset authTokenOffset; + if (withSensitiveData) { + authTokenOffset = builder.CreateString(authToken); + } else { + authTokenOffset = 0; + } + + return Serialization::Configuration::CreateBackendConfig(builder, domainOffset, authTokenOffset); +} + +bool BackendConfig::FromJSON(const cJSON* json) { + if (json == nullptr) { + ESP_LOGE(TAG, "json is null"); + return false; + } + + if (cJSON_IsObject(json) == 0) { + ESP_LOGE(TAG, "json is not an object"); + return false; + } + + Internal::Utils::FromJsonStr(domain, json, "domain", OPENSHOCK_API_DOMAIN); + Internal::Utils::FromJsonStr(authToken, json, "authToken", ""); + + return true; +} + +cJSON* BackendConfig::ToJSON(bool withSensitiveData) const { + cJSON* root = cJSON_CreateObject(); + + cJSON_AddStringToObject(root, "domain", domain.c_str()); + if (withSensitiveData) { + cJSON_AddStringToObject(root, "authToken", authToken.c_str()); + } + + return root; +} diff --git a/src/config/CaptivePortalConfig.cpp b/src/config/CaptivePortalConfig.cpp new file mode 100644 index 00000000..52a16d9e --- /dev/null +++ b/src/config/CaptivePortalConfig.cpp @@ -0,0 +1,57 @@ +#include "config/CaptivePortalConfig.h" + +#include "config/internal/utils.h" +#include "Logging.h" + +const char* const TAG = "Config::CaptivePortalConfig"; + +using namespace OpenShock::Config; + +CaptivePortalConfig::CaptivePortalConfig() : alwaysEnabled(false) { } + +CaptivePortalConfig::CaptivePortalConfig(bool alwaysEnabled) { + this->alwaysEnabled = alwaysEnabled; +} + +void CaptivePortalConfig::ToDefault() { + alwaysEnabled = false; +} + +bool CaptivePortalConfig::FromFlatbuffers(const Serialization::Configuration::CaptivePortalConfig* config) { + if (config == nullptr) { + ESP_LOGE(TAG, "config is null"); + return false; + } + + alwaysEnabled = config->always_enabled(); + + return true; +} + +flatbuffers::Offset CaptivePortalConfig::ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const { + return Serialization::Configuration::CreateCaptivePortalConfig(builder, alwaysEnabled); +} + +bool CaptivePortalConfig::FromJSON(const cJSON* json) { + if (json == nullptr) { + ESP_LOGE(TAG, "json is null"); + return false; + } + + if (cJSON_IsObject(json) == 0) { + ESP_LOGE(TAG, "json is not an object"); + return false; + } + + Internal::Utils::FromJsonBool(alwaysEnabled, json, "alwaysEnabled", false); + + return true; +} + +cJSON* CaptivePortalConfig::ToJSON(bool withSensitiveData) const { + cJSON* root = cJSON_CreateObject(); + + cJSON_AddBoolToObject(root, "alwaysEnabled", alwaysEnabled); + + return root; +} diff --git a/src/config/Config.cpp b/src/config/Config.cpp new file mode 100644 index 00000000..e46cca17 --- /dev/null +++ b/src/config/Config.cpp @@ -0,0 +1,674 @@ +#include "config/Config.h" + +#include "Common.h" +#include "config/RootConfig.h" +#include "Logging.h" +#include "ReadWriteMutex.h" + +#include +#include + +#include + +#include + +const char* const TAG = "Config"; + +using namespace OpenShock; + +static fs::LittleFSFS _configFS; +static Config::RootConfig _configData; +static ReadWriteMutex _configMutex; + +bool _tryDeserializeConfig(const std::uint8_t* buffer, std::size_t bufferLen, OpenShock::Config::RootConfig& config) { + if (buffer == nullptr || bufferLen == 0) { + ESP_LOGE(TAG, "Buffer is null or empty"); + return false; + } + + // Deserialize + auto fbsConfig = flatbuffers::GetRoot(buffer); + if (fbsConfig == nullptr) { + ESP_LOGE(TAG, "Failed to get deserialization root for config file"); + return false; + } + + // Validate buffer + flatbuffers::Verifier::Options verifierOptions { + .max_size = 4096, // Should be enough + }; + flatbuffers::Verifier verifier(buffer, bufferLen, verifierOptions); + if (!fbsConfig->Verify(verifier)) { + ESP_LOGE(TAG, "Failed to verify config file integrity"); + return false; + } + + // Read config + if (!config.FromFlatbuffers(fbsConfig)) { + ESP_LOGE(TAG, "Failed to read config file"); + return false; + } + + return true; +} +bool _tryLoadConfig(std::vector& buffer) { + File file = _configFS.open("/config", "rb"); + if (!file) { + ESP_LOGE(TAG, "Failed to open config file for reading"); + return false; + } + + // Get file size + std::size_t size = file.size(); + + // Resize buffer + buffer.resize(size); + + // Read file + if (file.read(buffer.data(), buffer.size()) != buffer.size()) { + ESP_LOGE(TAG, "Failed to read config file, size mismatch"); + return false; + } + + file.close(); + + return true; +} +bool _tryLoadConfig() { + std::vector buffer; + if (!_tryLoadConfig(buffer)) { + return false; + } + + return _tryDeserializeConfig(buffer.data(), buffer.size(), _configData); +} +bool _trySaveConfig(const std::uint8_t* data, std::size_t dataLen) { + File file = _configFS.open("/config", "wb"); + if (!file) { + ESP_LOGE(TAG, "Failed to open config file for writing"); + return false; + } + + // Write file + if (file.write(data, dataLen) != dataLen) { + ESP_LOGE(TAG, "Failed to write config file"); + return false; + } + + file.close(); + + return true; +} +bool _trySaveConfig() { + flatbuffers::FlatBufferBuilder builder; + + auto fbsConfig = _configData.ToFlatbuffers(builder, true); + + builder.Finish(fbsConfig); + + return _trySaveConfig(builder.GetBufferPointer(), builder.GetSize()); +} + +void Config::Init() { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + return; + } + + if (!_configFS.begin(true, "/config", 3, "config")) { + ESP_PANIC(TAG, "Unable to mount config LittleFS partition!"); + } + + if (_tryLoadConfig()) { + return; + } + + ESP_LOGW(TAG, "Failed to load config, writing default config"); + + _configData.ToDefault(); + + if (!_trySaveConfig()) { + ESP_PANIC(TAG, "Failed to save default config. Recommend formatting microcontroller and re-flashing firmware"); + } +} + +std::string Config::GetAsJSON(bool withSensitiveData) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + return ""; + } + + cJSON* root = _configData.ToJSON(withSensitiveData); + + lock.unlock(); + + char* json = cJSON_PrintUnformatted(root); + + std::string result(json); + + free(json); + + cJSON_Delete(root); + + return result; +} +bool Config::SaveFromJSON(const std::string& json) { + cJSON* root = cJSON_Parse(json.c_str()); + if (root == nullptr) { + ESP_LOGE(TAG, "Failed to parse JSON: %s", cJSON_GetErrorPtr()); + return false; + } + + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + cJSON_Delete(root); + return false; + } + + bool result = _configData.FromJSON(root); + + cJSON_Delete(root); + + if (!result) { + ESP_LOGE(TAG, "Failed to read JSON"); + return false; + } + + return _trySaveConfig(); +} + +flatbuffers::Offset Config::GetAsFlatBuffer(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + return 0; + } + + return _configData.ToFlatbuffers(builder, withSensitiveData); +} + +bool Config::SaveFromFlatBuffer(const Serialization::Configuration::Config* config) { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + return false; + } + + if (!_configData.FromFlatbuffers(config)) { + ESP_LOGE(TAG, "Failed to read config file"); + return false; + } + + return _trySaveConfig(); +} + +bool Config::GetRaw(std::vector& buffer) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + return false; + } + + return _tryLoadConfig(buffer); +} + +bool Config::SetRaw(const std::uint8_t* buffer, std::size_t size) { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + return false; + } + + OpenShock::Config::RootConfig config; + if (!_tryDeserializeConfig(buffer, size, config)) { + ESP_LOGE(TAG, "Failed to deserialize config"); + return false; + } + + return _trySaveConfig(buffer, size); +} + +void Config::FactoryReset() { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + return; + } + + _configData.ToDefault(); + + if (!_configFS.remove("/config") && _configFS.exists("/config")) { + ESP_PANIC(TAG, "Failed to remove existing config file for factory reset. Reccomend formatting microcontroller and re-flashing firmware"); + } + + if (!_trySaveConfig()) { + ESP_PANIC(TAG, "Failed to save default config. Recommend formatting microcontroller and re-flashing firmware"); + } + + ESP_LOGI(TAG, "Factory reset complete"); +} + +bool Config::GetRFConfig(Config::RFConfig& out) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + return false; + } + + out = _configData.rf; + + return true; +} + +bool Config::GetWiFiConfig(Config::WiFiConfig& out) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + return false; + } + + out = _configData.wifi; + + return true; +} + +bool Config::GetOtaUpdateConfig(Config::OtaUpdateConfig& out) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + return false; + } + + out = _configData.otaUpdate; + + return true; +} + +bool Config::GetWiFiCredentials(cJSON* array, bool withSensitiveData) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + return false; + } + + for (auto& creds : _configData.wifi.credentialsList) { + cJSON* jsonCreds = creds.ToJSON(withSensitiveData); + + cJSON_AddItemToArray(array, jsonCreds); + } + + return true; +} + +bool Config::GetWiFiCredentials(std::vector& out) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + return false; + } + + out = _configData.wifi.credentialsList; + + return true; +} + +bool Config::SetRFConfig(const Config::RFConfig& config) { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + return false; + } + + _configData.rf = config; + return _trySaveConfig(); +} + +bool Config::SetWiFiConfig(const Config::WiFiConfig& config) { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + return false; + } + + _configData.wifi = config; + return _trySaveConfig(); +} + +bool Config::SetWiFiCredentials(const std::vector& credentials) { + bool foundZeroId = std::any_of(credentials.begin(), credentials.end(), [](const Config::WiFiCredentials& creds) { return creds.id == 0; }); + if (foundZeroId) { + ESP_LOGE(TAG, "Cannot set WiFi credentials: credential ID cannot be 0"); + return false; + } + + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + return false; + } + + _configData.wifi.credentialsList = credentials; + return _trySaveConfig(); +} + +bool Config::SetCaptivePortalConfig(const Config::CaptivePortalConfig& config) { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + return false; + } + + _configData.captivePortal = config; + return _trySaveConfig(); +} + +bool Config::SetSerialInputConfig(const Config::SerialInputConfig& config) { + _configData.serialInput = config; + return _trySaveConfig(); +} + +bool Config::GetSerialInputConfigEchoEnabled(bool& out) { + out = _configData.serialInput.echoEnabled; + return true; +} + +bool Config::SetSerialInputConfigEchoEnabled(bool enabled) { + _configData.serialInput.echoEnabled = enabled; + return _trySaveConfig(); +} + +bool Config::SetBackendConfig(const Config::BackendConfig& config) { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + return false; + } + + _configData.backend = config; + return _trySaveConfig(); +} + +bool Config::GetRFConfigTxPin(std::uint8_t& out) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + return false; + } + + out = _configData.rf.txPin; + + return true; +} + +bool Config::SetRFConfigTxPin(std::uint8_t txPin) { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + return false; + } + + _configData.rf.txPin = txPin; + return _trySaveConfig(); +} + +bool Config::GetRFConfigKeepAliveEnabled(bool& out) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + return false; + } + + out = _configData.rf.keepAliveEnabled; + + return true; +} + +bool Config::SetRFConfigKeepAliveEnabled(bool enabled) { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + return false; + } + + _configData.rf.keepAliveEnabled = enabled; + return _trySaveConfig(); +} + +bool Config::AnyWiFiCredentials(std::function predicate) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + } + + auto& creds = _configData.wifi.credentialsList; + + return std::any_of(creds.begin(), creds.end(), predicate); +} + +std::uint8_t Config::AddWiFiCredentials(const std::string& ssid, const std::string& password) { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + return 0; + } + + std::uint8_t id = 0; + + std::bitset<255> bits; + for (auto it = _configData.wifi.credentialsList.begin(); it != _configData.wifi.credentialsList.end(); ++it) { + auto& creds = *it; + + if (creds.ssid == ssid) { + creds.password = password; + + id = creds.id; + + break; + } + + if (creds.id == 0) { + ESP_LOGW(TAG, "Found WiFi credentials with ID 0, removing"); + it = _configData.wifi.credentialsList.erase(it); + continue; + } + + // Mark ID as used + bits[creds.id - 1] = true; + } + + // Get first available ID + for (std::size_t i = 0; i < bits.size(); ++i) { + if (!bits[i]) { + id = i + 1; + break; + } + } + + if (id == 0) { + ESP_LOGE(TAG, "Failed to add WiFi credentials: no available IDs"); + return 0; + } + + _configData.wifi.credentialsList.push_back({ + .id = id, + .ssid = ssid, + .password = password, + }); + _trySaveConfig(); + + return id; +} + +bool Config::TryGetWiFiCredentialsByID(std::uint8_t id, Config::WiFiCredentials& credentials) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + return false; + } + + for (const auto& creds : _configData.wifi.credentialsList) { + if (creds.id == id) { + credentials = creds; + return true; + } + } + + return false; +} + +bool Config::TryGetWiFiCredentialsBySSID(const char* ssid, Config::WiFiCredentials& credentials) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + return false; + } + + for (const auto& creds : _configData.wifi.credentialsList) { + if (creds.ssid == ssid) { + credentials = creds; + return true; + } + } + + return false; +} + +std::uint8_t Config::GetWiFiCredentialsIDbySSID(const char* ssid) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + return 0; + } + + for (const auto& creds : _configData.wifi.credentialsList) { + if (creds.ssid == ssid) { + return creds.id; + } + } + + return 0; +} + +bool Config::RemoveWiFiCredentials(std::uint8_t id) { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + return false; + } + + for (auto it = _configData.wifi.credentialsList.begin(); it != _configData.wifi.credentialsList.end(); ++it) { + if (it->id == id) { + _configData.wifi.credentialsList.erase(it); + _trySaveConfig(); + return true; + } + } + + return false; +} + +bool Config::ClearWiFiCredentials() { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + return false; + } + + _configData.wifi.credentialsList.clear(); + + return _trySaveConfig(); +} + +bool Config::GetOtaUpdateId(std::int32_t& out) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + } + + out = _configData.otaUpdate.updateId; + + return true; +} + +bool Config::SetOtaUpdateId(std::int32_t updateId) { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + } + + if (_configData.otaUpdate.updateId == updateId) { + return true; + } + + _configData.otaUpdate.updateId = updateId; + return _trySaveConfig(); +} + +bool Config::GetOtaUpdateStep(OtaUpdateStep& out) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + } + + out = _configData.otaUpdate.updateStep; + + return true; +} + +bool Config::SetOtaUpdateStep(OtaUpdateStep updateStep) { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + } + + if (_configData.otaUpdate.updateStep == updateStep) { + return true; + } + + _configData.otaUpdate.updateStep = updateStep; + return _trySaveConfig(); +} + +bool Config::HasBackendAuthToken() { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + return false; + } + + return !_configData.backend.authToken.empty(); +} + +bool Config::GetBackendAuthToken(std::string& out) { + ScopedReadLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire read lock"); + return false; + } + + out = _configData.backend.authToken; + + return true; +} + +bool Config::SetBackendAuthToken(const std::string& token) { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + return false; + } + + _configData.backend.authToken = token; + return _trySaveConfig(); +} + +bool Config::ClearBackendAuthToken() { + ScopedWriteLock lock(&_configMutex); + if (!lock.isLocked()) { + ESP_LOGE(TAG, "Failed to acquire write lock"); + return false; + } + + _configData.backend.authToken.clear(); + return _trySaveConfig(); +} diff --git a/src/config/OtaUpdateConfig.cpp b/src/config/OtaUpdateConfig.cpp new file mode 100644 index 00000000..a9839d0d --- /dev/null +++ b/src/config/OtaUpdateConfig.cpp @@ -0,0 +1,115 @@ +#include "config/OtaUpdateConfig.h" + +#include "config/internal/utils.h" +#include "Logging.h" + +const char* const TAG = "Config::OtaUpdateConfig"; + +using namespace OpenShock::Config; + +OtaUpdateConfig::OtaUpdateConfig() { + ToDefault(); +} + +OtaUpdateConfig::OtaUpdateConfig( + bool isEnabled, + std::string cdnDomain, + OtaUpdateChannel updateChannel, + bool checkOnStartup, + bool checkPeriodically, + std::uint16_t checkInterval, + bool allowBackendManagement, + bool requireManualApproval, + std::int32_t updateId, + OtaUpdateStep updateStep +) { + this->isEnabled = isEnabled; + this->cdnDomain = cdnDomain; + this->updateChannel = updateChannel; + this->checkOnStartup = checkOnStartup; + this->checkPeriodically = checkPeriodically; + this->checkInterval = checkInterval; + this->allowBackendManagement = allowBackendManagement; + this->requireManualApproval = requireManualApproval; + this->updateId = updateId; + this->updateStep = updateStep; +} + +void OtaUpdateConfig::ToDefault() { + isEnabled = true; + cdnDomain = OPENSHOCK_FW_CDN_DOMAIN; + updateChannel = OtaUpdateChannel::Stable; + checkOnStartup = false; + checkPeriodically = false; + checkInterval = 30; // 30 minutes + allowBackendManagement = true; + requireManualApproval = false; + updateId = 0; + updateStep = OtaUpdateStep::None; +} + +bool OtaUpdateConfig::FromFlatbuffers(const Serialization::Configuration::OtaUpdateConfig* config) { + if (config == nullptr) { + ESP_LOGE(TAG, "config is null"); + return false; + } + + isEnabled = config->is_enabled(); + Internal::Utils::FromFbsStr(cdnDomain, config->cdn_domain(), OPENSHOCK_FW_CDN_DOMAIN); + updateChannel = config->update_channel(); + checkOnStartup = config->check_on_startup(); + checkPeriodically = config->check_periodically(); + checkInterval = config->check_interval(); + allowBackendManagement = config->allow_backend_management(); + requireManualApproval = config->require_manual_approval(); + updateId = config->update_id(); + updateStep = config->update_step(); + + return true; +} + +flatbuffers::Offset OtaUpdateConfig::ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const { + return Serialization::Configuration::CreateOtaUpdateConfig(builder, isEnabled, builder.CreateString(cdnDomain), updateChannel, checkOnStartup, checkPeriodically, checkInterval, allowBackendManagement, requireManualApproval, updateId, updateStep); +} + +bool OtaUpdateConfig::FromJSON(const cJSON* json) { + if (json == nullptr) { + ESP_LOGE(TAG, "json is null"); + return false; + } + + if (!cJSON_IsObject(json)) { + ESP_LOGE(TAG, "json is not an object"); + return false; + } + + Internal::Utils::FromJsonBool(isEnabled, json, "isEnabled", true); + Internal::Utils::FromJsonStr(cdnDomain, json, "cdnDomain", OPENSHOCK_FW_CDN_DOMAIN); + Internal::Utils::FromJsonStrParsed(updateChannel, json, "updateChannel", OpenShock::TryParseOtaUpdateChannel, OpenShock::OtaUpdateChannel::Stable); + Internal::Utils::FromJsonBool(checkOnStartup, json, "checkOnStartup", true); + Internal::Utils::FromJsonBool(checkPeriodically, json, "checkPeriodically", false); + Internal::Utils::FromJsonU16(checkInterval, json, "checkInterval", 0); + Internal::Utils::FromJsonBool(allowBackendManagement, json, "allowBackendManagement", true); + Internal::Utils::FromJsonBool(requireManualApproval, json, "requireManualApproval", false); + Internal::Utils::FromJsonI32(updateId, json, "updateId", 0); + Internal::Utils::FromJsonStrParsed(updateStep, json, "updateStep", OpenShock::TryParseOtaUpdateStep, OpenShock::OtaUpdateStep::None); + + return true; +} + +cJSON* OtaUpdateConfig::ToJSON(bool withSensitiveData) const { + cJSON* root = cJSON_CreateObject(); + + cJSON_AddBoolToObject(root, "isEnabled", isEnabled); + cJSON_AddStringToObject(root, "cdnDomain", cdnDomain.c_str()); + cJSON_AddStringToObject(root, "updateChannel", OpenShock::Serialization::Configuration::EnumNameOtaUpdateChannel(updateChannel)); + cJSON_AddBoolToObject(root, "checkOnStartup", checkOnStartup); + cJSON_AddBoolToObject(root, "checkPeriodically", checkPeriodically); + cJSON_AddNumberToObject(root, "checkInterval", checkInterval); + cJSON_AddBoolToObject(root, "allowBackendManagement", allowBackendManagement); + cJSON_AddBoolToObject(root, "requireManualApproval", requireManualApproval); + cJSON_AddNumberToObject(root, "updateId", updateId); + cJSON_AddStringToObject(root, "updateStep", OpenShock::Serialization::Configuration::EnumNameOtaUpdateStep(updateStep)); + + return root; +} diff --git a/src/config/RFConfig.cpp b/src/config/RFConfig.cpp new file mode 100644 index 00000000..be8007be --- /dev/null +++ b/src/config/RFConfig.cpp @@ -0,0 +1,60 @@ +#include "config/RFConfig.h" + +#include "config/internal/utils.h" +#include "Common.h" +#include "Logging.h" + +const char* const TAG = "Config::RFConfig"; + +using namespace OpenShock::Config; + +RFConfig::RFConfig() : txPin(OpenShock::Constants::GPIO_RF_TX), keepAliveEnabled(true) { } + +RFConfig::RFConfig(std::uint8_t txPin, bool keepAliveEnabled) : txPin(txPin), keepAliveEnabled(keepAliveEnabled) { } + +void RFConfig::ToDefault() { + txPin = OpenShock::Constants::GPIO_RF_TX; + keepAliveEnabled = true; +} + +bool RFConfig::FromFlatbuffers(const Serialization::Configuration::RFConfig* config) { + if (config == nullptr) { + ESP_LOGE(TAG, "config is null"); + return false; + } + + txPin = config->tx_pin(); + keepAliveEnabled = config->keepalive_enabled(); + + return true; +} + +flatbuffers::Offset RFConfig::ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const { + return Serialization::Configuration::CreateRFConfig(builder, txPin, keepAliveEnabled); +} + +bool RFConfig::FromJSON(const cJSON* json) { + if (json == nullptr) { + ESP_LOGE(TAG, "json is null"); + return false; + } + + if (cJSON_IsObject(json) == 0) { + ESP_LOGE(TAG, "json is not an object"); + return false; + } + + Internal::Utils::FromJsonU8(txPin, json, "txPin", OpenShock::Constants::GPIO_RF_TX); + Internal::Utils::FromJsonBool(keepAliveEnabled, json, "keepAliveEnabled", true); + + return true; +} + +cJSON* RFConfig::ToJSON(bool withSensitiveData) const { + cJSON* root = cJSON_CreateObject(); + + cJSON_AddNumberToObject(root, "txPin", txPin); //-V2564 + cJSON_AddBoolToObject(root, "keepAliveEnabled", keepAliveEnabled); + + return root; +} diff --git a/src/config/RootConfig.cpp b/src/config/RootConfig.cpp new file mode 100644 index 00000000..20e8ba41 --- /dev/null +++ b/src/config/RootConfig.cpp @@ -0,0 +1,122 @@ +#include "config/RootConfig.h" + +#include "Logging.h" + +const char* const TAG = "Config::RootConfig"; + +using namespace OpenShock::Config; + +void RootConfig::ToDefault() { + rf.ToDefault(); + wifi.ToDefault(); + captivePortal.ToDefault(); + backend.ToDefault(); + serialInput.ToDefault(); +} + +bool RootConfig::FromFlatbuffers(const Serialization::Configuration::Config* config) { + if (config == nullptr) { + ESP_LOGE(TAG, "config is null"); + return false; + } + + if (!rf.FromFlatbuffers(config->rf())) { + ESP_LOGE(TAG, "Unable to load rf config"); + return false; + } + + if (!wifi.FromFlatbuffers(config->wifi())) { + ESP_LOGE(TAG, "Unable to load wifi config"); + return false; + } + + if (!captivePortal.FromFlatbuffers(config->captive_portal())) { + ESP_LOGE(TAG, "Unable to load captive portal config"); + return false; + } + + if (!backend.FromFlatbuffers(config->backend())) { + ESP_LOGE(TAG, "Unable to load backend config"); + return false; + } + + if (!serialInput.FromFlatbuffers(config->serial_input())) { + ESP_LOGE(TAG, "Unable to load serial input config"); + return false; + } + + if (!otaUpdate.FromFlatbuffers(config->ota_update())) { + ESP_LOGE(TAG, "Unable to load ota update config"); + return false; + } + + return true; +} + +flatbuffers::Offset RootConfig::ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const { + auto rfOffset = rf.ToFlatbuffers(builder, withSensitiveData); + auto wifiOffset = wifi.ToFlatbuffers(builder, withSensitiveData); + auto captivePortalOffset = captivePortal.ToFlatbuffers(builder, withSensitiveData); + auto backendOffset = backend.ToFlatbuffers(builder, withSensitiveData); + auto serialInputOffset = serialInput.ToFlatbuffers(builder, withSensitiveData); + auto otaUpdateOffset = otaUpdate.ToFlatbuffers(builder, withSensitiveData); + + return Serialization::Configuration::CreateConfig(builder, rfOffset, wifiOffset, captivePortalOffset, backendOffset, serialInputOffset, otaUpdateOffset); +} + +bool RootConfig::FromJSON(const cJSON* json) { + if (json == nullptr) { + ESP_LOGE(TAG, "json is null"); + return false; + } + + if (cJSON_IsObject(json) == 0) { + ESP_LOGE(TAG, "json is not an object"); + return false; + } + + if (!rf.FromJSON(cJSON_GetObjectItemCaseSensitive(json, "rf"))) { + ESP_LOGE(TAG, "Unable to load rf config"); + return false; + } + + if (!wifi.FromJSON(cJSON_GetObjectItemCaseSensitive(json, "wifi"))) { + ESP_LOGE(TAG, "Unable to load wifi config"); + return false; + } + + if (!captivePortal.FromJSON(cJSON_GetObjectItemCaseSensitive(json, "captivePortal"))) { + ESP_LOGE(TAG, "Unable to load captive portal config"); + return false; + } + + if (!backend.FromJSON(cJSON_GetObjectItemCaseSensitive(json, "backend"))) { + ESP_LOGE(TAG, "Unable to load backend config"); + return false; + } + + if (!serialInput.FromJSON(cJSON_GetObjectItemCaseSensitive(json, "serialInput"))) { + ESP_LOGE(TAG, "Unable to load serial input config"); + return false; + } + + if (!otaUpdate.FromJSON(cJSON_GetObjectItemCaseSensitive(json, "otaUpdate"))) { + ESP_LOGE(TAG, "Unable to load ota update config"); + return false; + } + + return true; +} + +cJSON* RootConfig::ToJSON(bool withSensitiveData) const { + cJSON* root = cJSON_CreateObject(); + + cJSON_AddItemToObject(root, "rf", rf.ToJSON(withSensitiveData)); + cJSON_AddItemToObject(root, "wifi", wifi.ToJSON(withSensitiveData)); + cJSON_AddItemToObject(root, "captivePortal", captivePortal.ToJSON(withSensitiveData)); + cJSON_AddItemToObject(root, "backend", backend.ToJSON(withSensitiveData)); + cJSON_AddItemToObject(root, "serialInput", serialInput.ToJSON(withSensitiveData)); + cJSON_AddItemToObject(root, "otaUpdate", otaUpdate.ToJSON(withSensitiveData)); + + return root; +} diff --git a/src/config/SerialInputConfig.cpp b/src/config/SerialInputConfig.cpp new file mode 100644 index 00000000..2274fbe4 --- /dev/null +++ b/src/config/SerialInputConfig.cpp @@ -0,0 +1,57 @@ +#include "config/SerialInputConfig.h" + +#include "config/internal/utils.h" +#include "Logging.h" + +const char* const TAG = "Config::SerialInputConfig"; + +using namespace OpenShock::Config; + +SerialInputConfig::SerialInputConfig() : echoEnabled(true) { } + +SerialInputConfig::SerialInputConfig(bool echoEnabled) { + this->echoEnabled = echoEnabled; +} + +void SerialInputConfig::ToDefault() { + echoEnabled = true; +} + +bool SerialInputConfig::FromFlatbuffers(const Serialization::Configuration::SerialInputConfig* config) { + if (config == nullptr) { + ESP_LOGE(TAG, "config is null"); + return false; + } + + echoEnabled = config->echo_enabled(); + + return true; +} + +flatbuffers::Offset SerialInputConfig::ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const { + return Serialization::Configuration::CreateSerialInputConfig(builder, echoEnabled); +} + +bool SerialInputConfig::FromJSON(const cJSON* json) { + if (json == nullptr) { + ESP_LOGE(TAG, "json is null"); + return false; + } + + if (cJSON_IsObject(json) == 0) { + ESP_LOGE(TAG, "json is not an object"); + return false; + } + + Internal::Utils::FromJsonBool(echoEnabled, json, "echoEnabled", true); + + return true; +} + +cJSON* SerialInputConfig::ToJSON(bool withSensitiveData) const { + cJSON* root = cJSON_CreateObject(); + + cJSON_AddBoolToObject(root, "echoEnabled", echoEnabled); + + return root; +} diff --git a/src/config/WiFiConfig.cpp b/src/config/WiFiConfig.cpp new file mode 100644 index 00000000..bddcb246 --- /dev/null +++ b/src/config/WiFiConfig.cpp @@ -0,0 +1,89 @@ +#include "config/WiFiConfig.h" + +#include "config/internal/utils.h" +#include "Logging.h" + +const char* const TAG = "Config::WiFiConfig"; + +using namespace OpenShock::Config; + +WiFiConfig::WiFiConfig() : accessPointSSID(OPENSHOCK_FW_AP_PREFIX), hostname(OPENSHOCK_FW_HOSTNAME), credentialsList() { } + +WiFiConfig::WiFiConfig(const std::string& accessPointSSID, const std::string& hostname, const std::vector& credentialsList) : accessPointSSID(accessPointSSID), hostname(hostname), credentialsList(credentialsList) { } + +void WiFiConfig::ToDefault() { + accessPointSSID = OPENSHOCK_FW_AP_PREFIX; + hostname = OPENSHOCK_FW_HOSTNAME; + credentialsList.clear(); +} + +bool WiFiConfig::FromFlatbuffers(const Serialization::Configuration::WiFiConfig* config) { + if (config == nullptr) { + ESP_LOGE(TAG, "config is null"); + return false; + } + + Internal::Utils::FromFbsStr(accessPointSSID, config->ap_ssid(), OPENSHOCK_FW_AP_PREFIX); + Internal::Utils::FromFbsStr(hostname, config->hostname(), OPENSHOCK_FW_HOSTNAME); + Internal::Utils::FromFbsVec(credentialsList, config->credentials()); + + return true; +} + +flatbuffers::Offset WiFiConfig::ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const { + std::vector> fbsCredentialsList; + fbsCredentialsList.reserve(credentialsList.size()); + + for (auto& credentials : credentialsList) { + fbsCredentialsList.emplace_back(credentials.ToFlatbuffers(builder, withSensitiveData)); + } + + return Serialization::Configuration::CreateWiFiConfig(builder, builder.CreateString(accessPointSSID), builder.CreateString(hostname), builder.CreateVector(fbsCredentialsList)); +} + +bool WiFiConfig::FromJSON(const cJSON* json) { + if (json == nullptr) { + ESP_LOGE(TAG, "json is null"); + return false; + } + + if (cJSON_IsObject(json) == 0) { + ESP_LOGE(TAG, "json is not an object"); + return false; + } + + Internal::Utils::FromJsonStr(accessPointSSID, json, "accessPointSSID", OPENSHOCK_FW_AP_PREFIX); + Internal::Utils::FromJsonStr(hostname, json, "hostname", OPENSHOCK_FW_HOSTNAME); + + const cJSON* credentialsListJson = cJSON_GetObjectItemCaseSensitive(json, "credentials"); + if (credentialsListJson == nullptr) { + ESP_LOGE(TAG, "credentials is null"); + return false; + } + + if (cJSON_IsArray(credentialsListJson) == 0) { + ESP_LOGE(TAG, "credentials is not an array"); + return false; + } + + Internal::Utils::FromJsonArray(credentialsList, credentialsListJson); + + return true; +} + +cJSON* WiFiConfig::ToJSON(bool withSensitiveData) const { + cJSON* root = cJSON_CreateObject(); + + cJSON_AddStringToObject(root, "accessPointSSID", accessPointSSID.c_str()); + cJSON_AddStringToObject(root, "hostname", hostname.c_str()); + + cJSON* credentialsListJson = cJSON_CreateArray(); + + for (auto& credentials : credentialsList) { + cJSON_AddItemToArray(credentialsListJson, credentials.ToJSON(withSensitiveData)); + } + + cJSON_AddItemToObject(root, "credentials", credentialsListJson); + + return root; +} diff --git a/src/config/WiFiCredentials.cpp b/src/config/WiFiCredentials.cpp new file mode 100644 index 00000000..546404ee --- /dev/null +++ b/src/config/WiFiCredentials.cpp @@ -0,0 +1,75 @@ +#include "config/WiFiCredentials.h" + +#include "config/internal/utils.h" +#include "Logging.h" +#include "util/HexUtils.h" + +const char* const TAG = "Config::WiFiCredentials"; + +using namespace OpenShock::Config; + +WiFiCredentials::WiFiCredentials() : id(0), ssid(), password() { } + +WiFiCredentials::WiFiCredentials(std::uint8_t id, const std::string& ssid, const std::string& password) : id(id), ssid(ssid), password(password) { } + +void WiFiCredentials::ToDefault() { + id = 0; + ssid.clear(); + password.clear(); +} + +bool WiFiCredentials::FromFlatbuffers(const Serialization::Configuration::WiFiCredentials* config) { + if (config == nullptr) { + ESP_LOGE(TAG, "config is null"); + return false; + } + + id = config->id(); + Internal::Utils::FromFbsStr(ssid, config->ssid(), ""); + Internal::Utils::FromFbsStr(password, config->password(), ""); + + return true; +} + +flatbuffers::Offset WiFiCredentials::ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const { + auto ssidOffset = builder.CreateString(ssid); + + flatbuffers::Offset passwordOffset; + if (withSensitiveData) { + passwordOffset = builder.CreateString(password); + } else { + passwordOffset = 0; + } + + return Serialization::Configuration::CreateWiFiCredentials(builder, id, ssidOffset, passwordOffset); +} + +bool WiFiCredentials::FromJSON(const cJSON* json) { + if (json == nullptr) { + ESP_LOGE(TAG, "json is null"); + return false; + } + + if (cJSON_IsObject(json) == 0) { + ESP_LOGE(TAG, "json is not an object"); + return false; + } + + Internal::Utils::FromJsonU8(id, json, "id", 0); + Internal::Utils::FromJsonStr(ssid, json, "ssid", ""); + Internal::Utils::FromJsonStr(password, json, "password", ""); + + return true; +} + +cJSON* WiFiCredentials::ToJSON(bool withSensitiveData) const { + cJSON* root = cJSON_CreateObject(); + + cJSON_AddNumberToObject(root, "id", id); //-V2564 + cJSON_AddStringToObject(root, "ssid", ssid.c_str()); + if (withSensitiveData) { + cJSON_AddStringToObject(root, "password", password.c_str()); + } + + return root; +} diff --git a/src/config/internal/utils.cpp b/src/config/internal/utils.cpp new file mode 100644 index 00000000..8b3d014f --- /dev/null +++ b/src/config/internal/utils.cpp @@ -0,0 +1,93 @@ +#include "config/internal/utils.h" + +#include "Logging.h" + +const char* const TAG = "Config::Internal::Utils"; + +using namespace OpenShock; + +template +bool _utilFromJsonInt(T& val, const cJSON* json, const char* name, T defaultVal, int minVal, int maxVal) { + static_assert(std::is_integral::value, "T must be an integral type"); + + const cJSON* jsonVal = cJSON_GetObjectItemCaseSensitive(json, name); + if (jsonVal == nullptr) { + val = defaultVal; + return true; + } + + if (cJSON_IsNumber(jsonVal) == 0) { + ESP_LOGE(TAG, "value at '%s' is not a number", name); + return false; + } + + int intVal = jsonVal->valueint; + + if (intVal < minVal) { + ESP_LOGE(TAG, "value at '%s' is less than %d", name, minVal); + return false; + } + + if (intVal > maxVal) { + ESP_LOGE(TAG, "value at '%s' is greater than %d", name, maxVal); + return false; + } + + val = static_cast(intVal); + + return true; +} + +void Config::Internal::Utils::FromFbsStr(std::string& str, const flatbuffers::String* fbsStr, const char* defaultStr) { + if (fbsStr != nullptr) { + str = fbsStr->c_str(); + } else { + str = defaultStr; + } +} + +bool Config::Internal::Utils::FromJsonBool(bool& val, const cJSON* json, const char* name, bool defaultVal) { + const cJSON* jsonVal = cJSON_GetObjectItemCaseSensitive(json, name); + if (jsonVal == nullptr) { + val = defaultVal; + return true; + } + + if (cJSON_IsBool(jsonVal) == 0) { + ESP_LOGE(TAG, "value at '%s' is not a bool", name); + return false; + } + + val = cJSON_IsTrue(jsonVal); + + return true; +} + +bool Config::Internal::Utils::FromJsonU8(std::uint8_t& val, const cJSON* json, const char* name, std::uint8_t defaultVal) { + return _utilFromJsonInt(val, json, name, defaultVal, 0, UINT8_MAX); +} + +bool Config::Internal::Utils::FromJsonU16(std::uint16_t& val, const cJSON* json, const char* name, std::uint16_t defaultVal) { + return _utilFromJsonInt(val, json, name, defaultVal, 0, UINT16_MAX); +} + +bool Config::Internal::Utils::FromJsonI32(std::int32_t& val, const cJSON* json, const char* name, std::int32_t defaultVal) { + return _utilFromJsonInt(val, json, name, defaultVal, INT32_MIN, INT32_MAX); +} + +bool Config::Internal::Utils::FromJsonStr(std::string& str, const cJSON* json, const char* name, const char* defaultStr) { + const cJSON* jsonVal = cJSON_GetObjectItemCaseSensitive(json, name); + if (jsonVal == nullptr) { + str = defaultStr; + return true; + } + + if (cJSON_IsString(jsonVal) == 0) { + ESP_LOGE(TAG, "value at '%s' is not a string", name); + return false; + } + + str = jsonVal->valuestring; + + return true; +} diff --git a/src/event_handlers/WiFiScan.cpp b/src/event_handlers/WiFiScan.cpp deleted file mode 100644 index 60529fe2..00000000 --- a/src/event_handlers/WiFiScan.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "event_handlers/WiFiScan.h" - -#include "CaptivePortal.h" -#include "Config.h" -#include "serialization/WSLocal.h" -#include "wifi/WiFiManager.h" -#include "wifi/WiFiNetwork.h" -#include "wifi/WiFiScanManager.h" -#include "wifi/WiFiScanStatus.h" - -const char* const TAG = "WiFiScanEventHandlers"; - -static std::uint64_t s_scanStatusChangedHandle = 0; -static std::uint64_t s_scanNetworkDiscoveredHandle = 0; - -using namespace OpenShock; - -void _scanStatusChangedHandler(OpenShock::WiFiScanStatus status) { - Serialization::Local::SerializeWiFiScanStatusChangedEvent(status, CaptivePortal::BroadcastMessageBIN); -} - -void _scanNetworkDiscoveredHandler(const wifi_ap_record_t* record) { - std::uint8_t id = Config::GetWiFiCredentialsIDbyBSSIDorSSID(record->bssid, reinterpret_cast(record->ssid)); - WiFiNetwork network(record, id); - - Serialization::Local::SerializeWiFiNetworkEvent(Serialization::Types::WifiNetworkEventType::Discovered, network, CaptivePortal::BroadcastMessageBIN); -} - -void OpenShock::EventHandlers::WiFiScan::Init() { - s_scanStatusChangedHandle = WiFiScanManager::RegisterStatusChangedHandler(_scanStatusChangedHandler); - s_scanNetworkDiscoveredHandle = WiFiScanManager::RegisterNetworkDiscoveryHandler(_scanNetworkDiscoveredHandler); -} - -void OpenShock::EventHandlers::WiFiScan::Deinit() { - WiFiScanManager::UnregisterStatusChangedHandler(s_scanStatusChangedHandle); - WiFiScanManager::UnregisterNetworkDiscoveredHandler(s_scanNetworkDiscoveredHandle); -} diff --git a/src/event_handlers/websocket/Gateway.cpp b/src/event_handlers/websocket/Gateway.cpp index 4c9ef0f7..2d3849ef 100644 --- a/src/event_handlers/websocket/Gateway.cpp +++ b/src/event_handlers/websocket/Gateway.cpp @@ -4,7 +4,7 @@ #include "Logging.h" -#include "serialization/_fbs/ServerToDeviceMessage_generated.h" +#include "serialization/_fbs/GatewayToDeviceMessage_generated.h" #include @@ -13,9 +13,9 @@ static const char* TAG = "ServerMessageHandlers"; -namespace Schemas = OpenShock::Serialization; +namespace Schemas = OpenShock::Serialization::Gateway; namespace Handlers = OpenShock::MessageHandlers::Server::_Private; -typedef Schemas::ServerToDeviceMessagePayload PayloadType; +typedef Schemas::GatewayToDeviceMessagePayload PayloadType; using namespace OpenShock; @@ -29,13 +29,14 @@ static std::array s_serverHandlers = []() SET_HANDLER(PayloadType::ShockerCommandList, Handlers::HandleShockerCommandList); SET_HANDLER(PayloadType::CaptivePortalConfig, Handlers::HandleCaptivePortalConfig); + SET_HANDLER(PayloadType::OtaInstall, Handlers::HandleOtaInstall); return handlers; }(); void EventHandlers::WebSocket::HandleGatewayBinary(const std::uint8_t* data, std::size_t len) { // Deserialize - auto msg = flatbuffers::GetRoot(data); + auto msg = flatbuffers::GetRoot(data); if (msg == nullptr) { ESP_LOGE(TAG, "Failed to deserialize message"); return; diff --git a/src/event_handlers/websocket/gateway/CaptivePortalConfig.cpp b/src/event_handlers/websocket/gateway/CaptivePortalConfig.cpp index 60616e30..b29216d3 100644 --- a/src/event_handlers/websocket/gateway/CaptivePortalConfig.cpp +++ b/src/event_handlers/websocket/gateway/CaptivePortalConfig.cpp @@ -9,7 +9,7 @@ const char* const TAG = "ServerMessageHandlers"; using namespace OpenShock::MessageHandlers::Server; -void _Private::HandleCaptivePortalConfig(const OpenShock::Serialization::ServerToDeviceMessage* root) { +void _Private::HandleCaptivePortalConfig(const OpenShock::Serialization::Gateway::GatewayToDeviceMessage* root) { auto msg = root->payload_as_CaptivePortalConfig(); if (msg == nullptr) { ESP_LOGE(TAG, "Payload cannot be parsed as CaptivePortalConfig"); diff --git a/src/event_handlers/websocket/gateway/OtaInstall.cpp b/src/event_handlers/websocket/gateway/OtaInstall.cpp new file mode 100644 index 00000000..18634fee --- /dev/null +++ b/src/event_handlers/websocket/gateway/OtaInstall.cpp @@ -0,0 +1,42 @@ +#include "event_handlers/impl/WSGateway.h" + +#include "CaptivePortal.h" +#include "Logging.h" +#include "OtaUpdateManager.h" + +#include + +const char* const TAG = "ServerMessageHandlers"; + +using namespace OpenShock::MessageHandlers::Server; + +void _Private::HandleOtaInstall(const OpenShock::Serialization::Gateway::GatewayToDeviceMessage* root) { + auto msg = root->payload_as_OtaInstall(); + if (msg == nullptr) { + ESP_LOGE(TAG, "Payload cannot be parsed as OtaInstall"); + return; + } + + auto semver = msg->version(); + if (semver == nullptr) { + ESP_LOGE(TAG, "Version cannot be parsed"); + return; + } + + StringView prerelease, build; + if (semver->prerelease() != nullptr) { + prerelease = StringView(semver->prerelease()->c_str(), semver->prerelease()->size()); + } + if (semver->build() != nullptr) { + build = StringView(semver->build()->c_str(), semver->build()->size()); + } + + OpenShock::SemVer version(semver->major(), semver->minor(), semver->patch(), prerelease, build); + + ESP_LOGI(TAG, "OTA install requested for version %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + + if (!OpenShock::OtaUpdateManager::TryStartFirmwareInstallation(version)) { + ESP_LOGE(TAG, "Failed to install firmware"); // TODO: Send error message to server + return; + } +} diff --git a/src/event_handlers/websocket/gateway/ShockerCommandList.cpp b/src/event_handlers/websocket/gateway/ShockerCommandList.cpp index d1b9f6a2..6145c16d 100644 --- a/src/event_handlers/websocket/gateway/ShockerCommandList.cpp +++ b/src/event_handlers/websocket/gateway/ShockerCommandList.cpp @@ -10,7 +10,7 @@ const char* const TAG = "ServerMessageHandlers"; using namespace OpenShock::MessageHandlers::Server; -void _Private::HandleShockerCommandList(const OpenShock::Serialization::ServerToDeviceMessage* root) { +void _Private::HandleShockerCommandList(const OpenShock::Serialization::Gateway::GatewayToDeviceMessage* root) { auto msg = root->payload_as_ShockerCommandList(); if (msg == nullptr) { ESP_LOGE(TAG, "Payload cannot be parsed as ShockerCommandList"); @@ -23,7 +23,7 @@ void _Private::HandleShockerCommandList(const OpenShock::Serialization::ServerTo return; } - ESP_LOGV(TAG, "Received command list from API (%llu commands)", commands->size()); + ESP_LOGV(TAG, "Received command list from API (%u commands)", commands->size()); for (auto command : *commands) { std::uint16_t id = command->id(); diff --git a/src/event_handlers/websocket/gateway/_InvalidMessage.cpp b/src/event_handlers/websocket/gateway/_InvalidMessage.cpp index 68ef41b1..f5eb97d8 100644 --- a/src/event_handlers/websocket/gateway/_InvalidMessage.cpp +++ b/src/event_handlers/websocket/gateway/_InvalidMessage.cpp @@ -7,7 +7,7 @@ const char* const TAG = "ServerMessageHandlers"; using namespace OpenShock::MessageHandlers::Server; -void _Private::HandleInvalidMessage(const OpenShock::Serialization::ServerToDeviceMessage* root) { +void _Private::HandleInvalidMessage(const OpenShock::Serialization::Gateway::GatewayToDeviceMessage* root) { if (root == nullptr) { ESP_LOGE(TAG, "Message cannot be parsed"); return; diff --git a/src/event_handlers/websocket/local/AccountLinkCommand.cpp b/src/event_handlers/websocket/local/AccountLinkCommand.cpp index 90f9605d..c052beee 100644 --- a/src/event_handlers/websocket/local/AccountLinkCommand.cpp +++ b/src/event_handlers/websocket/local/AccountLinkCommand.cpp @@ -44,7 +44,7 @@ void _Private::HandleAccountLinkCommand(std::uint8_t socketId, const OpenShock:: return; } - auto result = GatewayConnectionManager::Pair(code->data()); + auto result = GatewayConnectionManager::Link(code->data()); serializeSetRfTxPinResult(socketId, result); } diff --git a/src/event_handlers/websocket/local/AccountUnlinkCommand.cpp b/src/event_handlers/websocket/local/AccountUnlinkCommand.cpp index 7be03289..aacba07a 100644 --- a/src/event_handlers/websocket/local/AccountUnlinkCommand.cpp +++ b/src/event_handlers/websocket/local/AccountUnlinkCommand.cpp @@ -10,11 +10,13 @@ const char* const TAG = "LocalMessageHandlers"; using namespace OpenShock::MessageHandlers::Local; void _Private::HandleAccountUnlinkCommand(std::uint8_t socketId, const OpenShock::Serialization::Local::LocalToDeviceMessage* root) { + (void)socketId; + auto msg = root->payload_as_AccountUnlinkCommand(); if (msg == nullptr) { ESP_LOGE(TAG, "Payload cannot be parsed as AccountUnlinkCommand"); return; } - GatewayConnectionManager::UnPair(); + GatewayConnectionManager::UnLink(); } diff --git a/src/event_handlers/websocket/local/SetRfTxPinCommand.cpp b/src/event_handlers/websocket/local/SetRfTxPinCommand.cpp index 2fb72b3b..d7456a13 100644 --- a/src/event_handlers/websocket/local/SetRfTxPinCommand.cpp +++ b/src/event_handlers/websocket/local/SetRfTxPinCommand.cpp @@ -2,7 +2,7 @@ #include "CaptivePortal.h" #include "CommandHandler.h" -#include "Constants.h" +#include "Common.h" #include "Logging.h" #include @@ -18,8 +18,8 @@ void serializeSetRfTxPinResult(std::uint8_t socketId, std::uint8_t pin, OpenShoc builder.Finish(msgOffset); - auto buffer = builder.GetBufferPointer(); - auto size = builder.GetSize(); + const std::uint8_t* buffer = builder.GetBufferPointer(); + std::uint8_t size = builder.GetSize(); OpenShock::CaptivePortal::SendMessageBIN(socketId, buffer, size); } diff --git a/src/event_handlers/websocket/local/WiFiNetworkConnectCommand.cpp b/src/event_handlers/websocket/local/WiFiNetworkConnectCommand.cpp index 947e82ed..445098c9 100644 --- a/src/event_handlers/websocket/local/WiFiNetworkConnectCommand.cpp +++ b/src/event_handlers/websocket/local/WiFiNetworkConnectCommand.cpp @@ -1,11 +1,9 @@ #include "event_handlers/impl/WSLocal.h" #include "Logging.h" -#include "Utils/HexUtils.h" +#include "util/HexUtils.h" #include "wifi/WiFiManager.h" -#include - #include const char* const TAG = "LocalMessageHandlers"; @@ -13,6 +11,8 @@ const char* const TAG = "LocalMessageHandlers"; using namespace OpenShock::MessageHandlers::Local; void _Private::HandleWiFiNetworkConnectCommand(std::uint8_t socketId, const OpenShock::Serialization::Local::LocalToDeviceMessage* root) { + (void)socketId; + auto msg = root->payload_as_WifiNetworkConnectCommand(); if (msg == nullptr) { ESP_LOGE(TAG, "Payload cannot be parsed as WiFiNetworkConnectCommand"); @@ -26,7 +26,7 @@ void _Private::HandleWiFiNetworkConnectCommand(std::uint8_t socketId, const Open return; } - if (ssid != nullptr && ssid->size() > 31) { + if (ssid->size() > 31) { ESP_LOGE(TAG, "WiFi SSID is too long"); return; } diff --git a/src/event_handlers/websocket/local/WiFiNetworkDisconnectCommand.cpp b/src/event_handlers/websocket/local/WiFiNetworkDisconnectCommand.cpp index b8d6a5e4..dfc6e913 100644 --- a/src/event_handlers/websocket/local/WiFiNetworkDisconnectCommand.cpp +++ b/src/event_handlers/websocket/local/WiFiNetworkDisconnectCommand.cpp @@ -1,11 +1,9 @@ #include "event_handlers/impl/WSLocal.h" #include "Logging.h" -#include "Utils/HexUtils.h" +#include "util/HexUtils.h" #include "wifi/WiFiManager.h" -#include - #include const char* const TAG = "LocalMessageHandlers"; @@ -13,6 +11,8 @@ const char* const TAG = "LocalMessageHandlers"; using namespace OpenShock::MessageHandlers::Local; void _Private::HandleWiFiNetworkDisconnectCommand(std::uint8_t socketId, const OpenShock::Serialization::Local::LocalToDeviceMessage* root) { + (void)socketId; + auto msg = root->payload_as_WifiNetworkDisconnectCommand(); if (msg == nullptr) { ESP_LOGE(TAG, "Payload cannot be parsed as WiFiNetworkDisconnectCommand"); diff --git a/src/event_handlers/websocket/local/WiFiNetworkForgetCommand.cpp b/src/event_handlers/websocket/local/WiFiNetworkForgetCommand.cpp index c43ed572..6514893e 100644 --- a/src/event_handlers/websocket/local/WiFiNetworkForgetCommand.cpp +++ b/src/event_handlers/websocket/local/WiFiNetworkForgetCommand.cpp @@ -1,11 +1,9 @@ #include "event_handlers/impl/WSLocal.h" #include "Logging.h" -#include "Utils/HexUtils.h" +#include "util/HexUtils.h" #include "wifi/WiFiManager.h" -#include - #include const char* const TAG = "LocalMessageHandlers"; @@ -13,6 +11,8 @@ const char* const TAG = "LocalMessageHandlers"; using namespace OpenShock::MessageHandlers::Local; void _Private::HandleWiFiNetworkForgetCommand(std::uint8_t socketId, const OpenShock::Serialization::Local::LocalToDeviceMessage* root) { + (void)socketId; + auto msg = root->payload_as_WifiNetworkForgetCommand(); if (msg == nullptr) { ESP_LOGE(TAG, "Payload cannot be parsed as WiFiNetworkForgetCommand"); diff --git a/src/event_handlers/websocket/local/WiFiNetworkSaveCommand.cpp b/src/event_handlers/websocket/local/WiFiNetworkSaveCommand.cpp index fdf511dc..3a254cee 100644 --- a/src/event_handlers/websocket/local/WiFiNetworkSaveCommand.cpp +++ b/src/event_handlers/websocket/local/WiFiNetworkSaveCommand.cpp @@ -1,11 +1,9 @@ #include "event_handlers/impl/WSLocal.h" #include "Logging.h" -#include "Utils/HexUtils.h" +#include "util/HexUtils.h" #include "wifi/WiFiManager.h" -#include - #include const char* const TAG = "LocalMessageHandlers"; @@ -13,6 +11,8 @@ const char* const TAG = "LocalMessageHandlers"; using namespace OpenShock::MessageHandlers::Local; void _Private::HandleWiFiNetworkSaveCommand(std::uint8_t socketId, const OpenShock::Serialization::Local::LocalToDeviceMessage* root) { + (void)socketId; + auto msg = root->payload_as_WifiNetworkSaveCommand(); if (msg == nullptr) { ESP_LOGE(TAG, "Payload cannot be parsed as WiFiNetworkSaveCommand"); @@ -20,10 +20,10 @@ void _Private::HandleWiFiNetworkSaveCommand(std::uint8_t socketId, const OpenSho } auto ssid = msg->ssid(); - auto password = msg->password(); + auto password = msg->password() ? msg->password()->str() : ""; - if (ssid == nullptr || password == nullptr) { - ESP_LOGE(TAG, "WiFi message is missing required properties"); + if (ssid == nullptr) { + ESP_LOGE(TAG, "WiFi message is missing SSID"); return; } @@ -32,12 +32,19 @@ void _Private::HandleWiFiNetworkSaveCommand(std::uint8_t socketId, const OpenSho return; } - if (password->size() > 63) { + std::size_t passwordLength = password.size(); + + if (passwordLength != 0 && passwordLength < 8) { + ESP_LOGE(TAG, "WiFi password is too short"); + return; + } + + if (passwordLength > 63) { ESP_LOGE(TAG, "WiFi password is too long"); return; } - if (!WiFiManager::Save(ssid->c_str(), password->str())) { // TODO: support hidden networks + if (!WiFiManager::Save(ssid->c_str(), password)) { ESP_LOGE(TAG, "Failed to save WiFi network"); } } diff --git a/src/event_handlers/websocket/local/WiFiScanCommand.cpp b/src/event_handlers/websocket/local/WiFiScanCommand.cpp index 430846a9..9450198c 100644 --- a/src/event_handlers/websocket/local/WiFiScanCommand.cpp +++ b/src/event_handlers/websocket/local/WiFiScanCommand.cpp @@ -8,6 +8,8 @@ const char* const TAG = "LocalMessageHandlers"; using namespace OpenShock::MessageHandlers::Local; void _Private::HandleWiFiScanCommand(std::uint8_t socketId, const OpenShock::Serialization::Local::LocalToDeviceMessage* root) { + (void)socketId; + auto msg = root->payload_as_WifiScanCommand(); if (msg == nullptr) { ESP_LOGE(TAG, "Payload cannot be parsed as WiFiScanCommand"); diff --git a/src/event_handlers/websocket/local/_InvalidMessage.cpp b/src/event_handlers/websocket/local/_InvalidMessage.cpp index c4945730..cb7b5817 100644 --- a/src/event_handlers/websocket/local/_InvalidMessage.cpp +++ b/src/event_handlers/websocket/local/_InvalidMessage.cpp @@ -7,6 +7,8 @@ const char* const TAG = "LocalMessageHandlers"; using namespace OpenShock::MessageHandlers::Local; void _Private::HandleInvalidMessage(std::uint8_t socketId, const OpenShock::Serialization::Local::LocalToDeviceMessage* root) { + (void)socketId; + if (root == nullptr) { ESP_LOGE(TAG, "Message cannot be parsed"); return; diff --git a/src/http/HTTPRequestManager.cpp b/src/http/HTTPRequestManager.cpp new file mode 100644 index 00000000..855b58ac --- /dev/null +++ b/src/http/HTTPRequestManager.cpp @@ -0,0 +1,552 @@ +#include "http/HTTPRequestManager.h" + +#include "Common.h" +#include "Time.h" + +#include + +#include +#include +#include +#include +#include + +constexpr std::size_t HTTP_BUFFER_SIZE = 4096LLU; +constexpr int HTTP_DOWNLOAD_SIZE_LIMIT = 200 * 1024 * 1024; // 200 MB + +const char* const TAG = "HTTPRequestManager"; + +struct RateLimit { + RateLimit() : m_mutex(xSemaphoreCreateMutex()), m_blockUntilMs(0), m_limits(), m_requests() { } + + void addLimit(std::uint32_t durationMs, std::uint16_t count) { + xSemaphoreTake(m_mutex, portMAX_DELAY); + + // Insert sorted + m_limits.insert(std::upper_bound(m_limits.begin(), m_limits.end(), durationMs, [](std::int64_t durationMs, const Limit& limit) { return durationMs > limit.durationMs; }), {durationMs, count}); + + xSemaphoreGive(m_mutex); + } + void clearLimits() { + xSemaphoreTake(m_mutex, portMAX_DELAY); + + m_limits.clear(); + + xSemaphoreGive(m_mutex); + } + + bool tryRequest() { + std::int64_t now = OpenShock::millis(); + + xSemaphoreTake(m_mutex, portMAX_DELAY); + + if (m_blockUntilMs > now) { + xSemaphoreGive(m_mutex); + return false; + } + + // Remove all requests that are older than the biggest limit + while (!m_requests.empty() && m_requests.front() < now - m_limits.back().durationMs) { + m_requests.erase(m_requests.begin()); + } + + // Check if we've exceeded any limits + auto it = std::find_if(m_limits.begin(), m_limits.end(), [this](const RateLimit::Limit& limit) { return m_requests.size() >= limit.count; }); + if (it != m_limits.end()) { + m_blockUntilMs = now + it->durationMs; + xSemaphoreGive(m_mutex); + return false; + } + + // Add the request + m_requests.push_back(now); + + xSemaphoreGive(m_mutex); + + return true; + } + void clearRequests() { + xSemaphoreTake(m_mutex, portMAX_DELAY); + m_requests.clear(); + xSemaphoreGive(m_mutex); + } + + void blockUntil(std::int64_t blockUntilMs) { + xSemaphoreTake(m_mutex, portMAX_DELAY); + m_blockUntilMs = blockUntilMs; + xSemaphoreGive(m_mutex); + } + + std::uint32_t requestsSince(std::int64_t sinceMs) { + xSemaphoreTake(m_mutex, portMAX_DELAY); + std::uint32_t result = std::count_if(m_requests.begin(), m_requests.end(), [sinceMs](std::int64_t requestMs) { return requestMs >= sinceMs; }); + xSemaphoreGive(m_mutex); + return result; + } + +private: + struct Limit { + std::int64_t durationMs; + std::uint16_t count; + }; + + SemaphoreHandle_t m_mutex; + std::int64_t m_blockUntilMs; + std::vector m_limits; + std::vector m_requests; +}; + +SemaphoreHandle_t s_rateLimitsMutex = xSemaphoreCreateMutex(); +std::unordered_map> s_rateLimits; + +using namespace OpenShock; + +StringView _getDomain(StringView url) { + if (url.isNullOrEmpty()) { + return StringView::Null(); + } + + // Remove the protocol, port, and path eg. "https://api.example.com:443/path" -> "api.example.com" + url = url.afterDelimiter("://").beforeDelimiter('/').beforeDelimiter(':'); + + // Remove all subdomains eg. "api.example.com" -> "example.com" + auto domainSep = url.rfind('.'); + if (domainSep == StringView::npos) { + return url; // E.g. "localhost" + } + domainSep = url.rfind('.', domainSep - 1); + if (domainSep != StringView::npos) { + url = url.substr(domainSep + 1); + } + + return url; +} + +std::shared_ptr _rateLimitFactory(StringView domain) { + auto rateLimit = std::make_shared(); + + // Add default limits + rateLimit->addLimit(1000, 5); // 5 per second + rateLimit->addLimit(10 * 1000, 10); // 10 per 10 seconds + + // per-domain limits + if (domain == OPENSHOCK_API_DOMAIN) { + rateLimit->addLimit(60 * 1000, 12); // 12 per minute + rateLimit->addLimit(60 * 60 * 1000, 120); // 120 per hour + } + + return rateLimit; +} + +std::shared_ptr _getRateLimiter(StringView url) { + auto domain = _getDomain(url).toString(); + if (domain.empty()) { + return nullptr; + } + + xSemaphoreTake(s_rateLimitsMutex, portMAX_DELAY); + + auto it = s_rateLimits.find(domain); + if (it == s_rateLimits.end()) { + s_rateLimits.emplace(domain, _rateLimitFactory(domain)); + it = s_rateLimits.find(domain); + } + + xSemaphoreGive(s_rateLimitsMutex); + + return it->second; +} + +void _setupClient(HTTPClient& client) { + client.setUserAgent(OpenShock::Constants::FW_USERAGENT); +} + +struct StreamReaderResult { + HTTP::RequestResult result; + std::size_t nWritten; +}; + +constexpr bool _isCRLF(const uint8_t* buffer) { + return buffer[0] == '\r' && buffer[1] == '\n'; +} +constexpr bool _tryFindCRLF(std::size_t& pos, const uint8_t* buffer, std::size_t len) { + const std::uint8_t* cur = buffer; + const std::uint8_t* end = buffer + len - 1; + + while (cur < end) { + if (_isCRLF(cur)) { + pos = static_cast(cur - buffer); + return true; + } + + ++cur; + } + + return false; +} +constexpr bool _tryParseHexSizeT(std::size_t& result, StringView str) { + if (str.isNullOrEmpty() || str.size() > sizeof(std::size_t) * 2) { + return false; + } + + result = 0; + + for (char c : str) { + if (c >= '0' && c <= '9') { + result = (result << 4) | (c - '0'); + } else if (c >= 'a' && c <= 'f') { + result = (result << 4) | (c - 'a' + 10); + } else if (c >= 'A' && c <= 'F') { + result = (result << 4) | (c - 'A' + 10); + } else { + return false; + } + } + + return true; +} + +enum ParserState : std::uint8_t { + Ok, + NeedMoreData, + Invalid, +}; + +ParserState _parseChunkHeader(const std::uint8_t* buffer, std::size_t bufferLen, std::size_t& headerLen, std::size_t& payloadLen) { + if (bufferLen < 5) { // Bare minimum: "0\r\n\r\n" + return ParserState::NeedMoreData; + } + + // Find the first CRLF + if (!_tryFindCRLF(headerLen, buffer, bufferLen)) { + return ParserState::NeedMoreData; + } + + // Header must have at least one character + if (headerLen == 0) { + ESP_LOGW(TAG, "Invalid chunk header length"); + return ParserState::Invalid; + } + + // Check for end of size field (possibly followed by extensions which is separated by a semicolon) + std::size_t sizeFieldEnd = headerLen; + for (std::size_t i = 0; i < headerLen; ++i) { + if (buffer[i] == ';') { + sizeFieldEnd = i; + break; + } + } + + // Bounds check + if (sizeFieldEnd == 0 || sizeFieldEnd > 16) { + ESP_LOGW(TAG, "Invalid chunk size field length"); + return ParserState::Invalid; + } + + StringView sizeField(reinterpret_cast(buffer), sizeFieldEnd); + + // Parse the chunk size + if (!_tryParseHexSizeT(payloadLen, sizeField)) { + ESP_LOGW(TAG, "Failed to parse chunk size"); + return ParserState::Invalid; + } + + if (payloadLen > HTTP_DOWNLOAD_SIZE_LIMIT) { + ESP_LOGW(TAG, "Chunk size too large"); + return ParserState::Invalid; + } + + // Set the header length to the end of the CRLF + headerLen += 2; + + return ParserState::Ok; +} + +ParserState _parseChunk(const std::uint8_t* buffer, std::size_t bufferLen, std::size_t& payloadPos, std::size_t& payloadLen) { + if (payloadPos == 0) { + ParserState state = _parseChunkHeader(buffer, bufferLen, payloadPos, payloadLen); + if (state != ParserState::Ok) { + return state; + } + } + + std::size_t totalLen = payloadPos + payloadLen + 2; // +2 for CRLF + if (bufferLen < totalLen) { + return ParserState::NeedMoreData; + } + + // Check for CRLF + if (!_isCRLF(buffer + totalLen - 2)) { + ESP_LOGW(TAG, "Invalid chunk payload CRLF"); + return ParserState::Invalid; + } + + return ParserState::Ok; +} + +void _alignChunk(std::uint8_t* buffer, std::size_t& bufferCursor, std::size_t payloadPos, std::size_t payloadLen) { + std::size_t totalLen = payloadPos + payloadLen + 2; // +2 for CRLF + std::size_t remaining = bufferCursor - totalLen; + if (remaining > 0) { + memmove(buffer, buffer + totalLen, remaining); + bufferCursor = remaining; + } else { + bufferCursor = 0; + } +} + +StreamReaderResult _readStreamDataChunked(HTTPClient& client, WiFiClient* stream, HTTP::DownloadCallback downloadCallback, std::int64_t begin, std::uint32_t timeoutMs) { + std::size_t totalWritten = 0; + HTTP::RequestResult result = HTTP::RequestResult::Success; + + std::uint8_t* buffer = static_cast(malloc(HTTP_BUFFER_SIZE)); + if (buffer == nullptr) { + ESP_LOGE(TAG, "Out of memory"); + return {HTTP::RequestResult::RequestFailed, 0}; + } + + ParserState state = ParserState::NeedMoreData; + std::size_t bufferCursor = 0, payloadPos = 0, payloadSize = 0; + + while (client.connected() && state != ParserState::Invalid) { + if (begin + timeoutMs < OpenShock::millis()) { + ESP_LOGW(TAG, "Request timed out"); + result = HTTP::RequestResult::TimedOut; + break; + } + + std::size_t bytesAvailable = stream->available(); + if (bytesAvailable == 0) { + vTaskDelay(pdMS_TO_TICKS(5)); + continue; + } + + std::size_t bytesRead = stream->readBytes(buffer + bufferCursor, HTTP_BUFFER_SIZE - bufferCursor); + if (bytesRead == 0) { + ESP_LOGW(TAG, "No bytes read"); + result = HTTP::RequestResult::RequestFailed; + break; + } + + bufferCursor += bytesRead; + +parseMore: + state = _parseChunk(buffer, bufferCursor, payloadPos, payloadSize); + if (state == ParserState::Invalid) { + ESP_LOGE(TAG, "Failed to parse chunk"); + result = HTTP::RequestResult::RequestFailed; + break; + } + ESP_LOGD(TAG, "Chunk parsed: %zu %zu", payloadPos, payloadSize); + + if (state == ParserState::NeedMoreData) { + if (bufferCursor == HTTP_BUFFER_SIZE) { + ESP_LOGE(TAG, "Chunk too large"); + result = HTTP::RequestResult::RequestFailed; + break; + } + continue; + } + + // Check for zero chunk size (end of transfer) + if (payloadSize == 0) { + break; + } + + if (!downloadCallback(totalWritten, buffer + payloadPos, payloadSize)) { + result = HTTP::RequestResult::Cancelled; + break; + } + + totalWritten += payloadSize; + _alignChunk(buffer, bufferCursor, payloadPos, payloadSize); + payloadSize = 0; + payloadPos = 0; + + if (bufferCursor > 0) { + goto parseMore; + } + + vTaskDelay(pdMS_TO_TICKS(5)); + } + + free(buffer); + + return {result, totalWritten}; +} + +StreamReaderResult _readStreamData(HTTPClient& client, WiFiClient* stream, std::size_t contentLength, HTTP::DownloadCallback downloadCallback, std::int64_t begin, std::uint32_t timeoutMs) { + std::size_t nWritten = 0; + HTTP::RequestResult result = HTTP::RequestResult::Success; + + std::uint8_t* buffer = static_cast(malloc(HTTP_BUFFER_SIZE)); + + while (client.connected() && nWritten < contentLength) { + if (begin + timeoutMs < OpenShock::millis()) { + ESP_LOGW(TAG, "Request timed out"); + result = HTTP::RequestResult::TimedOut; + break; + } + + std::size_t bytesAvailable = stream->available(); + if (bytesAvailable == 0) { + vTaskDelay(pdMS_TO_TICKS(5)); + continue; + } + + std::size_t bytesToRead = std::min(bytesAvailable, HTTP_BUFFER_SIZE); + + std::size_t bytesRead = stream->readBytes(buffer, bytesToRead); + if (bytesRead == 0) { + ESP_LOGW(TAG, "No bytes read"); + result = HTTP::RequestResult::RequestFailed; + break; + } + + if (!downloadCallback(nWritten, buffer, bytesRead)) { + ESP_LOGW(TAG, "Request cancelled by callback"); + result = HTTP::RequestResult::Cancelled; + break; + } + + nWritten += bytesRead; + + vTaskDelay(pdMS_TO_TICKS(10)); + } + + free(buffer); + + return {result, nWritten}; +} + +HTTP::Response _doGetStream( + HTTPClient& client, + StringView url, + const std::map& headers, + const std::vector& acceptedCodes, + std::shared_ptr rateLimiter, + HTTP::GotContentLengthCallback contentLengthCallback, + HTTP::DownloadCallback downloadCallback, + std::uint32_t timeoutMs +) { + std::int64_t begin = OpenShock::millis(); + if (!client.begin(url.toArduinoString())) { + ESP_LOGE(TAG, "Failed to begin HTTP request"); + return {HTTP::RequestResult::RequestFailed, 0}; + } + + for (auto& header : headers) { + client.addHeader(header.first, header.second); + } + + int responseCode = client.GET(); + + if (responseCode == HTTP_CODE_REQUEST_TIMEOUT || begin + timeoutMs < OpenShock::millis()) { + ESP_LOGW(TAG, "Request timed out"); + return {HTTP::RequestResult::TimedOut, responseCode, 0}; + } + + if (responseCode == HTTP_CODE_TOO_MANY_REQUESTS) { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After + + // Get "Retry-After" header + String retryAfterStr = client.header("Retry-After"); + + // Try to parse it as an integer (delay-seconds) + long retryAfter = 0; + if (retryAfterStr.length() > 0 && std::all_of(retryAfterStr.begin(), retryAfterStr.end(), isdigit)) { + retryAfter = retryAfterStr.toInt(); + } + + // If header missing/unparseable, default to 15 seconds + if (retryAfter <= 0) { + retryAfter = 15; + } + + // Get the block-until time + std::int64_t blockUntilMs = OpenShock::millis() + retryAfter * 1000; + + // Apply the block-until time + rateLimiter->blockUntil(blockUntilMs); + + return {HTTP::RequestResult::RateLimited, responseCode, 0}; + } + + if (responseCode == 418) { + ESP_LOGW(TAG, "The server refused to brew coffee because it is, permanently, a teapot."); + } + + if (std::find(acceptedCodes.begin(), acceptedCodes.end(), responseCode) == acceptedCodes.end()) { + ESP_LOGE(TAG, "Received unexpected response code %d", responseCode); + return {HTTP::RequestResult::CodeRejected, responseCode, 0}; + } + + int contentLength = client.getSize(); + if (contentLength == 0) { + return {HTTP::RequestResult::Success, responseCode, 0}; + } + + if (contentLength > 0) { + if (contentLength > HTTP_DOWNLOAD_SIZE_LIMIT) { + ESP_LOGE(TAG, "Content-Length too large"); + return {HTTP::RequestResult::RequestFailed, responseCode, 0}; + } + + if (!contentLengthCallback(contentLength)) { + ESP_LOGW(TAG, "Request cancelled by callback"); + return {HTTP::RequestResult::Cancelled, responseCode, 0}; + } + } + + WiFiClient* stream = client.getStreamPtr(); + if (stream == nullptr) { + ESP_LOGE(TAG, "Failed to get stream"); + return {HTTP::RequestResult::RequestFailed, 0}; + } + + StreamReaderResult result; + if (contentLength > 0) { + result = _readStreamData(client, stream, contentLength, downloadCallback, begin, timeoutMs); + } else { + result = _readStreamDataChunked(client, stream, downloadCallback, begin, timeoutMs); + } + + return {result.result, responseCode, result.nWritten}; +} + +HTTP::Response + HTTP::Download(StringView url, const std::map& headers, HTTP::GotContentLengthCallback contentLengthCallback, HTTP::DownloadCallback downloadCallback, const std::vector& acceptedCodes, std::uint32_t timeoutMs) { + std::shared_ptr rateLimiter = _getRateLimiter(url); + if (rateLimiter == nullptr) { + return {RequestResult::InvalidURL, 0, 0}; + } + + if (!rateLimiter->tryRequest()) { + return {RequestResult::RateLimited, 0, 0}; + } + + HTTPClient client; + _setupClient(client); + + return _doGetStream(client, url, headers, acceptedCodes, rateLimiter, contentLengthCallback, downloadCallback, timeoutMs); +} + +HTTP::Response HTTP::GetString(StringView url, const std::map& headers, const std::vector& acceptedCodes, std::uint32_t timeoutMs) { + std::string result; + + auto allocator = [&result](std::size_t contentLength) { + result.reserve(contentLength); + return true; + }; + auto writer = [&result](std::size_t offset, const uint8_t* data, std::size_t len) { + result.append(reinterpret_cast(data), len); + return true; + }; + + auto response = Download(url, headers, allocator, writer, acceptedCodes, timeoutMs); + if (response.result != RequestResult::Success) { + return {response.result, response.code, {}}; + } + + return {response.result, response.code, result}; +} diff --git a/src/http/JsonAPI.cpp b/src/http/JsonAPI.cpp new file mode 100644 index 00000000..c85444d4 --- /dev/null +++ b/src/http/JsonAPI.cpp @@ -0,0 +1,43 @@ +#include "http/JsonAPI.h" + +#include "Common.h" + +using namespace OpenShock; + +HTTP::Response HTTP::JsonAPI::LinkAccount(const char* accountLinkCode) { + char uri[256]; + sprintf(uri, OPENSHOCK_API_URL("/1/device/pair/%s"), accountLinkCode); + + return HTTP::GetJSON( + uri, + { + {"Accept", "application/json"} + }, + Serialization::JsonAPI::ParseAccountLinkJsonResponse, + {200, 404} + ); +} + +HTTP::Response HTTP::JsonAPI::GetDeviceInfo(const String& deviceToken) { + return HTTP::GetJSON( + OPENSHOCK_API_URL("/1/device/self"), + { + { "Accept", "application/json"}, + {"DeviceToken", deviceToken} + }, + Serialization::JsonAPI::ParseDeviceInfoJsonResponse, + {200, 401} + ); +} + +HTTP::Response HTTP::JsonAPI::AssignLcg(const String& deviceToken) { + return HTTP::GetJSON( + OPENSHOCK_API_URL("/1/device/assignLCG"), + { + { "Accept", "application/json"}, + {"DeviceToken", deviceToken} + }, + Serialization::JsonAPI::ParseAssignLcgJsonResponse, + {200, 401} + ); +} diff --git a/src/main.cpp b/src/main.cpp index 5a7c0a56..339068df 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,58 +1,107 @@ #include "CaptivePortal.h" #include "CommandHandler.h" -#include "Config.h" -#include "Constants.h" +#include "Common.h" +#include "config/Config.h" #include "EStopManager.h" #include "event_handlers/Init.h" #include "GatewayConnectionManager.h" #include "Logging.h" -#include "SerialInputHandler.h" +#include "OtaUpdateManager.h" +#include "serial/SerialInputHandler.h" +#include "util/TaskUtils.h" #include "VisualStateManager.h" #include "wifi/WiFiManager.h" #include "wifi/WiFiScanManager.h" -#include +#include #include const char* const TAG = "OpenShock"; -void setup() { - Serial.begin(115'200); - - if (!LittleFS.begin(true)) { - ESP_PANIC(TAG, "Unable to mount LittleFS"); - } - +// Internal setup function, returns true if setup succeeded, false otherwise. +bool trySetup() { OpenShock::EventHandlers::Init(); - OpenShock::VisualStateManager::Init(); - OpenShock::SerialInputHandler::PrintWelcomeHeader(); - OpenShock::SerialInputHandler::PrintVersionInfo(); + OpenShock::EStopManager::Init(100); // 100ms update interval - OpenShock::EStopManager::Init(); - - OpenShock::Config::Init(); + if (!OpenShock::SerialInputHandler::Init()) { + ESP_LOGE(TAG, "Unable to initialize SerialInputHandler"); + return false; + } if (!OpenShock::CommandHandler::Init()) { ESP_LOGW(TAG, "Unable to initialize CommandHandler"); + return false; } if (!OpenShock::WiFiManager::Init()) { - ESP_PANIC(TAG, "Unable to initialize WiFiManager"); + ESP_LOGE(TAG, "Unable to initialize WiFiManager"); + return false; } if (!OpenShock::GatewayConnectionManager::Init()) { - ESP_PANIC(TAG, "Unable to initialize GatewayConnectionManager"); + ESP_LOGE(TAG, "Unable to initialize GatewayConnectionManager"); + return false; + } + + return true; +} + +// OTA setup is the same as normal setup, but we invalidate the currently running app, and roll back if it fails. +void otaSetup() { + ESP_LOGI(TAG, "Validating OTA app"); + + if (!trySetup()) { + ESP_LOGE(TAG, "Unable to validate OTA app, rolling back"); + OpenShock::OtaUpdateManager::InvalidateAndRollback(); + } + + ESP_LOGI(TAG, "Marking OTA app as valid"); + + OpenShock::OtaUpdateManager::ValidateApp(); + + ESP_LOGI(TAG, "Done validating OTA app"); +} + +// App setup is the same as normal setup, but we restart if it fails. +void appSetup() { + if (!trySetup()) { + ESP_LOGI(TAG, "Restarting in 5 seconds..."); + vTaskDelay(pdMS_TO_TICKS(5000)); + esp_restart(); + } +} + +// Arduino setup function +void setup() { + Serial.begin(115'200); + + OpenShock::Config::Init(); + OpenShock::OtaUpdateManager::Init(); + if (OpenShock::OtaUpdateManager::IsValidatingApp()) { + otaSetup(); + } else { + appSetup(); + } +} + +void main_app(void* arg) { + while (true) { + OpenShock::SerialInputHandler::Update(); + OpenShock::CaptivePortal::Update(); + OpenShock::GatewayConnectionManager::Update(); + OpenShock::WiFiManager::Update(); + + vTaskDelay(5); // 5 ticks update interval } } void loop() { - OpenShock::SerialInputHandler::Update(); - OpenShock::CaptivePortal::Update(); - OpenShock::GatewayConnectionManager::Update(); - OpenShock::WiFiScanManager::Update(); - OpenShock::WiFiManager::Update(); - OpenShock::EStopManager::Update(); + // Start the main task + OpenShock::TaskUtils::TaskCreateExpensive(main_app, "main_app", 8192, nullptr, 1, nullptr); + + // Kill the loop task (Arduino is stinky) + vTaskDelete(nullptr); } diff --git a/src/radio/RFTransmitter.cpp b/src/radio/RFTransmitter.cpp index 720c7a7d..19138069 100644 --- a/src/radio/RFTransmitter.cpp +++ b/src/radio/RFTransmitter.cpp @@ -4,25 +4,29 @@ #include "Logging.h" #include "radio/rmt/MainEncoder.h" #include "Time.h" +#include "util/TaskUtils.h" #include #include -#include -#include +const char* const TAG = "RFTransmitter"; + +const UBaseType_t RFTRANSMITTER_QUEUE_SIZE = 32; +const BaseType_t RFTRANSMITTER_TASK_PRIORITY = 1; +const std::uint32_t RFTRANSMITTER_TASK_STACK_SIZE = 4096; +const float RFTRANSMITTER_TICKRATE_NS = 1000; + +using namespace OpenShock; struct command_t { std::int64_t until; std::vector sequence; std::shared_ptr> zeroSequence; std::uint16_t shockerId; + bool overwrite; }; -const char* const TAG = "RFTransmitter"; - -using namespace OpenShock; - -RFTransmitter::RFTransmitter(std::uint8_t gpioPin, int queueSize) : m_txPin(gpioPin), m_rmtHandle(nullptr), m_queueHandle(nullptr), m_taskHandle(nullptr) { +RFTransmitter::RFTransmitter(std::uint8_t gpioPin) : m_txPin(gpioPin), m_rmtHandle(nullptr), m_queueHandle(nullptr), m_taskHandle(nullptr) { ESP_LOGD(TAG, "[pin-%u] Creating RFTransmitter", m_txPin); m_rmtHandle = rmtInit(gpioPin, RMT_TX_MODE, RMT_MEM_64); @@ -32,10 +36,10 @@ RFTransmitter::RFTransmitter(std::uint8_t gpioPin, int queueSize) : m_txPin(gpio return; } - float realTick = rmtSetTick(m_rmtHandle, 1000); + float realTick = rmtSetTick(m_rmtHandle, RFTRANSMITTER_TICKRATE_NS); ESP_LOGD(TAG, "[pin-%u] real tick set to: %fns", m_txPin, realTick); - m_queueHandle = xQueueCreate(queueSize, sizeof(command_t*)); + m_queueHandle = xQueueCreate(RFTRANSMITTER_QUEUE_SIZE, sizeof(command_t*)); if (m_queueHandle == nullptr) { ESP_LOGE(TAG, "[pin-%u] Failed to create queue", m_txPin); destroy(); @@ -45,7 +49,7 @@ RFTransmitter::RFTransmitter(std::uint8_t gpioPin, int queueSize) : m_txPin(gpio char name[32]; snprintf(name, sizeof(name), "RFTransmitter-%u", m_txPin); - if (xTaskCreate(TransmitTask, name, 4096, this, 1, &m_taskHandle) != pdPASS) { + if (TaskUtils::TaskCreateExpensive(TransmitTask, name, RFTRANSMITTER_TASK_STACK_SIZE, this, RFTRANSMITTER_TASK_PRIORITY, &m_taskHandle) != pdPASS) { ESP_LOGE(TAG, "[pin-%u] Failed to create task", m_txPin); destroy(); return; @@ -56,16 +60,13 @@ RFTransmitter::~RFTransmitter() { destroy(); } -bool RFTransmitter::SendCommand(ShockerModelType model, std::uint16_t shockerId, ShockerCommandType type, std::uint8_t intensity, std::uint16_t durationMs) { +bool RFTransmitter::SendCommand(ShockerModelType model, std::uint16_t shockerId, ShockerCommandType type, std::uint8_t intensity, std::uint16_t durationMs, bool overwriteExisting) { if (m_queueHandle == nullptr) { ESP_LOGE(TAG, "[pin-%u] Queue is null", m_txPin); return false; } - // Intensity must be between 0 and 99 - intensity = std::min(intensity, (std::uint8_t)99); - - command_t* cmd = new command_t {.until = OpenShock::millis() + durationMs, .sequence = Rmt::GetSequence(model, shockerId, type, intensity), .zeroSequence = Rmt::GetZeroSequence(model, shockerId), .shockerId = shockerId}; + command_t* cmd = new command_t {.until = OpenShock::millis() + durationMs, .sequence = Rmt::GetSequence(model, shockerId, type, intensity), .zeroSequence = Rmt::GetZeroSequence(model, shockerId), .shockerId = shockerId, .overwrite = overwriteExisting}; // We will use nullptr commands to end the task, if we got a nullptr here, we are out of memory... :( if (cmd == nullptr) { @@ -153,20 +154,26 @@ void RFTransmitter::TransmitTask(void* arg) { } // Replace the command if it already exists - bool replaced = false; for (auto it = commands.begin(); it != commands.end(); ++it) { - if ((*it)->shockerId == cmd->shockerId) { - delete *it; - *it = cmd; + const command_t* existingCmd = *it; + + if (existingCmd->shockerId == cmd->shockerId) { + // Only replace the command if it should be overwritten + if (existingCmd->overwrite) { + delete *it; + *it = cmd; + } else { + delete cmd; + } - replaced = true; + cmd = nullptr; break; } } // If the command was not replaced, add it to the queue - if (!replaced) { + if (cmd != nullptr) { commands.push_back(cmd); } } @@ -176,7 +183,7 @@ void RFTransmitter::TransmitTask(void* arg) { cmd = *it; bool expired = cmd->until < OpenShock::millis(); - bool empty = cmd->sequence.size() <= 0; + bool empty = cmd->sequence.empty(); // Remove expired or empty commands, else send the command. // After sending/receiving a command, move to the next one. diff --git a/src/radio/rmt/CaiXianlinEncoder.cpp b/src/radio/rmt/CaiXianlinEncoder.cpp new file mode 100644 index 00000000..fa1b88cc --- /dev/null +++ b/src/radio/rmt/CaiXianlinEncoder.cpp @@ -0,0 +1,57 @@ +#include "radio/rmt/CaiXianlinEncoder.h" + +#include "radio/rmt/internal/Shared.h" + +#include "Checksum.h" + +// This is the encoder for the CaiXianlin shocker. +// +// It is based on the following documentation: +// https://wiki.openshock.org/hardware/shockers/caixianlin/#rf-specification + +const rmt_data_t kRmtPreamble = {1400, 1, 800, 0}; +const rmt_data_t kRmtOne = {800, 1, 300, 0}; +const rmt_data_t kRmtZero = {300, 1, 800, 0}; + +using namespace OpenShock; + +std::vector Rmt::CaiXianlinEncoder::GetSequence(std::uint16_t transmitterId, std::uint8_t channelId, ShockerCommandType type, std::uint8_t intensity) { + // Intensity must be between 0 and 99 + intensity = std::min(intensity, static_cast(99)); + + std::uint8_t typeVal = 0; + switch (type) { + case ShockerCommandType::Shock: + typeVal = 0x01; + break; + case ShockerCommandType::Vibrate: + typeVal = 0x02; + break; + case ShockerCommandType::Sound: + typeVal = 0x03; + break; + default: + return {}; // Invalid type + } + + // Payload layout: [transmitterId:16][channelId:4][type:4][intensity:8] + std::uint32_t payload = (static_cast(transmitterId & 0xFFFF) << 16) | (static_cast(channelId & 0xF) << 12) | (static_cast(typeVal) << 8) | static_cast(intensity & 0xFF); + + // Calculate the checksum of the payload + std::uint8_t checksum = Checksum::CRC8(payload); + + // Add the checksum to the payload + std::uint64_t data = (static_cast(payload) << 8) | static_cast(checksum); + + // Shift the data left by 3 bits to add the postamble (3 bits of 0) + data <<= 3; + + std::vector pulses; + pulses.reserve(44); + + // Generate the sequence + pulses.push_back(kRmtPreamble); + Internal::EncodeBits<43>(pulses, data, kRmtOne, kRmtZero); + + return pulses; +} diff --git a/src/radio/rmt/MainEncoder.cpp b/src/radio/rmt/MainEncoder.cpp index 59f93cc4..5da09093 100644 --- a/src/radio/rmt/MainEncoder.cpp +++ b/src/radio/rmt/MainEncoder.cpp @@ -1,9 +1,8 @@ #include "radio/rmt/MainEncoder.h" #include "Logging.h" -#include "radio/rmt/PetTrainerEncoder.h" -#include "radio/rmt/XlcEncoder.h" - +#include "radio/rmt/CaiXianlinEncoder.h" +#include "radio/rmt/PetrainerEncoder.h" #include @@ -13,10 +12,10 @@ using namespace OpenShock; std::vector Rmt::GetSequence(ShockerModelType model, std::uint16_t shockerId, ShockerCommandType type, std::uint8_t intensity) { switch (model) { - case ShockerModelType::PetTrainer: - return Rmt::PetTrainerEncoder::GetSequence(shockerId, type, intensity); + case ShockerModelType::Petrainer: + return Rmt::PetrainerEncoder::GetSequence(shockerId, type, intensity); case ShockerModelType::CaiXianlin: - return Rmt::XlcEncoder::GetSequence(shockerId, 0, type, intensity); + return Rmt::CaiXianlinEncoder::GetSequence(shockerId, 0, type, intensity); default: ESP_LOGE(TAG, "Unknown shocker model: %u", model); return {}; @@ -30,11 +29,11 @@ std::shared_ptr> Rmt::GetZeroSequence(ShockerModelType m std::shared_ptr> sequence; switch (model) { - case ShockerModelType::PetTrainer: - sequence = std::make_shared>(Rmt::PetTrainerEncoder::GetSequence(shockerId, ShockerCommandType::Vibrate, 0)); + case ShockerModelType::Petrainer: + sequence = std::make_shared>(Rmt::PetrainerEncoder::GetSequence(shockerId, ShockerCommandType::Vibrate, 0)); break; case ShockerModelType::CaiXianlin: - sequence = std::make_shared>(Rmt::XlcEncoder::GetSequence(shockerId, 0, ShockerCommandType::Vibrate, 0)); + sequence = std::make_shared>(Rmt::CaiXianlinEncoder::GetSequence(shockerId, 0, ShockerCommandType::Vibrate, 0)); break; default: ESP_LOGE(TAG, "Unknown shocker model: %u", model); diff --git a/src/radio/rmt/PetTrainerEncoder.cpp b/src/radio/rmt/PetTrainerEncoder.cpp deleted file mode 100644 index af53a63e..00000000 --- a/src/radio/rmt/PetTrainerEncoder.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "radio/rmt/PetTrainerEncoder.h" - -const rmt_data_t kRmtPreamble = {750, 1, 750, 0}; -const rmt_data_t kRmtOne = {200, 1, 1500, 0}; -const rmt_data_t kRmtZero = {200, 1, 750, 0}; -const rmt_data_t kRmtPostamble = {200, 1, 7000, 0}; - -using namespace OpenShock; - -std::vector Rmt::PetTrainerEncoder::GetSequence(std::uint16_t shockerId, ShockerCommandType type, std::uint8_t intensity) { - std::uint8_t methodBit = (0x80 | (1 << ((std::uint8_t)type - 1))) & 0xFF; - std::uint8_t methodChecksum = 0xFF ^ ((1 << (8 - (std::uint8_t)type)) | 1); - - std::uint64_t data = (std::uint64_t(methodBit) << 32) | (std::uint64_t(shockerId) << 16) | (std::uint64_t(intensity) << 8) | (std::uint64_t(methodChecksum) << 0); - - std::vector pulses; - pulses.reserve(42); - - pulses.push_back(kRmtPreamble); - for (int bit_pos = 39; bit_pos >= 0; --bit_pos) { - pulses.push_back((data >> bit_pos) & 1 ? kRmtOne : kRmtZero); - } - pulses.push_back(kRmtPostamble); - - return pulses; -} diff --git a/src/radio/rmt/PetrainerEncoder.cpp b/src/radio/rmt/PetrainerEncoder.cpp new file mode 100644 index 00000000..138a2200 --- /dev/null +++ b/src/radio/rmt/PetrainerEncoder.cpp @@ -0,0 +1,49 @@ +#include "radio/rmt/PetrainerEncoder.h" + +#include "radio/rmt/internal/Shared.h" + +const rmt_data_t kRmtPreamble = {750, 1, 750, 0}; +const rmt_data_t kRmtOne = {200, 1, 1500, 0}; +const rmt_data_t kRmtZero = {200, 1, 750, 0}; +const rmt_data_t kRmtPostamble = {200, 1, 7000, 0}; + +using namespace OpenShock; + +std::vector Rmt::PetrainerEncoder::GetSequence(std::uint16_t shockerId, ShockerCommandType type, std::uint8_t intensity) { + // Intensity must be between 0 and 100 + intensity = std::min(intensity, static_cast(100)); + + std::uint8_t nShift = 0; + switch (type) { + case ShockerCommandType::Shock: + nShift = 0; + break; + case ShockerCommandType::Vibrate: + nShift = 1; + break; + case ShockerCommandType::Sound: + nShift = 2; + break; + default: + return {}; // Invalid type + } + + // Type is 0x80 | (0x01 << nShift) + std::uint8_t typeVal = (0x80 | (0x01 << nShift)) & 0xFF; + + // TypeSum is NOT(0x01 | (0x80 >> nShift)) + std::uint8_t typeSum = (~(0x01 | (0x80 >> nShift))) & 0xFF; + + // Payload layout: [methodBit:8][shockerId:16][intensity:8][methodChecksum:8] + std::uint64_t data = (static_cast(typeVal) << 32) | (static_cast(shockerId) << 16) | (static_cast(intensity) << 8) | static_cast(typeSum); + + std::vector pulses; + pulses.reserve(42); + + // Generate the sequence + pulses.push_back(kRmtPreamble); + Internal::EncodeBits<40>(pulses, data, kRmtOne, kRmtZero); + pulses.push_back(kRmtPostamble); + + return pulses; +} diff --git a/src/radio/rmt/XlcEncoder.cpp b/src/radio/rmt/XlcEncoder.cpp deleted file mode 100644 index e9aa7c12..00000000 --- a/src/radio/rmt/XlcEncoder.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "radio/rmt/XlcEncoder.h" - -#include "Checksum.h" - -const rmt_data_t kRmtPreamble = {1400, 1, 800, 0}; -const rmt_data_t kRmtOne = {800, 1, 300, 0}; -const rmt_data_t kRmtZero = {300, 1, 800, 0}; - -using namespace OpenShock; - -std::vector Rmt::XlcEncoder::GetSequence(std::uint16_t transmitterId, std::uint8_t channelId, ShockerCommandType type, std::uint8_t intensity) { - std::uint64_t data = (std::uint64_t(transmitterId) << 24) | (std::uint64_t(channelId & 0xF) << 20) | (std::uint64_t((std::uint8_t)type & 0xF) << 16) | (std::uint64_t(intensity & 0xFF) << 8); - - data |= Checksum::CRC8(data) & 0xFF; - - data <<= 2; // The 2 last bits are always 0. this is the postamble of the packet. - - std::vector pulses; - pulses.reserve(43); - - pulses.push_back(kRmtPreamble); - for (int bit_pos = 41; bit_pos >= 0; --bit_pos) { - pulses.push_back((data >> bit_pos) & 1 ? kRmtOne : kRmtZero); - } - - return pulses; -} diff --git a/src/serial/SerialInputHandler.cpp b/src/serial/SerialInputHandler.cpp new file mode 100644 index 00000000..3b17ccb5 --- /dev/null +++ b/src/serial/SerialInputHandler.cpp @@ -0,0 +1,875 @@ +#include "serial/SerialInputHandler.h" + +#include "Chipset.h" +#include "CommandHandler.h" +#include "config/Config.h" +#include "config/SerialInputConfig.h" +#include "FormatHelpers.h" +#include "Logging.h" +#include "serialization/JsonSerial.h" +#include "Time.h" +#include "util/Base64Utils.h" +#include "wifi/WiFiManager.h" + +#include +#include + +#include + +const char* const TAG = "SerialInputHandler"; + +#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; + +constexpr std::int64_t PASTE_INTERVAL_THRESHOLD_MS = 20; +constexpr std::size_t SERIAL_BUFFER_CLEAR_THRESHOLD = 512; + +struct SerialCmdHandler { + const char* cmd; + const char* helpResponse; + void (*commandHandler)(char*, std::size_t); +}; + +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(const char* str, std::size_t strLen, bool& out) { + if (str == nullptr || strLen == 0) { + return false; + } + + if (strcasecmp(str, "true") == 0) { + out = true; + return true; + } + + if (strcasecmp(str, "false") == 0) { + out = false; + return true; + } + + return false; +} + +void _handleVersionCommand(char* arg, std::size_t argLength) { + (void)arg; + (void)argLength; + + Serial.print("\n"); + SerialInputHandler::PrintVersionInfo(); +} + +void _handleRestartCommand(char* arg, std::size_t argLength) { + (void)arg; + (void)argLength; + + Serial.println("Restarting ESP..."); + ESP.restart(); +} + +void _handleFactoryResetCommand(char* arg, std::size_t argLength) { + (void)arg; + (void)argLength; + + Serial.println("Resetting to factory defaults..."); + Config::FactoryReset(); + Serial.println("Restarting..."); + ESP.restart(); +} + +void _handleRfTxPinCommand(char* arg, std::size_t argLength) { + if (arg == nullptr || argLength == 0) { + std::uint8_t txPin; + if (!Config::GetRFConfigTxPin(txPin)) { + SERPR_ERROR("Failed to get RF TX pin from config"); + return; + } + + // Get rmt pin + SERPR_RESPONSE("RmtPin|%u", txPin); + return; + } + + unsigned int pin; + if (sscanf(arg, "%u", &pin) != 1) { + SERPR_ERROR("Invalid argument (not a number)"); + return; + } + + if (pin > UINT8_MAX) { + SERPR_ERROR("Invalid argument (out of range)"); + return; + } + + OpenShock::SetRfPinResultCode result = OpenShock::CommandHandler::SetRfTxPin(static_cast(pin)); + + switch (result) { + case OpenShock::SetRfPinResultCode::InvalidPin: + SERPR_ERROR("Invalid argument (invalid pin)"); + break; + + case OpenShock::SetRfPinResultCode::InternalError: + SERPR_ERROR("Internal error while setting RF TX pin"); + break; + + case OpenShock::SetRfPinResultCode::Success: + SERPR_SUCCESS("Saved config"); + break; + + default: + SERPR_ERROR("Unknown error while setting RF TX pin"); + break; + } +} + +void _handleAuthtokenCommand(char* arg, std::size_t argLength) { + if (arg == nullptr || argLength == 0) { + std::string authToken; + if (!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(std::string(arg, argLength)); + + if (result) { + SERPR_SUCCESS("Saved config"); + } else { + SERPR_ERROR("Failed to save config"); + } +} + +void _handleNetworksCommand(char* arg, std::size_t argLength) { + cJSON* root; + + if (arg == nullptr || argLength == 0) { + root = cJSON_CreateArray(); + if (root == nullptr) { + SERPR_ERROR("Failed to create JSON array"); + return; + } + + if (!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, argLength); + 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; + + std::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 (cred.id == 0) { + cred.id = id++; + } + + ESP_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(char* arg, std::size_t argLength) { + bool keepAliveEnabled; + + if (arg == nullptr || argLength == 0) { + // Get keep alive status + if (!Config::GetRFConfigKeepAliveEnabled(keepAliveEnabled)) { + SERPR_ERROR("Failed to get keep-alive status from config"); + return; + } + + SERPR_RESPONSE("KeepAlive|%s", keepAliveEnabled ? "true" : "false"); + return; + } + + if (!_tryParseBool(arg, argLength, 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"); + } +} + +void _handleSerialEchoCommand(char* arg, std::size_t argLength) { + if (arg == nullptr || argLength == 0) { + // Get current serial echo status + SERPR_RESPONSE("SerialEcho|%s", s_echoEnabled ? "true" : "false"); + return; + } + + bool enabled; + if (!_tryParseBool(arg, argLength, enabled)) { + SERPR_ERROR("Invalid argument (not a boolean)"); + return; + } + + bool result = Config::SetSerialInputConfigEchoEnabled(enabled); + s_echoEnabled = enabled; + + if (result) { + SERPR_SUCCESS("Saved config"); + } else { + SERPR_ERROR("Failed to save config"); + } +} + +void _handleValidGpiosCommand(char* arg, std::size_t argLength) { + if (arg != nullptr && argLength > 0) { + 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()); +} + +void _handleJsonConfigCommand(char* arg, std::size_t argLength) { + if (arg == nullptr || argLength == 0) { + // Get raw config + std::string json = Config::GetAsJSON(true); + + SERPR_RESPONSE("JsonConfig|%s", json.c_str()); + return; + } + + if (!Config::SaveFromJSON(std::string(arg, argLength))) { + SERPR_ERROR("Failed to save config"); + return; + } + + SERPR_SUCCESS("Saved config, restarting..."); + + ESP.restart(); +} + +void _handleRawConfigCommand(char* arg, std::size_t argLength) { + if (arg == nullptr || argLength == 0) { + std::vector buffer; + + // Get raw config + if (!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, argLength, 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(char* arg, std::size_t argLength) { + (void)arg; + (void)argLength; + + SERPR_RESPONSE("RTOSInfo|Free Heap|%u", xPortGetFreeHeapSize()); + SERPR_RESPONSE("RTOSInfo|Min Free Heap|%u", xPortGetMinimumEverFreeHeapSize()); + + const std::int64_t now = OpenShock::millis(); + SERPR_RESPONSE("RTOSInfo|UptimeMS|%lli", now); + + const std::int64_t seconds = now / 1000; + const std::int64_t minutes = seconds / 60; + const std::int64_t hours = minutes / 60; + const std::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(char* arg, std::size_t argLength) { + if (arg == nullptr || argLength == 0) { + SERPR_ERROR("No command"); + return; + } + cJSON* root = cJSON_ParseWithLength(arg, argLength); + 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; + } + + 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"); +} + +void _handleHelpCommand(char* arg, std::size_t argLength) { + if (arg != nullptr && argLength > 0) { + // Convert argument to lowercase + std::transform(arg, arg + argLength, arg, ::tolower); + + // Get help for a specific command + auto it = s_commandHandlers.find(std::string(arg, argLength)); + if (it != s_commandHandlers.end()) { + Serial.print(it->second.helpResponse); + return; + } + + SERPR_ERROR("Command \"%.*s\" not found", argLength, arg); + + return; + } + + 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 +authtoken get auth token +authtoken set auth token +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 +)"); +} + +static const SerialCmdHandler kVersionCmdHandler = { + "version", + R"(version + Print version information + Example: + version +)", + _handleVersionCommand, +}; +static const SerialCmdHandler kRestartCmdHandler = { + "restart", + R"(restart + Restart the board + Example: + restart +)", + _handleRestartCommand, +}; +static const SerialCmdHandler kSystemInfoCmdHandler = { + "sysinfo", + R"(sysinfo + Get system information from RTOS, WiFi, etc. + Example: + sysinfo +)", + _handleDebugInfoCommand, +}; +static const SerialCmdHandler kSerialEchoCmdHandler = { + "echo", + 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", + R"(validgpios + List all valid GPIO pins + Example: + validgpios +)", + _handleValidGpiosCommand, +}; +static const SerialCmdHandler kRfTxPinCmdHandler = { + "rftxpin", + 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 kAuthTokenCmdHandler = { + "authtoken", + R"(authtoken + Get the backend auth token. + +authtoken [] + Set the auth token. + Arguments: + must be a string. + Example: + authtoken mytoken +)", + _handleAuthtokenCommand, +}; +static const SerialCmdHandler kNetworksCmdHandler = { + "networks", + 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", + R"(keepalive + Get the shocker keep-alive status. + +keepalive [] + Enable/disable shocker keep-alive. + Arguments: + must be a boolean. + Example: + keepalive true +)", + _handleKeepAliveCommand, +}; +static const SerialCmdHandler kJsonConfigCmdHandler = { + "jsonconfig", + 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", + 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", + R"(rftransmit + Transmit a RF command + Arguments: + must be a JSON object with the following fields: + model (string) Model of the shocker ("caixianlin", "petrainer") + 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", + R"(factoryreset + Reset the device to factory defaults and restart + Example: + factoryreset +)", + _handleFactoryResetCommand, +}; +static const SerialCmdHandler khelpCmdHandler = { + "help", + 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; +} +int findChar(const char* buffer, std::size_t bufferSize, char c) { + for (int i = 0; i < bufferSize; i++) { + if (buffer[i] == c) { + return i; + } + } + + return -1; +} + +int findLineEnd(const char* buffer, int bufferSize) { + if (buffer == nullptr || bufferSize <= 0) return -1; + + for (int i = 0; i < bufferSize; i++) { + if (buffer[i] == '\r' || buffer[i] == '\n' || buffer[i] == '\0') { + return i; + } + } + + return -1; +} + +int findLineStart(const char* buffer, int bufferSize, int lineEnd) { + if (lineEnd < 0) return -1; + if (lineEnd >= bufferSize) return -1; + + for (int i = lineEnd + 1; i < bufferSize; i++) { + if (buffer[i] != '\r' && buffer[i] != '\n' && buffer[i] != '\0') { + return i; + } + } + + return -1; +} + +void processSerialLine(char* data, std::size_t length) { + int delimiter = findChar(data, length, ' '); + if (delimiter == 0) { + SERPR_ERROR("Command cannot start with a space"); + return; + } + + char* command = data; + std::size_t commandLength = length; + char* arg = nullptr; + std::size_t argLength = 0; + + // If there is a delimiter, split the command and argument + if (delimiter > 0) { + data[delimiter] = '\0'; + commandLength = delimiter; + arg = data + delimiter + 1; + argLength = length - delimiter - 1; + } + + // Convert command to lowercase + std::transform(command, command + commandLength, command, ::tolower); + + // TODO: Clean this up, test this + auto it = s_commandHandlers.find(std::string(command, commandLength)); + if (it != s_commandHandlers.end()) { + it->second.commandHandler(arg, argLength); + return; + } + + if (commandLength > 0) { + SERPR_ERROR("Command \"%.*s\" not found", commandLength, command); + } else { + SERPR_ERROR("No command"); + } +} + +bool SerialInputHandler::Init() { + static bool s_initialized = false; + if (s_initialized) { + ESP_LOGW(TAG, "Serial input handler already initialized"); + return false; + } + s_initialized = true; + + // Register command handlers + RegisterCommandHandler(kVersionCmdHandler); + RegisterCommandHandler(kRestartCmdHandler); + RegisterCommandHandler(kSystemInfoCmdHandler); + RegisterCommandHandler(kSerialEchoCmdHandler); + RegisterCommandHandler(kValidGpiosCmdHandler); + RegisterCommandHandler(kRfTxPinCmdHandler); + RegisterCommandHandler(kAuthTokenCmdHandler); + RegisterCommandHandler(kNetworksCmdHandler); + RegisterCommandHandler(kKeepAliveCmdHandler); + RegisterCommandHandler(kJsonConfigCmdHandler); + RegisterCommandHandler(kRawConfigCmdHandler); + RegisterCommandHandler(kRfTransmitCmdHandler); + RegisterCommandHandler(kFactoryResetCmdHandler); + RegisterCommandHandler(khelpCmdHandler); + + SerialInputHandler::PrintWelcomeHeader(); + SerialInputHandler::PrintVersionInfo(); + Serial.println(); + + if (!Config::GetSerialInputConfigEchoEnabled(s_echoEnabled)) { + ESP_LOGE(TAG, "Failed to get serial echo status from config"); + return false; + } + + return true; +} + +void SerialInputHandler::Update() { + static char* buffer = nullptr; // TODO: Clean up this buffer every once in a while + static std::size_t bufferSize = 0; + static std::size_t bufferIndex = 0; + static std::int64_t lastEcho = 0; + static bool suppressingPaste = false; + + while (true) { + int available = Serial.available(); + if (available <= 0 && findLineEnd(buffer, bufferIndex) < 0) { + // If we're suppressing paste, and we haven't printed anything in a while, print the buffer and stop suppressing + if (buffer != nullptr && s_echoEnabled && suppressingPaste && OpenShock::millis() - lastEcho > PASTE_INTERVAL_THRESHOLD_MS) { + // \r - carriage return, moves to start of line + // \x1B[K - clears rest of line + Serial.printf("\r\x1B[K> %.*s", bufferIndex, buffer); + lastEcho = OpenShock::millis(); + suppressingPaste = false; + } + break; + } + + if (bufferIndex + available > bufferSize) { + bufferSize = bufferIndex + available; + + void* newBuffer = realloc(buffer, bufferSize); + if (newBuffer == nullptr) { + free(buffer); + buffer = nullptr; + bufferSize = 0; + continue; + } + + buffer = static_cast(newBuffer); + } + + if (buffer == nullptr) { + continue; + } + + while (available-- > 0) { + char c = Serial.read(); + // Handle backspace + if (c == '\b') { + if (bufferIndex > 0) { + bufferIndex--; + } + continue; + } + buffer[bufferIndex++] = c; + } + + int lineEnd = findLineEnd(buffer, bufferIndex); + // No newline found, wait for more input + if (lineEnd == -1) { + if (s_echoEnabled) { + // If we're typing without pasting, echo the buffer + if (OpenShock::millis() - lastEcho > PASTE_INTERVAL_THRESHOLD_MS) { + // \r - carriage return, moves to start of line + // \x1B[K - clears rest of line + Serial.printf("\r\x1B[K> %.*s", bufferIndex, buffer); + lastEcho = OpenShock::millis(); + suppressingPaste = false; + } else { + lastEcho = OpenShock::millis(); + suppressingPaste = true; + } + } + break; + } + + buffer[lineEnd] = '\0'; + Serial.printf("\r> %s\n", buffer); + + processSerialLine(buffer, lineEnd); + + int nextLine = findLineStart(buffer, bufferSize, lineEnd + 1); + if (nextLine < 0) { + bufferIndex = 0; + // Free buffer if it's too big + if (bufferSize > SERIAL_BUFFER_CLEAR_THRESHOLD) { + ESP_LOGV(TAG, "Clearing serial input buffer"); + bufferSize = 0; + free(buffer); + buffer = nullptr; + } + break; + } + + int remaining = bufferIndex - nextLine; + if (remaining > 0) { + memmove(buffer, buffer + nextLine, remaining); + bufferIndex = remaining; + } else { + bufferIndex = 0; + // Free buffer if it's too big + if (bufferSize > SERIAL_BUFFER_CLEAR_THRESHOLD) { + ESP_LOGV(TAG, "Clearing serial input buffer"); + bufferSize = 0; + free(buffer); + buffer = nullptr; + } + } + } +} + +void SerialInputHandler::PrintWelcomeHeader() { + Serial.print(R"( +============== OPENSHOCK ============== + Contribute @ github.com/OpenShock + Discuss @ discord.gg/OpenShock + Type 'help' for available commands +======================================= +)"); +} + +void SerialInputHandler::PrintVersionInfo() { + Serial.print("\ + Version: " OPENSHOCK_FW_VERSION "\n\ + Build: " OPENSHOCK_FW_MODE "\n\ + Commit: " OPENSHOCK_FW_GIT_COMMIT "\n\ + Board: " OPENSHOCK_FW_BOARD "\n\ + Chip: " OPENSHOCK_FW_CHIP "\n\ +"); +} diff --git a/src/serialization/JsonAPI.cpp b/src/serialization/JsonAPI.cpp new file mode 100644 index 00000000..ab2a896a --- /dev/null +++ b/src/serialization/JsonAPI.cpp @@ -0,0 +1,150 @@ +#include "serialization/JsonAPI.h" + +#include "Logging.h" + +const char* const TAG = "JsonAPI"; + +#define ESP_LOGJSONE(err, root) ESP_LOGE(TAG, "Invalid JSON response (" err "): %s", cJSON_PrintUnformatted(root)) + +using namespace OpenShock::Serialization; + +bool JsonAPI::ParseAccountLinkJsonResponse(int code, const cJSON* root, JsonAPI::AccountLinkResponse& out) { + (void)code; + + if (cJSON_IsObject(root) == 0) { + ESP_LOGJSONE("not an object", root); + return false; + } + + const cJSON* data = cJSON_GetObjectItemCaseSensitive(root, "data"); + if (cJSON_IsString(data) == 0) { + ESP_LOGJSONE("value at 'data' is not a string", root); + return false; + } + + out = {}; + + out.authToken = data->valuestring; + + return true; +} +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; + } + + const cJSON* data = cJSON_GetObjectItemCaseSensitive(root, "data"); + if (cJSON_IsObject(data) == 0) { + ESP_LOGJSONE("value at 'data' is not an object", root); + return false; + } + + const cJSON* deviceId = cJSON_GetObjectItemCaseSensitive(data, "id"); + if (cJSON_IsString(deviceId) == 0) { + ESP_LOGJSONE("value at 'data.id' is not a string", root); + return false; + } + + const cJSON* deviceName = cJSON_GetObjectItemCaseSensitive(data, "name"); + if (cJSON_IsString(deviceName) == 0) { + ESP_LOGJSONE("value at 'data.name' is not a string", root); + return false; + } + + const cJSON* deviceShockers = cJSON_GetObjectItemCaseSensitive(data, "shockers"); + if (cJSON_IsArray(deviceShockers) == 0) { + ESP_LOGJSONE("value at 'data.shockers' is not an array", root); + return false; + } + + out = {}; + + out.deviceId = deviceId->valuestring; + out.deviceName = deviceName->valuestring; + + if (out.deviceId.empty() || out.deviceName.empty()) { + ESP_LOGJSONE("value at 'data.id' or 'data.name' is empty", root); + return false; + } + + cJSON* shocker = nullptr; + cJSON_ArrayForEach(shocker, deviceShockers) { + const cJSON* shockerId = cJSON_GetObjectItemCaseSensitive(shocker, "id"); + if (cJSON_IsString(shockerId) == 0) { + ESP_LOGJSONE("value at 'shocker.id' is not a string", shocker); + return false; + } + const char* shockerIdStr = shockerId->valuestring; + + if (shockerIdStr == nullptr || shockerIdStr[0] == '\0') { + ESP_LOGJSONE("value at 'shocker.id' is empty", shocker); + return false; + } + + const cJSON* shockerRfId = cJSON_GetObjectItemCaseSensitive(shocker, "rfId"); + if (cJSON_IsNumber(shockerRfId) == 0) { + ESP_LOGJSONE("value at 'shocker.rfId' is not a number", shocker); + return false; + } + int shockerRfIdInt = shockerRfId->valueint; + if (shockerRfIdInt < 0 || shockerRfIdInt > UINT16_MAX) { + ESP_LOGJSONE("value at 'shocker.rfId' is not a valid uint16_t", shocker); + return false; + } + std::uint16_t shockerRfIdU16 = static_cast(shockerRfIdInt); + + const cJSON* shockerModel = cJSON_GetObjectItemCaseSensitive(shocker, "model"); + if (cJSON_IsString(shockerModel) == 0) { + ESP_LOGJSONE("value at 'shocker.model' is not a string", shocker); + return false; + } + const char* shockerModelStr = shockerModel->valuestring; + + if (shockerModelStr == nullptr || shockerModelStr[0] == '\0') { + ESP_LOGJSONE("value at 'shocker.model' is empty", shocker); + return false; + } + + OpenShock::ShockerModelType shockerModelType; + 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; + } + + out.shockers.push_back({.id = shockerIdStr, .rfId = shockerRfIdU16, .model = shockerModelType}); + } + + return true; +} +bool JsonAPI::ParseAssignLcgJsonResponse(int code, const cJSON* root, JsonAPI::AssignLcgResponse& out) { + (void)code; + + if (cJSON_IsObject(root) == 0) { + ESP_LOGJSONE("not an object", root); + return false; + } + + const cJSON* data = cJSON_GetObjectItemCaseSensitive(root, "data"); + if (cJSON_IsObject(data) == 0) { + ESP_LOGJSONE("value at 'data' is not an object", root); + return false; + } + + const cJSON* fqdn = cJSON_GetObjectItemCaseSensitive(data, "fqdn"); + const cJSON* country = cJSON_GetObjectItemCaseSensitive(data, "country"); + + if (cJSON_IsString(fqdn) == 0 || cJSON_IsString(country) == 0) { + ESP_LOGJSONE("value at 'data.fqdn' or 'data.country' is not a string", root); + return false; + } + + out = {}; + + out.fqdn = fqdn->valuestring; + out.country = country->valuestring; + + return true; +} diff --git a/src/serialization/JsonSerial.cpp b/src/serialization/JsonSerial.cpp new file mode 100644 index 00000000..3155ca15 --- /dev/null +++ b/src/serialization/JsonSerial.cpp @@ -0,0 +1,101 @@ +#include "serialization/JsonSerial.h" + +#include "Logging.h" + +const char* const TAG = "JsonSerial"; + +using namespace OpenShock::Serialization; + +bool JsonSerial::ParseShockerCommand(const cJSON* root, JsonSerial::ShockerCommand& out) { + if (cJSON_IsObject(root) == 0) { + ESP_LOGE(TAG, "not an object"); + return false; + } + + const cJSON* model = cJSON_GetObjectItemCaseSensitive(root, "model"); + if (model == nullptr) { + ESP_LOGE(TAG, "missing 'model' field"); + return false; + } + if (cJSON_IsString(model) == 0) { + ESP_LOGE(TAG, "value at 'model' is not a string"); + return false; + } + ShockerModelType modelType; + if (!ShockerModelTypeFromString(model->valuestring, modelType)) { + ESP_LOGE(TAG, "value at 'model' is not a valid shocker model (caixianlin, petrainer)"); + return false; + } + + const cJSON* id = cJSON_GetObjectItemCaseSensitive(root, "id"); + if (id == nullptr) { + ESP_LOGE(TAG, "missing 'id' field"); + return false; + } + if (cJSON_IsNumber(id) == 0) { + ESP_LOGE(TAG, "value at 'id' is not a number"); + return false; + } + int idInt = id->valueint; + if (idInt < 0 || idInt > UINT16_MAX) { + ESP_LOGE(TAG, "value at 'id' is out of range (0-65535)"); + return false; + } + std::uint16_t idU16 = static_cast(idInt); + + const cJSON* command = cJSON_GetObjectItemCaseSensitive(root, "type"); + if (command == nullptr) { + ESP_LOGE(TAG, "missing 'type' field"); + return false; + } + if (cJSON_IsString(command) == 0) { + ESP_LOGE(TAG, "value at 'type' is not a string"); + return false; + } + ShockerCommandType commandType; + if (!ShockerCommandTypeFromString(command->valuestring, commandType)) { + ESP_LOGE(TAG, "value at 'type' is not a valid shocker command (stop, shock, vibrate, sound)"); + return false; + } + + const cJSON* intensity = cJSON_GetObjectItemCaseSensitive(root, "intensity"); + if (intensity == nullptr) { + ESP_LOGE(TAG, "missing 'intensity' field"); + return false; + } + if (cJSON_IsNumber(intensity) == 0) { + ESP_LOGE(TAG, "value at 'intensity' is not a number"); + return false; + } + int intensityInt = intensity->valueint; + if (intensityInt < 0 || intensityInt > UINT8_MAX) { + ESP_LOGE(TAG, "value at 'intensity' is out of range (0-255)"); + return false; + } + std::uint8_t intensityU8 = static_cast(intensityInt); + + const cJSON* durationMs = cJSON_GetObjectItemCaseSensitive(root, "durationMs"); + if (durationMs == nullptr) { + ESP_LOGE(TAG, "missing 'durationMs' field"); + return false; + } + if (cJSON_IsNumber(durationMs) == 0) { + ESP_LOGE(TAG, "value at 'durationMs' is not a number"); + return false; + } + if (durationMs->valueint < 0 || durationMs->valueint > UINT16_MAX) { + ESP_LOGE(TAG, "value at 'durationMs' is out of range (0-65535)"); + return false; + } + std::uint16_t durationMsU16 = static_cast(durationMs->valueint); + + out = { + .model = modelType, + .id = idU16, + .command = commandType, + .intensity = intensityU8, + .durationMs = durationMsU16, + }; + + return true; +} diff --git a/src/serialization/WSGateway.cpp b/src/serialization/WSGateway.cpp index eed037bb..0d0b046d 100644 --- a/src/serialization/WSGateway.cpp +++ b/src/serialization/WSGateway.cpp @@ -1 +1,92 @@ #include "serialization/WSGateway.h" + +#include "config/Config.h" +#include "Logging.h" +#include "Time.h" + +const char* const TAG = "WSGateway"; + +using namespace OpenShock::Serialization; + +bool Gateway::SerializeKeepAliveMessage(Common::SerializationCallbackFn callback) { + flatbuffers::FlatBufferBuilder builder(256); // TODO: Profile this and adjust the size accordingly + + std::int64_t uptime = OpenShock::millis(); + if (uptime < 0) { + ESP_LOGE(TAG, "Failed to get uptime"); + return false; + } + + Gateway::KeepAlive keepAlive(static_cast(uptime)); + auto keepAliveOffset = builder.CreateStruct(keepAlive); + + auto msg = Gateway::CreateDeviceToGatewayMessage(builder, Gateway::DeviceToGatewayMessagePayload::KeepAlive, keepAliveOffset.Union()); + + builder.Finish(msg); + + auto span = builder.GetBufferSpan(); + + return callback(span.data(), span.size()); +} + +bool Gateway::SerializeBootStatusMessage(std::int32_t updateId, OpenShock::FirmwareBootType bootType, const OpenShock::SemVer& version, Common::SerializationCallbackFn callback) { + flatbuffers::FlatBufferBuilder builder(256); // TODO: Profile this and adjust the size accordingly + + auto fbsVersion = Types::CreateSemVerDirect(builder, version.major, version.minor, version.patch, version.prerelease.data(), version.build.data()); + + auto fbsBootStatus = Gateway::CreateBootStatus(builder, bootType, fbsVersion, updateId); + + auto msg = Gateway::CreateDeviceToGatewayMessage(builder, Gateway::DeviceToGatewayMessagePayload::BootStatus, fbsBootStatus.Union()); + + builder.Finish(msg); + + auto span = builder.GetBufferSpan(); + + return callback(span.data(), span.size()); +} + +bool Gateway::SerializeOtaInstallStartedMessage(std::int32_t updateId, const OpenShock::SemVer& version, Common::SerializationCallbackFn callback) { + flatbuffers::FlatBufferBuilder builder(256); // TODO: Profile this and adjust the size accordingly + + auto versionOffset = Types::CreateSemVerDirect(builder, version.major, version.minor, version.patch, version.prerelease.data(), version.build.data()); + + auto otaInstallStartedOffset = Gateway::CreateOtaInstallStarted(builder, updateId, versionOffset); + + auto msg = Gateway::CreateDeviceToGatewayMessage(builder, Gateway::DeviceToGatewayMessagePayload::OtaInstallStarted, otaInstallStartedOffset.Union()); + + builder.Finish(msg); + + auto span = builder.GetBufferSpan(); + + return callback(span.data(), span.size()); +} + +bool Gateway::SerializeOtaInstallProgressMessage(std::int32_t updateId, Gateway::OtaInstallProgressTask task, float progress, Common::SerializationCallbackFn callback) { + flatbuffers::FlatBufferBuilder builder(64); // TODO: Profile this and adjust the size accordingly + + auto otaInstallProgressOffset = Gateway::CreateOtaInstallProgress(builder, updateId, task, progress); + + auto msg = Gateway::CreateDeviceToGatewayMessage(builder, Gateway::DeviceToGatewayMessagePayload::OtaInstallProgress, otaInstallProgressOffset.Union()); + + builder.Finish(msg); + + auto span = builder.GetBufferSpan(); + + return callback(span.data(), span.size()); +} + +bool Gateway::SerializeOtaInstallFailedMessage(std::int32_t updateId, StringView message, bool fatal, Common::SerializationCallbackFn callback) { + flatbuffers::FlatBufferBuilder builder(256); // TODO: Profile this and adjust the size accordingly + + auto messageOffset = builder.CreateString(message.data(), message.size()); + + auto otaInstallFailedOffset = Gateway::CreateOtaInstallFailed(builder, updateId, messageOffset, fatal); + + auto msg = Gateway::CreateDeviceToGatewayMessage(builder, Gateway::DeviceToGatewayMessagePayload::OtaInstallFailed, otaInstallFailedOffset.Union()); + + builder.Finish(msg); + + auto span = builder.GetBufferSpan(); + + return callback(span.data(), span.size()); +} diff --git a/src/serialization/WSLocal.cpp b/src/serialization/WSLocal.cpp index 31025d38..68baff7c 100644 --- a/src/serialization/WSLocal.cpp +++ b/src/serialization/WSLocal.cpp @@ -1,10 +1,14 @@ #include "serialization/WSLocal.h" -#include "Utils/HexUtils.h" +#include "config/Config.h" +#include "Logging.h" +#include "util/HexUtils.h" #include "wifi/WiFiNetwork.h" #include "serialization/_fbs/DeviceToLocalMessage_generated.h" +const char* const TAG = "WSLocal"; + using namespace OpenShock::Serialization; typedef OpenShock::Serialization::Types::WifiAuthMode WiFiAuthMode; @@ -57,7 +61,7 @@ bool Local::SerializeErrorMessage(const char* message, Common::SerializationCall return true; } -bool Local::SerializeReadyMessage(const WiFiNetwork* connectedNetwork, bool gatewayPaired, std::uint8_t radioTxPin, Common::SerializationCallbackFn callback) { +bool Local::SerializeReadyMessage(const WiFiNetwork* connectedNetwork, bool accountLinked, Common::SerializationCallbackFn callback) { flatbuffers::FlatBufferBuilder builder(256); flatbuffers::Offset fbsNetwork = 0; @@ -68,7 +72,13 @@ bool Local::SerializeReadyMessage(const WiFiNetwork* connectedNetwork, bool gate fbsNetwork = 0; } - auto readyMessageOffset = Serialization::Local::CreateReadyMessage(builder, true, fbsNetwork, gatewayPaired, radioTxPin); + auto configOffset = OpenShock::Config::GetAsFlatBuffer(builder, false); + if (configOffset.IsNull()) { + ESP_LOGE(TAG, "Failed to serialize config"); + return false; + } + + auto readyMessageOffset = Serialization::Local::CreateReadyMessage(builder, true, fbsNetwork, accountLinked, configOffset); auto msg = Serialization::Local::CreateDeviceToLocalMessage(builder, Serialization::Local::DeviceToLocalMessagePayload::ReadyMessage, readyMessageOffset.Union()); @@ -97,7 +107,30 @@ bool Local::SerializeWiFiScanStatusChangedEvent(OpenShock::WiFiScanStatus status bool Local::SerializeWiFiNetworkEvent(Types::WifiNetworkEventType eventType, const WiFiNetwork& network, Common::SerializationCallbackFn callback) { flatbuffers::FlatBufferBuilder builder(256); // TODO: Profile this and adjust the size accordingly - auto wrapperOffset = Local::CreateWifiNetworkEvent(builder, eventType, _createWiFiNetwork(builder, network)); + auto networkOffset = _createWiFiNetwork(builder, network); + + auto wrapperOffset = Local::CreateWifiNetworkEvent(builder, eventType, builder.CreateVector(&networkOffset, 1)); // Resulting vector will have 1 element + + auto msg = Local::CreateDeviceToLocalMessage(builder, Local::DeviceToLocalMessagePayload::WifiNetworkEvent, wrapperOffset.Union()); + + builder.Finish(msg); + + auto span = builder.GetBufferSpan(); + + return callback(span.data(), span.size()); +} + +bool Local::SerializeWiFiNetworksEvent(Types::WifiNetworkEventType eventType, const std::vector& networks, Common::SerializationCallbackFn callback) { + flatbuffers::FlatBufferBuilder builder(256); // TODO: Profile this and adjust the size accordingly + + std::vector> fbsNetworks; + fbsNetworks.reserve(networks.size()); + + for (const auto& network : networks) { + fbsNetworks.push_back(_createWiFiNetwork(builder, network)); + } + + auto wrapperOffset = Local::CreateWifiNetworkEvent(builder, eventType, builder.CreateVector(fbsNetworks)); auto msg = Local::CreateDeviceToLocalMessage(builder, Local::DeviceToLocalMessagePayload::WifiNetworkEvent, wrapperOffset.Union()); diff --git a/src/util/Base64Utils.cpp b/src/util/Base64Utils.cpp new file mode 100644 index 00000000..0aba678c --- /dev/null +++ b/src/util/Base64Utils.cpp @@ -0,0 +1,84 @@ +#include "util/Base64Utils.h" + +#include "Logging.h" + +#include + +const char* const TAG = "Base64Utils"; + +using namespace OpenShock; + +std::size_t Base64Utils::Encode(const std::uint8_t* data, std::size_t dataLen, char* output, std::size_t outputLen) noexcept { + std::size_t requiredLen = 0; + + int retval = mbedtls_base64_encode(reinterpret_cast(output), outputLen, &requiredLen, data, dataLen); + if (retval != 0) { + if (retval == MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) { + ESP_LOGW(TAG, "Output buffer too small (expected %zu, got %zu)", requiredLen, outputLen); + } else { + ESP_LOGW(TAG, "Failed to encode data, unknown error: %d", retval); + } + return 0; + } + + return requiredLen; +} + +bool Base64Utils::Encode(const std::uint8_t* data, std::size_t dataLen, std::string& output) { + std::size_t requiredLen = Base64Utils::CalculateEncodedSize(dataLen) + 1; // +1 for null terminator + char* buffer = new char[requiredLen]; + + std::size_t written = Encode(data, dataLen, buffer, requiredLen); + if (written == 0) { + output.clear(); + delete[] buffer; + return false; + } + + buffer[written] = '\0'; + + output.assign(buffer, written); + + delete[] buffer; + + if (written < requiredLen) { + output.resize(written); + } + + return true; +} + +std::size_t Base64Utils::Decode(const char* data, std::size_t dataLen, std::uint8_t* output, std::size_t outputLen) noexcept { + std::size_t requiredLen = 0; + + int retval = mbedtls_base64_decode(output, outputLen, &requiredLen, reinterpret_cast(data), dataLen); + if (retval != 0) { + if (retval == MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) { + ESP_LOGW(TAG, "Output buffer too small (expected %zu, got %zu)", requiredLen, outputLen); + } else if (retval == MBEDTLS_ERR_BASE64_INVALID_CHARACTER) { + ESP_LOGW(TAG, "Invalid character in input data"); + } else { + ESP_LOGW(TAG, "Failed to decode data, unknown error: %d", retval); + } + return 0; + } + + return requiredLen; +} + +bool Base64Utils::Decode(const char* data, std::size_t dataLen, std::vector& output) noexcept { + std::size_t requiredLen = Base64Utils::CalculateDecodedSize(dataLen); + output.resize(requiredLen); + + std::size_t written = Decode(data, dataLen, output.data(), output.size()); + if (written == 0) { + output.clear(); + return false; + } + + if (written < requiredLen) { + output.resize(written); + } + + return true; +} diff --git a/src/CertificateUtils.cpp b/src/util/CertificateUtils.cpp similarity index 95% rename from src/CertificateUtils.cpp rename to src/util/CertificateUtils.cpp index ab41a3a5..17fb46ef 100644 --- a/src/CertificateUtils.cpp +++ b/src/util/CertificateUtils.cpp @@ -1,4 +1,4 @@ -#include "CertificateUtils.h" +#include "util/CertificateUtils.h" #include "Logging.h" @@ -24,7 +24,7 @@ bool OpenShock::CertificateUtils::GetHostCertificate(const char* host, std::vect client.connect(host, 443); - if (!client.connected()) { + if (client.connected() == 0) { ESP_LOGE(TAG, "Failed to connect to host %s", host); return false; } diff --git a/src/util/ParitionUtils.cpp b/src/util/ParitionUtils.cpp new file mode 100644 index 00000000..222fd7e7 --- /dev/null +++ b/src/util/ParitionUtils.cpp @@ -0,0 +1,110 @@ +#include "util/PartitionUtils.h" + +#include "Hashing.h" +#include "http/HTTPRequestManager.h" +#include "Time.h" +#include "util/HexUtils.h" + +#include + +const char* const TAG = "PartitionUtils"; + +bool OpenShock::TryGetPartitionHash(const esp_partition_t* partition, char (&hash)[65]) { + std::uint8_t buffer[32]; + esp_err_t err = esp_partition_get_sha256(partition, buffer); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to get partition hash: %s", esp_err_to_name(err)); + return false; + } + + // Copy the hash to the output buffer + HexUtils::ToHex<32>(buffer, hash, false); + + return true; +} + +bool OpenShock::FlashPartitionFromUrl(const esp_partition_t* partition, StringView remoteUrl, const std::uint8_t (&remoteHash)[32], std::function progressCallback) { + OpenShock::SHA256 sha256; + if (!sha256.begin()) { + ESP_LOGE(TAG, "Failed to initialize SHA256 hash"); + return false; + } + + std::size_t contentLength = 0; + std::size_t contentWritten = 0; + std::int64_t lastProgress = 0; + + auto sizeValidator = [partition, &contentLength, progressCallback, &lastProgress](std::size_t size) -> bool { + if (size > partition->size) { + ESP_LOGE(TAG, "Remote partition binary is too large"); + return false; + } + + // Erase app partition. + if (esp_partition_erase_range(partition, 0, partition->size) != ESP_OK) { + ESP_LOGE(TAG, "Failed to erase partition in preparation for update"); + return false; + } + + contentLength = size; + + lastProgress = OpenShock::millis(); + progressCallback(0, contentLength, 0.0f); + + return true; + }; + auto dataWriter = [partition, &sha256, &contentLength, &contentWritten, progressCallback, &lastProgress](std::size_t offset, const std::uint8_t* data, std::size_t length) -> bool { + if (esp_partition_write(partition, offset, data, length) != ESP_OK) { + ESP_LOGE(TAG, "Failed to write to partition"); + return false; + } + + if (!sha256.update(data, length)) { + ESP_LOGE(TAG, "Failed to update SHA256 hash"); + return false; + } + + contentWritten += length; + + std::int64_t now = OpenShock::millis(); + if (now - lastProgress >= 500) { // Send progress every 500ms + lastProgress = now; + progressCallback(contentWritten, contentLength, static_cast(contentWritten) / static_cast(contentLength)); + } + + return true; + }; + + // Start streaming binary to app partition. + auto appBinaryResponse = OpenShock::HTTP::Download( + remoteUrl, + { + {"Accept", "application/octet-stream"} + }, + sizeValidator, + dataWriter, + {200, 304}, + 180'000 + ); // 3 minutes + if (appBinaryResponse.result != OpenShock::HTTP::RequestResult::Success) { + ESP_LOGE(TAG, "Failed to download remote partition binary: [%u]", appBinaryResponse.code); + return false; + } + + progressCallback(contentLength, contentLength, 1.0f); + ESP_LOGD(TAG, "Wrote %u bytes to partition", appBinaryResponse.data); + + std::array localHash; + if (!sha256.finish(localHash)) { + ESP_LOGE(TAG, "Failed to finish SHA256 hash"); + return false; + } + + // Compare hashes. + if (memcmp(localHash.data(), remoteHash, 32) != 0) { + ESP_LOGE(TAG, "App binary hash mismatch"); + return false; + } + + return true; +} diff --git a/src/util/StringUtils.cpp b/src/util/StringUtils.cpp new file mode 100644 index 00000000..3d7603d2 --- /dev/null +++ b/src/util/StringUtils.cpp @@ -0,0 +1,59 @@ +#include "util/StringUtils.h" + +#include "Logging.h" + +#include +#include + +static const char* TAG = "StringUtils"; + +bool OpenShock::FormatToString(std::string& out, const char* format, ...) { + constexpr std::size_t STACK_BUFFER_SIZE = 128; + + char buffer[STACK_BUFFER_SIZE]; + char* bufferPtr = buffer; + + va_list args; + + // Try format with stack buffer. + va_start(args, format); + int result = vsnprintf(buffer, STACK_BUFFER_SIZE, format, args); + va_end(args); + + // If result is negative, something went wrong. + if (result < 0) { + ESP_LOGE(TAG, "Failed to format string"); + return false; + } + + if (result >= STACK_BUFFER_SIZE) { + // Account for null terminator. + result += 1; + + // Allocate heap buffer. + bufferPtr = new char[result]; + + // Try format with heap buffer. + va_start(args, format); + result = vsnprintf(bufferPtr, result, format, args); + va_end(args); + + // If we still fail, something is wrong. + // Free heap buffer and return false. + if (result < 0) { + delete[] bufferPtr; + ESP_LOGE(TAG, "Failed to format string"); + return false; + } + } + + // Set output string. + out = std::string(bufferPtr, result); + + // Free heap buffer if we used it. + if (bufferPtr != buffer) { + delete[] bufferPtr; + } + + return true; +} diff --git a/src/wifi/WiFiManager.cpp b/src/wifi/WiFiManager.cpp index f3fddeee..04e1890e 100644 --- a/src/wifi/WiFiManager.cpp +++ b/src/wifi/WiFiManager.cpp @@ -1,7 +1,7 @@ #include "wifi/WiFiManager.h" #include "CaptivePortal.h" -#include "Config.h" +#include "config/Config.h" #include "FormatHelpers.h" #include "Logging.h" #include "serialization/WSLocal.h" @@ -10,10 +10,9 @@ #include "wifi/WiFiNetwork.h" #include "wifi/WiFiScanManager.h" -#include - #include +#include #include #include @@ -71,9 +70,7 @@ bool _isConnectRateLimited(const WiFiNetwork& net) { } bool _isSaved(std::function predicate) { - const auto& credentials = Config::GetWiFiCredentials(); - - return std::any_of(credentials.begin(), credentials.end(), predicate); + return Config::AnyWiFiCredentials(predicate); } std::vector::iterator _findNetwork(std::function predicate, bool sortByAttractivity = true) { if (sortByAttractivity) { @@ -117,8 +114,6 @@ bool _getNextWiFiNetwork(OpenShock::Config::WiFiCredentials& creds) { return false; } - memcpy(creds.bssid, net.bssid, sizeof(creds.bssid)); - return true; }) != s_wifiNetworks.end(); } @@ -138,6 +133,8 @@ bool _connectImpl(const char* ssid, const char* password, const std::uint8_t (&b return true; } bool _connectHidden(const std::uint8_t (&bssid)[6], const std::string& password) { + (void)password; + ESP_LOGV(TAG, "Connecting to hidden network " BSSID_FMT, BSSID_ARG(bssid)); // TODO: Implement hidden network support @@ -145,13 +142,10 @@ bool _connectHidden(const std::uint8_t (&bssid)[6], const std::string& password) return false; } -bool _connect(const std::string& ssid, const std::string& password, const std::uint8_t (&bssid)[6]) { +bool _connect(const std::string& ssid, const std::string& password) { if (ssid.empty()) { - return _connectHidden(bssid, password); - } - - if (!_isZeroBSSID(bssid)) { - return _connectImpl(ssid.c_str(), password.c_str(), bssid); + ESP_LOGW(TAG, "Cannot connect to network with empty SSID"); + return false; } auto it = _findNetworkBySSID(ssid.c_str()); @@ -162,9 +156,23 @@ bool _connect(const std::string& ssid, const std::string& password, const std::u return _connectImpl(ssid.c_str(), password.c_str(), it->bssid); } +bool _connect(const std::uint8_t (&bssid)[6], const std::string& password) { + if (_isZeroBSSID(bssid)) { + ESP_LOGW(TAG, "Cannot connect to network with zero BSSID"); + return false; + } + + auto it = _findNetworkByBSSID(bssid); + if (it == s_wifiNetworks.end()) { + ESP_LOGE(TAG, "Failed to find network " BSSID_FMT, BSSID_ARG(bssid)); + return false; + } + + return _connectImpl(it->ssid, password.c_str(), bssid); +} bool _authenticate(const WiFiNetwork& net, const std::string& password) { - std::uint8_t id = Config::AddWiFiCredentials(net.ssid, net.bssid, password); + std::uint8_t id = Config::AddWiFiCredentials(net.ssid, password); if (id == 0) { Serialization::Local::SerializeErrorMessage("too_many_credentials", CaptivePortal::BroadcastMessageBIN); return false; @@ -172,7 +180,7 @@ bool _authenticate(const WiFiNetwork& net, const std::string& password) { Serialization::Local::SerializeWiFiNetworkEvent(Serialization::Types::WifiNetworkEventType::Saved, net, CaptivePortal::BroadcastMessageBIN); - return _connect(net.ssid, password, net.bssid); + return _connect(net.ssid, password); } void _evWiFiConnected(arduino_event_t* event) { @@ -185,7 +193,7 @@ void _evWiFiConnected(arduino_event_t* event) { if (it == s_wifiNetworks.end()) { s_connectedCredentialsID = 0; - ESP_LOGW(TAG, "Connected to unknown network " BSSID_FMT, BSSID_ARG(info.bssid)); + ESP_LOGW(TAG, "Connected to unscanned network \"%s\", BSSID: " BSSID_FMT, reinterpret_cast(info.ssid), BSSID_ARG(info.bssid)); return; } @@ -197,7 +205,7 @@ void _evWiFiConnected(arduino_event_t* event) { Serialization::Local::SerializeWiFiNetworkEvent(Serialization::Types::WifiNetworkEventType::Connected, *it, CaptivePortal::BroadcastMessageBIN); } void _evWiFiGotIP(arduino_event_t* event) { - auto& info = event->event_info.got_ip; + const auto& info = event->event_info.got_ip; std::uint8_t ip[4]; memcpy(ip, &info.ip_info.ip.addr, sizeof(ip)); @@ -207,10 +215,9 @@ void _evWiFiGotIP(arduino_event_t* event) { void _evWiFiGotIP6(arduino_event_t* event) { auto& info = event->event_info.got_ip6; - std::uint8_t ip6[16]; - memcpy(ip6, &info.ip6_info.ip.addr, sizeof(ip6)); + std::uint8_t* ip6 = reinterpret_cast(&info.ip6_info.ip.addr); - ESP_LOGI(TAG, "Got IPv6 address %02x%02x:%02x%02x:%02x%02x:%02x%02x from network " BSSID_FMT, ip6[0], ip6[1], ip6[2], ip6[3], ip6[4], ip6[5], ip6[6], ip6[7], BSSID_ARG(s_connectedBSSID)); + ESP_LOGI(TAG, "Got IPv6 address " IPV6ADDR_FMT " from network " BSSID_FMT, IPV6ADDR_ARG(ip6), BSSID_ARG(s_connectedBSSID)); } void _evWiFiDisconnected(arduino_event_t* event) { s_wifiState = WiFiState::Disconnected; @@ -218,7 +225,7 @@ void _evWiFiDisconnected(arduino_event_t* event) { auto& info = event->event_info.wifi_sta_disconnected; Config::WiFiCredentials creds; - if (!Config::TryGetWiFiCredentialsBySSID(reinterpret_cast(info.ssid), creds) && !Config::TryGetWiFiCredentialsByBSSID(info.bssid, creds)) { + if (!Config::TryGetWiFiCredentialsBySSID(reinterpret_cast(info.ssid), creds)) { ESP_LOGW(TAG, "Disconnected from unknown network... WTF?"); return; } @@ -237,12 +244,13 @@ void _evWiFiScanStarted() { } void _evWiFiScanStatusChanged(OpenShock::WiFiScanStatus status) { // If the scan started, remove any networks that have not been seen in 3 scans if (status == OpenShock::WiFiScanStatus::Started) { - for (auto it = s_wifiNetworks.begin(); it != s_wifiNetworks.end(); ++it) { + for (auto it = s_wifiNetworks.begin(); it != s_wifiNetworks.end();) { if (it->scansMissed++ > 3) { ESP_LOGV(TAG, "Network %s (" BSSID_FMT ") has not been seen in 3 scans, removing from list", it->ssid, BSSID_ARG(it->bssid)); Serialization::Local::SerializeWiFiNetworkEvent(Serialization::Types::WifiNetworkEventType::Lost, *it, CaptivePortal::BroadcastMessageBIN); - s_wifiNetworks.erase(it); - it--; + it = s_wifiNetworks.erase(it); + } else { + ++it; } } } @@ -252,42 +260,59 @@ void _evWiFiScanStatusChanged(OpenShock::WiFiScanStatus status) { // Sort the networks by RSSI std::sort(s_wifiNetworks.begin(), s_wifiNetworks.end(), [](const WiFiNetwork& a, const WiFiNetwork& b) { return a.rssi > b.rssi; }); } + + // Send the scan status changed event + Serialization::Local::SerializeWiFiScanStatusChangedEvent(status, CaptivePortal::BroadcastMessageBIN); } -void _evWiFiNetworkDiscovery(const wifi_ap_record_t* record) { - std::uint8_t credsId = Config::GetWiFiCredentialsIDbyBSSIDorSSID(record->bssid, reinterpret_cast(record->ssid)); +void _evWiFiNetworksDiscovery(const std::vector& records) { + std::vector updatedNetworks; + std::vector discoveredNetworks; - auto it = _findNetworkByBSSID(record->bssid); - if (it != s_wifiNetworks.end()) { - // Update the network - memcpy(it->ssid, record->ssid, sizeof(it->ssid)); - it->channel = record->primary; - it->rssi = record->rssi; - it->authMode = record->authmode; - it->credentialsID = credsId; // TODO: I don't understand why I need to set this here, but it seems to fix a bug where the credentials ID is not set correctly - it->scansMissed = 0; + for (const wifi_ap_record_t* record : records) { + std::uint8_t credsId = Config::GetWiFiCredentialsIDbySSID(reinterpret_cast(record->ssid)); - Serialization::Local::SerializeWiFiNetworkEvent(Serialization::Types::WifiNetworkEventType::Updated, *it, CaptivePortal::BroadcastMessageBIN); - ESP_LOGV(TAG, "Updated network %s (" BSSID_FMT ") with new scan info", it->ssid, BSSID_ARG(it->bssid)); + auto it = _findNetworkByBSSID(record->bssid); + if (it != s_wifiNetworks.end()) { + // Update the network + memcpy(it->ssid, record->ssid, sizeof(it->ssid)); + it->channel = record->primary; + it->rssi = record->rssi; + it->authMode = record->authmode; + it->credentialsID = credsId; // TODO: I don't understand why I need to set this here, but it seems to fix a bug where the credentials ID is not set correctly + it->scansMissed = 0; - return; - } + updatedNetworks.push_back(*it); + ESP_LOGV(TAG, "Updated network %s (" BSSID_FMT ") with new scan info", it->ssid, BSSID_ARG(it->bssid)); - WiFiNetwork network(record->ssid, record->bssid, record->primary, record->rssi, record->authmode, credsId); + continue; + } + + WiFiNetwork network(record->ssid, record->bssid, record->primary, record->rssi, record->authmode, credsId); - Serialization::Local::SerializeWiFiNetworkEvent(Serialization::Types::WifiNetworkEventType::Discovered, network, CaptivePortal::BroadcastMessageBIN); - ESP_LOGV(TAG, "Discovered new network %s (" BSSID_FMT ")", network.ssid, BSSID_ARG(network.bssid)); + discoveredNetworks.push_back(network); + ESP_LOGV(TAG, "Discovered new network %s (" BSSID_FMT ")", network.ssid, BSSID_ARG(network.bssid)); - // Insert the network into the list of networks sorted by RSSI - s_wifiNetworks.insert(std::lower_bound(s_wifiNetworks.begin(), s_wifiNetworks.end(), network, [](const WiFiNetwork& a, const WiFiNetwork& b) { return a.rssi > b.rssi; }), std::move(network)); + // Insert the network into the list of networks sorted by RSSI + s_wifiNetworks.insert(std::lower_bound(s_wifiNetworks.begin(), s_wifiNetworks.end(), network, [](const WiFiNetwork& a, const WiFiNetwork& b) { return a.rssi > b.rssi; }), std::move(network)); + } + + if (!updatedNetworks.empty()) { + Serialization::Local::SerializeWiFiNetworksEvent(Serialization::Types::WifiNetworkEventType::Updated, updatedNetworks, CaptivePortal::BroadcastMessageBIN); + } + if (!discoveredNetworks.empty()) { + Serialization::Local::SerializeWiFiNetworksEvent(Serialization::Types::WifiNetworkEventType::Discovered, discoveredNetworks, CaptivePortal::BroadcastMessageBIN); + } } +esp_err_t set_esp_interface_dns(esp_interface_t interface, IPAddress main_dns, IPAddress backup_dns, IPAddress fallback_dns); + bool WiFiManager::Init() { WiFi.onEvent(_evWiFiConnected, ARDUINO_EVENT_WIFI_STA_CONNECTED); WiFi.onEvent(_evWiFiGotIP, ARDUINO_EVENT_WIFI_STA_GOT_IP); WiFi.onEvent(_evWiFiGotIP6, ARDUINO_EVENT_WIFI_STA_GOT_IP6); WiFi.onEvent(_evWiFiDisconnected, ARDUINO_EVENT_WIFI_STA_DISCONNECTED); WiFiScanManager::RegisterStatusChangedHandler(_evWiFiScanStatusChanged); - WiFiScanManager::RegisterNetworkDiscoveryHandler(_evWiFiNetworkDiscovery); + WiFiScanManager::RegisterNetworksDiscoveredHandler(_evWiFiNetworksDiscovery); if (!WiFiScanManager::Init()) { ESP_LOGE(TAG, "Failed to initialize WiFiScanManager"); @@ -299,6 +324,21 @@ bool WiFiManager::Init() { WiFi.enableSTA(true); WiFi.setHostname(OPENSHOCK_FW_HOSTNAME); // TODO: Add the device name to the hostname (retrieve from API and store in LittleFS) + // If we recognize the network in the ESP's WiFi cache, try to connect to it + wifi_config_t current_conf; + if (esp_wifi_get_config(static_cast(ESP_IF_WIFI_STA), ¤t_conf) == ESP_OK) { + if (current_conf.sta.ssid[0] != '\0') { + if (Config::GetWiFiCredentialsIDbySSID(reinterpret_cast(current_conf.sta.ssid)) != 0) { + WiFi.begin(); + } + } + } + + if (set_esp_interface_dns(ESP_IF_WIFI_STA, IPAddress(1, 1, 1, 1), IPAddress(8, 8, 8, 8), IPAddress(9, 9, 9, 9)) != ESP_OK) { + ESP_LOGE(TAG, "Failed to set DNS servers"); + return false; + } + return true; } @@ -317,21 +357,6 @@ bool WiFiManager::Save(const char* ssid, const std::string& password) { return _authenticate(*it, password); } -bool WiFiManager::Save(const std::uint8_t (&bssid)[6], const std::string& password) { - ESP_LOGV(TAG, "Authenticating to network " BSSID_FMT, BSSID_ARG(bssid)); - - auto it = _findNetworkByBSSID(bssid); - if (it == s_wifiNetworks.end()) { - ESP_LOGE(TAG, "Failed to find network with BSSID " BSSID_FMT, BSSID_ARG(bssid)); - - Serialization::Local::SerializeErrorMessage("network_not_found", CaptivePortal::BroadcastMessageBIN); - - return false; - } - - return _authenticate(*it, password); -} - bool WiFiManager::Forget(const char* ssid) { ESP_LOGV(TAG, "Forgetting network %s", ssid); @@ -358,38 +383,12 @@ bool WiFiManager::Forget(const char* ssid) { return true; } -bool WiFiManager::Forget(const std::uint8_t (&bssid)[6]) { - ESP_LOGV(TAG, "Forgetting network " BSSID_FMT, BSSID_ARG(bssid)); - - auto it = std::find_if(s_wifiNetworks.begin(), s_wifiNetworks.end(), [bssid](const WiFiNetwork& net) { return memcmp(net.bssid, bssid, sizeof(bssid)) == 0; }); - if (it == s_wifiNetworks.end()) { - ESP_LOGE(TAG, "Failed to find network with BSSID " BSSID_FMT, BSSID_ARG(bssid)); - return false; - } - - std::uint8_t credsId = it->credentialsID; - - // Check if the network is currently connected - if (s_connectedCredentialsID == credsId) { - // Disconnect from the network - WiFiManager::Disconnect(); - } - - // Remove the credentials from the config - if (Config::RemoveWiFiCredentials(credsId)) { - it->credentialsID = 0; - Serialization::Local::SerializeWiFiNetworkEvent(Serialization::Types::WifiNetworkEventType::Removed, *it, CaptivePortal::BroadcastMessageBIN); - } - - return true; -} - bool WiFiManager::RefreshNetworkCredentials() { ESP_LOGV(TAG, "Refreshing network credentials"); for (auto& net : s_wifiNetworks) { Config::WiFiCredentials creds; - if (Config::TryGetWiFiCredentialsBySSID(net.ssid, creds) || Config::TryGetWiFiCredentialsByBSSID(net.bssid, creds)) { + if (Config::TryGetWiFiCredentialsBySSID(net.ssid, creds)) { ESP_LOGV(TAG, "Found credentials for network %s (" BSSID_FMT ")", net.ssid, BSSID_ARG(net.bssid)); net.credentialsID = creds.id; } else { @@ -405,14 +404,6 @@ bool WiFiManager::IsSaved(const char* ssid) { return _isSaved([ssid](const Config::WiFiCredentials& creds) { return creds.ssid == ssid; }); } -bool WiFiManager::IsSaved(const std::uint8_t (&bssid)[6]) { - return _isSaved([bssid](const Config::WiFiCredentials& creds) { return memcmp(creds.bssid, bssid, sizeof(bssid)) == 0; }); -} - -bool WiFiManager::IsSaved(const char* ssid, const std::uint8_t (&bssid)[6]) { - return _isSaved([ssid, bssid](const Config::WiFiCredentials& creds) { return creds.ssid == ssid || memcmp(creds.bssid, bssid, sizeof(bssid)) == 0; }); -} - bool WiFiManager::Connect(const char* ssid) { Config::WiFiCredentials creds; if (!Config::TryGetWiFiCredentialsBySSID(ssid, creds)) { @@ -435,9 +426,15 @@ bool WiFiManager::Connect(const char* ssid) { } bool WiFiManager::Connect(const std::uint8_t (&bssid)[6]) { + auto it = _findNetworkByBSSID(bssid); + if (it == s_wifiNetworks.end()) { + ESP_LOGE(TAG, "Failed to find network " BSSID_FMT, BSSID_ARG(bssid)); + return false; + } + Config::WiFiCredentials creds; - if (!Config::TryGetWiFiCredentialsByBSSID(bssid, creds)) { - ESP_LOGE(TAG, "Failed to find credentials for network " BSSID_FMT, BSSID_ARG(bssid)); + if (!Config::TryGetWiFiCredentialsBySSID(it->ssid, creds)) { + ESP_LOGE(TAG, "Failed to find credentials for network %s (" BSSID_FMT ")", it->ssid, BSSID_ARG(it->bssid)); return false; } @@ -464,6 +461,15 @@ bool WiFiManager::IsConnected() { } bool WiFiManager::GetConnectedNetwork(OpenShock::WiFiNetwork& network) { if (s_connectedCredentialsID == 0) { + if (IsConnected()) { + // We connected without a scan, so populate the network with the current connection info manually + network.credentialsID = 0; + memcpy(network.ssid, WiFi.SSID().c_str(), WiFi.SSID().length() + 1); + memcpy(network.bssid, WiFi.BSSID(), sizeof(network.bssid)); + network.channel = WiFi.channel(); + network.rssi = WiFi.RSSI(); + return true; + } return false; } @@ -477,6 +483,29 @@ bool WiFiManager::GetConnectedNetwork(OpenShock::WiFiNetwork& network) { return true; } +bool WiFiManager::GetIPAddress(char* ipAddress) { + if (!IsConnected()) { + return false; + } + + IPAddress ip = WiFi.localIP(); + snprintf(ipAddress, IPV4ADDR_FMT_LEN + 1, IPV4ADDR_FMT, IPV4ADDR_ARG(ip)); + + return true; +} + +bool WiFiManager::GetIPv6Address(char* ipAddress) { + if (!IsConnected()) { + return false; + } + + IPv6Address ip = WiFi.localIPv6(); + const std::uint8_t* ipPtr = ip; // Using the implicit conversion operator of IPv6Address + snprintf(ipAddress, IPV6ADDR_FMT_LEN + 1, IPV6ADDR_FMT, IPV6ADDR_ARG(ipPtr)); + + return true; +} + static std::int64_t s_lastScanRequest = 0; void WiFiManager::Update() { if (s_wifiState != WiFiState::Disconnected || WiFiScanManager::IsScanning()) return; @@ -492,11 +521,11 @@ void WiFiManager::Update() { return; } - if (_connect(creds.ssid, creds.password, creds.bssid)) { + if (_connect(creds.ssid, creds.password)) { return; } - ESP_LOGE(TAG, "Failed to connect to network %s (" BSSID_FMT ")", creds.ssid.c_str(), BSSID_ARG(creds.bssid)); + ESP_LOGE(TAG, "Failed to connect to network %s", creds.ssid.c_str()); } Config::WiFiCredentials creds; @@ -511,5 +540,9 @@ void WiFiManager::Update() { return; } - _connect(creds.ssid, creds.password, creds.bssid); + _connect(creds.ssid, creds.password); +} + +std::vector WiFiManager::GetDiscoveredWiFiNetworks() { + return s_wifiNetworks; } diff --git a/src/wifi/WiFiNetwork.cpp b/src/wifi/WiFiNetwork.cpp index aa8e038e..388fead1 100644 --- a/src/wifi/WiFiNetwork.cpp +++ b/src/wifi/WiFiNetwork.cpp @@ -1,18 +1,18 @@ #include "wifi/WiFiNetwork.h" -#include "Utils/HexUtils.h" +#include "util/HexUtils.h" #include using namespace OpenShock; -WiFiNetwork::WiFiNetwork() : channel(0), rssi(0), authMode(WIFI_AUTH_MAX), credentialsID(0), connectAttempts(0), lastConnectAttempt(0), scansMissed(0) { +WiFiNetwork::WiFiNetwork() : ssid {0}, bssid {0}, channel(0), rssi(0), authMode(WIFI_AUTH_MAX), credentialsID(0), connectAttempts(0), lastConnectAttempt(0), scansMissed(0) { memset(ssid, 0, sizeof(ssid)); memset(bssid, 0, sizeof(bssid)); } WiFiNetwork::WiFiNetwork(const wifi_ap_record_t* apRecord, std::uint8_t credentialsId) - : channel(apRecord->primary), rssi(apRecord->rssi), authMode(apRecord->authmode), credentialsID(credentialsId), connectAttempts(0), lastConnectAttempt(0), scansMissed(0) { + : ssid {0}, bssid {0}, channel(apRecord->primary), rssi(apRecord->rssi), authMode(apRecord->authmode), credentialsID(credentialsId), connectAttempts(0), lastConnectAttempt(0), scansMissed(0) { static_assert(sizeof(ssid) == sizeof(apRecord->ssid) && sizeof(ssid) == 33, "SSID buffers must be 33 bytes long! (32 bytes for the SSID + 1 byte for the null terminator)"); static_assert(sizeof(bssid) == sizeof(apRecord->bssid) && sizeof(bssid) == 6, "BSSIDs must be 6 bytes long!"); @@ -21,7 +21,7 @@ WiFiNetwork::WiFiNetwork(const wifi_ap_record_t* apRecord, std::uint8_t credenti } WiFiNetwork::WiFiNetwork(const char (&ssid)[33], const std::uint8_t (&bssid)[6], std::uint8_t channel, std::int8_t rssi, wifi_auth_mode_t authMode, std::uint8_t credentialsId) - : channel(channel), rssi(rssi), authMode(authMode), credentialsID(credentialsId), connectAttempts(0), lastConnectAttempt(0), scansMissed(0) { + : ssid {0}, bssid {0}, channel(channel), rssi(rssi), authMode(authMode), credentialsID(credentialsId), connectAttempts(0), lastConnectAttempt(0), scansMissed(0) { static_assert(sizeof(ssid) == sizeof(this->ssid) && sizeof(ssid) == 33, "SSID buffers must be 33 bytes long! (32 bytes for the SSID + 1 byte for the null terminator)"); static_assert(sizeof(bssid) == sizeof(this->bssid) && sizeof(bssid) == 6, "BSSIDs must be 6 bytes long!"); diff --git a/src/wifi/WiFiScanManager.cpp b/src/wifi/WiFiScanManager.cpp index dbf3502a..7998cadb 100644 --- a/src/wifi/WiFiScanManager.cpp +++ b/src/wifi/WiFiScanManager.cpp @@ -10,86 +10,167 @@ const char* const TAG = "WiFiScanManager"; constexpr const std::uint8_t OPENSHOCK_WIFI_SCAN_MAX_CHANNEL = 13; constexpr const std::uint32_t OPENSHOCK_WIFI_SCAN_MAX_MS_PER_CHANNEL = 300; // Adjusting this value will affect the scan rate, but may also affect the scan results +constexpr const std::uint32_t OPENSHOCK_WIFI_SCAN_TIMEOUT_MS = 10 * 1000; + +enum WiFiScanTaskNotificationFlags { + CHANNEL_DONE = 1 << 0, + ERROR = 1 << 1, + WIFI_DISABLED = 1 << 2, + CLEAR_FLAGS = CHANNEL_DONE | ERROR +}; using namespace OpenShock; -static bool s_initialized = false; -static bool s_scanInProgress = false; -static bool s_channelScanDone = false; -static bool s_scanAborted = false; -static std::uint8_t s_currentChannel = 0; +static bool s_initialized = false; +static TaskHandle_t s_scanTaskHandle = nullptr; +static SemaphoreHandle_t s_scanTaskMutex = xSemaphoreCreateMutex(); +static std::uint8_t s_currentChannel = 0; static std::map s_statusChangedHandlers; -static std::map s_networkDiscoveredHandlers; - -void _setScanInProgress(bool inProgress) { - if (s_scanInProgress != inProgress) { - s_scanInProgress = inProgress; - if (inProgress) { - for (auto& it : s_statusChangedHandlers) { - it.second(WiFiScanStatus::Started); - it.second(WiFiScanStatus::InProgress); - } - WiFi.scanDelete(); - } else { - WiFiScanStatus status; - if (s_scanAborted) { - status = WiFiScanStatus::Aborted; - s_scanAborted = false; - } else { - status = WiFiScanStatus::Completed; - } - for (auto& it : s_statusChangedHandlers) { - it.second(status); - } - } +static std::map s_networksDiscoveredHandlers; + +bool _notifyTask(WiFiScanTaskNotificationFlags flags) { + xSemaphoreTake(s_scanTaskMutex, portMAX_DELAY); + + bool success = false; + + if (s_scanTaskHandle != nullptr) { + success = xTaskNotify(s_scanTaskHandle, flags, eSetBits) == pdPASS; } - if (!inProgress) { - s_currentChannel = 0; - s_channelScanDone = false; + xSemaphoreGive(s_scanTaskMutex); + + return success; +} + +void _notifyStatusChangedHandlers(OpenShock::WiFiScanStatus status) { + for (auto& it : s_statusChangedHandlers) { + it.second(status); } } +bool _isScanError(std::int16_t retval) { + return retval < 0 && retval != WIFI_SCAN_RUNNING; +} + void _handleScanError(std::int16_t retval) { - s_channelScanDone = true; + if (retval >= 0) return; + + _notifyTask(WiFiScanTaskNotificationFlags::ERROR); if (retval == WIFI_SCAN_FAILED) { ESP_LOGE(TAG, "Failed to start scan on channel %u", s_currentChannel); - for (auto& it : s_statusChangedHandlers) { - it.second(WiFiScanStatus::Error); - } + return; + } + + if (retval == WIFI_SCAN_RUNNING) { + ESP_LOGE(TAG, "Scan is running on channel %u", s_currentChannel); return; } ESP_LOGE(TAG, "Scan returned an unknown error"); } -void _iterateChannel() { - if (s_currentChannel-- <= 1) { - s_currentChannel = 0; - _setScanInProgress(false); - return; +std::int16_t _scanChannel(std::uint8_t channel) { + std::int16_t retval = WiFi.scanNetworks(true, true, false, OPENSHOCK_WIFI_SCAN_MAX_MS_PER_CHANNEL, channel); + if (!_isScanError(retval)) { + return retval; } - s_channelScanDone = false; + _handleScanError(retval); - std::int16_t retval = WiFi.scanNetworks(true, true, false, OPENSHOCK_WIFI_SCAN_MAX_MS_PER_CHANNEL, s_currentChannel); + return retval; +} - if (retval == WIFI_SCAN_RUNNING) { - _setScanInProgress(true); - return; +WiFiScanStatus _scanningTaskImpl() { + // Start the scan on the highest channel and work our way down + std::uint8_t channel = OPENSHOCK_WIFI_SCAN_MAX_CHANNEL; + + // Start the scan on the first channel + std::int16_t retval = _scanChannel(channel); + if (_isScanError(retval)) { + return WiFiScanStatus::Error; } - _handleScanError(retval); + // Notify the status changed handlers that the scan has started and is in progress + _notifyStatusChangedHandlers(WiFiScanStatus::Started); + _notifyStatusChangedHandlers(WiFiScanStatus::InProgress); + + // Scan each channel until we're done + while (true) { + std::uint32_t notificationFlags = 0; + + // Wait for the scan to complete, _evScanCompleted will notify us when it's done + if (xTaskNotifyWait(0, WiFiScanTaskNotificationFlags::CLEAR_FLAGS, ¬ificationFlags, pdMS_TO_TICKS(OPENSHOCK_WIFI_SCAN_TIMEOUT_MS)) != pdTRUE) { + ESP_LOGE(TAG, "Scan timed out"); + return WiFiScanStatus::TimedOut; + } + + // Check if we were notified of an error or if WiFi was disabled + if (notificationFlags != WiFiScanTaskNotificationFlags::CHANNEL_DONE) { + if (notificationFlags & WiFiScanTaskNotificationFlags::WIFI_DISABLED) { + ESP_LOGE(TAG, "Scan task exiting due to being notified that WiFi was disabled"); + return WiFiScanStatus::Aborted; + } + + if (notificationFlags & WiFiScanTaskNotificationFlags::ERROR) { + ESP_LOGE(TAG, "Scan task exiting due to being notified of an error"); + return WiFiScanStatus::Error; + } + + return WiFiScanStatus::Error; + } + + // Select the next channel, or break if we're done + if (--channel <= 0) { + break; + } + + // Start the scan on the next channel + retval = _scanChannel(channel); + if (_isScanError(retval)) { + return WiFiScanStatus::Error; + } + } + + return WiFiScanStatus::Completed; +} + +void _scanningTask(void* arg) { + (void)arg; + + // Start the scan + WiFiScanStatus status = _scanningTaskImpl(); + + // Notify the status changed handlers of the scan result + _notifyStatusChangedHandlers(status); + + // Clear the task handle + xSemaphoreTake(s_scanTaskMutex, portMAX_DELAY); + s_scanTaskHandle = nullptr; + xSemaphoreGive(s_scanTaskMutex); + + // Kill this task + vTaskDelete(nullptr); } void _evScanCompleted(arduino_event_id_t event, arduino_event_info_t info) { + (void)event; + (void)info; + std::int16_t numNetworks = WiFi.scanComplete(); - if (numNetworks < 0) { + if (_isScanError(numNetworks)) { _handleScanError(numNetworks); return; } + if (numNetworks == WIFI_SCAN_RUNNING) { + ESP_LOGE(TAG, "Scan completed but scan is still running... WTF?"); + return; + } + + std::vector networkRecords; + networkRecords.reserve(numNetworks); + for (std::int16_t i = 0; i < numNetworks; i++) { wifi_ap_record_t* record = reinterpret_cast(WiFi.getScanInfoByIndex(i)); if (record == nullptr) { @@ -97,21 +178,28 @@ void _evScanCompleted(arduino_event_id_t event, arduino_event_info_t info) { return; } - for (auto& it : s_networkDiscoveredHandlers) { - it.second(record); - } + networkRecords.push_back(record); + } + + // Notify the networks discovered handlers + for (auto& it : s_networksDiscoveredHandlers) { + it.second(networkRecords); } - s_channelScanDone = true; + // Notify the scan task that we're done + _notifyTask(WiFiScanTaskNotificationFlags::CHANNEL_DONE); } void _evSTAStopped(arduino_event_id_t event, arduino_event_info_t info) { - _setScanInProgress(false); + (void)event; + (void)info; + + _notifyTask(WiFiScanTaskNotificationFlags::WIFI_DISABLED); } bool WiFiScanManager::Init() { if (s_initialized) { - ESP_LOGW(TAG, "WiFiScanManager::Init() called twice"); - return false; + ESP_LOGW(TAG, "WiFiScanManager is already initialized"); + return true; } WiFi.onEvent(_evScanCompleted, ARDUINO_EVENT_WIFI_SCAN_DONE); @@ -123,29 +211,53 @@ bool WiFiScanManager::Init() { } bool WiFiScanManager::IsScanning() { - return s_scanInProgress; + return s_scanTaskHandle != nullptr; } bool WiFiScanManager::StartScan() { - if (s_scanInProgress) { - ESP_LOGW(TAG, "Cannot start scan: scan is already in progress"); + xSemaphoreTake(s_scanTaskMutex, portMAX_DELAY); + + // Check if a scan is already in progress + if (s_scanTaskHandle != nullptr && eTaskGetState(s_scanTaskHandle) != eDeleted) { + ESP_LOGW(TAG, "Cannot start scan: scan task is already running"); + + xSemaphoreGive(s_scanTaskMutex); return false; } - WiFi.enableSTA(true); - s_currentChannel = OPENSHOCK_WIFI_SCAN_MAX_CHANNEL; - _iterateChannel(); + // Start the scan task + if (xTaskCreate(_scanningTask, "WiFiScanManager", 4096, nullptr, 1, &s_scanTaskHandle) != pdPASS) { + ESP_LOGE(TAG, "Failed to create scan task"); + xSemaphoreGive(s_scanTaskMutex); + return false; + } + + xSemaphoreGive(s_scanTaskMutex); return true; } -void WiFiScanManager::AbortScan() { - if (!s_scanInProgress) { - ESP_LOGW(TAG, "Cannot cancel scan: no scan is in progress"); - return; +bool WiFiScanManager::AbortScan() { + xSemaphoreTake(s_scanTaskMutex, portMAX_DELAY); + + // Check if a scan is in progress + if (s_scanTaskHandle == nullptr || eTaskGetState(s_scanTaskHandle) == eDeleted) { + ESP_LOGW(TAG, "Cannot abort scan: no scan is in progress"); + + xSemaphoreGive(s_scanTaskMutex); + return false; } - s_scanAborted = true; - s_currentChannel = 0; + // Kill the task + vTaskDelete(s_scanTaskHandle); + s_scanTaskHandle = nullptr; + + // Inform the change handlers that the scan was aborted + for (auto& it : s_statusChangedHandlers) { + it.second(WiFiScanStatus::Aborted); + } + + xSemaphoreGive(s_scanTaskMutex); + return true; } std::uint64_t WiFiScanManager::RegisterStatusChangedHandler(const WiFiScanManager::StatusChangedHandler& handler) { @@ -162,24 +274,16 @@ void WiFiScanManager::UnregisterStatusChangedHandler(std::uint64_t handle) { } } -std::uint64_t WiFiScanManager::RegisterNetworkDiscoveryHandler(const WiFiScanManager::NetworkDiscoveryHandler& handler) { - static std::uint64_t nextHandle = 0; - std::uint64_t handle = nextHandle++; - s_networkDiscoveredHandlers[handle] = handler; +std::uint64_t WiFiScanManager::RegisterNetworksDiscoveredHandler(const WiFiScanManager::NetworksDiscoveredHandler& handler) { + static std::uint64_t nextHandle = 0; + std::uint64_t handle = nextHandle++; + s_networksDiscoveredHandlers[handle] = handler; return handle; } -void WiFiScanManager::UnregisterNetworkDiscoveredHandler(std::uint64_t handle) { - auto it = s_networkDiscoveredHandlers.find(handle); - - if (it != s_networkDiscoveredHandlers.end()) { - s_networkDiscoveredHandlers.erase(it); - } -} - -void WiFiScanManager::Update() { - if (!s_initialized) return; +void WiFiScanManager::UnregisterNetworksDiscoveredHandler(std::uint64_t handle) { + auto it = s_networksDiscoveredHandlers.find(handle); - if (s_scanInProgress && s_channelScanDone) { - _iterateChannel(); + if (it != s_networksDiscoveredHandlers.end()) { + s_networksDiscoveredHandlers.erase(it); } }