diff --git a/app/index.html b/app/index.html index 7507292..f1fe239 100644 --- a/app/index.html +++ b/app/index.html @@ -1,9 +1,12 @@ - + - +
diff --git a/app/package.json b/app/package.json index b53ed0e..bc121f9 100644 --- a/app/package.json +++ b/app/package.json @@ -1,49 +1,52 @@ { - "name": "app", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "dev:mock": "vite --mode MOCK", - "build": "cross-env FOR_EMBEDDED=true vite build", - "build:web": "cross-env FOR_EMBEDDED=false vite build --mode WEB", - "preview": "vite preview", - "check": "svelte-check --tsconfig ./tsconfig.json", - "format": "prettier --plugin-search-dir . --write ." - }, - "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.2", - "@tsconfig/svelte": "^5.0.2", - "@types/three": "^0.160.0", - "@typescript-eslint/eslint-plugin": "^6.20.0", - "@typescript-eslint/parser": "^6.20.0", - "autoprefixer": "^10.4.17", - "cross-env": "^7.0.3", - "husky": "^9.0.7", - "lint-staged": "^15.2.0", - "postcss": "^8.4.33", - "prettier": "3.2.4", - "svelte": "^4.2.9", - "svelte-check": "^3.6.3", - "svelte-hero-icons": "^5.0.0", - "tailwindcss": "^3.4.1", - "tslib": "^2.6.2", - "typescript": "^5.3.3", - "vite": "^5.0.12", - "vite-plugin-compression": "^0.5.1", - "vite-plugin-singlefile": "^1.0.0" - }, - "dependencies": { - "nipplejs": "^0.10.1", - "svelte-routing": "^2.11.0", - "three": "^0.160.1", - "urdf-loader": "^0.12.1", - "uzip": "^0.20201231.0", - "xacro-parser": "^0.3.9" - }, - "lint-staged": { - "*.js": "eslint --cache --fix", - "*.{js,css,md,ts,svelte}": "prettier --write" - } + "name": "app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "dev:mock": "vite --mode MOCK", + "build": "cross-env FOR_EMBEDDED=true vite build", + "build:web": "cross-env FOR_EMBEDDED=false vite build --mode WEB", + "preview": "vite preview", + "test": "vitest", + "check": "svelte-check --tsconfig ./tsconfig.json", + "format": "prettier --plugin-search-dir . --write ." + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.2", + "@tsconfig/svelte": "^5.0.2", + "@types/three": "^0.160.0", + "@typescript-eslint/eslint-plugin": "^6.20.0", + "@typescript-eslint/parser": "^6.20.0", + "autoprefixer": "^10.4.17", + "cross-env": "^7.0.3", + "husky": "^9.0.7", + "lint-staged": "^15.2.0", + "postcss": "^8.4.33", + "prettier": "3.2.4", + "svelte": "^4.2.9", + "svelte-check": "^3.6.3", + "svelte-hero-icons": "^5.0.0", + "tailwindcss": "^3.4.1", + "tslib": "^2.6.2", + "typescript": "^5.3.3", + "vite": "^5.0.12", + "vite-plugin-compression": "^0.5.1", + "vite-plugin-singlefile": "^1.0.0", + "vitest": "^1.3.1" + }, + "dependencies": { + "nipplejs": "^0.10.1", + "prettier-plugin-svelte": "^3.2.1", + "svelte-routing": "^2.11.0", + "three": "^0.160.1", + "urdf-loader": "^0.12.1", + "uzip": "^0.20201231.0", + "xacro-parser": "^0.3.9" + }, + "lint-staged": { + "*.js": "eslint --cache --fix", + "*.{js,css,md,ts,svelte}": "prettier --write" + } } diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 9211806..9b33dc4 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -8,6 +8,9 @@ dependencies: nipplejs: specifier: ^0.10.1 version: 0.10.1 + prettier-plugin-svelte: + specifier: ^3.2.1 + version: 3.2.1(prettier@3.2.4)(svelte@4.2.9) svelte-routing: specifier: ^2.11.0 version: 2.11.0 @@ -85,6 +88,9 @@ devDependencies: vite-plugin-singlefile: specifier: ^1.0.0 version: 1.0.0(rollup@4.9.6)(vite@5.0.12) + vitest: + specifier: ^1.3.1 + version: 1.3.1 packages: @@ -104,7 +110,6 @@ packages: dependencies: '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.18 - dev: true /@esbuild/aix-ppc64@0.19.12: resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} @@ -370,6 +375,13 @@ packages: resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} dev: true + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} @@ -377,32 +389,26 @@ packages: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/trace-mapping': 0.3.18 - dev: true /@jridgewell/resolve-uri@3.1.0: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} - dev: true /@jridgewell/set-array@1.1.2: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} engines: {node: '>=6.0.0'} - dev: true /@jridgewell/sourcemap-codec@1.4.14: resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} - dev: true /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: true /@jridgewell/trace-mapping@0.3.18: resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} dependencies: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 - dev: true /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -529,6 +535,10 @@ packages: dev: true optional: true + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + /@steeze-ui/heroicons@2.2.3: resolution: {integrity: sha512-bv4YK375U9TDS50SmJ5VdFy2UmMhgKbJCCbo6IUU3qs2luEj2VX6sQoDy96j8AIKttxMd62ih1j0lZj7CTjIfA==} dev: true @@ -575,7 +585,6 @@ packages: /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - dev: true /@types/json-schema@7.0.12: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} @@ -742,6 +751,45 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true + /@vitest/expect@1.3.1: + resolution: {integrity: sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==} + dependencies: + '@vitest/spy': 1.3.1 + '@vitest/utils': 1.3.1 + chai: 4.4.1 + dev: true + + /@vitest/runner@1.3.1: + resolution: {integrity: sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==} + dependencies: + '@vitest/utils': 1.3.1 + p-limit: 5.0.0 + pathe: 1.1.2 + dev: true + + /@vitest/snapshot@1.3.1: + resolution: {integrity: sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==} + dependencies: + magic-string: 0.30.5 + pathe: 1.1.2 + pretty-format: 29.7.0 + dev: true + + /@vitest/spy@1.3.1: + resolution: {integrity: sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==} + dependencies: + tinyspy: 2.2.1 + dev: true + + /@vitest/utils@1.3.1: + resolution: {integrity: sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==} + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + dev: true + /acorn-jsx@5.3.2(acorn@8.11.3): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -750,11 +798,15 @@ packages: acorn: 8.11.3 dev: true + /acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + dev: true + /acorn@8.11.3: resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} engines: {node: '>=0.4.0'} hasBin: true - dev: true /ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -787,6 +839,11 @@ packages: dependencies: color-convert: 2.0.1 + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + /ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} @@ -816,13 +873,16 @@ packages: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} dependencies: dequal: 2.0.3 - dev: true /array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} dev: true + /assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + /autoprefixer@10.4.17(postcss@8.4.33): resolution: {integrity: sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==} engines: {node: ^10 || ^12 || >=14} @@ -843,7 +903,6 @@ packages: resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==} dependencies: dequal: 2.0.3 - dev: true /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -889,6 +948,11 @@ packages: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} dev: true + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -903,6 +967,19 @@ packages: resolution: {integrity: sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==} dev: true + /chai@4.4.1: + resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.3 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -916,6 +993,12 @@ packages: engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} dev: true + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 + dev: true + /chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -963,7 +1046,6 @@ packages: acorn: 8.11.3 estree-walker: 3.0.3 periscopic: 3.1.0 - dev: true /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} @@ -1015,7 +1097,6 @@ packages: dependencies: mdn-data: 2.0.30 source-map-js: 1.0.2 - dev: true /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} @@ -1035,6 +1116,13 @@ packages: ms: 2.1.2 dev: true + /deep-eql@4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -1047,7 +1135,6 @@ packages: /dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - dev: true /detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} @@ -1058,6 +1145,11 @@ packages: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1224,7 +1316,6 @@ packages: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} dependencies: '@types/estree': 1.0.5 - dev: true /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} @@ -1361,6 +1452,10 @@ packages: engines: {node: '>=18'} dev: true + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + dev: true + /get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -1537,7 +1632,6 @@ packages: resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} dependencies: '@types/estree': 1.0.5 - dev: true /is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} @@ -1553,6 +1647,10 @@ packages: hasBin: true dev: true + /js-tokens@8.0.3: + resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==} + dev: true + /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -1572,6 +1670,10 @@ packages: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true + /jsonc-parser@3.2.1: + resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + dev: true + /jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: @@ -1644,9 +1746,16 @@ packages: wrap-ansi: 9.0.0 dev: true + /local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} + dependencies: + mlly: 1.6.0 + pkg-types: 1.0.3 + dev: true + /locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} - dev: true /locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} @@ -1670,6 +1779,12 @@ packages: wrap-ansi: 9.0.0 dev: true + /loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + dependencies: + get-func-name: 2.0.2 + dev: true + /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -1682,11 +1797,9 @@ packages: engines: {node: '>=12'} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 - dev: true /mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} - dev: true /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -1748,6 +1861,15 @@ packages: minimist: 1.2.8 dev: true + /mlly@1.6.0: + resolution: {integrity: sha512-YOvg9hfYQmnaB56Yb+KrJE2u0Yzz5zR+sLejEvF4fzwzV1Al6hkf2vyHTwqCRyv0hCi9rVCqVoXpyYevQIRwLQ==} + dependencies: + acorn: 8.11.3 + pathe: 1.1.2 + pkg-types: 1.0.3 + ufo: 1.4.0 + dev: true + /mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -1849,6 +1971,13 @@ packages: yocto-queue: 0.1.0 dev: true + /p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + dependencies: + yocto-queue: 1.0.0 + dev: true + /p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -1892,13 +2021,20 @@ packages: engines: {node: '>=8'} dev: true + /pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + dev: true + + /pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + /periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} dependencies: '@types/estree': 1.0.5 estree-walker: 3.0.3 is-reference: 3.0.2 - dev: true /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -1925,6 +2061,14 @@ packages: engines: {node: '>= 6'} dev: true + /pkg-types@1.0.3: + resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + dependencies: + jsonc-parser: 3.2.1 + mlly: 1.6.0 + pathe: 1.1.2 + dev: true + /postcss-import@15.1.0(postcss@8.4.33): resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -2000,10 +2144,28 @@ packages: engines: {node: '>= 0.8.0'} dev: true + /prettier-plugin-svelte@3.2.1(prettier@3.2.4)(svelte@4.2.9): + resolution: {integrity: sha512-ENAPbIxASf2R79IZwgkG5sBdeNA9kLRlXVvKKmTXh79zWTy0KKoT86XO2pHrTitUPINd+iXWy12MRmgzKGVckA==} + peerDependencies: + prettier: ^3.0.0 + svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 + dependencies: + prettier: 3.2.4 + svelte: 4.2.9 + dev: false + /prettier@3.2.4: resolution: {integrity: sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==} engines: {node: '>=14'} hasBin: true + + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 dev: true /punycode@2.3.1: @@ -2015,6 +2177,10 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: true + /read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} dependencies: @@ -2143,6 +2309,10 @@ packages: engines: {node: '>=8'} dev: true + /siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true @@ -2186,6 +2356,13 @@ packages: /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} + + /stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + + /std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} dev: true /string-argv@0.3.2: @@ -2241,6 +2418,12 @@ packages: engines: {node: '>=8'} dev: true + /strip-literal@2.0.0: + resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==} + dependencies: + js-tokens: 8.0.3 + dev: true + /sucrase@3.33.0: resolution: {integrity: sha512-ARGC7vbufOHfpvyGcZZXFaXCMZ9A4fffOGC5ucOW7+WHDGlAe8LJdf3Jts1sWhDeiI1RSWrKy5Hodl+JWGdW2A==} engines: {node: '>=8'} @@ -2380,7 +2563,6 @@ packages: locate-character: 3.0.0 magic-string: 0.30.5 periscopic: 3.1.0 - dev: true /tailwindcss@3.4.1: resolution: {integrity: sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==} @@ -2434,6 +2616,20 @@ packages: resolution: {integrity: sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==} dev: false + /tinybench@2.6.0: + resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} + dev: true + + /tinypool@0.8.2: + resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + engines: {node: '>=14.0.0'} + dev: true + /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2465,6 +2661,11 @@ packages: prelude-ls: 1.2.1 dev: true + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + /type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -2481,6 +2682,10 @@ packages: hasBin: true dev: true + /ufo@1.4.0: + resolution: {integrity: sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==} + dev: true + /universalify@2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} @@ -2519,6 +2724,27 @@ packages: resolution: {integrity: sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==} dev: false + /vite-node@1.3.1: + resolution: {integrity: sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + pathe: 1.1.2 + picocolors: 1.0.0 + vite: 5.0.12 + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vite-plugin-compression@0.5.1(vite@5.0.12): resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==} peerDependencies: @@ -2590,6 +2816,61 @@ packages: vite: 5.0.12 dev: true + /vitest@1.3.1: + resolution: {integrity: sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.3.1 + '@vitest/ui': 1.3.1 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@vitest/expect': 1.3.1 + '@vitest/runner': 1.3.1 + '@vitest/snapshot': 1.3.1 + '@vitest/spy': 1.3.1 + '@vitest/utils': 1.3.1 + acorn-walk: 8.3.2 + chai: 4.4.1 + debug: 4.3.4 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.5 + pathe: 1.1.2 + picocolors: 1.0.0 + std-env: 3.7.0 + strip-literal: 2.0.0 + tinybench: 2.6.0 + tinypool: 0.8.2 + vite: 5.0.12 + vite-node: 1.3.1 + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2598,6 +2879,15 @@ packages: isexe: 2.0.0 dev: true + /why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -2668,3 +2958,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + + /yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: true diff --git a/app/src/App.svelte b/app/src/App.svelte index 448f0fe..06e56f8 100644 --- a/app/src/App.svelte +++ b/app/src/App.svelte @@ -2,40 +2,40 @@ import { Router, Route } from 'svelte-routing'; import { onMount } from 'svelte'; import TopBar from './components/TopBar.svelte'; - import { connect } from './lib/socket'; + import socketService from '$lib/services/socket-service'; import Controller from './routes/Controller.svelte'; - import FileCache from './lib/cache'; - import { socketLocation } from './lib/location'; - import Settings from './routes/Settings.svelte'; - import { jointNames, model } from './lib/store'; - import { loadModelAsync } from './lib/modelLoader'; + import { fileService } from '$lib/services'; + import Settings from './routes/Settings.svelte'; + import { jointNames, model, outControllerData } from '$lib/store'; + import { loadModelAsync } from '$lib/utilities'; + import { socketLocation } from '$lib/utilities'; + import type { Result } from '$lib/utilities/result'; - export let url = window.location.pathname + export let url = window.location.pathname; onMount(async () => { - connect(socketLocation); - registerFetchIntercept() - const [urdf, JOINT_NAME] = await loadModelAsync('/spot_micro.urdf.xacro') - jointNames.set(JOINT_NAME) - model.set(urdf) + socketService.connect(socketLocation); + socketService.addPublisher(outControllerData); + registerFetchIntercept(); + const modelRes = await loadModelAsync('/spot_micro.urdf.xacro'); + + if (modelRes.isOk()) { + const [urdf, JOINT_NAME] = modelRes.inner; + jointNames.set(JOINT_NAME); + model.set(urdf); + } else { + console.error(modelRes.inner, { exception: modelRes.exception }); + } }); - const registerFetchIntercept = () => { - const { fetch: originalFetch } = window; - window.fetch = async (...args) => { - const [resource, config] = args; - await FileCache.openDatabase(); - let file: BodyInit | Uint8Array | undefined | null; - try { - file = await FileCache.getFile(resource.toString()); - } catch (error) { - console.log(error); - } - - return file - ? new Response(file) - : originalFetch(resource, config) - }; - } + const registerFetchIntercept = () => { + const { fetch: originalFetch } = window; + window.fetch = async (...args) => { + const [resource, config] = args; + let file: Result; + file = await fileService.getFile(resource.toString()); + return file.isOk() ? new Response(file.inner) : originalFetch(resource, config); + }; + }; diff --git a/app/src/components/Controls.svelte b/app/src/components/Controls.svelte index 9ef3569..557bdf8 100644 --- a/app/src/components/Controls.svelte +++ b/app/src/components/Controls.svelte @@ -1,8 +1,8 @@
diff --git a/app/src/components/Topbar.svelte b/app/src/components/Topbar.svelte index 90c238e..8d0bec9 100644 --- a/app/src/components/Topbar.svelte +++ b/app/src/components/Topbar.svelte @@ -1,76 +1,84 @@ -
-
- {#if settingOpen} - - - - {:else} - - - - {/if} - - - -
+
+ {#if settingOpen} + + + + {:else} + + + + {/if} + -
- - - -
-
- -
+ +
+ +
+ + + +
+
+ +
\ No newline at end of file + .topbar { + height: 50px; + } + .action_button { + border-radius: 4px; + width: 34px; + height: 34px; + display: flex; + justify-content: center; + align-items: center; + outline: 1px solid #52525b; + } + diff --git a/app/src/components/Views/Model.svelte b/app/src/components/Views/Model.svelte index bc7ca07..b39ae7e 100644 --- a/app/src/components/Views/Model.svelte +++ b/app/src/components/Views/Model.svelte @@ -1,147 +1,170 @@ - - + + {#if showStream} - - {/if} + +{/if} - \ No newline at end of file + diff --git a/app/src/components/Views/Stream.svelte b/app/src/components/Views/Stream.svelte index 5da0b82..760e754 100644 --- a/app/src/components/Views/Stream.svelte +++ b/app/src/components/Views/Stream.svelte @@ -1,6 +1,6 @@ +
@@ -49,23 +49,36 @@
-{#if selectedServo !== null} -
-

Servo {formatServo(servos[selectedServo])} Calibration

- - updateServoValue(selectedServo, 'minPWM', Number(event.target.value))} /> + {#if selectedServo !== null} +
+

Servo {formatServo(servos[selectedServo])} Calibration

+ + updateServoValue(selectedServo, 'minPWM', Number(event.target.value))} + /> - - updateServoValue(selectedServo, 'maxPWM', Number(event.target.value))} /> + + updateServoValue(selectedServo, 'maxPWM', Number(event.target.value))} + /> - - updateServoValue(selectedServo, 'pwmFor180', Number(event.target.value))} /> -
-{/if} + + + updateServoValue(selectedServo, 'pwmFor180', Number(event.target.value))} + /> +
+ {/if}
diff --git a/app/src/components/settings/Configuration.svelte b/app/src/components/settings/Configuration.svelte index 0aff776..be8290f 100644 --- a/app/src/components/settings/Configuration.svelte +++ b/app/src/components/settings/Configuration.svelte @@ -1,23 +1,25 @@
-
- {#each Object.entries($settings) as entry} -
-
{entry[0]}:
-
{entry[1]}
-
- {/each} -
-
\ No newline at end of file +
+ {#each Object.entries($settings) as entry} +
+
{entry[0]}:
+
{entry[1]}
+
+ {/each} +
+
diff --git a/app/src/components/settings/Info.svelte b/app/src/components/settings/Info.svelte index c064ade..4cd7b14 100644 --- a/app/src/components/settings/Info.svelte +++ b/app/src/components/settings/Info.svelte @@ -1,27 +1,31 @@ +
-
- {#each Object.entries($systemInfo ?? {}) as entry} -
-
{entry[0]}:
- {#if entry[0].includes("Size") || entry[0].includes("Free") || entry[0].includes("Min")} -
{humanFileSize(entry[1])}
- {:else} -
{entry[1]}
- {/if} -
- {/each} -
-
\ No newline at end of file +
+ {#each Object.entries($systemInfo ?? {}) as entry} +
+
{entry[0]}:
+ {#if entry[0].includes('Size') || entry[0].includes('Free') || entry[0].includes('Min')} +
{humanFileSize(entry[1])}
+ {:else} +
{entry[1]}
+ {/if} +
+ {/each} +
+ diff --git a/app/src/components/settings/Log.svelte b/app/src/components/settings/Log.svelte index 75da7e0..d3a59a3 100644 --- a/app/src/components/settings/Log.svelte +++ b/app/src/components/settings/Log.svelte @@ -1,18 +1,20 @@
- {#each $log as entry} -
{entry}
- {/each} -
\ No newline at end of file + {#each $log as entry} +
{entry}
+ {/each} + diff --git a/app/src/lib/cache.ts b/app/src/lib/cache.ts deleted file mode 100644 index e298621..0000000 --- a/app/src/lib/cache.ts +++ /dev/null @@ -1,102 +0,0 @@ -class FileCache { - private request: IDBOpenDBRequest; - private db: IDBDatabase | null = null; - private store: IDBObjectStore | null = null; - - dbName = 'fileStorageDB'; - dbVersion = 1; - storeName = 'files'; - constructor() { - this.request = indexedDB.open(this.dbName, this.dbVersion); - this.request.onerror = (event) => { - console.error("An error occurred with IndexedDB", event); - }; - - this.request.onupgradeneeded = () => { - this.db = this.request.result; - this.store = this.db.createObjectStore(this.storeName); - }; - - this.request.onsuccess = () => { - this.db = this.request.result; - const transaction = this.db.transaction(this.storeName, "readwrite"); - this.store = transaction.objectStore(this.storeName); - } - } - - public isOpen(): boolean { - return this.db !== null; - } - - public async saveFile(key:string, file: Uint8Array): Promise { - return new Promise((resolve, reject) => { - if(!this.db) { - reject("Database not open") - return; - } - const transaction = this.db.transaction(this.storeName, "readwrite"); - const store = transaction.objectStore(this.storeName); - const request = store.put(file, key); - if(!request) { - reject("Request not created") - return - } - request.onsuccess = () => { - resolve(request.result); - }; - request.onerror = () => { - reject(request.error); - }; - }); - } - - public async getFile(key:string): Promise { - return new Promise((resolve, reject) => { - if(!key) { - reject("Key was not defined") - return; - } - if(!this.db) { - reject("Database not open") - return; - } - const transaction = this.db.transaction(this.storeName, "readwrite"); - const store = transaction.objectStore(this.storeName); - - const request = store.get(key); - if(!request) { - reject("Request not created") - return - } - request.onsuccess = () => { - resolve(request.result); - }; - request.onerror = () => { - reject(request.error); - }; - }); - } - - public async openDatabase(): Promise { - return new Promise((resolve, reject) => { - const request = indexedDB.open(this.dbName, this.dbVersion); - - request.onerror = (event) => { - reject('Error opening database'); - }; - - request.onsuccess = (event) => { - const db = event.target?.result; - this.db = db; - resolve(db); - }; - - request.onupgradeneeded = (event) => { - this.db = event.target?.result; - this.db?.createObjectStore('files', { autoIncrement: true }); - }; - }); - } -} - -export default new FileCache(); \ No newline at end of file diff --git a/app/src/lib/kinematic.ts b/app/src/lib/kinematic.ts index f1f444b..f239f9a 100644 --- a/app/src/lib/kinematic.ts +++ b/app/src/lib/kinematic.ts @@ -1,381 +1,393 @@ export default class Kinematic { - private l1: number; - private l2: number; - private l3: number; - private l4: number; - - private L: number; - private W: number; - - constructor() { - this.l1 = 50; - this.l2 = 20; - this.l3 = 120; - this.l4 = 155; - - this.L = 140; - this.W = 75; - } - - bodyIK(omega: number, phi: number, psi: number, xm: number, ym: number, zm: number): number[][][] { - const { cos, sin } = Math; - - const Rx: number[][] = [ - [1, 0, 0, 0], - [0, cos(omega), -sin(omega), 0], - [0, sin(omega), cos(omega), 0], - [0, 0, 0, 1], - ]; - const Ry: number[][] = [ - [cos(phi), 0, sin(phi), 0], - [0, 1, 0, 0], - [-sin(phi), 0, cos(phi), 0], - [0, 0, 0, 1], - ]; - const Rz: number[][] = [ - [cos(psi), -sin(psi), 0, 0], - [sin(psi), cos(psi), 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 1], - ]; - const Rxyz: number[][] = this.matrixMultiply(this.matrixMultiply(Rx, Ry), Rz); - - const T: number[][] = [ - [0, 0, 0, xm], - [0, 0, 0, ym], - [0, 0, 0, zm], - [0, 0, 0, 0], - ]; - const Tm: number[][] = this.matrixAdd(T, Rxyz); - - const sHp = sin(Math.PI / 2); - const cHp = cos(Math.PI / 2); - const L = this.L; - const W = this.W; - - return [ - this.matrixMultiply(Tm, [ - [cHp, 0, sHp, L / 2], - [0, 1, 0, 0], - [-sHp, 0, cHp, W / 2], - [0, 0, 0, 1], - ]), - this.matrixMultiply(Tm, [ - [cHp, 0, sHp, L / 2], - [0, 1, 0, 0], - [-sHp, 0, cHp, -W / 2], - [0, 0, 0, 1], - ]), - this.matrixMultiply(Tm, [ - [cHp, 0, sHp, -L / 2], - [0, 1, 0, 0], - [-sHp, 0, cHp, W / 2], - [0, 0, 0, 1], - ]), - this.matrixMultiply(Tm, [ - [cHp, 0, sHp, -L / 2], - [0, 1, 0, 0], - [-sHp, 0, cHp, -W / 2], - [0, 0, 0, 1], - ]), - ]; - } - - private legIK(point: number[]): number[] { - const [x, y, z] = point; - const { atan2, cos, sin, sqrt, acos } = Math; - const { l1, l2, l3, l4 } = this; - - let F; + private l1: number; + private l2: number; + private l3: number; + private l4: number; - try { - F = sqrt(x ** 2 + y ** 2 - l1 ** 2); - if(isNaN(F)) throw new Error("F is NaN") - } catch (error) { - //console.log(error) - F = l1 - } - const G = F - l2; - const H = sqrt(G ** 2 + z ** 2); + private L: number; + private W: number; - const theta1 = -atan2(y, x) - atan2(F, -l1); - const D = (H ** 2 - l3 ** 2 - l4 ** 2) / (2 * l3 * l4); - let theta3: number - try { - theta3 = acos(D); - if(isNaN(theta3)) throw new Error("theta3 is NaN") - } catch (error) { - theta3 = 0 - } - const theta2 = atan2(z, G) - atan2(l4 * sin(theta3), l3 + l4 * cos(theta3)); + constructor() { + this.l1 = 50; + this.l2 = 20; + this.l3 = 120; + this.l4 = 155; - return [theta1, theta2, theta3]; - } - - matrixMultiply(a: number[][], b: number[][]): number[][] { - const result: number[][] = []; - - for (let i = 0; i < a.length; i++) { - const row: number[] = []; - - for (let j = 0; j < b[0].length; j++) { - let sum = 0; - - for (let k = 0; k < a[i].length; k++) { - sum += a[i][k] * b[k][j]; - } - - row.push(sum); - } - - result.push(row); - } - - return result; - } + this.L = 140; + this.W = 75; + } - multiplyVector(matrix: number[][], vector: number[]): number[] { - const rows = matrix.length; - const cols = matrix[0].length; - const vectorLength = vector.length; - - if (cols !== vectorLength) { - throw new Error("Matrix and vector dimensions do not match for multiplication."); - } - - const result = []; - - for (let i = 0; i < rows; i++) { - let sum = 0; - - for (let j = 0; j < cols; j++) { - sum += matrix[i][j] * vector[j]; - } - - result.push(sum); - } - - return result; - } - - private matrixAdd(a: number[][], b: number[][]): number[][] { - const result: number[][] = []; - - for (let i = 0; i < a.length; i++) { - const row: number[] = []; - - for (let j = 0; j < a[i].length; j++) { - row.push(a[i][j] + b[i][j]); - } - - result.push(row); - } - - return result; - } - - public calcLegPoints(angles: number[]): number[][] { - const [theta1, theta2, theta3] = angles; - const theta23 = theta2 + theta3; - - const T0: number[] = [0, 0, 0, 1]; - const T1: number[] = this.vectorAdd( - T0, - [-this.l1 * Math.cos(theta1), this.l1 * Math.sin(theta1), 0, 0] - ); - const T2: number[] = this.vectorAdd( - T1, - [-this.l2 * Math.sin(theta1), -this.l2 * Math.cos(theta1), 0, 0] - ); - const T3: number[] = this.vectorAdd( - T2, - [ - -this.l3 * Math.sin(theta1) * Math.cos(theta2), - -this.l3 * Math.cos(theta1) * Math.cos(theta2), - this.l3 * Math.sin(theta2), - 0, - ] - ); - const T4: number[] = this.vectorAdd( - T3, - [ - -this.l4 * Math.sin(theta1) * Math.cos(theta23), - -this.l4 * Math.cos(theta1) * Math.cos(theta23), - this.l4 * Math.sin(theta23), - 0, - ] - ); - - return [T0, T1, T2, T3, T4]; - } - - public calcIK(Lp: number[][], angles: number[], center: number[]): number[][] { - const [omega, phi, psi] = angles; - const [xm, ym, zm] = center; - - const [Tlf, Trf, Tlb, Trb] = this.bodyIK(omega, phi, psi, xm, ym, zm); - - const Ix: number[][] = [ - [-1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 1], - ]; + bodyIK( + omega: number, + phi: number, + psi: number, + xm: number, + ym: number, + zm: number + ): number[][][] { + const { cos, sin } = Math; - return [ - this.legIK(this.multiplyVector(this.matrixInverse(Tlf), Lp[0])), - this.legIK(this.multiplyVector(Ix, this.multiplyVector(this.matrixInverse(Trf), Lp[1]))), - this.legIK(this.multiplyVector(this.matrixInverse(Tlb), Lp[2])), - this.legIK(this.multiplyVector(Ix, this.multiplyVector(this.matrixInverse(Trb), Lp[3]))), - ]; - } - - private vectorAdd(a: number[], b: number[]): number[] { - return a.map((val, index) => val + b[index]); - } - - private matrixInverse(matrix: number[][]): number[][] { - const det = this.determinant(matrix); - const adjugate = this.adjugate(matrix); - const scalar = 1 / det; - const inverse: number[][] = []; - - for (let i = 0; i < matrix.length; i++) { - const row: number[] = []; - - for (let j = 0; j < matrix[i].length; j++) { - row.push(adjugate[i][j] * scalar); - } - - inverse.push(row); - } - - return inverse; - } - - private determinant(matrix: number[][]): number { - if (matrix.length !== matrix[0].length) { - throw new Error("The matrix is not square."); - } - - if (matrix.length === 2) { - return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0]; - } - - let det = 0; - - for (let i = 0; i < matrix.length; i++) { - const sign = i % 2 === 0 ? 1 : -1; - const subMatrix: number[][] = []; - - for (let j = 1; j < matrix.length; j++) { - const row: number[] = []; - - for (let k = 0; k < matrix.length; k++) { - if (k !== i) { - row.push(matrix[j][k]); - } - } - - subMatrix.push(row); - } - - det += sign * matrix[0][i] * this.determinant(subMatrix); - } - - return det; - } - - private adjugate(matrix: number[][]): number[][] { - if (matrix.length !== matrix[0].length) { - throw new Error("The matrix is not square."); - } - - const adjugate: number[][] = []; - - for (let i = 0; i < matrix.length; i++) { - const row: number[] = []; - - for (let j = 0; j < matrix[i].length; j++) { - const sign = (i + j) % 2 === 0 ? 1 : -1; - const subMatrix: number[][] = []; - - for (let k = 0; k < matrix.length; k++) { - if (k !== i) { - const subRow: number[] = []; - - for (let l = 0; l < matrix.length; l++) { - if (l !== j) { - subRow.push(matrix[k][l]); - } - } - - subMatrix.push(subRow); - } - } - - const cofactor = sign * this.determinant(subMatrix); - row.push(cofactor); - } - - adjugate.push(row); - } - - return this.transpose(adjugate); - } - - private transpose(matrix: number[][]): number[][] { - const transposed: number[][] = []; - - for (let i = 0; i < matrix.length; i++) { - const row: number[] = []; - - for (let j = 0; j < matrix[i].length; j++) { - row.push(matrix[j][i]); - } - - transposed.push(row); - } - - return transposed; - } - } + const Rx: number[][] = [ + [1, 0, 0, 0], + [0, cos(omega), -sin(omega), 0], + [0, sin(omega), cos(omega), 0], + [0, 0, 0, 1] + ]; + const Ry: number[][] = [ + [cos(phi), 0, sin(phi), 0], + [0, 1, 0, 0], + [-sin(phi), 0, cos(phi), 0], + [0, 0, 0, 1] + ]; + const Rz: number[][] = [ + [cos(psi), -sin(psi), 0, 0], + [sin(psi), cos(psi), 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ]; + const Rxyz: number[][] = this.matrixMultiply(this.matrixMultiply(Rx, Ry), Rz); + const T: number[][] = [ + [0, 0, 0, xm], + [0, 0, 0, ym], + [0, 0, 0, zm], + [0, 0, 0, 0] + ]; + const Tm: number[][] = this.matrixAdd(T, Rxyz); + + const sHp = sin(Math.PI / 2); + const cHp = cos(Math.PI / 2); + const L = this.L; + const W = this.W; + + return [ + this.matrixMultiply(Tm, [ + [cHp, 0, sHp, L / 2], + [0, 1, 0, 0], + [-sHp, 0, cHp, W / 2], + [0, 0, 0, 1] + ]), + this.matrixMultiply(Tm, [ + [cHp, 0, sHp, L / 2], + [0, 1, 0, 0], + [-sHp, 0, cHp, -W / 2], + [0, 0, 0, 1] + ]), + this.matrixMultiply(Tm, [ + [cHp, 0, sHp, -L / 2], + [0, 1, 0, 0], + [-sHp, 0, cHp, W / 2], + [0, 0, 0, 1] + ]), + this.matrixMultiply(Tm, [ + [cHp, 0, sHp, -L / 2], + [0, 1, 0, 0], + [-sHp, 0, cHp, -W / 2], + [0, 0, 0, 1] + ]) + ]; + } + + private legIK(point: number[]): number[] { + const [x, y, z] = point; + const { atan2, cos, sin, sqrt, acos } = Math; + const { l1, l2, l3, l4 } = this; + + let F; + + try { + F = sqrt(x ** 2 + y ** 2 - l1 ** 2); + if (isNaN(F)) throw new Error('F is NaN'); + } catch (error) { + //console.log(error) + F = l1; + } + const G = F - l2; + const H = sqrt(G ** 2 + z ** 2); + + const theta1 = -atan2(y, x) - atan2(F, -l1); + const D = (H ** 2 - l3 ** 2 - l4 ** 2) / (2 * l3 * l4); + let theta3: number; + try { + theta3 = acos(D); + if (isNaN(theta3)) throw new Error('theta3 is NaN'); + } catch (error) { + theta3 = 0; + } + const theta2 = atan2(z, G) - atan2(l4 * sin(theta3), l3 + l4 * cos(theta3)); + + return [theta1, theta2, theta3]; + } + + matrixMultiply(a: number[][], b: number[][]): number[][] { + const result: number[][] = []; + + for (let i = 0; i < a.length; i++) { + const row: number[] = []; + + for (let j = 0; j < b[0].length; j++) { + let sum = 0; + + for (let k = 0; k < a[i].length; k++) { + sum += a[i][k] * b[k][j]; + } + + row.push(sum); + } + + result.push(row); + } + + return result; + } + + multiplyVector(matrix: number[][], vector: number[]): number[] { + const rows = matrix.length; + const cols = matrix[0].length; + const vectorLength = vector.length; + + if (cols !== vectorLength) { + throw new Error('Matrix and vector dimensions do not match for multiplication.'); + } + + const result = []; + + for (let i = 0; i < rows; i++) { + let sum = 0; + + for (let j = 0; j < cols; j++) { + sum += matrix[i][j] * vector[j]; + } + + result.push(sum); + } + + return result; + } + + private matrixAdd(a: number[][], b: number[][]): number[][] { + const result: number[][] = []; + + for (let i = 0; i < a.length; i++) { + const row: number[] = []; + + for (let j = 0; j < a[i].length; j++) { + row.push(a[i][j] + b[i][j]); + } + + result.push(row); + } + + return result; + } + + public calcLegPoints(angles: number[]): number[][] { + const [theta1, theta2, theta3] = angles; + const theta23 = theta2 + theta3; + + const T0: number[] = [0, 0, 0, 1]; + const T1: number[] = this.vectorAdd(T0, [ + -this.l1 * Math.cos(theta1), + this.l1 * Math.sin(theta1), + 0, + 0 + ]); + const T2: number[] = this.vectorAdd(T1, [ + -this.l2 * Math.sin(theta1), + -this.l2 * Math.cos(theta1), + 0, + 0 + ]); + const T3: number[] = this.vectorAdd(T2, [ + -this.l3 * Math.sin(theta1) * Math.cos(theta2), + -this.l3 * Math.cos(theta1) * Math.cos(theta2), + this.l3 * Math.sin(theta2), + 0 + ]); + const T4: number[] = this.vectorAdd(T3, [ + -this.l4 * Math.sin(theta1) * Math.cos(theta23), + -this.l4 * Math.cos(theta1) * Math.cos(theta23), + this.l4 * Math.sin(theta23), + 0 + ]); + + return [T0, T1, T2, T3, T4]; + } + + public calcIK(Lp: number[][], angles: number[], center: number[]): number[][] { + const [omega, phi, psi] = angles; + const [xm, ym, zm] = center; + + const [Tlf, Trf, Tlb, Trb] = this.bodyIK(omega, phi, psi, xm, ym, zm); + + const Ix: number[][] = [ + [-1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ]; + + return [ + this.legIK(this.multiplyVector(this.matrixInverse(Tlf), Lp[0])), + this.legIK(this.multiplyVector(Ix, this.multiplyVector(this.matrixInverse(Trf), Lp[1]))), + this.legIK(this.multiplyVector(this.matrixInverse(Tlb), Lp[2])), + this.legIK(this.multiplyVector(Ix, this.multiplyVector(this.matrixInverse(Trb), Lp[3]))) + ]; + } + + private vectorAdd(a: number[], b: number[]): number[] { + return a.map((val, index) => val + b[index]); + } + + private matrixInverse(matrix: number[][]): number[][] { + const det = this.determinant(matrix); + const adjugate = this.adjugate(matrix); + const scalar = 1 / det; + const inverse: number[][] = []; + + for (let i = 0; i < matrix.length; i++) { + const row: number[] = []; + + for (let j = 0; j < matrix[i].length; j++) { + row.push(adjugate[i][j] * scalar); + } + + inverse.push(row); + } + + return inverse; + } + + private determinant(matrix: number[][]): number { + if (matrix.length !== matrix[0].length) { + throw new Error('The matrix is not square.'); + } + + if (matrix.length === 2) { + return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0]; + } + + let det = 0; + + for (let i = 0; i < matrix.length; i++) { + const sign = i % 2 === 0 ? 1 : -1; + const subMatrix: number[][] = []; + + for (let j = 1; j < matrix.length; j++) { + const row: number[] = []; + + for (let k = 0; k < matrix.length; k++) { + if (k !== i) { + row.push(matrix[j][k]); + } + } + + subMatrix.push(row); + } + + det += sign * matrix[0][i] * this.determinant(subMatrix); + } + + return det; + } + + private adjugate(matrix: number[][]): number[][] { + if (matrix.length !== matrix[0].length) { + throw new Error('The matrix is not square.'); + } + + const adjugate: number[][] = []; + + for (let i = 0; i < matrix.length; i++) { + const row: number[] = []; + + for (let j = 0; j < matrix[i].length; j++) { + const sign = (i + j) % 2 === 0 ? 1 : -1; + const subMatrix: number[][] = []; + + for (let k = 0; k < matrix.length; k++) { + if (k !== i) { + const subRow: number[] = []; + + for (let l = 0; l < matrix.length; l++) { + if (l !== j) { + subRow.push(matrix[k][l]); + } + } + + subMatrix.push(subRow); + } + } + + const cofactor = sign * this.determinant(subMatrix); + row.push(cofactor); + } + + adjugate.push(row); + } + + return this.transpose(adjugate); + } + + private transpose(matrix: number[][]): number[][] { + const transposed: number[][] = []; + + for (let i = 0; i < matrix.length; i++) { + const row: number[] = []; + + for (let j = 0; j < matrix[i].length; j++) { + row.push(matrix[j][i]); + } + + transposed.push(row); + } + + return transposed; + } +} export class ForwardKinematics { - private l1: number; - private l2: number; - private l3: number; - private l4: number; - - constructor() { - this.l1 = 50; - this.l2 = 20; - this.l3 = 120; - this.l4 = 155; - } - - public calculateFootpoint(theta1: number, theta2: number, theta3: number): number[] { - const { cos, sin } = Math; - - const x = this.l1 * cos(theta1) + this.l2 * cos(theta1) + this.l3 * cos(theta1 + theta2) + this.l4 * cos(theta1 + theta2 + theta3); - const y = this.l1 * sin(theta1) + this.l2 * sin(theta1) + this.l3 * sin(theta1 + theta2) + this.l4 * sin(theta1 + theta2 + theta3); - const z = 0; - - return [x, y, z]; - } + private l1: number; + private l2: number; + private l3: number; + private l4: number; - public calculateFootpoints(angles: number[]): number[][] { - const footpoints: number[][] = []; - - for (let i = 0; i < angles.length; i += 3) { - const theta1 = angles[i]; - const theta2 = angles[i + 1]; - const theta3 = angles[i + 2]; - const footpoint = this.calculateFootpoint(theta1, theta2, theta3); - footpoints.push(footpoint); - } - - return footpoints; - } - } \ No newline at end of file + constructor() { + this.l1 = 50; + this.l2 = 20; + this.l3 = 120; + this.l4 = 155; + } + + public calculateFootpoint(theta1: number, theta2: number, theta3: number): number[] { + const { cos, sin } = Math; + + const x = + this.l1 * cos(theta1) + + this.l2 * cos(theta1) + + this.l3 * cos(theta1 + theta2) + + this.l4 * cos(theta1 + theta2 + theta3); + const y = + this.l1 * sin(theta1) + + this.l2 * sin(theta1) + + this.l3 * sin(theta1 + theta2) + + this.l4 * sin(theta1 + theta2 + theta3); + const z = 0; + + return [x, y, z]; + } + + public calculateFootpoints(angles: number[]): number[][] { + const footpoints: number[][] = []; + + for (let i = 0; i < angles.length; i += 3) { + const theta1 = angles[i]; + const theta2 = angles[i + 1]; + const theta3 = angles[i + 2]; + const footpoint = this.calculateFootpoint(theta1, theta2, theta3); + footpoints.push(footpoint); + } + + return footpoints; + } +} diff --git a/app/src/lib/location.ts b/app/src/lib/location.ts deleted file mode 100644 index 6c00c47..0000000 --- a/app/src/lib/location.ts +++ /dev/null @@ -1,8 +0,0 @@ -const forWeb = import.meta.env.MODE === "WEB" -const mock = import.meta.env.MODE === "MOCK" - -const location = mock ? `${window.location.hostname}:2096` : "leika.local" - -export const socketLocation = forWeb ? `wss://${window.location.hostname}:2096` : `ws://${location}` - -export default location; \ No newline at end of file diff --git a/app/src/lib/modelLoader.ts b/app/src/lib/modelLoader.ts deleted file mode 100644 index e892ef3..0000000 --- a/app/src/lib/modelLoader.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { LoaderUtils } from "three"; -import URDFLoader, { type URDFRobot } from "urdf-loader" -import { XacroLoader } from "xacro-parser" - -export const loadModelAsync = async (url:string):Promise<[URDFRobot, string[]]> => { - return new Promise((resolve, reject) => { - const xacroLoader = new XacroLoader(); - - xacroLoader.load(url, async (xml) => { - const urdfLoader = new URDFLoader(); - - urdfLoader.workingPath = LoaderUtils.extractUrlBase(url); - - try { - const model = urdfLoader.parse(xml); - model.rotation.x = -Math.PI / 2; - model.rotation.z = Math.PI / 2; - model.traverse(c => c.castShadow = true); - model.updateMatrixWorld(true); - model.scale.setScalar(10); - const joints = Object.entries(model.joints) - .filter(joint => joint[1]._jointType !== 'fixed') - .map(joint => joint[0]) - - resolve([model, joints]); - } catch (error) { - reject(error); - } - }, (error) => reject(error)); - }); -} \ No newline at end of file diff --git a/app/src/lib/models.ts b/app/src/lib/models.ts new file mode 100644 index 0000000..f23f351 --- /dev/null +++ b/app/src/lib/models.ts @@ -0,0 +1,13 @@ +export type angles = number[] | Int16Array; + +type AnglesData = { + type: 'angles'; + data: angles; +}; + +type LogData = { + type: 'log'; + data: string; +}; + +export type WebSocketJsonMsg = AnglesData | LogData; diff --git a/app/src/lib/sceneBuilder.ts b/app/src/lib/sceneBuilder.ts index 51c64fe..1ef64d1 100644 --- a/app/src/lib/sceneBuilder.ts +++ b/app/src/lib/sceneBuilder.ts @@ -1,293 +1,318 @@ -import { Mesh, - PerspectiveCamera, - PlaneGeometry, - Scene, - ShadowMaterial, - WebGLRenderer, - AmbientLight, - DirectionalLight, - PCFSoftShadowMap, - GridHelper, - ArrowHelper, - Vector3, - LoaderUtils, - Object3D, - FogExp2, - CanvasTexture, - type ColorRepresentation, - type WebGLRendererParameters, - MeshPhongMaterial, - EquirectangularReflectionMapping, - ACESFilmicToneMapping, - MathUtils, -} from "three"; +import { + Mesh, + PerspectiveCamera, + PlaneGeometry, + Scene, + ShadowMaterial, + WebGLRenderer, + AmbientLight, + DirectionalLight, + PCFSoftShadowMap, + GridHelper, + ArrowHelper, + Vector3, + LoaderUtils, + Object3D, + FogExp2, + CanvasTexture, + type ColorRepresentation, + type WebGLRendererParameters, + MeshPhongMaterial, + EquirectangularReflectionMapping, + ACESFilmicToneMapping, + MathUtils +} from 'three'; import { Sky } from 'three/addons/objects/Sky.js'; -import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; -import { type URDFMimicJoint } from "urdf-loader"; -import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls' +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; +import { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader'; +import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls'; -export const addScene = () => new Scene() +export const addScene = () => new Scene(); interface position { - x?: number, - y?: number, - z?: number + x?: number; + y?: number; + z?: number; } interface light { - color?: ColorRepresentation, - intensity?: number + color?: ColorRepresentation; + intensity?: number; } interface gridOptions { - divisions?: number, - size?: number, + divisions?: number; + size?: number; } interface arrowOptions { - origin:position, - direction:position, - length?:number, - color?:ColorRepresentation + origin: position; + direction: position; + length?: number; + color?: ColorRepresentation; } -type directionalLight = position & light +type directionalLight = position & light; -type gridHelperOptions = gridOptions & position +type gridHelperOptions = gridOptions & position; function calculateCurrentSunElevation() { - let now = new Date(); - let decimalTime = now.getHours() + now.getMinutes() / 60; - let normalizedTime = ((decimalTime - 6) % 12) / 6 - 1; - return 10 * Math.sin(normalizedTime * Math.PI); + let now = new Date(); + let decimalTime = now.getHours() + now.getMinutes() / 60; + let normalizedTime = (decimalTime % 12) / 6 - 1; + return 10 * Math.sin(normalizedTime * Math.PI); } export default class SceneBuilder { - public scene: Scene - public camera: PerspectiveCamera - public ground: Mesh - public renderer:WebGLRenderer - public controls:OrbitControls - public callback:Function - public gridHelper: GridHelper; - public model: Object3D - public liveStreamTexture: CanvasTexture - private fog:FogExp2 - private isLoaded:boolean = false - highlightMaterial: any; + public scene: Scene; + public camera: PerspectiveCamera; + public ground: Mesh; + public renderer: WebGLRenderer; + public controls: OrbitControls; + public callback: Function; + public gridHelper: GridHelper; + public model: URDFRobot; + public liveStreamTexture: CanvasTexture; + private fog: FogExp2; + private isLoaded: boolean = false; + highlightMaterial: any; - constructor() { - this.scene = new Scene() - if (this.scene.environment?.mapping) { - this.scene.environment.mapping = EquirectangularReflectionMapping; - } - return this - } + constructor() { + this.scene = new Scene(); + if (this.scene.environment?.mapping) { + this.scene.environment.mapping = EquirectangularReflectionMapping; + } + return this; + } - public addRenderer = (parameters?: WebGLRendererParameters) => { - this.renderer = new WebGLRenderer(parameters); - this.renderer.outputColorSpace = "srgb"; - this.renderer.shadowMap.enabled = true; - this.renderer.shadowMap.type = PCFSoftShadowMap; - this.renderer.toneMapping = ACESFilmicToneMapping; - this.renderer.toneMappingExposure = 0.85; - document.body.appendChild(this.renderer.domElement); - return this - } + public addRenderer = (parameters?: WebGLRendererParameters) => { + this.renderer = new WebGLRenderer(parameters); + this.renderer.outputColorSpace = 'srgb'; + this.renderer.shadowMap.enabled = true; + this.renderer.shadowMap.type = PCFSoftShadowMap; + this.renderer.toneMapping = ACESFilmicToneMapping; + this.renderer.toneMappingExposure = 0.85; + document.body.appendChild(this.renderer.domElement); + return this; + }; - public addSky = () => { - const sky = new Sky(); - sky.scale.setScalar(450000); - this.scene.add(sky); - const effectController = { - turbidity: 10, - rayleigh: 3, - mieCoefficient: 0.005, - mieDirectionalG: 0.7, - elevation: calculateCurrentSunElevation(), - azimuth: 180, - exposure: this.renderer.toneMappingExposure - }; - const uniforms = sky.material.uniforms; - uniforms['turbidity'].value = effectController.turbidity; - uniforms['rayleigh'].value = effectController.rayleigh; - uniforms['mieCoefficient'].value = effectController.mieCoefficient; - uniforms['mieDirectionalG'].value = effectController.mieDirectionalG; - this.renderer.toneMappingExposure = 0.5; - const phi = MathUtils.degToRad( 90 - effectController.elevation ); - const theta = MathUtils.degToRad( effectController.azimuth ); - const sun = new Vector3(); + public addSky = () => { + const sky = new Sky(); + sky.scale.setScalar(450000); + this.scene.add(sky); + const effectController = { + turbidity: 10, + rayleigh: 3, + mieCoefficient: 0.005, + mieDirectionalG: 0.7, + elevation: calculateCurrentSunElevation(), + azimuth: 180, + exposure: this.renderer.toneMappingExposure + }; + const uniforms = sky.material.uniforms; + uniforms['turbidity'].value = effectController.turbidity; + uniforms['rayleigh'].value = effectController.rayleigh; + uniforms['mieCoefficient'].value = effectController.mieCoefficient; + uniforms['mieDirectionalG'].value = effectController.mieDirectionalG; + this.renderer.toneMappingExposure = 0.5; + const phi = MathUtils.degToRad(90 - effectController.elevation); + const theta = MathUtils.degToRad(effectController.azimuth); + const sun = new Vector3(); - sun.setFromSphericalCoords( 1, phi, theta ); - uniforms[ 'sunPosition' ].value.copy( sun ); - return this - } + sun.setFromSphericalCoords(1, phi, theta); + uniforms['sunPosition'].value.copy(sun); + return this; + }; - public addPerspectiveCamera = (options:position) => { - this.camera = new PerspectiveCamera(); - this.camera.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); - this.scene.add(this.camera); - return this - } + public addPerspectiveCamera = (options: position) => { + this.camera = new PerspectiveCamera(); + this.camera.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); + this.scene.add(this.camera); + return this; + }; - public addGroundPlane = (options:position) => { - this.ground = new Mesh( new PlaneGeometry(), new ShadowMaterial({side: 2})); - this.ground.rotation.x = -Math.PI / 2; - this.ground.scale.setScalar(30); - this.ground.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); - this.ground.receiveShadow = true; - this.scene.add(this.ground); - return this - } + public addGroundPlane = (options: position) => { + this.ground = new Mesh(new PlaneGeometry(), new ShadowMaterial({ side: 2 })); + this.ground.rotation.x = -Math.PI / 2; + this.ground.scale.setScalar(30); + this.ground.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); + this.ground.receiveShadow = true; + this.scene.add(this.ground); + return this; + }; - public addOrbitControls = (minDistance:number, maxDistance:number) => { - this.controls = new OrbitControls(this.camera, this.renderer.domElement); - this.controls.minDistance = minDistance; - this.controls.maxDistance = maxDistance; - this.controls.update(); - return this - } + public addOrbitControls = (minDistance: number, maxDistance: number) => { + this.controls = new OrbitControls(this.camera, this.renderer.domElement); + this.controls.minDistance = minDistance; + this.controls.maxDistance = maxDistance; + this.controls.update(); + return this; + }; - public addAmbientLight = (options:light) => { - const ambientLight = new AmbientLight(options.color, options.intensity); - this.scene.add(ambientLight); - return this - } + public addAmbientLight = (options: light) => { + const ambientLight = new AmbientLight(options.color, options.intensity); + this.scene.add(ambientLight); + return this; + }; - public addDirectionalLight = (options:directionalLight) => { - const directionalLight = new DirectionalLight(options.color, options.intensity); - directionalLight.castShadow = true; - directionalLight.shadow.mapSize.setScalar(2048); - directionalLight.shadow.mapSize.width = 1024; - directionalLight.shadow.mapSize.height = 1024; - directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); - directionalLight.shadow.radius = 5 - this.scene.add(directionalLight); - return this - } + public addDirectionalLight = (options: directionalLight) => { + const directionalLight = new DirectionalLight(options.color, options.intensity); + directionalLight.castShadow = true; + directionalLight.shadow.mapSize.setScalar(2048); + directionalLight.shadow.mapSize.width = 1024; + directionalLight.shadow.mapSize.height = 1024; + directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); + directionalLight.shadow.radius = 5; + this.scene.add(directionalLight); + return this; + }; - public addGridHelper = (options:gridHelperOptions) => { - this.gridHelper = new GridHelper(options.size, options.divisions); - this.gridHelper.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); - this.gridHelper.material.opacity = 0.2; - this.gridHelper.material.depthWrite = false; - this.gridHelper.material.transparent = true; - this.scene.add(this.gridHelper); - return this - } + public addGridHelper = (options: gridHelperOptions) => { + this.gridHelper = new GridHelper(options.size, options.divisions); + this.gridHelper.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); + this.gridHelper.material.opacity = 0.2; + this.gridHelper.material.depthWrite = false; + this.gridHelper.material.transparent = true; + this.scene.add(this.gridHelper); + return this; + }; - public addFogExp2 = (color:ColorRepresentation, density?:number) => { - this.scene.fog = new FogExp2(color, density); - return this - } + public addFogExp2 = (color: ColorRepresentation, density?: number) => { + this.scene.fog = new FogExp2(color, density); + return this; + }; - public handleResize = () => { - this.renderer.setSize(window.innerWidth, window.innerHeight); - this.renderer.setPixelRatio(window.devicePixelRatio); - this.camera.aspect = window.innerWidth / window.innerHeight; - this.camera.updateProjectionMatrix(); - return this - } + public handleResize = () => { + this.renderer.setSize(window.innerWidth, window.innerHeight); + this.renderer.setPixelRatio(window.devicePixelRatio); + this.camera.aspect = window.innerWidth / window.innerHeight; + this.camera.updateProjectionMatrix(); + return this; + }; - public addRenderCb = (callback:Function) => { - this.callback = callback - return this - } + public addRenderCb = (callback: Function) => { + this.callback = callback; + return this; + }; - public startRenderLoop = () => { - this.renderer.setAnimationLoop(() => { - this.renderer.render(this.scene, this.camera); - this.handleRobotShadow() - if(this.callback) this.callback() - if(!this.liveStreamTexture) return - }); - return this - } + public startRenderLoop = () => { + this.renderer.setAnimationLoop(() => { + this.renderer.render(this.scene, this.camera); + this.handleRobotShadow(); + if (this.callback) this.callback(); + if (!this.liveStreamTexture) return; + }); + return this; + }; - public addArrowHelper = (options?:arrowOptions) => { - const dir = new Vector3(options?.direction.x ?? 0, options?.direction.y ?? 0, options?.direction.z ?? 0); - const origin = new Vector3(options?.origin.x ?? 0, options?.origin.y ?? 0, options?.origin.z ?? 0); - const arrowHelper = new ArrowHelper( dir, origin, options?.length ?? 1.5, options?.color ?? 0xff0000 ); - this.scene.add( arrowHelper ); - return this - } + public addArrowHelper = (options?: arrowOptions) => { + const dir = new Vector3( + options?.direction.x ?? 0, + options?.direction.y ?? 0, + options?.direction.z ?? 0 + ); + const origin = new Vector3( + options?.origin.x ?? 0, + options?.origin.y ?? 0, + options?.origin.z ?? 0 + ); + const arrowHelper = new ArrowHelper( + dir, + origin, + options?.length ?? 1.5, + options?.color ?? 0xff0000 + ); + this.scene.add(arrowHelper); + return this; + }; - private setJointValue(jointName:string, angle:number) { - if (!this.model) return; - if (!this.model.joints[jointName]) return; - this.model.joints[jointName].setJointValue(angle) - } + private setJointValue(jointName: string, angle: number) { + if (!this.model) return; + if (!this.model.joints[jointName]) return; + this.model.joints[jointName].setJointValue(angle); + } - isJoint = j => j.isURDFJoint && j.jointType !== 'fixed'; + isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed'; - highlightLinkGeometry = (m: URDFMimicJoint, revert:boolean, material: MeshPhongMaterial) => { - const traverse = c => { - if (c.type === 'Mesh') { - if (revert) { - c.material = c.__origMaterial; - delete c.__origMaterial; - } else { - c.__origMaterial = c.material; - c.material = material; - } - } + highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => { + const traverse = (c: any) => { + if (c.type === 'Mesh') { + if (revert) { + c.material = c.__origMaterial; + delete c.__origMaterial; + } else { + c.__origMaterial = c.material; + c.material = material; + } + } - if (c === m || !this.isJoint(c)) { - for (let i = 0; i < c.children.length; i++) { - const child = c.children[i]; - if (!child.isURDFCollider) { - traverse(c.children[i]); - } - } - } - }; - traverse(m); - }; + if (c === m || !this.isJoint(c)) { + for (let i = 0; i < c.children.length; i++) { + const child = c.children[i]; + if (!child.isURDFCollider) { + traverse(c.children[i]); + } + } + } + }; + traverse(m); + }; - public addModel = (model: any) => { - this.model = model - this.scene.add(model) - return this - } + public addModel = (model: any) => { + this.model = model; + this.scene.add(model); + return this; + }; - public addDragControl = (updateAngle:any) => { - const highlightColor = '#FFFFFF' - const highlightMaterial = - new MeshPhongMaterial({ - shininess: 10, - color: highlightColor, - emissive: highlightColor, - emissiveIntensity: 0.25, - }); - - const dragControls = new PointerURDFDragControls(this.scene, this.camera, this.renderer.domElement); - dragControls.updateJoint = (joint:URDFMimicJoint, angle:number) => { - this.setJointValue(joint.name, angle); - updateAngle(joint.name, angle) - }; - dragControls.onDragStart = () => this.controls.enabled = false; - dragControls.onDragEnd = () => this.controls.enabled = true; - dragControls.onHover = (joint:URDFMimicJoint) => this.highlightLinkGeometry(joint, false, highlightMaterial); - dragControls.onUnhover = (joint:URDFMimicJoint) => this.highlightLinkGeometry(joint, true, highlightMaterial); + public addDragControl = (updateAngle: any) => { + const highlightColor = '#FFFFFF'; + const highlightMaterial = new MeshPhongMaterial({ + shininess: 10, + color: highlightColor, + emissive: highlightColor, + emissiveIntensity: 0.25 + }); - this.renderer.domElement.addEventListener('touchstart', (data) => dragControls._mouseDown(data.touches[0])); - this.renderer.domElement.addEventListener('touchmove', (data) => dragControls._mouseMove(data.touches[0])) - this.renderer.domElement.addEventListener('touchup', (data) => dragControls._mouseUp(data.touches[0])); - return this - } + const dragControls = new PointerURDFDragControls( + this.scene, + this.camera, + this.renderer.domElement + ); + dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => { + this.setJointValue(joint.name, angle); + updateAngle(joint.name, angle); + }; + dragControls.onDragStart = () => (this.controls.enabled = false); + dragControls.onDragEnd = () => (this.controls.enabled = true); + dragControls.onHover = (joint: URDFMimicJoint) => + this.highlightLinkGeometry(joint, false, highlightMaterial); + dragControls.onUnhover = (joint: URDFMimicJoint) => + this.highlightLinkGeometry(joint, true, highlightMaterial); - public toggleFog = () => { - this.scene.fog = this.scene.fog ? null : this.fog; - } + this.renderer.domElement.addEventListener('touchstart', (data) => + dragControls._mouseDown(data.touches[0]) + ); + this.renderer.domElement.addEventListener('touchmove', (data) => + dragControls._mouseMove(data.touches[0]) + ); + this.renderer.domElement.addEventListener('touchend', (data) => + dragControls._mouseUp(data.touches[0]) + ); + return this; + }; - private handleRobotShadow = () => { - if(this.isLoaded) return - const intervalId = setInterval(() => { - this.model?.traverse(c => c.castShadow = true); - }, 10); - setTimeout(() => { - clearInterval(intervalId) - }, 1000); - this.isLoaded = true; - } -} \ No newline at end of file + public toggleFog = () => { + this.scene.fog = this.scene.fog ? null : this.fog; + }; + + private handleRobotShadow = () => { + if (this.isLoaded) return; + const intervalId = setInterval(() => { + this.model?.traverse((c) => (c.castShadow = true)); + }, 10); + setTimeout(() => { + clearInterval(intervalId); + }, 1000); + this.isLoaded = true; + }; +} diff --git a/app/src/lib/services/file-service.ts b/app/src/lib/services/file-service.ts new file mode 100644 index 0000000..2a529db --- /dev/null +++ b/app/src/lib/services/file-service.ts @@ -0,0 +1,71 @@ +import { Result } from '$lib/utilities/result'; + +class FileService { + private dbName = 'fileStorageDB'; + private dbVersion = 1; + private storeName = 'files'; + private dbPromise: Promise>; + + constructor() { + this.dbPromise = this.openDatabase(); + } + + private async openDatabase(): Promise> { + return new Promise((resolve) => { + const request = indexedDB.open(this.dbName, this.dbVersion); + + request.onerror = () => resolve(Result.err('Error opening database')); + + request.onsuccess = () => resolve(Result.ok(request.result)); + + request.onupgradeneeded = (event) => { + const db = request.result; + if (!db.objectStoreNames.contains(this.storeName)) { + db.createObjectStore(this.storeName); + } + }; + }); + } + + private async getStore(mode: IDBTransactionMode): Promise> { + const dbResult = await this.dbPromise; + if (dbResult.isErr()) { + return Result.err('Database not initialized properly'); + } + const db = dbResult.inner; + const transaction = db.transaction(this.storeName, mode); + return Result.ok(transaction.objectStore(this.storeName)); + } + + public async saveFile(key: string, file: Uint8Array): Promise> { + const storeResult = await this.getStore('readwrite'); + if (storeResult.isErr()) { + return Result.err('Failed to access object store for writing'); + } + const store = storeResult.inner; + + return new Promise((resolve) => { + const request = store.put(file, key); + request.onsuccess = () => resolve(Result.ok(request.result)); + request.onerror = () => resolve(Result.err('Failed to save file')); + }); + } + + public async getFile(key: string): Promise> { + const storeResult = await this.getStore('readonly'); + if (storeResult.isErr()) { + return Result.err('Failed to access object store for reading'); + } + const store = storeResult.inner; + + return new Promise((resolve) => { + const request = store.get(key); + + request.onsuccess = () => + resolve(request.result ? Result.ok(request.result) : Result.err('File content not found')); + request.onerror = () => resolve(Result.err('Failed to retrieve file')); + }); + } +} + +export default new FileService(); diff --git a/app/src/lib/services/index.ts b/app/src/lib/services/index.ts new file mode 100644 index 0000000..5b10489 --- /dev/null +++ b/app/src/lib/services/index.ts @@ -0,0 +1,3 @@ +export { default as fileService } from './file-service'; +export { default as socketService } from './socket-service'; +export { default as resultService } from './result-service'; diff --git a/app/src/lib/services/result-service.ts b/app/src/lib/services/result-service.ts new file mode 100644 index 0000000..ceb4900 --- /dev/null +++ b/app/src/lib/services/result-service.ts @@ -0,0 +1,19 @@ +import { errorLogs, latestErrorLog } from '$lib/stores'; +import type { Result } from '$lib/utilities'; + +class ResultService { + public handleResult(result: Result, tag?: string) { + if (result.isErr()) { + const errorLogEntry = { tag, message: result.inner, exception: result.exception }; + latestErrorLog.set(errorLogEntry); + errorLogs.update((entries) => { + entries.push(errorLogEntry); + return entries; + }); + } + + return result; + } +} + +export default new ResultService(); diff --git a/app/src/lib/services/socket-service.ts b/app/src/lib/services/socket-service.ts new file mode 100644 index 0000000..03a08a7 --- /dev/null +++ b/app/src/lib/services/socket-service.ts @@ -0,0 +1,91 @@ +import { isConnected, socketData } from '$lib/stores'; +import { Result, Ok } from '$lib/utilities'; +import { resultService } from '$lib/services'; +import { type WebSocketJsonMsg } from '$lib/models'; +import type { Writable } from 'svelte/store'; + +type WebsocketOutData = string | ArrayBufferLike | Blob | ArrayBufferView; + +// TODO +/** + * MOVE THE store to a store.ts file + * + * Make an object on the class that encapsulate all the stores + * + * Make the handle message function look up the type and set the value, to simplify the code + */ + +class SocketService { + private socket!: WebSocket; + + constructor() {} + + public connect(url: string): void { + this.socket = new WebSocket(url); + this.socket.binaryType = 'arraybuffer'; + this.socket.onopen = () => this.handleConnected(); + this.socket.onclose = () => this.handleDisconnected(); + this.socket.onmessage = (event: MessageEvent) => + resultService.handleResult(this.handleMessage(event), 'SocketService'); + this.socket.onerror = (error: Event) => console.log(error); + } + + public send(data: WebsocketOutData): Result { + if (this.socket.readyState === WebSocket.OPEN) { + this.socket.send(data); + return Ok.void(); + } + return Result.err('The connection is not open'); + } + + public addPublisher(store: Writable, type?: string) { + store.subscribe((data) => this.send(type ? JSON.stringify({ type, data }) : data)); + } + + private handleConnected(): void { + isConnected.set(true); + } + + private handleDisconnected(): void { + isConnected.set(false); + } + + private getJsonFromMessage(msg: string): Result { + try { + return Result.ok(JSON.parse(msg) as WebSocketJsonMsg); + } catch (error) { + return Result.err('Failed to parse socket message', error); + } + } + + private handleBufferMessage(buffer: ArrayBuffer): Result { + console.log(buffer); + return Ok.void(); + } + + private handleMessage(event: MessageEvent): Result { + if (event.data instanceof ArrayBuffer) { + return this.handleBufferMessage(event.data); + } + let msgRes = this.getJsonFromMessage(event.data); + if (msgRes.isErr()) { + return msgRes; + } + const msg = msgRes.inner; + + if (msg.type === 'log') { + socketData.logs.update((entries) => { + entries.push(msg.data); + return entries; + }); + return Ok.void(); + } else if (msg.data && msg.type in socketData) { + socketData[msg.type].set(msg.data); + return Ok.void(); + } + + return Result.err(`Got invalid msg: ${JSON.stringify(msg)}`); + } +} + +export default new SocketService(); diff --git a/app/src/lib/socket.ts b/app/src/lib/socket.ts deleted file mode 100644 index 2fcfddb..0000000 --- a/app/src/lib/socket.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { writable, type Writable } from 'svelte/store'; - -export type WebSocketStatus = 'OPEN' | 'CONNECTING' | 'CLOSED' - -export const isConnected = writable(false) - -export const angles = writable(new Int16Array(12).fill(0)) -export const log = writable([]) -export const battery = writable({}) -export const mpu = writable({heading:0}) -export const distances = writable({}) -export const settings = writable({}) -export const systemInfo = writable({} as number) - -export const dataBuffer = writable(new Float32Array(13)) - -export const servoBuffer:Writable = writable(new Int16Array(12)) - -export const data = writable(); - -export const status:Writable = writable('CLOSED') - -export const socket:Writable = writable() - -export const connect = (url:string) => { - status.set('CONNECTING') - let _socket = new WebSocket(url); - _socket.binaryType = "arraybuffer"; - _socket.onopen = _connected; - _socket.onclose = _disconnected; - _socket.onmessage = _message; - socket.set(_socket) - - servoBuffer.subscribe(data => { - if(_socket.readyState !== 1) return - const buffer = [] - buffer[0] = 1 - buffer.push(...data) - _socket.send(new Int16Array(buffer)) - }) -} - -const _connected = () => { - status.set('OPEN') - isConnected.set(true) -} - -const _disconnected = () => { - status.set('CLOSED') - isConnected.set(false) -} - -const _message = (event:any) => { - if (event.data instanceof ArrayBuffer) { - let buffer = new Int8Array(event.data); - if(buffer.length === 44) { - dataBuffer.set(new Float32Array(buffer.buffer) ) - } - } else { - let data = event.data - try { - data = JSON.parse(event.data) - } catch (error) { - console.warn(error) - } - switch (data.type) { - case "angles": - angles.set(data.angles) - break - case "logs": - log.set(data.logs) - break - case "log": - log.update(entries => {entries.push(data.log); return entries}) - break - case "settings": - settings.set(data.settings) - case "info": - systemInfo.set(data.info) - break - case "mpu": - mpu.set(data.mpu) - break - case "distances": - distances.set(data.distances) - break - case "battery": - battery.set(data.battery) - break - } - } -} \ No newline at end of file diff --git a/app/src/lib/store.ts b/app/src/lib/store.ts index 26ed9ff..8d4bdb1 100644 --- a/app/src/lib/store.ts +++ b/app/src/lib/store.ts @@ -1,12 +1,17 @@ import { writable } from 'svelte/store'; -import { persistentStore } from './utils'; +import { persistentStore } from '$lib/utilities'; export const emulateModel = writable(true); -export const input = writable({left:{x:0, y:0}, right:{x:0, y:0}, height:70, speed:0}); +export const input = writable({ + left: { x: 0, y: 0 }, + right: { x: 0, y: 0 }, + height: 70, + speed: 0 +}); -export const outControllerData = writable(new Uint8Array([0, 128, 128, 128, 128, 70, 0])); +export const outControllerData = writable(new Int8Array([0, 0, 0, 0, 0, 70, 0])); -export const jointNames = persistentStore("joint_names", []) +export const jointNames = persistentStore('joint_names', []); -export const model = writable() \ No newline at end of file +export const model = writable(); diff --git a/app/src/lib/stores/index.ts b/app/src/lib/stores/index.ts new file mode 100644 index 0000000..e416c99 --- /dev/null +++ b/app/src/lib/stores/index.ts @@ -0,0 +1,2 @@ +export * from './socket-store'; +export * from './logging-store'; diff --git a/app/src/lib/stores/logging-store.ts b/app/src/lib/stores/logging-store.ts new file mode 100644 index 0000000..ef01666 --- /dev/null +++ b/app/src/lib/stores/logging-store.ts @@ -0,0 +1,11 @@ +import { writable, type Writable } from 'svelte/store'; + +export interface errorLog { + message: unknown; + tag?: string; + exception?: unknown; +} + +export const latestErrorLog: Writable = writable(); + +export const errorLogs: Writable = writable([]); diff --git a/app/src/lib/stores/socket-store.ts b/app/src/lib/stores/socket-store.ts new file mode 100644 index 0000000..b915c22 --- /dev/null +++ b/app/src/lib/stores/socket-store.ts @@ -0,0 +1,31 @@ +import { writable, type Writable } from 'svelte/store'; +import { type angles } from '$lib/models'; + +export const isConnected = writable(false); +export const servoAngles: Writable = writable(new Int16Array(12).fill(0)); +export const logs = writable([] as string[]); +export const battery = writable({}); +export const mpu = writable({ heading: 0 }); +export const distances = writable({}); +export const settings = writable({}); +export const systemInfo = writable({} as number); + +export interface socketDataCollection { + angles: Writable; + logs: Writable; + battery: Writable; + mpu: Writable; + distances: Writable; + settings: Writable; + systemInfo: Writable; +} + +export const socketData = { + angles: servoAngles, + logs, + battery, + mpu, + distances, + settings, + systemInfo +}; diff --git a/app/src/lib/throttle.ts b/app/src/lib/throttle.ts deleted file mode 100644 index d6c610b..0000000 --- a/app/src/lib/throttle.ts +++ /dev/null @@ -1,15 +0,0 @@ -export class throttler { - private _throttlePause: boolean; - constructor() { - this._throttlePause = false; - } - throttle = (callback:Function, time:number) => { - if (this._throttlePause) return; - - this._throttlePause = true; - setTimeout(() => { - callback(); - this._throttlePause = false; - }, time); - }; -} diff --git a/app/src/lib/utilities/buffer-utilities.ts b/app/src/lib/utilities/buffer-utilities.ts new file mode 100644 index 0000000..a5f5182 --- /dev/null +++ b/app/src/lib/utilities/buffer-utilities.ts @@ -0,0 +1,15 @@ +export class throttler { + private _throttlePause: boolean; + constructor() { + this._throttlePause = false; + } + throttle = (callback: Function, time: number) => { + if (this._throttlePause) return; + + this._throttlePause = true; + setTimeout(() => { + callback(); + this._throttlePause = false; + }, time); + }; +} diff --git a/app/src/lib/utilities/index.ts b/app/src/lib/utilities/index.ts new file mode 100644 index 0000000..90b8760 --- /dev/null +++ b/app/src/lib/utilities/index.ts @@ -0,0 +1,7 @@ +export * from './result'; +export * from './string-utilities'; +export * from './svelte-utilities'; +export * from './math-utilities'; +export * from './buffer-utilities'; +export * from './model-utilities'; +export * from './location-utilities'; diff --git a/app/src/lib/utilities/location-utilities.ts b/app/src/lib/utilities/location-utilities.ts new file mode 100644 index 0000000..da1a008 --- /dev/null +++ b/app/src/lib/utilities/location-utilities.ts @@ -0,0 +1,8 @@ +const forWeb = import.meta.env.MODE === 'WEB'; +const mock = import.meta.env.MODE === 'MOCK'; + +export const location = mock ? `${window.location.hostname}:2096` : 'leika.local'; + +export const socketLocation = forWeb + ? `wss://${window.location.hostname}:2096` + : `ws://${location}`; diff --git a/app/src/lib/utilities/math-utilities.ts b/app/src/lib/utilities/math-utilities.ts new file mode 100644 index 0000000..e4e8996 --- /dev/null +++ b/app/src/lib/utilities/math-utilities.ts @@ -0,0 +1,11 @@ +export const toUint8 = (number: number, min: number, max: number) => { + number = Math.max(min, Math.min(max, number)); + let scaled = ((number - min) / (max - min)) * 255; + return Math.round(scaled) & 0xff; +}; + +export const toInt8 = (number: number, min: number, max: number) => { + number = Math.max(min, Math.min(max, number)); + let scaled = ((number - min) / (max - min)) * 255 - 128; + return Math.max(-128, Math.min(127, Math.round(scaled))) | 0; +}; \ No newline at end of file diff --git a/app/src/lib/utilities/model-utilities.ts b/app/src/lib/utilities/model-utilities.ts new file mode 100644 index 0000000..7feda9c --- /dev/null +++ b/app/src/lib/utilities/model-utilities.ts @@ -0,0 +1,38 @@ +import { LoaderUtils } from 'three'; +import URDFLoader, { type URDFRobot } from 'urdf-loader'; +import { XacroLoader } from 'xacro-parser'; +import { Result } from '$lib/utilities'; + +export const loadModelAsync = async ( + url: string +): Promise> => { + return new Promise((resolve, reject) => { + const xacroLoader = new XacroLoader(); + + xacroLoader.load( + url, + async (xml) => { + const urdfLoader = new URDFLoader(); + + urdfLoader.workingPath = LoaderUtils.extractUrlBase(url); + + try { + const model = urdfLoader.parse(xml); + model.rotation.x = -Math.PI / 2; + model.rotation.z = Math.PI / 2; + model.traverse((c) => (c.castShadow = true)); + model.updateMatrixWorld(true); + model.scale.setScalar(10); + const joints = Object.entries(model.joints) + .filter((joint) => joint[1].jointType !== 'fixed') + .map((joint) => joint[0]); + + resolve(Result.ok([model, joints])); + } catch (error) { + resolve(Result.err('Failed to load model', error)); + } + }, + (error) => reject(error) + ); + }); +}; diff --git a/app/src/lib/utilities/result/err.ts b/app/src/lib/utilities/result/err.ts new file mode 100644 index 0000000..8c879be --- /dev/null +++ b/app/src/lib/utilities/result/err.ts @@ -0,0 +1,42 @@ +export class Err { + #inner: T; + #exception?: U; + + constructor(inner: T, exception?: U) { + this.#inner = inner; + this.#exception = exception; + } + + get inner(): T { + return this.#inner; + } + + get exception(): U | undefined { + return this.#exception; + } + + /** + * Type guard for `Ok` + * @returns `true` if `Ok`; `false` if `Err` + */ + isOk(): false { + return false; + } + + /** + * Type guard for `Err` + * @returns `true` if `Err`; `false` if `Ok` + */ + isErr(): this is Err { + return true; + } + + /** + * Create an `Err` + * @param inner + * @returns `Err(inner)` + */ + static new(inner: E, exception: F): Err { + return new Err(inner, exception); + } +} diff --git a/app/src/lib/utilities/result/index.ts b/app/src/lib/utilities/result/index.ts new file mode 100644 index 0000000..55e069b --- /dev/null +++ b/app/src/lib/utilities/result/index.ts @@ -0,0 +1,3 @@ +export * from './err'; +export * from './ok'; +export * from './result'; diff --git a/app/src/lib/utilities/result/ok.ts b/app/src/lib/utilities/result/ok.ts new file mode 100644 index 0000000..868b3c8 --- /dev/null +++ b/app/src/lib/utilities/result/ok.ts @@ -0,0 +1,44 @@ +export class Ok { + #inner: T; + + constructor(inner: T) { + this.#inner = inner; + } + + get inner(): T { + return this.#inner; + } + + /** + * Type guard for `Ok` + * @returns `true` if `Ok`; `false` if `Err` + */ + isOk(): this is Ok { + return true; + } + + /** + * Type guard for `Err` + * @returns `true` if `Err`; `false` if `Ok` + */ + isErr(): false { + return false; + } + + /** + * Create an `Ok` + * @param inner + * @returns `Ok(inner)` + */ + static new(inner: T): Ok { + return new Ok(inner); + } + + /** + * Create an empty `Ok` + * @returns `Ok(void)` + */ + static void(): Ok { + return new Ok(undefined); + } +} diff --git a/app/src/lib/utilities/result/result.ts b/app/src/lib/utilities/result/result.ts new file mode 100644 index 0000000..4e86e00 --- /dev/null +++ b/app/src/lib/utilities/result/result.ts @@ -0,0 +1,20 @@ +import { Err } from './err'; +import { Ok } from './ok'; + +export type Result = Ok | Err; + +export namespace Result { + /** + * @returns `Ok` + */ + export function ok(value: T) { + return Ok.new(value); + } + + /** + * @returns `Err` + */ + export function err(error: E, exception?: F) { + return Err.new(error, exception); + } +} diff --git a/app/src/lib/utilities/string-utilities.ts b/app/src/lib/utilities/string-utilities.ts new file mode 100644 index 0000000..4394aaa --- /dev/null +++ b/app/src/lib/utilities/string-utilities.ts @@ -0,0 +1,5 @@ +export const humanFileSize = (size: number): string => { + const units = ['B', 'kB', 'MB', 'GB', 'TB']; + var i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); + return Number((size / Math.pow(1024, i)).toFixed(2)) * 1 + units[i]; +}; diff --git a/app/src/lib/utilities/svelte-utilities.ts b/app/src/lib/utilities/svelte-utilities.ts new file mode 100644 index 0000000..5a72ba8 --- /dev/null +++ b/app/src/lib/utilities/svelte-utilities.ts @@ -0,0 +1,13 @@ +import { writable } from 'svelte/store'; + +export const persistentStore = (key: string, initialValue: any) => { + const savedValue = JSON.parse(localStorage.getItem(key) as string); + const data = savedValue !== null ? savedValue : initialValue; + const store = writable(data); + + store.subscribe((value) => { + localStorage.setItem(key, JSON.stringify(value)); + }); + + return store; +}; diff --git a/app/src/lib/utils.ts b/app/src/lib/utils.ts deleted file mode 100644 index e979218..0000000 --- a/app/src/lib/utils.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { writable } from 'svelte/store'; - -export const humanFileSize = (size:number):string => { - var i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); - return Number((size / Math.pow(1024, i)).toFixed(2)) * 1 + ['B', 'kB', 'MB', 'GB', 'TB'][i]; -} - -export const lerp = (start: number, end: number, amt: number) => { - return (1 - amt) * start + amt * end; -}; - -export const persistentStore = (key:string, initialValue:any) => { - const savedValue = JSON.parse(localStorage.getItem(key) as string); - const data = savedValue !== null ? savedValue : initialValue; - const store = writable(data); - - store.subscribe(value => { - localStorage.setItem(key, JSON.stringify(value)); - }); - - return store; -} \ No newline at end of file diff --git a/app/src/routes/Controller.svelte b/app/src/routes/Controller.svelte index e6108e1..6475a03 100644 --- a/app/src/routes/Controller.svelte +++ b/app/src/routes/Controller.svelte @@ -1,15 +1,15 @@
- {#if $emulateModel} - - {:else} - - {/if} + {#if $emulateModel} + + {:else} + + {/if}
diff --git a/app/src/routes/Settings.svelte b/app/src/routes/Settings.svelte index d827f93..c9b5b89 100644 --- a/app/src/routes/Settings.svelte +++ b/app/src/routes/Settings.svelte @@ -3,54 +3,63 @@ import Info from '../components/settings/Info.svelte'; import Log from '../components/settings/Log.svelte'; import Configuration from '../components/settings/Configuration.svelte'; - import { Icon, Wifi, CommandLine, InformationCircle, BookOpen, AdjustmentsVertical, Cog6Tooth, Newspaper } from 'svelte-hero-icons'; + import { + Icon, + Wifi, + CommandLine, + InformationCircle, + BookOpen, + AdjustmentsVertical, + Cog6Tooth, + Newspaper + } from 'svelte-hero-icons'; import Calibration from '../components/settings/Calibration.svelte'; - export const page = "" + export const page = ''; const menu = [ { title: 'Calibration', path: '/calibration', - icon: AdjustmentsVertical, + icon: AdjustmentsVertical, component: Calibration }, { - title: 'System info', + title: 'System info', path: '/info', - icon: InformationCircle, + icon: InformationCircle, component: Info }, - { - title: 'Log', + { + title: 'Log', path: '/log', - icon: BookOpen, + icon: BookOpen, component: Log }, - { - title: 'Settings', - path: '/settings', - icon: Cog6Tooth, - component: Configuration - }, + { + title: 'Settings', + path: '/settings', + icon: Cog6Tooth, + component: Configuration + } ];
- - {#each menu as link} - - {/each} - + + {#each menu as link} + + {/each} +
diff --git a/app/tailwind.config.js b/app/tailwind.config.js index 34038df..bbcf24f 100644 --- a/app/tailwind.config.js +++ b/app/tailwind.config.js @@ -2,22 +2,22 @@ export default { content: ['./src/**/*.{html,js,ts,svelte}'], theme: { - extend: { - colors: { - 'primary': '#6200EE', - 'primary-variant': '#3700B3', - 'secondary': '#3700B3', - 'secondary-variant': '#3700B3', - 'background': '#1e1e1e', - 'surface': '#2c2c2c', - 'error': '#B00020', - 'on-primary': '#FFFFFF', - 'on-secondary': '#FFFFFF', - 'on-background': '#FFFFFF', - 'on-surface': '#FFFFFF', - 'on-error': '#FFFFFF', - } - }, + extend: { + colors: { + primary: '#6200EE', + 'primary-variant': '#3700B3', + secondary: '#3700B3', + 'secondary-variant': '#3700B3', + background: '#1e1e1e', + surface: '#2c2c2c', + error: '#B00020', + 'on-primary': '#FFFFFF', + 'on-secondary': '#FFFFFF', + 'on-background': '#FFFFFF', + 'on-surface': '#FFFFFF', + 'on-error': '#FFFFFF' + } + } }, plugins: [] }; diff --git a/app/test/specs/byteformat.spec.ts b/app/test/specs/byteformat.spec.ts new file mode 100644 index 0000000..f8d5df9 --- /dev/null +++ b/app/test/specs/byteformat.spec.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from 'vitest'; +import { humanFileSize } from '../../src/lib/utilities'; + +describe('humanFileSize', () => { + it('returns "0B" for 0 bytes', () => { + expect(humanFileSize(0)).toBe('0B'); + }); + + it('returns the size in bytes correctly', () => { + expect(humanFileSize(500)).toBe('500B'); + }); + + it('returns the size in kB correctly', () => { + expect(humanFileSize(1024)).toBe('1kB'); + }); + + it('returns the size in MB correctly', () => { + expect(humanFileSize(1048576)).toBe('1MB'); // 1024 * 1024 + }); + + it('returns the size in GB correctly', () => { + expect(humanFileSize(1073741824)).toBe('1GB'); // 1024 * 1024 * 1024 + }); + + it('rounds to 2 decimal places correctly', () => { + expect(humanFileSize(1536)).toBe('1.5kB'); // 1024 + 512 + }); +}); diff --git a/app/test/specs/number-convert.spec.ts b/app/test/specs/number-convert.spec.ts new file mode 100644 index 0000000..6499494 --- /dev/null +++ b/app/test/specs/number-convert.spec.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'vitest'; +import { toUint8, toInt8 } from '../../src/lib/utilities'; + +describe('toUint8', () => { + it('min interval value should get 0', () => { + expect(toUint8(-1, -1, 1)).toBe(0); + }); + it('middle interval value should get 128', () => { + expect(toUint8(0, -1, 1)).toBe(128); + }); + + it('max interval value should get 255', () => { + expect(toUint8(1, -1, 1)).toBe(255); + }); + + it('min value should be clamped', () => { + expect(toUint8(-2, -1, 1)).toBe(0); + }); + + it('max value should be clamped', () => { + expect(toUint8(2, -1, 1)).toBe(255); + }); +}); + +describe('toInt8', () => { + it('min interval value should get -128', () => { + expect(toInt8(-1, -1, 1)).toBe(-128); + }); + it('middle interval value should get 0', () => { + expect(toInt8(0, -1, 1)).toBe(0); + }); + + it('max interval value should get 127', () => { + expect(toInt8(1, -1, 1)).toBe(127); + }); + + it('min value should be clamped', () => { + expect(toInt8(-2, -1, 1)).toBe(-128); + }); + + it('max value should be clamped', () => { + expect(toInt8(2, -1, 1)).toBe(127); + }); +}); + + diff --git a/app/test/specs/result.spec.ts b/app/test/specs/result.spec.ts new file mode 100644 index 0000000..eff37ad --- /dev/null +++ b/app/test/specs/result.spec.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from 'vitest'; +import { Result } from '../../src/lib/utilities'; + +describe('Result', () => { + it('should create a success result correctly', () => { + const successValue = 'Success value'; + const result = Result.ok(successValue); + + expect(result.isOk()).toBe(true); + expect(result.isErr()).toBe(false); + expect(result.inner).toBe(successValue); + }); + + it('should create an error result correctly', () => { + const errorMessage = 'Error message'; + const result = Result.err(errorMessage); + + expect(result.isOk()).toBe(false); + expect(result.isErr()).toBe(true); + expect(result.inner).toBe(errorMessage); + }); + + it('should type guard success and error results correctly', () => { + const successResult = Result.ok(123); + const errorResult = Result.err('Error'); + + if (successResult.isOk()) { + expect(typeof successResult.inner).toBe('number'); + } else { + throw new Error('Expected successResult to be ok'); + } + + if (errorResult.isErr()) { + expect(typeof errorResult.inner).toBe('string'); + } else { + throw new Error('Expected errorResult to be fail'); + } + }); +}); diff --git a/app/test/specs/throttler.spec.ts b/app/test/specs/throttler.spec.ts new file mode 100644 index 0000000..f416362 --- /dev/null +++ b/app/test/specs/throttler.spec.ts @@ -0,0 +1,46 @@ +import { throttler } from '../../src/lib/utilities'; +import { describe, it, expect, beforeEach, afterEach, test, vitest } from 'vitest'; + +describe('throttler', () => { + let throttleInstance: throttler; + let callback; + + beforeEach(() => { + vitest.useFakeTimers(); + throttleInstance = new throttler(); + callback = vitest.fn(); + }); + + afterEach(() => { + vitest.useRealTimers(); + }); + + it('should call the callback function after the specified time', () => { + throttleInstance.throttle(callback, 1000); + expect(callback).not.toHaveBeenCalled(); + + vitest.advanceTimersByTime(1000); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should not call the callback function if throttle is called again within the timeout period', () => { + throttleInstance.throttle(callback, 1000); + throttleInstance.throttle(callback, 1000); + + vitest.advanceTimersByTime(500); + expect(callback).not.toHaveBeenCalled(); + + vitest.advanceTimersByTime(500); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should allow the callback to be called again after the timeout period', () => { + throttleInstance.throttle(callback, 1000); + vitest.advanceTimersByTime(1000); + expect(callback).toHaveBeenCalledTimes(1); + + throttleInstance.throttle(callback, 1000); + vitest.advanceTimersByTime(1000); + expect(callback).toHaveBeenCalledTimes(2); + }); +}); diff --git a/app/tsconfig.json b/app/tsconfig.json index 2e1fbc5..f36052d 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -5,7 +5,7 @@ "useDefineForClassFields": true, "module": "ESNext", "resolveJsonModule": true, - "moduleResolution": "Node", + "moduleResolution": "Node", /** * Typecheck JS in `.svelte` and `.js` files by default. * Disable checkJs if you'd like to use dynamic types in JS. @@ -15,13 +15,12 @@ "allowJs": true, "checkJs": true, "isolatedModules": true, - "paths": { + "paths": { "$lib/*": ["./src/lib/*"], "$utils/*": ["./src/utils/*"], "$components/*": ["./src/components/*"], "$stores/*": ["./src/stores/*"] } - }, "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/app/vite.config.ts b/app/vite.config.ts index 9c50df8..4a7058c 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -2,24 +2,26 @@ import { defineConfig } from 'vite'; import { svelte } from '@sveltejs/vite-plugin-svelte'; import { viteSingleFile } from 'vite-plugin-singlefile'; import viteCompression from 'vite-plugin-compression'; -import path from 'path' +import path from 'path'; -const forEmbedded = process.env.FOR_EMBEDDED == 'true' +const forEmbedded = process.env.FOR_EMBEDDED == 'true'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [svelte(), - ...(forEmbedded ? [ viteSingleFile(), viteCompression({deleteOriginFile: true})]: [])], + plugins: [ + svelte(), + ...(forEmbedded ? [viteSingleFile(), viteCompression({ deleteOriginFile: true })] : []) + ], build: { - outDir: forEmbedded ? '../data': './build', + outDir: forEmbedded ? '../data' : './build', emptyOutDir: true }, - resolve: { - alias: { - '$lib': path.resolve('./src/lib/'), - '$components': path.resolve('./src/components'), - '$utils': path.resolve('./src/utils'), - '$stores': path.resolve('./src/stores'), - }, - }, + resolve: { + alias: { + $lib: path.resolve('./src/lib/'), + $components: path.resolve('./src/components'), + $utils: path.resolve('./src/utils'), + $stores: path.resolve('./src/stores') + } + } }); diff --git a/mock/server.js b/mock/server.js index 555154b..6ccf222 100644 --- a/mock/server.js +++ b/mock/server.js @@ -5,7 +5,7 @@ import { WebSocketServer } from "ws"; const app = express(); const kinematic = new Kinematic(); -const wss = new WebSocketServer({ port: 8080 }); +const wss = new WebSocketServer({ port: 2096 }); app.use(cors()); app.use(express.json()); @@ -89,6 +89,7 @@ const model = { rssi: 100, }, running: true, + mode: "stand", }; const settings = { @@ -197,99 +198,170 @@ const updateAngles = (angles) => { return model.servos.angles; }; +const bufferToController = (buffer) => { + return { + stop: buffer[0], + lx: buffer[1], + ly: buffer[2], + rx: buffer[3], + ry: buffer[4], + h: buffer[5], + s: buffer[6], + }; +}; + +const unpackMessageBuffer = (data) => { + return { + angles: [0, data.rx / 4, data.ry / 4], + position: [data.ly / 2, 70, data.lx / 2], + }; +}; + +const updateStanding = (ws, controller) => { + if (!ws.clientState.model.running) return; + const data = unpackMessageBuffer(controller); + ws.send( + JSON.stringify({ + type: "angles", + data: updateBodyState(ws.clientState.model, data.angles, data.position), + }) + ); +}; + +const handelController = (ws, buffer) => { + const controllerData = bufferToController(new Int8Array(buffer)); + if (controllerData.stop) { + ws.clientState.model.running = false; + ws.clientState.logs.push("[2024-02-05 19:10:00] [Warning] STOPPING SERVOS"); + ws.send(JSON.stringify({ type: "log", data: ws.clientState.logs.last() })); + return; + } + if (ws.clientState.model.mode === "stand") { + updateStanding(ws, controllerData); + } +}; + +const handleBufferMessage = (ws, buffer) => { + if (buffer.length === 6) { + handelController(ws, buffer); + } +}; + +const handleJsonMessage = (ws, message) => { + let data = message; + try { + data = JSON.parse(message); + } catch (error) { + return; + } + switch (data.type) { + case "subscribe": + subscribeClientToCategory(ws, data.category); + break; + case "unsubscribe": + unsubscribeClientFromCategory(ws, data.category); + break; + case "sensor/battery": + ws.send({ type: "battery", data: JSON.stringify(updateBattery()) }); + break; + case "sensor/mpu": + ws.send({ type: "battery", data: JSON.stringify(updateMpu()) }); + break; + case "sensor/distances": + ws.send(JSON.stringify(updateDistances())); + break; + case "sensor/distance": + ws.send(JSON.stringify({ distance: updateDistance(data.position) })); + break; + case "kinematic/angle": + if (data.angle && data.id) { + ws.clientState.model.servos.angles[data.id] = data.angle; + ws.send( + JSON.stringify({ + type: "angles", + data: ws.clientState.model.servos.angles, + }) + ); + } else { + ws.send(JSON.stringify(updateAngle(data.id, data.angle))); + } + break; + case "kinematic/angles": + if (data.angles) { + ws.clientState.model.servos.angles = data.angles; + ws.send( + JSON.stringify({ + type: "angles", + data: ws.clientState.model.servos.angles, + }) + ); + } else { + ws.send(JSON.stringify(updateAngles(data.angles))); + } + break; + case "kinematic/bodystate": + if (data.angles) { + ws.send( + JSON.stringify({ + type: "angles", + data: updateBodyState( + ws.clientState.model, + data.angles, + data.position + ), + }) + ); + } else { + ws.send(JSON.stringify({ angles: model.servos.angles })); + } + break; + case "system/logs": + ws.send(JSON.stringify({ type: "logs", data: ws.clientState.logs })); + break; + case "system/info": + ws.send(JSON.stringify({ type: "info", data: updateSystem() })); + break; + case "system/settings": + if (data.settings) { + Object.entries(data.settings).forEach( + ([key, value]) => (ws.clientState.settings[key] = value) + ); + ws.send(JSON.stringify(ws.clientState.settings)); + } else { + ws.send( + JSON.stringify({ + type: "settings", + settings: ws.clientState.settings, + }) + ); + } + break; + case "system/stop": + ws.clientState.model.running = false; + ws.clientState.logs.push( + "[2024-02-05 19:10:00] [Warning] STOPPING SERVOS" + ); + ws.send( + JSON.stringify({ type: "log", data: ws.clientState.logs.last() }) + ); + break; + default: + ws.send(JSON.stringify({ error: "Unknown request type" })); + } +}; + wss.on("connection", (ws) => { const clientState = createNewClientState(); ws.clientState = clientState; ws.on("error", console.error); ws.on("message", (message) => { - let data = message; - try { - data = JSON.parse(message); - } catch (error) { + if (typeof message === "object") { + handleBufferMessage(ws, message); return; } - switch (data.type) { - case "subscribe": - subscribeClientToCategory(ws, data.category); - break; - case "unsubscribe": - unsubscribeClientFromCategory(ws, data.category); - break; - case "sensor/battery": - ws.send({ type: "battery", battery: JSON.stringify(updateBattery()) }); - break; - case "sensor/mpu": - ws.send({ type: "battery", mpu: JSON.stringify(updateMpu()) }); - break; - case "sensor/distances": - ws.send(JSON.stringify(updateDistances())); - break; - case "sensor/distance": - ws.send(JSON.stringify({ distance: updateDistance(data.position) })); - break; - case "kinematic/angle": - if (data.angle && data.id) { - ws.clientState.model.servos.angles[data.id] = data.angle; - ws.send( - JSON.stringify({ - type: "angles", - angles: ws.clientState.model.servos.angles, - }) - ); - } else { - ws.send(JSON.stringify(updateAngle(data.id, data.angle))); - } - break; - case "kinematic/angles": - if (data.angles) { - ws.clientState.model.servos.angles = data.angles; - ws.send( - JSON.stringify({ - type: "angles", - angles: ws.clientState.model.servos.angles, - }) - ); - } else { - ws.send(JSON.stringify(updateAngles(data.angles))); - } - break; - case "kinematic/bodystate": - if (data.angles) { - ws.send( - JSON.stringify({ - type: "angles", - angles: updateBodyState(ws.clientState.model, data.angles, data.position), - }) - ); - } else { - ws.send(JSON.stringify({ angles: model.servos.angles })); - } - break; - case "system/logs": - ws.send(JSON.stringify({ type: "logs", logs:ws.clientState.logs })); - break; - case "system/info": - ws.send(JSON.stringify({ type: "info", info: updateSystem() })); - break; - case "system/settings": - if (data.settings) { - Object.entries(data.settings).forEach( - ([key, value]) => (ws.clientState.settings[key] = value) - ); - ws.send(JSON.stringify(ws.clientState.settings)); - } else { - ws.send(JSON.stringify({type:"settings", settings: ws.clientState.settings})); - } - break; - case "system/stop": - ws.clientState.model.running = false; - ws.clientState.logs.push("[2024-02-05 19:10:00] [Warning] STOPPING SERVOS") - ws.send(JSON.stringify({type:"log", log:ws.clientState.logs.last()})); - break; - default: - ws.send(JSON.stringify({ error: "Unknown request type" })); - } + + handleJsonMessage(ws, message); }); ws.on("close", () => {