Merge pull request #4 from runeharlyk/new-api
Refactoring for controller
This commit is contained in:
+4
-1
@@ -5,7 +5,9 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"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",
|
"preview": "vite preview",
|
||||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||||
"format": "prettier --plugin-search-dir . --write ."
|
"format": "prettier --plugin-search-dir . --write ."
|
||||||
@@ -17,6 +19,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||||
"@typescript-eslint/parser": "^6.20.0",
|
"@typescript-eslint/parser": "^6.20.0",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"husky": "^9.0.7",
|
"husky": "^9.0.7",
|
||||||
"lint-staged": "^15.2.0",
|
"lint-staged": "^15.2.0",
|
||||||
"postcss": "^8.4.33",
|
"postcss": "^8.4.33",
|
||||||
|
|||||||
Generated
+11
@@ -43,6 +43,9 @@ devDependencies:
|
|||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.4.17
|
specifier: ^10.4.17
|
||||||
version: 10.4.17(postcss@8.4.33)
|
version: 10.4.17(postcss@8.4.33)
|
||||||
|
cross-env:
|
||||||
|
specifier: ^7.0.3
|
||||||
|
version: 7.0.3
|
||||||
husky:
|
husky:
|
||||||
specifier: ^9.0.7
|
specifier: ^9.0.7
|
||||||
version: 9.0.7
|
version: 9.0.7
|
||||||
@@ -989,6 +992,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/cross-env@7.0.3:
|
||||||
|
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
|
||||||
|
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
cross-spawn: 7.0.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
/cross-spawn@7.0.3:
|
/cross-spawn@7.0.3:
|
||||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|||||||
+13
-12
@@ -4,16 +4,19 @@
|
|||||||
import TopBar from './components/TopBar.svelte';
|
import TopBar from './components/TopBar.svelte';
|
||||||
import { connect } from './lib/socket';
|
import { connect } from './lib/socket';
|
||||||
import Controller from './routes/Controller.svelte';
|
import Controller from './routes/Controller.svelte';
|
||||||
import Config from './routes/Config.svelte';
|
|
||||||
import Health from './routes/SystemHealth.svelte';
|
|
||||||
import location from './lib/location';
|
|
||||||
import Sidebar from './components/Sidebar.svelte';
|
|
||||||
import FileCache from './lib/cache';
|
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';
|
||||||
|
|
||||||
export let url = window.location.pathname;
|
export let url = window.location.pathname
|
||||||
onMount(() => {
|
onMount(async () => {
|
||||||
connect(`ws://${location}`);
|
connect(socketLocation);
|
||||||
registerFetchIntercept()
|
registerFetchIntercept()
|
||||||
|
const [urdf, JOINT_NAME] = await loadModelAsync('/spot_micro.urdf.xacro')
|
||||||
|
jointNames.set(JOINT_NAME)
|
||||||
|
model.set(urdf)
|
||||||
});
|
});
|
||||||
|
|
||||||
const registerFetchIntercept = () => {
|
const registerFetchIntercept = () => {
|
||||||
@@ -21,9 +24,9 @@
|
|||||||
window.fetch = async (...args) => {
|
window.fetch = async (...args) => {
|
||||||
const [resource, config] = args;
|
const [resource, config] = args;
|
||||||
await FileCache.openDatabase();
|
await FileCache.openDatabase();
|
||||||
let file;
|
let file: BodyInit | Uint8Array | undefined | null;
|
||||||
try {
|
try {
|
||||||
file = await FileCache.getFile(resource.url);
|
file = await FileCache.getFile(resource.toString());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
@@ -37,10 +40,8 @@
|
|||||||
|
|
||||||
<Router {url}>
|
<Router {url}>
|
||||||
<TopBar />
|
<TopBar />
|
||||||
<Sidebar />
|
|
||||||
<div class="absolute w-full h-full bg-background text-on-background">
|
<div class="absolute w-full h-full bg-background text-on-background">
|
||||||
<Route path="/" component={Controller} />
|
<Route path="/" component={Controller} />
|
||||||
<Route path="/config" component={Config} />
|
<Route path="/settings/*page" component={Settings} />
|
||||||
<Route path="/health" component={Health} />
|
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<div class={`${$$props.class} p-4 m-2 rounded-md shadow-lg bg-surface`}>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<h2 class={`${$$props.class} text-lg border-b border-gray-600 text-on-background`}><slot/></h2>
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import nipplejs from 'nipplejs';
|
import nipplejs from 'nipplejs';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { throttler } from '../lib/throttle';
|
import { throttler } from '$lib/throttle';
|
||||||
import { socket } from '../lib/socket';
|
import { socket } from '$lib/socket';
|
||||||
import { emulateModel, input, outControllerData } from '../lib/store';
|
import { emulateModel, input, outControllerData } from '$lib/store';
|
||||||
|
|
||||||
let throttle = new throttler();
|
let throttle = new throttler();
|
||||||
let left: nipplejs.JoystickManager;
|
let left: nipplejs.JoystickManager;
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let value
|
|
||||||
export let placeholder = ""
|
|
||||||
export let required = false
|
|
||||||
export let disabled = false
|
|
||||||
</script>
|
|
||||||
<div class="{$$restProps.class || ''}">
|
|
||||||
<label for="first_name" class="block text-sm font-medium text-gray-900 dark:text-white"><slot/></label>
|
|
||||||
<input type="number" on:change placeholder={placeholder} disabled={disabled} bind:value={value} required={required} class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-1 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
|
||||||
</div>
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { CanvasTexture, CircleGeometry, Mesh, MeshBasicMaterial} from 'three';
|
|
||||||
import { dataBuffer, servoBuffer } from '../../lib/socket'
|
|
||||||
import { lerp } from '../../lib/utils';
|
|
||||||
import uzip from 'uzip';
|
|
||||||
import { outControllerData } from '../../lib/store';
|
|
||||||
import Kinematic, { ForwardKinematics } from '../../lib/kinematic';
|
|
||||||
import location from '../../lib/location';
|
|
||||||
import FileCache from '../../lib/cache';
|
|
||||||
import SceneBuilder from './sceneBuilder';
|
|
||||||
|
|
||||||
let sceneManager:SceneBuilder
|
|
||||||
let canvas: HTMLCanvasElement, streamCanvas: HTMLCanvasElement, stream: HTMLImageElement
|
|
||||||
let context: CanvasRenderingContext2D, texture: CanvasTexture
|
|
||||||
|
|
||||||
let modelAngles:number[] | Int16Array = new Array(12).fill(0)
|
|
||||||
let modelTargetAngles:number[] | Int16Array = new Array(12).fill(0)
|
|
||||||
|
|
||||||
let modelBodyAngles:EulerAngle = { omega: 0, phi: 0, psi: 0 }
|
|
||||||
let modelTargeBodyAngles:EulerAngle = { omega: 0, phi: 0, psi: 0 }
|
|
||||||
|
|
||||||
let modelBodyPoint:Point = {x: 0, y: 0, z: 0 }
|
|
||||||
let modelTargetBodyPoint:Point = {x: 0, y: 0, z: 0 }
|
|
||||||
|
|
||||||
const dir = [ -1, -1, -1, 1, -1, -1, -1, -1, -1, 1, -1, -1]
|
|
||||||
const videoStream = `//${location}/api/stream`;
|
|
||||||
|
|
||||||
let showModel = true, showStream = false
|
|
||||||
|
|
||||||
const servoNames = [
|
|
||||||
"front_left_shoulder", "front_left_leg", "front_left_foot",
|
|
||||||
"front_right_shoulder", "front_right_leg", "front_right_foot",
|
|
||||||
"rear_left_shoulder", "rear_left_leg", "rear_left_foot",
|
|
||||||
"rear_right_shoulder", "rear_right_leg", "rear_right_foot"
|
|
||||||
]
|
|
||||||
|
|
||||||
interface EulerAngle {
|
|
||||||
omega: number;
|
|
||||||
phi: number;
|
|
||||||
psi: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Point {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
z: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BodyState {
|
|
||||||
euler: EulerAngle;
|
|
||||||
position: Point;
|
|
||||||
legPositions:[number, number, number, number];
|
|
||||||
}
|
|
||||||
|
|
||||||
const radToDeg = (val:number) => val * (180 / Math.PI)
|
|
||||||
const degToRad = (val:number) => val * (Math.PI / 180)
|
|
||||||
|
|
||||||
const idle = () => {
|
|
||||||
const angles = new Array(12).fill(0)
|
|
||||||
servoBuffer.set(angles)
|
|
||||||
}
|
|
||||||
|
|
||||||
const rest = () => {
|
|
||||||
const angles = [0, 90, -180, 0, 90, -180, 0, 90, -180, 0, 90, -180, ]
|
|
||||||
servoBuffer.set(angles)
|
|
||||||
}
|
|
||||||
|
|
||||||
const stand = () => {
|
|
||||||
const angles = [0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90]
|
|
||||||
servoBuffer.set(angles)
|
|
||||||
}
|
|
||||||
|
|
||||||
const calculateKinematics = () => {
|
|
||||||
const kinematic = new Kinematic();
|
|
||||||
const angles: number[] = [degToRad(modelTargeBodyAngles.omega), degToRad(modelTargeBodyAngles.phi), degToRad(modelTargeBodyAngles.psi)];
|
|
||||||
const center: number[] = [modelTargetBodyPoint.x, modelTargetBodyPoint.y, modelTargetBodyPoint.z];
|
|
||||||
const Lp = [[100,-100,100,1],[100,-100,-100,1],[-100,-100,100,1],[-100,-100,-100,1]]
|
|
||||||
|
|
||||||
const legs = kinematic.calcIK(Lp, angles, center)
|
|
||||||
|
|
||||||
const legsAngles = legs
|
|
||||||
.map(x => x.map(y => radToDeg(y)))
|
|
||||||
.flat()
|
|
||||||
.map((x, i) => x * dir[i])
|
|
||||||
servoBuffer.set(legsAngles)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await cacheModelFiles()
|
|
||||||
createScene()
|
|
||||||
|
|
||||||
outControllerData.subscribe(data => {
|
|
||||||
modelTargeBodyAngles = {omega:0, phi:(data[1]-128) / 3, psi:(data[2]-128) / 4}
|
|
||||||
modelTargetBodyPoint = {x:(data[4]-128) / 2, y:data[5], z:(data[3]-128) / 2} // (data[5]-128) / 4
|
|
||||||
calculateKinematics()
|
|
||||||
})
|
|
||||||
|
|
||||||
servoBuffer.subscribe(angles => modelTargetAngles = angles)
|
|
||||||
|
|
||||||
modelTargeBodyAngles = {omega:0, phi:0, psi:0}
|
|
||||||
modelTargetBodyPoint = {x:0, y:0, z:0}
|
|
||||||
stand()
|
|
||||||
});
|
|
||||||
|
|
||||||
const cacheModelFiles = async () => {
|
|
||||||
let data = await fetch("/stl.zip").then(data => data.arrayBuffer())
|
|
||||||
|
|
||||||
var files = uzip.parse(data);
|
|
||||||
await FileCache.openDatabase()
|
|
||||||
|
|
||||||
for(const [path, data] of Object.entries(files) as [path:string, data:Uint8Array][]){
|
|
||||||
const url = new URL(path, window.location.href)
|
|
||||||
FileCache.saveFile(url.toString(), data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createScene = () => {
|
|
||||||
sceneManager = new SceneBuilder()
|
|
||||||
.addRenderer({ antialias: true, canvas: canvas, alpha: true})
|
|
||||||
.addPerspectiveCamera({x:-0.5, y:0.5, z:1})
|
|
||||||
.addOrbitControls(10, 30)
|
|
||||||
.addGroundPlane({x:0, y:-2, z:0})
|
|
||||||
.addGridHelper({size:250, divisions:125, y:-2})
|
|
||||||
.addAmbientLight({color:0xffffff, intensity:0.7})
|
|
||||||
.addDirectionalLight({x:10, y:100, z:10, color:0xffffff, intensity:1})
|
|
||||||
.addArrowHelper({origin:{x:0, y:0, z:0}, direction:{x:0, y:-2, z:0}})
|
|
||||||
.addFogExp2(0xcccccc, 0.015)
|
|
||||||
.loadModel('/spot_micro.urdf.xacro')
|
|
||||||
.addDragControl((name:string, angle:number) => modelTargetAngles[servoNames.indexOf(name)] = angle * (180/Math.PI))
|
|
||||||
.handleResize()
|
|
||||||
.addRenderCb(render)
|
|
||||||
.startRenderLoop()
|
|
||||||
|
|
||||||
addVideoStream()
|
|
||||||
}
|
|
||||||
|
|
||||||
const addVideoStream = () => {
|
|
||||||
context = streamCanvas.getContext("2d");
|
|
||||||
texture = new CanvasTexture( stream );
|
|
||||||
const liveStream = new Mesh( new CircleGeometry(35, 32), new MeshBasicMaterial({ map: texture }))
|
|
||||||
liveStream.position.z = -50
|
|
||||||
liveStream.visible = showStream
|
|
||||||
sceneManager.scene.add(liveStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleVideoStream = () => {
|
|
||||||
if(!showStream) return
|
|
||||||
context.drawImage(stream, 0, 0)
|
|
||||||
texture.needsUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const render = () => {
|
|
||||||
const robot = sceneManager.model
|
|
||||||
if(!robot) return
|
|
||||||
|
|
||||||
const forwardKinematics = new ForwardKinematics()
|
|
||||||
|
|
||||||
const points = forwardKinematics.calculateFootpoints(modelTargetAngles.map(ang => degToRad(ang)) as number[])
|
|
||||||
robot.position.y = Math.max(...points.map(coord => coord[0] / 100)) - 2.7
|
|
||||||
robot.rotation.z = lerp(robot.rotation.z, degToRad($dataBuffer[1] + 90), 0.1)
|
|
||||||
|
|
||||||
handleVideoStream()
|
|
||||||
|
|
||||||
for (let i = 0; i < servoNames.length; i++) {
|
|
||||||
modelAngles[i] = lerp(robot.joints[servoNames[i]].angle * (180/Math.PI), modelTargetAngles[i], 0.1)
|
|
||||||
robot.joints[servoNames[i]].setJointValue(degToRad(modelAngles[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
modelBodyAngles.omega = lerp(robot.rotation.x * (180/Math.PI), modelTargeBodyAngles.omega - 90, 0.1)
|
|
||||||
modelBodyAngles.phi = lerp(robot.rotation.y * (180/Math.PI), modelTargeBodyAngles.phi, 0.1)
|
|
||||||
modelBodyAngles.psi = lerp(robot.rotation.z * (180/Math.PI), modelTargeBodyAngles.psi + 90, 0.1)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:window on:resize={sceneManager.handleResize}></svelte:window>
|
|
||||||
|
|
||||||
<div class="absolute top-0 z-10 left-0 m-10">
|
|
||||||
<h1 class="text-on-background text-xl mb-2">Poses</h1>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<button class="outline outline-primary p-2 rounded-md" on:click={idle}>Idle</button>
|
|
||||||
<button class="outline outline-primary p-2 rounded-md" on:click={rest}>Rest</button>
|
|
||||||
<button class="outline outline-primary p-2 rounded-md" on:click={stand}>Stand</button>
|
|
||||||
</div>
|
|
||||||
<div class="w-full">
|
|
||||||
<h1 class="text-on-background text-xl mt-4">Motor angles</h1>
|
|
||||||
{#each Object.entries(sceneManager?.model?.joints ?? {}).filter(x => x[1].jointValue.length > 0) as [name, joint], i}
|
|
||||||
<div class="flex justify-between mb-2">
|
|
||||||
<span class="w-40">{name}: </span>
|
|
||||||
<input type="range" min="{radToDeg(joint.limit.lower)}" max="{radToDeg(joint.limit.upper)}" step="0.1" class="accent-primary" bind:value={$servoBuffer[i]}>
|
|
||||||
<input class="w-24 bg-background" min="{radToDeg(joint.limit.lower)}" max="{radToDeg(joint.limit.upper)}" step="0.1" bind:value={$servoBuffer[i]}>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1 class="text-on-background text-xl mb-2">Body rotation</h1>
|
|
||||||
{#each Object.keys(modelBodyAngles) as name}
|
|
||||||
<div class="flex justify-between mb-2">
|
|
||||||
<span class="w-40">{name}: </span>
|
|
||||||
<input type="range" min="-180" max="180" step="0.1" class="accent-primary" bind:value={modelTargeBodyAngles[name]} on:input={calculateKinematics}>
|
|
||||||
<input class="w-24 bg-background" min="-180" max="180" step="0.1" bind:value={modelTargeBodyAngles[name]} on:input={calculateKinematics}>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1 class="text-on-background text-xl mb-2">Body position</h1>
|
|
||||||
{#each Object.keys(modelBodyPoint) as name}
|
|
||||||
<div class="flex justify-between mb-2">
|
|
||||||
<span class="w-40">{name}: </span>
|
|
||||||
<input type="range" min="-180" max="180" step="0.1" class="accent-primary" bind:value={modelTargetBodyPoint[name]} on:input={calculateKinematics}>
|
|
||||||
<input class="w-24 bg-background" min="-180" max="180" step="0.1" bind:value={modelTargetBodyPoint[name]} on:input={calculateKinematics}>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if showStream}
|
|
||||||
<img
|
|
||||||
bind:this={stream}
|
|
||||||
src={videoStream}
|
|
||||||
class="hidden"
|
|
||||||
alt="Live stream is down"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
<canvas bind:this={streamCanvas} class="hidden"></canvas>
|
|
||||||
<canvas bind:this={canvas} class="absolute"></canvas>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Link } from 'svelte-routing';
|
|
||||||
import { Icon, XMark, ChartBar, DevicePhoneMobile, Cog6Tooth } from 'svelte-hero-icons';
|
|
||||||
import { sidebarOpen } from '../lib/store';
|
|
||||||
|
|
||||||
const closeMenu = () => {
|
|
||||||
sidebarOpen.set(false);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="drawer-example"
|
|
||||||
style="background-color:#36393f"
|
|
||||||
class="fixed top-0 left-0 z-40 h-screen p-4 overflow-y-auto transition-transform
|
|
||||||
{$sidebarOpen ? '' : '-translate-x-full'} bg-white w-40 dark:bg-gray-800"
|
|
||||||
tabindex="-1"
|
|
||||||
aria-labelledby="drawer-label"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
on:click={closeMenu}
|
|
||||||
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 absolute top-2.5 right-2.5 inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
|
||||||
>
|
|
||||||
<Icon src={XMark} size="32" />
|
|
||||||
</button>
|
|
||||||
<div class="h-full w-full flex flex-col justify-evenly items-center">
|
|
||||||
<Link to="/"><Icon src={DevicePhoneMobile} size="32" /></Link>
|
|
||||||
<Link to="/health"><Icon src={ChartBar} size="32" /></Link>
|
|
||||||
<Link to="/config"><Icon src={Cog6Tooth} size="32" /></Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,117 +1,76 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { isConnected, dataBuffer } from '../lib/socket';
|
import { isConnected, status, socket } from '$lib/socket';
|
||||||
import { Icon, ArrowsPointingIn, ArrowsPointingOut, Bars3, Power, Cube } from 'svelte-hero-icons';
|
import { Icon, Bars3, XMark, Power, Battery100, Signal, SignalSlash } from 'svelte-hero-icons';
|
||||||
import { tweened } from 'svelte/motion';
|
import { emulateModel } from '$lib/store';
|
||||||
import { quadInOut } from 'svelte/easing';
|
import { Link, useLocation } from 'svelte-routing'
|
||||||
import { emulateModel, sidebarOpen } from '../lib/store';
|
|
||||||
|
|
||||||
let isFullscreen = false;
|
const views = ["Virtual environment", "Robot camera"]
|
||||||
|
const modes = ["Drive", "Choreography"]
|
||||||
|
|
||||||
const width = tweened(0, {
|
const location = useLocation()
|
||||||
duration: 250,
|
|
||||||
easing: quadInOut
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleClick() {
|
let selected_view = views[0];
|
||||||
if ($width === 0) width.set(75);
|
let selected_modes = modes[0];
|
||||||
else width.set(0);
|
let settingOpen = window.location.pathname.includes('/settings')
|
||||||
|
|
||||||
|
$: emulateModel.set(selected_view === views[0])
|
||||||
|
$: settingOpen = $location.pathname.includes('/settings')
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
if ($isConnected) {
|
||||||
|
$socket.send(JSON.stringify({type:"system/stop"}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleFullScreen = () => {
|
|
||||||
if (!document.fullscreenElement) document.documentElement.requestFullscreen();
|
|
||||||
else if (document.exitFullscreen) document.exitFullscreen();
|
|
||||||
isFullscreen = !document.fullscreenElement;
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="absolute flex justify-between w-full z-10 h-10" on:dblclick={handleClick}>
|
|
||||||
<div class="absolute flex justify-between w-full">
|
<div class="topbar absolute left-0 top-0 w-full z-10 flex justify-between bg-zinc-800">
|
||||||
<div class="w-20 p-4 z-20">
|
<div class="flex gap-2 p-2">
|
||||||
<button on:click={() => sidebarOpen.set(true)}>
|
{#if settingOpen}
|
||||||
|
<Link to="/">
|
||||||
|
<Icon src={XMark} size="32" />
|
||||||
|
</Link>
|
||||||
|
{:else}
|
||||||
|
<Link to="/settings">
|
||||||
<Icon src={Bars3} size="32" />
|
<Icon src={Bars3} size="32" />
|
||||||
</button>
|
</Link>
|
||||||
</div>
|
|
||||||
<div class="w-20 p-4 text-right">{Math.floor($dataBuffer[0])}°🌡️<br>
|
|
||||||
{Math.floor($dataBuffer[9]*10)/10}V<br>
|
|
||||||
{Math.floor($dataBuffer[8]*1000)/1000}A
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="absolute flex justify-center w-full">
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
style="height:{$width}px; width:300px; background-color:#36393f"
|
|
||||||
class="rounded-b-xl overflow-hidden flex justify-end flex-col"
|
|
||||||
>
|
|
||||||
{#if $width !== 0}
|
|
||||||
<div class="flex justify-evenly p-4 w-full">
|
|
||||||
<button on:click={toggleFullScreen}>
|
|
||||||
<Icon src={isFullscreen ? ArrowsPointingIn : ArrowsPointingOut} size="32" />
|
|
||||||
</button>
|
|
||||||
<button>
|
|
||||||
<Icon src={Power} size="32" />
|
|
||||||
</button>
|
|
||||||
<button class:text-blue-600={$emulateModel} on:click={() => emulateModel.update(v => !v)}>
|
|
||||||
<Icon src={Cube} size="32"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
<select bind:value={selected_modes} class="rounded-md outline outline-2 text-zinc-200 outline-zinc-600 bg-zinc-800">
|
||||||
|
{#each modes as mode}
|
||||||
|
<option>{mode}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select bind:value={selected_view} class="rounded-md outline outline-2 text-zinc-200 outline-zinc-600 bg-zinc-800">
|
||||||
|
{#each views as view}
|
||||||
|
<option>{view}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center" on:mouseup={handleClick}>
|
|
||||||
<svg height="40" width="300" class="Settings_topSVG__2VXbU">
|
<div class="flex gap-2 p-2">
|
||||||
<path
|
<button class="action_button bg-zinc-600">
|
||||||
stroke="none"
|
<Icon src={Power} size="24" />
|
||||||
fill="#36393f"
|
</button>
|
||||||
d="M 0 0 C 40 0 40 40 80 40 H 220 C 260 40 260 0 300 0 Z"
|
<button class="action_button"><Icon src={Battery100} size="24" /></button>
|
||||||
/>
|
<button class="action_button"><Icon src={$isConnected ? Signal : SignalSlash} size="24" /></button>
|
||||||
</svg>
|
|
||||||
<div
|
|
||||||
class="absolute flex gap-1 h-10 w-36 justify-center items-center dots
|
|
||||||
{$isConnected ? 'connected' : 'disconnected'}"
|
|
||||||
>
|
|
||||||
<span class="dot h-4 w-4" />
|
|
||||||
<span class="dot h-4 w-4" />
|
|
||||||
<span class="dot h-4 w-4" />
|
|
||||||
<span class="dot h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="h-full w-20 bg-red-600 text-white" on:click={stop}>STOP</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
.dot {
|
.topbar {
|
||||||
background-color: grey;
|
height: 50px;
|
||||||
}
|
|
||||||
.disconnected .dot {
|
|
||||||
animation: _fade 0.5s 3s infinite alternate forwards;
|
|
||||||
}
|
|
||||||
.connected .dot:first-child {
|
|
||||||
background-color: #00bbe3;
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
.dots .dot:first-child {
|
|
||||||
animation-delay: 0.25s;
|
|
||||||
}
|
|
||||||
.dots .dot:nth-child(2) {
|
|
||||||
animation-delay: 0.5s;
|
|
||||||
}
|
|
||||||
.dots .dot:nth-child(3) {
|
|
||||||
animation-delay: 0.75s;
|
|
||||||
}
|
|
||||||
.dots .dot:last-child {
|
|
||||||
animation-delay: 1s;
|
|
||||||
}
|
|
||||||
.dots .dot:last-child {
|
|
||||||
animation-delay: 1s;
|
|
||||||
}
|
|
||||||
@keyframes _fade {
|
|
||||||
from {
|
|
||||||
background-color: #00bbe3;
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
background-color: grey;
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
}
|
||||||
|
.action_button {
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
outline: 1px solid #52525b;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
import { CanvasTexture, CircleGeometry, Mesh, MeshBasicMaterial} from 'three';
|
||||||
|
import {socket, angles, mpu } from '$lib/socket'
|
||||||
|
import { lerp } from '$lib/utils';
|
||||||
|
import uzip from 'uzip';
|
||||||
|
import { model, outControllerData } from '$lib/store';
|
||||||
|
import { ForwardKinematics } from '$lib/kinematic';
|
||||||
|
import location from '$lib/location';
|
||||||
|
import FileCache from '$lib/cache';
|
||||||
|
import SceneBuilder from '$lib/sceneBuilder';
|
||||||
|
|
||||||
|
let sceneManager:SceneBuilder
|
||||||
|
let canvas: HTMLCanvasElement, streamCanvas: HTMLCanvasElement, stream: HTMLImageElement
|
||||||
|
let context: CanvasRenderingContext2D, texture: CanvasTexture
|
||||||
|
|
||||||
|
let modelAngles:number[] | Int16Array = new Array(12).fill(0)
|
||||||
|
let modelTargetAngles:number[] | Int16Array = new Array(12).fill(0)
|
||||||
|
|
||||||
|
let modelBodyAngles:EulerAngle = { omega: 0, phi: 0, psi: 0 }
|
||||||
|
let modelTargeBodyAngles:EulerAngle = { omega: 0, phi: 0, psi: 0 }
|
||||||
|
|
||||||
|
const videoStream = `//${location}/api/stream`;
|
||||||
|
|
||||||
|
let showModel = true, showStream = false
|
||||||
|
|
||||||
|
const servoNames = [
|
||||||
|
"front_left_shoulder", "front_left_leg", "front_left_foot",
|
||||||
|
"front_right_shoulder", "front_right_leg", "front_right_foot",
|
||||||
|
"rear_left_shoulder", "rear_left_leg", "rear_left_foot",
|
||||||
|
"rear_right_shoulder", "rear_right_leg", "rear_right_foot"
|
||||||
|
]
|
||||||
|
|
||||||
|
interface EulerAngle {
|
||||||
|
omega: number;
|
||||||
|
phi: number;
|
||||||
|
psi: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const degToRad = (val:number) => val * (Math.PI / 180)
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await cacheModelFiles()
|
||||||
|
await createScene()
|
||||||
|
|
||||||
|
outControllerData.subscribe(data => {
|
||||||
|
$socket.send(JSON.stringify({
|
||||||
|
type: "kinematic/bodystate",
|
||||||
|
angles:[0, (data[1]-128)/3, (data[2]-128) / 4],
|
||||||
|
position:[(data[4]-128)/2, data[5], (data[3]-128)/2]}))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
canvas.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
const cacheModelFiles = async () => {
|
||||||
|
let data = await fetch("/stl.zip").then(data => data.arrayBuffer())
|
||||||
|
|
||||||
|
var files = uzip.parse(data);
|
||||||
|
await FileCache.openDatabase()
|
||||||
|
|
||||||
|
for(const [path, data] of Object.entries(files) as [path:string, data:Uint8Array][]){
|
||||||
|
const url = new URL(path, window.location.href)
|
||||||
|
FileCache.saveFile(url.toString(), data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateAngles = (name:string, angle:number) => {
|
||||||
|
modelTargetAngles[servoNames.indexOf(name)] = angle * (180/Math.PI)
|
||||||
|
$socket.send(JSON.stringify({type:"kinematic/angle", angle:angle * (180/Math.PI), id:servoNames.indexOf(name)}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const createScene = async () => {
|
||||||
|
sceneManager = new SceneBuilder()
|
||||||
|
.addRenderer({ antialias: true, canvas: canvas, alpha: true})
|
||||||
|
.addPerspectiveCamera({x:-0.5, y:0.5, z:1})
|
||||||
|
.addOrbitControls(10, 30)
|
||||||
|
.addSky()
|
||||||
|
.addGroundPlane({x:0, y:-2, z:0})
|
||||||
|
.addGridHelper({size:250, divisions:125, y:-2})
|
||||||
|
.addAmbientLight({color:0xffffff, intensity:0.7})
|
||||||
|
.addDirectionalLight({x:10, y:100, z:10, color:0xffffff, intensity:1})
|
||||||
|
.addArrowHelper({origin:{x:0, y:0, z:0}, direction:{x:0, y:-2, z:0}})
|
||||||
|
.addFogExp2(0xcccccc, 0.015)
|
||||||
|
.addModel($model)
|
||||||
|
.addDragControl(updateAngles)
|
||||||
|
.handleResize()
|
||||||
|
.addRenderCb(render)
|
||||||
|
.startRenderLoop()
|
||||||
|
|
||||||
|
addVideoStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
const addVideoStream = () => {
|
||||||
|
context = streamCanvas.getContext("2d");
|
||||||
|
texture = new CanvasTexture( stream );
|
||||||
|
const liveStream = new Mesh( new CircleGeometry(35, 32), new MeshBasicMaterial({ map: texture }))
|
||||||
|
liveStream.position.z = -50
|
||||||
|
liveStream.visible = showStream
|
||||||
|
sceneManager.scene.add(liveStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleVideoStream = () => {
|
||||||
|
if(!showStream) return
|
||||||
|
context.drawImage(stream, 0, 0)
|
||||||
|
texture.needsUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
const robot = sceneManager.model
|
||||||
|
if(!robot) return
|
||||||
|
|
||||||
|
const forwardKinematics = new ForwardKinematics()
|
||||||
|
|
||||||
|
const points = forwardKinematics.calculateFootpoints(modelAngles.map(ang => degToRad(ang)) as number[])
|
||||||
|
robot.position.y = Math.max(...points.map(coord => coord[0] / 100)) - 2.7
|
||||||
|
robot.rotation.z = lerp(robot.rotation.z, degToRad($mpu.heading + 90), 0.1)
|
||||||
|
modelTargetAngles = $angles
|
||||||
|
|
||||||
|
handleVideoStream()
|
||||||
|
|
||||||
|
for (let i = 0; i < servoNames.length; i++) {
|
||||||
|
modelAngles[i] = lerp(robot.joints[servoNames[i]].angle * (180/Math.PI), modelTargetAngles[i], 0.1)
|
||||||
|
robot.joints[servoNames[i]].setJointValue(degToRad(modelAngles[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
modelBodyAngles.omega = lerp(robot.rotation.x * (180/Math.PI), modelTargeBodyAngles.omega - 90, 0.1)
|
||||||
|
modelBodyAngles.phi = lerp(robot.rotation.y * (180/Math.PI), modelTargeBodyAngles.phi, 0.1)
|
||||||
|
modelBodyAngles.psi = lerp(robot.rotation.z * (180/Math.PI), modelTargeBodyAngles.psi + 90, 0.1)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:resize={sceneManager.handleResize}></svelte:window>
|
||||||
|
|
||||||
|
{#if showStream}
|
||||||
|
<img
|
||||||
|
bind:this={stream}
|
||||||
|
src={videoStream}
|
||||||
|
class="hidden"
|
||||||
|
alt="Live stream is down"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<canvas bind:this={streamCanvas} class="hidden"></canvas>
|
||||||
|
<canvas bind:this={canvas} class="absolute"></canvas>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import location from '../lib/location';
|
import location from '$lib/location';
|
||||||
|
|
||||||
let videoStream = `//${location}/api/stream`;
|
let videoStream = `//${location}/api/stream`;
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { default as Card } from "./Card.svelte";
|
|
||||||
export { default as CardHeader } from "./CardHeader.svelte";
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { jointNames } from '../../lib/store';
|
||||||
|
|
||||||
|
type Servo = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
minPWM: number;
|
||||||
|
maxPWM: number;
|
||||||
|
pwmFor180: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
let servos: any[] = [];
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
jointNames.subscribe(data => {
|
||||||
|
servos = data.map((name:string, i:number) => {
|
||||||
|
return {
|
||||||
|
id: i,
|
||||||
|
name,
|
||||||
|
minPWM: 0,
|
||||||
|
maxPWM: 0,
|
||||||
|
pwmFor180: 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
let selectedServo: number | null = null;
|
||||||
|
|
||||||
|
function updateServoValue(index: number, field: keyof Servo, value: number): void {
|
||||||
|
servos[index] = { ...servos[index], [field]: value };
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatServo = (servo:Servo) => {
|
||||||
|
const string = servo.name
|
||||||
|
const name = string.charAt(0).toUpperCase() + string.split('_').join(' ').slice(1);
|
||||||
|
return `${servo.id} ${name}`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<div>
|
||||||
|
<div class="servo-selector">
|
||||||
|
<label for="servo-select">Select Servo:</label>
|
||||||
|
<select id="servo-select" class="bg-zinc-800" bind:value={selectedServo}>
|
||||||
|
{#each servos as servo}
|
||||||
|
<option value={servo.id}>{formatServo(servo)}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if selectedServo !== null}
|
||||||
|
<div class="mt-5">
|
||||||
|
<h2>Servo {formatServo(servos[selectedServo])} Calibration</h2>
|
||||||
|
<label for="minPWM">Min PWM:</label>
|
||||||
|
<input type="number" id="minPWM" class="bg-zinc-800"
|
||||||
|
value={servos[selectedServo].minPWM}
|
||||||
|
on:blur={(event) => updateServoValue(selectedServo, 'minPWM', Number(event.target.value))} />
|
||||||
|
|
||||||
|
<label for="maxPWM">Max PWM:</label>
|
||||||
|
<input type="number" id="maxPWM" class="bg-zinc-800"
|
||||||
|
value={servos[selectedServo].maxPWM}
|
||||||
|
on:blur={(event) => updateServoValue(selectedServo, 'maxPWM', Number(event.target.value))} />
|
||||||
|
|
||||||
|
<label for="pwmFor180">PWM for 180°:</label>
|
||||||
|
<input type="number" id="pwmFor180" class="bg-zinc-800"
|
||||||
|
value={servos[selectedServo].pwmFor180}
|
||||||
|
on:blur={(event) => updateServoValue(selectedServo, 'pwmFor180', Number(event.target.value))} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { socket, isConnected, settings } from "../../lib/socket";
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if ($isConnected) {
|
||||||
|
const message = JSON.stringify({type: 'system/settings'})
|
||||||
|
$socket.send(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<div>
|
||||||
|
{#each Object.entries($settings) as entry}
|
||||||
|
<div class="flex gap-8">
|
||||||
|
<div class="w-32">{entry[0]}:</div>
|
||||||
|
<div>{entry[1]}</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { socket, isConnected, systemInfo } from "../../lib/socket";
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { humanFileSize } from "../../lib/utils";
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if ($isConnected) {
|
||||||
|
const message = JSON.stringify({type: 'system/info'})
|
||||||
|
$socket.send(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<div class="w-1/3">
|
||||||
|
{#each Object.entries($systemInfo ?? {}) as entry}
|
||||||
|
<div class="flex gap-8">
|
||||||
|
<div class="w-32">{entry[0]}:</div>
|
||||||
|
{#if entry[0].includes("Size") || entry[0].includes("Free") || entry[0].includes("Min")}
|
||||||
|
<div>{humanFileSize(entry[1])}</div>
|
||||||
|
{:else}
|
||||||
|
<div>{entry[1]}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { socket, isConnected, log } from "../../lib/socket";
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if ($isConnected) {
|
||||||
|
const message = JSON.stringify({type: 'system/logs'})
|
||||||
|
$socket.send(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full h-full">
|
||||||
|
{#each $log as entry}
|
||||||
|
<div>{entry}</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
const location = import.meta.env.DEV ? "leika.local" : window.location.host
|
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;
|
export default location;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -17,11 +17,14 @@ import { Mesh,
|
|||||||
type ColorRepresentation,
|
type ColorRepresentation,
|
||||||
type WebGLRendererParameters,
|
type WebGLRendererParameters,
|
||||||
MeshPhongMaterial,
|
MeshPhongMaterial,
|
||||||
|
EquirectangularReflectionMapping,
|
||||||
|
ACESFilmicToneMapping,
|
||||||
|
MathUtils,
|
||||||
} from "three";
|
} from "three";
|
||||||
|
import { Sky } from 'three/addons/objects/Sky.js';
|
||||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
|
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
|
||||||
import URDFLoader, { type URDFMimicJoint } from "urdf-loader";
|
import { type URDFMimicJoint } from "urdf-loader";
|
||||||
import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls'
|
import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls'
|
||||||
import { XacroLoader } from "xacro-parser";
|
|
||||||
|
|
||||||
export const addScene = () => new Scene()
|
export const addScene = () => new Scene()
|
||||||
|
|
||||||
@@ -52,6 +55,13 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
export default class SceneBuilder {
|
export default class SceneBuilder {
|
||||||
public scene: Scene
|
public scene: Scene
|
||||||
public camera: PerspectiveCamera
|
public camera: PerspectiveCamera
|
||||||
@@ -68,6 +78,9 @@ export default class SceneBuilder {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.scene = new Scene()
|
this.scene = new Scene()
|
||||||
|
if (this.scene.environment?.mapping) {
|
||||||
|
this.scene.environment.mapping = EquirectangularReflectionMapping;
|
||||||
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,10 +89,40 @@ export default class SceneBuilder {
|
|||||||
this.renderer.outputColorSpace = "srgb";
|
this.renderer.outputColorSpace = "srgb";
|
||||||
this.renderer.shadowMap.enabled = true;
|
this.renderer.shadowMap.enabled = true;
|
||||||
this.renderer.shadowMap.type = PCFSoftShadowMap;
|
this.renderer.shadowMap.type = PCFSoftShadowMap;
|
||||||
|
this.renderer.toneMapping = ACESFilmicToneMapping;
|
||||||
|
this.renderer.toneMappingExposure = 0.85;
|
||||||
document.body.appendChild(this.renderer.domElement);
|
document.body.appendChild(this.renderer.domElement);
|
||||||
return this
|
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();
|
||||||
|
|
||||||
|
sun.setFromSphericalCoords( 1, phi, theta );
|
||||||
|
uniforms[ 'sunPosition' ].value.copy( sun );
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
public addPerspectiveCamera = (options:position) => {
|
public addPerspectiveCamera = (options:position) => {
|
||||||
this.camera = new PerspectiveCamera();
|
this.camera = new PerspectiveCamera();
|
||||||
this.camera.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
|
this.camera.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
|
||||||
@@ -126,6 +169,9 @@ export default class SceneBuilder {
|
|||||||
public addGridHelper = (options:gridHelperOptions) => {
|
public addGridHelper = (options:gridHelperOptions) => {
|
||||||
this.gridHelper = new GridHelper(options.size, options.divisions);
|
this.gridHelper = new GridHelper(options.size, options.divisions);
|
||||||
this.gridHelper.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
|
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);
|
this.scene.add(this.gridHelper);
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
@@ -174,7 +220,7 @@ export default class SceneBuilder {
|
|||||||
|
|
||||||
isJoint = j => j.isURDFJoint && j.jointType !== 'fixed';
|
isJoint = j => j.isURDFJoint && j.jointType !== 'fixed';
|
||||||
|
|
||||||
highlightLinkGeometry = (m, revert:boolean, material) => {
|
highlightLinkGeometry = (m: URDFMimicJoint, revert:boolean, material: MeshPhongMaterial) => {
|
||||||
const traverse = c => {
|
const traverse = c => {
|
||||||
if (c.type === 'Mesh') {
|
if (c.type === 'Mesh') {
|
||||||
if (revert) {
|
if (revert) {
|
||||||
@@ -198,26 +244,13 @@ export default class SceneBuilder {
|
|||||||
traverse(m);
|
traverse(m);
|
||||||
};
|
};
|
||||||
|
|
||||||
public loadModel = (urlXacro:string) => {
|
public addModel = (model: any) => {
|
||||||
const xacroLoader = new XacroLoader();
|
this.model = model
|
||||||
xacroLoader.load(urlXacro, xml => {
|
this.scene.add(model)
|
||||||
const urdfLoader = new URDFLoader();
|
|
||||||
urdfLoader.workingPath = LoaderUtils.extractUrlBase(urlXacro);
|
|
||||||
|
|
||||||
this.model = urdfLoader.parse(xml);
|
|
||||||
this.model.rotation.x = -Math.PI / 2;
|
|
||||||
this.model.rotation.z = Math.PI / 2;
|
|
||||||
this.model.traverse(c => c.castShadow = true);
|
|
||||||
this.model.updateMatrixWorld(true);
|
|
||||||
this.model.scale.setScalar(10);
|
|
||||||
|
|
||||||
this.scene.add(this.model);
|
|
||||||
|
|
||||||
}, (error) => console.log(error));
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
public addDragControl = (updateAngle) => {
|
public addDragControl = (updateAngle:any) => {
|
||||||
const highlightColor = '#FFFFFF'
|
const highlightColor = '#FFFFFF'
|
||||||
const highlightMaterial =
|
const highlightMaterial =
|
||||||
new MeshPhongMaterial({
|
new MeshPhongMaterial({
|
||||||
@@ -232,18 +265,14 @@ export default class SceneBuilder {
|
|||||||
this.setJointValue(joint.name, angle);
|
this.setJointValue(joint.name, angle);
|
||||||
updateAngle(joint.name, angle)
|
updateAngle(joint.name, angle)
|
||||||
};
|
};
|
||||||
dragControls.onDragStart = () => {
|
dragControls.onDragStart = () => this.controls.enabled = false;
|
||||||
this.controls.enabled = false;
|
dragControls.onDragEnd = () => this.controls.enabled = true;
|
||||||
};
|
dragControls.onHover = (joint:URDFMimicJoint) => this.highlightLinkGeometry(joint, false, highlightMaterial);
|
||||||
dragControls.onDragEnd = () => {
|
dragControls.onUnhover = (joint:URDFMimicJoint) => this.highlightLinkGeometry(joint, true, highlightMaterial);
|
||||||
this.controls.enabled = true;
|
|
||||||
};
|
this.renderer.domElement.addEventListener('touchstart', (data) => dragControls._mouseDown(data.touches[0]));
|
||||||
dragControls.onHover = (joint:URDFMimicJoint) => {
|
this.renderer.domElement.addEventListener('touchmove', (data) => dragControls._mouseMove(data.touches[0]))
|
||||||
this.highlightLinkGeometry(joint, false, highlightMaterial);
|
this.renderer.domElement.addEventListener('touchup', (data) => dragControls._mouseUp(data.touches[0]));
|
||||||
}
|
|
||||||
dragControls.onUnhover = (joint:URDFMimicJoint) => {
|
|
||||||
this.highlightLinkGeometry(joint, true, highlightMaterial);
|
|
||||||
}
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
+43
-3
@@ -4,6 +4,14 @@ export type WebSocketStatus = 'OPEN' | 'CONNECTING' | 'CLOSED'
|
|||||||
|
|
||||||
export const isConnected = writable(false)
|
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 dataBuffer = writable(new Float32Array(13))
|
||||||
|
|
||||||
export const servoBuffer:Writable<Int16Array|number[]> = writable(new Int16Array(12))
|
export const servoBuffer:Writable<Int16Array|number[]> = writable(new Int16Array(12))
|
||||||
@@ -12,7 +20,7 @@ export const data = writable();
|
|||||||
|
|
||||||
export const status:Writable<WebSocketStatus> = writable('CLOSED')
|
export const status:Writable<WebSocketStatus> = writable('CLOSED')
|
||||||
|
|
||||||
export const socket:Writable<WebSocket> = writable(null)
|
export const socket:Writable<WebSocket> = writable()
|
||||||
|
|
||||||
export const connect = (url:string) => {
|
export const connect = (url:string) => {
|
||||||
status.set('CONNECTING')
|
status.set('CONNECTING')
|
||||||
@@ -42,11 +50,43 @@ const _disconnected = () => {
|
|||||||
isConnected.set(false)
|
isConnected.set(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const _message = (event) => {
|
const _message = (event:any) => {
|
||||||
if (event.data instanceof ArrayBuffer) {
|
if (event.data instanceof ArrayBuffer) {
|
||||||
let buffer = new Int8Array(event.data);
|
let buffer = new Int8Array(event.data);
|
||||||
if(buffer.length === 44) {
|
if(buffer.length === 44) {
|
||||||
dataBuffer.set(new Float32Array(buffer.buffer) )
|
dataBuffer.set(new Float32Array(buffer.buffer) )
|
||||||
}
|
}
|
||||||
} else dataBuffer.set(JSON.parse(event.data));
|
} 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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
import { persistentStore } from './utils';
|
||||||
export const sidebarOpen = writable(false);
|
|
||||||
|
|
||||||
export const emulateModel = writable(true);
|
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(6));
|
export const outControllerData = writable(new Uint8Array([0, 128, 128, 128, 128, 70, 0]));
|
||||||
|
|
||||||
|
export const jointNames = persistentStore("joint_names", [])
|
||||||
|
|
||||||
|
export const model = writable()
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
export const humanFileSize = (size:number):string => {
|
export const humanFileSize = (size:number):string => {
|
||||||
var i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
|
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];
|
return Number((size / Math.pow(1024, i)).toFixed(2)) * 1 + ['B', 'kB', 'MB', 'GB', 'TB'][i];
|
||||||
@@ -6,3 +8,15 @@ export const humanFileSize = (size:number):string => {
|
|||||||
export const lerp = (start: number, end: number, amt: number) => {
|
export const lerp = (start: number, end: number, amt: number) => {
|
||||||
return (1 - amt) * start + amt * end;
|
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;
|
||||||
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Card, CardHeader } from "../components/index";
|
|
||||||
import { socket, isConnected } from "../lib/socket";
|
|
||||||
import { throttler } from "../lib/throttle";
|
|
||||||
|
|
||||||
let throttle = new throttler();
|
|
||||||
let throttle_timing = 25;
|
|
||||||
let pwm = 300
|
|
||||||
let servo = 0
|
|
||||||
|
|
||||||
let min = 0;
|
|
||||||
let halfWay = 0;
|
|
||||||
let max = 0;
|
|
||||||
let overallRange = 0;
|
|
||||||
|
|
||||||
$: buffer = (overallRange - 180) / 2
|
|
||||||
$: conversionRate = (max - min) / overallRange;
|
|
||||||
$: zeroMark = (conversionRate * buffer) + min;
|
|
||||||
$: oneEightyMark = (conversionRate * buffer) + halfWay;
|
|
||||||
|
|
||||||
$: pwm, throttle.throttle(() => $isConnected ? $socket.send(JSON.stringify({servo, pwm, action:0})) : console.log("Is not connected yet"), throttle_timing);
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="w-full h-full absolute top-0 p-4 flex justify-center items-center">
|
|
||||||
<Card class="w-full h-1/2">
|
|
||||||
<CardHeader>Servo calibration</CardHeader>
|
|
||||||
<div>Servo</div>
|
|
||||||
<input type="number" class="bg-background" bind:value={servo}>
|
|
||||||
<div class="flex w-full">
|
|
||||||
<label for="pwm">Pwm ({pwm})</label>
|
|
||||||
<div class="flex w-full">
|
|
||||||
<button class="p-2" on:click={() => pwm--}>-</button>
|
|
||||||
<input class="flex-1" bind:value={pwm} type="range" id="pwm" min={0} max={700}>
|
|
||||||
<button class="p-2" on:click={() => pwm++}>+</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 w-1/2">
|
|
||||||
<div>Min pwm</div>
|
|
||||||
<input type="number" class="bg-background" bind:value={min}>
|
|
||||||
<div>Pwm at Halfway from min</div> <input type="number" class="bg-background" bind:value={halfWay}>
|
|
||||||
<div>Max pwm</div> <input type="number" class="bg-background" bind:value={max}>
|
|
||||||
<div>Overall angle</div> <input type="number" class="bg-background" bind:value={overallRange}>
|
|
||||||
<div>Buffer</div><div>{buffer}</div>
|
|
||||||
<div>Conversion rate</div><div>{conversionRate}</div>
|
|
||||||
<div>pwm for 0° mark</div><div>{zeroMark}</div>
|
|
||||||
<div>pwm for 180° Mark</div><div>{oneEightyMark}</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Stream from '../Views/Stream.svelte';
|
import Stream from '$components/Views/Stream.svelte';
|
||||||
import Controls from '../components/Controls.svelte';
|
import Model from '$components/Views/Model.svelte';
|
||||||
import ModelView from '../components/Model/ModelView.svelte';
|
import Controls from '$components/Controls.svelte';
|
||||||
import { emulateModel } from '../lib/store';
|
import { emulateModel } from '$lib/store';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex justify-center items-center w-full h-full">
|
<div class="flex justify-center items-center w-full h-full">
|
||||||
{#if $emulateModel}
|
{#if $emulateModel}
|
||||||
<ModelView />
|
<Model />
|
||||||
{:else}
|
{:else}
|
||||||
<Stream />
|
<Stream />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Link, Route, Router } from 'svelte-routing';
|
||||||
|
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 Calibration from '../components/settings/Calibration.svelte';
|
||||||
|
|
||||||
|
export const page = ""
|
||||||
|
|
||||||
|
const menu = [
|
||||||
|
{
|
||||||
|
title: 'Calibration',
|
||||||
|
path: '/calibration',
|
||||||
|
icon: AdjustmentsVertical,
|
||||||
|
component: Calibration
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'System info',
|
||||||
|
path: '/info',
|
||||||
|
icon: InformationCircle,
|
||||||
|
component: Info
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Log',
|
||||||
|
path: '/log',
|
||||||
|
icon: BookOpen,
|
||||||
|
component: Log
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Settings',
|
||||||
|
path: '/settings',
|
||||||
|
icon: Cog6Tooth,
|
||||||
|
component: Configuration
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="pt-14 flex h-full">
|
||||||
|
<nav class="w-1/6 flex flex-col">
|
||||||
|
{#each menu as link}
|
||||||
|
<Link to={'/settings'+link.path}>
|
||||||
|
<div class="px-4 py-2 flex gap-2 items-center">
|
||||||
|
<Icon src={link.icon} size="24" />{link.title}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
{/each}
|
||||||
|
</nav>
|
||||||
|
<main class="w-full h-full">
|
||||||
|
<Router>
|
||||||
|
{#each menu as link}
|
||||||
|
<Route path={link.path} component={link.component}></Route>
|
||||||
|
{/each}
|
||||||
|
</Router>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { onDestroy, onMount } from 'svelte';
|
|
||||||
import { dataBuffer, socket } from '../lib/socket';
|
|
||||||
import { humanFileSize } from '../lib/utils';
|
|
||||||
|
|
||||||
let buf = new Uint8Array(2);
|
|
||||||
buf[0] = 1;
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
buf[1] = 1;
|
|
||||||
$socket.send(buf);
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
buf[1] = 0;
|
|
||||||
$socket.send(buf);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="p-10 flex gap-4">
|
|
||||||
<div class="bg-slate-600 rounded-md p-2 drop-shadow-lg">
|
|
||||||
<b>Heap allocation:</b>
|
|
||||||
<div class="flex gap-2"><span>Total free:</span>{humanFileSize($dataBuffer[8])}</div>
|
|
||||||
<div class="flex gap-2"><span>Max free block:</span>{humanFileSize($dataBuffer[12])}</div>
|
|
||||||
<div class="flex gap-2"><span>Min:</span>{humanFileSize($dataBuffer[10])}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-slate-600 rounded-md p-2 drop-shadow-lg">
|
|
||||||
<b>PSRam allocation:</b>
|
|
||||||
<div class="flex gap-2"><span>Free</span>{humanFileSize($dataBuffer[9])}</div>
|
|
||||||
<div class="flex gap-2"><span>Min:</span>{humanFileSize($dataBuffer[11])}</div>
|
|
||||||
<div class="flex gap-2"><span>Max block:</span>{humanFileSize($dataBuffer[13])}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
+8
-1
@@ -14,7 +14,14 @@
|
|||||||
*/
|
*/
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"isolatedModules": true
|
"isolatedModules": true,
|
||||||
|
"paths": {
|
||||||
|
"$lib/*": ["./src/lib/*"],
|
||||||
|
"$utils/*": ["./src/utils/*"],
|
||||||
|
"$components/*": ["./src/components/*"],
|
||||||
|
"$stores/*": ["./src/stores/*"]
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
|||||||
+15
-3
@@ -2,12 +2,24 @@ import { defineConfig } from 'vite';
|
|||||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||||
import { viteSingleFile } from 'vite-plugin-singlefile';
|
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||||
import viteCompression from 'vite-plugin-compression';
|
import viteCompression from 'vite-plugin-compression';
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const forEmbedded = process.env.FOR_EMBEDDED == 'true'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [svelte(), viteSingleFile(), viteCompression({deleteOriginFile: true})],
|
plugins: [svelte(),
|
||||||
|
...(forEmbedded ? [ viteSingleFile(), viteCompression({deleteOriginFile: true})]: [])],
|
||||||
build: {
|
build: {
|
||||||
outDir: '../data',
|
outDir: forEmbedded ? '../data': './build',
|
||||||
emptyOutDir: true
|
emptyOutDir: true
|
||||||
}
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'$lib': path.resolve('./src/lib/'),
|
||||||
|
'$components': path.resolve('./src/components'),
|
||||||
|
'$utils': path.resolve('./src/utils'),
|
||||||
|
'$stores': path.resolve('./src/stores'),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
# API
|
||||||
|
|
||||||
|
https://dev.bostondynamics.com/docs/concepts/choreography/choreography_in_tablet.html
|
||||||
|
|
||||||
|
| HTTP Method | Endpoint | Description | Parameters |
|
||||||
|
|-------------|----------------|----------------------------|---------------------------|
|
||||||
|
| GET | /api/sensor/battery | Retrieve the battery state | |
|
||||||
|
| GET | /api/sensor/mpu | Retrieve the mpu state | |
|
||||||
|
| GET | /api/sensor/magnetometer | Retrieve the magnetometer state | |
|
||||||
|
| GET | /api/sensor/distances | Retrieve the distances state | |
|
||||||
|
| GET | /api/sensor/distance/{position} | Retrieve the distance state | `position`: The position of the distance sensor **LEFT** and **RIGHT** |
|
||||||
|
| GET | /api/sensor/stream | Retrieve the camera stream | |
|
||||||
|
| GET | /api/actuator | Retrieve the actuator states | |
|
||||||
|
| GET | /api/actuator/{id} | Retrieve the actuator state for `id` | `id`: The ID of the actuator |
|
||||||
|
| POST | /api/actuator/{id} | Set the actuator state | `id`: The ID of the actuator|
|
||||||
|
| GET | /api/kinematics/feet | Retrieve the current feet positions as (x, y, z) coordinates| |
|
||||||
|
| GET | /api/kinematics/body | Retrieve the current body position as a (x, y, z) coordinates| |
|
||||||
|
| GET | /api/kinematics/bodystate | Retrieve the current body and feet positions | |
|
||||||
|
| GET | /api/system/log | Retrieve the system log | |
|
||||||
|
| GET | /api/system/info | Retrieve the system information | |
|
||||||
|
| GET | /api/system/settings | Retrieve the system settings | |
|
||||||
|
| POST | /api/system/settings | Set the system settings | |
|
||||||
|
| POST | /api/system/reset | Reset system | |
|
||||||
|
| POST | /api/system/power/off | Power of the system | |
|
||||||
|
| POST | /api/system/stop | Stop power to actuators | `id`: The stop level **CUT**, **SETTLE_THEN_CUT**, **NONE** |
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# ABOUT SPOT MICRO
|
||||||
|
|
||||||
|
## Cameras
|
||||||
|
|
||||||
|
## Hips and joints
|
||||||
|
|
||||||
|
## Robot specifications
|
||||||
|
|
||||||
|
### Dimensions
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
| Specification | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| Ingress protection | *IP42 |
|
||||||
|
| Operating temperature | 0C to 30C |
|
||||||
|
|
||||||
|
### Power
|
||||||
|
|
||||||
|
### Sensing
|
||||||
|
|
||||||
|
| Specification | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| Camera type | single |
|
||||||
|
| Field of view | 160 degrees |
|
||||||
|
|
||||||
|
### Connectivity
|
||||||
|
|
||||||
|
| Specification | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| Wifi | 802.11 |
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/node_modules
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
export default class Kinematic {
|
||||||
|
l1;
|
||||||
|
l2;
|
||||||
|
l3;
|
||||||
|
l4;
|
||||||
|
|
||||||
|
L;
|
||||||
|
W;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.l1 = 50;
|
||||||
|
this.l2 = 20;
|
||||||
|
this.l3 = 120;
|
||||||
|
this.l4 = 155;
|
||||||
|
|
||||||
|
this.L = 140;
|
||||||
|
this.W = 75;
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyIK(omega, phi, psi, xm, ym, zm) {
|
||||||
|
const { cos, sin } = Math;
|
||||||
|
|
||||||
|
const Rx = [
|
||||||
|
[1, 0, 0, 0],
|
||||||
|
[0, cos(omega), -sin(omega), 0],
|
||||||
|
[0, sin(omega), cos(omega), 0],
|
||||||
|
[0, 0, 0, 1],
|
||||||
|
];
|
||||||
|
const Ry = [
|
||||||
|
[cos(phi), 0, sin(phi), 0],
|
||||||
|
[0, 1, 0, 0],
|
||||||
|
[-sin(phi), 0, cos(phi), 0],
|
||||||
|
[0, 0, 0, 1],
|
||||||
|
];
|
||||||
|
const Rz = [
|
||||||
|
[cos(psi), -sin(psi), 0, 0],
|
||||||
|
[sin(psi), cos(psi), 0, 0],
|
||||||
|
[0, 0, 1, 0],
|
||||||
|
[0, 0, 0, 1],
|
||||||
|
];
|
||||||
|
const Rxyz = this.matrixMultiply(this.matrixMultiply(Rx, Ry), Rz);
|
||||||
|
|
||||||
|
const T = [
|
||||||
|
[0, 0, 0, xm],
|
||||||
|
[0, 0, 0, ym],
|
||||||
|
[0, 0, 0, zm],
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
];
|
||||||
|
const Tm = this.matrixAdd(T, Rxyz);
|
||||||
|
|
||||||
|
const sHp = sin(Math.PI / 2);
|
||||||
|
const cHp = cos(Math.PI / 2);
|
||||||
|
|
||||||
|
return [
|
||||||
|
this.matrixMultiply(Tm, [
|
||||||
|
[cHp, 0, sHp, this.L / 2],
|
||||||
|
[0, 1, 0, 0],
|
||||||
|
[-sHp, 0, cHp, this.W / 2],
|
||||||
|
[0, 0, 0, 1],
|
||||||
|
]),
|
||||||
|
this.matrixMultiply(Tm, [
|
||||||
|
[cHp, 0, sHp, this.L / 2],
|
||||||
|
[0, 1, 0, 0],
|
||||||
|
[-sHp, 0, cHp, -this.W / 2],
|
||||||
|
[0, 0, 0, 1],
|
||||||
|
]),
|
||||||
|
this.matrixMultiply(Tm, [
|
||||||
|
[cHp, 0, sHp, -this.L / 2],
|
||||||
|
[0, 1, 0, 0],
|
||||||
|
[-sHp, 0, cHp, this.W / 2],
|
||||||
|
[0, 0, 0, 1],
|
||||||
|
]),
|
||||||
|
this.matrixMultiply(Tm, [
|
||||||
|
[cHp, 0, sHp, -this.L / 2],
|
||||||
|
[0, 1, 0, 0],
|
||||||
|
[-sHp, 0, cHp, -this.W / 2],
|
||||||
|
[0, 0, 0, 1],
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
legIK(point) {
|
||||||
|
const [x, y, z] = point;
|
||||||
|
const { atan2, cos, sin, sqrt, acos } = Math;
|
||||||
|
|
||||||
|
let F;
|
||||||
|
|
||||||
|
try {
|
||||||
|
F = sqrt(x ** 2 + y ** 2 - this.l1 ** 2);
|
||||||
|
if (isNaN(F)) throw new Error("F is NaN");
|
||||||
|
} catch (error) {
|
||||||
|
F = this.l1;
|
||||||
|
}
|
||||||
|
const G = F - this.l2;
|
||||||
|
const H = sqrt(G ** 2 + z ** 2);
|
||||||
|
|
||||||
|
const theta1 = -atan2(y, x) - atan2(F, -this.l1);
|
||||||
|
let theta3;
|
||||||
|
try {
|
||||||
|
theta3 = acos(
|
||||||
|
(H ** 2 - this.l3 ** 2 - this.l4 ** 2) / (2 * this.l3 * this.l4)
|
||||||
|
);
|
||||||
|
if (isNaN(theta3)) throw new Error("theta3 is NaN");
|
||||||
|
} catch (error) {
|
||||||
|
theta3 = 0;
|
||||||
|
}
|
||||||
|
const theta2 =
|
||||||
|
atan2(z, G) -
|
||||||
|
atan2(this.l4 * sin(theta3), this.l3 + this.l4 * cos(theta3));
|
||||||
|
|
||||||
|
return [theta1, theta2, theta3];
|
||||||
|
}
|
||||||
|
|
||||||
|
matrixMultiply(a, b) {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
const row = [];
|
||||||
|
|
||||||
|
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, vector) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
matrixAdd(a, b) {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
const row = [];
|
||||||
|
|
||||||
|
for (let j = 0; j < a[i].length; j++) {
|
||||||
|
row.push(a[i][j] + b[i][j]);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
calcLegPoints(angles) {
|
||||||
|
const [theta1, theta2, theta3] = angles;
|
||||||
|
const theta23 = theta2 + theta3;
|
||||||
|
|
||||||
|
const T0 = [0, 0, 0, 1];
|
||||||
|
const T1 = this.vectorAdd(T0, [
|
||||||
|
-this.l1 * Math.cos(theta1),
|
||||||
|
this.l1 * Math.sin(theta1),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
]);
|
||||||
|
const T2 = this.vectorAdd(T1, [
|
||||||
|
-this.l2 * Math.sin(theta1),
|
||||||
|
-this.l2 * Math.cos(theta1),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
]);
|
||||||
|
const T3 = 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 = 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];
|
||||||
|
}
|
||||||
|
|
||||||
|
calcIK(Lp, angles, center) {
|
||||||
|
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 = [
|
||||||
|
[-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])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
vectorAdd(a, b) {
|
||||||
|
return a.map((val, index) => val + b[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
matrixInverse(matrix) {
|
||||||
|
const det = this.determinant(matrix);
|
||||||
|
const adjugate = this.adjugate(matrix);
|
||||||
|
const scalar = 1 / det;
|
||||||
|
const inverse = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < matrix.length; i++) {
|
||||||
|
const row = [];
|
||||||
|
|
||||||
|
for (let j = 0; j < matrix[i].length; j++) {
|
||||||
|
row.push(adjugate[i][j] * scalar);
|
||||||
|
}
|
||||||
|
|
||||||
|
inverse.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return inverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
determinant(matrix) {
|
||||||
|
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 = [];
|
||||||
|
|
||||||
|
for (let j = 1; j < matrix.length; j++) {
|
||||||
|
const row = [];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
adjugate(matrix) {
|
||||||
|
if (matrix.length !== matrix[0].length) {
|
||||||
|
throw new Error("The matrix is not square.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const adjugate = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < matrix.length; i++) {
|
||||||
|
const row = [];
|
||||||
|
|
||||||
|
for (let j = 0; j < matrix[i].length; j++) {
|
||||||
|
const sign = (i + j) % 2 === 0 ? 1 : -1;
|
||||||
|
const subMatrix = [];
|
||||||
|
|
||||||
|
for (let k = 0; k < matrix.length; k++) {
|
||||||
|
if (k !== i) {
|
||||||
|
const subRow = [];
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
transpose(matrix) {
|
||||||
|
const transposed = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < matrix.length; i++) {
|
||||||
|
const row = [];
|
||||||
|
|
||||||
|
for (let j = 0; j < matrix[i].length; j++) {
|
||||||
|
row.push(matrix[j][i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
transposed.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return transposed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ForwardKinematics {
|
||||||
|
l1;
|
||||||
|
l2;
|
||||||
|
l3;
|
||||||
|
l4;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.l1 = 50;
|
||||||
|
this.l2 = 20;
|
||||||
|
this.l3 = 120;
|
||||||
|
this.l4 = 155;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateFootpoint(theta1, theta2, theta3) {
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateFootpoints(angles) {
|
||||||
|
const footpoints = [];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "mock",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"ws": "^8.16.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+483
@@ -0,0 +1,483 @@
|
|||||||
|
lockfileVersion: '6.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
cors:
|
||||||
|
specifier: ^2.8.5
|
||||||
|
version: 2.8.5
|
||||||
|
express:
|
||||||
|
specifier: ^4.18.2
|
||||||
|
version: 4.18.2
|
||||||
|
ws:
|
||||||
|
specifier: ^8.16.0
|
||||||
|
version: 8.16.0
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
/accepts@1.3.8:
|
||||||
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dependencies:
|
||||||
|
mime-types: 2.1.35
|
||||||
|
negotiator: 0.6.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/array-flatten@1.1.1:
|
||||||
|
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/body-parser@1.20.1:
|
||||||
|
resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
|
||||||
|
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||||
|
dependencies:
|
||||||
|
bytes: 3.1.2
|
||||||
|
content-type: 1.0.5
|
||||||
|
debug: 2.6.9
|
||||||
|
depd: 2.0.0
|
||||||
|
destroy: 1.2.0
|
||||||
|
http-errors: 2.0.0
|
||||||
|
iconv-lite: 0.4.24
|
||||||
|
on-finished: 2.4.1
|
||||||
|
qs: 6.11.0
|
||||||
|
raw-body: 2.5.1
|
||||||
|
type-is: 1.6.18
|
||||||
|
unpipe: 1.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/bytes@3.1.2:
|
||||||
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/call-bind@1.0.5:
|
||||||
|
resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==}
|
||||||
|
dependencies:
|
||||||
|
function-bind: 1.1.2
|
||||||
|
get-intrinsic: 1.2.2
|
||||||
|
set-function-length: 1.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/content-disposition@0.5.4:
|
||||||
|
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/content-type@1.0.5:
|
||||||
|
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/cookie-signature@1.0.6:
|
||||||
|
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/cookie@0.5.0:
|
||||||
|
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/cors@2.8.5:
|
||||||
|
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
dependencies:
|
||||||
|
object-assign: 4.1.1
|
||||||
|
vary: 1.1.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/debug@2.6.9:
|
||||||
|
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||||
|
peerDependencies:
|
||||||
|
supports-color: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
supports-color:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
ms: 2.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/define-data-property@1.1.1:
|
||||||
|
resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
get-intrinsic: 1.2.2
|
||||||
|
gopd: 1.0.1
|
||||||
|
has-property-descriptors: 1.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/depd@2.0.0:
|
||||||
|
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/destroy@1.2.0:
|
||||||
|
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
||||||
|
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/ee-first@1.1.1:
|
||||||
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/encodeurl@1.0.2:
|
||||||
|
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/escape-html@1.0.3:
|
||||||
|
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/etag@1.8.1:
|
||||||
|
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/express@4.18.2:
|
||||||
|
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
|
||||||
|
engines: {node: '>= 0.10.0'}
|
||||||
|
dependencies:
|
||||||
|
accepts: 1.3.8
|
||||||
|
array-flatten: 1.1.1
|
||||||
|
body-parser: 1.20.1
|
||||||
|
content-disposition: 0.5.4
|
||||||
|
content-type: 1.0.5
|
||||||
|
cookie: 0.5.0
|
||||||
|
cookie-signature: 1.0.6
|
||||||
|
debug: 2.6.9
|
||||||
|
depd: 2.0.0
|
||||||
|
encodeurl: 1.0.2
|
||||||
|
escape-html: 1.0.3
|
||||||
|
etag: 1.8.1
|
||||||
|
finalhandler: 1.2.0
|
||||||
|
fresh: 0.5.2
|
||||||
|
http-errors: 2.0.0
|
||||||
|
merge-descriptors: 1.0.1
|
||||||
|
methods: 1.1.2
|
||||||
|
on-finished: 2.4.1
|
||||||
|
parseurl: 1.3.3
|
||||||
|
path-to-regexp: 0.1.7
|
||||||
|
proxy-addr: 2.0.7
|
||||||
|
qs: 6.11.0
|
||||||
|
range-parser: 1.2.1
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
send: 0.18.0
|
||||||
|
serve-static: 1.15.0
|
||||||
|
setprototypeof: 1.2.0
|
||||||
|
statuses: 2.0.1
|
||||||
|
type-is: 1.6.18
|
||||||
|
utils-merge: 1.0.1
|
||||||
|
vary: 1.1.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/finalhandler@1.2.0:
|
||||||
|
resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dependencies:
|
||||||
|
debug: 2.6.9
|
||||||
|
encodeurl: 1.0.2
|
||||||
|
escape-html: 1.0.3
|
||||||
|
on-finished: 2.4.1
|
||||||
|
parseurl: 1.3.3
|
||||||
|
statuses: 2.0.1
|
||||||
|
unpipe: 1.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/forwarded@0.2.0:
|
||||||
|
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/fresh@0.5.2:
|
||||||
|
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/function-bind@1.1.2:
|
||||||
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/get-intrinsic@1.2.2:
|
||||||
|
resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==}
|
||||||
|
dependencies:
|
||||||
|
function-bind: 1.1.2
|
||||||
|
has-proto: 1.0.1
|
||||||
|
has-symbols: 1.0.3
|
||||||
|
hasown: 2.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/gopd@1.0.1:
|
||||||
|
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
||||||
|
dependencies:
|
||||||
|
get-intrinsic: 1.2.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/has-property-descriptors@1.0.1:
|
||||||
|
resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==}
|
||||||
|
dependencies:
|
||||||
|
get-intrinsic: 1.2.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/has-proto@1.0.1:
|
||||||
|
resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/has-symbols@1.0.3:
|
||||||
|
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/hasown@2.0.0:
|
||||||
|
resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
function-bind: 1.1.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/http-errors@2.0.0:
|
||||||
|
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dependencies:
|
||||||
|
depd: 2.0.0
|
||||||
|
inherits: 2.0.4
|
||||||
|
setprototypeof: 1.2.0
|
||||||
|
statuses: 2.0.1
|
||||||
|
toidentifier: 1.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/iconv-lite@0.4.24:
|
||||||
|
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dependencies:
|
||||||
|
safer-buffer: 2.1.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/inherits@2.0.4:
|
||||||
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/ipaddr.js@1.9.1:
|
||||||
|
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/media-typer@0.3.0:
|
||||||
|
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/merge-descriptors@1.0.1:
|
||||||
|
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/methods@1.1.2:
|
||||||
|
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/mime-db@1.52.0:
|
||||||
|
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/mime-types@2.1.35:
|
||||||
|
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dependencies:
|
||||||
|
mime-db: 1.52.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/mime@1.6.0:
|
||||||
|
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/ms@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/ms@2.1.3:
|
||||||
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/negotiator@0.6.3:
|
||||||
|
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/object-assign@4.1.1:
|
||||||
|
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/object-inspect@1.13.1:
|
||||||
|
resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/on-finished@2.4.1:
|
||||||
|
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dependencies:
|
||||||
|
ee-first: 1.1.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/parseurl@1.3.3:
|
||||||
|
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/path-to-regexp@0.1.7:
|
||||||
|
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/proxy-addr@2.0.7:
|
||||||
|
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
dependencies:
|
||||||
|
forwarded: 0.2.0
|
||||||
|
ipaddr.js: 1.9.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/qs@6.11.0:
|
||||||
|
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
|
||||||
|
engines: {node: '>=0.6'}
|
||||||
|
dependencies:
|
||||||
|
side-channel: 1.0.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/range-parser@1.2.1:
|
||||||
|
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/raw-body@2.5.1:
|
||||||
|
resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dependencies:
|
||||||
|
bytes: 3.1.2
|
||||||
|
http-errors: 2.0.0
|
||||||
|
iconv-lite: 0.4.24
|
||||||
|
unpipe: 1.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/safe-buffer@5.2.1:
|
||||||
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/safer-buffer@2.1.2:
|
||||||
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/send@0.18.0:
|
||||||
|
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
dependencies:
|
||||||
|
debug: 2.6.9
|
||||||
|
depd: 2.0.0
|
||||||
|
destroy: 1.2.0
|
||||||
|
encodeurl: 1.0.2
|
||||||
|
escape-html: 1.0.3
|
||||||
|
etag: 1.8.1
|
||||||
|
fresh: 0.5.2
|
||||||
|
http-errors: 2.0.0
|
||||||
|
mime: 1.6.0
|
||||||
|
ms: 2.1.3
|
||||||
|
on-finished: 2.4.1
|
||||||
|
range-parser: 1.2.1
|
||||||
|
statuses: 2.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/serve-static@1.15.0:
|
||||||
|
resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
dependencies:
|
||||||
|
encodeurl: 1.0.2
|
||||||
|
escape-html: 1.0.3
|
||||||
|
parseurl: 1.3.3
|
||||||
|
send: 0.18.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/set-function-length@1.2.0:
|
||||||
|
resolution: {integrity: sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
define-data-property: 1.1.1
|
||||||
|
function-bind: 1.1.2
|
||||||
|
get-intrinsic: 1.2.2
|
||||||
|
gopd: 1.0.1
|
||||||
|
has-property-descriptors: 1.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/setprototypeof@1.2.0:
|
||||||
|
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/side-channel@1.0.4:
|
||||||
|
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.5
|
||||||
|
get-intrinsic: 1.2.2
|
||||||
|
object-inspect: 1.13.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/statuses@2.0.1:
|
||||||
|
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/toidentifier@1.0.1:
|
||||||
|
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||||
|
engines: {node: '>=0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/type-is@1.6.18:
|
||||||
|
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dependencies:
|
||||||
|
media-typer: 0.3.0
|
||||||
|
mime-types: 2.1.35
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/unpipe@1.0.0:
|
||||||
|
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/utils-merge@1.0.1:
|
||||||
|
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||||
|
engines: {node: '>= 0.4.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/vary@1.1.2:
|
||||||
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/ws@8.16.0:
|
||||||
|
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: '>=5.0.2'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
dev: false
|
||||||
+336
@@ -0,0 +1,336 @@
|
|||||||
|
import express from "express";
|
||||||
|
import cors from "cors";
|
||||||
|
import Kinematic from "./kinematic.js";
|
||||||
|
import { WebSocketServer } from "ws";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const kinematic = new Kinematic();
|
||||||
|
const wss = new WebSocketServer({ port: 8080 });
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
const port = 3000;
|
||||||
|
const subscriptions = {};
|
||||||
|
|
||||||
|
const randomFloatFromInterval = (min, max) =>
|
||||||
|
Math.floor((Math.random() * (max - min + 1) + min) * 100) / 100;
|
||||||
|
|
||||||
|
const radToDeg = (val) => val * (180 / Math.PI);
|
||||||
|
const degToRad = (val) => val * (Math.PI / 180);
|
||||||
|
|
||||||
|
function createNewClientState() {
|
||||||
|
return {
|
||||||
|
model: JSON.parse(JSON.stringify(model)),
|
||||||
|
settings: JSON.parse(JSON.stringify(settings)),
|
||||||
|
logs: JSON.parse(JSON.stringify(logs)),
|
||||||
|
subscriptions: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribeClientToCategory(ws, category) {
|
||||||
|
if (!subscriptions[category]) {
|
||||||
|
subscriptions[category] = new Set();
|
||||||
|
}
|
||||||
|
subscriptions[category].add(ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsubscribeClientFromCategory = (ws, category) => {
|
||||||
|
if (!subscriptions[category]) return;
|
||||||
|
subscriptions[category].delete(ws);
|
||||||
|
if (subscriptions[category].size === 0) {
|
||||||
|
delete subscriptions[category].size;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendUpdateToSubscribers = (category, data) => {
|
||||||
|
if (subscriptions[category]) {
|
||||||
|
const message = JSON.stringify(data);
|
||||||
|
for (const client of subscriptions[category]) {
|
||||||
|
client.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!Array.prototype.last){
|
||||||
|
Array.prototype.last = function(){
|
||||||
|
return this[this.length - 1];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const model = {
|
||||||
|
battery: {
|
||||||
|
voltage: randomFloatFromInterval(7.6, 8.2),
|
||||||
|
ampere: randomFloatFromInterval(0.2, 3),
|
||||||
|
power_button: false,
|
||||||
|
},
|
||||||
|
servos: {
|
||||||
|
angles: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
dir: [-1, -1, -1, 1, -1, -1, -1, -1, -1, 1, -1, -1],
|
||||||
|
on: true,
|
||||||
|
},
|
||||||
|
mpu: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
z: 0,
|
||||||
|
heading: 240,
|
||||||
|
temperature: 21,
|
||||||
|
},
|
||||||
|
display: [[]],
|
||||||
|
distance_sensors: {
|
||||||
|
left: 22,
|
||||||
|
right: 23,
|
||||||
|
},
|
||||||
|
appTime: 1123321,
|
||||||
|
connectivity: {
|
||||||
|
ssid: "best network",
|
||||||
|
ip: "192.168.0.118",
|
||||||
|
mDNS: "leika.local",
|
||||||
|
rssi: 100,
|
||||||
|
},
|
||||||
|
running: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
useMetric: true,
|
||||||
|
name: "Leika",
|
||||||
|
ssid: "Rune private network",
|
||||||
|
pass: "12345678",
|
||||||
|
ap: "Leika",
|
||||||
|
apPass: "12345678",
|
||||||
|
apChannel: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const logs = [
|
||||||
|
"[2023-02-05 10:00:00] [verbose] Booting up",
|
||||||
|
"[2023-02-05 10:00:10] [verbose] Starting webserver",
|
||||||
|
"[2023-02-05 10:00:20] [verbose] Loading setting",
|
||||||
|
"[2023-02-05 10:00:30] [verbose] Connected to Rune private network",
|
||||||
|
];
|
||||||
|
|
||||||
|
const system = {
|
||||||
|
HeapSize: 400000,
|
||||||
|
HeapFree: 0,
|
||||||
|
HeapMin: 0,
|
||||||
|
DmaHeapSize: 0,
|
||||||
|
DmaHeapFree: 0,
|
||||||
|
DmaHeapMin: 0,
|
||||||
|
PsramSize: 400000,
|
||||||
|
PsramFree: 0,
|
||||||
|
PsramMin: 0,
|
||||||
|
ChipModel: 0,
|
||||||
|
ChipRevision: 0,
|
||||||
|
ChipCores: 2,
|
||||||
|
CpuFreqMHz: 80,
|
||||||
|
SketchSize: 0,
|
||||||
|
FreeSketchSpace: 10200,
|
||||||
|
FlashChipSize: 0,
|
||||||
|
CpuUsed: 0,
|
||||||
|
CpuUsedCore0: 0,
|
||||||
|
CpuUsedCore1: 0,
|
||||||
|
arduinoVersion: "3.2.1",
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBattery = () => {
|
||||||
|
model.battery.voltage = randomFloatFromInterval(7.6, 8.2);
|
||||||
|
model.battery.ampere = randomFloatFromInterval(0.2, 3);
|
||||||
|
return model.battery;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMpu = () => {
|
||||||
|
model.mpu.x = randomFloatFromInterval(0, 1);
|
||||||
|
model.mpu.y = randomFloatFromInterval(0, 1);
|
||||||
|
model.mpu.z = randomFloatFromInterval(0, 1);
|
||||||
|
model.mpu.temperature = randomFloatFromInterval(20, 22);
|
||||||
|
return model.mpu;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDistances = () => {
|
||||||
|
model.distance_sensors = {
|
||||||
|
left: randomFloatFromInterval(10, 220),
|
||||||
|
right: randomFloatFromInterval(10, 220),
|
||||||
|
};
|
||||||
|
return model.distance_sensors;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDistance = (position) => {
|
||||||
|
model.distance_sensors[position] = randomFloatFromInterval(10, 220);
|
||||||
|
return model.distance_sensors[position];
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSystem = () => {
|
||||||
|
system.CpuUsedCore0 = randomFloatFromInterval(0, 100);
|
||||||
|
system.CpuUsedCore1 = randomFloatFromInterval(0, 100);
|
||||||
|
system.CpuUsed =
|
||||||
|
Math.floor((system.CpuUsedCore0 + system.CpuUsedCore1) / 0.02) / 100;
|
||||||
|
system.HeapFree = randomFloatFromInterval(0, 20000);
|
||||||
|
system.HeapMin = randomFloatFromInterval(0, 20000);
|
||||||
|
return system;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBodyState = (model, angles, position) => {
|
||||||
|
const Lp = [
|
||||||
|
[100, -100, 100, 1],
|
||||||
|
[100, -100, -100, 1],
|
||||||
|
[-100, -100, 100, 1],
|
||||||
|
[-100, -100, -100, 1],
|
||||||
|
];
|
||||||
|
|
||||||
|
model.servos.angles = kinematic
|
||||||
|
.calcIK(
|
||||||
|
Lp,
|
||||||
|
angles.map((x) => degToRad(x)),
|
||||||
|
position
|
||||||
|
)
|
||||||
|
.flat()
|
||||||
|
.map((x, i) => radToDeg(x * model.servos.dir[i]));
|
||||||
|
return model.servos.angles;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAngle = (id, angle) => {
|
||||||
|
model.servos.angles[id] = angle;
|
||||||
|
return model.servos.angles;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAngles = (angles) => {
|
||||||
|
model.servos.angles = angles;
|
||||||
|
return model.servos.angles;
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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" }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("close", () => {
|
||||||
|
for (const category in subscriptions) {
|
||||||
|
unsubscribeClientFromCategory(ws, category);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/sensor/battery", (req, res) => res.send(updateBattery()));
|
||||||
|
app.get("/sensor/mpu", (req, res) => res.send(updateMpu()));
|
||||||
|
app.get("/sensor/distances", (req, res) => res.send(updateDistances()));
|
||||||
|
app.get("/sensor/distance/:position", (req, res) =>
|
||||||
|
res.send({ distance: updateDistance(req.params.position) })
|
||||||
|
);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------- //
|
||||||
|
|
||||||
|
app.post("/kinematic/angle/:id", (req, res) =>
|
||||||
|
res.send(updateAngle(req.params.id, req.body.angle))
|
||||||
|
);
|
||||||
|
app.post("/kinematic/angles/", (req, res) =>
|
||||||
|
res.send(updateAngles(req.body.angles))
|
||||||
|
);
|
||||||
|
app.get("/kinematic/bodystate", (req, res) => res.send(model.servos.angles));
|
||||||
|
app.post("/kinematic/bodystate", (req, res) => {
|
||||||
|
sendUpdateToSubscribers("angles", model.servos.angles);
|
||||||
|
res.send(updateBodyState(model, req.body.angles, req.body.position));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------- //
|
||||||
|
|
||||||
|
app.get("/system/log", (req, res) => res.send(logs));
|
||||||
|
app.get("/system/info", (req, res) => res.send(updateSystem()));
|
||||||
|
app.get("/system/settings", (req, res) => res.send(settings));
|
||||||
|
app.post("/system/settings", (req, res) => {
|
||||||
|
Object.entries(req.body).forEach((x) => (settings[x[0]] = x[1]));
|
||||||
|
res.send(settings);
|
||||||
|
});
|
||||||
|
app.post("/system/stop", (req, res) => {
|
||||||
|
model.running = false;
|
||||||
|
model.res.send(settings);
|
||||||
|
});
|
||||||
|
app.listen(port, () => console.log(`Open at http://localhost:${port}`));
|
||||||
Reference in New Issue
Block a user