Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50b3741b1a |
27
.github/workflows/playwright.yml
vendored
27
.github/workflows/playwright.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -25,23 +25,9 @@ vite.config.ts.timestamp-*
|
||||
/src-tauri/target/
|
||||
/src-tauri/target-codex-check*/
|
||||
/src-tauri/gen/schemas/
|
||||
/src-tauri/gen/android/app/build/
|
||||
/src-tauri/gen/android/buildSrc/build/
|
||||
/src-tauri/gen/android/.gradle/
|
||||
/src-tauri/gen/android/app/.gradle/
|
||||
/src-tauri/gen/android/buildSrc/.gradle/
|
||||
/src-tauri/gen/android/build/reports/
|
||||
|
||||
/src-tauri/program.log*
|
||||
/src-tauri/recording_replay_debug_*.csv
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Playwright
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/playwright/.auth/
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "eskin-finger-sdk"]
|
||||
path = eskin-finger-sdk
|
||||
url = https://gitea.e-skin.top/yanjie/eskin-finger-sdk.git
|
||||
Binary file not shown.
Binary file not shown.
BIN
ad_solver.exe
BIN
ad_solver.exe
Binary file not shown.
BIN
ad_solver.pdb
BIN
ad_solver.pdb
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,359 +0,0 @@
|
||||
('d:\\JE-Skin-main\\devkit\\dist\\je-skin-devkit-server.exe',
|
||||
True,
|
||||
False,
|
||||
False,
|
||||
'C:\\Python314\\Lib\\site-packages\\PyInstaller\\bootloader\\images\\icon-console.ico',
|
||||
None,
|
||||
False,
|
||||
False,
|
||||
b'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<assembly xmlns='
|
||||
b'"urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">\n <trustInfo x'
|
||||
b'mlns="urn:schemas-microsoft-com:asm.v3">\n <security>\n <requested'
|
||||
b'Privileges>\n <requestedExecutionLevel level="asInvoker" uiAccess='
|
||||
b'"false"/>\n </requestedPrivileges>\n </security>\n </trustInfo>\n '
|
||||
b'<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">\n <'
|
||||
b'application>\n <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f'
|
||||
b'0}"/>\n <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>\n '
|
||||
b' <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>\n <s'
|
||||
b'upportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>\n <supporte'
|
||||
b'dOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>\n </application>\n <'
|
||||
b'/compatibility>\n <application xmlns="urn:schemas-microsoft-com:asm.v3">'
|
||||
b'\n <windowsSettings>\n <longPathAware xmlns="http://schemas.micros'
|
||||
b'oft.com/SMI/2016/WindowsSettings">true</longPathAware>\n </windowsSett'
|
||||
b'ings>\n </application>\n <dependency>\n <dependentAssembly>\n <ass'
|
||||
b'emblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version='
|
||||
b'"6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" langua'
|
||||
b'ge="*"/>\n </dependentAssembly>\n </dependency>\n</assembly>',
|
||||
True,
|
||||
False,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
'd:\\JE-Skin-main\\devkit\\build\\je-skin-devkit-server\\je-skin-devkit-server.pkg',
|
||||
[('pyi-contents-directory _internal', '', 'OPTION'),
|
||||
('PYZ-00.pyz',
|
||||
'd:\\JE-Skin-main\\devkit\\build\\je-skin-devkit-server\\PYZ-00.pyz',
|
||||
'PYZ'),
|
||||
('struct',
|
||||
'd:\\JE-Skin-main\\devkit\\build\\je-skin-devkit-server\\localpycs\\struct.pyc',
|
||||
'PYMODULE'),
|
||||
('pyimod01_archive',
|
||||
'd:\\JE-Skin-main\\devkit\\build\\je-skin-devkit-server\\localpycs\\pyimod01_archive.pyc',
|
||||
'PYMODULE'),
|
||||
('pyimod02_importers',
|
||||
'd:\\JE-Skin-main\\devkit\\build\\je-skin-devkit-server\\localpycs\\pyimod02_importers.pyc',
|
||||
'PYMODULE'),
|
||||
('pyimod03_ctypes',
|
||||
'd:\\JE-Skin-main\\devkit\\build\\je-skin-devkit-server\\localpycs\\pyimod03_ctypes.pyc',
|
||||
'PYMODULE'),
|
||||
('pyimod04_pywin32',
|
||||
'd:\\JE-Skin-main\\devkit\\build\\je-skin-devkit-server\\localpycs\\pyimod04_pywin32.pyc',
|
||||
'PYMODULE'),
|
||||
('pyiboot01_bootstrap',
|
||||
'C:\\Python314\\Lib\\site-packages\\PyInstaller\\loader\\pyiboot01_bootstrap.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_inspect',
|
||||
'C:\\Python314\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_pkgutil',
|
||||
'C:\\Python314\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_multiprocessing',
|
||||
'C:\\Python314\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_multiprocessing.py',
|
||||
'PYSOURCE'),
|
||||
('sensor_server', 'D:\\JE-Skin-main\\devkit\\sensor_server.py', 'PYSOURCE'),
|
||||
('python314.dll', 'C:\\Python314\\python314.dll', 'BINARY'),
|
||||
('numpy.libs\\msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy.libs\\msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll',
|
||||
'BINARY'),
|
||||
('numpy.libs\\libscipy_openblas64_-63c857e738469261263c764a36be9436.dll',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy.libs\\libscipy_openblas64_-63c857e738469261263c764a36be9436.dll',
|
||||
'BINARY'),
|
||||
('select.pyd', 'C:\\Python314\\DLLs\\select.pyd', 'EXTENSION'),
|
||||
('_multiprocessing.pyd',
|
||||
'C:\\Python314\\DLLs\\_multiprocessing.pyd',
|
||||
'EXTENSION'),
|
||||
('_zstd.pyd', 'C:\\Python314\\DLLs\\_zstd.pyd', 'EXTENSION'),
|
||||
('pyexpat.pyd', 'C:\\Python314\\DLLs\\pyexpat.pyd', 'EXTENSION'),
|
||||
('_lzma.pyd', 'C:\\Python314\\DLLs\\_lzma.pyd', 'EXTENSION'),
|
||||
('_bz2.pyd', 'C:\\Python314\\DLLs\\_bz2.pyd', 'EXTENSION'),
|
||||
('_ssl.pyd', 'C:\\Python314\\DLLs\\_ssl.pyd', 'EXTENSION'),
|
||||
('_hashlib.pyd', 'C:\\Python314\\DLLs\\_hashlib.pyd', 'EXTENSION'),
|
||||
('unicodedata.pyd', 'C:\\Python314\\DLLs\\unicodedata.pyd', 'EXTENSION'),
|
||||
('_decimal.pyd', 'C:\\Python314\\DLLs\\_decimal.pyd', 'EXTENSION'),
|
||||
('_socket.pyd', 'C:\\Python314\\DLLs\\_socket.pyd', 'EXTENSION'),
|
||||
('_ctypes.pyd', 'C:\\Python314\\DLLs\\_ctypes.pyd', 'EXTENSION'),
|
||||
('_queue.pyd', 'C:\\Python314\\DLLs\\_queue.pyd', 'EXTENSION'),
|
||||
('numpy\\_core\\_multiarray_tests.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\_core\\_multiarray_tests.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\_core\\_multiarray_umath.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\_core\\_multiarray_umath.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('_wmi.pyd', 'C:\\Python314\\DLLs\\_wmi.pyd', 'EXTENSION'),
|
||||
('_overlapped.pyd', 'C:\\Python314\\DLLs\\_overlapped.pyd', 'EXTENSION'),
|
||||
('_asyncio.pyd', 'C:\\Python314\\DLLs\\_asyncio.pyd', 'EXTENSION'),
|
||||
('numpy\\linalg\\_umath_linalg.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\linalg\\_umath_linalg.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\mtrand.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\mtrand.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\bit_generator.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\bit_generator.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\_sfc64.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\_sfc64.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\_philox.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\_philox.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\_pcg64.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\_pcg64.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\_mt19937.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\_mt19937.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\_generator.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\_generator.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\_common.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\_common.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\_bounded_integers.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\_bounded_integers.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\fft\\_pocketfft_umath.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\fft\\_pocketfft_umath.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('_elementtree.pyd', 'C:\\Python314\\DLLs\\_elementtree.pyd', 'EXTENSION'),
|
||||
('grpc\\_cython\\cygrpc.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\grpc\\_cython\\cygrpc.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('google\\_upb\\_message.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\google\\_upb\\_message.pyd',
|
||||
'EXTENSION'),
|
||||
('grpc_tools\\_protoc_compiler.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\grpc_tools\\_protoc_compiler.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('api-ms-win-crt-filesystem-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-filesystem-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-locale-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-locale-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-time-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-time-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-environment-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-environment-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-runtime-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-runtime-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-convert-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-convert-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-heap-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-heap-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-string-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-string-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-process-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-process-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-math-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-math-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-stdio-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-stdio-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-conio-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-conio-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('VCRUNTIME140.dll', 'C:\\Python314\\VCRUNTIME140.dll', 'BINARY'),
|
||||
('VCRUNTIME140_1.dll', 'C:\\Python314\\VCRUNTIME140_1.dll', 'BINARY'),
|
||||
('api-ms-win-crt-utility-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-utility-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-private-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-private-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('libcrypto-3.dll', 'C:\\Python314\\DLLs\\libcrypto-3.dll', 'BINARY'),
|
||||
('libssl-3.dll', 'C:\\Python314\\DLLs\\libssl-3.dll', 'BINARY'),
|
||||
('libffi-8.dll', 'C:\\Python314\\DLLs\\libffi-8.dll', 'BINARY'),
|
||||
('python3.dll', 'C:\\Python314\\python3.dll', 'BINARY'),
|
||||
('ucrtbase.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\ucrtbase.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-profile-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-profile-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-processthreads-l1-1-1.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-processthreads-l1-1-1.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-errorhandling-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-errorhandling-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-memory-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-memory-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-file-l2-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-file-l2-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-processthreads-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-processthreads-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-debug-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-debug-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-file-l1-2-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-file-l1-2-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-string-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-string-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-namedpipe-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-namedpipe-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-timezone-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-timezone-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-rtlsupport-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-rtlsupport-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-localization-l1-2-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-localization-l1-2-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-datetime-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-datetime-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-util-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-util-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-console-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-console-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-synch-l1-2-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-synch-l1-2-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-interlocked-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-interlocked-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-handle-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-handle-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-libraryloader-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-libraryloader-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-fibers-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-fibers-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-heap-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-heap-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-file-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-file-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-sysinfo-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-sysinfo-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-processenvironment-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-processenvironment-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-synch-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-synch-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('grpc\\_cython\\_credentials\\roots.pem',
|
||||
'C:\\Python314\\Lib\\site-packages\\grpc\\_cython\\_credentials\\roots.pem',
|
||||
'DATA'),
|
||||
('grpc\\_cython\\_cygrpc\\private_key_signing\\private_key_signer_py_wrapper.h',
|
||||
'C:\\Python314\\Lib\\site-packages\\grpc\\_cython\\_cygrpc\\private_key_signing\\private_key_signer_py_wrapper.h',
|
||||
'DATA'),
|
||||
('grpc\\_cython\\_cygrpc\\private_key_signing\\private_key_signer_py_wrapper.cc',
|
||||
'C:\\Python314\\Lib\\site-packages\\grpc\\_cython\\_cygrpc\\private_key_signing\\private_key_signer_py_wrapper.cc',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\common\\pythoncapi-compat\\COPYING',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\common\\pythoncapi-compat\\COPYING',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\INSTALLER',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\INSTALLER',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\REQUESTED',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\REQUESTED',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\random\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\random\\LICENSE.md',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\fft\\pocketfft\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\fft\\pocketfft\\LICENSE.md',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\splitmix64\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\splitmix64\\LICENSE.md',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\linalg\\lapack_lite\\LICENSE.txt',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\linalg\\lapack_lite\\LICENSE.txt',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\include\\numpy\\libdivide\\LICENSE.txt',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\include\\numpy\\libdivide\\LICENSE.txt',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\RECORD',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\RECORD',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\WHEEL',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\WHEEL',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\sfc64\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\sfc64\\LICENSE.md',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\philox\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\philox\\LICENSE.md',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\pcg64\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\pcg64\\LICENSE.md',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\ma\\LICENSE',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\ma\\LICENSE',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\umath\\svml\\LICENSE',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\umath\\svml\\LICENSE',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\entry_points.txt',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\entry_points.txt',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\mt19937\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\mt19937\\LICENSE.md',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\LICENSE.txt',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\LICENSE.txt',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\multiarray\\dragon4_LICENSE.txt',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\multiarray\\dragon4_LICENSE.txt',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\METADATA',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\METADATA',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\DELVEWHEEL',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\DELVEWHEEL',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\highway\\LICENSE',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\highway\\LICENSE',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\distributions\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\distributions\\LICENSE.md',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\npysort\\x86-simd-sort\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\npysort\\x86-simd-sort\\LICENSE.md',
|
||||
'DATA'),
|
||||
('base_library.zip',
|
||||
'd:\\JE-Skin-main\\devkit\\build\\je-skin-devkit-server\\base_library.zip',
|
||||
'DATA')],
|
||||
[],
|
||||
False,
|
||||
False,
|
||||
1779678963,
|
||||
[('run.exe',
|
||||
'C:\\Python314\\Lib\\site-packages\\PyInstaller\\bootloader\\Windows-64bit-intel\\run.exe',
|
||||
'EXECUTABLE')],
|
||||
'C:\\Python314\\python314.dll')
|
||||
@@ -1,337 +0,0 @@
|
||||
('d:\\JE-Skin-main\\devkit\\build\\je-skin-devkit-server\\je-skin-devkit-server.pkg',
|
||||
{'BINARY': True,
|
||||
'DATA': True,
|
||||
'EXECUTABLE': True,
|
||||
'EXTENSION': True,
|
||||
'PYMODULE': True,
|
||||
'PYSOURCE': True,
|
||||
'PYZ': False,
|
||||
'SPLASH': True,
|
||||
'SYMLINK': False},
|
||||
[('pyi-contents-directory _internal', '', 'OPTION'),
|
||||
('PYZ-00.pyz',
|
||||
'd:\\JE-Skin-main\\devkit\\build\\je-skin-devkit-server\\PYZ-00.pyz',
|
||||
'PYZ'),
|
||||
('struct',
|
||||
'd:\\JE-Skin-main\\devkit\\build\\je-skin-devkit-server\\localpycs\\struct.pyc',
|
||||
'PYMODULE'),
|
||||
('pyimod01_archive',
|
||||
'd:\\JE-Skin-main\\devkit\\build\\je-skin-devkit-server\\localpycs\\pyimod01_archive.pyc',
|
||||
'PYMODULE'),
|
||||
('pyimod02_importers',
|
||||
'd:\\JE-Skin-main\\devkit\\build\\je-skin-devkit-server\\localpycs\\pyimod02_importers.pyc',
|
||||
'PYMODULE'),
|
||||
('pyimod03_ctypes',
|
||||
'd:\\JE-Skin-main\\devkit\\build\\je-skin-devkit-server\\localpycs\\pyimod03_ctypes.pyc',
|
||||
'PYMODULE'),
|
||||
('pyimod04_pywin32',
|
||||
'd:\\JE-Skin-main\\devkit\\build\\je-skin-devkit-server\\localpycs\\pyimod04_pywin32.pyc',
|
||||
'PYMODULE'),
|
||||
('pyiboot01_bootstrap',
|
||||
'C:\\Python314\\Lib\\site-packages\\PyInstaller\\loader\\pyiboot01_bootstrap.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_inspect',
|
||||
'C:\\Python314\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_pkgutil',
|
||||
'C:\\Python314\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py',
|
||||
'PYSOURCE'),
|
||||
('pyi_rth_multiprocessing',
|
||||
'C:\\Python314\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_multiprocessing.py',
|
||||
'PYSOURCE'),
|
||||
('sensor_server', 'D:\\JE-Skin-main\\devkit\\sensor_server.py', 'PYSOURCE'),
|
||||
('python314.dll', 'C:\\Python314\\python314.dll', 'BINARY'),
|
||||
('numpy.libs\\msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy.libs\\msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll',
|
||||
'BINARY'),
|
||||
('numpy.libs\\libscipy_openblas64_-63c857e738469261263c764a36be9436.dll',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy.libs\\libscipy_openblas64_-63c857e738469261263c764a36be9436.dll',
|
||||
'BINARY'),
|
||||
('select.pyd', 'C:\\Python314\\DLLs\\select.pyd', 'EXTENSION'),
|
||||
('_multiprocessing.pyd',
|
||||
'C:\\Python314\\DLLs\\_multiprocessing.pyd',
|
||||
'EXTENSION'),
|
||||
('_zstd.pyd', 'C:\\Python314\\DLLs\\_zstd.pyd', 'EXTENSION'),
|
||||
('pyexpat.pyd', 'C:\\Python314\\DLLs\\pyexpat.pyd', 'EXTENSION'),
|
||||
('_lzma.pyd', 'C:\\Python314\\DLLs\\_lzma.pyd', 'EXTENSION'),
|
||||
('_bz2.pyd', 'C:\\Python314\\DLLs\\_bz2.pyd', 'EXTENSION'),
|
||||
('_ssl.pyd', 'C:\\Python314\\DLLs\\_ssl.pyd', 'EXTENSION'),
|
||||
('_hashlib.pyd', 'C:\\Python314\\DLLs\\_hashlib.pyd', 'EXTENSION'),
|
||||
('unicodedata.pyd', 'C:\\Python314\\DLLs\\unicodedata.pyd', 'EXTENSION'),
|
||||
('_decimal.pyd', 'C:\\Python314\\DLLs\\_decimal.pyd', 'EXTENSION'),
|
||||
('_socket.pyd', 'C:\\Python314\\DLLs\\_socket.pyd', 'EXTENSION'),
|
||||
('_ctypes.pyd', 'C:\\Python314\\DLLs\\_ctypes.pyd', 'EXTENSION'),
|
||||
('_queue.pyd', 'C:\\Python314\\DLLs\\_queue.pyd', 'EXTENSION'),
|
||||
('numpy\\_core\\_multiarray_tests.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\_core\\_multiarray_tests.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\_core\\_multiarray_umath.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\_core\\_multiarray_umath.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('_wmi.pyd', 'C:\\Python314\\DLLs\\_wmi.pyd', 'EXTENSION'),
|
||||
('_overlapped.pyd', 'C:\\Python314\\DLLs\\_overlapped.pyd', 'EXTENSION'),
|
||||
('_asyncio.pyd', 'C:\\Python314\\DLLs\\_asyncio.pyd', 'EXTENSION'),
|
||||
('numpy\\linalg\\_umath_linalg.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\linalg\\_umath_linalg.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\mtrand.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\mtrand.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\bit_generator.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\bit_generator.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\_sfc64.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\_sfc64.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\_philox.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\_philox.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\_pcg64.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\_pcg64.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\_mt19937.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\_mt19937.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\_generator.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\_generator.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\_common.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\_common.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\random\\_bounded_integers.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\random\\_bounded_integers.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('numpy\\fft\\_pocketfft_umath.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy\\fft\\_pocketfft_umath.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('_elementtree.pyd', 'C:\\Python314\\DLLs\\_elementtree.pyd', 'EXTENSION'),
|
||||
('grpc\\_cython\\cygrpc.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\grpc\\_cython\\cygrpc.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('google\\_upb\\_message.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\google\\_upb\\_message.pyd',
|
||||
'EXTENSION'),
|
||||
('grpc_tools\\_protoc_compiler.cp314-win_amd64.pyd',
|
||||
'C:\\Python314\\Lib\\site-packages\\grpc_tools\\_protoc_compiler.cp314-win_amd64.pyd',
|
||||
'EXTENSION'),
|
||||
('api-ms-win-crt-filesystem-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-filesystem-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-locale-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-locale-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-time-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-time-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-environment-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-environment-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-runtime-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-runtime-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-convert-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-convert-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-heap-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-heap-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-string-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-string-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-process-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-process-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-math-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-math-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-stdio-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-stdio-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-conio-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-conio-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('VCRUNTIME140.dll', 'C:\\Python314\\VCRUNTIME140.dll', 'BINARY'),
|
||||
('VCRUNTIME140_1.dll', 'C:\\Python314\\VCRUNTIME140_1.dll', 'BINARY'),
|
||||
('api-ms-win-crt-utility-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-utility-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-crt-private-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-crt-private-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('libcrypto-3.dll', 'C:\\Python314\\DLLs\\libcrypto-3.dll', 'BINARY'),
|
||||
('libssl-3.dll', 'C:\\Python314\\DLLs\\libssl-3.dll', 'BINARY'),
|
||||
('libffi-8.dll', 'C:\\Python314\\DLLs\\libffi-8.dll', 'BINARY'),
|
||||
('python3.dll', 'C:\\Python314\\python3.dll', 'BINARY'),
|
||||
('ucrtbase.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\ucrtbase.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-profile-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-profile-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-processthreads-l1-1-1.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-processthreads-l1-1-1.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-errorhandling-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-errorhandling-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-memory-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-memory-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-file-l2-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-file-l2-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-processthreads-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-processthreads-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-debug-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-debug-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-file-l1-2-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-file-l1-2-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-string-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-string-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-namedpipe-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-namedpipe-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-timezone-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-timezone-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-rtlsupport-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-rtlsupport-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-localization-l1-2-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-localization-l1-2-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-datetime-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-datetime-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-util-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-util-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-console-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-console-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-synch-l1-2-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-synch-l1-2-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-interlocked-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-interlocked-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-handle-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-handle-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-libraryloader-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-libraryloader-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-fibers-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-fibers-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-heap-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-heap-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-file-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-file-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-sysinfo-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-sysinfo-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-processenvironment-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-processenvironment-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('api-ms-win-core-synch-l1-1-0.dll',
|
||||
'C:\\Users\\Administrator\\miniconda3\\api-ms-win-core-synch-l1-1-0.dll',
|
||||
'BINARY'),
|
||||
('grpc\\_cython\\_credentials\\roots.pem',
|
||||
'C:\\Python314\\Lib\\site-packages\\grpc\\_cython\\_credentials\\roots.pem',
|
||||
'DATA'),
|
||||
('grpc\\_cython\\_cygrpc\\private_key_signing\\private_key_signer_py_wrapper.h',
|
||||
'C:\\Python314\\Lib\\site-packages\\grpc\\_cython\\_cygrpc\\private_key_signing\\private_key_signer_py_wrapper.h',
|
||||
'DATA'),
|
||||
('grpc\\_cython\\_cygrpc\\private_key_signing\\private_key_signer_py_wrapper.cc',
|
||||
'C:\\Python314\\Lib\\site-packages\\grpc\\_cython\\_cygrpc\\private_key_signing\\private_key_signer_py_wrapper.cc',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\common\\pythoncapi-compat\\COPYING',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\common\\pythoncapi-compat\\COPYING',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\INSTALLER',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\INSTALLER',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\REQUESTED',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\REQUESTED',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\random\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\random\\LICENSE.md',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\fft\\pocketfft\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\fft\\pocketfft\\LICENSE.md',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\splitmix64\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\splitmix64\\LICENSE.md',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\linalg\\lapack_lite\\LICENSE.txt',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\linalg\\lapack_lite\\LICENSE.txt',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\include\\numpy\\libdivide\\LICENSE.txt',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\include\\numpy\\libdivide\\LICENSE.txt',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\RECORD',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\RECORD',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\WHEEL',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\WHEEL',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\sfc64\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\sfc64\\LICENSE.md',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\philox\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\philox\\LICENSE.md',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\pcg64\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\pcg64\\LICENSE.md',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\ma\\LICENSE',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\ma\\LICENSE',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\umath\\svml\\LICENSE',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\umath\\svml\\LICENSE',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\entry_points.txt',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\entry_points.txt',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\mt19937\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\mt19937\\LICENSE.md',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\LICENSE.txt',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\LICENSE.txt',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\multiarray\\dragon4_LICENSE.txt',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\multiarray\\dragon4_LICENSE.txt',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\METADATA',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\METADATA',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\DELVEWHEEL',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\DELVEWHEEL',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\highway\\LICENSE',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\highway\\LICENSE',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\distributions\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\random\\src\\distributions\\LICENSE.md',
|
||||
'DATA'),
|
||||
('numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\npysort\\x86-simd-sort\\LICENSE.md',
|
||||
'C:\\Python314\\Lib\\site-packages\\numpy-2.4.4.dist-info\\licenses\\numpy\\_core\\src\\npysort\\x86-simd-sort\\LICENSE.md',
|
||||
'DATA'),
|
||||
('base_library.zip',
|
||||
'd:\\JE-Skin-main\\devkit\\build\\je-skin-devkit-server\\base_library.zip',
|
||||
'DATA')],
|
||||
'python314.dll',
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
[],
|
||||
None,
|
||||
None,
|
||||
None)
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,245 +0,0 @@
|
||||
|
||||
This file lists modules PyInstaller was not able to find. This does not
|
||||
necessarily mean these modules are required for running your program. Both
|
||||
Python's standard library and 3rd-party Python packages often conditionally
|
||||
import optional modules, some of which may be available only on certain
|
||||
platforms.
|
||||
|
||||
Types of import:
|
||||
* top-level: imported at the top-level - look at these first
|
||||
* conditional: imported within an if-statement
|
||||
* delayed: imported within a function
|
||||
* optional: imported within a try-except-statement
|
||||
|
||||
IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
|
||||
tracking down the missing module yourself. Thanks!
|
||||
|
||||
missing module named pwd - imported by posixpath (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (optional), netrc (delayed, optional), subprocess (delayed, conditional, optional), http.server (delayed, optional)
|
||||
missing module named grp - imported by shutil (delayed, optional), tarfile (optional), pathlib (optional), subprocess (delayed, conditional, optional)
|
||||
missing module named 'collections.abc' - imported by _colorize (top-level), typing (top-level), traceback (top-level), logging (top-level), selectors (top-level), http.client (top-level), importlib.resources.readers (top-level), inspect (top-level), tracemalloc (top-level), multiprocessing.managers (top-level), typing_extensions (top-level), asyncio.base_events (top-level), asyncio.coroutines (top-level), grpc.aio._metadata (top-level), google.protobuf.internal.containers (top-level), google.protobuf.internal.well_known_types (top-level), numpy._typing._array_like (top-level), numpy._typing._nested_sequence (conditional), numpy._typing._shape (top-level), numpy._typing._dtype_like (top-level), numpy.lib._function_base_impl (top-level), _pyrepl.types (top-level), numpy.lib._npyio_impl (top-level), numpy.random._common (top-level), numpy.random._generator (top-level), numpy.random.bit_generator (top-level), numpy.random.mtrand (top-level), numpy.polynomial._polybase (top-level), xml.etree.ElementTree (top-level)
|
||||
missing module named _posixsubprocess - imported by subprocess (conditional), multiprocessing.util (delayed)
|
||||
missing module named fcntl - imported by pathlib._os (optional), subprocess (optional)
|
||||
missing module named _posixshmem - imported by multiprocessing.resource_tracker (conditional), multiprocessing.shared_memory (conditional)
|
||||
missing module named _scproxy - imported by urllib.request (conditional)
|
||||
missing module named posix - imported by posixpath (optional), shutil (conditional), importlib._bootstrap_external (conditional), pathlib._os (optional), os (conditional, optional), _pyrepl.trace (conditional)
|
||||
missing module named resource - imported by posix (top-level)
|
||||
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional), zipimport (top-level)
|
||||
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional), zipimport (top-level)
|
||||
missing module named multiprocessing.BufferTooShort - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
|
||||
missing module named multiprocessing.AuthenticationError - imported by multiprocessing (top-level), multiprocessing.forkserver (top-level), multiprocessing.connection (top-level)
|
||||
missing module named multiprocessing.get_context - imported by multiprocessing (top-level), multiprocessing.pool (top-level), multiprocessing.managers (top-level), multiprocessing.sharedctypes (top-level)
|
||||
missing module named multiprocessing.TimeoutError - imported by multiprocessing (top-level), multiprocessing.pool (top-level)
|
||||
missing module named multiprocessing.set_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
|
||||
missing module named multiprocessing.get_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
|
||||
missing module named pyimod02_importers - imported by C:\Python314\Lib\site-packages\PyInstaller\hooks\rthooks\pyi_rth_pkgutil.py (delayed)
|
||||
missing module named _dummy_thread - imported by numpy._core.arrayprint (optional)
|
||||
missing module named 'numpy_distutils.cpuinfo' - imported by numpy.f2py.diagnose (delayed, conditional, optional)
|
||||
missing module named 'numpy_distutils.fcompiler' - imported by numpy.f2py.diagnose (delayed, conditional, optional)
|
||||
missing module named 'numpy_distutils.command' - imported by numpy.f2py.diagnose (delayed, conditional, optional)
|
||||
missing module named numpy_distutils - imported by numpy.f2py.diagnose (delayed, optional)
|
||||
missing module named charset_normalizer - imported by numpy.f2py.crackfortran (optional)
|
||||
missing module named vms_lib - imported by platform (delayed, optional)
|
||||
missing module named 'java.lang' - imported by platform (delayed, optional)
|
||||
missing module named java - imported by platform (delayed)
|
||||
missing module named psutil - imported by numpy.testing._private.utils (delayed, optional)
|
||||
missing module named termios - imported by tty (top-level), _pyrepl.pager (delayed, optional)
|
||||
missing module named readline - imported by cmd (delayed, conditional, optional), code (delayed, conditional, optional), pdb (delayed, conditional, optional), rlcompleter (optional)
|
||||
missing module named win32pdh - imported by numpy.testing._private.utils (delayed, conditional)
|
||||
missing module named _typeshed - imported by numpy.random.bit_generator (top-level)
|
||||
missing module named numpy.random.RandomState - imported by numpy.random (top-level), numpy.random._generator (top-level)
|
||||
missing module named pyodide_js - imported by threadpoolctl (delayed, optional)
|
||||
missing module named numpy._core.zeros - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.vstack - imported by numpy._core (top-level), numpy.lib._shape_base_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.void - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.vecmat - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.vecdot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.ushort - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.unsignedinteger - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ulonglong - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ulong - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.uintp - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.uintc - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.uint64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.uint32 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.uint16 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.uint - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ubyte - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.trunc - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.true_divide - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.transpose - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._function_base_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.trace - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.timedelta64 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.tensordot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.tanh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.tan - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.swapaxes - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.sum - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.subtract - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.str_ - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.square - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.sqrt - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level)
|
||||
missing module named numpy._core.spacing - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.sort - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.sinh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.single - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.signedinteger - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.signbit - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.sign - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.short - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.rint - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.right_shift - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.result_type - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional), numpy.fft._pocketfft (top-level)
|
||||
missing module named numpy._core.remainder - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.reciprocal - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level)
|
||||
missing module named numpy._core.radians - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.rad2deg - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.prod - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.power - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.positive - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.pi - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.outer - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.ones - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.object_ - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.number - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.not_equal - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.nextafter - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.newaxis - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.negative - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ndarray - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy.lib._utils_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.multiply - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.moveaxis - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.modf - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.mod - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.minimum - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.maximum - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.max - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.matvec - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.matrix_transpose - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.matmul - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.longlong - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.longdouble - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.long - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logical_xor - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logical_or - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logical_not - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logical_and - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logaddexp2 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.logaddexp - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.log10 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.log2 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.log1p - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.log - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.linspace - imported by numpy._core (top-level), numpy.lib._index_tricks_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.less_equal - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.less - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.left_shift - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ldexp - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.lcm - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.isscalar - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.isnat - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy (conditional)
|
||||
missing module named numpy._core.isnan - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.isfinite - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.intp - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (top-level), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.integer - imported by numpy._core (conditional), numpy (conditional), numpy.fft._helper (top-level)
|
||||
missing module named numpy._core.intc - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.int64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.int32 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.int16 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.int8 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.inf - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.inexact - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.iinfo - imported by numpy._core (top-level), numpy.lib._twodim_base_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.hypot - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.hstack - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.heaviside - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.half - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.greater_equal - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.greater - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.gcd - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.frompyfunc - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.frexp - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.fmod - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.fmin - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.fmax - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.floor_divide - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.floor - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.floating - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.float_power - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.float32 - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.float16 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.finfo - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.fabs - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.expm1 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.exp2 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.exp - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.euler_gamma - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.errstate - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.equal - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.empty_like - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level)
|
||||
missing module named numpy._core.empty - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (top-level), numpy (conditional), numpy.fft._helper (top-level)
|
||||
missing module named numpy._core.e - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.double - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.dot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.divmod - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.divide - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.diagonal - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.degrees - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.deg2rad - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.datetime64 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.csingle - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.cross - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.count_nonzero - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.cosh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.cos - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.copysign - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.conjugate - imported by numpy._core (conditional), numpy (conditional), numpy.fft._pocketfft (top-level)
|
||||
missing module named numpy._core.conj - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.complexfloating - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.complex64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
|
||||
missing module named numpy._core.clongdouble - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.character - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.ceil - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.cdouble - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.cbrt - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bytes_ - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.byte - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bool_ - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bitwise_xor - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bitwise_or - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bitwise_count - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.bitwise_and - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.atleast_3d - imported by numpy._core (top-level), numpy.lib._shape_base_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.atleast_2d - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.atleast_1d - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.asarray - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._array_utils_impl (top-level), numpy (conditional), numpy.fft._helper (top-level), numpy.fft._pocketfft (top-level)
|
||||
missing module named numpy._core.asanyarray - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.array_repr - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy (conditional)
|
||||
missing module named numpy._core.array2string - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.array - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
|
||||
missing module named numpy._core.argsort - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.arctanh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arctan2 - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arctan - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arcsinh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arcsin - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arccosh - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arccos - imported by numpy._core (conditional), numpy (conditional)
|
||||
missing module named numpy._core.arange - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy (conditional), numpy.fft._helper (top-level)
|
||||
missing module named numpy._core.amin - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.amax - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named numpy._core.all - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
|
||||
missing module named numpy._core.add - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
|
||||
missing module named yaml - imported by numpy.__config__ (delayed)
|
||||
missing module named numpy._distributor_init_local - imported by numpy (optional), numpy._distributor_init (optional)
|
||||
missing module named defusedxml - imported by openpyxl.xml (delayed, optional)
|
||||
missing module named lxml - imported by openpyxl.xml (delayed, optional)
|
||||
missing module named 'defusedxml.ElementTree' - imported by openpyxl.xml.functions (conditional)
|
||||
missing module named 'lxml.etree' - imported by openpyxl.xml.functions (conditional)
|
||||
missing module named PIL - imported by openpyxl.drawing.image (optional)
|
||||
missing module named openpyxl.tests - imported by openpyxl.reader.excel (optional)
|
||||
missing module named google.protobuf.pyext._message - imported by google.protobuf.pyext (conditional, optional), google.protobuf.internal.api_implementation (conditional, optional), google.protobuf.descriptor (conditional), google.protobuf.pyext.cpp_message (conditional)
|
||||
missing module named google.protobuf.enable_deterministic_proto_serialization - imported by google.protobuf (optional), google.protobuf.internal.api_implementation (optional)
|
||||
missing module named google.protobuf.internal._api_implementation - imported by google.protobuf.internal (optional), google.protobuf.internal.api_implementation (optional)
|
||||
missing module named grpc_reflection - imported by grpc (optional)
|
||||
missing module named grpc_health - imported by grpc (optional)
|
||||
missing module named pkg_resources - imported by grpc_tools.protoc (conditional)
|
||||
File diff suppressed because it is too large
Load Diff
BIN
devkit/dist/je-skin-devkit-server.exe
vendored
BIN
devkit/dist/je-skin-devkit-server.exe
vendored
Binary file not shown.
Binary file not shown.
@@ -5,8 +5,8 @@ a = Analysis(
|
||||
['sensor_server.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
datas=[('sensor_stream_pb2.py', '.'), ('sensor_stream_pb2_grpc.py', '.')],
|
||||
hiddenimports=['grpc', 'openpyxl', 'numpy'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
|
||||
@@ -26,6 +26,7 @@ import sys
|
||||
import time
|
||||
from concurrent import futures
|
||||
from pathlib import Path
|
||||
|
||||
import grpc
|
||||
import sensor_stream_pb2
|
||||
import sensor_stream_pb2_grpc
|
||||
@@ -230,56 +231,30 @@ def _append_analysis_log(source_csv: str, stats: dict):
|
||||
class SensorPushServicer(sensor_stream_pb2_grpc.SensorPushServicer):
|
||||
"""接收实时传感器帧(streaming)"""
|
||||
|
||||
_csv_path = None # 类变量,记录当前 CSV 路径
|
||||
|
||||
def __init__(self):
|
||||
self.frame_count = 0
|
||||
self.last_report_time = time.time()
|
||||
self.last_angle = None
|
||||
self._csv_file = None
|
||||
self._csv_writer = None
|
||||
|
||||
def _open_csv(self):
|
||||
"""打开一个新的 CSV 文件用于持续写入"""
|
||||
ts = time.strftime("%Y%m%d_%H%M%S")
|
||||
SensorPushServicer._csv_path = os.path.join(os.getcwd(), f"sensor_log_{ts}.csv")
|
||||
self._csv_file = open(SensorPushServicer._csv_path, "w", newline="", encoding="utf-8-sig")
|
||||
self._csv_writer = csv.writer(self._csv_file)
|
||||
header = ["seq", "timestamp_ms", "dts_ms", "angle", "magnitude", "state", "cop_x", "cop_y", "base_x", "base_y", "resultant_force"] + [f"ch{i}" for i in range(SENSOR_ROWS * SENSOR_COLS)]
|
||||
self._csv_writer.writerow(header)
|
||||
self._csv_file.flush()
|
||||
print(f"[SensorPush] CSV logging to: {SensorPushServicer._csv_path}")
|
||||
|
||||
def _close_csv(self):
|
||||
"""关闭 CSV 文件"""
|
||||
if self._csv_file:
|
||||
self._csv_file.close()
|
||||
print(f"[SensorPush] CSV saved: {SensorPushServicer._csv_path}")
|
||||
self._csv_file = None
|
||||
self._csv_writer = None
|
||||
|
||||
def Upload(self, request_iterator, context):
|
||||
print("[SensorPush] Client connected, waiting for frames...")
|
||||
reset_baseline()
|
||||
self.last_angle = None
|
||||
self.frame_count = 0
|
||||
self._open_csv()
|
||||
|
||||
for frame in request_iterator:
|
||||
self.frame_count += 1
|
||||
angle = 0.0
|
||||
magnitude = 0.0
|
||||
state = 0
|
||||
ok = True
|
||||
message = "OK"
|
||||
cop_x = cop_y = base_x = base_y = 0.0
|
||||
total_press = 0.0
|
||||
threshold = 0.0
|
||||
if len(frame.matrix) == SENSOR_ROWS * SENSOR_COLS:
|
||||
try:
|
||||
angle, magnitude, state, cop_x, cop_y, base_x, base_y, total_press, threshold = get_pzt_angle(frame.matrix)
|
||||
angle = get_pzt_angle(frame.matrix)
|
||||
self.last_angle = angle
|
||||
print(f"devkit: angle={angle:.2f}, magnitude={magnitude:.4f}, state={state}, cop_x={cop_x:.4f}, cop_y={cop_y:.4f}, base_x={base_x:.4f}, base_y={base_y:.4f}, total_press={total_press:.2f}, thresh={threshold:.2f}")
|
||||
if self.frame_count <= 10 or self.frame_count % 30 == 0:
|
||||
print(
|
||||
f"[SensorPush] PZT angle frame #{frame.seq} "
|
||||
f"dts={frame.dts_ms} angle={angle:.2f}"
|
||||
)
|
||||
except Exception as e:
|
||||
ok = False
|
||||
message = str(e)
|
||||
@@ -287,18 +262,6 @@ class SensorPushServicer(sensor_stream_pb2_grpc.SensorPushServicer):
|
||||
else:
|
||||
ok = False
|
||||
message = f"Invalid matrix length: {len(frame.matrix)}"
|
||||
print(f"[Recv #{frame.seq}] INVALID len={len(frame.matrix)}")
|
||||
|
||||
# 持续写入 CSV
|
||||
if self._csv_writer:
|
||||
row = [frame.seq, frame.timestamp_ms, frame.dts_ms,
|
||||
f"{angle:.4f}", f"{magnitude:.4f}", state,
|
||||
f"{cop_x:.4f}", f"{cop_y:.4f}", f"{base_x:.4f}", f"{base_y:.4f}",
|
||||
frame.resultant_force]
|
||||
row += list(frame.matrix)
|
||||
self._csv_writer.writerow(row)
|
||||
if self.frame_count % 10 == 0:
|
||||
self._csv_file.flush()
|
||||
|
||||
yield sensor_stream_pb2.PztAngleResponse(
|
||||
seq=frame.seq,
|
||||
@@ -307,14 +270,6 @@ class SensorPushServicer(sensor_stream_pb2_grpc.SensorPushServicer):
|
||||
dts_ms=frame.dts_ms,
|
||||
ok=ok,
|
||||
message=message,
|
||||
magnitude=magnitude,
|
||||
state=state,
|
||||
cop_x=cop_x,
|
||||
cop_y=cop_y,
|
||||
base_x=base_x,
|
||||
base_y=base_y,
|
||||
total_press=total_press,
|
||||
threshold=threshold,
|
||||
)
|
||||
|
||||
if self.frame_count % 100 == 0:
|
||||
@@ -335,7 +290,6 @@ class SensorPushServicer(sensor_stream_pb2_grpc.SensorPushServicer):
|
||||
f"total={self.frame_count} | ~{fps:.1f} fps"
|
||||
)
|
||||
|
||||
self._close_csv()
|
||||
print(f"[SensorPush] Stream ended. Total: {self.frame_count}")
|
||||
|
||||
|
||||
@@ -396,84 +350,67 @@ def serve(port: int):
|
||||
|
||||
|
||||
import numpy as np
|
||||
from collections import deque
|
||||
import threading
|
||||
|
||||
# ===================== 算法参数=====================
|
||||
COP_INIT_MEDIAN_FRAMES = 1 # 初始COP取中位数的帧数
|
||||
NOISE_COLLECT_FRAMES = 10 # 动态阈值基线采集帧数
|
||||
THRESH_K = 5 # 阈值 = K * mean
|
||||
SENSOR_ROWS = 12
|
||||
SENSOR_COLS = 7
|
||||
|
||||
# ===================== 二次静置精修参数 =====================
|
||||
POST_INIT_WINDOW_CNT = 60000
|
||||
POST_INIT_STABLE_CNT = 100
|
||||
POST_INIT_STABLE_THRESH = 0.1
|
||||
TOTAL_PRESSURE_LOW_THRESHOLD = 500
|
||||
COP_STABILITY_FRAMES_REQUIRED = 5
|
||||
SENSOR_ROWS = 12
|
||||
SENSOR_COLS = 7
|
||||
|
||||
# ===================== 线程安全全局状态 =====================
|
||||
first_contact_CoP_x = None
|
||||
first_contact_CoP_y = None
|
||||
contact_initialized = False
|
||||
first_frame = None
|
||||
first_frame_lock = threading.Lock()
|
||||
|
||||
# 候选初始CoP缓冲
|
||||
cop_init_x_buf = deque(maxlen=COP_INIT_MEDIAN_FRAMES)
|
||||
cop_init_y_buf = deque(maxlen=COP_INIT_MEDIAN_FRAMES)
|
||||
first_contact_CoP_x = None
|
||||
first_contact_CoP_y = None
|
||||
contact_initialized = False
|
||||
|
||||
# 动态阈值
|
||||
noise_sum_buf = deque(maxlen=NOISE_COLLECT_FRAMES)
|
||||
dynamic_thresh = None
|
||||
|
||||
# 二次静置精修状态
|
||||
post_init_frame_cnt = 0
|
||||
post_stable_cnt = 0
|
||||
post_refined_flag = False
|
||||
post_cand_x = None
|
||||
post_cand_y = None
|
||||
total_pressure_low_counter = 0
|
||||
|
||||
# ===================== 基线减除 =====================
|
||||
def subtract_baseline(current_frame):
|
||||
global first_frame
|
||||
current_frame = np.array(current_frame, dtype=np.float32).flatten()
|
||||
|
||||
with first_frame_lock:
|
||||
if first_frame is None:
|
||||
first_frame = current_frame.copy()
|
||||
|
||||
diff = current_frame - first_frame
|
||||
return np.clip(diff, 0, None)
|
||||
|
||||
# ===================== 重置CoP状态 =====================
|
||||
def reset_cop_state():
|
||||
global first_contact_CoP_x, first_contact_CoP_y, contact_initialized
|
||||
global post_init_frame_cnt, post_stable_cnt, post_refined_flag
|
||||
global post_cand_x, post_cand_y
|
||||
global total_pressure_low_counter
|
||||
|
||||
first_contact_CoP_x = None
|
||||
first_contact_CoP_y = None
|
||||
contact_initialized = False
|
||||
cop_init_x_buf.clear()
|
||||
cop_init_y_buf.clear()
|
||||
post_init_frame_cnt = 0
|
||||
post_stable_cnt = 0
|
||||
post_refined_flag = False
|
||||
post_cand_x = None
|
||||
post_cand_y = None
|
||||
|
||||
total_pressure_low_counter = 0
|
||||
|
||||
# ===================== CoP压力中心计算 =====================
|
||||
def compute_pressure_direction(raw_frame):
|
||||
def compute_pressure_direction(baseline_subtracted_frame):
|
||||
global first_contact_CoP_x, first_contact_CoP_y, contact_initialized
|
||||
global post_init_frame_cnt, post_stable_cnt, post_refined_flag
|
||||
global post_cand_x, post_cand_y
|
||||
global noise_sum_buf, dynamic_thresh
|
||||
global total_pressure_low_counter
|
||||
|
||||
rows, cols = SENSOR_ROWS, SENSOR_COLS
|
||||
frame_flat = np.asarray(raw_frame, dtype=np.float32).flatten()
|
||||
frame_flat = np.asarray(baseline_subtracted_frame, dtype=np.float32).flatten()
|
||||
frame2d = frame_flat.reshape(rows, cols)
|
||||
|
||||
total_pressure = np.sum(frame2d)
|
||||
if total_pressure < TOTAL_PRESSURE_LOW_THRESHOLD:
|
||||
total_pressure_low_counter += 1
|
||||
else:
|
||||
total_pressure_low_counter = 0
|
||||
|
||||
# 动态阈值
|
||||
if dynamic_thresh is None:
|
||||
noise_sum_buf.append(total_pressure)
|
||||
if len(noise_sum_buf) >= NOISE_COLLECT_FRAMES:
|
||||
sums = np.array(noise_sum_buf)
|
||||
dynamic_thresh = THRESH_K * float(np.mean(sums))
|
||||
if total_pressure_low_counter >= COP_STABILITY_FRAMES_REQUIRED:
|
||||
reset_cop_state()
|
||||
return 0.0, 0.0
|
||||
|
||||
# 低压重置
|
||||
if total_pressure == 0 or (dynamic_thresh is not None and total_pressure < dynamic_thresh):
|
||||
if contact_initialized and dynamic_thresh is not None:
|
||||
reset_cop_state()
|
||||
return 0.0, 0.0, 0, rows-1, 0, cols-1, 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, dynamic_thresh
|
||||
if total_pressure == 0:
|
||||
return 0.0, 0.0
|
||||
|
||||
x_grid = np.tile(np.arange(cols), (rows, 1))
|
||||
y_grid = np.repeat(np.arange(rows), cols).reshape(rows, cols)
|
||||
@@ -482,98 +419,44 @@ def compute_pressure_direction(raw_frame):
|
||||
|
||||
delta_CoP_x = 0.0
|
||||
delta_CoP_y = 0.0
|
||||
base_x = cop_x
|
||||
base_y = cop_y
|
||||
|
||||
# ============ 初始点稳定判断(中位数判定) ============
|
||||
if not contact_initialized:
|
||||
cop_init_x_buf.append(cop_x)
|
||||
cop_init_y_buf.append(cop_y)
|
||||
|
||||
if len(cop_init_x_buf) >= COP_INIT_MEDIAN_FRAMES:
|
||||
first_contact_CoP_x = float(np.median(cop_init_x_buf))
|
||||
first_contact_CoP_y = float(np.median(cop_init_y_buf))
|
||||
contact_initialized = True
|
||||
cop_init_x_buf.clear()
|
||||
cop_init_y_buf.clear()
|
||||
|
||||
# ========== 计算偏移量 ==========
|
||||
first_contact_CoP_x = cop_x
|
||||
first_contact_CoP_y = cop_y
|
||||
contact_initialized = True
|
||||
else:
|
||||
# 二次静置精修
|
||||
post_init_frame_cnt += 1
|
||||
if not post_refined_flag and post_init_frame_cnt <= POST_INIT_WINDOW_CNT:
|
||||
if post_cand_x is not None:
|
||||
dist_val = np.hypot(cop_x - post_cand_x, cop_y - post_cand_y)
|
||||
if dist_val <= POST_INIT_STABLE_THRESH:
|
||||
post_stable_cnt += 1
|
||||
else:
|
||||
post_cand_x = cop_x
|
||||
post_cand_y = cop_y
|
||||
post_stable_cnt = 1
|
||||
else:
|
||||
post_cand_x = cop_x
|
||||
post_cand_y = cop_y
|
||||
post_stable_cnt = 1
|
||||
|
||||
if post_stable_cnt >= POST_INIT_STABLE_CNT:
|
||||
first_contact_CoP_x = post_cand_x
|
||||
first_contact_CoP_y = post_cand_y
|
||||
post_refined_flag = True
|
||||
else:
|
||||
post_refined_flag = True
|
||||
|
||||
delta_CoP_x = cop_x - first_contact_CoP_x
|
||||
delta_CoP_y = first_contact_CoP_y - cop_y
|
||||
base_x = first_contact_CoP_x
|
||||
base_y = first_contact_CoP_y
|
||||
|
||||
magnitude = np.hypot(delta_CoP_x, delta_CoP_y)
|
||||
if not contact_initialized:
|
||||
state = 0
|
||||
elif not post_refined_flag:
|
||||
state = 1
|
||||
else:
|
||||
state = 2
|
||||
|
||||
return (cop_x, cop_y,
|
||||
0, rows-1, 0, cols-1,
|
||||
delta_CoP_x, delta_CoP_y,
|
||||
base_x, base_y,
|
||||
magnitude, state,
|
||||
total_pressure, dynamic_thresh)
|
||||
delta_CoP_y = cop_y - first_contact_CoP_y
|
||||
|
||||
return delta_CoP_x, delta_CoP_y
|
||||
|
||||
# ===================== 角度计算核心 =====================
|
||||
def compute_vector_angle(x: float, y: float) -> tuple[float, float]:
|
||||
epsilon = 1e-8
|
||||
mag = np.hypot(x, y)
|
||||
angle = np.degrees(np.arctan2(y, x + epsilon))
|
||||
mag = np.hypot(x, y)
|
||||
angle = np.degrees(np.arctan2(y, x + epsilon))
|
||||
if angle < 0:
|
||||
angle += 360
|
||||
return angle, mag
|
||||
|
||||
def compute_PZT_angle(Px: float, Py: float) -> tuple[float, float]:
|
||||
return compute_vector_angle(Px, Py)
|
||||
|
||||
return compute_vector_angle(Px, -Py)
|
||||
|
||||
# ===================== 核心入口函数 =====================
|
||||
def get_pzt_angle(adc_data):
|
||||
if len(adc_data) != 84:
|
||||
raise ValueError("ADC数据长度必须为84")
|
||||
result = compute_pressure_direction(adc_data)
|
||||
cop_x, cop_y = result[0], result[1]
|
||||
dx, dy = result[6], result[7]
|
||||
base_x, base_y = result[8], result[9]
|
||||
magnitude = result[10]
|
||||
state = int(result[11])
|
||||
total_press = result[12]
|
||||
threshold = result[13]
|
||||
baseline_subtracted = subtract_baseline(adc_data)
|
||||
dx, dy = compute_pressure_direction(baseline_subtracted)
|
||||
pzt_angle, _ = compute_PZT_angle(dx, dy)
|
||||
return pzt_angle, magnitude, state, cop_x, cop_y, base_x, base_y, total_press, threshold
|
||||
|
||||
|
||||
return pzt_angle
|
||||
|
||||
# ===================== 重置基线(校准用) =====================
|
||||
def reset_baseline():
|
||||
global first_frame
|
||||
with first_frame_lock:
|
||||
first_frame = None
|
||||
reset_cop_state()
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13sensor_stream.proto\x12\rsensor_stream\"\x85\x01\n\x0bSensorFrame\x12\x0b\n\x03seq\x18\x01 \x01(\x04\x12\x14\n\x0ctimestamp_ms\x18\x02 \x01(\x04\x12\x0c\n\x04rows\x18\x03 \x01(\r\x12\x0c\n\x04\x63ols\x18\x04 \x01(\r\x12\x0e\n\x06matrix\x18\x05 \x03(\r\x12\x17\n\x0fresultant_force\x18\x06 \x01(\x01\x12\x0e\n\x06\x64ts_ms\x18\x07 \x01(\r\"\xd1\x01\n\x10PztAngleResponse\x12\x0b\n\x03seq\x18\x01 \x01(\x04\x12\x14\n\x0ctimestamp_ms\x18\x02 \x01(\x04\x12\r\n\x05\x61ngle\x18\x03 \x01(\x02\x12\x0e\n\x06\x64ts_ms\x18\x04 \x01(\r\x12\n\n\x02ok\x18\x05 \x01(\x08\x12\x0f\n\x07message\x18\x06 \x01(\t\x12\x11\n\tmagnitude\x18\x07 \x01(\x02\x12\r\n\x05state\x18\x08 \x01(\r\x12\r\n\x05\x63op_x\x18\t \x01(\x02\x12\r\n\x05\x63op_y\x18\n \x01(\x02\x12\x0e\n\x06\x62\x61se_x\x18\x0b \x01(\x02\x12\x0e\n\x06\x62\x61se_y\x18\x0c \x01(\x02\"8\n\x0eProcessRequest\x12\x10\n\x08\x63sv_path\x18\x01 \x01(\t\x12\x14\n\x0csave_as_xlsx\x18\x02 \x01(\x08\"\xa6\x01\n\x0fProcessResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x13\n\x0boutput_path\x18\x02 \x01(\t\x12\x13\n\x0bgroups_used\x18\x03 \x01(\r\x12\x12\n\nmean_value\x18\x04 \x01(\x01\x12\x11\n\tthreshold\x18\x05 \x01(\x01\x12\x12\n\nrows_total\x18\x06 \x01(\r\x12\x11\n\trows_kept\x18\x07 \x01(\r\x12\x0f\n\x07message\x18\x08 \x01(\t2W\n\nSensorPush\x12I\n\x06Upload\x12\x1a.sensor_stream.SensorFrame\x1a\x1f.sensor_stream.PztAngleResponse(\x01\x30\x01\x32_\n\x0f\x45xportProcessor\x12L\n\x0bProcessFile\x12\x1d.sensor_stream.ProcessRequest\x1a\x1e.sensor_stream.ProcessResponseb\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13sensor_stream.proto\x12\rsensor_stream\"\x85\x01\n\x0bSensorFrame\x12\x0b\n\x03seq\x18\x01 \x01(\x04\x12\x14\n\x0ctimestamp_ms\x18\x02 \x01(\x04\x12\x0c\n\x04rows\x18\x03 \x01(\r\x12\x0c\n\x04\x63ols\x18\x04 \x01(\r\x12\x0e\n\x06matrix\x18\x05 \x03(\r\x12\x17\n\x0fresultant_force\x18\x06 \x01(\x01\x12\x0e\n\x06\x64ts_ms\x18\x07 \x01(\r\"q\n\x10PztAngleResponse\x12\x0b\n\x03seq\x18\x01 \x01(\x04\x12\x14\n\x0ctimestamp_ms\x18\x02 \x01(\x04\x12\r\n\x05\x61ngle\x18\x03 \x01(\x02\x12\x0e\n\x06\x64ts_ms\x18\x04 \x01(\r\x12\n\n\x02ok\x18\x05 \x01(\x08\x12\x0f\n\x07message\x18\x06 \x01(\t\"8\n\x0eProcessRequest\x12\x10\n\x08\x63sv_path\x18\x01 \x01(\t\x12\x14\n\x0csave_as_xlsx\x18\x02 \x01(\x08\"\xa6\x01\n\x0fProcessResponse\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\x13\n\x0boutput_path\x18\x02 \x01(\t\x12\x13\n\x0bgroups_used\x18\x03 \x01(\r\x12\x12\n\nmean_value\x18\x04 \x01(\x01\x12\x11\n\tthreshold\x18\x05 \x01(\x01\x12\x12\n\nrows_total\x18\x06 \x01(\r\x12\x11\n\trows_kept\x18\x07 \x01(\r\x12\x0f\n\x07message\x18\x08 \x01(\t2W\n\nSensorPush\x12I\n\x06Upload\x12\x1a.sensor_stream.SensorFrame\x1a\x1f.sensor_stream.PztAngleResponse(\x01\x30\x01\x32_\n\x0f\x45xportProcessor\x12L\n\x0bProcessFile\x12\x1d.sensor_stream.ProcessRequest\x1a\x1e.sensor_stream.ProcessResponseb\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
@@ -33,14 +33,14 @@ if not _descriptor._USE_C_DESCRIPTORS:
|
||||
DESCRIPTOR._loaded_options = None
|
||||
_globals['_SENSORFRAME']._serialized_start=39
|
||||
_globals['_SENSORFRAME']._serialized_end=172
|
||||
_globals['_PZTANGLERESPONSE']._serialized_start=175
|
||||
_globals['_PZTANGLERESPONSE']._serialized_end=384
|
||||
_globals['_PROCESSREQUEST']._serialized_start=386
|
||||
_globals['_PROCESSREQUEST']._serialized_end=442
|
||||
_globals['_PROCESSRESPONSE']._serialized_start=445
|
||||
_globals['_PROCESSRESPONSE']._serialized_end=611
|
||||
_globals['_SENSORPUSH']._serialized_start=613
|
||||
_globals['_SENSORPUSH']._serialized_end=700
|
||||
_globals['_EXPORTPROCESSOR']._serialized_start=702
|
||||
_globals['_EXPORTPROCESSOR']._serialized_end=797
|
||||
_globals['_PZTANGLERESPONSE']._serialized_start=174
|
||||
_globals['_PZTANGLERESPONSE']._serialized_end=287
|
||||
_globals['_PROCESSREQUEST']._serialized_start=289
|
||||
_globals['_PROCESSREQUEST']._serialized_end=345
|
||||
_globals['_PROCESSRESPONSE']._serialized_start=348
|
||||
_globals['_PROCESSRESPONSE']._serialized_end=514
|
||||
_globals['_SENSORPUSH']._serialized_start=516
|
||||
_globals['_SENSORPUSH']._serialized_end=603
|
||||
_globals['_EXPORTPROCESSOR']._serialized_start=605
|
||||
_globals['_EXPORTPROCESSOR']._serialized_end=700
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
"""
|
||||
独立测试脚本:读取84个原始ADC数据,传入CoP算法计算角度,终端打印结果。
|
||||
|
||||
用法:
|
||||
python test_pzt.py # 从 stdin 逐行读取(每行84个逗号分隔数值)
|
||||
python test_pzt.py data.csv # 从 CSV 文件逐行读取
|
||||
python test_pzt.py --random # 生成随机测试数据(调试用)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import csv
|
||||
import numpy as np
|
||||
|
||||
# ── 从 sensor_server.py 导入算法 ──
|
||||
sys.path.insert(0, ".")
|
||||
from sensor_server import (
|
||||
get_pzt_angle,
|
||||
reset_baseline,
|
||||
subtract_baseline,
|
||||
compute_pressure_direction,
|
||||
compute_PZT_angle,
|
||||
)
|
||||
|
||||
|
||||
def print_result(data_label: str, pzt_angle: float, magnitude: float, state: int, cop_x: float, cop_y: float, base_x: float, base_y: float):
|
||||
dx = cop_x - base_x
|
||||
dy = base_y - cop_y
|
||||
print(
|
||||
f"devkit: angle={pzt_angle:.2f}, magnitude={magnitude:.4f}, state={state}, "
|
||||
f"cop_x={cop_x:.4f}, cop_y={cop_y:.4f}, dx={dx:.4f}, dy={dy:.4f}"
|
||||
)
|
||||
|
||||
|
||||
def process_values(values: list[int | float]):
|
||||
"""处理一帧84个值并打印结果"""
|
||||
if len(values) != 84:
|
||||
print(f"[ERROR] 期望84个值,实际收到 {len(values)} 个", file=sys.stderr)
|
||||
return
|
||||
|
||||
try:
|
||||
pzt_angle, magnitude, state, cop_x, cop_y, base_x, base_y = get_pzt_angle(values)
|
||||
print_result("", pzt_angle, magnitude, state, cop_x, cop_y, base_x, base_y)
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 计算失败: {e}", file=sys.stderr)
|
||||
|
||||
|
||||
def run_random_test():
|
||||
"""生成随机数据测试算法"""
|
||||
reset_baseline()
|
||||
print("[TEST] 使用随机数据测试 CoP 算法")
|
||||
print("[TEST] 先用全零帧建立基线...")
|
||||
process_values([0] * 84)
|
||||
print("[TEST] 模拟右侧偏移按压...")
|
||||
# 模拟:row 5-7, col 4-6 区域有压力
|
||||
data = [0.0] * 84
|
||||
for r in range(5, 8):
|
||||
for c in range(4, 7):
|
||||
idx = r * 7 + c
|
||||
data[idx] = 100.0 + (c - 4) * 50 # 右侧更强
|
||||
process_values(data)
|
||||
print("[TEST] 模拟下方偏移按压...")
|
||||
data2 = [0.0] * 84
|
||||
for r in range(8, 11):
|
||||
for c in range(2, 5):
|
||||
idx = r * 7 + c
|
||||
data2[idx] = 150.0 + (r - 8) * 30
|
||||
process_values(data2)
|
||||
print("[TEST] 完成")
|
||||
|
||||
|
||||
def run_csv_mode(filepath: str):
|
||||
"""从 CSV 文件逐行读取并处理"""
|
||||
reset_baseline()
|
||||
print(f"[CSV] 读取文件: {filepath}")
|
||||
with open(filepath, "r", encoding="utf-8-sig", newline="") as f:
|
||||
reader = csv.reader(f)
|
||||
for i, row in enumerate(reader):
|
||||
if not row:
|
||||
continue
|
||||
# 跳过 header
|
||||
if row[0].strip() in ("seq", "timestamp_ms"):
|
||||
print(f"[CSV] 跳过 header: {row[:5]}...")
|
||||
continue
|
||||
try:
|
||||
values = [float(v) for v in row]
|
||||
if len(values) == 84:
|
||||
process_values(values)
|
||||
elif len(values) > 84:
|
||||
process_values(values[:84])
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
|
||||
def run_stdin_mode():
|
||||
"""从 stdin 逐行读取"""
|
||||
reset_baseline()
|
||||
print("[STDIN] 等待输入(每行84个逗号分隔数值,Ctrl+C 退出)...")
|
||||
try:
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
values = [float(v) for v in line.split(",")]
|
||||
if len(values) >= 84:
|
||||
process_values(values[:84])
|
||||
except ValueError:
|
||||
continue
|
||||
except KeyboardInterrupt:
|
||||
print("\n[STDIN] 已退出")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1:
|
||||
arg = sys.argv[1]
|
||||
if arg == "--random":
|
||||
run_random_test()
|
||||
elif arg == "--help" or arg == "-h":
|
||||
print(__doc__)
|
||||
else:
|
||||
run_csv_mode(arg)
|
||||
else:
|
||||
run_stdin_mode()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
82
package-lock.json
generated
82
package-lock.json
generated
@@ -16,12 +16,10 @@
|
||||
"three": "^0.183.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.61.0",
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.9.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/node": "^26.0.0",
|
||||
"@types/three": "^0.183.1",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
@@ -528,22 +526,6 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz",
|
||||
"integrity": "sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.61.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@polka/url": {
|
||||
"version": "1.0.0-next.29",
|
||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||
@@ -1339,16 +1321,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz",
|
||||
"integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/stats.js": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||
@@ -1714,53 +1686,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.61.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz",
|
||||
"integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz",
|
||||
"integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/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,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
||||
@@ -1993,13 +1918,6 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.4.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
|
||||
|
||||
@@ -22,12 +22,10 @@
|
||||
"three": "^0.183.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.61.0",
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.9.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/node": "^26.0.0",
|
||||
"@types/three": "^0.183.1",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// import dotenv from 'dotenv';
|
||||
// import path from 'path';
|
||||
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('')`. */
|
||||
// baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// url: 'http://localhost:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
});
|
||||
98
src-tauri/Cargo.lock
generated
98
src-tauri/Cargo.lock
generated
@@ -8,17 +8,14 @@ version = "0.4.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
"crc",
|
||||
"csv",
|
||||
"dirs",
|
||||
"eskin-finger-sdk",
|
||||
"fern",
|
||||
"futures-util",
|
||||
"humantime",
|
||||
"log",
|
||||
"ndarray",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"protoc-bin-vendored",
|
||||
@@ -1152,6 +1149,23 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eskin-finger-sdk"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"crc",
|
||||
"crossbeam-channel",
|
||||
"fern",
|
||||
"libc",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serialport",
|
||||
"thiserror 2.0.18",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.1"
|
||||
@@ -2314,9 +2328,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
@@ -2340,6 +2354,26 @@ dependencies = [
|
||||
"redox_syscall 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libudev"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libudev-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libudev-sys"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
@@ -2442,16 +2476,6 @@ version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "matrixmultiply"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"rawpointer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
@@ -2541,19 +2565,6 @@ version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
||||
|
||||
[[package]]
|
||||
name = "ndarray"
|
||||
version = "0.15.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32"
|
||||
dependencies = [
|
||||
"matrixmultiply",
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"rawpointer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
@@ -2619,30 +2630,12 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -3647,12 +3640,6 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "rawpointer"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
@@ -4263,6 +4250,7 @@ dependencies = [
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"io-kit-sys",
|
||||
"libudev",
|
||||
"mach2",
|
||||
"nix 0.26.4",
|
||||
"scopeguard",
|
||||
@@ -5565,9 +5553,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.22.0"
|
||||
version = "1.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
|
||||
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
|
||||
dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"js-sys",
|
||||
|
||||
@@ -15,9 +15,8 @@ name = "tauri_demo_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["multi-dim"]
|
||||
default = []
|
||||
devkit = ["dep:tonic", "dep:prost", "dep:prost-types", "dep:async-stream", "dep:dirs"]
|
||||
multi-dim = ["dep:ndarray"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
@@ -37,22 +36,19 @@ async-stream = { version = "0.3", optional = true }
|
||||
dirs = { version = "6", optional = true }
|
||||
tokio-serial = { version = "5.4.5" }
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
async-trait = "0.1.89"
|
||||
tokio-util = "0.7.18"
|
||||
serde_json = "1"
|
||||
fern = { version = "0.7.1", features = ["colored", "date-based"] }
|
||||
log = "0.4.29"
|
||||
humantime = "2.3.0"
|
||||
csv = "1.4.0"
|
||||
chrono = "0.4.44"
|
||||
crc = "3.4.0"
|
||||
axum = { version = "0.8", features = ["ws"] }
|
||||
tower-http = { version = "0.6", features = ["cors"] }
|
||||
futures-util = "0.3"
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
rand = "0.8"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
ndarray = { version = "0.15", optional = true }
|
||||
eskin-finger-sdk = { path = "../eskin-finger-sdk" }
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-updater = "2"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
|
||||
-keep class com.lenn.tauri_serial.TauriActivity {
|
||||
public app.tauri.plugin.PluginManager getPluginManager();
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"$schema":"https://schema.tauri.app/config/2","productName":"JE-Skin","version":"0.4.0","identifier":"com.lenn.tauri-serial","app":{"windows":[{"label":"main","create":true,"url":"index.html","dragDropEnabled":true,"center":false,"width":1366.0,"height":860.0,"resizable":true,"maximizable":true,"minimizable":true,"closable":true,"title":"JE-Skin","fullscreen":false,"focus":true,"focusable":true,"transparent":false,"maximized":false,"visible":true,"decorations":false,"alwaysOnBottom":false,"alwaysOnTop":false,"visibleOnAllWorkspaces":false,"contentProtected":false,"skipTaskbar":false,"titleBarStyle":"Visible","hiddenTitle":false,"acceptFirstMouse":false,"shadow":true,"incognito":false,"zoomHotkeysEnabled":false,"browserExtensionsEnabled":false,"useHttpsScheme":false,"javascriptDisabled":false,"allowLinkPreview":true,"disableInputAccessoryView":false,"scrollBarStyle":"default"}],"security":{"freezePrototype":false,"dangerousDisableAssetCspModification":false,"assetProtocol":{"scope":[],"enable":false},"pattern":{"use":"brownfield"},"capabilities":[]},"macOSPrivateApi":false,"withGlobalTauri":false,"enableGTKAppId":false},"build":{"devUrl":"http://localhost:1420/","frontendDist":"../build","beforeDevCommand":"npm run dev","beforeBuildCommand":"npm run build","removeUnusedCommands":false,"additionalWatchFolders":[]},"bundle":{"active":true,"targets":"all","createUpdaterArtifacts":true,"icon":["icons/32x32.png","icons/128x128.png","icons/128x128@2x.png","icons/icon.icns","icons/icon.ico"],"resources":["resources/je-skin-devkit-server.exe"],"useLocalToolsDir":false,"windows":{"digestAlgorithm":null,"certificateThumbprint":null,"timestampUrl":null,"tsp":false,"webviewInstallMode":{"type":"downloadBootstrapper","silent":true},"allowDowngrades":true,"wix":null,"nsis":{"template":"nsis/installer.nsi","headerImage":null,"sidebarImage":null,"installerIcon":"icons/icon.ico","installMode":"both","languages":null,"customLanguageFiles":null,"displayLanguageSelector":false,"compression":"lzma","startMenuFolder":null,"installerHooks":null,"minimumWebview2Version":null},"signCommand":null},"linux":{"appimage":{"bundleMediaFramework":false,"files":{}},"deb":{"files":{}},"rpm":{"release":"1","epoch":0,"files":{}}},"macOS":{"files":{},"minimumSystemVersion":"10.13","hardenedRuntime":true,"dmg":{"windowSize":{"width":660,"height":400},"appPosition":{"x":180,"y":170},"applicationFolderPosition":{"x":480,"y":170}}},"iOS":{"minimumSystemVersion":"14.0"},"android":{"minSdkVersion":24,"autoIncrementVersionCode":false}},"plugins":{}}
|
||||
@@ -1,33 +0,0 @@
|
||||
/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */
|
||||
|
||||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package com.lenn.tauri_serial
|
||||
|
||||
import android.webkit.*
|
||||
|
||||
class Ipc(val webViewClient: RustWebViewClient) {
|
||||
@JavascriptInterface
|
||||
fun postMessage(message: String?) {
|
||||
message?.let {m ->
|
||||
// we're not using WebView::getUrl() here because it needs to be executed on the main thread
|
||||
// and it would slow down the Ipc
|
||||
// so instead we track the current URL on the webview client
|
||||
this.ipc(webViewClient.currentUrl, m)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("tauri_demo_lib")
|
||||
}
|
||||
}
|
||||
|
||||
private external fun ipc(url: String, message: String)
|
||||
|
||||
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */
|
||||
|
||||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package com.lenn.tauri_serial
|
||||
|
||||
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/Logger.java
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
|
||||
class Logger {
|
||||
companion object {
|
||||
private const val LOG_TAG_CORE = "Tauri"
|
||||
|
||||
fun tags(vararg subtags: String): String {
|
||||
return if (subtags.isNotEmpty()) {
|
||||
LOG_TAG_CORE + "/" + TextUtils.join("/", subtags)
|
||||
} else LOG_TAG_CORE
|
||||
}
|
||||
|
||||
fun verbose(message: String) {
|
||||
verbose(LOG_TAG_CORE, message)
|
||||
}
|
||||
|
||||
private fun verbose(tag: String, message: String) {
|
||||
if (!shouldLog()) {
|
||||
return
|
||||
}
|
||||
Log.v(tag, message)
|
||||
}
|
||||
|
||||
fun debug(message: String) {
|
||||
debug(LOG_TAG_CORE, message)
|
||||
}
|
||||
|
||||
fun debug(tag: String, message: String) {
|
||||
if (!shouldLog()) {
|
||||
return
|
||||
}
|
||||
Log.d(tag, message)
|
||||
}
|
||||
|
||||
fun info(message: String) {
|
||||
info(LOG_TAG_CORE, message)
|
||||
}
|
||||
|
||||
fun info(tag: String, message: String) {
|
||||
if (!shouldLog()) {
|
||||
return
|
||||
}
|
||||
Log.i(tag, message)
|
||||
}
|
||||
|
||||
fun warn(message: String) {
|
||||
warn(LOG_TAG_CORE, message)
|
||||
}
|
||||
|
||||
fun warn(tag: String, message: String) {
|
||||
if (!shouldLog()) {
|
||||
return
|
||||
}
|
||||
Log.w(tag, message)
|
||||
}
|
||||
|
||||
fun error(message: String) {
|
||||
error(LOG_TAG_CORE, message, null)
|
||||
}
|
||||
|
||||
fun error(message: String, e: Throwable?) {
|
||||
error(LOG_TAG_CORE, message, e)
|
||||
}
|
||||
|
||||
fun error(tag: String, message: String, e: Throwable?) {
|
||||
if (!shouldLog()) {
|
||||
return
|
||||
}
|
||||
Log.e(tag, message, e)
|
||||
}
|
||||
|
||||
private fun shouldLog(): Boolean {
|
||||
return BuildConfig.DEBUG
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */
|
||||
|
||||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package com.lenn.tauri_serial
|
||||
|
||||
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/PermissionHelper.java
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.app.ActivityCompat
|
||||
import java.util.ArrayList
|
||||
|
||||
object PermissionHelper {
|
||||
/**
|
||||
* Checks if a list of given permissions are all granted by the user
|
||||
*
|
||||
* @param permissions Permissions to check.
|
||||
* @return True if all permissions are granted, false if at least one is not.
|
||||
*/
|
||||
fun hasPermissions(context: Context?, permissions: Array<String>): Boolean {
|
||||
for (perm in permissions) {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
context!!,
|
||||
perm
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given permission has been defined in the AndroidManifest.xml
|
||||
*
|
||||
* @param permission A permission to check.
|
||||
* @return True if the permission has been defined in the Manifest, false if not.
|
||||
*/
|
||||
fun hasDefinedPermission(context: Context, permission: String): Boolean {
|
||||
var hasPermission = false
|
||||
val requestedPermissions = getManifestPermissions(context)
|
||||
if (!requestedPermissions.isNullOrEmpty()) {
|
||||
val requestedPermissionsList = listOf(*requestedPermissions)
|
||||
val requestedPermissionsArrayList = ArrayList(requestedPermissionsList)
|
||||
if (requestedPermissionsArrayList.contains(permission)) {
|
||||
hasPermission = true
|
||||
}
|
||||
}
|
||||
return hasPermission
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether all of the given permissions have been defined in the AndroidManifest.xml
|
||||
* @param context the app context
|
||||
* @param permissions a list of permissions
|
||||
* @return true only if all permissions are defined in the AndroidManifest.xml
|
||||
*/
|
||||
fun hasDefinedPermissions(context: Context, permissions: Array<String>): Boolean {
|
||||
for (permission in permissions) {
|
||||
if (!hasDefinedPermission(context, permission)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permissions defined in AndroidManifest.xml
|
||||
*
|
||||
* @return The permissions defined in AndroidManifest.xml
|
||||
*/
|
||||
private fun getManifestPermissions(context: Context): Array<String>? {
|
||||
var requestedPermissions: Array<String>? = null
|
||||
try {
|
||||
val pm = context.packageManager
|
||||
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pm.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
pm.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)
|
||||
}
|
||||
if (packageInfo != null) {
|
||||
requestedPermissions = packageInfo.requestedPermissions
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
return requestedPermissions
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml
|
||||
*
|
||||
* @param neededPermissions The permissions needed.
|
||||
* @return The permissions not present in AndroidManifest.xml
|
||||
*/
|
||||
fun getUndefinedPermissions(context: Context, neededPermissions: Array<String?>): Array<String?> {
|
||||
val undefinedPermissions = ArrayList<String?>()
|
||||
val requestedPermissions = getManifestPermissions(context)
|
||||
if (!requestedPermissions.isNullOrEmpty()) {
|
||||
val requestedPermissionsList = listOf(*requestedPermissions)
|
||||
val requestedPermissionsArrayList = ArrayList(requestedPermissionsList)
|
||||
for (permission in neededPermissions) {
|
||||
if (!requestedPermissionsArrayList.contains(permission)) {
|
||||
undefinedPermissions.add(permission)
|
||||
}
|
||||
}
|
||||
var undefinedPermissionArray = arrayOfNulls<String>(undefinedPermissions.size)
|
||||
undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray)
|
||||
return undefinedPermissionArray
|
||||
}
|
||||
return neededPermissions
|
||||
}
|
||||
}
|
||||
@@ -1,495 +0,0 @@
|
||||
/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */
|
||||
|
||||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@file:Suppress("ObsoleteSdkInt", "RedundantOverride", "QueryPermissionsNeeded", "SimpleDateFormat")
|
||||
|
||||
package com.lenn.tauri_serial
|
||||
|
||||
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.view.View
|
||||
import android.webkit.*
|
||||
import android.widget.EditText
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() {
|
||||
private interface PermissionListener {
|
||||
fun onPermissionSelect(isGranted: Boolean?)
|
||||
}
|
||||
|
||||
private interface ActivityResultListener {
|
||||
fun onActivityResult(result: ActivityResult?)
|
||||
}
|
||||
|
||||
private val activity: WryActivity
|
||||
private var permissionLauncher: ActivityResultLauncher<Array<String>>
|
||||
private var activityLauncher: ActivityResultLauncher<Intent>
|
||||
private var permissionListener: PermissionListener? = null
|
||||
private var activityListener: ActivityResultListener? = null
|
||||
|
||||
init {
|
||||
activity = appActivity
|
||||
val permissionCallback =
|
||||
ActivityResultCallback { isGranted: Map<String, Boolean> ->
|
||||
if (permissionListener != null) {
|
||||
var granted = true
|
||||
for ((_, value) in isGranted) {
|
||||
if (!value) granted = false
|
||||
}
|
||||
permissionListener!!.onPermissionSelect(granted)
|
||||
}
|
||||
}
|
||||
permissionLauncher =
|
||||
activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions(), permissionCallback)
|
||||
activityLauncher = activity.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (activityListener != null) {
|
||||
activityListener!!.onActivityResult(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render web content in `view`.
|
||||
*
|
||||
* Both this method and [.onHideCustomView] are required for
|
||||
* rendering web content in full screen.
|
||||
*
|
||||
* @see [](https://developer.android.com/reference/android/webkit/WebChromeClient.onShowCustomView
|
||||
) */
|
||||
override fun onShowCustomView(view: View, callback: CustomViewCallback) {
|
||||
callback.onCustomViewHidden()
|
||||
super.onShowCustomView(view, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render web content in the original Web View again.
|
||||
*
|
||||
* Do not remove this method--@see #onShowCustomView(View, CustomViewCallback).
|
||||
*/
|
||||
override fun onHideCustomView() {
|
||||
super.onHideCustomView()
|
||||
}
|
||||
|
||||
override fun onPermissionRequest(request: PermissionRequest) {
|
||||
val isRequestPermissionRequired = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
val permissionList: MutableList<String> = ArrayList()
|
||||
if (listOf(*request.resources).contains("android.webkit.resource.VIDEO_CAPTURE")) {
|
||||
permissionList.add(Manifest.permission.CAMERA)
|
||||
}
|
||||
if (listOf(*request.resources).contains("android.webkit.resource.AUDIO_CAPTURE")) {
|
||||
permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS)
|
||||
permissionList.add(Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
if (permissionList.isNotEmpty() && isRequestPermissionRequired) {
|
||||
val permissions = permissionList.toTypedArray()
|
||||
permissionListener = object : PermissionListener {
|
||||
override fun onPermissionSelect(isGranted: Boolean?) {
|
||||
if (isGranted == true) {
|
||||
request.grant(request.resources)
|
||||
} else {
|
||||
request.deny()
|
||||
}
|
||||
}
|
||||
}
|
||||
permissionLauncher.launch(permissions)
|
||||
} else {
|
||||
request.grant(request.resources)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the browser alert modal
|
||||
* @param view
|
||||
* @param url
|
||||
* @param message
|
||||
* @param result
|
||||
* @return
|
||||
*/
|
||||
override fun onJsAlert(view: WebView, url: String, message: String, result: JsResult): Boolean {
|
||||
if (activity.isFinishing) {
|
||||
return true
|
||||
}
|
||||
val builder = AlertDialog.Builder(view.context)
|
||||
builder
|
||||
.setMessage(message)
|
||||
.setPositiveButton(
|
||||
"OK"
|
||||
) { dialog: DialogInterface, _: Int ->
|
||||
dialog.dismiss()
|
||||
result.confirm()
|
||||
}
|
||||
.setOnCancelListener { dialog: DialogInterface ->
|
||||
dialog.dismiss()
|
||||
result.cancel()
|
||||
}
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the browser confirm modal
|
||||
* @param view
|
||||
* @param url
|
||||
* @param message
|
||||
* @param result
|
||||
* @return
|
||||
*/
|
||||
override fun onJsConfirm(view: WebView, url: String, message: String, result: JsResult): Boolean {
|
||||
if (activity.isFinishing) {
|
||||
return true
|
||||
}
|
||||
val builder = AlertDialog.Builder(view.context)
|
||||
builder
|
||||
.setMessage(message)
|
||||
.setPositiveButton(
|
||||
"OK"
|
||||
) { dialog: DialogInterface, _: Int ->
|
||||
dialog.dismiss()
|
||||
result.confirm()
|
||||
}
|
||||
.setNegativeButton(
|
||||
"Cancel"
|
||||
) { dialog: DialogInterface, _: Int ->
|
||||
dialog.dismiss()
|
||||
result.cancel()
|
||||
}
|
||||
.setOnCancelListener { dialog: DialogInterface ->
|
||||
dialog.dismiss()
|
||||
result.cancel()
|
||||
}
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the browser prompt modal
|
||||
* @param view
|
||||
* @param url
|
||||
* @param message
|
||||
* @param defaultValue
|
||||
* @param result
|
||||
* @return
|
||||
*/
|
||||
override fun onJsPrompt(
|
||||
view: WebView,
|
||||
url: String,
|
||||
message: String,
|
||||
defaultValue: String,
|
||||
result: JsPromptResult
|
||||
): Boolean {
|
||||
if (activity.isFinishing) {
|
||||
return true
|
||||
}
|
||||
val builder = AlertDialog.Builder(view.context)
|
||||
val input = EditText(view.context)
|
||||
builder
|
||||
.setMessage(message)
|
||||
.setView(input)
|
||||
.setPositiveButton(
|
||||
"OK"
|
||||
) { dialog: DialogInterface, _: Int ->
|
||||
dialog.dismiss()
|
||||
val inputText1 = input.text.toString().trim { it <= ' ' }
|
||||
result.confirm(inputText1)
|
||||
}
|
||||
.setNegativeButton(
|
||||
"Cancel"
|
||||
) { dialog: DialogInterface, _: Int ->
|
||||
dialog.dismiss()
|
||||
result.cancel()
|
||||
}
|
||||
.setOnCancelListener { dialog: DialogInterface ->
|
||||
dialog.dismiss()
|
||||
result.cancel()
|
||||
}
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the browser geolocation permission prompt
|
||||
* @param origin
|
||||
* @param callback
|
||||
*/
|
||||
override fun onGeolocationPermissionsShowPrompt(
|
||||
origin: String,
|
||||
callback: GeolocationPermissions.Callback
|
||||
) {
|
||||
super.onGeolocationPermissionsShowPrompt(origin, callback)
|
||||
Logger.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: $origin")
|
||||
val geoPermissions =
|
||||
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
if (!PermissionHelper.hasPermissions(activity, geoPermissions)) {
|
||||
permissionListener = object : PermissionListener {
|
||||
override fun onPermissionSelect(isGranted: Boolean?) {
|
||||
if (isGranted == true) {
|
||||
callback.invoke(origin, true, false)
|
||||
} else {
|
||||
val coarsePermission =
|
||||
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
||||
PermissionHelper.hasPermissions(activity, coarsePermission)
|
||||
) {
|
||||
callback.invoke(origin, true, false)
|
||||
} else {
|
||||
callback.invoke(origin, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
permissionLauncher.launch(geoPermissions)
|
||||
} else {
|
||||
// permission is already granted
|
||||
callback.invoke(origin, true, false)
|
||||
Logger.debug("onGeolocationPermissionsShowPrompt: has required permission")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShowFileChooser(
|
||||
webView: WebView,
|
||||
filePathCallback: ValueCallback<Array<Uri?>?>,
|
||||
fileChooserParams: FileChooserParams
|
||||
): Boolean {
|
||||
val acceptTypes = listOf(*fileChooserParams.acceptTypes)
|
||||
val captureEnabled = fileChooserParams.isCaptureEnabled
|
||||
val capturePhoto = captureEnabled && acceptTypes.contains("image/*")
|
||||
val captureVideo = captureEnabled && acceptTypes.contains("video/*")
|
||||
if (capturePhoto || captureVideo) {
|
||||
if (isMediaCaptureSupported) {
|
||||
showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo)
|
||||
} else {
|
||||
permissionListener = object : PermissionListener {
|
||||
override fun onPermissionSelect(isGranted: Boolean?) {
|
||||
if (isGranted == true) {
|
||||
showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo)
|
||||
} else {
|
||||
Logger.warn(Logger.tags("FileChooser"), "Camera permission not granted")
|
||||
filePathCallback.onReceiveValue(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
val camPermission = arrayOf(Manifest.permission.CAMERA)
|
||||
permissionLauncher.launch(camPermission)
|
||||
}
|
||||
} else {
|
||||
showFilePicker(filePathCallback, fileChooserParams)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private val isMediaCaptureSupported: Boolean
|
||||
get() {
|
||||
val permissions = arrayOf(Manifest.permission.CAMERA)
|
||||
return PermissionHelper.hasPermissions(activity, permissions) ||
|
||||
!PermissionHelper.hasDefinedPermission(activity, Manifest.permission.CAMERA)
|
||||
}
|
||||
|
||||
private fun showMediaCaptureOrFilePicker(
|
||||
filePathCallback: ValueCallback<Array<Uri?>?>,
|
||||
fileChooserParams: FileChooserParams,
|
||||
isVideo: Boolean
|
||||
) {
|
||||
val isVideoCaptureSupported = true
|
||||
val shown = if (isVideo && isVideoCaptureSupported) {
|
||||
showVideoCapturePicker(filePathCallback)
|
||||
} else {
|
||||
showImageCapturePicker(filePathCallback)
|
||||
}
|
||||
if (!shown) {
|
||||
Logger.warn(
|
||||
Logger.tags("FileChooser"),
|
||||
"Media capture intent could not be launched. Falling back to default file picker."
|
||||
)
|
||||
showFilePicker(filePathCallback, fileChooserParams)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showImageCapturePicker(filePathCallback: ValueCallback<Array<Uri?>?>): Boolean {
|
||||
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||
if (takePictureIntent.resolveActivity(activity.packageManager) == null) {
|
||||
return false
|
||||
}
|
||||
val imageFileUri: Uri = try {
|
||||
createImageFileUri()
|
||||
} catch (ex: Exception) {
|
||||
Logger.error("Unable to create temporary media capture file: " + ex.message)
|
||||
return false
|
||||
}
|
||||
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri)
|
||||
activityListener = object : ActivityResultListener {
|
||||
override fun onActivityResult(result: ActivityResult?) {
|
||||
var res: Array<Uri?>? = null
|
||||
if (result?.resultCode == Activity.RESULT_OK) {
|
||||
res = arrayOf(imageFileUri)
|
||||
}
|
||||
filePathCallback.onReceiveValue(res)
|
||||
}
|
||||
}
|
||||
activityLauncher.launch(takePictureIntent)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun showVideoCapturePicker(filePathCallback: ValueCallback<Array<Uri?>?>): Boolean {
|
||||
val takeVideoIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
|
||||
if (takeVideoIntent.resolveActivity(activity.packageManager) == null) {
|
||||
return false
|
||||
}
|
||||
activityListener = object : ActivityResultListener {
|
||||
override fun onActivityResult(result: ActivityResult?) {
|
||||
var res: Array<Uri?>? = null
|
||||
if (result?.resultCode == Activity.RESULT_OK) {
|
||||
res = arrayOf(result.data!!.data)
|
||||
}
|
||||
filePathCallback.onReceiveValue(res)
|
||||
}
|
||||
}
|
||||
activityLauncher.launch(takeVideoIntent)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun showFilePicker(
|
||||
filePathCallback: ValueCallback<Array<Uri?>?>,
|
||||
fileChooserParams: FileChooserParams
|
||||
) {
|
||||
val intent = fileChooserParams.createIntent()
|
||||
if (fileChooserParams.mode == FileChooserParams.MODE_OPEN_MULTIPLE) {
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
}
|
||||
if (fileChooserParams.acceptTypes.size > 1 || intent.type!!.startsWith(".")) {
|
||||
val validTypes = getValidTypes(fileChooserParams.acceptTypes)
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, validTypes)
|
||||
if (intent.type!!.startsWith(".")) {
|
||||
intent.type = validTypes[0]
|
||||
}
|
||||
}
|
||||
try {
|
||||
activityListener = object : ActivityResultListener {
|
||||
override fun onActivityResult(result: ActivityResult?) {
|
||||
val res: Array<Uri?>?
|
||||
val resultIntent = result?.data
|
||||
if (result?.resultCode == Activity.RESULT_OK && resultIntent!!.clipData != null) {
|
||||
val numFiles = resultIntent.clipData!!.itemCount
|
||||
res = arrayOfNulls(numFiles)
|
||||
for (i in 0 until numFiles) {
|
||||
res[i] = resultIntent.clipData!!.getItemAt(i).uri
|
||||
}
|
||||
} else {
|
||||
res = FileChooserParams.parseResult(
|
||||
result?.resultCode ?: 0,
|
||||
resultIntent
|
||||
)
|
||||
}
|
||||
filePathCallback.onReceiveValue(res)
|
||||
}
|
||||
}
|
||||
activityLauncher.launch(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
filePathCallback.onReceiveValue(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getValidTypes(currentTypes: Array<String>): Array<String> {
|
||||
val validTypes: MutableList<String> = ArrayList()
|
||||
val mtm = MimeTypeMap.getSingleton()
|
||||
for (mime in currentTypes) {
|
||||
if (mime.startsWith(".")) {
|
||||
val extension = mime.substring(1)
|
||||
val extensionMime = mtm.getMimeTypeFromExtension(extension)
|
||||
if (extensionMime != null && !validTypes.contains(extensionMime)) {
|
||||
validTypes.add(extensionMime)
|
||||
}
|
||||
} else if (!validTypes.contains(mime)) {
|
||||
validTypes.add(mime)
|
||||
}
|
||||
}
|
||||
val validObj: Array<Any> = validTypes.toTypedArray()
|
||||
return Arrays.copyOf(
|
||||
validObj, validObj.size,
|
||||
Array<String>::class.java
|
||||
)
|
||||
}
|
||||
|
||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
|
||||
val tag: String = Logger.tags("Console")
|
||||
if (consoleMessage.message() != null && isValidMsg(consoleMessage.message())) {
|
||||
val msg = String.format(
|
||||
"File: %s - Line %d - Msg: %s",
|
||||
consoleMessage.sourceId(),
|
||||
consoleMessage.lineNumber(),
|
||||
consoleMessage.message()
|
||||
)
|
||||
val level = consoleMessage.messageLevel().name
|
||||
if ("ERROR".equals(level, ignoreCase = true)) {
|
||||
Logger.error(tag, msg, null)
|
||||
} else if ("WARNING".equals(level, ignoreCase = true)) {
|
||||
Logger.warn(tag, msg)
|
||||
} else if ("TIP".equals(level, ignoreCase = true)) {
|
||||
Logger.debug(tag, msg)
|
||||
} else {
|
||||
Logger.info(tag, msg)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun isValidMsg(msg: String): Boolean {
|
||||
return !(msg.contains("%cresult %c") ||
|
||||
msg.contains("%cnative %c") ||
|
||||
msg.equals("[object Object]", ignoreCase = true) ||
|
||||
msg.equals("console.groupEnd", ignoreCase = true))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun createImageFileUri(): Uri {
|
||||
val photoFile = createImageFile(activity)
|
||||
return FileProvider.getUriForFile(
|
||||
activity,
|
||||
activity.packageName.toString() + ".fileprovider",
|
||||
photoFile
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun createImageFile(activity: Activity): File {
|
||||
// Create an image file name
|
||||
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
|
||||
val imageFileName = "JPEG_" + timeStamp + "_"
|
||||
val storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
||||
return File.createTempFile(imageFileName, ".jpg", storageDir)
|
||||
}
|
||||
|
||||
override fun onReceivedTitle(
|
||||
view: WebView,
|
||||
title: String
|
||||
) {
|
||||
handleReceivedTitle(view, title)
|
||||
}
|
||||
|
||||
private external fun handleReceivedTitle(webview: WebView, title: String)
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */
|
||||
|
||||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@file:Suppress("unused", "SetJavaScriptEnabled")
|
||||
|
||||
package com.lenn.tauri_serial
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.webkit.*
|
||||
import android.content.Context
|
||||
import androidx.webkit.WebViewCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import kotlin.collections.Map
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
class RustWebView(context: Context, val initScripts: Array<String>, val id: String): WebView(context) {
|
||||
val isDocumentStartScriptEnabled: Boolean
|
||||
|
||||
init {
|
||||
settings.javaScriptEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
settings.setGeolocationEnabled(true)
|
||||
settings.databaseEnabled = true
|
||||
settings.mediaPlaybackRequiresUserGesture = false
|
||||
settings.javaScriptCanOpenWindowsAutomatically = true
|
||||
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
|
||||
isDocumentStartScriptEnabled = true
|
||||
for (script in initScripts) {
|
||||
WebViewCompat.addDocumentStartJavaScript(this, script, setOf("*"));
|
||||
}
|
||||
} else {
|
||||
isDocumentStartScriptEnabled = false
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fun loadUrlMainThread(url: String) {
|
||||
post {
|
||||
loadUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadUrlMainThread(url: String, additionalHttpHeaders: Map<String, String>) {
|
||||
post {
|
||||
loadUrl(url, additionalHttpHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadUrl(url: String) {
|
||||
if (!shouldOverride(url)) {
|
||||
super.loadUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadUrl(url: String, additionalHttpHeaders: Map<String, String>) {
|
||||
if (!shouldOverride(url)) {
|
||||
super.loadUrl(url, additionalHttpHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
fun loadHTMLMainThread(html: String) {
|
||||
post {
|
||||
super.loadData(html, "text/html", null)
|
||||
}
|
||||
}
|
||||
|
||||
fun evalScript(id: Int, script: String) {
|
||||
post {
|
||||
super.evaluateJavascript(script) { result ->
|
||||
onEval(id, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearAllBrowsingData() {
|
||||
try {
|
||||
super.getContext().deleteDatabase("webviewCache.db")
|
||||
super.getContext().deleteDatabase("webview.db")
|
||||
super.clearCache(true)
|
||||
super.clearHistory()
|
||||
super.clearFormData()
|
||||
} catch (ex: Exception) {
|
||||
Logger.error("Unable to create temporary media capture file: " + ex.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun getCookies(url: String): String {
|
||||
val cookieManager = CookieManager.getInstance()
|
||||
return cookieManager.getCookie(url)
|
||||
}
|
||||
|
||||
private external fun shouldOverride(url: String): Boolean
|
||||
private external fun onEval(id: Int, result: String)
|
||||
|
||||
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */
|
||||
|
||||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package com.lenn.tauri_serial
|
||||
|
||||
import android.net.Uri
|
||||
import android.webkit.*
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.webkit.WebViewAssetLoader
|
||||
|
||||
class RustWebViewClient(context: Context): WebViewClient() {
|
||||
private val interceptedState = mutableMapOf<String, Boolean>()
|
||||
var currentUrl: String = "about:blank"
|
||||
private var lastInterceptedUrl: Uri? = null
|
||||
private var pendingUrlRedirect: String? = null
|
||||
|
||||
private val assetLoader = WebViewAssetLoader.Builder()
|
||||
.setDomain(assetLoaderDomain())
|
||||
.addPathHandler("/", WebViewAssetLoader.AssetsPathHandler(context))
|
||||
.build()
|
||||
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): WebResourceResponse? {
|
||||
pendingUrlRedirect?.let {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
view.loadUrl(it)
|
||||
}
|
||||
pendingUrlRedirect = null
|
||||
return null
|
||||
}
|
||||
|
||||
lastInterceptedUrl = request.url
|
||||
return if (withAssetLoader()) {
|
||||
assetLoader.shouldInterceptRequest(request.url)
|
||||
} else {
|
||||
val rustWebview = view as RustWebView;
|
||||
val response = handleRequest(rustWebview.id, request, rustWebview.isDocumentStartScriptEnabled)
|
||||
interceptedState[request.url.toString()] = response != null
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): Boolean {
|
||||
return shouldOverride(request.url.toString())
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
|
||||
currentUrl = url
|
||||
if (interceptedState[url] == false) {
|
||||
val webView = view as RustWebView
|
||||
for (script in webView.initScripts) {
|
||||
view.evaluateJavascript(script, null)
|
||||
}
|
||||
}
|
||||
return onPageLoading(url)
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
onPageLoaded(url)
|
||||
}
|
||||
|
||||
override fun onReceivedError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
error: WebResourceError
|
||||
) {
|
||||
// we get a net::ERR_CONNECTION_REFUSED when an external URL redirects to a custom protocol
|
||||
// e.g. oauth flow, because shouldInterceptRequest is not called on redirects
|
||||
// so we must force retry here with loadUrl() to get a chance of the custom protocol to kick in
|
||||
if (error.errorCode == ERROR_CONNECT && request.isForMainFrame && request.url != lastInterceptedUrl) {
|
||||
// prevent the default error page from showing
|
||||
view.stopLoading()
|
||||
// without this initial loadUrl the app is stuck
|
||||
view.loadUrl(request.url.toString())
|
||||
// ensure the URL is actually loaded - for some reason there's a race condition and we need to call loadUrl() again later
|
||||
pendingUrlRedirect = request.url.toString()
|
||||
} else {
|
||||
super.onReceivedError(view, request, error)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("tauri_demo_lib")
|
||||
}
|
||||
}
|
||||
|
||||
private external fun assetLoaderDomain(): String
|
||||
private external fun withAssetLoader(): Boolean
|
||||
private external fun handleRequest(webviewId: String, request: WebResourceRequest, isDocumentStartScriptEnabled: Boolean): WebResourceResponse?
|
||||
private external fun shouldOverride(url: String): Boolean
|
||||
private external fun onPageLoading(url: String)
|
||||
private external fun onPageLoaded(url: String)
|
||||
|
||||
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */
|
||||
|
||||
package com.lenn.tauri_serial
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import app.tauri.plugin.PluginManager
|
||||
|
||||
abstract class TauriActivity : WryActivity() {
|
||||
var pluginManager: PluginManager = PluginManager(this)
|
||||
override val handleBackNavigation: Boolean = false
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
pluginManager.onNewIntent(intent)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
pluginManager.onResume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
pluginManager.onPause()
|
||||
}
|
||||
|
||||
override fun onRestart() {
|
||||
super.onRestart()
|
||||
pluginManager.onRestart()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
pluginManager.onStop()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
pluginManager.onDestroy()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
pluginManager.onConfigurationChanged(newConfig)
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */
|
||||
|
||||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package com.lenn.tauri_serial
|
||||
|
||||
import com.lenn.tauri_serial.RustWebView
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebView
|
||||
import android.view.KeyEvent
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
abstract class WryActivity : AppCompatActivity() {
|
||||
private lateinit var mWebView: RustWebView
|
||||
open val handleBackNavigation: Boolean = true
|
||||
|
||||
open fun onWebViewCreate(webView: WebView) { }
|
||||
|
||||
fun setWebView(webView: RustWebView) {
|
||||
mWebView = webView
|
||||
|
||||
if (handleBackNavigation) {
|
||||
val callback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (this@WryActivity.mWebView.canGoBack()) {
|
||||
this@WryActivity.mWebView.goBack()
|
||||
} else {
|
||||
this.isEnabled = false
|
||||
this@WryActivity.onBackPressed()
|
||||
this.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, callback)
|
||||
}
|
||||
|
||||
onWebViewCreate(webView)
|
||||
}
|
||||
|
||||
val version: String
|
||||
@SuppressLint("WebViewApiAvailability", "ObsoleteSdkInt")
|
||||
get() {
|
||||
// Check getCurrentWebViewPackage() directly if above Android 8
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
return WebView.getCurrentWebViewPackage()?.versionName ?: ""
|
||||
}
|
||||
|
||||
// Otherwise manually check WebView versions
|
||||
var webViewPackage = "com.google.android.webview"
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
webViewPackage = "com.android.chrome"
|
||||
}
|
||||
try {
|
||||
@Suppress("DEPRECATION")
|
||||
val info = packageManager.getPackageInfo(webViewPackage, 0)
|
||||
return info.versionName.toString()
|
||||
} catch (ex: Exception) {
|
||||
Logger.warn("Unable to get package info for '$webViewPackage'$ex")
|
||||
}
|
||||
|
||||
try {
|
||||
@Suppress("DEPRECATION")
|
||||
val info = packageManager.getPackageInfo("com.android.webview", 0)
|
||||
return info.versionName.toString()
|
||||
} catch (ex: Exception) {
|
||||
Logger.warn("Unable to get package info for 'com.android.webview'$ex")
|
||||
}
|
||||
|
||||
// Could not detect any webview, return empty string
|
||||
return ""
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
create(this)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
start()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
resume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
pause()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
stop()
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
focus(hasFocus)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
save()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
destroy()
|
||||
onActivityDestroy()
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
super.onLowMemory()
|
||||
memory()
|
||||
}
|
||||
|
||||
fun getAppClass(name: String): Class<*> {
|
||||
return Class.forName(name)
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("tauri_demo_lib")
|
||||
}
|
||||
}
|
||||
|
||||
private external fun create(activity: WryActivity)
|
||||
private external fun start()
|
||||
private external fun resume()
|
||||
private external fun pause()
|
||||
private external fun stop()
|
||||
private external fun save()
|
||||
private external fun destroy()
|
||||
private external fun onActivityDestroy()
|
||||
private external fun memory()
|
||||
private external fun focus(focus: Boolean)
|
||||
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
# THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!!
|
||||
|
||||
# Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
-keep class com.lenn.tauri_serial.* {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
-keep class com.lenn.tauri_serial.WryActivity {
|
||||
public <init>(...);
|
||||
|
||||
void setWebView(com.lenn.tauri_serial.RustWebView);
|
||||
java.lang.Class getAppClass(...);
|
||||
java.lang.String getVersion();
|
||||
}
|
||||
|
||||
-keep class com.lenn.tauri_serial.Ipc {
|
||||
public <init>(...);
|
||||
|
||||
@android.webkit.JavascriptInterface public <methods>;
|
||||
}
|
||||
|
||||
-keep class com.lenn.tauri_serial.RustWebView {
|
||||
public <init>(...);
|
||||
|
||||
void loadUrlMainThread(...);
|
||||
void loadHTMLMainThread(...);
|
||||
void evalScript(...);
|
||||
}
|
||||
|
||||
-keep class com.lenn.tauri_serial.RustWebChromeClient,com.lenn.tauri_serial.RustWebViewClient {
|
||||
public <init>(...);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
/home/lenn/Workspace/JE-Skin/src-tauri/target/aarch64-linux-android/release/libtauri_demo_lib.so
|
||||
@@ -1 +0,0 @@
|
||||
/home/lenn/Workspace/JE-Skin/src-tauri/target/armv7-linux-androideabi/release/libtauri_demo_lib.so
|
||||
@@ -1 +0,0 @@
|
||||
/home/lenn/Workspace/JE-Skin/src-tauri/target/i686-linux-android/release/libtauri_demo_lib.so
|
||||
@@ -1 +0,0 @@
|
||||
/home/lenn/Workspace/JE-Skin/src-tauri/target/x86_64-linux-android/release/libtauri_demo_lib.so
|
||||
@@ -1,6 +0,0 @@
|
||||
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
val implementation by configurations
|
||||
dependencies {
|
||||
implementation(project(":tauri-android"))
|
||||
implementation(project(":tauri-plugin-opener"))
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
tauri.android.versionName=0.4.0
|
||||
tauri.android.versionCode=4000
|
||||
@@ -1,5 +0,0 @@
|
||||
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
include ':tauri-android'
|
||||
project(':tauri-android').projectDir = new File("/home/lenn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.10.3/mobile/android")
|
||||
include ':tauri-plugin-opener'
|
||||
project(':tauri-plugin-opener').projectDir = new File("/home/lenn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-opener-2.5.3/android")
|
||||
@@ -27,14 +27,6 @@ message PztAngleResponse {
|
||||
uint32 dts_ms = 4;
|
||||
bool ok = 5;
|
||||
string message = 6;
|
||||
float magnitude = 7;
|
||||
uint32 state = 8;
|
||||
float cop_x = 9;
|
||||
float cop_y = 10;
|
||||
float base_x = 11;
|
||||
float base_y = 12;
|
||||
float total_press = 13;
|
||||
float threshold = 14;
|
||||
}
|
||||
|
||||
message ProcessRequest {
|
||||
|
||||
Binary file not shown.
@@ -1,162 +0,0 @@
|
||||
/// AD值反解x计算器
|
||||
/// AD = -5.732*x^3 - 131.5*x^2 + 31980*x + 13490 (x <= 6.57)
|
||||
/// AD = -377.8*x^2 + 26040*x + 51120 (x > 6.57)
|
||||
|
||||
const X_BOUNDARY: f64 = 6.57;
|
||||
|
||||
/// 二次方程在边界处的AD值
|
||||
/// 当 x = 6.57 时,AD = -377.8*6.57^2 + 26040*6.57 + 51120
|
||||
const AD_BOUNDARY: f64 = 205895.10;
|
||||
|
||||
/// 二次方程求解器
|
||||
/// -377.8*x^2 + 26040*x + 51120 = ad
|
||||
/// 返回 x > 6.57 的那个解
|
||||
fn solve_quadratic(ad: f64) -> Option<f64> {
|
||||
let a = -377.8;
|
||||
let b = 26040.0;
|
||||
let c = 51120.0 - ad;
|
||||
|
||||
let discriminant = b * b - 4.0 * a * c;
|
||||
if discriminant < 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sqrt_d = discriminant.sqrt();
|
||||
let x1 = (-b + sqrt_d) / (2.0 * a);
|
||||
let x2 = (-b - sqrt_d) / (2.0 * a);
|
||||
|
||||
// 选择 x > 6.57 的解(只可能有一个解满足这个条件)
|
||||
if x1 > X_BOUNDARY && x1 > 0.0 {
|
||||
Some(x1)
|
||||
} else if x2 > X_BOUNDARY && x2 > 0.0 {
|
||||
Some(x2)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算三次多项式的值
|
||||
/// f(x) = -5.732*x^3 - 131.5*x^2 + 31980*x + 13490
|
||||
fn cubic_value(x: f64) -> f64 {
|
||||
-5.732 * x.powi(3) - 131.5 * x.powi(2) + 31980.0 * x + 13490.0
|
||||
}
|
||||
|
||||
/// 使用二分法求解三次方程 (x <= 6.57)
|
||||
/// 三次方程在 [0, 6.57] 范围内是单调递增的
|
||||
fn solve_cubic_bisection(ad: f64) -> Option<f64> {
|
||||
let mut low = 0.0;
|
||||
let mut high = X_BOUNDARY;
|
||||
|
||||
let target = ad;
|
||||
|
||||
// 检查目标是否在范围内
|
||||
let low_ad = cubic_value(low);
|
||||
let high_ad = cubic_value(high);
|
||||
|
||||
if target < low_ad.min(high_ad) || target > low_ad.max(high_ad) {
|
||||
return None;
|
||||
}
|
||||
|
||||
for _i in 0..100 {
|
||||
let mid = (low + high) / 2.0;
|
||||
let mid_ad = cubic_value(mid);
|
||||
|
||||
if (high - low).abs() < 1e-10 {
|
||||
return Some((low + high) / 2.0);
|
||||
}
|
||||
|
||||
if mid_ad > target {
|
||||
high = mid;
|
||||
} else {
|
||||
low = mid;
|
||||
}
|
||||
}
|
||||
|
||||
Some((low + high) / 2.0)
|
||||
}
|
||||
|
||||
/// 主求解函数:根据AD值反解x
|
||||
pub fn solve_for_x(ad: f64) -> Option<f64> {
|
||||
// 如果 AD <= 边界值,使用三次方程 (x <= 6.57)
|
||||
// 如果 AD > 边界值,使用二次方程 (x > 6.57)
|
||||
if ad <= AD_BOUNDARY {
|
||||
return solve_cubic_bisection(ad);
|
||||
}
|
||||
|
||||
// AD > 边界值,使用二次方程
|
||||
solve_quadratic(ad)
|
||||
}
|
||||
|
||||
/// 批量求解,用于验证所有解
|
||||
pub fn solve_for_x_all(ad: f64) -> Vec<f64> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
// 三次方程解
|
||||
if let Some(x) = solve_cubic_bisection(ad) {
|
||||
results.push(x);
|
||||
}
|
||||
|
||||
// 二次方程解
|
||||
if let Some(x) = solve_quadratic(ad) {
|
||||
results.push(x);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cubic_forward() {
|
||||
// 测试 x <= 6.57 的正向计算
|
||||
let x = 5.0;
|
||||
let ad = cubic_value(x);
|
||||
println!("x={}, ad={}", x, ad);
|
||||
let solved = solve_for_x(ad).unwrap();
|
||||
println!("solved={}", solved);
|
||||
assert!((solved - x).abs() < 0.01, "x={}, solved={}", x, solved);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quadratic_forward() {
|
||||
// 测试 x > 6.57 的正向计算
|
||||
let x = 10.0;
|
||||
let ad = -377.8 * x * x + 26040.0 * x + 51120.0;
|
||||
let solved = solve_for_x(ad).unwrap();
|
||||
assert!((solved - x).abs() < 0.01, "x={}, solved={}", x, solved);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_boundary() {
|
||||
// 测试边界值
|
||||
let x = 6.57;
|
||||
let ad_cubic = cubic_value(x);
|
||||
let ad_quad = -377.8 * x * x + 26040.0 * x + 51120.0;
|
||||
|
||||
println!("x=6.57 时三次方程 AD = {:.2}", ad_cubic);
|
||||
println!("x=6.57 时二次方程 AD = {:.2}", ad_quad);
|
||||
println!("边界值 AD_BOUNDARY = {:.2}", AD_BOUNDARY);
|
||||
|
||||
// 边界处两个公式应该有显著差异
|
||||
assert!((ad_cubic - ad_quad).abs() > 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_known_values() {
|
||||
// 测试一些已知值
|
||||
let test_cases = [
|
||||
(0.0, cubic_value(0.0)),
|
||||
(3.0, cubic_value(3.0)),
|
||||
(6.0, cubic_value(6.0)),
|
||||
(8.0, -377.8 * 8.0 * 8.0 + 26040.0 * 8.0 + 51120.0),
|
||||
(15.0, -377.8 * 15.0 * 15.0 + 26040.0 * 15.0 + 51120.0),
|
||||
];
|
||||
|
||||
for (x, ad) in test_cases {
|
||||
let solved = solve_for_x(ad).unwrap();
|
||||
assert!((solved - x).abs() < 0.01, "x={}, ad={}, solved={}", x, ad, solved);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,18 @@
|
||||
use crate::serial_core::codecs::hand_gateway::HandGatewayCodec;
|
||||
use crate::serial_core::codecs::tactile_a::{
|
||||
export_recording_csv, TactileACodec, TactileACsvImporter, TactileAHandler,
|
||||
};
|
||||
use crate::serial_core::error::SerialError;
|
||||
use crate::serial_core::record::CsvImporter;
|
||||
use crate::serial_core::serial::{PollMode, TactileAPollRequester};
|
||||
use crate::serial_core::{serial, TactileARecording};
|
||||
use crate::serial_core::record::{self, FingerRecording};
|
||||
use crate::serial_core::serial;
|
||||
use eskin_finger_sdk::device::EskinDevice;
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use serde::Serialize;
|
||||
use std::io::Cursor;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::{async_runtime::JoinHandle, AppHandle, Manager, State};
|
||||
use tokio_serial::{available_ports, SerialPortBuilderExt};
|
||||
use tokio_serial::available_ports;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
const DEFAULT_TACTILE_COLS: usize = 7;
|
||||
const DEFAULT_TACTILE_ROWS: usize = 12;
|
||||
const DEFAULT_TACTILE_POLL_INTERVAL_MS: u64 = 10;
|
||||
const DEFAULT_TACTILE_REPLY_TIMEOUT_MS: u64 = 140;
|
||||
|
||||
// MCU currently uses a fixed node layout. Update this list to match the firmware:
|
||||
// one entry per Config bit, containing that node's number of u16 samples.
|
||||
const DEFAULT_HAND_GATEWAY_NODE_SAMPLE_COUNTS: &[u16] = &[84, 84, 84, 84, 84, 70, 44];
|
||||
|
||||
type SharedTactileRecording = Arc<Mutex<TactileARecording>>;
|
||||
type SharedRecording = Arc<Mutex<FingerRecording>>;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -36,13 +22,6 @@ pub struct SerialConnectResponse {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum SerialProtocol {
|
||||
TactileA,
|
||||
HandGateway,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SerialExportResponse {
|
||||
@@ -79,18 +58,18 @@ struct SerialSession {
|
||||
port: String,
|
||||
cancel: CancellationToken,
|
||||
task: JoinHandle<()>,
|
||||
current_record: Option<SharedTactileRecording>,
|
||||
current_record: SharedRecording,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SerialConnectionState {
|
||||
session: Mutex<Option<SerialSession>>,
|
||||
last_record: Mutex<Option<SharedTactileRecording>>,
|
||||
last_record: Mutex<Option<SharedRecording>>,
|
||||
}
|
||||
|
||||
pub async fn shutdown_active_session(
|
||||
state: &SerialConnectionState,
|
||||
) -> Result<Option<(String, Option<SharedTactileRecording>)>, SerialError> {
|
||||
) -> Result<Option<(String, SharedRecording)>, SerialError> {
|
||||
let session = {
|
||||
let mut guard = state.session.lock().map_err(|_| SerialError::StateError)?;
|
||||
guard.take()
|
||||
@@ -110,15 +89,13 @@ pub async fn shutdown_active_session(
|
||||
let _ = task.await;
|
||||
|
||||
let frame_count = current_record
|
||||
.as_ref()
|
||||
.and_then(|record| record.lock().ok().map(|record| record.frames.len()))
|
||||
.lock()
|
||||
.map(|record| record.frames.len())
|
||||
.unwrap_or(0);
|
||||
|
||||
info!("last_record has {} frames", frame_count);
|
||||
|
||||
if let (Some(current_record), Ok(mut last_record)) =
|
||||
(current_record.as_ref(), state.last_record.lock())
|
||||
{
|
||||
if let Ok(mut last_record) = state.last_record.lock() {
|
||||
*last_record = Some(current_record.clone());
|
||||
}
|
||||
|
||||
@@ -147,7 +124,6 @@ pub fn serial_enum() -> Result<Vec<String>, SerialError> {
|
||||
pub async fn serial_connect(
|
||||
app: AppHandle,
|
||||
port: String,
|
||||
protocol: SerialProtocol,
|
||||
state: State<'_, SerialConnectionState>,
|
||||
) -> Result<SerialConnectResponse, SerialError> {
|
||||
let port_name = port.trim().to_string();
|
||||
@@ -163,84 +139,41 @@ pub async fn serial_connect(
|
||||
}
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
let current_record = match protocol {
|
||||
SerialProtocol::TactileA => Some(Arc::new(Mutex::new(TactileARecording::new()))),
|
||||
SerialProtocol::HandGateway => None,
|
||||
};
|
||||
let current_record = Arc::new(Mutex::new(FingerRecording::new()));
|
||||
let task_record = current_record.clone();
|
||||
let task_cancel = cancel.clone();
|
||||
let task_app = app.clone();
|
||||
let task_port_name = port_name.clone();
|
||||
|
||||
let port = tokio_serial::new(&port_name, 921600)
|
||||
.open_native_async()
|
||||
.map_err(|_| SerialError::OpenError)?;
|
||||
let session_started_at = Instant::now();
|
||||
|
||||
let task = tauri::async_runtime::spawn(async move {
|
||||
let run_result = match protocol {
|
||||
SerialProtocol::TactileA => {
|
||||
let codec = TactileACodec::new(DEFAULT_TACTILE_COLS, DEFAULT_TACTILE_ROWS);
|
||||
let handler = TactileAHandler;
|
||||
let poll_mode = PollMode::Enabled(Box::new(TactileAPollRequester::new(
|
||||
Duration::from_millis(DEFAULT_TACTILE_POLL_INTERVAL_MS),
|
||||
DEFAULT_TACTILE_COLS,
|
||||
DEFAULT_TACTILE_ROWS,
|
||||
Duration::from_millis(DEFAULT_TACTILE_REPLY_TIMEOUT_MS),
|
||||
)));
|
||||
|
||||
serial::run_serial_with_poll(
|
||||
task_app.clone(),
|
||||
port,
|
||||
codec,
|
||||
handler,
|
||||
session_started_at,
|
||||
task_record
|
||||
.clone()
|
||||
.expect("tactile protocol must have a recording"),
|
||||
task_cancel,
|
||||
poll_mode,
|
||||
)
|
||||
.await
|
||||
}
|
||||
SerialProtocol::HandGateway => {
|
||||
let codec = HandGatewayCodec::new(DEFAULT_HAND_GATEWAY_NODE_SAMPLE_COUNTS);
|
||||
serial::run_hand_gateway_serial(
|
||||
task_app.clone(),
|
||||
port,
|
||||
codec,
|
||||
session_started_at,
|
||||
task_cancel,
|
||||
)
|
||||
.await
|
||||
// Open device using SDK
|
||||
let session = match serial::open_device(&task_port_name) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to open device: {e}");
|
||||
cleanup_session(&task_app, &task_port_name, task_record).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(error) = run_result {
|
||||
let mut device = session.device;
|
||||
|
||||
// Run stream with recording
|
||||
if let Err(error) = serial::run_stream_with_record(
|
||||
task_app.clone(),
|
||||
&mut device,
|
||||
task_cancel,
|
||||
task_record.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("serial task exited with error: {error}");
|
||||
}
|
||||
|
||||
let manager = task_app.state::<SerialConnectionState>();
|
||||
if let (Some(task_record), Ok(mut last_record)) = (task_record, manager.last_record.lock())
|
||||
{
|
||||
*last_record = Some(task_record);
|
||||
}
|
||||
// Close device
|
||||
let _ = device.close();
|
||||
|
||||
let mut session = match manager.session.lock() {
|
||||
Ok(session) => session,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
{
|
||||
let should_clear = session
|
||||
.as_ref()
|
||||
.map(|current| current.port.as_str() == task_port_name.as_str())
|
||||
.unwrap_or(false);
|
||||
|
||||
if should_clear {
|
||||
session.take();
|
||||
}
|
||||
}
|
||||
cleanup_session(&task_app, &task_port_name, task_record).await;
|
||||
});
|
||||
|
||||
let mut session = state.session.lock().map_err(|_| SerialError::StateError)?;
|
||||
@@ -264,6 +197,31 @@ pub async fn serial_connect(
|
||||
})
|
||||
}
|
||||
|
||||
async fn cleanup_session(
|
||||
app: &AppHandle,
|
||||
port_name: &str,
|
||||
record: SharedRecording,
|
||||
) {
|
||||
let manager = app.state::<SerialConnectionState>();
|
||||
if let Ok(mut last_record) = manager.last_record.lock() {
|
||||
*last_record = Some(record);
|
||||
}
|
||||
|
||||
let mut session = match manager.session.lock() {
|
||||
Ok(session) => session,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let should_clear = session
|
||||
.as_ref()
|
||||
.map(|current| current.port.as_str() == port_name)
|
||||
.unwrap_or(false);
|
||||
|
||||
if should_clear {
|
||||
session.take();
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn serial_disconnect(
|
||||
state: State<'_, SerialConnectionState>,
|
||||
@@ -330,8 +288,8 @@ pub fn serial_export_csv_to_path(
|
||||
state: State<'_, SerialConnectionState>,
|
||||
) -> Result<SerialExportResponse, SerialError> {
|
||||
let output_path = resolve_export_path(file_path)?;
|
||||
let record = resolve_record_for_export(&state)?;
|
||||
let frame_count = write_record_to_csv(record, &output_path)?;
|
||||
let rec = resolve_record_for_export(&state)?;
|
||||
let frame_count = write_record_to_csv(rec, &output_path)?;
|
||||
let path = output_path.display().to_string();
|
||||
|
||||
info!("csv exported to {path}, frame_count={frame_count}");
|
||||
@@ -348,22 +306,20 @@ pub fn serial_import_csv(
|
||||
file_name: String,
|
||||
csv_content: String,
|
||||
) -> Result<SerialImportResponse, SerialError> {
|
||||
let mut importer = TactileACsvImporter::new(file_name.as_str());
|
||||
let packets = importer
|
||||
.load(Cursor::new(csv_content.into_bytes()))
|
||||
let packets = record::import_csv(Cursor::new(csv_content.into_bytes()))
|
||||
.map_err(|_| SerialError::ImportError)?;
|
||||
|
||||
if packets.is_empty() {
|
||||
return Err(SerialError::NoRecordedData);
|
||||
}
|
||||
|
||||
let channel_count = packets.first().map(|item| item.data.len()).unwrap_or(0);
|
||||
let channel_count = 1; // fz is a single value per sample
|
||||
let frame_count = packets.len();
|
||||
let frames = packets
|
||||
.into_iter()
|
||||
.map(|packet| SerialImportFrame {
|
||||
data: packet.data,
|
||||
dts_ms: packet.dts_ms,
|
||||
data: vec![packet.fz as i32],
|
||||
dts_ms: packet.timestamp_us / 1000,
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -392,12 +348,12 @@ pub fn serial_import_csv_from_path(file_path: String) -> Result<SerialImportResp
|
||||
|
||||
fn resolve_record_for_export(
|
||||
state: &State<'_, SerialConnectionState>,
|
||||
) -> Result<SharedTactileRecording, SerialError> {
|
||||
) -> Result<SharedRecording, SerialError> {
|
||||
let current_record = {
|
||||
let session = state.session.lock().map_err(|_| SerialError::StateError)?;
|
||||
session
|
||||
.as_ref()
|
||||
.and_then(|current_session| current_session.current_record.clone())
|
||||
.map(|current_session| current_session.current_record.clone())
|
||||
};
|
||||
|
||||
if let Some(recording) = current_record {
|
||||
@@ -418,7 +374,7 @@ fn snapshot_record_frame_count(
|
||||
let session = state.session.lock().map_err(|_| SerialError::StateError)?;
|
||||
session
|
||||
.as_ref()
|
||||
.and_then(|current_session| current_session.current_record.clone())
|
||||
.map(|current_session| current_session.current_record.clone())
|
||||
};
|
||||
|
||||
if let Some(record) = current_record {
|
||||
@@ -443,7 +399,7 @@ fn snapshot_record_frame_count(
|
||||
}
|
||||
|
||||
fn write_record_to_csv(
|
||||
record: SharedTactileRecording,
|
||||
record: SharedRecording,
|
||||
output_path: &Path,
|
||||
) -> Result<usize, SerialError> {
|
||||
if let Some(parent) = output_path.parent() {
|
||||
@@ -452,14 +408,14 @@ fn write_record_to_csv(
|
||||
}
|
||||
}
|
||||
|
||||
let mut file = File::create(output_path).map_err(|_| SerialError::ExportError)?;
|
||||
let file = std::fs::File::create(output_path).map_err(|_| SerialError::ExportError)?;
|
||||
let frame_count = {
|
||||
let recording = record.lock().map_err(|_| SerialError::StateError)?;
|
||||
if recording.frames.is_empty() {
|
||||
return Err(SerialError::NoRecordedData);
|
||||
}
|
||||
|
||||
export_recording_csv(&recording, &mut file).map_err(|_| SerialError::ExportError)?;
|
||||
record::export_recording_csv(&recording, file).map_err(|_| SerialError::ExportError)?;
|
||||
recording.frames.len()
|
||||
};
|
||||
|
||||
@@ -505,4 +461,4 @@ fn resolve_absolute_path(raw_path: &str) -> std::io::Result<PathBuf> {
|
||||
} else {
|
||||
Ok(std::env::current_dir()?.join(path))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,16 +278,10 @@ async fn run_grpc_upload(
|
||||
angle: message.angle,
|
||||
};
|
||||
::log::debug!(
|
||||
"devkit: angle={:.2}, magnitude={:.4}, state={}, cop_x={:.4}, cop_y={:.4}, base_x={:.4}, base_y={:.4}, total_press={:.2}, thresh={:.2}",
|
||||
message.angle,
|
||||
message.magnitude,
|
||||
message.state,
|
||||
message.cop_x,
|
||||
message.cop_y,
|
||||
message.base_x,
|
||||
message.base_y,
|
||||
message.total_press,
|
||||
message.threshold
|
||||
"python pzt angle: seq={} dts_ms={} angle={:.2}",
|
||||
message.seq,
|
||||
message.dts_ms,
|
||||
message.angle
|
||||
);
|
||||
app.emit("devkit_pzt_angle", payload)?;
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod ad_solver;
|
||||
mod commands;
|
||||
mod lan_game;
|
||||
pub mod log;
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
use crate::serial_core::error::CodecError;
|
||||
use std::time::Instant;
|
||||
pub trait Codec<F> {
|
||||
fn decode(&mut self, input: &[u8], session_started_at: Instant) -> Result<Vec<F>, CodecError>;
|
||||
fn encode(&self, frame: &F) -> Result<Vec<u8>, CodecError>;
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
use crate::serial_core::codec::Codec;
|
||||
use crate::serial_core::error::CodecError;
|
||||
use crate::serial_core::frame::{
|
||||
HandGatewayDataRepFrame, HandGatewayFrame, HandGatewayFrameMetaData, HandGatewayFrameNode,
|
||||
};
|
||||
use crate::serial_core::utils::{calc_crc8_itu, elapsed_millis};
|
||||
use log::debug;
|
||||
use std::time::Instant;
|
||||
|
||||
const FRAME_HEADER: [u8; 2] = [0x55, 0xAA];
|
||||
const RESPONSE_DATA_CMD: u8 = 0x81;
|
||||
const RESPONSE_DATA_FIXED_LENGTH: usize = 16;
|
||||
const RESPONSE_DATA_PREFIX_LENGTH: usize = 19;
|
||||
const MIN_FRAME_LENGTH: usize = 7;
|
||||
const DEFAULT_MAX_FRAME_LENGTH: usize = 64 * 1024;
|
||||
|
||||
/// Describes one fixed MCU data block.
|
||||
///
|
||||
/// `config_mask` identifies the bit associated with this block in the protocol's
|
||||
/// Config/ValidConfig fields. `sample_count` is the number of little-endian u16
|
||||
/// samples in the block.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HandGatewayNodeConfig {
|
||||
pub config_mask: u32,
|
||||
pub sample_count: usize,
|
||||
}
|
||||
|
||||
impl HandGatewayNodeConfig {
|
||||
pub const fn new(config_mask: u32, sample_count: usize) -> Self {
|
||||
Self {
|
||||
config_mask,
|
||||
sample_count,
|
||||
}
|
||||
}
|
||||
|
||||
fn payload_len(&self, bytes_per_sample: usize) -> Option<usize> {
|
||||
self.sample_count.checked_mul(bytes_per_sample)
|
||||
}
|
||||
}
|
||||
|
||||
/// Host-side description of the configuration currently hard-coded in the MCU.
|
||||
///
|
||||
/// Keep the node order identical to the MCU Config bit order. When the MCU gains
|
||||
/// GET_CONFIG support, construct this value from that response instead of changing
|
||||
/// the decoder.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HandGatewayConfig {
|
||||
pub protocol_version: u8,
|
||||
pub bytes_per_sample: usize,
|
||||
pub max_frame_length: usize,
|
||||
pub nodes: Vec<HandGatewayNodeConfig>,
|
||||
}
|
||||
|
||||
impl HandGatewayConfig {
|
||||
pub fn new(nodes: Vec<HandGatewayNodeConfig>) -> Self {
|
||||
Self {
|
||||
protocol_version: 0x01,
|
||||
bytes_per_sample: 2,
|
||||
max_frame_length: DEFAULT_MAX_FRAME_LENGTH,
|
||||
nodes,
|
||||
}
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<(), CodecError> {
|
||||
if self.bytes_per_sample == 0 || self.max_frame_length < MIN_FRAME_LENGTH {
|
||||
return Err(CodecError::InvalidLength);
|
||||
}
|
||||
|
||||
let mut used_masks = 0u32;
|
||||
for node in &self.nodes {
|
||||
if node.config_mask == 0
|
||||
|| node.config_mask.count_ones() != 1
|
||||
|| used_masks & node.config_mask != 0
|
||||
|| node.payload_len(self.bytes_per_sample).is_none()
|
||||
{
|
||||
return Err(CodecError::InvalidLength);
|
||||
}
|
||||
used_masks |= node.config_mask;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HandGatewayCodec {
|
||||
buffer: Vec<u8>,
|
||||
config: HandGatewayConfig,
|
||||
}
|
||||
|
||||
impl HandGatewayCodec {
|
||||
/// Convenience constructor for sequential Config bits (bit 0, bit 1, ...).
|
||||
/// Each entry is the u16 sample count of one MCU node.
|
||||
pub fn new(node_sample_counts: &[u16]) -> Self {
|
||||
let nodes = node_sample_counts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, &sample_count)| {
|
||||
let config_mask = 1u32.checked_shl(index as u32).unwrap_or(0);
|
||||
HandGatewayNodeConfig::new(config_mask, usize::from(sample_count))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
buffer: Vec::new(),
|
||||
config: HandGatewayConfig::new(nodes),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_config(config: HandGatewayConfig) -> Result<Self, CodecError> {
|
||||
config.validate()?;
|
||||
Ok(Self {
|
||||
buffer: Vec::new(),
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn config(&self) -> &HandGatewayConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
pub fn parse_node_payload(data: &[u8]) -> Result<Vec<u16>, CodecError> {
|
||||
if data.len() % 2 != 0 {
|
||||
return Err(CodecError::InvalidLength);
|
||||
}
|
||||
|
||||
Ok(data
|
||||
.chunks_exact(2)
|
||||
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn retain_possible_header_prefix(&mut self) {
|
||||
if self.buffer.last() == Some(&FRAME_HEADER[0]) {
|
||||
self.buffer.drain(..self.buffer.len() - 1);
|
||||
} else {
|
||||
self.buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_data_response(
|
||||
&self,
|
||||
frame: &[u8],
|
||||
length: u16,
|
||||
session_started_at: Instant,
|
||||
) -> Result<HandGatewayFrame, CodecError> {
|
||||
let payload_len = usize::from(length)
|
||||
.checked_sub(RESPONSE_DATA_FIXED_LENGTH)
|
||||
.ok_or(CodecError::InvalidLength)?;
|
||||
let payload_end = RESPONSE_DATA_PREFIX_LENGTH
|
||||
.checked_add(payload_len)
|
||||
.ok_or(CodecError::PayloadTooLarge)?;
|
||||
|
||||
if payload_end + 1 != frame.len() {
|
||||
return Err(CodecError::InvalidLength);
|
||||
}
|
||||
|
||||
let version = frame[5];
|
||||
if version != self.config.protocol_version {
|
||||
return Err(CodecError::InvalidFrameType);
|
||||
}
|
||||
|
||||
let timestamp = u32::from_le_bytes(frame[6..10].try_into().unwrap());
|
||||
let config = u32::from_le_bytes(frame[10..14].try_into().unwrap());
|
||||
let valid_config = u32::from_le_bytes(frame[14..18].try_into().unwrap());
|
||||
let block_count = frame[18];
|
||||
|
||||
let active_nodes: Vec<&HandGatewayNodeConfig> = self
|
||||
.config
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|node| config & node.config_mask != 0)
|
||||
.collect();
|
||||
|
||||
if active_nodes.len() != usize::from(block_count) {
|
||||
return Err(CodecError::InvalidLength);
|
||||
}
|
||||
|
||||
let expected_payload_len = active_nodes.iter().try_fold(0usize, |total, node| {
|
||||
let node_len = node
|
||||
.payload_len(self.config.bytes_per_sample)
|
||||
.ok_or(CodecError::PayloadTooLarge)?;
|
||||
total
|
||||
.checked_add(node_len)
|
||||
.ok_or(CodecError::PayloadTooLarge)
|
||||
})?;
|
||||
|
||||
if expected_payload_len != payload_len {
|
||||
return Err(CodecError::InvalidLength);
|
||||
}
|
||||
|
||||
let mut cursor = RESPONSE_DATA_PREFIX_LENGTH;
|
||||
let mut nodes = Vec::with_capacity(active_nodes.len());
|
||||
for node in active_nodes {
|
||||
let node_len = node
|
||||
.payload_len(self.config.bytes_per_sample)
|
||||
.ok_or(CodecError::PayloadTooLarge)?;
|
||||
let next = cursor + node_len;
|
||||
nodes.push(HandGatewayFrameNode {
|
||||
config_mask: node.config_mask,
|
||||
valid: valid_config & node.config_mask != 0,
|
||||
payload: frame[cursor..next].to_vec(),
|
||||
});
|
||||
cursor = next;
|
||||
}
|
||||
|
||||
Ok(HandGatewayFrame::DataRep(HandGatewayDataRepFrame {
|
||||
meta: HandGatewayFrameMetaData {
|
||||
header: FRAME_HEADER,
|
||||
length,
|
||||
cmd: RESPONSE_DATA_CMD,
|
||||
version,
|
||||
checksum: frame[frame.len() - 1],
|
||||
},
|
||||
timestamp,
|
||||
config,
|
||||
valid_config,
|
||||
block_count,
|
||||
nodes,
|
||||
dts_ms: elapsed_millis(session_started_at),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Codec<HandGatewayFrame> for HandGatewayCodec {
|
||||
fn decode(
|
||||
&mut self,
|
||||
input: &[u8],
|
||||
session_started_at: Instant,
|
||||
) -> Result<Vec<HandGatewayFrame>, CodecError> {
|
||||
self.config.validate()?;
|
||||
self.buffer.extend_from_slice(input);
|
||||
let mut frames = Vec::new();
|
||||
|
||||
loop {
|
||||
let Some(header_pos) = self
|
||||
.buffer
|
||||
.windows(FRAME_HEADER.len())
|
||||
.position(|window| window == FRAME_HEADER)
|
||||
else {
|
||||
self.retain_possible_header_prefix();
|
||||
break;
|
||||
};
|
||||
|
||||
if header_pos > 0 {
|
||||
self.buffer.drain(..header_pos);
|
||||
}
|
||||
|
||||
if self.buffer.len() < 4 {
|
||||
break;
|
||||
}
|
||||
|
||||
let length = u16::from_le_bytes([self.buffer[2], self.buffer[3]]);
|
||||
let frame_len = usize::from(length)
|
||||
.checked_add(4)
|
||||
.ok_or(CodecError::PayloadTooLarge)?;
|
||||
|
||||
if frame_len < MIN_FRAME_LENGTH || frame_len > self.config.max_frame_length {
|
||||
debug!("invalid hand gateway frame length: {frame_len}");
|
||||
self.buffer.drain(..1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if self.buffer.len() < frame_len {
|
||||
break;
|
||||
}
|
||||
|
||||
let expected_checksum = calc_crc8_itu(&self.buffer[..frame_len - 1]);
|
||||
let received_checksum = self.buffer[frame_len - 1];
|
||||
if expected_checksum != received_checksum {
|
||||
debug!(
|
||||
"hand gateway checksum mismatch: expected {expected_checksum:02X}, got {received_checksum:02X}"
|
||||
);
|
||||
self.buffer.drain(..1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if self.buffer[4] == RESPONSE_DATA_CMD {
|
||||
match self.parse_data_response(
|
||||
&self.buffer[..frame_len],
|
||||
length,
|
||||
session_started_at,
|
||||
) {
|
||||
Ok(frame) => frames.push(frame),
|
||||
Err(error) => debug!("invalid hand gateway data response: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
self.buffer.drain(..frame_len);
|
||||
}
|
||||
|
||||
Ok(frames)
|
||||
}
|
||||
|
||||
fn encode(&self, _frame: &HandGatewayFrame) -> Result<Vec<u8>, CodecError> {
|
||||
Err(CodecError::InvalidFrameType)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_codec() -> HandGatewayCodec {
|
||||
HandGatewayCodec::with_config(HandGatewayConfig::new(vec![
|
||||
HandGatewayNodeConfig::new(1 << 0, 2),
|
||||
HandGatewayNodeConfig::new(1 << 1, 1),
|
||||
]))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn data_response(config: u32, valid_config: u32, payload: &[u8]) -> Vec<u8> {
|
||||
let block_count = config.count_ones() as u8;
|
||||
let length = (RESPONSE_DATA_FIXED_LENGTH + payload.len()) as u16;
|
||||
let mut frame = Vec::new();
|
||||
frame.extend_from_slice(&FRAME_HEADER);
|
||||
frame.extend_from_slice(&length.to_le_bytes());
|
||||
frame.push(RESPONSE_DATA_CMD);
|
||||
frame.push(0x01);
|
||||
frame.extend_from_slice(&123_456u32.to_le_bytes());
|
||||
frame.extend_from_slice(&config.to_le_bytes());
|
||||
frame.extend_from_slice(&valid_config.to_le_bytes());
|
||||
frame.push(block_count);
|
||||
frame.extend_from_slice(payload);
|
||||
frame.push(calc_crc8_itu(&frame));
|
||||
frame
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_configured_nodes() {
|
||||
let mut codec = test_codec();
|
||||
let bytes = data_response(0b11, 0b01, &[1, 0, 2, 0, 3, 0]);
|
||||
let frames = codec.decode(&bytes, Instant::now()).unwrap();
|
||||
|
||||
let HandGatewayFrame::DataRep(frame) = &frames[0];
|
||||
assert_eq!(frame.timestamp, 123_456);
|
||||
assert_eq!(frame.block_count, 2);
|
||||
assert_eq!(frame.nodes.len(), 2);
|
||||
assert_eq!(frame.nodes[0].config_mask, 1);
|
||||
assert!(frame.nodes[0].valid);
|
||||
assert_eq!(
|
||||
HandGatewayCodec::parse_node_payload(&frame.nodes[0].payload).unwrap(),
|
||||
[1, 2]
|
||||
);
|
||||
assert!(!frame.nodes[1].valid);
|
||||
assert_eq!(
|
||||
HandGatewayCodec::parse_node_payload(&frame.nodes[1].payload).unwrap(),
|
||||
[3]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_split_and_sticky_packets() {
|
||||
let mut codec = test_codec();
|
||||
let frame = data_response(0b01, 0b01, &[1, 0, 2, 0]);
|
||||
let split = frame.len() / 2;
|
||||
|
||||
assert!(codec
|
||||
.decode(&frame[..split], Instant::now())
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
|
||||
let mut remainder_and_next = frame[split..].to_vec();
|
||||
remainder_and_next.extend_from_slice(&frame);
|
||||
let frames = codec.decode(&remainder_and_next, Instant::now()).unwrap();
|
||||
assert_eq!(frames.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resynchronizes_after_noise_and_bad_crc() {
|
||||
let mut codec = test_codec();
|
||||
let valid = data_response(0b10, 0b10, &[9, 0]);
|
||||
let mut bad = valid.clone();
|
||||
let last = bad.len() - 1;
|
||||
bad[last] ^= 0xFF;
|
||||
|
||||
let mut bytes = vec![0x13, 0x55, 0x12];
|
||||
bytes.extend_from_slice(&bad);
|
||||
bytes.extend_from_slice(&valid);
|
||||
|
||||
let frames = codec.decode(&bytes, Instant::now()).unwrap();
|
||||
assert_eq!(frames.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_payload_that_does_not_match_fixed_config() {
|
||||
let mut codec = test_codec();
|
||||
let bytes = data_response(0b01, 0b01, &[1, 0]);
|
||||
assert!(codec.decode(&bytes, Instant::now()).unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
use crate::serial_core::{frame::TestFrame, record::Recording};
|
||||
|
||||
pub mod test;
|
||||
pub mod tactile_a;
|
||||
pub mod hand_gateway;
|
||||
pub type TestRecording = Recording<TestFrame>;
|
||||
@@ -1,383 +0,0 @@
|
||||
use crate::serial_core::error::CodecError;
|
||||
use crate::serial_core::frame::{
|
||||
FrameHandler, TactileAFrameMetaData, TactileARepFrame, TactileAReqFrame,
|
||||
};
|
||||
use crate::serial_core::record::{write_csv, CsvExporter, CsvImporter, RecordedFrame, Recording};
|
||||
use crate::serial_core::utils::{calc_crc8_itu, elapsed_millis};
|
||||
use crate::serial_core::{
|
||||
codec::Codec,
|
||||
frame::{TactileAFrame, TactileAFrameStatusCode},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use csv::StringRecord;
|
||||
use anyhow::anyhow;
|
||||
use std::io::Read;
|
||||
use log::debug;
|
||||
|
||||
const FINGER_FRAME_BUFFER_MIN_LENGTH: usize = 15;
|
||||
|
||||
pub struct TactileACodec {
|
||||
buffer: Vec<u8>,
|
||||
expected_data_len: usize,
|
||||
}
|
||||
|
||||
pub struct TactileACsvExporter {
|
||||
channels: usize,
|
||||
}
|
||||
|
||||
pub struct TactileACsvImporter {
|
||||
channels: usize,
|
||||
data_row: usize,
|
||||
packets: Vec<TactileADataPacket>,
|
||||
}
|
||||
|
||||
pub struct TactileAHandler;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TactileADataPacket {
|
||||
pub data: Vec<i32>,
|
||||
pub dts_ms: u64,
|
||||
}
|
||||
|
||||
impl From<u8> for TactileAFrameStatusCode {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
0 => TactileAFrameStatusCode::Success,
|
||||
_ => TactileAFrameStatusCode::Failure,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&TactileARepFrame> for TactileADataPacket {
|
||||
type Error = CodecError;
|
||||
fn try_from(value: &TactileARepFrame) -> Result<TactileADataPacket, Self::Error> {
|
||||
let data = TactileACodec::parse_data_frame(&value.payload)?;
|
||||
let dts_ms = value.dts_ms;
|
||||
Ok(TactileADataPacket {
|
||||
data: data,
|
||||
dts_ms: dts_ms,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TactileACodec {
|
||||
pub fn new(cols: usize, rows: usize) -> TactileACodec {
|
||||
Self {
|
||||
buffer: Vec::new(),
|
||||
expected_data_len: cols * rows * 2,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_data_frame(data: &[u8]) -> Result<Vec<i32>, CodecError> {
|
||||
if data.len() % 2 != 0 {
|
||||
return Err(CodecError::InvalidLength);
|
||||
}
|
||||
|
||||
let vals: Vec<i32> = data
|
||||
.chunks_exact(2)
|
||||
.map(|chunk| {
|
||||
let raw = u16::from_le_bytes([chunk[0], chunk[1]]) as i32;
|
||||
raw
|
||||
// if raw < 15 {
|
||||
// 0
|
||||
// } else {
|
||||
// raw
|
||||
// }
|
||||
})
|
||||
.collect::<Vec<i32>>();
|
||||
|
||||
Ok(vals)
|
||||
}
|
||||
|
||||
pub fn build_req_frame(cols: usize, rows: usize) -> anyhow::Result<TactileAFrame> {
|
||||
let header = [0x55, 0xAA];
|
||||
let payload_len: usize = 9;
|
||||
let device_addr: u8 = 0x34;
|
||||
let extend_code: u8 = 0x00;
|
||||
let func_code: u8 = 0xFB;
|
||||
let start_addr: u32 = 7168;
|
||||
let except_data_len: usize = cols * rows * 2;
|
||||
let checksum: u8 = 0;
|
||||
Ok(TactileAFrame::Req(TactileAReqFrame {
|
||||
meta: TactileAFrameMetaData {
|
||||
header,
|
||||
payload_len,
|
||||
device_addr,
|
||||
extend_code,
|
||||
func_code,
|
||||
start_addr,
|
||||
except_data_len,
|
||||
checksum,
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Codec<TactileAFrame> for TactileACodec {
|
||||
fn decode(
|
||||
&mut self,
|
||||
input: &[u8],
|
||||
session_started_at: std::time::Instant,
|
||||
) -> Result<Vec<TactileAFrame>, CodecError> {
|
||||
self.buffer.extend_from_slice(input);
|
||||
let mut frames: Vec<TactileAFrame> = Vec::new();
|
||||
|
||||
loop {
|
||||
if self.buffer.len() < FINGER_FRAME_BUFFER_MIN_LENGTH {
|
||||
break;
|
||||
}
|
||||
|
||||
let header_pos = self.buffer.windows(2).position(|w| w == [0xAA, 0x55]);
|
||||
|
||||
let Some(pos) = header_pos else {
|
||||
self.buffer.clear();
|
||||
break;
|
||||
};
|
||||
if pos > 0 {
|
||||
self.buffer.drain(0..pos);
|
||||
}
|
||||
|
||||
if self.buffer.len() < FINGER_FRAME_BUFFER_MIN_LENGTH {
|
||||
break;
|
||||
}
|
||||
|
||||
let header = [self.buffer[0], self.buffer[1]];
|
||||
let payload_len = u16::from_le_bytes([self.buffer[2], self.buffer[3]]) as usize;
|
||||
let device_addr = self.buffer[4];
|
||||
let extend_code = self.buffer[5];
|
||||
let func_code = self.buffer[6];
|
||||
let start_addr = u32::from_le_bytes([
|
||||
self.buffer[7],
|
||||
self.buffer[8],
|
||||
self.buffer[9],
|
||||
self.buffer[10],
|
||||
]);
|
||||
let except_data_len = u16::from_le_bytes([self.buffer[11], self.buffer[12]]) as usize;
|
||||
let status = TactileAFrameStatusCode::from(self.buffer[13]);
|
||||
if except_data_len != self.expected_data_len {
|
||||
debug!(
|
||||
"unexpected payload length: expected {}, got {}, buffer_len={}",
|
||||
self.expected_data_len,
|
||||
except_data_len,
|
||||
self.buffer.len()
|
||||
);
|
||||
self.buffer.drain(0..1);
|
||||
continue;
|
||||
}
|
||||
|
||||
let frame_length = except_data_len + FINGER_FRAME_BUFFER_MIN_LENGTH;
|
||||
if self.buffer.len() < frame_length {
|
||||
break;
|
||||
}
|
||||
|
||||
let need_check_data = self.buffer[0..14 + except_data_len].to_vec();
|
||||
let payload = self.buffer[14..14 + except_data_len].to_vec();
|
||||
let crc8_itu_alg = crc::Crc::<u8>::new(&crc::CRC_8_I_432_1);
|
||||
let checksum = crc8_itu_alg.checksum(&need_check_data.as_slice());
|
||||
if self.buffer[frame_length - 1] != checksum {
|
||||
debug!(
|
||||
"checksum mismatch: expected {:02X}, got {:02X}, frame_len={}",
|
||||
checksum,
|
||||
self.buffer[frame_length - 1],
|
||||
frame_length
|
||||
);
|
||||
self.buffer.drain(0..1);
|
||||
continue;
|
||||
}
|
||||
let dts_ms = elapsed_millis(session_started_at);
|
||||
let meta: TactileAFrameMetaData = TactileAFrameMetaData {
|
||||
header,
|
||||
payload_len,
|
||||
device_addr,
|
||||
extend_code,
|
||||
func_code,
|
||||
start_addr,
|
||||
except_data_len,
|
||||
checksum,
|
||||
};
|
||||
frames.push(TactileAFrame::Rep({
|
||||
TactileARepFrame {
|
||||
meta,
|
||||
status,
|
||||
payload,
|
||||
dts_ms,
|
||||
}
|
||||
}));
|
||||
|
||||
self.buffer.drain(0..frame_length);
|
||||
}
|
||||
|
||||
Ok(frames)
|
||||
}
|
||||
|
||||
fn encode(
|
||||
&self,
|
||||
frame: &TactileAFrame,
|
||||
) -> Result<Vec<u8>, crate::serial_core::error::CodecError> {
|
||||
match frame {
|
||||
TactileAFrame::Req(f) => {
|
||||
let mut req_bytes: Vec<u8> = Vec::new();
|
||||
req_bytes.extend_from_slice(f.meta.header.as_slice());
|
||||
req_bytes.extend_from_slice((f.meta.payload_len as u16).to_le_bytes().as_slice());
|
||||
req_bytes.push(f.meta.device_addr);
|
||||
req_bytes.push(f.meta.extend_code);
|
||||
req_bytes.push(f.meta.func_code);
|
||||
|
||||
req_bytes.extend_from_slice(f.meta.start_addr.to_le_bytes().as_slice());
|
||||
req_bytes.extend_from_slice((f.meta.except_data_len as u16).to_le_bytes().as_slice());
|
||||
let checksum = calc_crc8_itu(req_bytes.as_slice());
|
||||
req_bytes.push(checksum);
|
||||
Ok(req_bytes)
|
||||
}
|
||||
_ => {
|
||||
Err(CodecError::InvalidFrameType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FrameHandler<TactileAFrame, i32> for TactileAHandler {
|
||||
async fn on_frame(&mut self, frame: &TactileAFrame) -> anyhow::Result<Option<Vec<i32>>> {
|
||||
match frame {
|
||||
TactileAFrame::Rep(rep) => {
|
||||
let vals = TactileACodec::parse_data_frame(&rep.payload)?;
|
||||
Ok(Some(vals))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TactileACsvExporter {
|
||||
fn new(channels: usize) -> Self {
|
||||
TactileACsvExporter { channels }
|
||||
}
|
||||
}
|
||||
|
||||
impl CsvExporter<TactileARepFrame> for TactileACsvExporter {
|
||||
type Error = CodecError;
|
||||
fn csv_header(&self, _recording: &Recording<TactileARepFrame>) -> Vec<String> {
|
||||
let mut header: Vec<String> = Vec::new();
|
||||
for i in 0..self.channels {
|
||||
header.push(format!("channel{}", i + 1));
|
||||
}
|
||||
|
||||
header.push("dts".to_string());
|
||||
header.push("summary".to_string());
|
||||
header
|
||||
}
|
||||
|
||||
fn csv_row(
|
||||
&self,
|
||||
item: &RecordedFrame<TactileARepFrame>,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
let packet = TactileADataPacket::try_from(&item.frame)?;
|
||||
let summary: i32 = packet.data.iter().sum();
|
||||
let mut row: Vec<String> = packet.data.iter().map(|x| x.to_string()).collect();
|
||||
row.push(packet.dts_ms.to_string());
|
||||
row.push(summary.to_string());
|
||||
Ok(row)
|
||||
}
|
||||
}
|
||||
|
||||
impl CsvExporter<TactileAFrame> for TactileACsvExporter {
|
||||
type Error = CodecError;
|
||||
|
||||
fn csv_header(&self, _recording: &Recording<TactileAFrame>) -> Vec<String> {
|
||||
let mut header: Vec<String> = Vec::new();
|
||||
for i in 0..self.channels {
|
||||
header.push(format!("channel{}", i + 1));
|
||||
}
|
||||
|
||||
header.push("dts".to_string());
|
||||
header
|
||||
}
|
||||
|
||||
fn csv_row(
|
||||
&self,
|
||||
item: &RecordedFrame<TactileAFrame>,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
let rep = match &item.frame {
|
||||
TactileAFrame::Rep(rep) => rep,
|
||||
TactileAFrame::Req(_) => return Err(anyhow!("request frame cannot be exported to csv row")),
|
||||
};
|
||||
|
||||
let packet = TactileADataPacket::try_from(rep)?;
|
||||
let mut row: Vec<String> = packet.data.iter().map(|x| x.to_string()).collect();
|
||||
row.push(packet.dts_ms.to_string());
|
||||
Ok(row)
|
||||
}
|
||||
}
|
||||
|
||||
impl TactileACsvImporter {
|
||||
pub fn new(_path: &str) -> TactileACsvImporter {
|
||||
Self {
|
||||
channels: 0,
|
||||
data_row: 0,
|
||||
packets: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_record(&mut self, record: StringRecord) -> anyhow::Result<TactileADataPacket> {
|
||||
if self.channels == 0 {
|
||||
return Err(anyhow!("csv header is missing channel columns"));
|
||||
}
|
||||
|
||||
if record.len() < self.channels + 1 {
|
||||
return Err(anyhow!("csv row has insufficient columns"));
|
||||
}
|
||||
|
||||
let mut data = Vec::with_capacity(self.channels);
|
||||
for index in 0..self.channels {
|
||||
let cell = record.get(index).ok_or_else(|| anyhow!("missing channel cell"))?;
|
||||
data.push(cell.parse::<i32>()?);
|
||||
}
|
||||
|
||||
let dts_cell = record
|
||||
.get(self.channels)
|
||||
.ok_or_else(|| anyhow!("missing dts cell"))?;
|
||||
let dts_ms = dts_cell.parse::<u64>()?;
|
||||
|
||||
Ok(TactileADataPacket {
|
||||
data: data,
|
||||
dts_ms: dts_ms,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CsvImporter<TactileADataPacket> for TactileACsvImporter {
|
||||
fn load<R: Read>(&mut self, reader: R) -> anyhow::Result<Vec<TactileADataPacket>> {
|
||||
let mut rdr = csv::Reader::from_reader(reader);
|
||||
let headers = rdr.headers()?.clone();
|
||||
self.channels = headers.len().saturating_sub(1);
|
||||
self.data_row = 0;
|
||||
self.packets.clear();
|
||||
|
||||
for record in rdr.records() {
|
||||
let record = record?;
|
||||
let packet = self.parse_record(record)?;
|
||||
self.packets.push(packet);
|
||||
self.data_row += 1;
|
||||
}
|
||||
|
||||
Ok(self.packets.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn export_recording_csv<W>(recording: &Recording<TactileAFrame>, writer: W) -> anyhow::Result<()>
|
||||
where
|
||||
W: std::io::Write,
|
||||
{
|
||||
let channel_nb = recording
|
||||
.frames
|
||||
.iter()
|
||||
.find_map(|frame| match &frame.frame {
|
||||
TactileAFrame::Rep(rep) => Some(rep.payload.len() / 2),
|
||||
TactileAFrame::Req(_) => None,
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
let exporter = TactileACsvExporter::new(channel_nb);
|
||||
write_csv(recording, &exporter, writer)
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
use std::io::Read;
|
||||
use std::time::Instant;
|
||||
use crate::serial_core::frame::{FrameHandler};
|
||||
use crate::serial_core::{codec::Codec, error::CodecError, frame::TestFrame};
|
||||
use anyhow::anyhow;
|
||||
use async_trait::async_trait;
|
||||
use csv::StringRecord;
|
||||
use crate::serial_core::record::{write_csv, CsvExporter, CsvImporter, RecordedFrame, Recording};
|
||||
use crate::serial_core::utils::{
|
||||
elapsed_millis,
|
||||
usize_to_u16_be_bytes
|
||||
};
|
||||
pub struct TestCodec {
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct TestHandler;
|
||||
|
||||
impl TestCodec {
|
||||
pub fn new() -> TestCodec {
|
||||
Self { buffer: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Codec<TestFrame> for TestCodec {
|
||||
fn decode(&mut self, input: &[u8], session_started_at: Instant) -> Result<Vec<TestFrame>, CodecError> {
|
||||
self.buffer.extend_from_slice(input);
|
||||
let mut frames = Vec::new();
|
||||
|
||||
loop {
|
||||
if self.buffer.len() < 6 {
|
||||
break;
|
||||
}
|
||||
|
||||
let header_pos = self.buffer.windows(2).position(|w| w == [0xAA, 0x55]);
|
||||
|
||||
let Some(pos) = header_pos else {
|
||||
self.buffer.clear();
|
||||
break;
|
||||
};
|
||||
if pos > 0 {
|
||||
self.buffer.drain(0..pos);
|
||||
}
|
||||
|
||||
if self.buffer.len() < 6 {
|
||||
break;
|
||||
}
|
||||
|
||||
let cmd = self.buffer[2];
|
||||
let length_bytes = [self.buffer[3], self.buffer[4]];
|
||||
let length = u16::from_be_bytes(length_bytes) as usize;
|
||||
let frame_length = (length + 6) as usize;
|
||||
if self.buffer.len() < frame_length {
|
||||
break;
|
||||
}
|
||||
let payload = self.buffer[5..5 + length].to_vec();
|
||||
// let checksum = crc8(payload.as_slice());
|
||||
let crc8_alg = crc::Crc::<u8>::new(&crc::CRC_8_SMBUS);
|
||||
let checksum = crc8_alg.checksum(payload.as_slice());
|
||||
if self.buffer[frame_length - 1] != checksum {
|
||||
self.buffer.drain(0..1);
|
||||
continue;
|
||||
}
|
||||
let dts = elapsed_millis(session_started_at);
|
||||
println!("dts_ms: {dts}");
|
||||
frames.push(TestFrame {
|
||||
header: [0xAA, 0x55],
|
||||
cmd: cmd,
|
||||
length: length,
|
||||
payload: payload,
|
||||
checksum: checksum,
|
||||
dts_ms: dts,
|
||||
});
|
||||
|
||||
self.buffer.drain(0..frame_length);
|
||||
}
|
||||
|
||||
Ok(frames)
|
||||
}
|
||||
fn encode(&self, frame: &TestFrame) -> Result<Vec<u8>, CodecError> {
|
||||
let _ = u16::try_from(frame.payload.len()).map_err(|_| CodecError::PayloadTooLarge)?;
|
||||
let mut out = Vec::with_capacity(6 + frame.length);
|
||||
out.extend_from_slice(&frame.header);
|
||||
out.push(frame.cmd);
|
||||
out.extend_from_slice(&usize_to_u16_be_bytes(frame.length));
|
||||
out.extend_from_slice(&frame.payload);
|
||||
out.push(frame.checksum);
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FrameHandler<TestFrame, i32> for TestHandler {
|
||||
async fn on_frame(&mut self, frame: &TestFrame) -> anyhow::Result<Option<Vec<i32>>> {
|
||||
match frame.cmd {
|
||||
0x01 => {
|
||||
let vals = parse_data_frame(&frame.payload)?;
|
||||
Ok(Some(vals))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_data_frame(data: &[u8]) -> Result<Vec<i32>, CodecError> {
|
||||
if data.len() % 2 != 0 {
|
||||
return Err(CodecError::InvalidLength);
|
||||
}
|
||||
|
||||
let vals: Vec<i32> = data
|
||||
.chunks_exact(2)
|
||||
.map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]) as i32)
|
||||
.collect::<Vec<i32>>();
|
||||
|
||||
Ok(vals)
|
||||
}
|
||||
|
||||
pub struct TestCsvExporter;
|
||||
pub struct TestCsvImporter {
|
||||
channels: usize,
|
||||
data_row: usize,
|
||||
packets: Vec<TestDataPacket>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TestDataPacket {
|
||||
pub data: Vec<i32>,
|
||||
pub dts_ms: u64
|
||||
}
|
||||
|
||||
impl TryFrom<&TestFrame> for TestDataPacket {
|
||||
type Error = CodecError;
|
||||
fn try_from(frame: &TestFrame) -> Result<TestDataPacket, Self::Error> {
|
||||
let data = parse_data_frame(&frame.payload)?;
|
||||
let dts = frame.dts_ms;
|
||||
Ok(TestDataPacket { data: data, dts_ms: dts })
|
||||
}
|
||||
}
|
||||
// impl From<TestFrame> for TestDataPacket {
|
||||
// fn from(frame: TestFrame) -> Self {
|
||||
// let data = parse_data_frame(&frame.payload)?;
|
||||
// let dts = frame.dts_ms;
|
||||
// TestDataPacket { data: data, dts_ms: dts }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
impl CsvExporter<TestFrame> for TestCsvExporter {
|
||||
type Error = CodecError;
|
||||
fn csv_header(&self, recording: &Recording<TestFrame>) -> Vec<String> {
|
||||
let channel_nb = recording
|
||||
.frames
|
||||
.iter()
|
||||
.find_map(|frame| parse_data_frame(&frame.frame.payload).ok().map(|vals| vals.len()))
|
||||
.unwrap_or(0);
|
||||
let mut header: Vec<String> = Vec::new();
|
||||
for i in 0..channel_nb {
|
||||
header.push(format!("channel{}", i + 1));
|
||||
}
|
||||
header.push("dts".to_string());
|
||||
|
||||
header
|
||||
}
|
||||
|
||||
fn csv_row(&self, item: &RecordedFrame<TestFrame>) -> anyhow::Result<Vec<String>> {
|
||||
let packet: TestDataPacket = TestDataPacket::try_from(&item.frame)?;
|
||||
let mut row: Vec<String> = packet.data.iter().map(|&x| x.to_string()).collect();
|
||||
row.push(packet.dts_ms.to_string());
|
||||
Ok(row)
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCsvImporter {
|
||||
pub fn new(_path: &str) -> TestCsvImporter {
|
||||
Self {
|
||||
channels: 0,
|
||||
data_row: 0,
|
||||
packets: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_record(&mut self, record: StringRecord) -> anyhow::Result<TestDataPacket>{
|
||||
if self.channels == 0 {
|
||||
return Err(anyhow!("csv header is missing channel columns"));
|
||||
}
|
||||
|
||||
if record.len() < self.channels + 1 {
|
||||
return Err(anyhow!("csv row has insufficient columns"));
|
||||
}
|
||||
|
||||
let mut data = Vec::with_capacity(self.channels);
|
||||
for index in 0..self.channels {
|
||||
let cell = record.get(index).ok_or_else(|| anyhow!("missing channel cell"))?;
|
||||
data.push(cell.parse::<i32>()?);
|
||||
}
|
||||
|
||||
let dts_cell = record
|
||||
.get(self.channels)
|
||||
.ok_or_else(|| anyhow!("missing dts cell"))?;
|
||||
let dts_ms = dts_cell.parse::<u64>()?;
|
||||
|
||||
Ok(TestDataPacket {
|
||||
data: data,
|
||||
dts_ms: dts_ms,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CsvImporter<TestDataPacket> for TestCsvImporter {
|
||||
fn load<R: Read>(&mut self, reader: R) -> anyhow::Result<Vec<TestDataPacket>> {
|
||||
let mut rdr = csv::Reader::from_reader(reader);
|
||||
let headers = rdr.headers()?.clone();
|
||||
self.channels = headers.len().saturating_sub(1);
|
||||
self.data_row = 0;
|
||||
self.packets.clear();
|
||||
|
||||
for record in rdr.records() {
|
||||
let record = record?;
|
||||
let packet = self.parse_record(record)?;
|
||||
self.packets.push(packet);
|
||||
self.data_row += 1;
|
||||
}
|
||||
|
||||
Ok(self.packets.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn export_recording_csv<W>(recording: &Recording<TestFrame>, writer: W) -> anyhow::Result<()>
|
||||
where
|
||||
W: std::io::Write,
|
||||
{
|
||||
write_csv(recording, &TestCsvExporter, writer)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use csv::Reader;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[test]
|
||||
fn test_read_csv_basic() -> anyhow::Result<()> {
|
||||
let mut rdr = Reader::from_path("recording_20260329_125238.csv")?;
|
||||
let headers = rdr.headers()?;
|
||||
println!("headers: {:?}", headers);
|
||||
|
||||
for result in rdr.records() {
|
||||
let record = result?;
|
||||
println!("record: {:?}", record);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TestFrame {
|
||||
pub header: [u8; 2],
|
||||
pub cmd: u8,
|
||||
pub length: usize,
|
||||
pub payload: Vec<u8>,
|
||||
pub checksum: u8,
|
||||
pub dts_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TactileAFrameMetaData {
|
||||
pub header: [u8; 2],
|
||||
pub payload_len: usize,
|
||||
pub device_addr: u8,
|
||||
pub extend_code: u8,
|
||||
pub func_code: u8,
|
||||
pub start_addr: u32,
|
||||
pub except_data_len: usize,
|
||||
// pub status: u8,
|
||||
// pub payload_data: Vec<u8>,
|
||||
pub checksum: u8,
|
||||
// pub dts_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TactileAReqFrame {
|
||||
pub meta: TactileAFrameMetaData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TactileARepFrame {
|
||||
pub meta: TactileAFrameMetaData,
|
||||
pub status: TactileAFrameStatusCode,
|
||||
pub payload: Vec<u8>,
|
||||
pub dts_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TactileAFrameStatusCode {
|
||||
Success,
|
||||
Failure,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TactileAFrame {
|
||||
Req(TactileAReqFrame),
|
||||
Rep(TactileARepFrame),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HandGatewayFrameMetaData {
|
||||
pub header: [u8; 2],
|
||||
pub length: u16,
|
||||
pub cmd: u8,
|
||||
pub version: u8,
|
||||
pub checksum: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HandGatewayFrameNode {
|
||||
pub config_mask: u32,
|
||||
pub valid: bool,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HandGatewayDataRepFrame {
|
||||
pub meta: HandGatewayFrameMetaData,
|
||||
pub timestamp: u32,
|
||||
pub config: u32,
|
||||
pub valid_config: u32,
|
||||
pub block_count: u8,
|
||||
pub nodes: Vec<HandGatewayFrameNode>,
|
||||
pub dts_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum HandGatewayFrame {
|
||||
DataRep(HandGatewayDataRepFrame),
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait FrameHandler<F, T>: Send {
|
||||
async fn on_frame(&mut self, frame: &F) -> Result<Option<Vec<T>>>;
|
||||
}
|
||||
@@ -1,34 +1,4 @@
|
||||
use crate::serial_core::{
|
||||
frame::{TactileAFrame, TestFrame},
|
||||
record::Recording,
|
||||
};
|
||||
|
||||
pub mod codec;
|
||||
pub mod codecs;
|
||||
pub mod error;
|
||||
pub mod frame;
|
||||
pub mod model;
|
||||
pub mod serial;
|
||||
pub mod record;
|
||||
pub mod utils;
|
||||
#[cfg(feature = "multi-dim")]
|
||||
pub mod multi_dim_force;
|
||||
|
||||
pub type TestRecording = Recording<TestFrame>;
|
||||
pub type TactileARecording = Recording<TactileAFrame>;
|
||||
|
||||
pub struct SerialConnection {
|
||||
pub port: String,
|
||||
}
|
||||
|
||||
pub fn connect(port: &str) -> Result<SerialConnection, String> {
|
||||
let port = port.trim();
|
||||
|
||||
if port.is_empty() {
|
||||
return Err("Serial port is required".to_string());
|
||||
}
|
||||
|
||||
Ok(SerialConnection {
|
||||
port: port.to_string(),
|
||||
})
|
||||
}
|
||||
pub mod record;
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::serial_core::frame::TestFrame;
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
const MAX_POINTS: usize = 28;
|
||||
const MAX_SUMMARY_POINTS: usize = 42;
|
||||
const PANEL_STALE_AFTER: Duration = Duration::from_millis(2400);
|
||||
|
||||
@@ -13,7 +11,6 @@ pub struct HudPacket {
|
||||
pub panels: Vec<HudSignalPanel>,
|
||||
pub summary: HudSummary,
|
||||
pub pressure_matrix: Option<Vec<f32>>,
|
||||
pub spatial_force: Option<HudSpatialForce>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
@@ -75,33 +72,19 @@ pub struct HudSignalIcon {
|
||||
pub tone: HudTone,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HudSpatialForce {
|
||||
pub angle_deg: f32,
|
||||
pub magnitude: f32,
|
||||
pub confidence: f32,
|
||||
}
|
||||
|
||||
struct HudPanelUpdate {
|
||||
source_id: String,
|
||||
values: Vec<f32>,
|
||||
}
|
||||
|
||||
struct PanelEntry {
|
||||
panel: HudSignalPanel,
|
||||
last_seen: Instant,
|
||||
}
|
||||
|
||||
pub struct HudChartState {
|
||||
panels: HashMap<String, PanelEntry>,
|
||||
order: Vec<String>,
|
||||
summary_points: Vec<f32>,
|
||||
pressure_matrix: Option<Vec<f32>>,
|
||||
spatial_force: Option<HudSpatialForce>,
|
||||
last_frame_seen: Option<Instant>,
|
||||
}
|
||||
|
||||
struct PanelEntry {
|
||||
panel: HudSignalPanel,
|
||||
last_seen: Instant,
|
||||
}
|
||||
|
||||
impl HudChartState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -109,93 +92,27 @@ impl HudChartState {
|
||||
order: Vec::new(),
|
||||
summary_points: Vec::new(),
|
||||
pressure_matrix: None,
|
||||
spatial_force: None,
|
||||
last_frame_seen: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_summary(&mut self, value: f32) {
|
||||
push_summary_point(&mut self.summary_points, value);
|
||||
self.last_frame_seen = Some(Instant::now());
|
||||
}
|
||||
|
||||
pub fn record_pressure_matrix(&mut self, values: &[i32]) {
|
||||
pub fn record_pressure_matrix(&mut self, values: &[f32]) {
|
||||
if values.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.pressure_matrix = Some(values.iter().map(|value| *value as f32).collect());
|
||||
}
|
||||
|
||||
pub fn record_spatial_force(&mut self, spatial_force: Option<HudSpatialForce>) {
|
||||
self.spatial_force = spatial_force;
|
||||
}
|
||||
|
||||
pub fn apply_frame(&mut self, frame: &TestFrame, decoded_values: Option<&[i32]>) -> HudPacket {
|
||||
let now = Instant::now();
|
||||
self.last_frame_seen = Some(now);
|
||||
|
||||
for update in expand_frame_updates(frame, decoded_values) {
|
||||
self.apply_update(update, now);
|
||||
}
|
||||
|
||||
self.prune_stale_at(now);
|
||||
self.snapshot()
|
||||
self.pressure_matrix = Some(values.to_vec());
|
||||
}
|
||||
|
||||
pub fn prune_stale(&mut self) -> Option<HudPacket> {
|
||||
let now = Instant::now();
|
||||
let before = self.panels.len();
|
||||
let summary_points_before = self.summary_points.len();
|
||||
let had_pressure_matrix = self.pressure_matrix.is_some();
|
||||
let had_spatial_force = self.spatial_force.is_some();
|
||||
self.prune_stale_at(Instant::now());
|
||||
let summary_before = self.summary_points.len();
|
||||
|
||||
if before == self.panels.len()
|
||||
&& summary_points_before == self.summary_points.len()
|
||||
&& had_pressure_matrix == self.pressure_matrix.is_some()
|
||||
&& had_spatial_force == self.spatial_force.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(self.snapshot())
|
||||
}
|
||||
|
||||
fn apply_update(&mut self, update: HudPanelUpdate, now: Instant) {
|
||||
if update.values.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if !self.panels.contains_key(&update.source_id) {
|
||||
let next_side = side_for_index(self.order.len());
|
||||
self.order.push(update.source_id.clone());
|
||||
self.panels.insert(
|
||||
update.source_id.clone(),
|
||||
PanelEntry {
|
||||
panel: build_panel(&update.source_id, next_side, update.values.len()),
|
||||
last_seen: now,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let entry = self
|
||||
.panels
|
||||
.get_mut(&update.source_id)
|
||||
.expect("panel entry should exist after insertion");
|
||||
|
||||
entry.last_seen = now;
|
||||
entry.panel.active = true;
|
||||
ensure_panel_channels(&mut entry.panel, update.values.len());
|
||||
|
||||
for (index, value) in update.values.into_iter().enumerate() {
|
||||
if let Some(series) = entry.panel.series.get_mut(index) {
|
||||
push_point(&mut series.points, value);
|
||||
}
|
||||
}
|
||||
|
||||
refresh_panel_stats(&mut entry.panel);
|
||||
}
|
||||
|
||||
fn prune_stale_at(&mut self, now: Instant) {
|
||||
self.panels
|
||||
.retain(|_, entry| now.duration_since(entry.last_seen) <= PANEL_STALE_AFTER);
|
||||
self.order.retain(|id| self.panels.contains_key(id));
|
||||
@@ -208,9 +125,18 @@ impl HudChartState {
|
||||
if summary_stale {
|
||||
self.summary_points.clear();
|
||||
self.pressure_matrix = None;
|
||||
self.spatial_force = None;
|
||||
self.last_frame_seen = None;
|
||||
}
|
||||
|
||||
if before == self.panels.len() && summary_before == self.summary_points.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(self.snapshot())
|
||||
}
|
||||
|
||||
pub fn build_snapshot(&mut self) -> HudPacket {
|
||||
self.snapshot()
|
||||
}
|
||||
|
||||
fn snapshot(&mut self) -> HudPacket {
|
||||
@@ -227,7 +153,6 @@ impl HudChartState {
|
||||
panels,
|
||||
summary: build_summary(&self.summary_points),
|
||||
pressure_matrix: self.pressure_matrix.clone(),
|
||||
spatial_force: self.spatial_force.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,106 +171,6 @@ impl Default for HudChartState {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_panel(source_id: &str, side: HudPanelSide, channel_count: usize) -> HudSignalPanel {
|
||||
HudSignalPanel {
|
||||
id: format!("panel-{source_id}"),
|
||||
code: source_id.to_string(),
|
||||
title: format!("Source {source_id}"),
|
||||
side,
|
||||
active: true,
|
||||
series: build_panel_series(source_id, channel_count, &[]),
|
||||
icons: build_panel_icons(source_id, channel_count),
|
||||
latest: None,
|
||||
min: None,
|
||||
max: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_frame_updates(frame: &TestFrame, decoded_values: Option<&[i32]>) -> Vec<HudPanelUpdate> {
|
||||
if let Some(values) = decoded_values {
|
||||
if values.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
return vec![HudPanelUpdate {
|
||||
source_id: format_source_id(frame.cmd),
|
||||
values: values.iter().map(|value| *value as f32).collect(),
|
||||
}];
|
||||
}
|
||||
|
||||
let chunks = frame.payload.chunks_exact(4);
|
||||
|
||||
if !frame.payload.is_empty() && chunks.remainder().is_empty() {
|
||||
return chunks.map(build_update_from_chunk).collect();
|
||||
}
|
||||
|
||||
vec![HudPanelUpdate {
|
||||
source_id: format_source_id(frame.cmd),
|
||||
values: fallback_values(frame),
|
||||
}]
|
||||
}
|
||||
|
||||
fn build_update_from_chunk(chunk: &[u8]) -> HudPanelUpdate {
|
||||
HudPanelUpdate {
|
||||
source_id: format_source_id(chunk[0]),
|
||||
values: chunk[1..]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, byte)| normalize_value(*byte, tone_for_index(index)))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fallback_values(frame: &TestFrame) -> Vec<f32> {
|
||||
let mut bytes = frame.payload.clone();
|
||||
|
||||
if bytes.is_empty() {
|
||||
bytes.extend([
|
||||
frame.cmd,
|
||||
frame.length as u8,
|
||||
frame.checksum,
|
||||
frame.cmd.wrapping_add(frame.checksum),
|
||||
]);
|
||||
}
|
||||
|
||||
while bytes.len() < 3 {
|
||||
let previous = *bytes.last().unwrap_or(&frame.cmd);
|
||||
bytes.push(
|
||||
previous
|
||||
.wrapping_add(frame.cmd)
|
||||
.wrapping_add(bytes.len() as u8),
|
||||
);
|
||||
}
|
||||
|
||||
bytes
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, byte)| normalize_value(byte, tone_for_index(index)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn normalize_value(byte: u8, tone: HudTone) -> f32 {
|
||||
let base = (byte as f32 / 255.0) * 100.0;
|
||||
let offset = match tone {
|
||||
HudTone::Cyan => 6.0,
|
||||
HudTone::Lime => 0.0,
|
||||
HudTone::Orange => -6.0,
|
||||
HudTone::Violet => 10.0,
|
||||
HudTone::Gold => -10.0,
|
||||
HudTone::Rose => 3.0,
|
||||
};
|
||||
|
||||
(base + offset).clamp(0.0, 100.0)
|
||||
}
|
||||
|
||||
fn format_source_id(byte: u8) -> String {
|
||||
if byte.is_ascii_alphanumeric() {
|
||||
(byte as char).to_ascii_uppercase().to_string()
|
||||
} else {
|
||||
format!("CH{:02X}", byte)
|
||||
}
|
||||
}
|
||||
|
||||
fn side_for_index(index: usize) -> HudPanelSide {
|
||||
if index % 2 == 0 {
|
||||
HudPanelSide::Left
|
||||
@@ -354,91 +179,6 @@ fn side_for_index(index: usize) -> HudPanelSide {
|
||||
}
|
||||
}
|
||||
|
||||
fn push_point(points: &mut Vec<f32>, value: f32) {
|
||||
if points.len() >= MAX_POINTS {
|
||||
points.remove(0);
|
||||
}
|
||||
|
||||
points.push((value * 10.0).round() / 10.0);
|
||||
}
|
||||
|
||||
fn build_panel_series(
|
||||
source_id: &str,
|
||||
channel_count: usize,
|
||||
previous: &[HudSignalSeries],
|
||||
) -> Vec<HudSignalSeries> {
|
||||
(0..channel_count)
|
||||
.map(|index| HudSignalSeries {
|
||||
id: format!("{source_id}-series-{}", index + 1),
|
||||
tone: tone_for_index(index),
|
||||
points: previous
|
||||
.get(index)
|
||||
.map(|series| series.points.clone())
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_panel_icons(source_id: &str, channel_count: usize) -> Vec<HudSignalIcon> {
|
||||
(0..channel_count)
|
||||
.map(|index| HudSignalIcon {
|
||||
id: format!("{source_id}-icon-{}", index + 1),
|
||||
label: if channel_count == 1 {
|
||||
"TOTAL".to_string()
|
||||
} else {
|
||||
format!("{source_id}-{}", index + 1)
|
||||
},
|
||||
tone: tone_for_index(index),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn ensure_panel_channels(panel: &mut HudSignalPanel, channel_count: usize) {
|
||||
if panel.series.len() == channel_count && panel.icons.len() == channel_count {
|
||||
return;
|
||||
}
|
||||
|
||||
panel.series = build_panel_series(&panel.code, channel_count, &panel.series);
|
||||
panel.icons = build_panel_icons(&panel.code, channel_count);
|
||||
}
|
||||
|
||||
fn refresh_panel_stats(panel: &mut HudSignalPanel) {
|
||||
let latest_values: Vec<f32> = panel
|
||||
.series
|
||||
.iter()
|
||||
.filter_map(|series| series.points.last().copied())
|
||||
.collect();
|
||||
|
||||
panel.latest = if latest_values.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(latest_values.iter().sum::<f32>() / latest_values.len() as f32)
|
||||
};
|
||||
|
||||
panel.min = panel
|
||||
.series
|
||||
.iter()
|
||||
.flat_map(|series| series.points.iter().copied())
|
||||
.reduce(f32::min);
|
||||
|
||||
panel.max = panel
|
||||
.series
|
||||
.iter()
|
||||
.flat_map(|series| series.points.iter().copied())
|
||||
.reduce(f32::max);
|
||||
}
|
||||
|
||||
fn tone_for_index(index: usize) -> HudTone {
|
||||
match index % 6 {
|
||||
0 => HudTone::Cyan,
|
||||
1 => HudTone::Lime,
|
||||
2 => HudTone::Orange,
|
||||
3 => HudTone::Violet,
|
||||
4 => HudTone::Gold,
|
||||
_ => HudTone::Rose,
|
||||
}
|
||||
}
|
||||
|
||||
fn push_summary_point(points: &mut Vec<f32>, value: f32) {
|
||||
if points.len() >= MAX_SUMMARY_POINTS {
|
||||
points.remove(0);
|
||||
@@ -462,62 +202,4 @@ fn now_millis() -> u64 {
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_millis() as u64)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
//
|
||||
// fn sample_frame() -> TestFrame {
|
||||
// TestFrame {
|
||||
// header: [0xAA, 0x55],
|
||||
// cmd: 0x01,
|
||||
// length: 4,
|
||||
// payload: vec![0x00, 0x0A, 0x00, 0x14],
|
||||
// checksum: 0,
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn prune_stale_clears_panels_and_summary_after_timeout() {
|
||||
// let mut state = HudChartState::new();
|
||||
// let frame = sample_frame();
|
||||
//
|
||||
// state.record_summary(30.0);
|
||||
// let _ = state.apply_frame(&frame, Some(&[10, 20]));
|
||||
//
|
||||
// let stale_now = Instant::now();
|
||||
// let stale_seen = stale_now - PANEL_STALE_AFTER - Duration::from_millis(1);
|
||||
//
|
||||
// state.last_frame_seen = Some(stale_seen);
|
||||
//
|
||||
// for entry in state.panels.values_mut() {
|
||||
// entry.last_seen = stale_seen;
|
||||
// }
|
||||
//
|
||||
// let packet = state
|
||||
// .prune_stale()
|
||||
// .expect("stale data should emit an update");
|
||||
//
|
||||
// assert!(packet.panels.is_empty());
|
||||
// assert!(packet.summary.points.is_empty());
|
||||
// assert!(state.panels.is_empty());
|
||||
// assert!(state.summary_points.is_empty());
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn prune_stale_keeps_recent_summary_points() {
|
||||
// let mut state = HudChartState::new();
|
||||
// let frame = sample_frame();
|
||||
//
|
||||
// state.record_summary(30.0);
|
||||
// let _ = state.apply_frame(&frame, Some(&[10, 20]));
|
||||
//
|
||||
// state.last_frame_seen = Some(Instant::now());
|
||||
//
|
||||
// assert!(state.prune_stale().is_none());
|
||||
// assert_eq!(state.summary_points, vec![30.0]);
|
||||
// assert_eq!(state.panels.len(), 1);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -1,696 +0,0 @@
|
||||
const SENSOR_ROWS: usize = 12;
|
||||
const SENSOR_COLS: usize = 7;
|
||||
const SENSOR_COUNT: usize = SENSOR_ROWS * SENSOR_COLS;
|
||||
|
||||
const CONTACT_ENTER_TOTAL_THRESHOLD: f32 = 520.0;
|
||||
const CONTACT_ENTER_PEAK_THRESHOLD: f32 = 50.0;
|
||||
const CONTACT_EXIT_TOTAL_THRESHOLD: f32 = 260.0;
|
||||
const CONTACT_EXIT_PEAK_THRESHOLD: f32 = 28.0;
|
||||
const CONTACT_ENTER_FRAMES_REQUIRED: usize = 2;
|
||||
const CONTACT_EXIT_FRAMES_REQUIRED: usize = 8;
|
||||
|
||||
const BASELINE_IDLE_ALPHA: f32 = 0.035;
|
||||
const BASELINE_BOOTSTRAP_ALPHA: f32 = 1.0;
|
||||
const BASELINE_NOISE_FLOOR: f32 = 5.0;
|
||||
|
||||
const ACTIVE_CELL_MIN_VALUE: f32 = 18.0;
|
||||
const ACTIVE_CELL_PEAK_RATIO: f32 = 0.14;
|
||||
const MIN_ACTIVE_CELLS: usize = 3;
|
||||
|
||||
const VECTOR_SMOOTHING_ALPHA: f32 = 0.16;
|
||||
|
||||
const REPORT_MAGNITUDE_ENTER: f32 = 0.12;
|
||||
const REPORT_MAGNITUDE_EXIT: f32 = 0.045;
|
||||
const REPORT_CONFIDENCE_ENTER: f32 = 0.14;
|
||||
const REPORT_CONFIDENCE_EXIT: f32 = 0.06;
|
||||
const REPORT_HOLD_FRAMES: usize = 10;
|
||||
|
||||
const ASYMMETRY_WEIGHT: f32 = 1.1;
|
||||
const DRIFT_WEIGHT: f32 = 0.65;
|
||||
const MOTION_WEIGHT: f32 = 0.25;
|
||||
const EDGE_ASYMMETRY_DAMPING: f32 = 0.35;
|
||||
const EDGE_INWARD_ROLLING_BIAS: f32 = 0.55;
|
||||
const EDGE_START_COP_THRESHOLD: f32 = 0.45;
|
||||
const EDGE_START_BIAS_WEIGHT: f32 = 1.1;
|
||||
const ROLLING_FRICTION_ALPHA: f32 = 0.68;
|
||||
const ROLLING_FRICTION_MIN_MAGNITUDE: f32 = 0.05;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PztSpatialAnalysis {
|
||||
pub angle_deg: f32,
|
||||
pub magnitude: f32,
|
||||
pub planar_x: f32,
|
||||
pub planar_y: f32,
|
||||
pub confidence: f32,
|
||||
pub contact_active: bool,
|
||||
pub reportable: bool,
|
||||
}
|
||||
|
||||
pub struct PztProcessor {
|
||||
baseline_frame: Option<Vec<f32>>,
|
||||
contact_active: bool,
|
||||
contact_enter_counter: usize,
|
||||
contact_exit_counter: usize,
|
||||
anchor_cop_x: Option<f32>,
|
||||
anchor_cop_y: Option<f32>,
|
||||
last_cop_x: Option<f32>,
|
||||
last_cop_y: Option<f32>,
|
||||
edge_start_bias_x: f32,
|
||||
edge_start_bias_y: f32,
|
||||
smoothed_x: f32,
|
||||
smoothed_y: f32,
|
||||
report_active: bool,
|
||||
report_hold_counter: usize,
|
||||
held_report: Option<PztSpatialAnalysis>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ContactStats {
|
||||
total: f32,
|
||||
peak: f32,
|
||||
active_total: f32,
|
||||
active_cells: usize,
|
||||
min_row: usize,
|
||||
max_row: usize,
|
||||
min_col: usize,
|
||||
max_col: usize,
|
||||
cop_x: f32,
|
||||
cop_y: f32,
|
||||
asymmetry_x: f32,
|
||||
asymmetry_y: f32,
|
||||
}
|
||||
|
||||
impl PztProcessor {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
baseline_frame: None,
|
||||
contact_active: false,
|
||||
contact_enter_counter: 0,
|
||||
contact_exit_counter: 0,
|
||||
anchor_cop_x: None,
|
||||
anchor_cop_y: None,
|
||||
last_cop_x: None,
|
||||
last_cop_y: None,
|
||||
edge_start_bias_x: 0.0,
|
||||
edge_start_bias_y: 0.0,
|
||||
smoothed_x: 0.0,
|
||||
smoothed_y: 0.0,
|
||||
report_active: false,
|
||||
report_hold_counter: 0,
|
||||
held_report: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_tracking_state(&mut self) {
|
||||
self.contact_active = false;
|
||||
self.contact_enter_counter = 0;
|
||||
self.contact_exit_counter = 0;
|
||||
self.anchor_cop_x = None;
|
||||
self.anchor_cop_y = None;
|
||||
self.last_cop_x = None;
|
||||
self.last_cop_y = None;
|
||||
self.edge_start_bias_x = 0.0;
|
||||
self.edge_start_bias_y = 0.0;
|
||||
self.smoothed_x = 0.0;
|
||||
self.smoothed_y = 0.0;
|
||||
}
|
||||
|
||||
fn reset_report_state(&mut self) {
|
||||
self.report_active = false;
|
||||
self.report_hold_counter = 0;
|
||||
self.held_report = None;
|
||||
}
|
||||
|
||||
fn update_idle_baseline(&mut self, raw_frame: &[f32], alpha: f32) {
|
||||
match self.baseline_frame.as_mut() {
|
||||
Some(baseline) => {
|
||||
for (base, current) in baseline.iter_mut().zip(raw_frame.iter().copied()) {
|
||||
*base += (current - *base) * alpha;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.baseline_frame = Some(raw_frame.to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn subtract_baseline(&mut self, raw_frame: &[f32]) -> Vec<f32> {
|
||||
if self.baseline_frame.is_none() {
|
||||
self.update_idle_baseline(raw_frame, BASELINE_BOOTSTRAP_ALPHA);
|
||||
}
|
||||
|
||||
let baseline = self
|
||||
.baseline_frame
|
||||
.as_ref()
|
||||
.expect("baseline should exist after bootstrap");
|
||||
|
||||
raw_frame
|
||||
.iter()
|
||||
.zip(baseline.iter())
|
||||
.map(|(raw, base)| (raw - base - BASELINE_NOISE_FLOOR).max(0.0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn pressure_metrics(frame: &[f32]) -> (f32, f32) {
|
||||
let total = frame.iter().sum::<f32>();
|
||||
let peak = frame.iter().copied().fold(0.0, f32::max);
|
||||
(total, peak)
|
||||
}
|
||||
|
||||
fn is_contact_enter_frame(frame: &[f32]) -> bool {
|
||||
let (total, peak) = Self::pressure_metrics(frame);
|
||||
total >= CONTACT_ENTER_TOTAL_THRESHOLD && peak >= CONTACT_ENTER_PEAK_THRESHOLD
|
||||
}
|
||||
|
||||
fn is_contact_exit_frame(frame: &[f32]) -> bool {
|
||||
let (total, peak) = Self::pressure_metrics(frame);
|
||||
total <= CONTACT_EXIT_TOTAL_THRESHOLD || peak <= CONTACT_EXIT_PEAK_THRESHOLD
|
||||
}
|
||||
|
||||
fn inactive_analysis() -> PztSpatialAnalysis {
|
||||
PztSpatialAnalysis {
|
||||
angle_deg: 0.0,
|
||||
magnitude: 0.0,
|
||||
planar_x: 0.0,
|
||||
planar_y: 0.0,
|
||||
confidence: 0.0,
|
||||
contact_active: false,
|
||||
reportable: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn weak_contact_analysis() -> PztSpatialAnalysis {
|
||||
PztSpatialAnalysis {
|
||||
contact_active: true,
|
||||
..Self::inactive_analysis()
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_contact_stats(frame: &[f32]) -> Option<ContactStats> {
|
||||
let total = frame.iter().sum::<f32>();
|
||||
if total <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let peak = frame.iter().copied().fold(0.0, f32::max);
|
||||
if peak <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let active_threshold = (peak * ACTIVE_CELL_PEAK_RATIO).max(ACTIVE_CELL_MIN_VALUE);
|
||||
|
||||
let mut active_total = 0.0;
|
||||
let mut active_cells = 0usize;
|
||||
let mut weighted_col_sum = 0.0;
|
||||
let mut weighted_row_sum = 0.0;
|
||||
let mut min_row = SENSOR_ROWS;
|
||||
let mut max_row = 0usize;
|
||||
let mut min_col = SENSOR_COLS;
|
||||
let mut max_col = 0usize;
|
||||
|
||||
for row in 0..SENSOR_ROWS {
|
||||
for col in 0..SENSOR_COLS {
|
||||
let index = row * SENSOR_COLS + col;
|
||||
let value = frame[index];
|
||||
if value < active_threshold {
|
||||
continue;
|
||||
}
|
||||
|
||||
active_cells += 1;
|
||||
active_total += value;
|
||||
weighted_col_sum += value * col as f32;
|
||||
weighted_row_sum += value * row as f32;
|
||||
min_row = min_row.min(row);
|
||||
max_row = max_row.max(row);
|
||||
min_col = min_col.min(col);
|
||||
max_col = max_col.max(col);
|
||||
}
|
||||
}
|
||||
|
||||
if active_cells < MIN_ACTIVE_CELLS || active_total <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cop_x = weighted_col_sum / active_total;
|
||||
let cop_y = weighted_row_sum / active_total;
|
||||
let bbox_center_x = (min_col + max_col) as f32 * 0.5;
|
||||
let bbox_center_y = (min_row + max_row) as f32 * 0.5;
|
||||
let half_width = ((max_col - min_col).max(1) as f32) * 0.5;
|
||||
let half_height = ((max_row - min_row).max(1) as f32) * 0.5;
|
||||
|
||||
let mut asymmetry_x = 0.0;
|
||||
let mut asymmetry_y = 0.0;
|
||||
|
||||
for row in min_row..=max_row {
|
||||
for col in min_col..=max_col {
|
||||
let index = row * SENSOR_COLS + col;
|
||||
let value = frame[index];
|
||||
if value < active_threshold {
|
||||
continue;
|
||||
}
|
||||
|
||||
asymmetry_x += value * ((col as f32 - bbox_center_x) / half_width);
|
||||
asymmetry_y += value * ((row as f32 - bbox_center_y) / half_height);
|
||||
}
|
||||
}
|
||||
|
||||
Some(ContactStats {
|
||||
total,
|
||||
peak,
|
||||
active_total,
|
||||
active_cells,
|
||||
min_row,
|
||||
max_row,
|
||||
min_col,
|
||||
max_col,
|
||||
cop_x,
|
||||
cop_y,
|
||||
asymmetry_x: asymmetry_x / active_total,
|
||||
asymmetry_y: asymmetry_y / active_total,
|
||||
})
|
||||
}
|
||||
|
||||
fn compute_vector_angle(x: f32, y: f32) -> (f32, f32) {
|
||||
let magnitude = (x * x + y * y).sqrt();
|
||||
if magnitude <= f32::EPSILON {
|
||||
return (0.0, 0.0);
|
||||
}
|
||||
|
||||
let mut angle = y.atan2(x).to_degrees();
|
||||
if angle < 0.0 {
|
||||
angle += 360.0;
|
||||
}
|
||||
|
||||
(angle, magnitude)
|
||||
}
|
||||
|
||||
fn contact_touches_edge(stats: &ContactStats) -> bool {
|
||||
stats.min_row == 0
|
||||
|| stats.max_row == SENSOR_ROWS - 1
|
||||
|| stats.min_col == 0
|
||||
|| stats.max_col == SENSOR_COLS - 1
|
||||
}
|
||||
|
||||
fn damp_edge_asymmetry(
|
||||
stats: &ContactStats,
|
||||
kinematic_x: f32,
|
||||
kinematic_y: f32,
|
||||
) -> (f32, f32) {
|
||||
let mut asymmetry_x = stats.asymmetry_x * ASYMMETRY_WEIGHT;
|
||||
let mut asymmetry_y = stats.asymmetry_y * ASYMMETRY_WEIGHT;
|
||||
|
||||
if stats.min_col == 0 && asymmetry_x < 0.0 {
|
||||
asymmetry_x = -asymmetry_x * EDGE_INWARD_ROLLING_BIAS;
|
||||
}
|
||||
if stats.max_col == SENSOR_COLS - 1 && asymmetry_x > 0.0 {
|
||||
asymmetry_x = -asymmetry_x * EDGE_INWARD_ROLLING_BIAS;
|
||||
}
|
||||
if stats.min_row == 0 && asymmetry_y < 0.0 {
|
||||
asymmetry_y = -asymmetry_y * EDGE_INWARD_ROLLING_BIAS;
|
||||
}
|
||||
if stats.max_row == SENSOR_ROWS - 1 && asymmetry_y > 0.0 {
|
||||
asymmetry_y = -asymmetry_y * EDGE_INWARD_ROLLING_BIAS;
|
||||
}
|
||||
|
||||
if Self::contact_touches_edge(stats) {
|
||||
let opposing_dot = asymmetry_x * kinematic_x + asymmetry_y * kinematic_y;
|
||||
let kinematic_mag = (kinematic_x * kinematic_x + kinematic_y * kinematic_y).sqrt();
|
||||
if opposing_dot < 0.0 && kinematic_mag >= ROLLING_FRICTION_MIN_MAGNITUDE {
|
||||
asymmetry_x *= EDGE_ASYMMETRY_DAMPING;
|
||||
asymmetry_y *= EDGE_ASYMMETRY_DAMPING;
|
||||
}
|
||||
}
|
||||
|
||||
(asymmetry_x, asymmetry_y)
|
||||
}
|
||||
|
||||
fn edge_start_bias(stats: &ContactStats) -> (f32, f32) {
|
||||
let center_x = (SENSOR_COLS - 1) as f32 * 0.5;
|
||||
let center_y = (SENSOR_ROWS - 1) as f32 * 0.5;
|
||||
let normalized_x = ((stats.cop_x - center_x) / center_x.max(1.0)).clamp(-1.0, 1.0);
|
||||
let normalized_y = ((stats.cop_y - center_y) / center_y.max(1.0)).clamp(-1.0, 1.0);
|
||||
|
||||
let mut bias_x = 0.0;
|
||||
let mut bias_y = 0.0;
|
||||
|
||||
if stats.min_col == 0 || stats.max_col == SENSOR_COLS - 1 {
|
||||
bias_x = Self::edge_start_axis_bias(normalized_x);
|
||||
}
|
||||
if stats.min_row == 0 || stats.max_row == SENSOR_ROWS - 1 {
|
||||
bias_y = Self::edge_start_axis_bias(normalized_y);
|
||||
}
|
||||
|
||||
(bias_x, bias_y)
|
||||
}
|
||||
|
||||
fn edge_start_axis_bias(normalized_axis: f32) -> f32 {
|
||||
let distance = normalized_axis.abs();
|
||||
if distance <= EDGE_START_COP_THRESHOLD {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let strength = ((distance - EDGE_START_COP_THRESHOLD) / (1.0 - EDGE_START_COP_THRESHOLD))
|
||||
.clamp(0.0, 1.0);
|
||||
-normalized_axis.signum() * strength * EDGE_START_BIAS_WEIGHT
|
||||
}
|
||||
|
||||
fn apply_rolling_friction(
|
||||
previous_x: f32,
|
||||
previous_y: f32,
|
||||
current_x: f32,
|
||||
current_y: f32,
|
||||
) -> (f32, f32) {
|
||||
let previous_mag = (previous_x * previous_x + previous_y * previous_y).sqrt();
|
||||
let current_mag = (current_x * current_x + current_y * current_y).sqrt();
|
||||
|
||||
if previous_mag < ROLLING_FRICTION_MIN_MAGNITUDE
|
||||
|| current_mag < ROLLING_FRICTION_MIN_MAGNITUDE
|
||||
{
|
||||
return (current_x, current_y);
|
||||
}
|
||||
|
||||
let dot = previous_x * current_x + previous_y * current_y;
|
||||
if dot >= 0.0 {
|
||||
return (current_x, current_y);
|
||||
}
|
||||
|
||||
let mixed_x = current_x * (1.0 - ROLLING_FRICTION_ALPHA)
|
||||
+ previous_x * ROLLING_FRICTION_ALPHA;
|
||||
let mixed_y = current_y * (1.0 - ROLLING_FRICTION_ALPHA)
|
||||
+ previous_y * ROLLING_FRICTION_ALPHA;
|
||||
|
||||
if mixed_x * previous_x + mixed_y * previous_y >= 0.0 {
|
||||
return (mixed_x, mixed_y);
|
||||
}
|
||||
|
||||
let keep_mag = previous_mag.min(current_mag) * 0.5;
|
||||
(
|
||||
previous_x / previous_mag * keep_mag,
|
||||
previous_y / previous_mag * keep_mag,
|
||||
)
|
||||
}
|
||||
|
||||
fn update_contact_state(&mut self, raw_frame: &[f32], frame: &[f32]) -> bool {
|
||||
if self.contact_active {
|
||||
if Self::is_contact_exit_frame(frame) {
|
||||
self.contact_exit_counter += 1;
|
||||
if self.contact_exit_counter >= CONTACT_EXIT_FRAMES_REQUIRED {
|
||||
self.update_idle_baseline(raw_frame, BASELINE_IDLE_ALPHA);
|
||||
self.reset_tracking_state();
|
||||
self.reset_report_state();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
self.contact_exit_counter = 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if Self::is_contact_enter_frame(frame) {
|
||||
self.contact_enter_counter += 1;
|
||||
if self.contact_enter_counter >= CONTACT_ENTER_FRAMES_REQUIRED {
|
||||
self.contact_active = true;
|
||||
self.contact_enter_counter = 0;
|
||||
self.contact_exit_counter = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
self.contact_enter_counter = 0;
|
||||
self.update_idle_baseline(raw_frame, BASELINE_IDLE_ALPHA);
|
||||
false
|
||||
}
|
||||
|
||||
fn store_report(&mut self, mut analysis: PztSpatialAnalysis) -> PztSpatialAnalysis {
|
||||
analysis.reportable = true;
|
||||
self.report_active = true;
|
||||
self.report_hold_counter = 0;
|
||||
self.held_report = Some(analysis);
|
||||
analysis
|
||||
}
|
||||
|
||||
fn hold_or_drop_report(&mut self) -> PztSpatialAnalysis {
|
||||
if self.report_active && self.report_hold_counter < REPORT_HOLD_FRAMES {
|
||||
self.report_hold_counter += 1;
|
||||
if let Some(mut held) = self.held_report {
|
||||
held.reportable = true;
|
||||
return held;
|
||||
}
|
||||
}
|
||||
|
||||
self.reset_report_state();
|
||||
Self::weak_contact_analysis()
|
||||
}
|
||||
|
||||
fn stabilize_report(&mut self, analysis: PztSpatialAnalysis) -> PztSpatialAnalysis {
|
||||
if !analysis.contact_active {
|
||||
self.reset_report_state();
|
||||
return analysis;
|
||||
}
|
||||
|
||||
let can_enter = analysis.magnitude >= REPORT_MAGNITUDE_ENTER
|
||||
&& analysis.confidence >= REPORT_CONFIDENCE_ENTER;
|
||||
let can_stay = analysis.magnitude >= REPORT_MAGNITUDE_EXIT
|
||||
&& analysis.confidence >= REPORT_CONFIDENCE_EXIT;
|
||||
|
||||
if self.report_active {
|
||||
if can_stay {
|
||||
return self.store_report(analysis);
|
||||
}
|
||||
|
||||
return self.hold_or_drop_report();
|
||||
}
|
||||
|
||||
if can_enter {
|
||||
return self.store_report(analysis);
|
||||
}
|
||||
|
||||
analysis
|
||||
}
|
||||
|
||||
pub fn get_pzt_analysis(
|
||||
&mut self,
|
||||
adc_data: &[f32],
|
||||
) -> Result<PztSpatialAnalysis, &'static str> {
|
||||
if adc_data.len() != SENSOR_COUNT {
|
||||
return Err("ADC data length must be 84");
|
||||
}
|
||||
|
||||
let baseline_subtracted = self.subtract_baseline(adc_data);
|
||||
if !self.update_contact_state(adc_data, &baseline_subtracted) {
|
||||
return Ok(Self::inactive_analysis());
|
||||
}
|
||||
|
||||
let Some(stats) = Self::compute_contact_stats(&baseline_subtracted) else {
|
||||
return Ok(self.stabilize_report(Self::weak_contact_analysis()));
|
||||
};
|
||||
|
||||
let Some(anchor_x) = self.anchor_cop_x else {
|
||||
self.anchor_cop_x = Some(stats.cop_x);
|
||||
self.anchor_cop_y = Some(stats.cop_y);
|
||||
self.last_cop_x = Some(stats.cop_x);
|
||||
self.last_cop_y = Some(stats.cop_y);
|
||||
let (edge_start_bias_x, edge_start_bias_y) = Self::edge_start_bias(&stats);
|
||||
self.edge_start_bias_x = edge_start_bias_x;
|
||||
self.edge_start_bias_y = edge_start_bias_y;
|
||||
|
||||
return Ok(self.stabilize_report(Self::weak_contact_analysis()));
|
||||
};
|
||||
let anchor_y = self.anchor_cop_y.unwrap_or(stats.cop_y);
|
||||
let last_x = self.last_cop_x.unwrap_or(stats.cop_x);
|
||||
let last_y = self.last_cop_y.unwrap_or(stats.cop_y);
|
||||
|
||||
let drift_x = stats.cop_x - anchor_x;
|
||||
let drift_y = stats.cop_y - anchor_y;
|
||||
let motion_x = stats.cop_x - last_x;
|
||||
let motion_y = stats.cop_y - last_y;
|
||||
|
||||
let kinematic_x = drift_x * DRIFT_WEIGHT + motion_x * MOTION_WEIGHT;
|
||||
let kinematic_y = drift_y * DRIFT_WEIGHT + motion_y * MOTION_WEIGHT;
|
||||
let edge_bias_x = self.edge_start_bias_x;
|
||||
let edge_bias_y = self.edge_start_bias_y;
|
||||
let (asymmetry_x, asymmetry_y) =
|
||||
Self::damp_edge_asymmetry(&stats, kinematic_x + edge_bias_x, kinematic_y + edge_bias_y);
|
||||
|
||||
let combined_x = asymmetry_x + kinematic_x + edge_bias_x;
|
||||
let combined_y = asymmetry_y + kinematic_y + edge_bias_y;
|
||||
let (combined_x, combined_y) = Self::apply_rolling_friction(
|
||||
self.smoothed_x,
|
||||
self.smoothed_y,
|
||||
combined_x,
|
||||
combined_y,
|
||||
);
|
||||
|
||||
self.smoothed_x += (combined_x - self.smoothed_x) * VECTOR_SMOOTHING_ALPHA;
|
||||
self.smoothed_y += (combined_y - self.smoothed_y) * VECTOR_SMOOTHING_ALPHA;
|
||||
|
||||
self.last_cop_x = Some(stats.cop_x);
|
||||
self.last_cop_y = Some(stats.cop_y);
|
||||
|
||||
let planar_x = self.smoothed_x;
|
||||
let planar_y = -self.smoothed_y;
|
||||
let (angle_deg, magnitude) = Self::compute_vector_angle(planar_x, planar_y);
|
||||
|
||||
let active_span_rows = (stats.max_row - stats.min_row + 1) as f32 / SENSOR_ROWS as f32;
|
||||
let active_span_cols = (stats.max_col - stats.min_col + 1) as f32 / SENSOR_COLS as f32;
|
||||
let activity = (stats.active_cells as f32 / SENSOR_COUNT as f32).clamp(0.0, 1.0);
|
||||
let span = ((active_span_rows + active_span_cols) * 0.5).clamp(0.0, 1.0);
|
||||
let pressure_ratio = (stats.active_total / stats.total.max(1.0)).clamp(0.0, 1.0);
|
||||
let peak_ratio =
|
||||
(stats.peak / (stats.total / stats.active_cells as f32 + 1.0)).clamp(0.0, 1.0);
|
||||
let confidence =
|
||||
((activity * 0.35) + (span * 0.2) + (pressure_ratio * 0.3) + (peak_ratio * 0.15))
|
||||
.clamp(0.0, 1.0);
|
||||
|
||||
Ok(self.stabilize_report(PztSpatialAnalysis {
|
||||
angle_deg,
|
||||
magnitude,
|
||||
planar_x,
|
||||
planar_y,
|
||||
confidence,
|
||||
contact_active: true,
|
||||
reportable: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn get_pzt_angle(&mut self, adc_data: &[f32]) -> Result<f32, &'static str> {
|
||||
Ok(self.get_pzt_analysis(adc_data)?.angle_deg)
|
||||
}
|
||||
|
||||
pub fn should_report(analysis: &PztSpatialAnalysis) -> bool {
|
||||
analysis.reportable
|
||||
}
|
||||
|
||||
pub fn reset_baseline(&mut self) {
|
||||
self.baseline_frame = None;
|
||||
self.reset_tracking_state();
|
||||
self.reset_report_state();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ContactStats, PztProcessor, SENSOR_COLS, SENSOR_ROWS};
|
||||
|
||||
fn index(row: usize, col: usize) -> usize {
|
||||
row * SENSOR_COLS + col
|
||||
}
|
||||
|
||||
fn make_frame(active: &[(usize, usize, f32)]) -> [f32; SENSOR_ROWS * SENSOR_COLS] {
|
||||
let mut frame = [0.0; SENSOR_ROWS * SENSOR_COLS];
|
||||
for (row, col, value) in active {
|
||||
frame[index(*row, *col)] = *value;
|
||||
}
|
||||
frame
|
||||
}
|
||||
|
||||
fn stats_touching_bottom_edge() -> ContactStats {
|
||||
ContactStats {
|
||||
total: 1000.0,
|
||||
peak: 300.0,
|
||||
active_total: 900.0,
|
||||
active_cells: 6,
|
||||
min_row: SENSOR_ROWS - 2,
|
||||
max_row: SENSOR_ROWS - 1,
|
||||
min_col: 2,
|
||||
max_col: 4,
|
||||
cop_x: 3.0,
|
||||
cop_y: 10.5,
|
||||
asymmetry_x: 0.0,
|
||||
asymmetry_y: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_frame_does_not_report_contact() {
|
||||
let mut processor = PztProcessor::new();
|
||||
let frame = [0.0; SENSOR_ROWS * SENSOR_COLS];
|
||||
let analysis = processor.get_pzt_analysis(&frame).unwrap();
|
||||
assert!(!analysis.contact_active);
|
||||
assert!(!analysis.reportable);
|
||||
assert_eq!(analysis.magnitude, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right_heavy_contact_reports_rightward_angle_after_confirmation() {
|
||||
let mut processor = PztProcessor::new();
|
||||
let baseline = [0.0; SENSOR_ROWS * SENSOR_COLS];
|
||||
let contact = make_frame(&[
|
||||
(5, 2, 120.0),
|
||||
(5, 3, 180.0),
|
||||
(5, 4, 280.0),
|
||||
(6, 2, 110.0),
|
||||
(6, 3, 170.0),
|
||||
(6, 4, 260.0),
|
||||
(7, 2, 100.0),
|
||||
(7, 3, 150.0),
|
||||
(7, 4, 240.0),
|
||||
]);
|
||||
|
||||
let _ = processor.get_pzt_analysis(&baseline).unwrap();
|
||||
|
||||
let mut analysis = processor.get_pzt_analysis(&contact).unwrap();
|
||||
for _ in 0..8 {
|
||||
analysis = processor.get_pzt_analysis(&contact).unwrap();
|
||||
}
|
||||
|
||||
assert!(analysis.contact_active);
|
||||
assert!(analysis.reportable);
|
||||
assert!(analysis.magnitude > 0.0);
|
||||
assert!(analysis.angle_deg <= 45.0 || analysis.angle_deg >= 315.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_stays_active_through_short_weak_gap() {
|
||||
let mut processor = PztProcessor::new();
|
||||
let baseline = [0.0; SENSOR_ROWS * SENSOR_COLS];
|
||||
let contact = make_frame(&[
|
||||
(5, 2, 120.0),
|
||||
(5, 3, 180.0),
|
||||
(5, 4, 280.0),
|
||||
(6, 2, 110.0),
|
||||
(6, 3, 170.0),
|
||||
(6, 4, 260.0),
|
||||
(7, 2, 100.0),
|
||||
(7, 3, 150.0),
|
||||
(7, 4, 240.0),
|
||||
]);
|
||||
let weak = make_frame(&[(5, 3, 55.0), (5, 4, 60.0), (6, 3, 50.0), (6, 4, 58.0)]);
|
||||
|
||||
let _ = processor.get_pzt_analysis(&baseline).unwrap();
|
||||
for _ in 0..10 {
|
||||
let _ = processor.get_pzt_analysis(&contact).unwrap();
|
||||
}
|
||||
|
||||
let analysis = processor.get_pzt_analysis(&weak).unwrap();
|
||||
assert!(analysis.reportable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bottom_edge_outward_gradient_is_turned_inward() {
|
||||
let stats = stats_touching_bottom_edge();
|
||||
let (_asymmetry_x, asymmetry_y) = PztProcessor::damp_edge_asymmetry(&stats, 0.0, -0.2);
|
||||
|
||||
assert!(asymmetry_y < 0.0);
|
||||
assert!(asymmetry_y > -1.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bottom_edge_start_adds_fixed_upward_bias() {
|
||||
let stats = stats_touching_bottom_edge();
|
||||
let (_bias_x, bias_y) = PztProcessor::edge_start_bias(&stats);
|
||||
|
||||
assert!(bias_y < 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rolling_friction_resists_one_frame_reversal() {
|
||||
let (x, y) = PztProcessor::apply_rolling_friction(0.4, 0.0, -0.6, 0.0);
|
||||
|
||||
assert!(x > 0.0);
|
||||
assert_eq!(y, 0.0);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
use eskin_finger_sdk::types::FingerSample;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FrameTiming {
|
||||
pub pts_ms: Option<u64>,
|
||||
@@ -7,50 +9,82 @@ pub struct FrameTiming {
|
||||
#[derive(Clone)]
|
||||
pub struct RecordedFrame<F> {
|
||||
pub timing: FrameTiming,
|
||||
pub frame: F
|
||||
pub frame: F,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Recording<F> {
|
||||
pub frames: Vec<RecordedFrame<F>>
|
||||
pub frames: Vec<RecordedFrame<F>>,
|
||||
}
|
||||
|
||||
impl<F> Recording<F> {
|
||||
pub fn new() -> Recording<F> { Self { frames: Vec::new() } }
|
||||
pub fn push(&mut self, ite: RecordedFrame<F>) {
|
||||
self.frames.push(ite);
|
||||
pub fn new() -> Recording<F> {
|
||||
Self {
|
||||
frames: Vec::new(),
|
||||
}
|
||||
}
|
||||
pub fn push(&mut self, item: RecordedFrame<F>) {
|
||||
self.frames.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CsvExporter<F> {
|
||||
type Error: std::error::Error + Send + Sync + 'static;
|
||||
fn csv_header(&self, recording: &Recording<F>) -> Vec<String>;
|
||||
fn csv_row(&self, item: &RecordedFrame<F>) -> anyhow::Result<Vec<String>>;
|
||||
}
|
||||
pub type FingerRecording = Recording<FingerSample>;
|
||||
|
||||
// TODO: CsvImporter
|
||||
pub trait CsvImporter<P> {
|
||||
fn load<R: std::io::Read>(&mut self, reader: R) -> anyhow::Result<Vec<P>>;
|
||||
}
|
||||
|
||||
pub fn write_csv<F, E, W>(
|
||||
recording: &Recording<F>,
|
||||
exporter: &E,
|
||||
writer: W,
|
||||
pub fn export_recording_csv<W>(
|
||||
recording: &Recording<FingerSample>,
|
||||
mut writer: W,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
E: CsvExporter<F>,
|
||||
W: std::io::Write,
|
||||
{
|
||||
let header = exporter.csv_header(&recording);
|
||||
let mut wrt = csv::Writer::from_writer(writer);
|
||||
wrt.write_record(header)?;
|
||||
for f in &recording.frames {
|
||||
let row = exporter.csv_row(f)?;
|
||||
wrt.write_record(&row)?;
|
||||
// Infer channel count from the first sample's combined_forces (just fz)
|
||||
// We write: timestamp_us, sequence, module, fx, fy, fz
|
||||
let mut wrt = csv::Writer::from_writer(&mut writer);
|
||||
wrt.write_record(["timestamp_us", "sequence", "module", "fx", "fy", "fz"])?;
|
||||
|
||||
for frame in &recording.frames {
|
||||
let s = &frame.frame;
|
||||
wrt.write_record(&[
|
||||
s.timestamp_us.to_string(),
|
||||
s.sequence.to_string(),
|
||||
format!("{:?}", s.combined_forces.module),
|
||||
s.combined_forces.force.fx.to_string(),
|
||||
s.combined_forces.force.fy.to_string(),
|
||||
s.combined_forces.force.fz.to_string(),
|
||||
])?;
|
||||
}
|
||||
|
||||
wrt.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct FingerSampleCsvPacket {
|
||||
pub timestamp_us: u64,
|
||||
pub sequence: u32,
|
||||
pub fz: u32,
|
||||
}
|
||||
|
||||
pub fn import_csv<R: std::io::Read>(
|
||||
reader: R,
|
||||
) -> anyhow::Result<Vec<FingerSampleCsvPacket>> {
|
||||
let mut rdr = csv::Reader::from_reader(reader);
|
||||
let mut packets = Vec::new();
|
||||
|
||||
for result in rdr.records() {
|
||||
let record = result?;
|
||||
if record.len() < 6 {
|
||||
continue;
|
||||
}
|
||||
let timestamp_us = record.get(0).unwrap_or("0").parse::<u64>().unwrap_or(0);
|
||||
let sequence = record.get(1).unwrap_or("0").parse::<u32>().unwrap_or(0);
|
||||
let fz = record.get(5).unwrap_or("0").parse::<u32>().unwrap_or(0);
|
||||
|
||||
packets.push(FingerSampleCsvPacket {
|
||||
timestamp_us,
|
||||
sequence,
|
||||
fz,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(packets)
|
||||
}
|
||||
@@ -1,526 +1,160 @@
|
||||
use crate::ad_solver::solve_for_x;
|
||||
#[cfg(feature = "devkit")]
|
||||
use crate::devkit::{proto::SensorFrame, DevKitState};
|
||||
use crate::serial_core::codec::Codec;
|
||||
use crate::serial_core::codecs::hand_gateway::HandGatewayCodec;
|
||||
use crate::serial_core::codecs::tactile_a::TactileACodec;
|
||||
use crate::serial_core::frame::{FrameHandler, TactileAFrame, TestFrame};
|
||||
use crate::serial_core::model::{HudChartState, HudPacket, HudSpatialForce};
|
||||
#[cfg(feature = "multi-dim")]
|
||||
use crate::serial_core::multi_dim_force::PztProcessor;
|
||||
use crate::serial_core::model::HudChartState;
|
||||
use crate::serial_core::record::Recording;
|
||||
use crate::serial_core::record::{FrameTiming, RecordedFrame};
|
||||
use anyhow::Result;
|
||||
use log::debug;
|
||||
use log::info;
|
||||
use std::future::pending;
|
||||
#[cfg(feature = "devkit")]
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
#[cfg(feature = "devkit")]
|
||||
use tauri::Manager;
|
||||
use eskin_finger_sdk::channel::DeviceEvent;
|
||||
use eskin_finger_sdk::config::DeviceConfig;
|
||||
use eskin_finger_sdk::device::{EskinDevice, EskinDeviceInner};
|
||||
use eskin_finger_sdk::transport::SerialPortTransport;
|
||||
use eskin_finger_sdk::types::FingerSample;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::time::{self, Duration, MissedTickBehavior};
|
||||
use tokio_serial::SerialStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
const AUTO_SUB_INTERVAL: Duration = Duration::from_nanos(16_666_667);
|
||||
|
||||
pub enum PollMode<F> {
|
||||
Disable,
|
||||
Enabled(Box<dyn PollRequester<F>>),
|
||||
use super::model::HudPacket;
|
||||
|
||||
pub struct SdkSession {
|
||||
pub device: EskinDeviceInner,
|
||||
}
|
||||
|
||||
struct PendingSubFrame<F> {
|
||||
frame: F,
|
||||
values: Vec<i32>,
|
||||
spatial_force: Option<HudSpatialForce>,
|
||||
pub fn open_device(port: &str) -> Result<SdkSession, String> {
|
||||
let port = port.trim();
|
||||
if port.is_empty() {
|
||||
return Err("Serial port is required".to_string());
|
||||
}
|
||||
|
||||
let transport = SerialPortTransport::new(port, 921600);
|
||||
let config = DeviceConfig::default();
|
||||
let mut device = EskinDeviceInner::new(config, Box::new(transport));
|
||||
device.open().map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(SdkSession { device })
|
||||
}
|
||||
|
||||
pub trait SerialFrame: Clone + Send + 'static {
|
||||
fn dts_ms(&self) -> u64;
|
||||
|
||||
fn to_hud_packet(
|
||||
&self,
|
||||
chart_state: &mut HudChartState,
|
||||
display_values: Option<&[i32]>,
|
||||
) -> Option<HudPacket>;
|
||||
}
|
||||
|
||||
impl SerialFrame for TestFrame {
|
||||
fn dts_ms(&self) -> u64 {
|
||||
self.dts_ms
|
||||
}
|
||||
|
||||
fn to_hud_packet(
|
||||
&self,
|
||||
chart_state: &mut HudChartState,
|
||||
display_values: Option<&[i32]>,
|
||||
) -> Option<HudPacket> {
|
||||
Some(chart_state.apply_frame(self, display_values))
|
||||
}
|
||||
}
|
||||
|
||||
impl SerialFrame for TactileAFrame {
|
||||
fn dts_ms(&self) -> u64 {
|
||||
match self {
|
||||
TactileAFrame::Req(_) => 0,
|
||||
TactileAFrame::Rep(rep) => rep.dts_ms,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_hud_packet(
|
||||
&self,
|
||||
chart_state: &mut HudChartState,
|
||||
display_values: Option<&[i32]>,
|
||||
) -> Option<HudPacket> {
|
||||
match self {
|
||||
TactileAFrame::Req(_) => None,
|
||||
TactileAFrame::Rep(rep) => {
|
||||
let proxy = TestFrame {
|
||||
header: rep.meta.header,
|
||||
cmd: rep.meta.func_code,
|
||||
length: rep.meta.except_data_len,
|
||||
payload: rep.payload.clone(),
|
||||
checksum: rep.meta.checksum,
|
||||
dts_ms: rep.dts_ms,
|
||||
};
|
||||
|
||||
Some(chart_state.apply_frame(&proxy, display_values))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PollRequester<F>: Send {
|
||||
fn poll_interval(&self) -> Option<Duration> {
|
||||
None
|
||||
}
|
||||
|
||||
fn should_request(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn next_request(&mut self) -> Result<Option<F>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn on_rx_frame(&mut self, _frame: &F) {}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NoopPollRequester;
|
||||
|
||||
impl<F> PollRequester<F> for NoopPollRequester {}
|
||||
|
||||
pub struct TactileAPollRequester {
|
||||
period: Duration,
|
||||
cols: usize,
|
||||
rows: usize,
|
||||
awaiting_reply: bool,
|
||||
last_request_at: Option<Instant>,
|
||||
reply_timeout: Duration,
|
||||
}
|
||||
|
||||
impl TactileAPollRequester {
|
||||
pub fn new(period: Duration, cols: usize, rows: usize, reply_timeout: Duration) -> Self {
|
||||
Self {
|
||||
period,
|
||||
cols,
|
||||
rows,
|
||||
awaiting_reply: false,
|
||||
last_request_at: None,
|
||||
reply_timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PollRequester<TactileAFrame> for TactileAPollRequester {
|
||||
fn poll_interval(&self) -> Option<Duration> {
|
||||
Some(self.period)
|
||||
}
|
||||
|
||||
fn should_request(&mut self) -> bool {
|
||||
if !self.awaiting_reply {
|
||||
return true;
|
||||
}
|
||||
let timed_out = self
|
||||
.last_request_at
|
||||
.map(|t| t.elapsed() >= self.reply_timeout)
|
||||
.unwrap_or(false);
|
||||
|
||||
if timed_out {
|
||||
self.awaiting_reply = false;
|
||||
self.last_request_at = None;
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn next_request(&mut self) -> Result<Option<TactileAFrame>> {
|
||||
let req = TactileACodec::build_req_frame(self.cols, self.rows)?;
|
||||
self.awaiting_reply = true;
|
||||
self.last_request_at = Some(Instant::now());
|
||||
Ok(Some(req))
|
||||
}
|
||||
|
||||
fn on_rx_frame(&mut self, frame: &TactileAFrame) {
|
||||
if matches!(frame, TactileAFrame::Rep(_)) {
|
||||
self.awaiting_reply = false;
|
||||
self.last_request_at = None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_serial<C, H, T, F>(
|
||||
pub async fn run_stream(
|
||||
app: AppHandle,
|
||||
port: SerialStream,
|
||||
codec: C,
|
||||
handler: H,
|
||||
session_started_at: Instant,
|
||||
recording: Arc<Mutex<Recording<F>>>,
|
||||
device: &mut EskinDeviceInner,
|
||||
cancel: CancellationToken,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: SerialFrame,
|
||||
C: Codec<F> + Send + 'static,
|
||||
H: FrameHandler<F, T> + Send + 'static,
|
||||
T: Into<i32>,
|
||||
{
|
||||
run_serial_with_poll(
|
||||
app,
|
||||
port,
|
||||
codec,
|
||||
handler,
|
||||
session_started_at,
|
||||
recording,
|
||||
cancel,
|
||||
PollMode::Disable,
|
||||
)
|
||||
.await
|
||||
}
|
||||
) -> Result<(), String> {
|
||||
device
|
||||
.start_stream()
|
||||
.map_err(|e| format!("start_stream failed: {e}"))?;
|
||||
|
||||
/// Runs the hand gateway in passive mode. The MCU continuously uploads data, so
|
||||
/// this loop never writes request frames to the serial port.
|
||||
pub async fn run_hand_gateway_serial(
|
||||
app: AppHandle,
|
||||
mut port: SerialStream,
|
||||
mut codec: HandGatewayCodec,
|
||||
session_started_at: Instant,
|
||||
cancel: CancellationToken,
|
||||
) -> Result<()> {
|
||||
let mut buffer = [0u8; 4096];
|
||||
let channels = device.channels();
|
||||
let mut chart_state = HudChartState::new();
|
||||
|
||||
loop {
|
||||
let result = loop {
|
||||
tokio::select! {
|
||||
_ = cancel.cancelled() => break,
|
||||
read_result = port.read(&mut buffer) => {
|
||||
let n = read_result?;
|
||||
if n == 0 {
|
||||
tokio::task::yield_now().await;
|
||||
continue;
|
||||
}
|
||||
_ = cancel.cancelled() => {
|
||||
break Ok(());
|
||||
}
|
||||
_ = tokio::time::sleep(tokio::time::Duration::from_millis(1)) => {}
|
||||
}
|
||||
|
||||
let raw = &buffer[..n];
|
||||
app.emit("hand_gateway_raw", raw.to_vec())?;
|
||||
let raw_hex = raw
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02X}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
info!("[hand_gateway][raw] {raw_hex}");
|
||||
|
||||
for frame in codec.decode(raw, session_started_at)? {
|
||||
let crate::serial_core::frame::HandGatewayFrame::DataRep(frame) = frame;
|
||||
info!(
|
||||
"[hand_gateway][frame] timestamp_us={} config=0x{:08X} valid_config=0x{:08X} block_count={}",
|
||||
frame.timestamp,
|
||||
frame.config,
|
||||
frame.valid_config,
|
||||
frame.block_count,
|
||||
);
|
||||
let mut pressure_matrix = Vec::new();
|
||||
for node in &frame.nodes {
|
||||
info!(
|
||||
"[hand_gateway][node] mask=0x{:08X} valid={} payload={:02X?}",
|
||||
node.config_mask,
|
||||
node.valid,
|
||||
node.payload,
|
||||
);
|
||||
if node.valid {
|
||||
pressure_matrix.extend(
|
||||
HandGatewayCodec::parse_node_payload(&node.payload)?
|
||||
.into_iter()
|
||||
.map(f32::from),
|
||||
);
|
||||
} else {
|
||||
let sample_count = node.payload.len() / 2;
|
||||
pressure_matrix.extend(std::iter::repeat(0.0).take(sample_count));
|
||||
}
|
||||
}
|
||||
|
||||
if !pressure_matrix.is_empty() {
|
||||
app.emit("hand_gateway_pressure_matrix", pressure_matrix)?;
|
||||
}
|
||||
// Try to receive a sample (non-blocking-ish via small timeout)
|
||||
match channels.recv_sample(5) {
|
||||
Ok(sample) => {
|
||||
if let Some(packet) = build_hud_packet_from_sample(&sample, &mut chart_state) {
|
||||
let _ = app.emit("hud_stream", packet);
|
||||
}
|
||||
}
|
||||
Err(eskin_finger_sdk::error::SdkError::Timeout) => {
|
||||
// No sample yet, check for events
|
||||
}
|
||||
Err(e) => {
|
||||
break Err(format!("sample recv error: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_serial_with_poll<C, H, T, F>(
|
||||
app: AppHandle,
|
||||
mut port: SerialStream,
|
||||
mut codec: C,
|
||||
mut handler: H,
|
||||
session_started_at: Instant,
|
||||
recording: Arc<Mutex<Recording<F>>>,
|
||||
cancel: CancellationToken,
|
||||
poll_mode: PollMode<F>,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: SerialFrame,
|
||||
C: Codec<F> + Send + 'static,
|
||||
H: FrameHandler<F, T> + Send + 'static,
|
||||
T: Into<i32>,
|
||||
{
|
||||
let mut requester = match poll_mode {
|
||||
PollMode::Disable => None,
|
||||
PollMode::Enabled(r) => Some(r),
|
||||
// Drain any events
|
||||
if let Err(e) = drain_events(&channels) {
|
||||
break Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let mut poll_interval = requester.as_ref().and_then(|r| r.poll_interval()).map(|d| {
|
||||
let mut it = time::interval(d);
|
||||
it.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
it
|
||||
});
|
||||
let mut poll_sub_interval = time::interval(AUTO_SUB_INTERVAL);
|
||||
poll_sub_interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
let _ = device.stop_stream();
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn run_stream_with_record(
|
||||
app: AppHandle,
|
||||
device: &mut EskinDeviceInner,
|
||||
cancel: CancellationToken,
|
||||
recording: std::sync::Arc<std::sync::Mutex<Recording<FingerSample>>>,
|
||||
) -> Result<(), String> {
|
||||
device
|
||||
.start_stream()
|
||||
.map_err(|e| format!("start_stream failed: {e}"))?;
|
||||
|
||||
let channels = device.channels();
|
||||
let mut chart_state = HudChartState::new();
|
||||
let mut buffer = [0u8; 1024];
|
||||
let mut prune_interval = time::interval(Duration::from_millis(450));
|
||||
#[cfg(feature = "multi-dim")]
|
||||
let mut pzt_processor = PztProcessor::new();
|
||||
let mut pending_sub_frame: Option<PendingSubFrame<F>> = None;
|
||||
prune_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
|
||||
|
||||
loop {
|
||||
let result = loop {
|
||||
tokio::select! {
|
||||
_ = cancel.cancelled() => break,
|
||||
_ = async {
|
||||
match poll_interval.as_mut() {
|
||||
Some(it) => {
|
||||
it.tick().await;
|
||||
}
|
||||
None => pending::<()>().await,
|
||||
}
|
||||
} => {
|
||||
if let Some(r) = requester.as_mut() {
|
||||
if r.should_request() {
|
||||
if let Some(req) = r.next_request()? {
|
||||
let bytes = codec.encode(&req)?;
|
||||
port.write_all(&bytes).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = cancel.cancelled() => {
|
||||
break Ok(());
|
||||
}
|
||||
_ = prune_interval.tick() => {
|
||||
if let Some(packet) = chart_state.prune_stale() {
|
||||
app.emit("hud_stream", packet)?;
|
||||
}
|
||||
}
|
||||
_ = poll_sub_interval.tick() => {
|
||||
if let Some(pending) = pending_sub_frame.take() {
|
||||
let display_values = build_display_values(
|
||||
&mut chart_state,
|
||||
pending.values.as_slice(),
|
||||
pending.spatial_force,
|
||||
);
|
||||
|
||||
if let Some(packet) = pending
|
||||
.frame
|
||||
.to_hud_packet(&mut chart_state, display_values.as_deref())
|
||||
{
|
||||
app.emit("hud_stream", packet)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
read_result = port.read(&mut buffer) => {
|
||||
let n = read_result?;
|
||||
if n == 0 {
|
||||
// Some serial drivers can resolve reads with 0 bytes repeatedly.
|
||||
// Yield here so timer-driven poll requests are not starved by a busy loop.
|
||||
tokio::task::yield_now().await;
|
||||
continue;
|
||||
}
|
||||
|
||||
let frames = codec.decode(&buffer[..n], session_started_at)?;
|
||||
for frame in frames {
|
||||
if let Some(r) = requester.as_mut() {
|
||||
r.on_rx_frame(&frame);
|
||||
}
|
||||
|
||||
let decode_res = handler
|
||||
.on_frame(&frame)
|
||||
.await?
|
||||
.map(|vals| vals.into_iter().map(Into::into).collect::<Vec<i32>>());
|
||||
_ = tokio::time::sleep(tokio::time::Duration::from_millis(1)) => {}
|
||||
}
|
||||
|
||||
match channels.recv_sample(5) {
|
||||
Ok(sample) => {
|
||||
// Record
|
||||
{
|
||||
let mut record = recording
|
||||
.lock()
|
||||
.map_err(|_| anyhow::anyhow!("recording state poisoned"))?;
|
||||
record.push(RecordedFrame {
|
||||
timing: FrameTiming {
|
||||
.map_err(|_| "recording state poisoned".to_string())?;
|
||||
record.push(crate::serial_core::record::RecordedFrame {
|
||||
timing: crate::serial_core::record::FrameTiming {
|
||||
pts_ms: None,
|
||||
dts_ms: frame.dts_ms(),
|
||||
dts_ms: sample.timestamp_us / 1000,
|
||||
},
|
||||
frame: frame.clone(),
|
||||
frame: sample.clone(),
|
||||
});
|
||||
drop(record);
|
||||
}
|
||||
|
||||
if let Some(vals) = decode_res {
|
||||
let mut spatial_force = None;
|
||||
#[cfg(feature = "multi-dim")]
|
||||
{
|
||||
let pzt_values = vals.iter().map(|value| *value as f32).collect::<Vec<f32>>();
|
||||
if let Ok(analysis) = pzt_processor.get_pzt_analysis(&pzt_values) {
|
||||
// debug!(
|
||||
// "spatial force: angle={:.2}°, magnitude={:.2}, dx={:.2}, dy={:.2}",
|
||||
// analysis.angle_deg, analysis.magnitude, analysis.planar_x, analysis.planar_y
|
||||
// );
|
||||
if PztProcessor::should_report(&analysis) {
|
||||
spatial_force = Some(HudSpatialForce {
|
||||
angle_deg: analysis.angle_deg,
|
||||
magnitude: analysis.magnitude,
|
||||
confidence: analysis.confidence,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "devkit")]
|
||||
{
|
||||
let summary = vals.iter().copied().sum::<i32>();
|
||||
let force = raw_to_g1(summary as u32);
|
||||
|
||||
push_devkit_frame(&app, vals.as_slice(), frame.dts_ms(), force);
|
||||
}
|
||||
|
||||
pending_sub_frame = Some(PendingSubFrame {
|
||||
frame: frame.clone(),
|
||||
values: vals,
|
||||
spatial_force,
|
||||
});
|
||||
} else if let Some(packet) = frame.to_hud_packet(&mut chart_state, None) {
|
||||
app.emit("hud_stream", packet)?;
|
||||
}
|
||||
if let Some(packet) = build_hud_packet_from_sample(&sample, &mut chart_state) {
|
||||
let _ = app.emit("hud_stream", packet);
|
||||
}
|
||||
}
|
||||
Err(eskin_finger_sdk::error::SdkError::Timeout) => {}
|
||||
Err(e) => {
|
||||
break Err(format!("sample recv error: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
if let Err(e) = drain_events(&channels) {
|
||||
break Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let _ = device.stop_stream();
|
||||
result
|
||||
}
|
||||
|
||||
fn build_display_values(
|
||||
fn drain_events(channels: &std::sync::Arc<eskin_finger_sdk::channel::ChannelManager>) -> Result<(), String> {
|
||||
loop {
|
||||
match channels.recv_event(0) {
|
||||
Ok(DeviceEvent::IoError(msg)) => {
|
||||
eprintln!("SDK stream io error: {msg}");
|
||||
return Err(format!("stream io error: {msg}"));
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(eskin_finger_sdk::error::SdkError::Timeout) => return Ok(()),
|
||||
Err(eskin_finger_sdk::error::SdkError::ChannelClosed) => {
|
||||
return Err("event channel closed".into());
|
||||
}
|
||||
Err(_) => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_hud_packet_from_sample(
|
||||
sample: &FingerSample,
|
||||
chart_state: &mut HudChartState,
|
||||
values: &[i32],
|
||||
spatial_force: Option<HudSpatialForce>,
|
||||
) -> Option<Vec<i32>> {
|
||||
let summary = values.iter().copied().sum::<i32>();
|
||||
let force = raw_to_g1(summary as u32);
|
||||
// let force_solve = solve_for_x(summary as f64)?;
|
||||
// println!("force_solve: {force_solve}");
|
||||
chart_state.record_summary(force as f32);
|
||||
chart_state.record_pressure_matrix(values);
|
||||
chart_state.record_spatial_force(spatial_force);
|
||||
Some(vec![summary])
|
||||
}
|
||||
|
||||
#[cfg(feature = "devkit")]
|
||||
fn push_devkit_frame(app: &AppHandle, values: &[i32], dts_ms: u64, resultant_force: f64) {
|
||||
let devkit_state = app.state::<DevKitState>();
|
||||
if !devkit_state.running.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let (rows, cols) = infer_matrix_shape(values.len());
|
||||
let timestamp_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as u64;
|
||||
|
||||
let seq = timestamp_ms;
|
||||
let matrix = values
|
||||
.iter()
|
||||
.map(|value| (*value).max(0) as u32)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
devkit_state.push_frame(SensorFrame {
|
||||
seq,
|
||||
timestamp_ms,
|
||||
rows,
|
||||
cols,
|
||||
matrix,
|
||||
resultant_force,
|
||||
dts_ms: dts_ms as u32,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "devkit")]
|
||||
fn infer_matrix_shape(len: usize) -> (u32, u32) {
|
||||
if len == 84 {
|
||||
return (12, 7);
|
||||
}
|
||||
|
||||
if len == 0 {
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
let mut best = (len, 1);
|
||||
let mut factor = 1usize;
|
||||
while factor * factor <= len {
|
||||
if len % factor == 0 {
|
||||
best = (len / factor, factor);
|
||||
}
|
||||
factor += 1;
|
||||
}
|
||||
|
||||
(best.0 as u32, best.1 as u32)
|
||||
}
|
||||
|
||||
fn raw_to_g1(raw: u32) -> f64 {
|
||||
const X: [u32; 13] = [
|
||||
0, 16811, 41350, 79241, 94615, 127446, 149559, 175900, 195056, 237852, 267810, 322472,
|
||||
378511,
|
||||
];
|
||||
|
||||
const Y: [f64; 13] = [
|
||||
0.0, 57.0, 97.0, 197.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0,
|
||||
];
|
||||
|
||||
let n = X.len();
|
||||
if raw <= X[0] {
|
||||
return Y[0] / 100.0;
|
||||
}
|
||||
if raw >= X[n - 1] {
|
||||
return Y[n - 1] / 100.0;
|
||||
}
|
||||
|
||||
let mut left = 0;
|
||||
let mut right = n - 1;
|
||||
|
||||
while left + 1 < right {
|
||||
let mid = (left + right) / 2;
|
||||
if raw < X[mid] {
|
||||
right = mid;
|
||||
} else {
|
||||
left = mid;
|
||||
}
|
||||
}
|
||||
|
||||
let ratio = (raw - X[left]) as f64 / (X[right] - X[left]) as f64;
|
||||
Y[left] / 100.0 + ratio * (Y[right] - Y[left]) / 100.0
|
||||
) -> Option<HudPacket> {
|
||||
let fz = sample.combined_forces.force.fz as f32;
|
||||
chart_state.record_summary(fz);
|
||||
if !sample.raw_adcs.is_empty() {
|
||||
let pressure: Vec<f32> = sample.raw_adcs.iter().map(|&v| v as f32).collect();
|
||||
chart_state.record_pressure_matrix(&pressure);
|
||||
}
|
||||
Some(chart_state.build_snapshot())
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
pub fn usize_to_u16_be_bytes(n: usize) -> [u8; 2] {
|
||||
(n as u16).to_be_bytes()
|
||||
}
|
||||
|
||||
pub fn usize_to_u16_le_bytes(n: usize) -> [u8; 2] {
|
||||
(n as u16).to_be_bytes()
|
||||
}
|
||||
|
||||
pub fn u16_to_hex_be_bytes(n: u16) -> [u8; 2] {
|
||||
(n as u16).to_be_bytes()
|
||||
}
|
||||
|
||||
pub fn u16_to_hex_le_bytes(n: u16) -> [u8; 2] {
|
||||
(n as u16).to_le_bytes()
|
||||
}
|
||||
|
||||
pub fn calc_crc8_smbus(c: &[u8]) -> u8 {
|
||||
let crc8_smbus = crc::Crc::<u8>::new(&crc::CRC_8_SMBUS);
|
||||
let checksum = crc8_smbus.checksum(c);
|
||||
return checksum;
|
||||
}
|
||||
|
||||
pub fn calc_crc8_itu(c: &[u8]) -> u8 {
|
||||
let crc8_itu_alg = crc::Crc::<u8>::new(&crc::CRC_8_I_432_1);
|
||||
let checksum = crc8_itu_alg.checksum(c);
|
||||
return checksum;
|
||||
}
|
||||
|
||||
pub fn elapsed_millis(start_at: Instant) -> u64 {
|
||||
start_at.elapsed().as_millis() as u64
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use anyhow::Ok;
|
||||
|
||||
use crate::serial_core::utils::{calc_crc8_itu, calc_crc8_smbus};
|
||||
|
||||
#[test]
|
||||
fn test_crc8_itu() -> anyhow::Result<()> {
|
||||
let req_vec = vec![0x55, 0xAA, 0x09, 0x00, 0x34, 0x00, 0xFB, 0x00, 0x1C, 0x00, 0x00, 0x18, 0x00];
|
||||
let checksum = calc_crc8_itu(req_vec.as_slice());
|
||||
assert_eq!(checksum, 0x7A);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_crc8_smbus() -> anyhow::Result<()> {
|
||||
let req_vec = vec![0x55, 0xAA, 0x09, 0x00, 0x34, 0x00, 0xFB, 0x00, 0x1C, 0x00, 0x00, 0x18, 0x00];
|
||||
let checksum = calc_crc8_smbus(req_vec.as_slice());
|
||||
assert_eq!(checksum, 0x2F);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -6,21 +6,17 @@
|
||||
import { fly } from "svelte/transition";
|
||||
import { DEFAULT_PRESSURE_RANGE_MAX, DEFAULT_PRESSURE_RANGE_MIN } from "$lib/config/pressure-range";
|
||||
import ConfigPanel from "$lib/components/ConfigPanel.svelte";
|
||||
import ModelStage from "$lib/components/ModelStage.svelte";
|
||||
import NeonBreakoutArena from "$lib/components/NeonBreakoutArena.svelte";
|
||||
import PressureMatrixViewer from "$lib/components/PressureMatrixViewer.svelte";
|
||||
import SignalChart from "$lib/components/SignalChart.svelte";
|
||||
import SpatialForcePanel from "$lib/components/SpatialForcePanel.svelte";
|
||||
import SummaryCurve from "$lib/components/SummaryCurve.svelte";
|
||||
import type {
|
||||
HudColorMapOption,
|
||||
HudSignalPanel,
|
||||
HudSpatialForce,
|
||||
HudSummary,
|
||||
LocaleCode,
|
||||
MatrixDisplayMode,
|
||||
PressureColorMapPreset,
|
||||
StageViewMode
|
||||
PressureColorMapPreset
|
||||
} from "$lib/types/hud";
|
||||
|
||||
export let locale: LocaleCode = "zh-CN";
|
||||
@@ -28,8 +24,6 @@
|
||||
export let rightPanels: HudSignalPanel[] = [];
|
||||
export let summary: HudSummary;
|
||||
export let pressureMatrix: number[] | null = null;
|
||||
export let spatialForce: HudSpatialForce | null = null;
|
||||
export let devkitSpatialForce: HudSpatialForce | null = null;
|
||||
export let showConfigPanel = false;
|
||||
export let configPanelTitle = "";
|
||||
export let configPanelHint = "";
|
||||
@@ -47,8 +41,6 @@
|
||||
export let rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
|
||||
export let colorMapPreset: PressureColorMapPreset = "emerald";
|
||||
export let matrixDisplayMode: MatrixDisplayMode = "dots";
|
||||
export let stageViewMode: StageViewMode = "webgl";
|
||||
export let modelUrl = "/models/je-skin-model.gltf";
|
||||
export let replaySectionLabel = "";
|
||||
export let replayPlayLabel = "";
|
||||
export let replayPauseLabel = "";
|
||||
@@ -92,7 +84,6 @@
|
||||
$: summaryCurveVisible = summary.points.length > 0 && summary.points.some((value) => Number.isFinite(value) && Math.abs(value) >= 0.0001);
|
||||
$: splitMatrixTitle = locale === "zh-CN" ? "数字矩阵" : "Matrix";
|
||||
$: splitMatrixHint = locale === "zh-CN" ? "实时压力数据 / 数字矩阵" : "Live pressure matrix";
|
||||
$: isModelStage = stageViewMode === "model3d";
|
||||
|
||||
function toPxNumber(rawValue: string): number {
|
||||
const value = Number.parseFloat(rawValue);
|
||||
@@ -185,20 +176,7 @@
|
||||
bind:this={stagePlaneEl}
|
||||
style="--panel-zone-top-dyn: {panelZoneTopPx}px; --rail-scale-left: {leftRailScale}; --rail-scale-right: {rightRailScale};"
|
||||
>
|
||||
{#if isModelStage}
|
||||
<div class="canvas-wrap">
|
||||
{#key modelUrl}
|
||||
<ModelStage
|
||||
{locale}
|
||||
{modelUrl}
|
||||
{pressureMatrix}
|
||||
{rangeMin}
|
||||
{rangeMax}
|
||||
{colorMapPreset}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
{:else if showPrecisionTestPanel}
|
||||
{#if showPrecisionTestPanel}
|
||||
<div class="split-game-wrap">
|
||||
<section class="split-panel split-matrix-panel">
|
||||
<header class="split-panel-head">
|
||||
@@ -254,7 +232,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showConfigPanel && !showPrecisionTestPanel && !isModelStage}
|
||||
{#if showConfigPanel && !showPrecisionTestPanel}
|
||||
<div class="config-panel-wrap">
|
||||
<ConfigPanel
|
||||
bind:matrixRows
|
||||
@@ -276,7 +254,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !showPrecisionTestPanel && !isModelStage}
|
||||
{#if !showPrecisionTestPanel}
|
||||
<div class="panel-zone" bind:this={panelZoneEl}>
|
||||
<aside class="side-rail left-rail">
|
||||
<div class="rail-stack" bind:this={leftStackEl}>
|
||||
@@ -325,42 +303,6 @@
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div
|
||||
class="panel-motion-shell"
|
||||
in:fly={{ x: 180, duration: 340, opacity: 0.08, easing: cubicOut }}
|
||||
out:fly={{ x: 180, duration: 280, opacity: 0.06, easing: cubicIn }}
|
||||
>
|
||||
<SpatialForcePanel
|
||||
{spatialForce}
|
||||
{locale}
|
||||
side="right"
|
||||
panelIndex={rightPanels.length}
|
||||
panelCode="ALG"
|
||||
panelTitle={locale === "zh-CN" ? "本地切向力" : "Local Tangential"}
|
||||
badgeLabel={locale === "zh-CN" ? "算法" : "ALGO"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="panel-motion-shell"
|
||||
in:fly={{ x: 180, duration: 340, opacity: 0.08, easing: cubicOut }}
|
||||
out:fly={{ x: 180, duration: 280, opacity: 0.06, easing: cubicIn }}
|
||||
>
|
||||
<SpatialForcePanel
|
||||
spatialForce={devkitSpatialForce}
|
||||
{locale}
|
||||
side="right"
|
||||
panelIndex={rightPanels.length + 1}
|
||||
panelCode="DKT"
|
||||
panelTitle={locale === "zh-CN" ? "DevKit 切向力" : "DevKit Tangential"}
|
||||
badgeLabel="DEVKIT"
|
||||
badgeTone="lime"
|
||||
showMetrics={false}
|
||||
requireMagnitude={false}
|
||||
compactMetaText={locale === "zh-CN" ? "等待 DevKit 角度流" : "Waiting for DevKit angle"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if summaryCurveVisible && summarySide === "right"}
|
||||
<div
|
||||
class="panel-motion-shell"
|
||||
@@ -384,7 +326,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if replayHasData && !showPrecisionTestPanel && !isModelStage}
|
||||
{#if replayHasData && !showPrecisionTestPanel}
|
||||
<aside class="replay-floating-panel" class:is-left={replaySide === "left"} class:is-right={replaySide === "right"}>
|
||||
<div class="replay-panel-head">
|
||||
<div class="replay-panel-title-group">
|
||||
@@ -422,7 +364,7 @@
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
{#if !showPrecisionTestPanel && !isModelStage}
|
||||
{#if !showPrecisionTestPanel}
|
||||
<div class="stage-bottom-overlay">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
HudNoticeTone,
|
||||
LocaleCode,
|
||||
MatrixDisplayMode,
|
||||
StageViewMode,
|
||||
WindowControlAction
|
||||
} from "$lib/types/hud";
|
||||
|
||||
@@ -35,10 +34,6 @@
|
||||
export let matrixViewNumericLabel = "";
|
||||
export let matrixViewDotsLabel = "";
|
||||
export let matrixDisplayMode: MatrixDisplayMode = "dots";
|
||||
export let stageModeLabel = "";
|
||||
export let stageModeWebglLabel = "";
|
||||
export let stageModeModelLabel = "";
|
||||
export let stageViewMode: StageViewMode = "webgl";
|
||||
export let connectActionLabel = "";
|
||||
export let disconnectActionLabel = "";
|
||||
export let exportActionLabel = "";
|
||||
@@ -61,7 +56,6 @@
|
||||
localechange: LocaleCode;
|
||||
configlink: string;
|
||||
matrixdisplaytoggle: boolean;
|
||||
stagemodechange: StageViewMode;
|
||||
portchange: string;
|
||||
serialrefresh: void;
|
||||
serialconnect: string;
|
||||
@@ -111,10 +105,6 @@
|
||||
dispatch("matrixdisplaytoggle", matrixDisplayMode !== "dots");
|
||||
}
|
||||
|
||||
function emitStageModeChange(nextMode: StageViewMode): void {
|
||||
dispatch("stagemodechange", nextMode);
|
||||
}
|
||||
|
||||
function emitPortChange(event: Event): void {
|
||||
const target = event.currentTarget as HTMLSelectElement;
|
||||
dispatch("portchange", target.value);
|
||||
@@ -227,28 +217,6 @@
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="stage-mode-switch" aria-label={stageModeLabel}>
|
||||
<span class="stage-mode-label">{stageModeLabel}</span>
|
||||
<div class="stage-mode-options" role="group" aria-label={stageModeLabel}>
|
||||
<button
|
||||
type="button"
|
||||
class="stage-mode-btn"
|
||||
class:is-active={stageViewMode === "webgl"}
|
||||
on:click={() => emitStageModeChange("webgl")}
|
||||
>
|
||||
{stageModeWebglLabel}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="stage-mode-btn"
|
||||
class:is-active={stageViewMode === "model3d"}
|
||||
on:click={() => emitStageModeChange("model3d")}
|
||||
>
|
||||
{stageModeModelLabel}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="state-card" aria-label={connectionLabel}>
|
||||
<span class="state-dot" class:ok={connectionTone === "ok"} class:warn={connectionTone === "warn"}></span>
|
||||
<span class="state-label">{connectionLabel}</span>
|
||||
@@ -517,8 +485,7 @@
|
||||
background: var(--panel-surface);
|
||||
}
|
||||
|
||||
.matrix-switch-wrap,
|
||||
.stage-mode-switch {
|
||||
.matrix-switch-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
@@ -529,8 +496,7 @@
|
||||
background: var(--panel-surface);
|
||||
}
|
||||
|
||||
.matrix-switch-label,
|
||||
.stage-mode-label {
|
||||
.matrix-switch-label {
|
||||
color: var(--panel-text-dim);
|
||||
font-size: 0.66rem;
|
||||
letter-spacing: 0.08em;
|
||||
@@ -621,45 +587,6 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stage-mode-options {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.18rem;
|
||||
padding: 0.16rem;
|
||||
border: 1px solid rgb(var(--hud-border-rgb) / 0.24);
|
||||
border-radius: 999px;
|
||||
background: rgb(var(--hud-surface-deep-rgb) / 0.8);
|
||||
}
|
||||
|
||||
.stage-mode-btn {
|
||||
min-block-size: 1.38rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 999px;
|
||||
padding: 0.18rem 0.54rem;
|
||||
background: transparent;
|
||||
color: rgb(var(--hud-text-dim-rgb) / 0.88);
|
||||
font: inherit;
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.04em;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 180ms ease,
|
||||
background-color 180ms ease,
|
||||
color 180ms ease,
|
||||
box-shadow 180ms ease;
|
||||
}
|
||||
|
||||
.stage-mode-btn:hover {
|
||||
color: rgb(var(--hud-text-main-rgb) / 0.96);
|
||||
}
|
||||
|
||||
.stage-mode-btn.is-active {
|
||||
border-color: rgb(var(--hud-cyan-rgb) / 0.42);
|
||||
background: rgb(var(--hud-cyan-rgb) / 0.14);
|
||||
color: rgb(var(--hud-text-main-rgb) / 0.98);
|
||||
box-shadow: 0 0 12px rgb(var(--hud-cyan-rgb) / 0.1);
|
||||
}
|
||||
|
||||
.state-dot {
|
||||
inline-size: 0.55rem;
|
||||
block-size: 0.55rem;
|
||||
@@ -1289,4 +1216,4 @@
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -1,603 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import { pressureColorPalettes } from "$lib/config/color-map";
|
||||
import type { LocaleCode, PressureColorMapPreset } from "$lib/types/hud";
|
||||
|
||||
type ModelLoadState = "loading" | "ready" | "missing" | "error";
|
||||
type SensorRegion = {
|
||||
id: string;
|
||||
rows: number;
|
||||
cols: number;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
rotation: number;
|
||||
};
|
||||
|
||||
export let locale: LocaleCode = "zh-CN";
|
||||
export let modelUrl = "/models/je-skin-model.glb";
|
||||
export let pressureMatrix: number[] | null = null;
|
||||
export let rangeMin = 0;
|
||||
export let rangeMax = 4095;
|
||||
export let colorMapPreset: PressureColorMapPreset = "emerald";
|
||||
|
||||
let rootEl: HTMLDivElement | undefined;
|
||||
let canvasEl: HTMLCanvasElement | undefined;
|
||||
let loadState: ModelLoadState = "loading";
|
||||
let loadProgress = 0;
|
||||
let loadError = "";
|
||||
|
||||
const FLOOR_Y = -1.15;
|
||||
const MODEL_FLOOR_CLEARANCE = 0.035;
|
||||
const MODEL_TARGET_HEIGHT = 6.55;
|
||||
const MODEL_MIN_SCALE = 0.02;
|
||||
const MODEL_MAX_SCALE = 80;
|
||||
const SENSOR_Z = 0.91;
|
||||
const FIXED_CAMERA_POSITION = new THREE.Vector3(-0.113, -0.149, 10.911);
|
||||
const FIXED_CAMERA_TARGET = new THREE.Vector3(0, 2.35, 0);
|
||||
const MODEL_FRONT_ROTATION_Y = -Math.PI / 2;
|
||||
const MODEL_UPRIGHT_ROTATION_Z =
|
||||
Math.PI * 1.5 - THREE.MathUtils.degToRad(2.2);
|
||||
const sensorRegions: SensorRegion[] = [
|
||||
{
|
||||
id: "thumb",
|
||||
rows: 12,
|
||||
cols: 7,
|
||||
x: -1.38,
|
||||
y: 1.98,
|
||||
width: 0.52,
|
||||
height: 0.98,
|
||||
rotation: THREE.MathUtils.degToRad(35),
|
||||
},
|
||||
{
|
||||
id: "index",
|
||||
rows: 12,
|
||||
cols: 7,
|
||||
x: -0.6,
|
||||
y: 3.75,
|
||||
width: 0.52,
|
||||
height: 0.98,
|
||||
rotation: THREE.MathUtils.degToRad(6),
|
||||
},
|
||||
{
|
||||
id: "middle",
|
||||
rows: 12,
|
||||
cols: 7,
|
||||
x: 0.1,
|
||||
y: 4.0,
|
||||
width: 0.58,
|
||||
height: 1.06,
|
||||
rotation: 0,
|
||||
},
|
||||
{
|
||||
id: "ring",
|
||||
rows: 12,
|
||||
cols: 7,
|
||||
x: 0.82,
|
||||
y: 3.9,
|
||||
width: 0.52,
|
||||
height: 0.96,
|
||||
rotation: THREE.MathUtils.degToRad(-5),
|
||||
},
|
||||
{
|
||||
id: "pinky",
|
||||
rows: 12,
|
||||
cols: 7,
|
||||
x: 1.42,
|
||||
y: 3.2,
|
||||
width: 0.46,
|
||||
height: 0.88,
|
||||
rotation: THREE.MathUtils.degToRad(-8),
|
||||
},
|
||||
{
|
||||
id: "palm-horizontal",
|
||||
rows: 5,
|
||||
cols: 14,
|
||||
x: 0.26,
|
||||
y: 1.68,
|
||||
width: 1.84,
|
||||
height: 0.5,
|
||||
rotation: THREE.MathUtils.degToRad(-4),
|
||||
},
|
||||
{
|
||||
id: "palm-vertical",
|
||||
rows: 11,
|
||||
cols: 4,
|
||||
x: 0.86,
|
||||
y: 0.5,
|
||||
width: 0.42,
|
||||
height: 1.26,
|
||||
rotation: THREE.MathUtils.degToRad(-4),
|
||||
},
|
||||
];
|
||||
|
||||
$: canvasLabel = locale === "zh-CN" ? "3D 模型" : "3D Model";
|
||||
|
||||
$: updateSensorColors();
|
||||
|
||||
let sensorGeometry: THREE.BufferGeometry | null = null;
|
||||
let sensorColors: Float32Array | null = null;
|
||||
let sensorPointCount = 0;
|
||||
|
||||
function disposeObject3D(object: THREE.Object3D): void {
|
||||
object.traverse((child) => {
|
||||
const mesh = child as THREE.Mesh;
|
||||
if (mesh.geometry) {
|
||||
mesh.geometry.dispose();
|
||||
}
|
||||
|
||||
const material = mesh.material;
|
||||
if (Array.isArray(material)) {
|
||||
for (const item of material) {
|
||||
item.dispose();
|
||||
}
|
||||
} else if (material) {
|
||||
material.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildPlaceholderModel(): THREE.Group {
|
||||
const group = new THREE.Group();
|
||||
const cyan = new THREE.Color(0x5ee7ff);
|
||||
const lime = new THREE.Color(0xa6ff7a);
|
||||
|
||||
const platform = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(5.8, 6.7, 0.36, 96),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: 0x0c1824,
|
||||
emissive: 0x07131f,
|
||||
metalness: 0.62,
|
||||
roughness: 0.34,
|
||||
}),
|
||||
);
|
||||
platform.position.y = 0.18;
|
||||
group.add(platform);
|
||||
|
||||
const ringGeometry = new THREE.TorusGeometry(4.35, 0.035, 10, 128);
|
||||
const ringMaterial = new THREE.MeshBasicMaterial({
|
||||
color: cyan,
|
||||
transparent: true,
|
||||
opacity: 0.78,
|
||||
});
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
const ring = new THREE.Mesh(ringGeometry, ringMaterial);
|
||||
ring.position.y = 0.52 + index * 0.52;
|
||||
ring.rotation.x = Math.PI / 2;
|
||||
group.add(ring);
|
||||
}
|
||||
|
||||
const coreMaterial = new THREE.MeshStandardMaterial({
|
||||
color: 0x1b2a38,
|
||||
emissive: 0x0a2632,
|
||||
metalness: 0.48,
|
||||
roughness: 0.42,
|
||||
transparent: true,
|
||||
opacity: 0.72,
|
||||
});
|
||||
const core = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(2.2, 3.4, 1.1),
|
||||
coreMaterial,
|
||||
);
|
||||
core.position.y = 2.4;
|
||||
core.rotation.y = -0.36;
|
||||
group.add(core);
|
||||
|
||||
const sensorMaterial = new THREE.MeshBasicMaterial({
|
||||
color: lime,
|
||||
transparent: true,
|
||||
opacity: 0.88,
|
||||
});
|
||||
for (let index = 0; index < 7; index += 1) {
|
||||
const bead = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.13, 18, 18),
|
||||
sensorMaterial,
|
||||
);
|
||||
bead.position.set(
|
||||
-0.72 + index * 0.24,
|
||||
3.18 + Math.sin(index * 0.72) * 0.18,
|
||||
0.6,
|
||||
);
|
||||
group.add(bead);
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
function mixColors(
|
||||
low: THREE.Color,
|
||||
high: THREE.Color,
|
||||
amount: number,
|
||||
): THREE.Color {
|
||||
return low.clone().lerp(high, clamp(amount, 0, 1));
|
||||
}
|
||||
|
||||
function buildSensorOverlay(): THREE.Points {
|
||||
const positions: number[] = [];
|
||||
|
||||
for (const region of sensorRegions) {
|
||||
const sin = Math.sin(region.rotation);
|
||||
const cos = Math.cos(region.rotation);
|
||||
const colSpan = Math.max(region.cols - 1, 1);
|
||||
const rowSpan = Math.max(region.rows - 1, 1);
|
||||
|
||||
for (let row = 0; row < region.rows; row += 1) {
|
||||
for (let col = 0; col < region.cols; col += 1) {
|
||||
const localX = (col / colSpan - 0.5) * region.width;
|
||||
const localY = (0.5 - row / rowSpan) * region.height;
|
||||
const x = region.x + localX * cos - localY * sin;
|
||||
const y = region.y + localX * sin + localY * cos;
|
||||
|
||||
positions.push(x, y, SENSOR_Z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sensorPointCount = positions.length / 3;
|
||||
sensorColors = new Float32Array(sensorPointCount * 3);
|
||||
sensorGeometry = new THREE.BufferGeometry();
|
||||
sensorGeometry.setAttribute(
|
||||
"position",
|
||||
new THREE.Float32BufferAttribute(positions, 3),
|
||||
);
|
||||
sensorGeometry.setAttribute(
|
||||
"color",
|
||||
new THREE.BufferAttribute(sensorColors, 3),
|
||||
);
|
||||
|
||||
const points = new THREE.Points(
|
||||
sensorGeometry,
|
||||
new THREE.PointsMaterial({
|
||||
size: 0.052,
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 0.92,
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
sizeAttenuation: true,
|
||||
}),
|
||||
);
|
||||
points.renderOrder = 20;
|
||||
points.name = "hand-sensor-dot-overlay";
|
||||
updateSensorColors();
|
||||
return points;
|
||||
}
|
||||
|
||||
function updateSensorColors(): void {
|
||||
if (!sensorGeometry || !sensorColors || sensorPointCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const palette =
|
||||
pressureColorPalettes[colorMapPreset] ?? pressureColorPalettes.emerald;
|
||||
const base = new THREE.Color(palette.labelZero);
|
||||
const mid = new THREE.Color(palette.surfaceHigh);
|
||||
const hot = new THREE.Color(palette.surfaceHot);
|
||||
const span = Math.max(rangeMax - rangeMin, 1);
|
||||
const dimFactor = pressureMatrix ? 0.9 : 0.42;
|
||||
|
||||
for (let index = 0; index < sensorPointCount; index += 1) {
|
||||
const value = pressureMatrix?.[index];
|
||||
const normalized = Number.isFinite(value)
|
||||
? clamp((Number(value) - rangeMin) / span, 0, 1)
|
||||
: 0;
|
||||
const color =
|
||||
normalized <= 0.5
|
||||
? mixColors(base, mid, normalized * 2)
|
||||
: mixColors(mid, hot, (normalized - 0.5) * 2);
|
||||
|
||||
sensorColors[index * 3] = color.r * dimFactor;
|
||||
sensorColors[index * 3 + 1] = color.g * dimFactor;
|
||||
sensorColors[index * 3 + 2] = color.b * dimFactor;
|
||||
}
|
||||
|
||||
const colorAttribute = sensorGeometry.getAttribute("color");
|
||||
if (colorAttribute) {
|
||||
colorAttribute.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
function materialToUnlit(material: THREE.Material): THREE.Material {
|
||||
const source = material as THREE.MeshStandardMaterial &
|
||||
THREE.MeshPhysicalMaterial;
|
||||
const unlit = new THREE.MeshBasicMaterial({
|
||||
color: source.color?.clone() ?? new THREE.Color(0xffffff),
|
||||
map: source.map ?? null,
|
||||
transparent: source.transparent,
|
||||
opacity: source.opacity,
|
||||
alphaMap: source.alphaMap ?? null,
|
||||
side: source.side,
|
||||
depthWrite: source.depthWrite,
|
||||
depthTest: source.depthTest,
|
||||
vertexColors: source.vertexColors,
|
||||
toneMapped: false,
|
||||
});
|
||||
|
||||
return unlit;
|
||||
}
|
||||
|
||||
function applyUnlitMaterials(object: THREE.Object3D): void {
|
||||
object.traverse((child) => {
|
||||
const mesh = child as THREE.Mesh;
|
||||
if (!mesh.isMesh || !mesh.material) {
|
||||
return;
|
||||
}
|
||||
|
||||
mesh.castShadow = false;
|
||||
mesh.receiveShadow = false;
|
||||
mesh.material = Array.isArray(mesh.material)
|
||||
? mesh.material.map(materialToUnlit)
|
||||
: materialToUnlit(mesh.material);
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeObjectToStage(object: THREE.Object3D): THREE.Box3 {
|
||||
object.rotation.set(0, MODEL_FRONT_ROTATION_Y, MODEL_UPRIGHT_ROTATION_Z);
|
||||
object.updateMatrixWorld(true);
|
||||
let bounds = new THREE.Box3().setFromObject(object);
|
||||
const size = bounds.getSize(new THREE.Vector3());
|
||||
const currentHeight = Math.max(size.y, 0.001);
|
||||
const scale = clamp(
|
||||
MODEL_TARGET_HEIGHT / currentHeight,
|
||||
MODEL_MIN_SCALE,
|
||||
MODEL_MAX_SCALE,
|
||||
);
|
||||
|
||||
object.scale.multiplyScalar(scale);
|
||||
object.updateMatrixWorld(true);
|
||||
|
||||
bounds = new THREE.Box3().setFromObject(object);
|
||||
const center = bounds.getCenter(new THREE.Vector3());
|
||||
object.position.x -= center.x;
|
||||
object.position.z -= center.z;
|
||||
object.position.y += FLOOR_Y + MODEL_FLOOR_CLEARANCE - bounds.min.y;
|
||||
object.updateMatrixWorld(true);
|
||||
|
||||
return new THREE.Box3().setFromObject(object);
|
||||
}
|
||||
|
||||
function frameObject(
|
||||
object: THREE.Object3D,
|
||||
camera: THREE.PerspectiveCamera,
|
||||
controls: OrbitControls,
|
||||
): void {
|
||||
normalizeObjectToStage(object);
|
||||
camera.position.copy(FIXED_CAMERA_POSITION);
|
||||
camera.near = 0.05;
|
||||
camera.far = 600;
|
||||
camera.updateProjectionMatrix();
|
||||
controls.target.copy(FIXED_CAMERA_TARGET);
|
||||
controls.minDistance = 2.4;
|
||||
controls.maxDistance = 32;
|
||||
controls.update();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!rootEl || !canvasEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
canvas: canvasEl,
|
||||
antialias: true,
|
||||
alpha: true,
|
||||
powerPreference: "high-performance",
|
||||
});
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||
renderer.setClearColor(0x03070d, 1);
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = 1.08;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.fog = new THREE.FogExp2(0x03070d, 0.028);
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(38, 1, 0.05, 600);
|
||||
camera.position.copy(FIXED_CAMERA_POSITION);
|
||||
camera.lookAt(FIXED_CAMERA_TARGET);
|
||||
|
||||
const controls = new OrbitControls(camera, canvasEl);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.08;
|
||||
controls.minDistance = 2.4;
|
||||
controls.maxDistance = 32;
|
||||
controls.target.copy(FIXED_CAMERA_TARGET);
|
||||
|
||||
const labGroup = new THREE.Group();
|
||||
scene.add(labGroup);
|
||||
|
||||
const grid = new THREE.GridHelper(42, 42, 0x63e6ff, 0x123047);
|
||||
grid.position.y = FLOOR_Y;
|
||||
const gridMaterial = grid.material;
|
||||
if (Array.isArray(gridMaterial)) {
|
||||
for (const material of gridMaterial) {
|
||||
material.transparent = true;
|
||||
material.opacity = 0.28;
|
||||
}
|
||||
} else {
|
||||
gridMaterial.transparent = true;
|
||||
gridMaterial.opacity = 0.28;
|
||||
}
|
||||
labGroup.add(grid);
|
||||
|
||||
const backGrid = new THREE.GridHelper(42, 42, 0x5ee7ff, 0x0c2436);
|
||||
backGrid.position.set(0, 9.5, -17);
|
||||
backGrid.rotation.x = Math.PI / 2;
|
||||
const backGridMaterial = backGrid.material;
|
||||
if (Array.isArray(backGridMaterial)) {
|
||||
for (const material of backGridMaterial) {
|
||||
material.transparent = true;
|
||||
material.opacity = 0.12;
|
||||
}
|
||||
} else {
|
||||
backGridMaterial.transparent = true;
|
||||
backGridMaterial.opacity = 0.12;
|
||||
}
|
||||
labGroup.add(backGrid);
|
||||
|
||||
const floor = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(42, 42),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: 0x050c14,
|
||||
metalness: 0.28,
|
||||
roughness: 0.64,
|
||||
transparent: true,
|
||||
opacity: 0.72,
|
||||
}),
|
||||
);
|
||||
floor.rotation.x = -Math.PI / 2;
|
||||
floor.position.y = FLOOR_Y - 0.018;
|
||||
labGroup.add(floor);
|
||||
|
||||
let activeModel: THREE.Object3D = buildPlaceholderModel();
|
||||
applyUnlitMaterials(activeModel);
|
||||
scene.add(activeModel);
|
||||
frameObject(activeModel, camera, controls);
|
||||
const sensorOverlay = buildSensorOverlay();
|
||||
scene.add(sensorOverlay);
|
||||
|
||||
const loader = new GLTFLoader();
|
||||
loader.load(
|
||||
modelUrl,
|
||||
(gltf: GLTF) => {
|
||||
scene.remove(activeModel);
|
||||
disposeObject3D(activeModel);
|
||||
activeModel = gltf.scene;
|
||||
applyUnlitMaterials(activeModel);
|
||||
scene.add(activeModel);
|
||||
frameObject(activeModel, camera, controls);
|
||||
loadState = "ready";
|
||||
loadProgress = 100;
|
||||
},
|
||||
(event) => {
|
||||
if (event.total > 0) {
|
||||
loadProgress = (event.loaded / event.total) * 100;
|
||||
} else {
|
||||
loadProgress = 12;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
loadError = message || "Unknown model loader error";
|
||||
loadState = message.toLowerCase().includes("404") ? "missing" : "error";
|
||||
loadProgress = 0;
|
||||
},
|
||||
);
|
||||
|
||||
const resize = () => {
|
||||
if (!rootEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = rootEl.clientWidth;
|
||||
const height = rootEl.clientHeight;
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderer.setSize(width, height, false);
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
};
|
||||
|
||||
resize();
|
||||
const resizeObserver = new ResizeObserver(resize);
|
||||
resizeObserver.observe(rootEl);
|
||||
|
||||
renderer.setAnimationLoop((timestamp) => {
|
||||
const seconds = timestamp / 1000;
|
||||
labGroup.position.y = Math.sin(seconds * 0.75) * 0.015;
|
||||
if (loadState !== "ready") {
|
||||
activeModel.rotation.y = seconds * 0.32;
|
||||
}
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
});
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
renderer.setAnimationLoop(null);
|
||||
controls.dispose();
|
||||
disposeObject3D(activeModel);
|
||||
disposeObject3D(sensorOverlay);
|
||||
disposeObject3D(labGroup);
|
||||
renderer.dispose();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="model-stage" bind:this={rootEl}>
|
||||
<canvas class="model-canvas" bind:this={canvasEl} aria-label={canvasLabel}
|
||||
></canvas>
|
||||
<div class="model-vignette" aria-hidden="true"></div>
|
||||
<div class="model-scanlines" aria-hidden="true"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.model-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
background: radial-gradient(
|
||||
circle at 52% 62%,
|
||||
rgb(94 231 255 / 0.12),
|
||||
transparent 26%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 24% 18%,
|
||||
rgb(166 255 122 / 0.07),
|
||||
transparent 24%
|
||||
),
|
||||
linear-gradient(180deg, #03070d 0%, #07111b 48%, #02050a 100%);
|
||||
}
|
||||
|
||||
.model-canvas,
|
||||
.model-vignette,
|
||||
.model-scanlines {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
}
|
||||
|
||||
.model-canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.model-vignette,
|
||||
.model-scanlines {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.model-vignette {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgb(0 0 0 / 0.36),
|
||||
transparent 22%,
|
||||
transparent 78%,
|
||||
rgb(0 0 0 / 0.34)
|
||||
),
|
||||
radial-gradient(circle at center, transparent 48%, rgb(0 0 0 / 0.58) 100%);
|
||||
}
|
||||
|
||||
.model-scanlines {
|
||||
opacity: 0.32;
|
||||
background: repeating-linear-gradient(
|
||||
180deg,
|
||||
rgb(94 231 255 / 0.045) 0,
|
||||
rgb(94 231 255 / 0.045) 1px,
|
||||
transparent 1px,
|
||||
transparent 4px
|
||||
);
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
</style>
|
||||
@@ -1,523 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { HudSpatialForce } from "$lib/types/hud";
|
||||
|
||||
export let spatialForce: HudSpatialForce | null = null;
|
||||
export let side: "left" | "right" = "right";
|
||||
export let panelIndex = 0;
|
||||
export let locale: "zh-CN" | "en-US" = "zh-CN";
|
||||
export let panelCode = "TAN";
|
||||
export let panelTitle = "";
|
||||
export let badgeLabel = "";
|
||||
export let badgeTone: "cyan" | "lime" | "orange" = "cyan";
|
||||
export let showMetrics = true;
|
||||
export let requireMagnitude = true;
|
||||
export let compactMetaText = "";
|
||||
|
||||
function formatValue(value: number | null, digits = 1): string {
|
||||
if (value === null || !Number.isFinite(value)) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
return value.toFixed(digits);
|
||||
}
|
||||
|
||||
function normalizeAngle(value: number): number {
|
||||
return ((value % 360) + 360) % 360;
|
||||
}
|
||||
|
||||
function shortestAngleDelta(from: number, to: number): number {
|
||||
const delta = ((to - from + 540) % 360) - 180;
|
||||
return delta === -180 ? 180 : delta;
|
||||
}
|
||||
|
||||
const jumpAngleThresholdDeg = 72;
|
||||
|
||||
let visualAngleDeg = 0;
|
||||
let previousRawAngleDeg: number | null = null;
|
||||
let snapVector = false;
|
||||
let snapResetFrame: number | null = null;
|
||||
|
||||
function setSnapVector(): void {
|
||||
snapVector = true;
|
||||
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (snapResetFrame !== null) {
|
||||
window.cancelAnimationFrame(snapResetFrame);
|
||||
}
|
||||
|
||||
snapResetFrame = window.requestAnimationFrame(() => {
|
||||
snapVector = false;
|
||||
snapResetFrame = null;
|
||||
});
|
||||
}
|
||||
|
||||
function updateVisualAngle(rawAngleDeg: number, active: boolean): void {
|
||||
if (!active) {
|
||||
previousRawAngleDeg = null;
|
||||
visualAngleDeg = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousRawAngleDeg === null) {
|
||||
previousRawAngleDeg = rawAngleDeg;
|
||||
visualAngleDeg = rawAngleDeg;
|
||||
return;
|
||||
}
|
||||
|
||||
const delta = shortestAngleDelta(previousRawAngleDeg, rawAngleDeg);
|
||||
if (Math.abs(delta) < 0.001) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs(delta) >= jumpAngleThresholdDeg) {
|
||||
setSnapVector();
|
||||
}
|
||||
|
||||
visualAngleDeg += delta;
|
||||
previousRawAngleDeg = rawAngleDeg;
|
||||
}
|
||||
|
||||
$: i18n =
|
||||
locale === "zh-CN"
|
||||
? {
|
||||
title: "切向力方向",
|
||||
waiting: "等待数据",
|
||||
angle: "ANGLE",
|
||||
heading: "方向角",
|
||||
strength: "强度",
|
||||
confidence: "置信度"
|
||||
}
|
||||
: {
|
||||
title: "Tangential Direction",
|
||||
waiting: "Waiting",
|
||||
angle: "ANGLE",
|
||||
heading: "Heading",
|
||||
strength: "Strength",
|
||||
confidence: "Confidence"
|
||||
};
|
||||
$: resolvedTitle = panelTitle || i18n.title;
|
||||
$: resolvedBadgeLabel = badgeLabel || i18n.angle;
|
||||
$: resolvedCompactMetaText =
|
||||
compactMetaText || (locale === "zh-CN" ? "仅使用角度流" : "Angle stream only");
|
||||
|
||||
$: hasData =
|
||||
spatialForce !== null &&
|
||||
Number.isFinite(spatialForce.angleDeg) &&
|
||||
(!requireMagnitude || Number.isFinite(spatialForce.magnitude));
|
||||
$: angleDeg = hasData ? normalizeAngle(spatialForce?.angleDeg ?? 0) : 0;
|
||||
$: updateVisualAngle(angleDeg, hasData);
|
||||
$: magnitude = hasData ? spatialForce?.magnitude ?? 0 : null;
|
||||
$: confidence = hasData ? (spatialForce?.confidence ?? 0) * 100 : null;
|
||||
</script>
|
||||
|
||||
<article
|
||||
class="signal-panel spatial-panel side-{side}"
|
||||
class:is-empty={!hasData}
|
||||
aria-hidden={false}
|
||||
style="--panel-index: {panelIndex};"
|
||||
>
|
||||
<header class="panel-head">
|
||||
<div class="head-text">
|
||||
<p class="panel-code">{panelCode}</p>
|
||||
<p class="panel-title">{resolvedTitle}</p>
|
||||
</div>
|
||||
|
||||
<div class="icon-layer" aria-hidden="true">
|
||||
<span class={`icon-chip tone-${badgeTone}`}>{resolvedBadgeLabel}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="compass-stage">
|
||||
<div class="compass-core">
|
||||
<div class="compass-ring compass-ring-outer"></div>
|
||||
<div class="compass-ring compass-ring-inner"></div>
|
||||
<div class="compass-axis axis-horizontal"></div>
|
||||
<div class="compass-axis axis-vertical"></div>
|
||||
{#if hasData}
|
||||
<div
|
||||
class="compass-vector"
|
||||
class:is-snap={snapVector}
|
||||
style="transform: translateY(-50%) rotate({-visualAngleDeg}deg);"
|
||||
>
|
||||
<span class="vector-shaft"></span>
|
||||
<span class="vector-head"></span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="compass-center"></div>
|
||||
<span class="compass-label label-top">90</span>
|
||||
<span class="compass-label label-right">0</span>
|
||||
<span class="compass-label label-bottom">270</span>
|
||||
<span class="compass-label label-left">180</span>
|
||||
</div>
|
||||
|
||||
{#if !hasData}
|
||||
<div class="empty-state">
|
||||
<span>{i18n.waiting}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="angle-stage">
|
||||
<p class="angle-label">{i18n.heading}</p>
|
||||
{#if showMetrics}
|
||||
<p class="angle-meta">{i18n.strength}: {formatValue(magnitude, 2)}</p>
|
||||
<p class="angle-meta">{i18n.confidence}: {hasData ? `${formatValue(confidence, 0)}%` : "--"}</p>
|
||||
{:else}
|
||||
<p class="angle-meta">{resolvedCompactMetaText}</p>
|
||||
<p class="angle-meta">{hasData ? (locale === "zh-CN" ? "实时对比中" : "Live comparison") : "--"}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.signal-panel {
|
||||
--offset-x: 12%;
|
||||
--enter-ms: 1800ms;
|
||||
--fade-ms: 1000ms;
|
||||
overflow: hidden;
|
||||
inline-size: min(100%, clamp(34rem, 44vw, 44rem));
|
||||
justify-self: start;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: 0.68rem;
|
||||
padding: 0.88rem 0.96rem 1rem;
|
||||
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.42);
|
||||
border-radius: 0.92rem;
|
||||
background:
|
||||
linear-gradient(160deg, rgb(var(--hud-surface-alt-rgb) / 0.76) 0%, rgb(var(--hud-surface-rgb) / 0.62) 48%, rgb(var(--hud-surface-deep-rgb) / 0.76) 100%),
|
||||
radial-gradient(circle at 12% 0, rgb(var(--hud-glow-rgb) / 0.1), transparent 40%);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgb(var(--hud-border-strong-rgb) / 0.08),
|
||||
inset 0 -24px 32px rgb(0 0 0 / 0.48),
|
||||
0 0 14px rgb(var(--hud-glow-rgb) / 0.14);
|
||||
opacity: 1;
|
||||
transform: translateX(0) scale(1) rotate(0);
|
||||
transition:
|
||||
opacity var(--fade-ms) cubic-bezier(0.18, 0.88, 0.3, 1),
|
||||
transform var(--enter-ms) cubic-bezier(0.2, 0.9, 0.28, 1),
|
||||
border-color 460ms ease,
|
||||
filter 760ms ease;
|
||||
transition-delay: calc(var(--panel-index) * 140ms);
|
||||
}
|
||||
|
||||
.signal-panel.side-left {
|
||||
--offset-x: -132%;
|
||||
}
|
||||
|
||||
.signal-panel.side-right {
|
||||
--offset-x: 132%;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.spatial-panel.is-empty {
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 0.4rem;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.head-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panel-code {
|
||||
margin: 0;
|
||||
font-size: 0.63rem;
|
||||
color: rgb(var(--hud-text-dim-rgb) / 0.88);
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin: 0.12rem 0 0;
|
||||
font-size: 1.08rem;
|
||||
color: rgb(var(--hud-text-main-rgb) / 0.96);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.icon-layer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.26rem;
|
||||
}
|
||||
|
||||
.icon-chip {
|
||||
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.44);
|
||||
border-radius: 999px;
|
||||
padding: 0.08rem 0.36rem;
|
||||
font-size: 0.58rem;
|
||||
letter-spacing: 0.08em;
|
||||
color: rgb(var(--hud-text-main-rgb) / 0.94);
|
||||
background: rgb(var(--hud-surface-rgb) / 0.66);
|
||||
}
|
||||
|
||||
.icon-chip.tone-cyan {
|
||||
border-color: rgb(var(--hud-cyan-rgb) / 0.54);
|
||||
}
|
||||
|
||||
.icon-chip.tone-lime {
|
||||
border-color: rgb(var(--hud-lime-rgb) / 0.54);
|
||||
}
|
||||
|
||||
.icon-chip.tone-orange {
|
||||
border-color: rgb(var(--hud-orange-rgb) / 0.54);
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(10rem, 0.9fr);
|
||||
gap: 0.72rem;
|
||||
block-size: clamp(12rem, 15.5vw, 15rem);
|
||||
min-block-size: clamp(12rem, 15.5vw, 15rem);
|
||||
}
|
||||
|
||||
.compass-stage {
|
||||
position: relative;
|
||||
min-block-size: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.32);
|
||||
border-radius: 0.62rem;
|
||||
background:
|
||||
linear-gradient(180deg, rgb(var(--hud-surface-alt-rgb) / 0.68), rgb(var(--hud-surface-deep-rgb) / 0.78)),
|
||||
radial-gradient(circle at 50% 0, rgb(var(--hud-glow-rgb) / 0.09), transparent 45%);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.compass-core {
|
||||
position: relative;
|
||||
inline-size: min(72%, 13rem);
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.compass-ring,
|
||||
.compass-axis,
|
||||
.compass-center,
|
||||
.compass-vector {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.compass-ring {
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.compass-ring-outer {
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
border: 1px solid rgb(var(--hud-cyan-rgb) / 0.28);
|
||||
box-shadow: 0 0 18px rgb(var(--hud-glow-rgb) / 0.08);
|
||||
}
|
||||
|
||||
.compass-ring-inner {
|
||||
inline-size: 62%;
|
||||
block-size: 62%;
|
||||
border: 1px dashed rgb(var(--hud-border-strong-rgb) / 0.3);
|
||||
}
|
||||
|
||||
.compass-axis {
|
||||
background: rgb(var(--hud-border-strong-rgb) / 0.18);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.axis-horizontal {
|
||||
inline-size: 86%;
|
||||
block-size: 1px;
|
||||
}
|
||||
|
||||
.axis-vertical {
|
||||
inline-size: 1px;
|
||||
block-size: 86%;
|
||||
}
|
||||
|
||||
.compass-vector {
|
||||
inline-size: 42%;
|
||||
block-size: 0.9rem;
|
||||
transform-origin: 0 50%;
|
||||
transition: transform 220ms ease;
|
||||
}
|
||||
|
||||
.compass-vector.is-snap {
|
||||
transition-duration: 0ms;
|
||||
}
|
||||
|
||||
.vector-shaft {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0.7rem;
|
||||
block-size: 2px;
|
||||
transform: translateY(-50%);
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, rgb(var(--hud-cyan-rgb) / 0.18), rgb(var(--hud-cyan-rgb) / 0.96));
|
||||
box-shadow: 0 0 14px rgb(var(--hud-cyan-rgb) / 0.2);
|
||||
}
|
||||
|
||||
.vector-head {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
inline-size: 0;
|
||||
block-size: 0;
|
||||
transform: translateY(-50%);
|
||||
border-top: 0.36rem solid transparent;
|
||||
border-bottom: 0.36rem solid transparent;
|
||||
border-left: 0.7rem solid rgb(var(--hud-lime-rgb) / 0.96);
|
||||
filter: drop-shadow(0 0 8px rgb(var(--hud-lime-rgb) / 0.24));
|
||||
}
|
||||
|
||||
.compass-center {
|
||||
inline-size: 0.56rem;
|
||||
block-size: 0.56rem;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 50%;
|
||||
background: rgb(var(--hud-text-main-rgb) / 0.92);
|
||||
box-shadow: 0 0 10px rgb(var(--hud-text-main-rgb) / 0.12);
|
||||
}
|
||||
|
||||
.compass-label {
|
||||
position: absolute;
|
||||
font-size: 0.58rem;
|
||||
color: rgb(var(--hud-text-dim-rgb) / 0.8);
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.label-top {
|
||||
top: -0.9rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.label-right {
|
||||
top: 50%;
|
||||
right: -1rem;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.label-bottom {
|
||||
bottom: -0.9rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.label-left {
|
||||
top: 50%;
|
||||
left: -1.35rem;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgb(var(--hud-text-dim-rgb) / 0.76);
|
||||
font-size: 0.66rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
background: linear-gradient(180deg, rgb(var(--hud-surface-deep-rgb) / 0.06), rgb(var(--hud-surface-deep-rgb) / 0.18));
|
||||
}
|
||||
|
||||
.angle-stage {
|
||||
border: 1px solid rgb(var(--hud-border-rgb) / 0.26);
|
||||
border-radius: 0.62rem;
|
||||
padding: 0.9rem 0.85rem;
|
||||
block-size: 100%;
|
||||
min-block-size: 0;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(180deg, rgb(var(--hud-surface-rgb) / 0.72), rgb(var(--hud-surface-deep-rgb) / 0.84)),
|
||||
radial-gradient(circle at 50% 0, rgb(var(--hud-glow-rgb) / 0.05), transparent 58%);
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto;
|
||||
align-content: center;
|
||||
justify-items: start;
|
||||
gap: 0.36rem;
|
||||
}
|
||||
|
||||
.angle-label {
|
||||
margin: 0;
|
||||
color: rgb(var(--hud-text-dim-rgb) / 0.82);
|
||||
font-size: 0.68rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.angle-meta {
|
||||
margin: 0;
|
||||
inline-size: 10rem;
|
||||
min-block-size: 1rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: rgb(var(--hud-text-dim-rgb) / 0.84);
|
||||
font-size: 0.68rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.signal-panel {
|
||||
inline-size: min(100%, clamp(28rem, 40vw, 38rem));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 900px) {
|
||||
.signal-panel {
|
||||
inline-size: min(100%, clamp(28rem, 38vw, 36rem));
|
||||
padding: 0.7rem 0.76rem 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 760px) {
|
||||
.signal-panel {
|
||||
inline-size: min(100%, clamp(24rem, 34vw, 30rem));
|
||||
padding: 0.62rem 0.68rem 0.72rem;
|
||||
gap: 0.48rem;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
block-size: clamp(9rem, 10vw, 10.8rem);
|
||||
min-block-size: clamp(9rem, 10vw, 10.8rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 680px) {
|
||||
.signal-panel {
|
||||
inline-size: min(100%, clamp(20rem, 28vw, 26rem));
|
||||
padding: 0.52rem 0.58rem 0.6rem;
|
||||
gap: 0.36rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.signal-panel {
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
grid-template-columns: 1fr;
|
||||
block-size: auto;
|
||||
min-block-size: auto;
|
||||
}
|
||||
|
||||
.compass-core {
|
||||
inline-size: min(58vw, 12rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,6 @@ export type LocaleCode = "zh-CN" | "en-US";
|
||||
export type WindowControlAction = "minimize" | "toggle-maximize" | "close";
|
||||
|
||||
export type ConnectionState = "online" | "connecting" | "offline";
|
||||
export type StageViewMode = "webgl" | "model3d";
|
||||
|
||||
export type StageStatusTone = "ok" | "warn" | "idle";
|
||||
export type HudNoticeTone = "ok" | "warn" | "info";
|
||||
@@ -41,18 +40,11 @@ export interface HudSignalPanel {
|
||||
max: number | null;
|
||||
}
|
||||
|
||||
export interface HudSpatialForce {
|
||||
angleDeg: number;
|
||||
magnitude: number;
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
export interface HudPacket {
|
||||
ts: number;
|
||||
panels: HudSignalPanel[];
|
||||
summary: HudSummary;
|
||||
pressureMatrix: number[] | null;
|
||||
spatialForce: HudSpatialForce | null;
|
||||
}
|
||||
|
||||
export interface HudSummary {
|
||||
@@ -94,9 +86,6 @@ export interface HudCopy {
|
||||
matrixViewLabel: string;
|
||||
matrixViewNumericLabel: string;
|
||||
matrixViewDotsLabel: string;
|
||||
stageModeLabel: string;
|
||||
stageModeWebglLabel: string;
|
||||
stageModeModelLabel: string;
|
||||
resetConfigLabel: string;
|
||||
applyLiveHint: string;
|
||||
runtimeReady: string;
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
HudConfigLink,
|
||||
HudNoticeTone,
|
||||
HudPacket,
|
||||
HudSpatialForce,
|
||||
PressureColorMapPreset,
|
||||
HudSignalPanel,
|
||||
HudSignalSeries,
|
||||
@@ -34,7 +33,6 @@
|
||||
SerialRecordStateResult,
|
||||
SerialImportResult,
|
||||
SignalTone,
|
||||
StageViewMode,
|
||||
WindowControlAction
|
||||
} from "$lib/types/hud";
|
||||
|
||||
@@ -46,13 +44,6 @@
|
||||
dtsMs: number;
|
||||
}
|
||||
|
||||
interface DevKitPztAngleEvent {
|
||||
seq: number;
|
||||
timestampMs: number;
|
||||
dtsMs: number;
|
||||
angle: number;
|
||||
}
|
||||
|
||||
const copyByLocale: Record<LocaleCode, HudCopy> = {
|
||||
"zh-CN": {
|
||||
appName: "JE-Skin",
|
||||
@@ -71,9 +62,6 @@
|
||||
matrixViewLabel: "矩阵模式",
|
||||
matrixViewNumericLabel: "数字矩阵",
|
||||
matrixViewDotsLabel: "点矩阵",
|
||||
stageModeLabel: "渲染模式",
|
||||
stageModeWebglLabel: "WebGL",
|
||||
stageModeModelLabel: "3D 模型",
|
||||
resetConfigLabel: "恢复默认",
|
||||
applyLiveHint: "实时生效 / 矩阵尺寸变更将重建 viewer",
|
||||
runtimeReady: "WEBGL2 READY",
|
||||
@@ -133,9 +121,6 @@
|
||||
matrixViewLabel: "Matrix Mode",
|
||||
matrixViewNumericLabel: "Numeric",
|
||||
matrixViewDotsLabel: "Dots",
|
||||
stageModeLabel: "Render Mode",
|
||||
stageModeWebglLabel: "WebGL",
|
||||
stageModeModelLabel: "3D Model",
|
||||
resetConfigLabel: "Reset",
|
||||
applyLiveHint: "Live apply / size changes recreate the viewer",
|
||||
runtimeReady: "WEBGL2 READY",
|
||||
@@ -236,15 +221,12 @@
|
||||
let signalPanels: HudSignalPanel[] = buildInactivePanels();
|
||||
let summary: HudSummary = buildEmptySummary();
|
||||
let pressureMatrix: number[] | null = null;
|
||||
let spatialForce: HudSpatialForce | null = null;
|
||||
let devkitSpatialForce: HudSpatialForce | null = null;
|
||||
let matrixRows = 12;
|
||||
let matrixCols = 7;
|
||||
let rangeMin = DEFAULT_PRESSURE_RANGE_MIN;
|
||||
let rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
|
||||
let colorMapPreset: PressureColorMapPreset = "emerald";
|
||||
let matrixDisplayMode: MatrixDisplayMode = "dots";
|
||||
let stageViewMode: StageViewMode = "webgl";
|
||||
let replayFrames: ReplayFrame[] = [];
|
||||
let replayCurrentIndex = 0;
|
||||
let replayHasDisplayedFrame = false;
|
||||
@@ -278,7 +260,6 @@
|
||||
rowsKept: number;
|
||||
} | null = null;
|
||||
let devkitStatusTimer: number | null = null;
|
||||
let devkitSpatialForceClearTimer: number | null = null;
|
||||
let sessionStartedAt: number = Date.now();
|
||||
|
||||
$: uiCopy = copyByLocale[locale];
|
||||
@@ -306,35 +287,6 @@
|
||||
return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
|
||||
}
|
||||
|
||||
function serialProtocolForStage(mode: StageViewMode = stageViewMode): "tactileA" | "handGateway" {
|
||||
return mode === "model3d" ? "handGateway" : "tactileA";
|
||||
}
|
||||
|
||||
function clearDevkitSpatialForce(): void {
|
||||
devkitSpatialForce = null;
|
||||
if (devkitSpatialForceClearTimer != null && typeof window !== "undefined") {
|
||||
window.clearTimeout(devkitSpatialForceClearTimer);
|
||||
devkitSpatialForceClearTimer = null;
|
||||
}
|
||||
hasSignalData = signalPanels.length > 0 || summary.points.length > 0 || spatialForce !== null;
|
||||
}
|
||||
|
||||
function scheduleDevkitSpatialForceClear(): void {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (devkitSpatialForceClearTimer != null) {
|
||||
window.clearTimeout(devkitSpatialForceClearTimer);
|
||||
}
|
||||
|
||||
devkitSpatialForceClearTimer = window.setTimeout(() => {
|
||||
devkitSpatialForce = null;
|
||||
devkitSpatialForceClearTimer = null;
|
||||
hasSignalData = signalPanels.length > 0 || summary.points.length > 0 || spatialForce !== null;
|
||||
}, 420);
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
@@ -757,8 +709,6 @@
|
||||
|
||||
function resetReplayVisualState(): void {
|
||||
pressureMatrix = buildZeroMatrix();
|
||||
spatialForce = null;
|
||||
clearDevkitSpatialForce();
|
||||
signalPanels = buildInactivePanels();
|
||||
summary = buildEmptySummary();
|
||||
hasSignalData = false;
|
||||
@@ -794,8 +744,6 @@
|
||||
replayHasDisplayedFrame = true;
|
||||
replayProgress = replayFrames.length > 1 ? safeIndex / (replayFrames.length - 1) : 1;
|
||||
pressureMatrix = frameValuesToMatrix(replayFrames[safeIndex].values);
|
||||
spatialForce = null;
|
||||
clearDevkitSpatialForce();
|
||||
signalPanels = buildInactivePanels();
|
||||
summary = buildReplaySummaryAt(safeIndex);
|
||||
hasSignalData = true;
|
||||
@@ -1050,12 +998,7 @@
|
||||
summary = packet.summary;
|
||||
}
|
||||
pressureMatrix = packet.pressureMatrix;
|
||||
spatialForce = packet.spatialForce ?? null;
|
||||
hasSignalData =
|
||||
signalPanels.length > 0 ||
|
||||
packet.summary.points.length > 0 ||
|
||||
spatialForce !== null ||
|
||||
devkitSpatialForce !== null;
|
||||
hasSignalData = signalPanels.length > 0 || packet.summary.points.length > 0;
|
||||
}
|
||||
|
||||
function clearHudPanels(): void {
|
||||
@@ -1063,19 +1006,17 @@
|
||||
signalPanels = buildInactivePanels();
|
||||
summary = buildEmptySummary();
|
||||
pressureMatrix = null;
|
||||
spatialForce = null;
|
||||
clearDevkitSpatialForce();
|
||||
}
|
||||
|
||||
function startMockFeed(push: (packet: HudPacket) => void): () => void {
|
||||
let panels = buildInactivePanels();
|
||||
let summaryValue = buildSummary(createSummaryPoints(randomBetween(480, 1440)));
|
||||
push({ ts: Date.now(), panels, summary: summaryValue, pressureMatrix: null, spatialForce: null });
|
||||
push({ ts: Date.now(), panels, summary: summaryValue, pressureMatrix: null });
|
||||
|
||||
const timerId = window.setInterval(() => {
|
||||
summaryValue = evolveSummary(summaryValue);
|
||||
|
||||
push({ ts: Date.now(), panels, summary: summaryValue, pressureMatrix: null, spatialForce: null });
|
||||
push({ ts: Date.now(), panels, summary: summaryValue, pressureMatrix: null });
|
||||
}, signalRenderTickMs);
|
||||
|
||||
return () => {
|
||||
@@ -1449,10 +1390,7 @@
|
||||
connectionNotice = "";
|
||||
|
||||
try {
|
||||
const result = await invoke<SerialConnectResult>("serial_connect", {
|
||||
port: event.detail,
|
||||
protocol: serialProtocolForStage()
|
||||
});
|
||||
const result = await invoke<SerialConnectResult>("serial_connect", { port: event.detail });
|
||||
connectionState = result.connected ? "online" : "offline";
|
||||
serialPortValue = result.port;
|
||||
connectionNotice = "";
|
||||
@@ -1706,7 +1644,6 @@
|
||||
|
||||
function handleConfigLink(event: CustomEvent<string>): void {
|
||||
if (event.detail === "precision-test") {
|
||||
stageViewMode = "webgl";
|
||||
isPrecisionTestOpen = !isPrecisionTestOpen;
|
||||
isConfigPanelOpen = false;
|
||||
isDevKitConfigOpen = false;
|
||||
@@ -1714,7 +1651,6 @@
|
||||
}
|
||||
|
||||
if (event.detail === "settings") {
|
||||
stageViewMode = "webgl";
|
||||
isPrecisionTestOpen = false;
|
||||
isConfigPanelOpen = !isConfigPanelOpen;
|
||||
isDevKitConfigOpen = false;
|
||||
@@ -1807,49 +1743,9 @@
|
||||
matrixDisplayMode = event.detail ? "dots" : "numeric";
|
||||
}
|
||||
|
||||
async function handleStageModeChange(event: CustomEvent<StageViewMode>): Promise<void> {
|
||||
const previousMode = stageViewMode;
|
||||
stageViewMode = event.detail;
|
||||
if (stageViewMode === "model3d") {
|
||||
isPrecisionTestOpen = false;
|
||||
isConfigPanelOpen = false;
|
||||
}
|
||||
|
||||
if (
|
||||
previousMode === stageViewMode ||
|
||||
connectionState !== "online" ||
|
||||
!serialPortValue ||
|
||||
!isTauriRuntime()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
connectionState = "connecting";
|
||||
connectionNotice = "";
|
||||
try {
|
||||
await invoke<SerialConnectResult>("serial_disconnect");
|
||||
const result = await invoke<SerialConnectResult>("serial_connect", {
|
||||
port: serialPortValue,
|
||||
protocol: serialProtocolForStage(stageViewMode)
|
||||
});
|
||||
connectionState = result.connected ? "online" : "offline";
|
||||
serialPortValue = result.port;
|
||||
clearHudPanels();
|
||||
console.info(`[serial] switched protocol to ${serialProtocolForStage(stageViewMode)}`);
|
||||
} catch (error) {
|
||||
connectionState = "offline";
|
||||
connectionNotice = resolveSerialNotice(error, "connect");
|
||||
connectionNoticeTone = "warn";
|
||||
clearHudPanels();
|
||||
console.error("Serial protocol switch failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let disposed = false;
|
||||
let unlistenHudStream: UnlistenFn | null = null;
|
||||
let unlistenHandGatewayRaw: UnlistenFn | null = null;
|
||||
let unlistenHandGatewayPressureMatrix: UnlistenFn | null = null;
|
||||
let unlistenDevkitPztAngle: UnlistenFn | null = null;
|
||||
let stopMockFeed: (() => void) | null = null;
|
||||
|
||||
@@ -1874,61 +1770,12 @@
|
||||
.catch((error) => {
|
||||
console.error("Failed to listen for hud_stream:", error);
|
||||
});
|
||||
void listen<number[]>("hand_gateway_raw", (event) => {
|
||||
const rawHex = event.payload
|
||||
.map((byte) => byte.toString(16).padStart(2, "0").toUpperCase())
|
||||
.join(" ");
|
||||
console.info(`[hand_gateway][raw] ${rawHex}`);
|
||||
})
|
||||
.then((unlisten) => {
|
||||
if (disposed) {
|
||||
unlisten();
|
||||
return;
|
||||
}
|
||||
|
||||
unlistenHandGatewayRaw = unlisten;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to listen for hand_gateway_raw:", error);
|
||||
});
|
||||
void listen<number[]>("hand_gateway_pressure_matrix", (event) => {
|
||||
if (replayHasData) {
|
||||
return;
|
||||
void listen<{ seq: number; timestampMs: number; dtsMs: number; angle: number }>(
|
||||
"devkit_pzt_angle",
|
||||
(event) => {
|
||||
console.log("[devkit_pzt_angle]", event.payload);
|
||||
}
|
||||
|
||||
pressureMatrix = event.payload;
|
||||
hasSignalData = event.payload.length > 0;
|
||||
})
|
||||
.then((unlisten) => {
|
||||
if (disposed) {
|
||||
unlisten();
|
||||
return;
|
||||
}
|
||||
|
||||
unlistenHandGatewayPressureMatrix = unlisten;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to listen for hand_gateway_pressure_matrix:", error);
|
||||
});
|
||||
void listen<DevKitPztAngleEvent>("devkit_pzt_angle", (event) => {
|
||||
const angleDeg = Number(event.payload.angle);
|
||||
if (!Number.isFinite(angleDeg)) {
|
||||
clearDevkitSpatialForce();
|
||||
return;
|
||||
}
|
||||
|
||||
devkitSpatialForce = {
|
||||
angleDeg,
|
||||
magnitude: 0,
|
||||
confidence: 0
|
||||
};
|
||||
scheduleDevkitSpatialForceClear();
|
||||
hasSignalData =
|
||||
signalPanels.length > 0 ||
|
||||
summary.points.length > 0 ||
|
||||
spatialForce !== null ||
|
||||
devkitSpatialForce !== null;
|
||||
})
|
||||
)
|
||||
.then((unlisten) => {
|
||||
if (disposed) {
|
||||
unlisten();
|
||||
@@ -1947,11 +1794,8 @@
|
||||
return () => {
|
||||
disposed = true;
|
||||
pauseReplayPlayback();
|
||||
clearDevkitSpatialForce();
|
||||
stopMockFeed?.();
|
||||
unlistenHudStream?.();
|
||||
unlistenHandGatewayRaw?.();
|
||||
unlistenHandGatewayPressureMatrix?.();
|
||||
unlistenDevkitPztAngle?.();
|
||||
if (devkitStatusTimer != null) {
|
||||
window.clearInterval(devkitStatusTimer);
|
||||
@@ -1994,10 +1838,6 @@
|
||||
matrixViewNumericLabel={uiCopy.matrixViewNumericLabel}
|
||||
matrixViewDotsLabel={uiCopy.matrixViewDotsLabel}
|
||||
{matrixDisplayMode}
|
||||
stageModeLabel={uiCopy.stageModeLabel}
|
||||
stageModeWebglLabel={uiCopy.stageModeWebglLabel}
|
||||
stageModeModelLabel={uiCopy.stageModeModelLabel}
|
||||
{stageViewMode}
|
||||
connectActionLabel={uiCopy.connectActionLabel}
|
||||
disconnectActionLabel={uiCopy.disconnectActionLabel}
|
||||
exportActionLabel={uiCopy.exportActionLabel}
|
||||
@@ -2020,7 +1860,6 @@
|
||||
on:portchange={handlePortChange}
|
||||
on:configlink={handleConfigLink}
|
||||
on:matrixdisplaytoggle={handleMatrixDisplayToggle}
|
||||
on:stagemodechange={handleStageModeChange}
|
||||
on:serialrefresh={handleSerialRefresh}
|
||||
on:serialconnect={handleSerialConnect}
|
||||
on:serialexport={handleSerialExportRequest}
|
||||
@@ -2041,7 +1880,6 @@
|
||||
bind:rangeMax
|
||||
bind:colorMapPreset
|
||||
bind:matrixDisplayMode
|
||||
{stageViewMode}
|
||||
configPanelTitle={uiCopy.configPanelTitle}
|
||||
configPanelHint={uiCopy.configPanelHint}
|
||||
matrixSizeLabel={uiCopy.matrixSizeLabel}
|
||||
@@ -2068,8 +1906,6 @@
|
||||
leftPanels={leftSignalPanels}
|
||||
rightPanels={rightSignalPanels}
|
||||
{pressureMatrix}
|
||||
{spatialForce}
|
||||
{devkitSpatialForce}
|
||||
showConfigPanel={isConfigPanelOpen}
|
||||
showPrecisionTestPanel={isPrecisionTestOpen}
|
||||
{summary}
|
||||
@@ -2080,7 +1916,7 @@
|
||||
on:replayclose={handleReplayClose}
|
||||
on:configclose={() => (isConfigPanelOpen = false)}
|
||||
>
|
||||
{#if !isPrecisionTestOpen && stageViewMode === "webgl"}
|
||||
{#if !isPrecisionTestOpen}
|
||||
<section class="range-scale" aria-label="Signal Range">
|
||||
<p class="range-label">{locale === "zh-CN" ? "范围" : "Range"}</p>
|
||||
<div class="range-track">
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# 3D model assets
|
||||
|
||||
Put the first pipeline model here:
|
||||
|
||||
- Preferred: `static/models/je-skin-model.glb`
|
||||
- Format: glTF 2.0 `.glb`
|
||||
- Also supported after changing `modelUrl`: glTF 2.0 `.gltf` with its `.bin` and texture files in the same folder
|
||||
- Not supported directly: older glTF 1.0 assets. Convert them to glTF 2.0 first.
|
||||
|
||||
Runtime URL used by the app:
|
||||
|
||||
```text
|
||||
/models/je-skin-model.glb
|
||||
```
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1,18 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('has title', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev/');
|
||||
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(page).toHaveTitle(/Playwright/);
|
||||
});
|
||||
|
||||
test('get started link', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev/');
|
||||
|
||||
// Click the get started link.
|
||||
await page.getByRole('link', { name: 'Get started' }).click();
|
||||
|
||||
// Expects page to have a heading with the name of Installation.
|
||||
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
|
||||
// @ts-expect-error process is a nodejs global
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vite.dev/config/
|
||||
|
||||
Reference in New Issue
Block a user