Deletes old project
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
VITE_API_URL="leika.local"
|
||||
VITE_SOCKET_URL="leika.local"
|
||||
VITE_EMBEDDED_BUILD=true
|
||||
@@ -1,3 +0,0 @@
|
||||
VITE_API_URL="hostname"
|
||||
VITE_SOCKET_URL="hostname:2096"
|
||||
VITE_EMBEDDED_BUILD=true
|
||||
@@ -1,3 +0,0 @@
|
||||
VITE_API_URL="hostname"
|
||||
VITE_SOCKET_URL="hostname:2096"
|
||||
VITE_EMBEDDED_BUILD=false
|
||||
@@ -1,3 +0,0 @@
|
||||
VITE_API_URL="leika.local"
|
||||
VITE_SOCKET_URL="leika.local"
|
||||
VITE_EMBEDDED_BUILD=false
|
||||
+1
-1
@@ -10,4 +10,4 @@ node_modules
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
yarn.lock
|
||||
|
||||
+20
-9
@@ -1,20 +1,31 @@
|
||||
/** @type { import("eslint").Linter.Config } */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
|
||||
plugins: ['svelte3', '@typescript-eslint'],
|
||||
ignorePatterns: ['*.cjs'],
|
||||
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||
settings: {
|
||||
'svelte3/typescript': () => require('typescript')
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
}
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
"overrides": [],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": {}
|
||||
}
|
||||
+9
-23
@@ -1,24 +1,10 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
engine-strict=true
|
||||
+1
-10
@@ -1,13 +1,4 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
yarn.lock
|
||||
|
||||
@@ -4,6 +4,5 @@
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
|
||||
+37
-2
@@ -1,3 +1,38 @@
|
||||
# Controller App
|
||||
# create-svelte
|
||||
|
||||
This is the controller for my spot micro
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/logo512.png" />
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
+46
-41
@@ -1,55 +1,60 @@
|
||||
{
|
||||
"name": "app",
|
||||
"name": "app2",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --mode embedded",
|
||||
"dev:mock_embedded": "vite --mode mock_embedded",
|
||||
"dev:mock_web": "vite --mode mock_web",
|
||||
"build": "vite build --mode embedded",
|
||||
"build:mock_web": "vite build --mode mock_web",
|
||||
"build:web": "vite build --mode web",
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest --environment jsdom",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"format": "prettier --plugin-search-dir . --write ."
|
||||
"test": "npm run test:integration && npm run test:unit",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"test:integration": "playwright test",
|
||||
"test:unit": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@tsconfig/svelte": "^5.0.2",
|
||||
"@types/three": "^0.160.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"cross-env": "^7.0.3",
|
||||
"husky": "^9.0.7",
|
||||
"jsdom": "^24.0.0",
|
||||
"lint-staged": "^15.2.0",
|
||||
"postcss": "^8.4.33",
|
||||
"prettier": "3.2.4",
|
||||
"svelte": "^4.2.9",
|
||||
"svelte-check": "^3.6.3",
|
||||
"svelte-hero-icons": "^5.0.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-singlefile": "^1.0.0",
|
||||
"vitest": "^1.3.1"
|
||||
"@iconify-json/mdi": "^1.1.64",
|
||||
"@iconify-json/tabler": "^1.1.109",
|
||||
"@playwright/test": "^1.28.1",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.5.5",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@types/eslint": "^8.56.0",
|
||||
"@types/three": "^0.162.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
"svelte-focus-trap": "^1.2.0",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"unplugin-icons": "^0.18.5",
|
||||
"vite": "^5.0.3",
|
||||
"vitest": "^1.2.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"chart.js": "^4.4.2",
|
||||
"compare-versions": "^6.1.0",
|
||||
"daisyui": "^4.10.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"nipplejs": "^0.10.1",
|
||||
"prettier-plugin-svelte": "^3.2.1",
|
||||
"svelte-routing": "^2.11.0",
|
||||
"three": "^0.160.1",
|
||||
"svelte-dnd-list": "^0.1.8",
|
||||
"svelte-modals": "^1.3.0",
|
||||
"three": "^0.162.0",
|
||||
"urdf-loader": "^0.12.1",
|
||||
"uzip": "^0.20201231.0",
|
||||
"xacro-parser": "^0.3.9"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": "eslint --cache --fix",
|
||||
"*.{js,css,md,ts,svelte}": "prettier --write"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
webServer: {
|
||||
command: 'pnpm run build && pnpm run preview',
|
||||
port: 4173
|
||||
},
|
||||
testDir: 'tests',
|
||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
+949
-895
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
import tailwindcss from 'tailwindcss';
|
||||
import autoprefixer from 'autoprefixer';
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
plugins: [tailwindcss(), autoprefixer()]
|
||||
};
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 151 KiB |
Binary file not shown.
@@ -1,47 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Router, Route } from 'svelte-routing';
|
||||
import { onMount } from 'svelte';
|
||||
import TopBar from './components/Topbar.svelte';
|
||||
import socketService from '$lib/services/socket-service';
|
||||
import Controller from './routes/Controller.svelte';
|
||||
import { fileService } from '$lib/services';
|
||||
import Settings from './routes/Settings.svelte';
|
||||
import { jointNames, model, outControllerData, mode } from '$lib/stores';
|
||||
import { loadModelAsync, socketLocation } from '$lib/utilities';
|
||||
import type { Result } from '$lib/utilities/result';
|
||||
|
||||
export let url = window.location.pathname;
|
||||
onMount(async () => {
|
||||
socketService.connect(socketLocation);
|
||||
socketService.addPublisher(outControllerData);
|
||||
socketService.addPublisher(mode, 'mode');
|
||||
|
||||
registerFetchIntercept();
|
||||
const modelRes = await loadModelAsync('/spot_micro.urdf.xacro');
|
||||
|
||||
if (modelRes.isOk()) {
|
||||
const [urdf, JOINT_NAME] = modelRes.inner;
|
||||
jointNames.set(JOINT_NAME);
|
||||
model.set(urdf);
|
||||
} else {
|
||||
console.error(modelRes.inner, { exception: modelRes.exception });
|
||||
}
|
||||
});
|
||||
|
||||
const registerFetchIntercept = () => {
|
||||
const { fetch: originalFetch } = window;
|
||||
window.fetch = async (resource, config) => {
|
||||
let url = resource instanceof Request ? resource.url : resource.toString();
|
||||
let file = await fileService.getFile(url);
|
||||
return file.isOk() ? new Response(file.inner) : originalFetch(resource, config);
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<Router {url}>
|
||||
<TopBar />
|
||||
<div class="absolute w-full h-full bg-background text-on-background">
|
||||
<Route path="/" component={Controller} />
|
||||
<Route path="/settings/*page" component={Settings} />
|
||||
</div>
|
||||
</Router>
|
||||
+13
-15
@@ -1,20 +1,18 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
#nipple_0_0, #nipple_1_1 {
|
||||
z-index: 10!important;
|
||||
}
|
||||
|
||||
#three-gui-panel {
|
||||
top: 50px;
|
||||
right:0px
|
||||
top: 64px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
#three-gui-panel {
|
||||
top: 48px;
|
||||
}
|
||||
}
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/logo512.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,84 +0,0 @@
|
||||
<script lang="ts">
|
||||
import socketService from '$lib/services/socket-service';
|
||||
import { Icon, Bars3, XMark, Power, Battery100, Signal, SignalSlash } from 'svelte-hero-icons';
|
||||
import { emulateModel } from '$lib/stores';
|
||||
import { Link, useLocation } from 'svelte-routing';
|
||||
import { isConnected } from '$lib/stores';
|
||||
|
||||
const views = ['Virtual environment', 'Robot camera'];
|
||||
const modes = ['Drive', 'Choreography'];
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
let selected_view = views[0];
|
||||
let selected_modes = modes[0];
|
||||
let settingOpen = window.location.pathname.includes('/settings');
|
||||
|
||||
$: emulateModel.set(selected_view === views[0]);
|
||||
$: settingOpen = $location.pathname.includes('/settings');
|
||||
|
||||
const stop = () => {
|
||||
if ($isConnected) {
|
||||
socketService.send(JSON.stringify({ type: 'system/stop' }));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="topbar absolute left-0 top-0 w-full z-10 flex justify-between bg-zinc-800">
|
||||
<div class="flex gap-2 p-2">
|
||||
{#if settingOpen}
|
||||
<Link to="/">
|
||||
<Icon src={XMark} size="32" />
|
||||
</Link>
|
||||
{:else}
|
||||
<Link to="/settings">
|
||||
<Icon src={Bars3} size="32" />
|
||||
</Link>
|
||||
{/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 class="flex gap-2 p-2">
|
||||
<button class="action_button bg-zinc-600">
|
||||
<Icon src={Power} size="24" />
|
||||
</button>
|
||||
<button class="action_button"><Icon src={Battery100} size="24" /></button>
|
||||
<button class="action_button"
|
||||
><Icon src={$isConnected ? Signal : SignalSlash} size="24" /></button
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<button class="h-full w-20 bg-red-600 text-white" on:click={stop}>STOP</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.topbar {
|
||||
height: 50px;
|
||||
}
|
||||
.action_button {
|
||||
border-radius: 4px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
outline: 1px solid #52525b;
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let active = false
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click
|
||||
class={$$restProps.class + ' rounded-md outline outline-2 text-zinc-200 outline-zinc-600 p-2' +
|
||||
(active ? ' bg-zinc-600' : '')}
|
||||
>
|
||||
<slot/>
|
||||
</button>
|
||||
@@ -1,29 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value = 50;
|
||||
export let min = 0;
|
||||
export let max = 100;
|
||||
export let label = '';
|
||||
|
||||
const dispatchValueInput = () => {
|
||||
dispatch('value', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="">
|
||||
<input
|
||||
id="range"
|
||||
type="range"
|
||||
{min}
|
||||
{max}
|
||||
bind:value
|
||||
on:change
|
||||
on:input={dispatchValueInput}
|
||||
class="w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
<label for="range" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{label}</label
|
||||
>
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { jointNames } from '../../lib/stores';
|
||||
|
||||
type Servo = {
|
||||
id: number;
|
||||
name: string;
|
||||
minPWM: number;
|
||||
maxPWM: number;
|
||||
pwmFor180: number;
|
||||
};
|
||||
|
||||
let servos: Servo[] = [];
|
||||
|
||||
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 ?? 0, '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 ?? 0, '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 ?? 0, 'pwmFor180', Number(event.target?.value))}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,23 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { socketService } from '$lib/services';
|
||||
import { isConnected, settings } from '$lib/stores';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
if ($isConnected) {
|
||||
const message = JSON.stringify({ type: 'system/settings' });
|
||||
socketService.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>
|
||||
@@ -1,28 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { humanFileSize } from '$lib/utilities';
|
||||
import socketService from '$lib/services/socket-service';
|
||||
import { isConnected, systemInfo } from '$lib/stores';
|
||||
|
||||
onMount(() => {
|
||||
if ($isConnected) {
|
||||
const message = JSON.stringify({ type: 'system/info' });
|
||||
socketService.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>
|
||||
@@ -1,18 +0,0 @@
|
||||
<script lang="ts">
|
||||
import socketService from '$lib/services/socket-service';
|
||||
import { isConnected, logs } from '$lib/stores';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
if ($isConnected) {
|
||||
const message = JSON.stringify({ type: 'system/logs' });
|
||||
socketService.send(message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full">
|
||||
{#each $logs as entry}
|
||||
<div>{entry}</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -1,35 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* @layer base {
|
||||
:root {
|
||||
--primary: 98 0 238;
|
||||
--primary-variant: 55 0 179;
|
||||
--secondary: 55 0 179;
|
||||
--secondary-variant: 55 0 179;
|
||||
--background: 255 255 255;
|
||||
--surface: 251 251 250;
|
||||
--error: 176 0 32;
|
||||
--on-primary: 255 255 255;
|
||||
--on-secondary: 0 0 0;
|
||||
--on-background: 0 0 0;
|
||||
--on-surface: 0 0 0;
|
||||
--on-error: 255 255 255;
|
||||
}
|
||||
|
||||
:root[class~="dark"] {
|
||||
--primary: 98 0 238;
|
||||
--primary-variant: 55 0 179;
|
||||
--secondary: 55 0 179;
|
||||
--secondary-variant: 55 0 179;
|
||||
--background: 30 30 30;
|
||||
--surface: 36 36 36;
|
||||
--error: 176 0 32;
|
||||
--on-primary: 255 255 255;
|
||||
--on-secondary: 255 255 255;
|
||||
--on-background: 255 255 255;
|
||||
--on-surface: 255 255 255;
|
||||
--on-error: 255 255 255;
|
||||
}
|
||||
} */
|
||||
@@ -0,0 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('sum test', () => {
|
||||
it('adds 1 + 2 to equal 3', () => {
|
||||
expect(1 + 2).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
export function daisyColor(name: string, opacity: number = 100) {
|
||||
const color = getComputedStyle(document.documentElement).getPropertyValue(name);
|
||||
return `oklch(${color} / ${opacity}%)`;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import Battery0 from '~icons/tabler/battery';
|
||||
import Battery25 from '~icons/tabler/battery-1';
|
||||
import Battery50 from '~icons/tabler/battery-2';
|
||||
import Battery75 from '~icons/tabler/battery-3';
|
||||
import Battery100 from '~icons/tabler/battery-4';
|
||||
import BatteryCharging from '~icons/tabler/battery-charging-2';
|
||||
|
||||
export let charging = false;
|
||||
export let soc = 100;
|
||||
</script>
|
||||
|
||||
<div class="tooltip tooltip-bottom" data-tip="{soc} %">
|
||||
{#if charging}
|
||||
<BatteryCharging class="{$$props.class || ''} -rotate-90 animate-pulse" />
|
||||
{:else if soc > 75}
|
||||
<Battery100 class="{$$props.class || ''} -rotate-90" />
|
||||
{:else if soc > 55}
|
||||
<Battery75 class="{$$props.class || ''} -rotate-90" />
|
||||
{:else if soc > 30}
|
||||
<Battery50 class="{$$props.class || ''} -rotate-90" />
|
||||
{:else if soc > 5}
|
||||
<Battery25 class="{$$props.class || ''} -rotate-90" />
|
||||
{:else}
|
||||
<Battery0 class="{$$props.class || ''} text-error -rotate-90 animate-pulse" />
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import Down from '~icons/tabler/chevron-down';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function openCollapsible() {
|
||||
open = !open;
|
||||
if (open) {
|
||||
dispatch('opened');
|
||||
} else {
|
||||
dispatch('closed');
|
||||
}
|
||||
}
|
||||
|
||||
export let open = false;
|
||||
</script>
|
||||
|
||||
<div class="{$$props.class || ''} relative grid w-full max-w-2xl self-center overflow-hidden">
|
||||
<div class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium">
|
||||
<span class="inline-flex items-baseline">
|
||||
<slot name="icon" />
|
||||
<slot name="title" />
|
||||
</span>
|
||||
<button class="btn btn-circle btn-ghost btn-sm" on:click={() => openCollapsible()}>
|
||||
<Down
|
||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open
|
||||
? 'rotate-180'
|
||||
: ''}"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{#if open}
|
||||
<div
|
||||
class="flex flex-col gap-2 p-4 pt-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { closeModal } from 'svelte-modals';
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import Cancel from '~icons/tabler/x';
|
||||
import Check from '~icons/tabler/check';
|
||||
|
||||
// provided by <Modals />
|
||||
export let isOpen: boolean;
|
||||
|
||||
export let title: string;
|
||||
export let message: string;
|
||||
export let onConfirm: any;
|
||||
export let labels = {
|
||||
cancel: { label: 'Cancel', icon: Cancel },
|
||||
confirm: { label: 'OK', icon: Check }
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
on:introstart
|
||||
on:outroend
|
||||
use:focusTrap
|
||||
>
|
||||
<div
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
|
||||
>
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||
<div class="divider my-2" />
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
<div class="divider my-2" />
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-primary inline-flex items-center" on:click={closeModal}
|
||||
><svelte:component this={labels.cancel.icon} class="mr-2 h-5 w-5" /><span
|
||||
>{labels?.cancel.label}</span
|
||||
></button
|
||||
>
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex items-center"
|
||||
on:click={onConfirm}
|
||||
><svelte:component this={labels?.confirm.icon} class="mr-2 h-5 w-5" /><span
|
||||
>{labels?.confirm.label}</span
|
||||
></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import { closeAllModals, onBeforeClose } from 'svelte-modals';
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { telemetry } from '$lib/stores/telemetry';
|
||||
import Cancel from '~icons/tabler/x';
|
||||
|
||||
// provided by <Modals />
|
||||
export let isOpen: boolean;
|
||||
|
||||
let updating = true;
|
||||
|
||||
let progress = 0;
|
||||
$: if ($telemetry.download_ota.status == 'progress') {
|
||||
progress = $telemetry.download_ota.progress;
|
||||
}
|
||||
|
||||
$: if ($telemetry.download_ota.status == 'error') {
|
||||
updating = false;
|
||||
}
|
||||
|
||||
let message = 'Preparing ...';
|
||||
let timerId: number;
|
||||
|
||||
$: if ($telemetry.download_ota.status == 'progress') {
|
||||
message = 'Downloading ...';
|
||||
} else if ($telemetry.download_ota.status == 'error') {
|
||||
message = $telemetry.download_ota.error;
|
||||
} else if ($telemetry.download_ota.status == 'finished') {
|
||||
message = 'Restarting ...';
|
||||
progress = 0;
|
||||
// Reload page after 5 sec
|
||||
timerId = setTimeout(() => {
|
||||
closeAllModals();
|
||||
location.reload();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
onBeforeClose(() => {
|
||||
if (updating) {
|
||||
// prevents modal from closing
|
||||
return false;
|
||||
} else {
|
||||
$telemetry.download_ota.status = 'idle';
|
||||
$telemetry.download_ota.error = '';
|
||||
$telemetry.download_ota.progress = 0;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
on:introstart
|
||||
on:outroend
|
||||
use:focusTrap
|
||||
>
|
||||
<div
|
||||
class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
|
||||
>
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">Updating Firmware</h2>
|
||||
<div class="divider my-2" />
|
||||
<div class="overflow-y-auto">
|
||||
<div class="bg-base-100 flex flex-col items-center justify-center p-6">
|
||||
{#if $telemetry.download_ota.status == 'progress'}
|
||||
<progress class="progress progress-primary w-56" value={progress} max="100" />
|
||||
{:else}
|
||||
<progress class="progress progress-primary w-56" />
|
||||
{/if}
|
||||
<p class="mt-8 text-2xl">{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider my-2" />
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<div class="flex-grow" />
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex flex-none items-center"
|
||||
disabled={updating}
|
||||
on:click={() => {
|
||||
closeAllModals();
|
||||
location.reload();
|
||||
}}
|
||||
>
|
||||
<Cancel class="mr-2 h-5 w-5" /><span>Close</span></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { closeModal } from 'svelte-modals';
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import Check from '~icons/tabler/check';
|
||||
|
||||
// provided by <Modals />
|
||||
export let isOpen: boolean;
|
||||
|
||||
export let title: string;
|
||||
export let message: string;
|
||||
export let onDismiss: any;
|
||||
export let dismiss = { label: 'Dismiss', icon: Check };
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
on:introstart
|
||||
on:outroend
|
||||
use:focusTrap
|
||||
>
|
||||
<div
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
|
||||
>
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||
<div class="divider my-2" />
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
<div class="divider my-2" />
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex items-center"
|
||||
on:click={onDismiss}
|
||||
><svelte:component this={dismiss.icon} class="mr-2 h-5 w-5" /><span>{dismiss.label}</span
|
||||
></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
let show = false;
|
||||
$: type = show ? 'text' : 'password';
|
||||
|
||||
export let value = '';
|
||||
export let id = '';
|
||||
function handleInput(e: any) {
|
||||
value = e.target.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<input {type} class="input input-bordered w-full" {value} on:input={handleInput} {id} />
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-1">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-base-content/50 h-6 {show ? 'block' : 'hidden'}"
|
||||
on:click={() => (show = false)}
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
|
||||
<path
|
||||
d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87"
|
||||
/>
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-base-content/50 h-6 {show ? 'hidden' : 'block'}"
|
||||
on:click={() => (show = true)}
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import WiFi from '~icons/tabler/wifi';
|
||||
import WiFi0 from '~icons/tabler/wifi-0';
|
||||
import WiFi1 from '~icons/tabler/wifi-1';
|
||||
import WiFi2 from '~icons/tabler/wifi-2';
|
||||
|
||||
export let showDBm = false;
|
||||
export let rssi_dbm = 0;
|
||||
</script>
|
||||
|
||||
<div class="indicator">
|
||||
{#if showDBm}
|
||||
<span class="indicator-item indicator-start badge badge-accent badge-outline badge-xs">
|
||||
{rssi_dbm} dBm
|
||||
</span>
|
||||
{/if}
|
||||
{#if rssi_dbm >= -55}
|
||||
<WiFi class={$$props.class || ''} />
|
||||
{:else if rssi_dbm >= -75}
|
||||
<div class="{$$props.class || ''} relative">
|
||||
<WiFi class="absolute inset-0 h-full w-full opacity-30" />
|
||||
<WiFi2 class="absolute inset-0 h-full w-full" />
|
||||
</div>
|
||||
{:else if rssi_dbm >= -85}
|
||||
<div class="{$$props.class || ''} relative">
|
||||
<WiFi class="absolute inset-0 h-full w-full opacity-30" />
|
||||
<WiFi1 class="absolute inset-0 h-full w-full" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="{$$props.class || ''} relative">
|
||||
<WiFi class="absolute inset-0 h-full w-full opacity-30" />
|
||||
<WiFi0 class="absolute inset-0 h-full w-full" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import Down from '~icons/tabler/chevron-down';
|
||||
export let open = true;
|
||||
export let collapsible = true;
|
||||
</script>
|
||||
|
||||
{#if collapsible}
|
||||
<div
|
||||
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
|
||||
>
|
||||
<div
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
|
||||
>
|
||||
<span class="inline-flex items-baseline">
|
||||
<slot name="icon" />
|
||||
<slot name="title" />
|
||||
</span>
|
||||
<button
|
||||
class="btn btn-circle btn-ghost btn-sm"
|
||||
on:click={() => {
|
||||
open = !open;
|
||||
}}
|
||||
>
|
||||
<Down
|
||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open
|
||||
? 'rotate-180'
|
||||
: ''}"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{#if open}
|
||||
<div
|
||||
class="flex flex-col gap-2 p-4 pt-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
|
||||
>
|
||||
<div class="min-h-16 w-full p-4 text-xl font-medium">
|
||||
<span class="inline-flex items-baseline">
|
||||
<slot name="icon" />
|
||||
<slot name="title" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 p-4 pt-0">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import Loader from '~icons/tabler/loader-2';
|
||||
</script>
|
||||
|
||||
<div class="flex h-full w-full flex-col items-center justify-center p-6">
|
||||
<Loader class="text-primary h-14 w-auto animate-spin stroke-2" />
|
||||
<p class="text-xl">Loading...</p>
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script>
|
||||
import { flip } from 'svelte/animate';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import error from '~icons/tabler/circle-x';
|
||||
import success from '~icons/tabler/circle-check';
|
||||
import warning from '~icons/tabler/alert-triangle';
|
||||
import info from '~icons/tabler/info-circle';
|
||||
|
||||
export let theme = {
|
||||
error: 'alert-error',
|
||||
success: 'alert-success',
|
||||
warning: 'alert-warning',
|
||||
info: 'alert-info'
|
||||
};
|
||||
|
||||
export let icon = {
|
||||
error: error,
|
||||
success: success,
|
||||
warning: warning,
|
||||
info: info
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="toast toast-end mr-4">
|
||||
{#each $notifications as notification (notification.id)}
|
||||
<div
|
||||
animate:flip={{ duration: 400 }}
|
||||
class="alert animate-none {theme[notification.type]}"
|
||||
in:fly={{ y: 100, duration: 400 }}
|
||||
out:fly={{ x: 100, duration: 400 }}
|
||||
>
|
||||
<svelte:component this={icon[notification.type]} class="h-6 w-6 flex-shrink-0" />
|
||||
<span>{notification.message}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import MdiHamburgerMenu from '~icons/mdi/hamburger-menu';
|
||||
</script>
|
||||
|
||||
<div class="topbar absolute left-0 top-0 w-full z-10 flex justify-between bg-zinc-800">
|
||||
<div class="flex gap-2 p-2">
|
||||
<a href="/">
|
||||
<svelte:component this={MdiHamburgerMenu} class="h-8 w-8"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.topbar {
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { openModal, closeAllModals } from 'svelte-modals';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import Firmware from '~icons/tabler/refresh-alert';
|
||||
import Cancel from '~icons/tabler/x';
|
||||
import CloudDown from '~icons/tabler/cloud-download';
|
||||
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let update = false;
|
||||
|
||||
let firmwareVersion: string;
|
||||
let firmwareDownloadLink: string;
|
||||
|
||||
async function getGithubAPI() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'https://api.github.com/repos/' + $page.data.github + '/releases/latest',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
}
|
||||
}
|
||||
);
|
||||
const results = await response.json();
|
||||
if (results.message == "Not Found") {
|
||||
console.error('Error: Could not find releases in the repository');
|
||||
return;
|
||||
}
|
||||
|
||||
update = false;
|
||||
firmwareVersion = '';
|
||||
|
||||
if (compareVersions(results.tag_name, $page.data.features.firmware_version) === 1) {
|
||||
// iterate over assets and find the correct one
|
||||
for (let i = 0; i < results.assets.length; i++) {
|
||||
// check if the asset is of type *.bin
|
||||
if (
|
||||
results.assets[i].name.includes('.bin') &&
|
||||
results.assets[i].name.includes($page.data.features.firmware_built_target)
|
||||
) {
|
||||
update = true;
|
||||
firmwareVersion = results.tag_name;
|
||||
firmwareDownloadLink = results.assets[i].browser_download_url;
|
||||
notifications.info('Firmware update available.', 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function postGithubDownload(url: string) {
|
||||
try {
|
||||
const apiResponse = await fetch('/api/downloadUpdate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ download_url: url })
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if ($page.data.features.download_firmware && (!$page.data.features.security || $user.admin)) {
|
||||
getGithubAPI();
|
||||
const interval = setInterval(
|
||||
async () => {
|
||||
getGithubAPI();
|
||||
},
|
||||
60 * 60 * 1000
|
||||
); // once per hour
|
||||
}
|
||||
});
|
||||
|
||||
function confirmGithubUpdate(url: string) {
|
||||
openModal(ConfirmDialog, {
|
||||
title: 'Confirm flashing new firmware to the device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Update', icon: CloudDown }
|
||||
},
|
||||
onConfirm: () => {
|
||||
postGithubDownload(url);
|
||||
openModal(GithubUpdateDialog, {
|
||||
onConfirm: () => closeAllModals()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if update}
|
||||
<button
|
||||
class="btn btn-square btn-ghost h-9 w-9"
|
||||
on:click={() => confirmGithubUpdate(firmwareDownloadLink)}
|
||||
>
|
||||
<span
|
||||
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
|
||||
>{firmwareVersion}</span
|
||||
>
|
||||
<Firmware class="h-7 w-7" />
|
||||
</button>
|
||||
{/if}
|
||||
+56
-77
@@ -1,51 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { BufferGeometry, CanvasTexture, CircleGeometry, CubicBezierCurve3, Line, LineBasicMaterial, Mesh, MeshBasicMaterial, Vector3, type NormalBufferAttributes } from 'three';
|
||||
import socketService from '$lib/services/socket-service';
|
||||
import { BufferGeometry, Line, LineBasicMaterial, Vector3, type NormalBufferAttributes } from 'three';
|
||||
import uzip from 'uzip';
|
||||
import { model } from '$lib/stores';
|
||||
import { footColor, isEmbeddedApp, location, toeWorldPositions } from '$lib/utilities';
|
||||
import { model, servoAnglesOut } from '$lib/stores';
|
||||
import { footColor, isEmbeddedApp, toeWorldPositions } from '$lib/utilities';
|
||||
import { fileService } from '$lib/services';
|
||||
import { servoAngles, mpu, jointNames } from '$lib/stores';
|
||||
import SceneBuilder from '$lib/sceneBuilder';
|
||||
import { lerp, degToRad } from 'three/src/math/MathUtils';
|
||||
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
|
||||
|
||||
let sceneManager = new SceneBuilder();
|
||||
let canvas: HTMLCanvasElement, streamCanvas: HTMLCanvasElement, stream: HTMLImageElement;
|
||||
let context: CanvasRenderingContext2D, texture: CanvasTexture;
|
||||
export let sky = true
|
||||
export let orbit = false
|
||||
export let panel = true
|
||||
export let debug = false
|
||||
export let ground = true
|
||||
|
||||
let modelAngles: number[] | Int16Array = new Array(12).fill(0);
|
||||
let modelTargetAngles: number[] | Int16Array = new Array(12).fill(0);
|
||||
let sceneManager = new SceneBuilder();
|
||||
let canvas: HTMLCanvasElement
|
||||
|
||||
let currentModelAngles: number[] = new Array(12).fill(0);
|
||||
let modelTargetAngles: number[] = new Array(12).fill(0)
|
||||
let gui_panel: GUI
|
||||
|
||||
let feet_trace = new Array(4).fill([]);
|
||||
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = []
|
||||
|
||||
const videoStream = `//${location}/api/stream`;
|
||||
|
||||
let showStream = false;
|
||||
|
||||
let settings = {
|
||||
'Trace feet':true,
|
||||
'Trace points': 30
|
||||
'Trace feet':debug,
|
||||
'Trace points': 30,
|
||||
'Fix camera on robot': true
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await cacheModelFiles()
|
||||
await createScene();
|
||||
if (!isEmbeddedApp) createPanel();
|
||||
if (!isEmbeddedApp && panel) createPanel();
|
||||
servoAngles.subscribe(updateAnglesFromStore)
|
||||
});
|
||||
|
||||
const updateAnglesFromStore = (angles: number[]) => {
|
||||
if (sceneManager.isDragging) return
|
||||
modelTargetAngles = angles;
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
canvas.remove();
|
||||
});
|
||||
canvas.remove()
|
||||
gui_panel?.destroy()
|
||||
});
|
||||
|
||||
const createPanel = () => {
|
||||
const panel = new GUI({width: 310});
|
||||
panel.close();
|
||||
panel.domElement.id = 'three-gui-panel';
|
||||
gui_panel = new GUI({width: 310});
|
||||
gui_panel.close();
|
||||
gui_panel.domElement.id = 'three-gui-panel';
|
||||
|
||||
const visibility = panel.addFolder('Visualization');
|
||||
const visibility = gui_panel.addFolder('Visualization');
|
||||
visibility.add(settings, 'Trace feet')
|
||||
visibility.add(settings, 'Trace points', 1, 1000, 1)
|
||||
}
|
||||
@@ -63,36 +72,31 @@
|
||||
|
||||
const updateAngles = (name: string, angle: number) => {
|
||||
modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI);
|
||||
socketService.send(
|
||||
JSON.stringify({
|
||||
type: 'kinematic/angle',
|
||||
angle: angle * (180 / Math.PI),
|
||||
id: $jointNames.indexOf(name)
|
||||
})
|
||||
);
|
||||
servoAnglesOut.set(modelTargetAngles)
|
||||
};
|
||||
|
||||
const createScene = async () => {
|
||||
sceneManager
|
||||
.addRenderer({ antialias: true, canvas: canvas, alpha: true })
|
||||
.addRenderer({ antialias: true, canvas, alpha: true })
|
||||
.addPerspectiveCamera({ x: -0.5, y: 0.5, z: 1 })
|
||||
.addOrbitControls(8, 30)
|
||||
.addSky()
|
||||
.addGroundPlane()
|
||||
.addGridHelper({ size: 250, divisions: 125 })
|
||||
.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 } })
|
||||
.addOrbitControls(8, 30, orbit)
|
||||
.addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 0.9 })
|
||||
.addAmbientLight({ color: 0xffffff, intensity: 0.6 })
|
||||
.addFogExp2(0xcccccc, 0.015)
|
||||
.addModel($model)
|
||||
.addDragControl(updateAngles)
|
||||
.handleResize()
|
||||
.fillParent()
|
||||
.addRenderCb(render)
|
||||
.startRenderLoop();
|
||||
|
||||
if (ground) sceneManager
|
||||
.addGroundPlane()
|
||||
.addGridHelper({ size: 30, divisions: 25 })
|
||||
|
||||
|
||||
if (sky) sceneManager.addSky()
|
||||
if (debug) sceneManager.addDragControl(updateAngles)
|
||||
|
||||
addVideoStream();
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const geometry = new BufferGeometry();
|
||||
const material = new LineBasicMaterial({ color: footColor() });
|
||||
const line = new Line(geometry, material);
|
||||
@@ -101,24 +105,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
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 renderTraceLines = (foot_positions: Vector3[]) => {
|
||||
if (!settings['Trace feet']) {
|
||||
if (!feet_trace.length) return
|
||||
@@ -142,33 +128,26 @@
|
||||
|
||||
renderTraceLines(toes)
|
||||
|
||||
if (settings['Fix camera on robot']) {
|
||||
sceneManager.controls.target = robot.position.clone()
|
||||
}
|
||||
|
||||
robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y));
|
||||
robot.rotation.z = lerp(robot.rotation.z, degToRad($mpu.heading + 90), 0.1);
|
||||
modelTargetAngles = $servoAngles;
|
||||
|
||||
handleVideoStream();
|
||||
|
||||
for (let i = 0; i < $jointNames.length; i++) {
|
||||
modelAngles[i] = lerp(
|
||||
currentModelAngles[i] = lerp(
|
||||
(robot.joints[$jointNames[i]].angle as number) * (180 / Math.PI),
|
||||
modelTargetAngles[i],
|
||||
0.1
|
||||
);
|
||||
robot.joints[$jointNames[i]].setJointValue(degToRad(modelAngles[i]));
|
||||
robot.joints[$jointNames[i]].setJointValue(degToRad(currentModelAngles[i]));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={sceneManager.handleResize} />
|
||||
<svelte:window on:resize={sceneManager.fillParent} />
|
||||
|
||||
{#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>
|
||||
<canvas bind:this={canvas}></canvas>
|
||||
@@ -0,0 +1,52 @@
|
||||
import { writable, derived } from 'svelte/store';
|
||||
|
||||
type State = {
|
||||
id: string;
|
||||
type: string;
|
||||
message: string;
|
||||
timeout: number;
|
||||
};
|
||||
|
||||
function createNotificationStore() {
|
||||
const state: State[] = [];
|
||||
const _notifications = writable(state);
|
||||
|
||||
function send(message: string, type = 'info', timeout: number) {
|
||||
_notifications.update((state) => {
|
||||
return [...state, { id: id(), type, message, timeout }];
|
||||
});
|
||||
}
|
||||
|
||||
let timers = [];
|
||||
|
||||
const notifications = derived(_notifications, ($_notifications, set) => {
|
||||
set($_notifications);
|
||||
if ($_notifications.length > 0) {
|
||||
const timer = setTimeout(() => {
|
||||
_notifications.update((state) => {
|
||||
state.shift();
|
||||
return state;
|
||||
});
|
||||
}, $_notifications[0].timeout);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}
|
||||
});
|
||||
const { subscribe } = notifications;
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
send,
|
||||
error: (msg: string, timeout: number) => send(msg, 'error', timeout),
|
||||
warning: (msg: string, timeout: number) => send(msg, 'warning', timeout),
|
||||
info: (msg: string, timeout: number) => send(msg, 'info', timeout),
|
||||
success: (msg: string, timeout: number) => send(msg, 'success', timeout)
|
||||
};
|
||||
}
|
||||
|
||||
function id() {
|
||||
return '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
export const notifications = createNotificationStore();
|
||||
@@ -0,0 +1,37 @@
|
||||
<script>
|
||||
import { flip } from 'svelte/animate';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import error from '~icons/tabler/circle-x';
|
||||
import success from '~icons/tabler/circle-check';
|
||||
import warning from '~icons/tabler/alert-triangle';
|
||||
import info from '~icons/tabler/info-circle';
|
||||
|
||||
export let theme = {
|
||||
error: 'alert-error',
|
||||
success: 'alert-success',
|
||||
warning: 'alert-warning',
|
||||
info: 'alert-info'
|
||||
};
|
||||
|
||||
export let icon = {
|
||||
error: error,
|
||||
success: success,
|
||||
warning: warning,
|
||||
info: info
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="toast toast-end mr-4">
|
||||
{#each $notifications as notification (notification.id)}
|
||||
<div
|
||||
animate:flip={{ duration: 400 }}
|
||||
class="alert animate-none {theme[notification.type]}"
|
||||
in:fly={{ y: 100, duration: 400 }}
|
||||
out:fly={{ x: 100, duration: 400 }}
|
||||
>
|
||||
<svelte:component this={icon[notification.type]} class="h-6 w-6 flex-shrink-0" />
|
||||
<span>{notification.message}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,52 @@
|
||||
import { writable, derived, type Writable } from 'svelte/store';
|
||||
|
||||
type NotificationType = 'info' | 'error' | 'warning' | 'success';
|
||||
|
||||
type State = {
|
||||
id: string;
|
||||
type: NotificationType;
|
||||
message: string;
|
||||
timeout: number;
|
||||
};
|
||||
|
||||
function createNotificationStore() {
|
||||
const state: State[] = [];
|
||||
const _notifications = writable(state);
|
||||
|
||||
function send(message: string, type: NotificationType = 'info', timeout: number) {
|
||||
_notifications.update((state) => {
|
||||
return [...state, { id: id(), type, message, timeout }];
|
||||
});
|
||||
}
|
||||
|
||||
const notifications = derived(_notifications, ($_notifications, set) => {
|
||||
set($_notifications);
|
||||
if ($_notifications.length > 0) {
|
||||
const timer = setTimeout(() => {
|
||||
_notifications.update((state) => {
|
||||
state.shift();
|
||||
return state;
|
||||
});
|
||||
}, $_notifications[0].timeout);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}
|
||||
}) as Writable<State[]>;
|
||||
const { subscribe } = notifications;
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
send,
|
||||
error: (msg: string, timeout: number) => send(msg, 'error', timeout),
|
||||
warning: (msg: string, timeout: number) => send(msg, 'warning', timeout),
|
||||
info: (msg: string, timeout: number) => send(msg, 'info', timeout),
|
||||
success: (msg: string, timeout: number) => send(msg, 'success', timeout)
|
||||
};
|
||||
}
|
||||
|
||||
function id() {
|
||||
return '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
export const notifications = createNotificationStore();
|
||||
@@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
+127
-7
@@ -9,14 +9,134 @@ export interface ControllerInput {
|
||||
|
||||
export type angles = number[] | Int16Array;
|
||||
|
||||
export type AnglesData = {
|
||||
type: 'angles';
|
||||
data: angles;
|
||||
export type WifiStatus = {
|
||||
status: number;
|
||||
local_ip: string;
|
||||
mac_address: string;
|
||||
rssi: number;
|
||||
ssid: string;
|
||||
bssid: string;
|
||||
channel: number;
|
||||
subnet_mask: string;
|
||||
gateway_ip: string;
|
||||
dns_ip_1: string;
|
||||
dns_ip_2?: string;
|
||||
};
|
||||
|
||||
export type LogData = {
|
||||
type: 'log';
|
||||
data: string;
|
||||
export type WifiSettings = {
|
||||
hostname: string;
|
||||
priority_RSSI: boolean;
|
||||
wifi_networks: networkItem[];
|
||||
};
|
||||
|
||||
export type WebSocketJsonMsg = AnglesData | LogData;
|
||||
export type KnownNetworkItem = {
|
||||
ssid: string;
|
||||
password: string;
|
||||
static_ip_config: boolean;
|
||||
local_ip?: string;
|
||||
subnet_mask?: string;
|
||||
gateway_ip?: string;
|
||||
dns_ip_1?: string;
|
||||
dns_ip_2?: string;
|
||||
};
|
||||
|
||||
export type NetworkItem = {
|
||||
rssi: number;
|
||||
ssid: string;
|
||||
bssid: string;
|
||||
channel: number;
|
||||
encryption_type: number;
|
||||
};
|
||||
|
||||
export type ApStatus = {
|
||||
status: number;
|
||||
ip_address: string;
|
||||
mac_address: string;
|
||||
station_num: number;
|
||||
};
|
||||
|
||||
export type ApSettings = {
|
||||
provision_mode: number;
|
||||
ssid: string;
|
||||
password: string;
|
||||
channel: number;
|
||||
ssid_hidden: boolean;
|
||||
max_clients: number;
|
||||
local_ip: string;
|
||||
gateway_ip: string;
|
||||
subnet_mask: string;
|
||||
};
|
||||
|
||||
export type LightState = {
|
||||
led_on: boolean;
|
||||
};
|
||||
|
||||
export type BrokerSettings = {
|
||||
mqtt_path: string;
|
||||
name: string;
|
||||
unique_id: string;
|
||||
};
|
||||
|
||||
export type NTPStatus = {
|
||||
status: number;
|
||||
utc_time: string;
|
||||
local_time: string;
|
||||
server: string;
|
||||
uptime: number;
|
||||
};
|
||||
|
||||
export type NTPSettings = {
|
||||
enabled: boolean;
|
||||
server: string;
|
||||
tz_label: string;
|
||||
tz_format: string;
|
||||
};
|
||||
|
||||
export type Analytics = {
|
||||
max_alloc_heap: number;
|
||||
psram_size: number;
|
||||
free_psram: number;
|
||||
free_heap: number;
|
||||
total_heap: number;
|
||||
min_free_heap: number;
|
||||
core_temp: number;
|
||||
fs_total: number;
|
||||
fs_used: number;
|
||||
uptime: number;
|
||||
};
|
||||
|
||||
export type StaticSystemInformation = {
|
||||
esp_platform: string;
|
||||
firmware_version: string;
|
||||
cpu_freq_mhz: number;
|
||||
cpu_type: string;
|
||||
cpu_rev: number;
|
||||
cpu_cores: number;
|
||||
sketch_size: number;
|
||||
free_sketch_space: number;
|
||||
sdk_version: string;
|
||||
arduino_version: string;
|
||||
flash_chip_size: number;
|
||||
flash_chip_speed: number;
|
||||
cpu_reset_reason: string;
|
||||
};
|
||||
|
||||
export type SystemInformation = Analytics & StaticSystemInformation;
|
||||
|
||||
|
||||
export type MQTTStatus = {
|
||||
enabled: boolean;
|
||||
connected: boolean;
|
||||
client_id: string;
|
||||
last_error: string;
|
||||
};
|
||||
|
||||
export type MQTTSettings = {
|
||||
enabled: boolean;
|
||||
uri: string;
|
||||
username: string;
|
||||
password: string;
|
||||
client_id: string;
|
||||
keep_alive: number;
|
||||
clean_session: boolean;
|
||||
};
|
||||
+42
-20
@@ -11,8 +11,6 @@ import {
|
||||
GridHelper,
|
||||
ArrowHelper,
|
||||
Vector3,
|
||||
LoaderUtils,
|
||||
Object3D,
|
||||
FogExp2,
|
||||
CanvasTexture,
|
||||
type ColorRepresentation,
|
||||
@@ -20,7 +18,8 @@ import {
|
||||
MeshPhongMaterial,
|
||||
EquirectangularReflectionMapping,
|
||||
ACESFilmicToneMapping,
|
||||
MathUtils
|
||||
MathUtils,
|
||||
MeshStandardMaterial
|
||||
} from 'three';
|
||||
import { Sky } from 'three/addons/objects/Sky.js';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
|
||||
@@ -75,7 +74,9 @@ export default class SceneBuilder {
|
||||
public liveStreamTexture: CanvasTexture;
|
||||
private fog: FogExp2;
|
||||
private isLoaded: boolean = false;
|
||||
public isDragging: boolean = false;
|
||||
highlightMaterial: any;
|
||||
sky: Sky;
|
||||
|
||||
constructor() {
|
||||
this.scene = new Scene();
|
||||
@@ -92,14 +93,14 @@ export default class SceneBuilder {
|
||||
this.renderer.shadowMap.type = PCFSoftShadowMap;
|
||||
this.renderer.toneMapping = ACESFilmicToneMapping;
|
||||
this.renderer.toneMappingExposure = 0.85;
|
||||
document.body.appendChild(this.renderer.domElement);
|
||||
if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement);
|
||||
return this;
|
||||
};
|
||||
|
||||
public addSky = () => {
|
||||
const sky = new Sky();
|
||||
sky.scale.setScalar(450000);
|
||||
this.scene.add(sky);
|
||||
this.sky = new Sky();
|
||||
this.sky.scale.setScalar(450000);
|
||||
this.scene.add(this.sky);
|
||||
const effectController = {
|
||||
turbidity: 10,
|
||||
rayleigh: 3,
|
||||
@@ -109,7 +110,7 @@ export default class SceneBuilder {
|
||||
azimuth: 180,
|
||||
exposure: this.renderer.toneMappingExposure
|
||||
};
|
||||
const uniforms = sky.material.uniforms;
|
||||
const uniforms = this.sky.material.uniforms;
|
||||
uniforms['turbidity'].value = effectController.turbidity;
|
||||
uniforms['rayleigh'].value = effectController.rayleigh;
|
||||
uniforms['mieCoefficient'].value = effectController.mieCoefficient;
|
||||
@@ -126,13 +127,14 @@ export default class SceneBuilder {
|
||||
|
||||
public addPerspectiveCamera = (options: position) => {
|
||||
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 ?? 2.7, options.z ?? 0);
|
||||
this.scene.add(this.camera);
|
||||
return this;
|
||||
};
|
||||
|
||||
public addGroundPlane = (options?: position) => {
|
||||
this.ground = new Mesh(new PlaneGeometry(), new ShadowMaterial({ side: 2 }));
|
||||
var planeMaterial = new MeshStandardMaterial({ color: 0x808080, side: 2, opacity: 0.5 });
|
||||
this.ground = new Mesh(new PlaneGeometry(), planeMaterial);
|
||||
this.ground.rotation.x = -Math.PI / 2;
|
||||
this.ground.scale.setScalar(30);
|
||||
this.ground.position.set(options?.x ?? 0, options?.y ?? 0, options?.z ?? 0);
|
||||
@@ -141,10 +143,11 @@ export default class SceneBuilder {
|
||||
return this;
|
||||
};
|
||||
|
||||
public addOrbitControls = (minDistance: number, maxDistance: number) => {
|
||||
public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => {
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.minDistance = minDistance;
|
||||
this.controls.maxDistance = maxDistance;
|
||||
this.controls.autoRotate = autoRotate;
|
||||
this.controls.update();
|
||||
return this;
|
||||
};
|
||||
@@ -158,11 +161,13 @@ export default class SceneBuilder {
|
||||
public addDirectionalLight = (options: directionalLight) => {
|
||||
const directionalLight = new DirectionalLight(options.color, options.intensity);
|
||||
directionalLight.castShadow = true;
|
||||
directionalLight.shadow.mapSize.setScalar(2048);
|
||||
directionalLight.shadow.mapSize.width = 1024;
|
||||
directionalLight.shadow.mapSize.height = 1024;
|
||||
directionalLight.shadow.camera.top = 10;
|
||||
directionalLight.shadow.camera.bottom = -10;
|
||||
directionalLight.shadow.camera.right = 10;
|
||||
directionalLight.shadow.camera.left = -10;
|
||||
directionalLight.shadow.mapSize.set(4096, 4096);
|
||||
|
||||
directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
|
||||
directionalLight.shadow.radius = 5;
|
||||
this.scene.add(directionalLight);
|
||||
return this;
|
||||
};
|
||||
@@ -182,10 +187,20 @@ export default class SceneBuilder {
|
||||
return this;
|
||||
};
|
||||
|
||||
public handleResize = () => {
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
public fillParent = () => {
|
||||
const parentElement = this.renderer.domElement.parentElement;
|
||||
if (parentElement) {
|
||||
const width = parentElement.clientWidth;
|
||||
const height = parentElement.clientHeight;
|
||||
this.handleResize(width, height);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
public handleResize = (width = window.innerWidth, height = window.innerHeight) => {
|
||||
this.renderer.setSize(width, height);
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
this.camera.aspect = window.innerWidth / window.innerHeight;
|
||||
this.camera.aspect = width / height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
return this;
|
||||
};
|
||||
@@ -198,6 +213,7 @@ export default class SceneBuilder {
|
||||
public startRenderLoop = () => {
|
||||
this.renderer.setAnimationLoop(() => {
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
this.controls.update();
|
||||
this.handleRobotShadow();
|
||||
if (this.callback) this.callback();
|
||||
if (!this.liveStreamTexture) return;
|
||||
@@ -282,8 +298,14 @@ export default class SceneBuilder {
|
||||
this.setJointValue(joint.name, angle);
|
||||
updateAngle(joint.name, angle);
|
||||
};
|
||||
dragControls.onDragStart = () => (this.controls.enabled = false);
|
||||
dragControls.onDragEnd = () => (this.controls.enabled = true);
|
||||
dragControls.onDragStart = () => {
|
||||
this.controls.enabled = false;
|
||||
this.isDragging = true;
|
||||
};
|
||||
dragControls.onDragEnd = () => {
|
||||
this.controls.enabled = true;
|
||||
this.isDragging = false;
|
||||
};
|
||||
dragControls.onHover = (joint: URDFMimicJoint) =>
|
||||
this.highlightLinkGeometry(joint, false, highlightMaterial);
|
||||
dragControls.onUnhover = (joint: URDFMimicJoint) =>
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { default as fileService } from './file-service';
|
||||
export { default as socketService } from './socket-service';
|
||||
export { default as resultService } from './result-service';
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { isConnected, socketData } from '$lib/stores';
|
||||
import { Result, Ok } from '$lib/utilities';
|
||||
import { resultService } from '$lib/services';
|
||||
import { type WebSocketJsonMsg } from '$lib/models';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
type WebsocketOutData = string | ArrayBufferLike | Blob | ArrayBufferView;
|
||||
|
||||
// TODO
|
||||
/**
|
||||
* MOVE THE store to a store.ts file
|
||||
*
|
||||
* Make an object on the class that encapsulate all the stores
|
||||
*
|
||||
* Make the handle message function look up the type and set the value, to simplify the code
|
||||
*/
|
||||
|
||||
class SocketService {
|
||||
private socket!: WebSocket;
|
||||
|
||||
constructor() {}
|
||||
|
||||
public connect(url: string): void {
|
||||
this.socket = new WebSocket(url);
|
||||
this.socket.binaryType = 'arraybuffer';
|
||||
this.socket.onopen = () => this.handleConnected();
|
||||
this.socket.onclose = () => this.handleDisconnected();
|
||||
this.socket.onmessage = (event: MessageEvent) =>
|
||||
resultService.handleResult(this.handleMessage(event), 'SocketService');
|
||||
this.socket.onerror = (error: Event) => console.log(error);
|
||||
}
|
||||
|
||||
public send(data: WebsocketOutData): Result<void, string> {
|
||||
if (this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(data);
|
||||
return Ok.void();
|
||||
}
|
||||
return Result.err('The connection is not open');
|
||||
}
|
||||
|
||||
public addPublisher(store: Writable<WebsocketOutData>, type?: string) {
|
||||
const publish = (data: WebsocketOutData) =>
|
||||
this.send(type ? JSON.stringify({ type, data }) : data);
|
||||
store.subscribe(publish);
|
||||
}
|
||||
|
||||
private handleConnected(): void {
|
||||
isConnected.set(true);
|
||||
}
|
||||
|
||||
private handleDisconnected(): void {
|
||||
isConnected.set(false);
|
||||
}
|
||||
|
||||
private getJsonFromMessage(msg: string): Result<WebSocketJsonMsg, string> {
|
||||
try {
|
||||
return Result.ok(JSON.parse(msg) as WebSocketJsonMsg);
|
||||
} catch (error) {
|
||||
return Result.err('Failed to parse socket message', error);
|
||||
}
|
||||
}
|
||||
|
||||
private handleBufferMessage(buffer: ArrayBuffer): Result<void, string> {
|
||||
console.log(buffer);
|
||||
return Ok.void();
|
||||
}
|
||||
|
||||
private handleMessage(event: MessageEvent): Result<void, string> {
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
return this.handleBufferMessage(event.data);
|
||||
}
|
||||
let msgRes = this.getJsonFromMessage(event.data);
|
||||
if (msgRes.isErr()) {
|
||||
return msgRes;
|
||||
}
|
||||
const msg = msgRes.inner;
|
||||
|
||||
if (msg.type === 'log') {
|
||||
socketData.logs.update((entries) => {
|
||||
entries.push(msg.data);
|
||||
return entries;
|
||||
});
|
||||
return Ok.void();
|
||||
} else if (msg.data && msg.type in socketData) {
|
||||
socketData[msg.type].set(msg.data);
|
||||
return Ok.void();
|
||||
}
|
||||
|
||||
return Result.err(`Got invalid msg: ${JSON.stringify(msg)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new SocketService();
|
||||
@@ -0,0 +1,44 @@
|
||||
import { type Analytics } from '$lib/types/models';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let analytics_data = {
|
||||
uptime: <number[]>[],
|
||||
free_heap: <number[]>[],
|
||||
total_heap: <number[]>[],
|
||||
min_free_heap: <number[]>[],
|
||||
max_alloc_heap: <number[]>[],
|
||||
fs_used: <number[]>[],
|
||||
fs_total: <number[]>[],
|
||||
core_temp: <number[]>[]
|
||||
};
|
||||
|
||||
const maxAnalyticsData = 1000; // roughly 33 Minutes of data at 1 update per 2 seconds
|
||||
|
||||
function createAnalytics() {
|
||||
const { subscribe, update } = writable(analytics_data);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
addData: (content: Analytics) => {
|
||||
update((analytics_data) => ({
|
||||
...analytics_data,
|
||||
uptime: [...analytics_data.uptime, content.uptime].slice(-maxAnalyticsData),
|
||||
free_heap: [...analytics_data.free_heap, content.free_heap / 1000].slice(-maxAnalyticsData),
|
||||
total_heap: [...analytics_data.total_heap, content.total_heap / 1000].slice(
|
||||
-maxAnalyticsData
|
||||
),
|
||||
min_free_heap: [...analytics_data.min_free_heap, content.min_free_heap / 1000].slice(
|
||||
-maxAnalyticsData
|
||||
),
|
||||
max_alloc_heap: [...analytics_data.max_alloc_heap, content.max_alloc_heap / 1000].slice(
|
||||
-maxAnalyticsData
|
||||
),
|
||||
fs_used: [...analytics_data.fs_used, content.fs_used / 1000].slice(-maxAnalyticsData),
|
||||
fs_total: [...analytics_data.fs_total, content.fs_total / 1000].slice(-maxAnalyticsData),
|
||||
core_temp: [...analytics_data.core_temp, content.core_temp].slice(-maxAnalyticsData)
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const analytics = createAnalytics();
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './socket-store';
|
||||
export * from './logging-store';
|
||||
export * from './model-store';
|
||||
export * from './socket';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ControllerInput } from '$lib/models';
|
||||
import { persistentStore } from '$lib/utilities';
|
||||
import { persistentStore } from '$lib/utilities/svelte-utilities';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
|
||||
export const emulateModel = writable(true);
|
||||
@@ -12,7 +12,14 @@ export const modes = ['idle', 'rest', 'stand', 'walk'] as const;
|
||||
|
||||
export type Modes = (typeof modes)[number];
|
||||
|
||||
export const mode: Writable<Modes> = writable('idle');
|
||||
export enum ModesEnum {
|
||||
Idle,
|
||||
Rest,
|
||||
Stand,
|
||||
Walk
|
||||
}
|
||||
|
||||
export const mode: Writable<ModesEnum> = writable(ModesEnum.Idle);
|
||||
|
||||
export const outControllerData = writable(new Int8Array([0, 0, 0, 0, 0, 70, 0]));
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import { type angles } from '$lib/models';
|
||||
|
||||
export const isConnected = writable(false);
|
||||
export const servoAngles: Writable<angles> = writable(new Int16Array(12).fill(0));
|
||||
export const servoAnglesOut: Writable<number[]> = writable([
|
||||
0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
|
||||
]);
|
||||
export const servoAngles: Writable<number[]> = writable([
|
||||
0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
|
||||
]);
|
||||
export const logs = writable([] as string[]);
|
||||
export const battery = writable({});
|
||||
export const mpu = writable({ heading: 0 });
|
||||
export const distances = writable({});
|
||||
export const settings = writable({});
|
||||
export const systemInfo = writable({} as number);
|
||||
|
||||
export interface socketDataCollection {
|
||||
angles: Writable<angles>;
|
||||
@@ -16,8 +18,6 @@ export interface socketDataCollection {
|
||||
battery: Writable<unknown>;
|
||||
mpu: Writable<unknown>;
|
||||
distances: Writable<unknown>;
|
||||
settings: Writable<unknown>;
|
||||
systemInfo: Writable<unknown>;
|
||||
}
|
||||
|
||||
export const socketData = {
|
||||
@@ -25,7 +25,5 @@ export const socketData = {
|
||||
logs,
|
||||
battery,
|
||||
mpu,
|
||||
distances,
|
||||
settings,
|
||||
systemInfo
|
||||
distances
|
||||
};
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
function createWebSocket() {
|
||||
let listeners = new Map<string, Set<(data?: unknown) => void>>();
|
||||
const { subscribe, set } = writable(false);
|
||||
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const;
|
||||
type SocketEvent = (typeof socketEvents)[number];
|
||||
let unresponsiveTimeoutId: number;
|
||||
let reconnectTimeoutId: number;
|
||||
let ws: WebSocket;
|
||||
let socketUrl: string | URL;
|
||||
|
||||
function init(url: string | URL) {
|
||||
socketUrl = url;
|
||||
connect();
|
||||
}
|
||||
|
||||
function disconnect(reason: SocketEvent, event?: Event) {
|
||||
ws.close();
|
||||
set(false);
|
||||
clearTimeout(unresponsiveTimeoutId);
|
||||
clearTimeout(reconnectTimeoutId);
|
||||
listeners.get(reason)?.forEach((listener) => listener(event));
|
||||
reconnectTimeoutId = setTimeout(connect, 1000);
|
||||
}
|
||||
|
||||
function connect() {
|
||||
ws = new WebSocket(socketUrl);
|
||||
ws.onopen = (ev) => {
|
||||
set(true);
|
||||
clearTimeout(reconnectTimeoutId);
|
||||
listeners.get('open')?.forEach((listener) => listener(ev));
|
||||
for (const event of listeners.keys()) {
|
||||
if (socketEvents.includes(event as SocketEvent)) continue;
|
||||
sendEvent('subscribe', event);
|
||||
}
|
||||
};
|
||||
ws.onmessage = (message) => {
|
||||
resetUnresponsiveCheck();
|
||||
let data = message.data;
|
||||
|
||||
if (data instanceof ArrayBuffer) {
|
||||
listeners.get('binary')?.forEach((listener) => listener(data));
|
||||
return;
|
||||
}
|
||||
listeners.get('message')?.forEach((listener) => listener(data));
|
||||
try {
|
||||
data = JSON.parse(message.data);
|
||||
} catch (error) {
|
||||
listeners.get('error')?.forEach((listener) => listener(error));
|
||||
return;
|
||||
}
|
||||
listeners.get('json')?.forEach((listener) => listener(data));
|
||||
const [event, payload] = data;
|
||||
if (event) listeners.get(event)?.forEach((listener) => listener(payload));
|
||||
};
|
||||
ws.onerror = (ev) => disconnect('error', ev);
|
||||
ws.onclose = (ev) => disconnect('close', ev);
|
||||
}
|
||||
|
||||
function unsubscribe(event: string, listener?: (data: any) => void) {
|
||||
let eventListeners = listeners.get(event);
|
||||
if (!eventListeners) return;
|
||||
|
||||
if (!eventListeners.size) {
|
||||
sendEvent('unsubscribe', event);
|
||||
}
|
||||
if (listener) {
|
||||
eventListeners?.delete(listener);
|
||||
} else {
|
||||
listeners.delete(event);
|
||||
}
|
||||
}
|
||||
|
||||
function resetUnresponsiveCheck() {
|
||||
clearTimeout(unresponsiveTimeoutId);
|
||||
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), 2000);
|
||||
}
|
||||
|
||||
function send(msg: unknown) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
ws.send(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
function sendEvent(event: string, data: unknown) {
|
||||
send({ event, data });
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
send,
|
||||
sendEvent,
|
||||
init,
|
||||
on: <T>(event: string, listener: (data: T) => void): (() => void) => {
|
||||
let eventListeners = listeners.get(event);
|
||||
if (!eventListeners) {
|
||||
if (!socketEvents.includes(event as SocketEvent)) {
|
||||
sendEvent('subscribe', event);
|
||||
}
|
||||
eventListeners = new Set();
|
||||
listeners.set(event, eventListeners);
|
||||
}
|
||||
eventListeners.add(listener as (data: any) => void);
|
||||
|
||||
return () => {
|
||||
unsubscribe(event, listener);
|
||||
};
|
||||
},
|
||||
off: (event: string, listener?: (data: any) => void) => {
|
||||
unsubscribe(event, listener);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const socket = createWebSocket();
|
||||
@@ -0,0 +1,51 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let telemetry_data = {
|
||||
rssi: {
|
||||
rssi: 0,
|
||||
disconnected: true
|
||||
},
|
||||
battery: {
|
||||
soc: 100,
|
||||
charging: false
|
||||
},
|
||||
download_ota: {
|
||||
status: 'none',
|
||||
progress: 0,
|
||||
error: ''
|
||||
}
|
||||
};
|
||||
|
||||
function createTelemetry() {
|
||||
const { subscribe, set, update } = writable(telemetry_data);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
setRSSI: (data: string) => {
|
||||
if (!isNaN(Number(data))) {
|
||||
update((telemetry_data) => ({
|
||||
...telemetry_data,
|
||||
rssi: { rssi: Number(data), disconnected: false }
|
||||
}));
|
||||
} else {
|
||||
update((telemetry_data) => ({ ...telemetry_data, rssi: { rssi: 0, disconnected: true } }));
|
||||
}
|
||||
},
|
||||
setBattery: (data: string) => {
|
||||
const content = JSON.parse(data);
|
||||
update((telemetry_data) => ({
|
||||
...telemetry_data,
|
||||
battery: { soc: content.soc, charging: content.charging }
|
||||
}));
|
||||
},
|
||||
setDownloadOTA: (data: string) => {
|
||||
const content = JSON.parse(data);
|
||||
update((telemetry_data) => ({
|
||||
...telemetry_data,
|
||||
download_ota: { status: content.status, progress: content.progress, error: content.error }
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const telemetry = createTelemetry();
|
||||
@@ -0,0 +1,55 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
|
||||
export type userProfile = {
|
||||
username: string;
|
||||
admin: boolean;
|
||||
bearer_token: string;
|
||||
};
|
||||
|
||||
type decodedJWT = {
|
||||
username: string;
|
||||
admin: boolean;
|
||||
};
|
||||
|
||||
let empty = {
|
||||
username: '',
|
||||
admin: false,
|
||||
bearer_token: ''
|
||||
};
|
||||
|
||||
function createStore() {
|
||||
const { subscribe, set } = writable(empty);
|
||||
|
||||
// retrieve store from sessionStorage / localStorage if available
|
||||
const userdata = localStorage.getItem('user');
|
||||
if (userdata) {
|
||||
set(JSON.parse(userdata));
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
init: (access_token: string) => {
|
||||
const decoded: decodedJWT = jwtDecode(access_token);
|
||||
const userdata = {
|
||||
bearer_token: access_token,
|
||||
username: decoded.username,
|
||||
admin: decoded.admin
|
||||
};
|
||||
set(userdata);
|
||||
// persist store in sessionStorage / localStorage
|
||||
localStorage.setItem('user', JSON.stringify(userdata));
|
||||
},
|
||||
invalidate: () => {
|
||||
console.log('Log out user');
|
||||
set(empty);
|
||||
// remove localStorage "user"
|
||||
localStorage.removeItem('user');
|
||||
// redirect to login page
|
||||
goto('/');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const user = createStore();
|
||||
@@ -0,0 +1,113 @@
|
||||
export type WifiStatus = {
|
||||
status: number;
|
||||
local_ip: string;
|
||||
mac_address: string;
|
||||
rssi: number;
|
||||
ssid: string;
|
||||
bssid: string;
|
||||
channel: number;
|
||||
subnet_mask: string;
|
||||
gateway_ip: string;
|
||||
dns_ip_1: string;
|
||||
dns_ip_2?: string;
|
||||
};
|
||||
|
||||
export type WifiSettings = {
|
||||
hostname: string;
|
||||
priority_RSSI: boolean;
|
||||
wifi_networks: networkItem[];
|
||||
};
|
||||
|
||||
export type KnownNetworkItem = {
|
||||
ssid: string;
|
||||
password: string;
|
||||
static_ip_config: boolean;
|
||||
local_ip?: string;
|
||||
subnet_mask?: string;
|
||||
gateway_ip?: string;
|
||||
dns_ip_1?: string;
|
||||
dns_ip_2?: string;
|
||||
};
|
||||
|
||||
export type NetworkItem = {
|
||||
rssi: number;
|
||||
ssid: string;
|
||||
bssid: string;
|
||||
channel: number;
|
||||
encryption_type: number;
|
||||
};
|
||||
|
||||
export type ApStatus = {
|
||||
status: number;
|
||||
ip_address: string;
|
||||
mac_address: string;
|
||||
station_num: number;
|
||||
};
|
||||
|
||||
export type ApSettings = {
|
||||
provision_mode: number;
|
||||
ssid: string;
|
||||
password: string;
|
||||
channel: number;
|
||||
ssid_hidden: boolean;
|
||||
max_clients: number;
|
||||
local_ip: string;
|
||||
gateway_ip: string;
|
||||
subnet_mask: string;
|
||||
};
|
||||
|
||||
export type LightState = {
|
||||
led_on: boolean;
|
||||
};
|
||||
|
||||
export type BrokerSettings = {
|
||||
mqtt_path: string;
|
||||
name: string;
|
||||
unique_id: string;
|
||||
};
|
||||
|
||||
export type NTPStatus = {
|
||||
status: number;
|
||||
utc_time: string;
|
||||
local_time: string;
|
||||
server: string;
|
||||
uptime: number;
|
||||
};
|
||||
|
||||
export type NTPSettings = {
|
||||
enabled: boolean;
|
||||
server: string;
|
||||
tz_label: string;
|
||||
tz_format: string;
|
||||
};
|
||||
|
||||
export type Analytics = {
|
||||
max_alloc_heap: number;
|
||||
psram_size: number;
|
||||
free_psram: number;
|
||||
free_heap: number;
|
||||
total_heap: number;
|
||||
min_free_heap: number;
|
||||
core_temp: number;
|
||||
fs_total: number;
|
||||
fs_used: number;
|
||||
uptime: number;
|
||||
};
|
||||
|
||||
export type StaticSystemInformation = {
|
||||
esp_platform: string;
|
||||
firmware_version: string;
|
||||
cpu_freq_mhz: number;
|
||||
cpu_type: string;
|
||||
cpu_rev: number;
|
||||
cpu_cores: number;
|
||||
sketch_size: number;
|
||||
free_sketch_space: number;
|
||||
sdk_version: string;
|
||||
arduino_version: string;
|
||||
flash_chip_size: number;
|
||||
flash_chip_speed: number;
|
||||
cpu_reset_reason: string;
|
||||
};
|
||||
|
||||
export type SystemInformation = Analytics & StaticSystemInformation;
|
||||
@@ -1,10 +1,9 @@
|
||||
export const hostname = window.location.hostname;
|
||||
export const hostname = 'localhost'; //window.location.hostname;
|
||||
|
||||
export const isSecure = window.location.protocol === 'https:';
|
||||
export const isSecure = true; // window.location.protocol === 'https:';
|
||||
|
||||
export const location = import.meta.env.VITE_API_URL.replace('hostname', hostname);
|
||||
export const location = 'localhost:5173'; //window.location; //import.meta.env.VITE_API_URL.replace('hostname', hostname);
|
||||
|
||||
const socketScheme = isSecure ? 'wss://' : 'ws://';
|
||||
|
||||
export const socketLocation =
|
||||
socketScheme + import.meta.env.VITE_SOCKET_URL.replace('hostname', hostname);
|
||||
export const socketLocation = socketScheme + location; // import.meta.env.VITE_SOCKET_URL.replace('hostname', hostname);
|
||||
|
||||
@@ -33,7 +33,7 @@ export const loadModelAsync = async (
|
||||
resolve(Result.err('Failed to load model', error));
|
||||
}
|
||||
},
|
||||
(error) => reject(error)
|
||||
(error) => resolve(Result.err('Failed to load model', error))
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export const isEmbeddedApp = import.meta.env.VITE_EMBEDDED_BUILD === 'true';
|
||||
|
||||
export const persistentStore = (key: string, initialValue: any) => {
|
||||
const savedValue = JSON.parse(localStorage.getItem(key) as string);
|
||||
const savedValue = browser ? JSON.parse(localStorage.getItem(key) as string) : null;
|
||||
const data = savedValue !== null ? savedValue : initialValue;
|
||||
const store = writable(data);
|
||||
|
||||
store.subscribe((value) => {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
browser && localStorage.setItem(key, JSON.stringify(value));
|
||||
});
|
||||
|
||||
return store;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import './app.css';
|
||||
import './index.css';
|
||||
import App from './App.svelte';
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js');
|
||||
});
|
||||
}
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById('app') as HTMLElement
|
||||
});
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
goto('/');
|
||||
}, 3000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center items-center w-full h-full">
|
||||
<h1 class="text-4xl">404 - Page not found</h1>
|
||||
<p>You will be redirected to the home page in 3 seconds</p>
|
||||
</div>
|
||||
@@ -0,0 +1,148 @@
|
||||
<script lang="ts">
|
||||
import type { LayoutData } from './$types';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { telemetry } from '$lib/stores/telemetry';
|
||||
import { analytics } from '$lib/stores/analytics';
|
||||
import type { userProfile } from '$lib/stores/user';
|
||||
import { page } from '$app/stores';
|
||||
import { Modals, closeModal } from 'svelte-modals';
|
||||
import Toast from '$lib/components/toasts/Toast.svelte';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { fade } from 'svelte/transition';
|
||||
import '../app.css';
|
||||
import Menu from './menu.svelte';
|
||||
import Statusbar from './statusbar.svelte';
|
||||
import Login from './login.svelte';
|
||||
import { mode, outControllerData, servoAnglesOut, socket } from '$lib/stores';
|
||||
import type { Analytics } from '$lib/types/models';
|
||||
|
||||
export let data: LayoutData;
|
||||
|
||||
onMount(async () => {
|
||||
if ($user.bearer_token !== '') {
|
||||
await validateUser($user);
|
||||
}
|
||||
const ws_token = $page.data.features.security ? '?access_token=' + $user.bearer_token : '';
|
||||
socket.init(`ws://${window.location.host}/ws/events${ws_token}`);
|
||||
|
||||
addEventListeners();
|
||||
|
||||
outControllerData.subscribe((data) => socket.sendEvent("input", data));
|
||||
mode.subscribe((data) => socket.sendEvent("mode", data));
|
||||
servoAnglesOut.subscribe((data) => socket.sendEvent("angles", data));
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
removeEventListeners();
|
||||
});
|
||||
|
||||
const addEventListeners = () => {
|
||||
socket.on('open', handleOpen);
|
||||
socket.on('close', handleClose);
|
||||
socket.on('error', handleError);
|
||||
socket.on('rssi', handleNetworkStatus);
|
||||
socket.on('infoToast', handleInfoToast);
|
||||
socket.on('successToast', handleSuccessToast);
|
||||
socket.on('warningToast', handleWarningToast);
|
||||
socket.on('errorToast', handleErrorToast);
|
||||
if ($page.data.features.analytics) socket.on('analytics', handleAnalytics);
|
||||
if ($page.data.features.battery) socket.on('battery', handleBattery);
|
||||
if ($page.data.features.download_firmware) socket.on('otastatus', handleOAT);
|
||||
};
|
||||
|
||||
const removeEventListeners = () => {
|
||||
socket.off('analytics', handleAnalytics);
|
||||
socket.off('open', handleOpen);
|
||||
socket.off('close', handleClose);
|
||||
socket.off('rssi', handleNetworkStatus);
|
||||
socket.off('infoToast', handleInfoToast);
|
||||
socket.off('successToast', handleSuccessToast);
|
||||
socket.off('warningToast', handleWarningToast);
|
||||
socket.off('errorToast', handleErrorToast);
|
||||
socket.off('battery', handleBattery);
|
||||
socket.off('otastatus', handleOAT);
|
||||
};
|
||||
|
||||
async function validateUser(userdata: userProfile) {
|
||||
try {
|
||||
const response = await fetch('/api/verifyAuthorization', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + userdata.bearer_token,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
user.invalidate();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpen = () => {
|
||||
notifications.success('Connection to device established', 5000);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
notifications.error('Connection to device lost', 5000);
|
||||
telemetry.setRSSI('lost');
|
||||
};
|
||||
|
||||
const handleError = (data: any) => console.error(data);
|
||||
|
||||
const handleInfoToast = (data: string) => notifications.info(data, 5000);
|
||||
const handleWarningToast = (data: string) => notifications.warning(data, 5000);
|
||||
const handleErrorToast = (data: string) => notifications.error(data, 5000);
|
||||
const handleSuccessToast = (data: string) => notifications.success(data, 5000);
|
||||
|
||||
const handleAnalytics = (data: Analytics) => analytics.addData(data);
|
||||
|
||||
const handleNetworkStatus = (data: string) => telemetry.setRSSI(data);
|
||||
|
||||
const handleBattery = (data: string) => telemetry.setBattery(data);
|
||||
|
||||
const handleOAT = (data: string) => telemetry.setDownloadOTA(data);
|
||||
|
||||
let menuOpen = false;
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$page.data.title}</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if $page.data.features.security && $user.bearer_token === ''}
|
||||
<Login />
|
||||
{:else}
|
||||
<div class="drawer">
|
||||
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
|
||||
<div class="drawer-content flex flex-col">
|
||||
<!-- Status bar content here -->
|
||||
<Statusbar />
|
||||
|
||||
<!-- Main page content here -->
|
||||
<slot />
|
||||
</div>
|
||||
<!-- Side Navigation -->
|
||||
<div class="drawer-side z-30 shadow-lg">
|
||||
<label for="main-menu" class="drawer-overlay" />
|
||||
<Menu
|
||||
on:menuClicked={() => menuOpen = false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Modals>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
slot="backdrop"
|
||||
class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur"
|
||||
transition:fade
|
||||
on:click={closeModal}
|
||||
/>
|
||||
</Modals>
|
||||
|
||||
<Toast />
|
||||
@@ -0,0 +1,40 @@
|
||||
import { jointNames, model } from '$lib/stores';
|
||||
import { loadModelAsync } from '$lib/utilities/model-utilities';
|
||||
|
||||
export const prerender = true;
|
||||
export const ssr = false;
|
||||
|
||||
const registerFetchIntercept = async () => {
|
||||
const { fetch: originalFetch } = window;
|
||||
const fileService = (await import('$lib/services/file-service')).default;
|
||||
window.fetch = async (resource, config) => {
|
||||
let url = resource instanceof Request ? resource.url : resource.toString();
|
||||
let file = await fileService.getFile(url);
|
||||
return file.isOk() ? new Response(file.inner) : originalFetch(resource, config);
|
||||
};
|
||||
};
|
||||
|
||||
const loadModelFiles = async () => {
|
||||
const modelRes = await loadModelAsync('/spot_micro.urdf.xacro');
|
||||
if (modelRes.isOk()) {
|
||||
const [urdf, JOINT_NAME] = modelRes.inner;
|
||||
jointNames.set(JOINT_NAME);
|
||||
model.set(urdf);
|
||||
} else {
|
||||
console.error(modelRes.inner, { exception: modelRes.exception });
|
||||
}
|
||||
};
|
||||
|
||||
export const load = async ({ fetch }) => {
|
||||
await registerFetchIntercept();
|
||||
await loadModelFiles();
|
||||
const result = await fetch('/api/features');
|
||||
const features = await result.json();
|
||||
return {
|
||||
features,
|
||||
title: 'Spot micro controller',
|
||||
github: 'runeharlyk/SpotMicroESP32-Leika',
|
||||
app_name: 'Spot Micro Controller',
|
||||
copyright: '2024 Rune Harlyk'
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import Visualization from '$lib/components/Visualization.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<div class="hero bg-base-100 h-screen">
|
||||
<div class="card md:card-side bg-base-200 shadow-2xl flex justify-center items-center">
|
||||
<div class="w-64 h-64">
|
||||
<Visualization sky={false} orbit panel={false} ground={false}/>
|
||||
</div>
|
||||
<div class="card-body w-80">
|
||||
<h2 class="card-title text-center text-2xl">Welcome to {data.app_name}</h2>
|
||||
<p class="py-6 text-center"></p>
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
href="/controller"
|
||||
on:click={() => notifications.success('You did it!', 1000)}>Begin</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,15 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Stream from '$components/Views/Stream.svelte';
|
||||
import Model from '$components/Views/Model.svelte';
|
||||
import Controls from '$components/Controls.svelte';
|
||||
import { emulateModel } from '$lib/stores';
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center items-center w-full h-full">
|
||||
{#if $emulateModel}
|
||||
<Model />
|
||||
{:else}
|
||||
<Stream />
|
||||
{/if}
|
||||
<Controls />
|
||||
</div>
|
||||
@@ -1,62 +0,0 @@
|
||||
<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,
|
||||
InformationCircle,
|
||||
BookOpen,
|
||||
AdjustmentsVertical,
|
||||
Cog6Tooth
|
||||
} 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>
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export const load = (async () => {
|
||||
goto('/');
|
||||
return;
|
||||
}) satisfies PageLoad;
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from '../$types';
|
||||
import MQTT from './MQTT.svelte';
|
||||
import MqttConfig from './MQTTConfig.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="mx-0 my-1 flex flex-col space-y-4
|
||||
sm:mx-8 sm:my-8"
|
||||
>
|
||||
<MQTT />
|
||||
<MqttConfig />
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
return {
|
||||
title: "MQTT"
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
@@ -0,0 +1,288 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import InputPassword from '$lib/components/InputPassword.svelte';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { page } from '$app/stores';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import Collapsible from '$lib/components/Collapsible.svelte';
|
||||
import MQTT from '~icons/tabler/topology-star-3';
|
||||
import Client from '~icons/tabler/robot';
|
||||
import type { MQTTSettings, MQTTStatus } from '$lib/models';
|
||||
|
||||
let mqttSettings: MQTTSettings;
|
||||
let mqttStatus: MQTTStatus;
|
||||
|
||||
let formField: any;
|
||||
|
||||
async function getMQTTStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/mqttStatus', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
mqttStatus = await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
return mqttStatus;
|
||||
}
|
||||
|
||||
async function getMQTTSettings() {
|
||||
try {
|
||||
const response = await fetch('/api/mqttSettings', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
mqttSettings = await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
return mqttSettings;
|
||||
}
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
getMQTTStatus();
|
||||
}, 5000);
|
||||
|
||||
onDestroy(() => clearInterval(interval));
|
||||
|
||||
onMount(() => {
|
||||
if (!$page.data.features.security || $user.admin) {
|
||||
getMQTTSettings();
|
||||
}
|
||||
});
|
||||
|
||||
let formErrors = {
|
||||
host: false,
|
||||
port: false,
|
||||
keep_alive: false,
|
||||
topic_length: false
|
||||
};
|
||||
|
||||
async function postMQTTSettings(data: MQTTSettings) {
|
||||
try {
|
||||
const response = await fetch('/api/mqttSettings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (response.status == 200) {
|
||||
notifications.success('MQTT settings updated.', 3000);
|
||||
mqttSettings = await response.json();
|
||||
} else {
|
||||
notifications.error('User not authorized.', 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
function handleSubmitMQTT() {
|
||||
let valid = true;
|
||||
|
||||
// Validate Server URI
|
||||
const regexExpURL =
|
||||
/(([-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4})|(\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b))(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/i;
|
||||
|
||||
if (!regexExpURL.test(mqttSettings.uri)) {
|
||||
valid = false;
|
||||
formErrors.host = true;
|
||||
} else {
|
||||
formErrors.host = false;
|
||||
}
|
||||
|
||||
// Validate if port is a number and within the right range
|
||||
let keepalive = Number(mqttSettings.keep_alive);
|
||||
if (1 <= keepalive && keepalive <= 600) {
|
||||
formErrors.keep_alive = false;
|
||||
} else {
|
||||
formErrors.keep_alive = true;
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// Submit JSON to REST API
|
||||
if (valid) {
|
||||
postMQTTSettings(mqttSettings);
|
||||
//alert('Form Valid');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
<MQTT slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">MQTT</span>
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getMQTTStatus()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div
|
||||
class="mask mask-hexagon h-auto w-10 {mqttStatus.connected === true
|
||||
? 'bg-success'
|
||||
: 'bg-error'}"
|
||||
>
|
||||
<MQTT
|
||||
class="h-auto w-full scale-75 {mqttStatus.connected === true
|
||||
? 'text-success-content'
|
||||
: 'text-error-content'}"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Status</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{#if mqttStatus.connected}
|
||||
Connected
|
||||
{:else if !mqttStatus.enabled}
|
||||
MQTT Disabled
|
||||
{:else}
|
||||
{mqttStatus.last_error}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||
<Client class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Client ID</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{mqttStatus.client_id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
{#if !$page.data.features.security || $user.admin}
|
||||
<Collapsible open={false} class="shadow-lg" on:closed={getMQTTSettings}>
|
||||
<span slot="title">Change MQTT Settings</span>
|
||||
|
||||
<form on:submit|preventDefault={handleSubmitMQTT} novalidate bind:this={formField}>
|
||||
<div class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2">
|
||||
<!-- Enable -->
|
||||
<label class="label inline-flex cursor-pointer content-end justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={mqttSettings.enabled}
|
||||
class="checkbox checkbox-primary"
|
||||
/>
|
||||
<span>Enable MQTT</span>
|
||||
</label>
|
||||
<div class="hidden sm:block" />
|
||||
<!-- URI -->
|
||||
<div class="sm:col-span-2">
|
||||
<label class="label" for="host">
|
||||
<span class="label-text text-md">URI</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.host
|
||||
? 'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={mqttSettings.uri}
|
||||
id="host"
|
||||
min="3"
|
||||
max="64"
|
||||
required
|
||||
/>
|
||||
<label class="label" for="host">
|
||||
<span class="label-text-alt text-error {formErrors.host ? '' : 'hidden'}"
|
||||
>Must be a valid URI</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<!-- Username -->
|
||||
<div>
|
||||
<label class="label" for="user">
|
||||
<span class="label-text text-md">Username</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={mqttSettings.username}
|
||||
id="user"
|
||||
/>
|
||||
</div>
|
||||
<!-- Password -->
|
||||
<div>
|
||||
<label class="label" for="pwd">
|
||||
<span class="label-text text-md">Password</span>
|
||||
</label>
|
||||
<InputPassword bind:value={mqttSettings.password} id="pwd" />
|
||||
</div>
|
||||
<!-- Client ID -->
|
||||
<div>
|
||||
<label class="label" for="clientid">
|
||||
<span class="label-text text-md">Client ID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={mqttSettings.client_id}
|
||||
id="clientid"
|
||||
/>
|
||||
</div>
|
||||
<!-- Keep Alive -->
|
||||
<div>
|
||||
<label class="label" for="keepalive">
|
||||
<span class="label-text text-md">Keep Alive</span>
|
||||
</label>
|
||||
<label for="keepalive" class="input-group">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="600"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.keep_alive
|
||||
? 'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={mqttSettings.keep_alive}
|
||||
id="keepalive"
|
||||
required
|
||||
/>
|
||||
<span>Seconds</span>
|
||||
</label>
|
||||
<label for="keepalive" class="label"
|
||||
><span class="label-text-alt text-error {formErrors.keep_alive ? '' : 'hidden'}"
|
||||
>Must be between 1 and 600 seconds</span
|
||||
></label
|
||||
>
|
||||
</div>
|
||||
<!-- Clean Session -->
|
||||
<label class="label inline-flex cursor-pointer content-end justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={mqttSettings.clean_session}
|
||||
class="checkbox checkbox-primary"
|
||||
/>
|
||||
<span class="">Clean Session?</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="divider mb-2 mt-0" />
|
||||
<div class="mx-4 flex flex-wrap justify-end gap-2">
|
||||
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</Collapsible>
|
||||
{/if}
|
||||
</SettingsCard>
|
||||
@@ -0,0 +1,187 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { page } from '$app/stores';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import MQTT from '~icons/tabler/topology-star-3';
|
||||
import Info from '~icons/tabler/info-circle';
|
||||
import type { BrokerSettings } from '$lib/types/models';
|
||||
|
||||
let brokerSettings: BrokerSettings;
|
||||
|
||||
let formField: any;
|
||||
|
||||
async function getBrokerSettings() {
|
||||
try {
|
||||
const response = await fetch('/api/brokerSettings', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
brokerSettings = await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let formErrors = {
|
||||
uid: false,
|
||||
path: false,
|
||||
name: false
|
||||
};
|
||||
|
||||
async function postBrokerSettings() {
|
||||
try {
|
||||
const response = await fetch('/api/brokerSettings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(brokerSettings)
|
||||
});
|
||||
if (response.status == 200) {
|
||||
notifications.success('Broker settings updated.', 3000);
|
||||
brokerSettings = await response.json();
|
||||
} else {
|
||||
notifications.error('User not authorized.', 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
function handleSubmitBroker() {
|
||||
let valid = true;
|
||||
|
||||
// Validate unique ID
|
||||
if (brokerSettings.unique_id.length < 3 || brokerSettings.unique_id.length > 32) {
|
||||
valid = false;
|
||||
formErrors.uid = true;
|
||||
} else {
|
||||
formErrors.uid = false;
|
||||
}
|
||||
|
||||
// Validate name
|
||||
if (brokerSettings.name.length < 3 || brokerSettings.name.length > 32) {
|
||||
valid = false;
|
||||
formErrors.name = true;
|
||||
} else {
|
||||
formErrors.name = false;
|
||||
}
|
||||
// Validate MQTT Path
|
||||
if (brokerSettings.mqtt_path.length > 64) {
|
||||
valid = false;
|
||||
formErrors.path = true;
|
||||
} else {
|
||||
formErrors.path = false;
|
||||
}
|
||||
|
||||
// Submit JSON to REST API
|
||||
if (valid) {
|
||||
postBrokerSettings();
|
||||
//alert('Form Valid');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={true} open={false}>
|
||||
<MQTT slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">MQTT Broker Settings</span>
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getBrokerSettings()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<form
|
||||
on:submit|preventDefault={handleSubmitBroker}
|
||||
novalidate
|
||||
bind:this={formField}
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<div class="alert alert-info my-2 shadow-lg">
|
||||
<Info class="h-6 w-6 flex-shrink-0 stroke-current" />
|
||||
<span
|
||||
>The LED is controllable via MQTT with the demo project designed to work with Home
|
||||
Assistant's auto discovery feature.</span
|
||||
>
|
||||
</div>
|
||||
<div class="grid w-full grid-cols-1 content-center gap-x-4 px-4">
|
||||
<div>
|
||||
<label class="label" for="uid">
|
||||
<span class="label-text text-md">Unique ID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.uid
|
||||
? 'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={brokerSettings.unique_id}
|
||||
id="uid"
|
||||
min="3"
|
||||
max="32"
|
||||
required
|
||||
/>
|
||||
<label class="label" for="uid">
|
||||
<span class="label-text-alt text-error {formErrors.uid ? '' : 'hidden'}"
|
||||
>Unique ID must be between 3 and 32 characters long</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="name">
|
||||
<span class="label-text text-md">Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.name
|
||||
? 'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={brokerSettings.name}
|
||||
id="name"
|
||||
min="3"
|
||||
max="32"
|
||||
required
|
||||
/>
|
||||
<label class="label" for="name">
|
||||
<span class="label-text-alt text-error {formErrors.name ? '' : 'hidden'}"
|
||||
>Name must be between 3 and 32 characters long</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="path">
|
||||
<span class="label-text text-md">MQTT Path</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.path
|
||||
? 'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={brokerSettings.mqtt_path}
|
||||
id="path"
|
||||
min="0"
|
||||
max="64"
|
||||
required
|
||||
/>
|
||||
<label class="label" for="path">
|
||||
<span class="label-text-alt text-error {formErrors.path ? '' : 'hidden'}"
|
||||
>MQTT path is limited to 64 characters</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider mb-2 mt-0" />
|
||||
<div class="mx-4 flex flex-wrap justify-end gap-2">
|
||||
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
{/await}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from '../$types';
|
||||
import NTP from './NTP.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
|
||||
<NTP />
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
return {
|
||||
title: 'NTP'
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
@@ -0,0 +1,288 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import Collapsible from '$lib/components/Collapsible.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { page } from '$app/stores';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { TIME_ZONES } from './timezones';
|
||||
import NTP from '~icons/tabler/clock-check';
|
||||
import Server from '~icons/tabler/server';
|
||||
import Clock from '~icons/tabler/clock';
|
||||
import UTC from '~icons/tabler/clock-pin';
|
||||
import Stopwatch from '~icons/tabler/24-hours';
|
||||
import type { NTPSettings, NTPStatus } from '$lib/types/models';
|
||||
|
||||
let ntpSettings: NTPSettings;
|
||||
let ntpStatus: NTPStatus;
|
||||
|
||||
async function getNTPStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/ntpStatus', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
ntpStatus = await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async function getNTPSettings() {
|
||||
try {
|
||||
const response = await fetch('/api/ntpSettings', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
ntpSettings = await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
getNTPStatus();
|
||||
}, 5000);
|
||||
|
||||
onDestroy(() => clearInterval(interval));
|
||||
|
||||
onMount(() => {
|
||||
if (!$page.data.features.security || $user.admin) {
|
||||
getNTPSettings();
|
||||
}
|
||||
});
|
||||
|
||||
let formField: any;
|
||||
|
||||
let formErrors = {
|
||||
server: false
|
||||
};
|
||||
|
||||
async function postNTPSettings(data: NTPSettings) {
|
||||
try {
|
||||
const response = await fetch('/api/ntpSettings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.status == 200) {
|
||||
notifications.success('Security settings updated.', 3000);
|
||||
ntpSettings = await response.json();
|
||||
} else {
|
||||
notifications.error('User not authorized.', 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmitNTP() {
|
||||
let valid = true;
|
||||
|
||||
// Validate Server
|
||||
// RegEx for IPv4
|
||||
const regexExpIPv4 =
|
||||
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/;
|
||||
const regexExpURL =
|
||||
/[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/i;
|
||||
|
||||
if (!regexExpURL.test(ntpSettings.server) && !regexExpIPv4.test(ntpSettings.server)) {
|
||||
valid = false;
|
||||
formErrors.server = true;
|
||||
} else {
|
||||
formErrors.server = false;
|
||||
}
|
||||
|
||||
ntpSettings.tz_format = TIME_ZONES[ntpSettings.tz_label];
|
||||
|
||||
// Submit JSON to REST API
|
||||
if (valid) {
|
||||
postNTPSettings(ntpSettings);
|
||||
//alert('Form Valid');
|
||||
}
|
||||
}
|
||||
|
||||
function convertSeconds(seconds: number) {
|
||||
// Calculate the number of seconds, minutes, hours, and days
|
||||
let minutes = Math.floor(seconds / 60);
|
||||
let hours = Math.floor(minutes / 60);
|
||||
let days = Math.floor(hours / 24);
|
||||
|
||||
// Calculate the remaining hours, minutes, and seconds
|
||||
hours = hours % 24;
|
||||
minutes = minutes % 60;
|
||||
seconds = seconds % 60;
|
||||
|
||||
// Create the formatted string
|
||||
let result = '';
|
||||
if (days > 0) {
|
||||
result += days + ' day' + (days > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
if (hours > 0) {
|
||||
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
if (minutes > 0) {
|
||||
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
result += seconds + ' second' + (seconds > 1 ? 's' : '');
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
<Clock slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">Network Time</span>
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getNTPStatus()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div
|
||||
class="mask mask-hexagon h-auto w-10 {ntpStatus.status === 1
|
||||
? 'bg-success'
|
||||
: 'bg-error'}"
|
||||
>
|
||||
<NTP
|
||||
class="h-auto w-full scale-75 {ntpStatus.status === 1
|
||||
? 'text-success-content'
|
||||
: 'text-error-content'}"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Status</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{ntpStatus.status === 1 ? 'Active' : 'Inactive'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||
<Server class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">NTP Server</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{ntpStatus.server}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||
<Clock class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Local Time</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{new Intl.DateTimeFormat('en-GB', {
|
||||
dateStyle: 'long',
|
||||
timeStyle: 'long'
|
||||
}).format(new Date(ntpStatus.local_time))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||
<UTC class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">UTC Time</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{new Intl.DateTimeFormat('en-GB', {
|
||||
dateStyle: 'long',
|
||||
timeStyle: 'long',
|
||||
timeZone: 'UTC'
|
||||
}).format(new Date(ntpStatus.utc_time))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||
<Stopwatch class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Uptime</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{convertSeconds(ntpStatus.uptime)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
{#if !$page.data.features.security || $user.admin}
|
||||
<Collapsible open={false} class="shadow-lg" on:closed={getNTPSettings}>
|
||||
<span slot="title">Change NTP Settings</span>
|
||||
<form
|
||||
class="form-control w-full"
|
||||
on:submit|preventDefault={handleSubmitNTP}
|
||||
novalidate
|
||||
bind:this={formField}
|
||||
>
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={ntpSettings.enabled}
|
||||
class="checkbox checkbox-primary"
|
||||
/>
|
||||
<span class="">Enable NTP</span>
|
||||
</label>
|
||||
<label class="label" for="server">
|
||||
<span class="label-text text-md">Server</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
min="3"
|
||||
max="64"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.server
|
||||
? 'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={ntpSettings.server}
|
||||
id="server"
|
||||
required
|
||||
/>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text-alt text-error {formErrors.server ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address or URL</span
|
||||
>
|
||||
</label>
|
||||
<label class="label" for="tz">
|
||||
<span class="label-text text-md">Pick Time Zone</span>
|
||||
</label>
|
||||
<select class="select select-bordered" bind:value={ntpSettings.tz_label} id="tz">
|
||||
{#each Object.entries(TIME_ZONES) as [tz_label, tz_format]}
|
||||
<option value={tz_label}>{tz_label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<div class="mt-6 place-self-end">
|
||||
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</Collapsible>
|
||||
{/if}
|
||||
</SettingsCard>
|
||||
@@ -0,0 +1,466 @@
|
||||
export type TimeZones = {
|
||||
[name: string]: string
|
||||
};
|
||||
|
||||
export const TIME_ZONES: TimeZones = {
|
||||
"Africa/Abidjan": "GMT0",
|
||||
"Africa/Accra": "GMT0",
|
||||
"Africa/Addis_Ababa": "EAT-3",
|
||||
"Africa/Algiers": "CET-1",
|
||||
"Africa/Asmara": "EAT-3",
|
||||
"Africa/Bamako": "GMT0",
|
||||
"Africa/Bangui": "WAT-1",
|
||||
"Africa/Banjul": "GMT0",
|
||||
"Africa/Bissau": "GMT0",
|
||||
"Africa/Blantyre": "CAT-2",
|
||||
"Africa/Brazzaville": "WAT-1",
|
||||
"Africa/Bujumbura": "CAT-2",
|
||||
"Africa/Cairo": "EET-2",
|
||||
"Africa/Casablanca": "UNK-1",
|
||||
"Africa/Ceuta": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Africa/Conakry": "GMT0",
|
||||
"Africa/Dakar": "GMT0",
|
||||
"Africa/Dar_es_Salaam": "EAT-3",
|
||||
"Africa/Djibouti": "EAT-3",
|
||||
"Africa/Douala": "WAT-1",
|
||||
"Africa/El_Aaiun": "UNK-1",
|
||||
"Africa/Freetown": "GMT0",
|
||||
"Africa/Gaborone": "CAT-2",
|
||||
"Africa/Harare": "CAT-2",
|
||||
"Africa/Johannesburg": "SAST-2",
|
||||
"Africa/Juba": "EAT-3",
|
||||
"Africa/Kampala": "EAT-3",
|
||||
"Africa/Khartoum": "CAT-2",
|
||||
"Africa/Kigali": "CAT-2",
|
||||
"Africa/Kinshasa": "WAT-1",
|
||||
"Africa/Lagos": "WAT-1",
|
||||
"Africa/Libreville": "WAT-1",
|
||||
"Africa/Lome": "GMT0",
|
||||
"Africa/Luanda": "WAT-1",
|
||||
"Africa/Lubumbashi": "CAT-2",
|
||||
"Africa/Lusaka": "CAT-2",
|
||||
"Africa/Malabo": "WAT-1",
|
||||
"Africa/Maputo": "CAT-2",
|
||||
"Africa/Maseru": "SAST-2",
|
||||
"Africa/Mbabane": "SAST-2",
|
||||
"Africa/Mogadishu": "EAT-3",
|
||||
"Africa/Monrovia": "GMT0",
|
||||
"Africa/Nairobi": "EAT-3",
|
||||
"Africa/Ndjamena": "WAT-1",
|
||||
"Africa/Niamey": "WAT-1",
|
||||
"Africa/Nouakchott": "GMT0",
|
||||
"Africa/Ouagadougou": "GMT0",
|
||||
"Africa/Porto-Novo": "WAT-1",
|
||||
"Africa/Sao_Tome": "GMT0",
|
||||
"Africa/Tripoli": "EET-2",
|
||||
"Africa/Tunis": "CET-1",
|
||||
"Africa/Windhoek": "CAT-2",
|
||||
"America/Adak": "HST10HDT,M3.2.0,M11.1.0",
|
||||
"America/Anchorage": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Anguilla": "AST4",
|
||||
"America/Antigua": "AST4",
|
||||
"America/Araguaina": "UNK3",
|
||||
"America/Argentina/Buenos_Aires": "UNK3",
|
||||
"America/Argentina/Catamarca": "UNK3",
|
||||
"America/Argentina/Cordoba": "UNK3",
|
||||
"America/Argentina/Jujuy": "UNK3",
|
||||
"America/Argentina/La_Rioja": "UNK3",
|
||||
"America/Argentina/Mendoza": "UNK3",
|
||||
"America/Argentina/Rio_Gallegos": "UNK3",
|
||||
"America/Argentina/Salta": "UNK3",
|
||||
"America/Argentina/San_Juan": "UNK3",
|
||||
"America/Argentina/San_Luis": "UNK3",
|
||||
"America/Argentina/Tucuman": "UNK3",
|
||||
"America/Argentina/Ushuaia": "UNK3",
|
||||
"America/Aruba": "AST4",
|
||||
"America/Asuncion": "UNK4UNK,M10.1.0/0,M3.4.0/0",
|
||||
"America/Atikokan": "EST5",
|
||||
"America/Bahia": "UNK3",
|
||||
"America/Bahia_Banderas": "CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Barbados": "AST4",
|
||||
"America/Belem": "UNK3",
|
||||
"America/Belize": "CST6",
|
||||
"America/Blanc-Sablon": "AST4",
|
||||
"America/Boa_Vista": "UNK4",
|
||||
"America/Bogota": "UNK5",
|
||||
"America/Boise": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Cambridge_Bay": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Campo_Grande": "UNK4",
|
||||
"America/Cancun": "EST5",
|
||||
"America/Caracas": "UNK4",
|
||||
"America/Cayenne": "UNK3",
|
||||
"America/Cayman": "EST5",
|
||||
"America/Chicago": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Chihuahua": "MST7MDT,M4.1.0,M10.5.0",
|
||||
"America/Costa_Rica": "CST6",
|
||||
"America/Creston": "MST7",
|
||||
"America/Cuiaba": "UNK4",
|
||||
"America/Curacao": "AST4",
|
||||
"America/Danmarkshavn": "GMT0",
|
||||
"America/Dawson": "MST7",
|
||||
"America/Dawson_Creek": "MST7",
|
||||
"America/Denver": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Detroit": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Dominica": "AST4",
|
||||
"America/Edmonton": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Eirunepe": "UNK5",
|
||||
"America/El_Salvador": "CST6",
|
||||
"America/Fort_Nelson": "MST7",
|
||||
"America/Fortaleza": "UNK3",
|
||||
"America/Glace_Bay": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Godthab": "UNK3UNK,M3.5.0/-2,M10.5.0/-1",
|
||||
"America/Goose_Bay": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Grand_Turk": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Grenada": "AST4",
|
||||
"America/Guadeloupe": "AST4",
|
||||
"America/Guatemala": "CST6",
|
||||
"America/Guayaquil": "UNK5",
|
||||
"America/Guyana": "UNK4",
|
||||
"America/Halifax": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Havana": "CST5CDT,M3.2.0/0,M11.1.0/1",
|
||||
"America/Hermosillo": "MST7",
|
||||
"America/Indiana/Indianapolis": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Knox": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Marengo": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Petersburg": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Tell_City": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Vevay": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Vincennes": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Winamac": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Inuvik": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Iqaluit": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Jamaica": "EST5",
|
||||
"America/Juneau": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Kentucky/Louisville": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Kentucky/Monticello": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Kralendijk": "AST4",
|
||||
"America/La_Paz": "UNK4",
|
||||
"America/Lima": "UNK5",
|
||||
"America/Los_Angeles": "PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Lower_Princes": "AST4",
|
||||
"America/Maceio": "UNK3",
|
||||
"America/Managua": "CST6",
|
||||
"America/Manaus": "UNK4",
|
||||
"America/Marigot": "AST4",
|
||||
"America/Martinique": "AST4",
|
||||
"America/Matamoros": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Mazatlan": "MST7MDT,M4.1.0,M10.5.0",
|
||||
"America/Menominee": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Merida": "CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Metlakatla": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Mexico_City": "CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Miquelon": "UNK3UNK,M3.2.0,M11.1.0",
|
||||
"America/Moncton": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Monterrey": "CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Montevideo": "UNK3",
|
||||
"America/Montreal": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Montserrat": "AST4",
|
||||
"America/Nassau": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/New_York": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Nipigon": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Nome": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Noronha": "UNK2",
|
||||
"America/North_Dakota/Beulah": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/North_Dakota/Center": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/North_Dakota/New_Salem": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Ojinaga": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Panama": "EST5",
|
||||
"America/Pangnirtung": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Paramaribo": "UNK3",
|
||||
"America/Phoenix": "MST7",
|
||||
"America/Port-au-Prince": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Port_of_Spain": "AST4",
|
||||
"America/Porto_Velho": "UNK4",
|
||||
"America/Puerto_Rico": "AST4",
|
||||
"America/Punta_Arenas": "UNK3",
|
||||
"America/Rainy_River": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Rankin_Inlet": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Recife": "UNK3",
|
||||
"America/Regina": "CST6",
|
||||
"America/Resolute": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Rio_Branco": "UNK5",
|
||||
"America/Santarem": "UNK3",
|
||||
"America/Santiago": "UNK4UNK,M9.1.6/24,M4.1.6/24",
|
||||
"America/Santo_Domingo": "AST4",
|
||||
"America/Sao_Paulo": "UNK3",
|
||||
"America/Scoresbysund": "UNK1UNK,M3.5.0/0,M10.5.0/1",
|
||||
"America/Sitka": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/St_Barthelemy": "AST4",
|
||||
"America/St_Johns": "NST3:30NDT,M3.2.0,M11.1.0",
|
||||
"America/St_Kitts": "AST4",
|
||||
"America/St_Lucia": "AST4",
|
||||
"America/St_Thomas": "AST4",
|
||||
"America/St_Vincent": "AST4",
|
||||
"America/Swift_Current": "CST6",
|
||||
"America/Tegucigalpa": "CST6",
|
||||
"America/Thule": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Thunder_Bay": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Tijuana": "PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Toronto": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Tortola": "AST4",
|
||||
"America/Vancouver": "PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Whitehorse": "MST7",
|
||||
"America/Winnipeg": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Yakutat": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Yellowknife": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"Antarctica/Casey": "UNK-8",
|
||||
"Antarctica/Davis": "UNK-7",
|
||||
"Antarctica/DumontDUrville": "UNK-10",
|
||||
"Antarctica/Macquarie": "UNK-11",
|
||||
"Antarctica/Mawson": "UNK-5",
|
||||
"Antarctica/McMurdo": "NZST-12NZDT,M9.5.0,M4.1.0/3",
|
||||
"Antarctica/Palmer": "UNK3",
|
||||
"Antarctica/Rothera": "UNK3",
|
||||
"Antarctica/Syowa": "UNK-3",
|
||||
"Antarctica/Troll": "UNK0UNK-2,M3.5.0/1,M10.5.0/3",
|
||||
"Antarctica/Vostok": "UNK-6",
|
||||
"Arctic/Longyearbyen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Asia/Aden": "UNK-3",
|
||||
"Asia/Almaty": "UNK-6",
|
||||
"Asia/Amman": "EET-2EEST,M3.5.4/24,M10.5.5/1",
|
||||
"Asia/Anadyr": "UNK-12",
|
||||
"Asia/Aqtau": "UNK-5",
|
||||
"Asia/Aqtobe": "UNK-5",
|
||||
"Asia/Ashgabat": "UNK-5",
|
||||
"Asia/Atyrau": "UNK-5",
|
||||
"Asia/Baghdad": "UNK-3",
|
||||
"Asia/Bahrain": "UNK-3",
|
||||
"Asia/Baku": "UNK-4",
|
||||
"Asia/Bangkok": "UNK-7",
|
||||
"Asia/Barnaul": "UNK-7",
|
||||
"Asia/Beirut": "EET-2EEST,M3.5.0/0,M10.5.0/0",
|
||||
"Asia/Bishkek": "UNK-6",
|
||||
"Asia/Brunei": "UNK-8",
|
||||
"Asia/Chita": "UNK-9",
|
||||
"Asia/Choibalsan": "UNK-8",
|
||||
"Asia/Colombo": "UNK-5:30",
|
||||
"Asia/Damascus": "EET-2EEST,M3.5.5/0,M10.5.5/0",
|
||||
"Asia/Dhaka": "UNK-6",
|
||||
"Asia/Dili": "UNK-9",
|
||||
"Asia/Dubai": "UNK-4",
|
||||
"Asia/Dushanbe": "UNK-5",
|
||||
"Asia/Famagusta": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Asia/Gaza": "EET-2EEST,M3.5.5/0,M10.5.6/1",
|
||||
"Asia/Hebron": "EET-2EEST,M3.5.5/0,M10.5.6/1",
|
||||
"Asia/Ho_Chi_Minh": "UNK-7",
|
||||
"Asia/Hong_Kong": "HKT-8",
|
||||
"Asia/Hovd": "UNK-7",
|
||||
"Asia/Irkutsk": "UNK-8",
|
||||
"Asia/Jakarta": "WIB-7",
|
||||
"Asia/Jayapura": "WIT-9",
|
||||
"Asia/Jerusalem": "IST-2IDT,M3.4.4/26,M10.5.0",
|
||||
"Asia/Kabul": "UNK-4:30",
|
||||
"Asia/Kamchatka": "UNK-12",
|
||||
"Asia/Karachi": "PKT-5",
|
||||
"Asia/Kathmandu": "UNK-5:45",
|
||||
"Asia/Khandyga": "UNK-9",
|
||||
"Asia/Kolkata": "IST-5:30",
|
||||
"Asia/Krasnoyarsk": "UNK-7",
|
||||
"Asia/Kuala_Lumpur": "UNK-8",
|
||||
"Asia/Kuching": "UNK-8",
|
||||
"Asia/Kuwait": "UNK-3",
|
||||
"Asia/Macau": "CST-8",
|
||||
"Asia/Magadan": "UNK-11",
|
||||
"Asia/Makassar": "WITA-8",
|
||||
"Asia/Manila": "PST-8",
|
||||
"Asia/Muscat": "UNK-4",
|
||||
"Asia/Nicosia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Asia/Novokuznetsk": "UNK-7",
|
||||
"Asia/Novosibirsk": "UNK-7",
|
||||
"Asia/Omsk": "UNK-6",
|
||||
"Asia/Oral": "UNK-5",
|
||||
"Asia/Phnom_Penh": "UNK-7",
|
||||
"Asia/Pontianak": "WIB-7",
|
||||
"Asia/Pyongyang": "KST-9",
|
||||
"Asia/Qatar": "UNK-3",
|
||||
"Asia/Qyzylorda": "UNK-5",
|
||||
"Asia/Riyadh": "UNK-3",
|
||||
"Asia/Sakhalin": "UNK-11",
|
||||
"Asia/Samarkand": "UNK-5",
|
||||
"Asia/Seoul": "KST-9",
|
||||
"Asia/Shanghai": "CST-8",
|
||||
"Asia/Singapore": "UNK-8",
|
||||
"Asia/Srednekolymsk": "UNK-11",
|
||||
"Asia/Taipei": "CST-8",
|
||||
"Asia/Tashkent": "UNK-5",
|
||||
"Asia/Tbilisi": "UNK-4",
|
||||
"Asia/Tehran": "UNK-3:30UNK,J79/24,J263/24",
|
||||
"Asia/Thimphu": "UNK-6",
|
||||
"Asia/Tokyo": "JST-9",
|
||||
"Asia/Tomsk": "UNK-7",
|
||||
"Asia/Ulaanbaatar": "UNK-8",
|
||||
"Asia/Urumqi": "UNK-6",
|
||||
"Asia/Ust-Nera": "UNK-10",
|
||||
"Asia/Vientiane": "UNK-7",
|
||||
"Asia/Vladivostok": "UNK-10",
|
||||
"Asia/Yakutsk": "UNK-9",
|
||||
"Asia/Yangon": "UNK-6:30",
|
||||
"Asia/Yekaterinburg": "UNK-5",
|
||||
"Asia/Yerevan": "UNK-4",
|
||||
"Atlantic/Azores": "UNK1UNK,M3.5.0/0,M10.5.0/1",
|
||||
"Atlantic/Bermuda": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"Atlantic/Canary": "WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Cape_Verde": "UNK1",
|
||||
"Atlantic/Faroe": "WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Madeira": "WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Reykjavik": "GMT0",
|
||||
"Atlantic/South_Georgia": "UNK2",
|
||||
"Atlantic/St_Helena": "GMT0",
|
||||
"Atlantic/Stanley": "UNK3",
|
||||
"Australia/Adelaide": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Brisbane": "AEST-10",
|
||||
"Australia/Broken_Hill": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Currie": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Darwin": "ACST-9:30",
|
||||
"Australia/Eucla": "UNK-8:45",
|
||||
"Australia/Hobart": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Lindeman": "AEST-10",
|
||||
"Australia/Lord_Howe": "UNK-10:30UNK-11,M10.1.0,M4.1.0",
|
||||
"Australia/Melbourne": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Perth": "AWST-8",
|
||||
"Australia/Sydney": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Etc/GMT": "GMT0",
|
||||
"Etc/GMT+0": "GMT0",
|
||||
"Etc/GMT+1": "UNK1",
|
||||
"Etc/GMT+10": "UNK10",
|
||||
"Etc/GMT+11": "UNK11",
|
||||
"Etc/GMT+12": "UNK12",
|
||||
"Etc/GMT+2": "UNK2",
|
||||
"Etc/GMT+3": "UNK3",
|
||||
"Etc/GMT+4": "UNK4",
|
||||
"Etc/GMT+5": "UNK5",
|
||||
"Etc/GMT+6": "UNK6",
|
||||
"Etc/GMT+7": "UNK7",
|
||||
"Etc/GMT+8": "UNK8",
|
||||
"Etc/GMT+9": "UNK9",
|
||||
"Etc/GMT-0": "GMT0",
|
||||
"Etc/GMT-1": "UNK-1",
|
||||
"Etc/GMT-10": "UNK-10",
|
||||
"Etc/GMT-11": "UNK-11",
|
||||
"Etc/GMT-12": "UNK-12",
|
||||
"Etc/GMT-13": "UNK-13",
|
||||
"Etc/GMT-14": "UNK-14",
|
||||
"Etc/GMT-2": "UNK-2",
|
||||
"Etc/GMT-3": "UNK-3",
|
||||
"Etc/GMT-4": "UNK-4",
|
||||
"Etc/GMT-5": "UNK-5",
|
||||
"Etc/GMT-6": "UNK-6",
|
||||
"Etc/GMT-7": "UNK-7",
|
||||
"Etc/GMT-8": "UNK-8",
|
||||
"Etc/GMT-9": "UNK-9",
|
||||
"Etc/GMT0": "GMT0",
|
||||
"Etc/Greenwich": "GMT0",
|
||||
"Etc/UCT": "UTC0",
|
||||
"Etc/UTC": "UTC0",
|
||||
"Etc/Universal": "UTC0",
|
||||
"Etc/Zulu": "UTC0",
|
||||
"Europe/Amsterdam": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Andorra": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Astrakhan": "UNK-4",
|
||||
"Europe/Athens": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Belgrade": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Berlin": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Bratislava": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Brussels": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Bucharest": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Budapest": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Busingen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Chisinau": "EET-2EEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Copenhagen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Dublin": "IST-1GMT0,M10.5.0,M3.5.0/1",
|
||||
"Europe/Gibraltar": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Guernsey": "GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Helsinki": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Isle_of_Man": "GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Istanbul": "UNK-3",
|
||||
"Europe/Jersey": "GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Kaliningrad": "EET-2",
|
||||
"Europe/Kiev": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Kirov": "UNK-3",
|
||||
"Europe/Lisbon": "WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Ljubljana": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/London": "GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Luxembourg": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Madrid": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Malta": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Mariehamn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Minsk": "UNK-3",
|
||||
"Europe/Monaco": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Moscow": "MSK-3",
|
||||
"Europe/Oslo": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Paris": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Podgorica": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Prague": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Riga": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Rome": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Samara": "UNK-4",
|
||||
"Europe/San_Marino": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Sarajevo": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Saratov": "UNK-4",
|
||||
"Europe/Simferopol": "MSK-3",
|
||||
"Europe/Skopje": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Sofia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Stockholm": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Tallinn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Tirane": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Ulyanovsk": "UNK-4",
|
||||
"Europe/Uzhgorod": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Vaduz": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Vatican": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Vienna": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Vilnius": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Volgograd": "UNK-4",
|
||||
"Europe/Warsaw": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Zagreb": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Zaporozhye": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Zurich": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Indian/Antananarivo": "EAT-3",
|
||||
"Indian/Chagos": "UNK-6",
|
||||
"Indian/Christmas": "UNK-7",
|
||||
"Indian/Cocos": "UNK-6:30",
|
||||
"Indian/Comoro": "EAT-3",
|
||||
"Indian/Kerguelen": "UNK-5",
|
||||
"Indian/Mahe": "UNK-4",
|
||||
"Indian/Maldives": "UNK-5",
|
||||
"Indian/Mauritius": "UNK-4",
|
||||
"Indian/Mayotte": "EAT-3",
|
||||
"Indian/Reunion": "UNK-4",
|
||||
"Pacific/Apia": "UNK-13UNK,M9.5.0/3,M4.1.0/4",
|
||||
"Pacific/Auckland": "NZST-12NZDT,M9.5.0,M4.1.0/3",
|
||||
"Pacific/Bougainville": "UNK-11",
|
||||
"Pacific/Chatham": "UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45",
|
||||
"Pacific/Chuuk": "UNK-10",
|
||||
"Pacific/Easter": "UNK6UNK,M9.1.6/22,M4.1.6/22",
|
||||
"Pacific/Efate": "UNK-11",
|
||||
"Pacific/Enderbury": "UNK-13",
|
||||
"Pacific/Fakaofo": "UNK-13",
|
||||
"Pacific/Fiji": "UNK-12UNK,M11.2.0,M1.2.3/99",
|
||||
"Pacific/Funafuti": "UNK-12",
|
||||
"Pacific/Galapagos": "UNK6",
|
||||
"Pacific/Gambier": "UNK9",
|
||||
"Pacific/Guadalcanal": "UNK-11",
|
||||
"Pacific/Guam": "ChST-10",
|
||||
"Pacific/Honolulu": "HST10",
|
||||
"Pacific/Kiritimati": "UNK-14",
|
||||
"Pacific/Kosrae": "UNK-11",
|
||||
"Pacific/Kwajalein": "UNK-12",
|
||||
"Pacific/Majuro": "UNK-12",
|
||||
"Pacific/Marquesas": "UNK9:30",
|
||||
"Pacific/Midway": "SST11",
|
||||
"Pacific/Nauru": "UNK-12",
|
||||
"Pacific/Niue": "UNK11",
|
||||
"Pacific/Norfolk": "UNK-11UNK,M10.1.0,M4.1.0/3",
|
||||
"Pacific/Noumea": "UNK-11",
|
||||
"Pacific/Pago_Pago": "SST11",
|
||||
"Pacific/Palau": "UNK-9",
|
||||
"Pacific/Pitcairn": "UNK8",
|
||||
"Pacific/Pohnpei": "UNK-11",
|
||||
"Pacific/Port_Moresby": "UNK-10",
|
||||
"Pacific/Rarotonga": "UNK10",
|
||||
"Pacific/Saipan": "ChST-10",
|
||||
"Pacific/Tahiti": "UNK10",
|
||||
"Pacific/Tarawa": "UNK-12",
|
||||
"Pacific/Tongatapu": "UNK-13",
|
||||
"Pacific/Wake": "UNK-12",
|
||||
"Pacific/Wallis": "UNK-12"
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import Controls from './Controls.svelte';
|
||||
import { socket } from '$lib/stores';
|
||||
</script>
|
||||
<div>
|
||||
{#if $socket}
|
||||
<Controls />
|
||||
<slot/>
|
||||
{:else}
|
||||
<div class="flex justify-center items-center">
|
||||
<h2>Waiting for connection</h2>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import Visualization from "$lib/components/Visualization.svelte";
|
||||
</script>
|
||||
|
||||
<div class="grow flex">
|
||||
<div class="absolute h-screen w-full top-0">
|
||||
<Visualization debug />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
export const load = async () => {
|
||||
return { title: 'Controller' };
|
||||
};
|
||||
@@ -2,10 +2,8 @@
|
||||
import nipplejs from 'nipplejs';
|
||||
import { onMount } from 'svelte';
|
||||
import { capitalize, throttler, toInt8 } from '$lib/utilities';
|
||||
import { input, outControllerData, mode, modes, type Modes } from '$lib/stores';
|
||||
import { input, outControllerData, mode, modes, type Modes, ModesEnum, socket } from '$lib/stores';
|
||||
import type { vector } from '$lib/models';
|
||||
import Range from './input/Range.svelte';
|
||||
import Button from './input/Button.svelte';
|
||||
|
||||
let throttle = new throttler();
|
||||
let left: nipplejs.JoystickManager;
|
||||
@@ -69,7 +67,7 @@
|
||||
throttle.throttle(updateData, throttle_timing);
|
||||
};
|
||||
|
||||
const handleRange = (event:CustomEvent, key: 'speed' | 'height') => {
|
||||
const handleRange = (event:Event, key: 'speed' | 'height') => {
|
||||
const value:number = event.detail
|
||||
input.update((inputData) => {
|
||||
inputData[key] = value;
|
||||
@@ -79,7 +77,7 @@
|
||||
}
|
||||
|
||||
const changeMode = (modeValue: Modes) => {
|
||||
mode.set(modeValue);
|
||||
mode.set(modes.indexOf(modeValue));
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -91,20 +89,17 @@
|
||||
</div>
|
||||
<div class="absolute bottom-0 z-10 p-4 gap-4 flex items-end">
|
||||
{#each modes as modeValue}
|
||||
<div>
|
||||
<Button
|
||||
on:click={() => changeMode(modeValue)}
|
||||
active={$mode === modeValue}
|
||||
>
|
||||
{capitalize(modeValue)}
|
||||
</Button>
|
||||
</div>
|
||||
<button class="btn btn-outline" class:btn-active={$mode === modes.indexOf(modeValue)} on:click={() => changeMode(modeValue)}>
|
||||
{capitalize(modeValue)}
|
||||
</button>
|
||||
{/each}
|
||||
<div>
|
||||
{#if $mode === 'walk'}
|
||||
<Range label="Speed" on:value={(e) => handleRange(e, 'speed')}></Range>
|
||||
{#if $mode === ModesEnum.Walk}
|
||||
<label for="speed">Speed</label>
|
||||
<input type="range" name="speed" min="0" max="100" on:input={(e) => handleRange(e, 'speed')} class="range range-sm" />
|
||||
{/if}
|
||||
<Range label="Height" on:value={(e) => handleRange(e, 'height')}></Range>
|
||||
<label for="height">Height</label>
|
||||
<input type="range" name="height" min="0" max="100" on:input={(e) => handleRange(e, 'height')} class="range range-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,119 @@
|
||||
<script lang="ts">
|
||||
import logo from '$lib/assets/logo512.png';
|
||||
import InputPassword from '$lib/components/InputPassword.svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import Login from '~icons/tabler/login';
|
||||
|
||||
type SignInData = {
|
||||
password: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
let username = '';
|
||||
let password = '';
|
||||
|
||||
let loginFailed = false;
|
||||
|
||||
let token = { access_token: '' };
|
||||
|
||||
async function signInUser(data: SignInData) {
|
||||
try {
|
||||
const response = await fetch('/api/signIn', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (response.status === 200) {
|
||||
token = await response.json();
|
||||
user.init(token.access_token);
|
||||
let username = $user.username;
|
||||
notifications.success('User ' + username + ' signed in', 5000);
|
||||
} else {
|
||||
username = '';
|
||||
password = '';
|
||||
notifications.error('Wrong Username or Password!', 5000);
|
||||
loginFailed = true;
|
||||
setTimeout(() => {
|
||||
loginFailed = false;
|
||||
}, 1500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="hero from-primary/30 to-secondary/30 min-h-screen bg-gradient-to-br">
|
||||
<div
|
||||
class="card lg:card-side bg-base-100 face shadow-2xl {loginFailed
|
||||
? 'failure border-error border-2'
|
||||
: ''}"
|
||||
in:fly={{ delay: 200, y: 100, duration: 500 }}
|
||||
out:fade={{ duration: 200 }}
|
||||
>
|
||||
<figure class="bg-base-200"><img src={logo} alt="Logo" class="h-auto w-48 lg:w-64" /></figure>
|
||||
<div class="card-body w-80">
|
||||
<h2 class="card-title text-2xl">Login</h2>
|
||||
<form class="form-control w-full max-w-xs">
|
||||
<label class="label" for="user">
|
||||
<span class="label-text text-md">Username</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
id="user"
|
||||
bind:value={username}
|
||||
/>
|
||||
|
||||
<label class="label" for="pwd">
|
||||
<span class="label-text text-md">Password</span>
|
||||
</label>
|
||||
<InputPassword id="pwd" bind:value={password} />
|
||||
|
||||
<div class="card-actions mt-4 justify-end">
|
||||
<button
|
||||
class="btn btn-primary inline-flex items-center"
|
||||
on:click={() => {
|
||||
signInUser({ username, password });
|
||||
}}><Login class="mr-2 h-5 w-5" /><span>Login</span></button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.failure {
|
||||
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
transform: translate3d(0, 0, 0);
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000px;
|
||||
}
|
||||
@keyframes shake {
|
||||
10%,
|
||||
90% {
|
||||
transform: translatex(-1px);
|
||||
}
|
||||
|
||||
20%,
|
||||
80% {
|
||||
transform: translatex(2px);
|
||||
}
|
||||
|
||||
30%,
|
||||
50%,
|
||||
70% {
|
||||
transform: translatex(-4px);
|
||||
}
|
||||
|
||||
40%,
|
||||
60% {
|
||||
transform: translatex(4px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,258 @@
|
||||
<script lang="ts">
|
||||
import logo from '$lib/assets/logo512.png';
|
||||
import MdiGithub from '~icons/mdi/github';
|
||||
import Users from '~icons/mdi/users';
|
||||
import Settings from '~icons/mdi/settings';
|
||||
import MdiController from '~icons/mdi/controller';
|
||||
import Health from '~icons/mdi/stethoscope';
|
||||
import Update from '~icons/mdi/reload';
|
||||
import WiFi from '~icons/mdi/wifi';
|
||||
import Router from '~icons/mdi/router';
|
||||
import AP from '~icons/mdi/access-point';
|
||||
import Remote from '~icons/mdi/network';
|
||||
import Avatar from '~icons/mdi/user-circle';
|
||||
import Logout from '~icons/mdi/logout';
|
||||
import Copyright from '~icons/mdi/copyright';
|
||||
import MQTT from '~icons/tabler/topology-star-3';
|
||||
import NTP from '~icons/mdi/clock-check';
|
||||
import Metrics from '~icons/mdi/report-bar';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const appName = $page.data.app_name;
|
||||
|
||||
const copyright = $page.data.copyright;
|
||||
|
||||
const github = { href: 'https://github.com/' + $page.data.github, active: true };
|
||||
|
||||
type menuItem = {
|
||||
title: string;
|
||||
icon: object;
|
||||
href?: string;
|
||||
feature: boolean;
|
||||
active?: boolean;
|
||||
submenu?: subMenuItem[];
|
||||
};
|
||||
|
||||
type subMenuItem = {
|
||||
title: string;
|
||||
icon: object;
|
||||
href: string;
|
||||
feature: boolean;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
let menuItems = [
|
||||
{
|
||||
title: 'Controller',
|
||||
icon: MdiController,
|
||||
href: '/controller',
|
||||
feature: true,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
title: 'Connections',
|
||||
icon: Remote,
|
||||
feature: $page.data.features.mqtt || $page.data.features.ntp,
|
||||
submenu: [
|
||||
{
|
||||
title: 'MQTT',
|
||||
icon: MQTT,
|
||||
href: '/connections/mqtt',
|
||||
feature: $page.data.features.mqtt,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
title: 'NTP',
|
||||
icon: NTP,
|
||||
href: '/connections/ntp',
|
||||
feature: $page.data.features.ntp,
|
||||
active: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'WiFi',
|
||||
icon: WiFi,
|
||||
feature: true,
|
||||
submenu: [
|
||||
{
|
||||
title: 'WiFi Station',
|
||||
icon: Router,
|
||||
href: '/wifi/sta',
|
||||
feature: true,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
title: 'Access Point',
|
||||
icon: AP,
|
||||
href: '/wifi/ap',
|
||||
feature: true,
|
||||
active: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Users',
|
||||
icon: Users,
|
||||
href: '/user',
|
||||
feature: $page.data.features.security && $user.admin,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
title: 'System',
|
||||
icon: Settings,
|
||||
feature: true,
|
||||
submenu: [
|
||||
{
|
||||
title: 'System Status',
|
||||
icon: Health,
|
||||
href: '/system/status',
|
||||
feature: true,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
title: 'System Metrics',
|
||||
icon: Metrics,
|
||||
href: '/system/metrics',
|
||||
feature: $page.data.features.analytics,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
title: 'Firmware Update',
|
||||
icon: Update,
|
||||
href: '/system/update',
|
||||
feature:
|
||||
($page.data.features.ota ||
|
||||
$page.data.features.upload_firmware ||
|
||||
$page.data.features.download_firmware) &&
|
||||
(!$page.data.features.security || $user.admin),
|
||||
active: false
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function setActiveMenuItem(menuItems: menuItem[], targetTitle: string) {
|
||||
for (let i = 0; i < menuItems.length; i++) {
|
||||
const menuItem = menuItems[i];
|
||||
|
||||
// Clear any previous set active flags
|
||||
menuItem.active = false;
|
||||
|
||||
// Check if the current menu item's title matches the target title
|
||||
if (menuItem.title === targetTitle) {
|
||||
menuItem.active = true; // Set the active property to true
|
||||
dispatch('menuClicked');
|
||||
}
|
||||
|
||||
// Check if the current menu item has a submenu
|
||||
if (menuItem.submenu && menuItem.submenu.length > 0) {
|
||||
// Recursively call the function for each submenu item
|
||||
setActiveMenuItem(menuItem.submenu, targetTitle);
|
||||
}
|
||||
}
|
||||
if (targetTitle == '') {
|
||||
dispatch('menuClicked');
|
||||
}
|
||||
menuItems = menuItems;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
setActiveMenuItem(menuItems, $page.data.title);
|
||||
menuItems = menuItems;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="bg-base-200 text-base-content flex h-full w-80 flex-col p-4">
|
||||
<!-- Sidebar content here -->
|
||||
<a
|
||||
href="/"
|
||||
class="rounded-box mb-4 flex items-center hover:scale-[1.02] active:scale-[0.98]"
|
||||
on:click={() => setActiveMenuItem(menuItems, '')}
|
||||
>
|
||||
<img src={logo} alt="Logo" class="h-12 w-12" />
|
||||
<h1 class="px-4 text-2xl font-bold">{appName}</h1>
|
||||
</a>
|
||||
<ul class="menu rounded-box menu-vertical flex-nowrap overflow-y-auto">
|
||||
{#each menuItems as menuItem (menuItem.title)}
|
||||
{#if menuItem.feature}
|
||||
{#if menuItem.submenu}
|
||||
<li>
|
||||
<details>
|
||||
<summary class="text-lg font-bold"
|
||||
><svelte:component this={menuItem.icon} class="h-6 w-6" />{menuItem.title}</summary
|
||||
>
|
||||
<ul>
|
||||
{#each menuItem.submenu as subMenuItem}
|
||||
{#if subMenuItem.feature}
|
||||
<li class="hover-bordered">
|
||||
<a
|
||||
href={subMenuItem.href}
|
||||
class="text-ml font-bold {subMenuItem.active ? 'bg-base-100' : ''}"
|
||||
on:click={() => {
|
||||
setActiveMenuItem(menuItems, subMenuItem.title);
|
||||
menuItems = menuItems;
|
||||
}}
|
||||
><svelte:component
|
||||
this={subMenuItem.icon}
|
||||
class="h-5 w-5"
|
||||
/>{subMenuItem.title}</a
|
||||
>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
{:else}
|
||||
<li class="hover-bordered">
|
||||
<a
|
||||
href={menuItem.href}
|
||||
class="text-lg font-bold {menuItem.active ? 'bg-base-100' : ''}"
|
||||
on:click={() => {
|
||||
setActiveMenuItem(menuItems, menuItem.title);
|
||||
menuItems = menuItems;
|
||||
}}><svelte:component this={menuItem.icon} class="h-6 w-6" />{menuItem.title}</a
|
||||
>
|
||||
</li>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<div class="flex-col" />
|
||||
<div class="flex-grow" />
|
||||
|
||||
{#if $page.data.features.security}
|
||||
<div class="flex items-center">
|
||||
<Avatar class="h-8 w-8" />
|
||||
<span class="flex-grow px-4 text-xl font-bold">{$user.username}</span>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="btn btn-ghost"
|
||||
on:click={() => {
|
||||
user.invalidate();
|
||||
}}
|
||||
>
|
||||
<Logout class="h-8 w-8 rotate-180" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="divider my-0" />
|
||||
<div class="flex items-center">
|
||||
{#if github.active}
|
||||
<a href={github.href} class="btn btn-ghost" target="_blank" rel="noopener noreferrer"
|
||||
><MdiGithub class="h-5 w-5" /></a
|
||||
>
|
||||
{/if}
|
||||
<div class="inline-flex flex-grow items-center justify-end text-sm">
|
||||
<Copyright class="h-4 w-4" /><span class="px-2">{copyright}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,56 @@
|
||||
<!-- <script lang="ts">
|
||||
import Info from '$lib/components/settings/Info.svelte';
|
||||
import Log from '$lib/components/settings/Log.svelte';
|
||||
import Configuration from '$lib/components/settings/Configuration.svelte';
|
||||
import Calibration from '$lib/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> -->
|
||||
|
||||
<h1 class="text-2xl font-bold">Settings</h1>
|
||||
|
||||
<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>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { telemetry } from '$lib/stores/telemetry';
|
||||
import { openModal, closeModal } from 'svelte-modals';
|
||||
import { user } from '$lib/stores/user';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import WiFiOff from '~icons/tabler/wifi-off';
|
||||
import Hamburger from '~icons/tabler/menu-2';
|
||||
import Power from '~icons/tabler/power';
|
||||
import Cancel from '~icons/tabler/x';
|
||||
import RssiIndicator from '$lib/components/RSSIIndicator.svelte';
|
||||
import BatteryIndicator from '$lib/components/BatteryIndicator.svelte';
|
||||
import UpdateIndicator from '$lib/components/UpdateIndicator.svelte';
|
||||
import MdiWeatherSunny from '~icons/mdi/weather-sunny';
|
||||
import MdiMoonAndStars from '~icons/mdi/moon-and-stars';
|
||||
|
||||
async function postSleep() {
|
||||
const response = await fetch('/api/sleep', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function confirmSleep() {
|
||||
openModal(ConfirmDialog, {
|
||||
title: 'Confirm Power Down',
|
||||
message: 'Are you sure you want to switch off the device?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Switch Off', icon: Power }
|
||||
},
|
||||
onConfirm: () => {
|
||||
closeModal();
|
||||
postSleep();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="navbar bg-base-300 sticky top-0 z-10 h-12 min-h-fit drop-shadow-lg lg:h-16 gap-2">
|
||||
<div class="flex-1">
|
||||
<!-- Page Hamburger Icon here -->
|
||||
<label for="main-menu" class="btn btn-ghost btn-circle btn-sm drawer-button"
|
||||
><Hamburger class="h-6 w-auto" /></label
|
||||
>
|
||||
<span class="px-2 text-xl font-bold lg:text-2xl">{$page.data.title}</span>
|
||||
</div>
|
||||
<div class="indicator flex-none">
|
||||
<UpdateIndicator />
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<label class="swap swap-rotate">
|
||||
<input type="checkbox" value="light" class="theme-controller"/>
|
||||
<MdiWeatherSunny class="swap-off h-7 w-7"/>
|
||||
<MdiMoonAndStars class="swap-on h-7 w-7"/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
{#if $telemetry.rssi.disconnected}
|
||||
<WiFiOff class="h-7 w-7" />
|
||||
{:else}
|
||||
<RssiIndicator showDBm={false} rssi_dbm={$telemetry.rssi.rssi} class="h-7 w-7" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $page.data.features.battery}
|
||||
<div class="flex-none">
|
||||
<BatteryIndicator
|
||||
charging={$telemetry.battery.charging}
|
||||
soc={$telemetry.battery.soc}
|
||||
class="h-7 w-7"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $page.data.features.sleep}
|
||||
<div class="flex-none">
|
||||
<button class="btn btn-square btn-ghost h-9 w-10" on:click={confirmSleep}>
|
||||
<Power class="text-error h-9 w-9" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export const load = (async () => {
|
||||
goto('/');
|
||||
return;
|
||||
}) satisfies PageLoad;
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import SystemMetrics from './SystemMetrics.svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
if (!$page.data.features.analytics) {
|
||||
goto('/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="mx-0 my-1 flex flex-col space-y-4
|
||||
sm:mx-8 sm:my-8"
|
||||
>
|
||||
<SystemMetrics />
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
return { title: 'System Metrics' };
|
||||
}) satisfies PageLoad;
|
||||
@@ -0,0 +1,303 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import Metrics from '~icons/tabler/report-analytics';
|
||||
import { daisyColor } from '$lib/DaisyUiHelper';
|
||||
import { analytics } from '$lib/stores/analytics';
|
||||
|
||||
Chart.register(...registerables);
|
||||
|
||||
let heapChartElement: HTMLCanvasElement;
|
||||
let heapChart: Chart;
|
||||
|
||||
let filesystemChartElement: HTMLCanvasElement;
|
||||
let filesystemChart: Chart;
|
||||
|
||||
let temperatureChartElement: HTMLCanvasElement;
|
||||
let temperatureChart: Chart;
|
||||
|
||||
onMount(() => {
|
||||
heapChart = new Chart(heapChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Free Heap',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.free_heap,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: 'Max Alloc Heap',
|
||||
borderColor: daisyColor('--s'),
|
||||
backgroundColor: daisyColor('--s', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.max_alloc_heap,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 1
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Heap [kb]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: Math.round($analytics.total_heap[0]),
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
filesystemChart = new Chart(filesystemChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'File System Used',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.fs_used,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 1
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'File System [kb]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: Math.round($analytics.fs_total[0]),
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
temperatureChart = new Chart(temperatureChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Core Temperature',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.core_temp,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 1
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Core Temperature [°C]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
suggestedMin: 20,
|
||||
suggestedMax: 100,
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
setInterval(() => {
|
||||
updateData(), 2000;
|
||||
});
|
||||
});
|
||||
|
||||
function updateData() {
|
||||
heapChart.data.labels = $analytics.uptime;
|
||||
heapChart.data.datasets[0].data = $analytics.free_heap;
|
||||
heapChart.data.datasets[1].data = $analytics.max_alloc_heap;
|
||||
heapChart.update('none');
|
||||
|
||||
filesystemChart.data.labels = $analytics.uptime;
|
||||
filesystemChart.data.datasets[0].data = $analytics.fs_used;
|
||||
filesystemChart.update('none');
|
||||
|
||||
temperatureChart.data.labels = $analytics.uptime;
|
||||
temperatureChart.data.datasets[0].data = $analytics.core_temp;
|
||||
temperatureChart.update('none');
|
||||
}
|
||||
|
||||
function convertSeconds(seconds: number) {
|
||||
// Calculate the number of seconds, minutes, hours, and days
|
||||
let minutes = Math.floor(seconds / 60);
|
||||
let hours = Math.floor(minutes / 60);
|
||||
let days = Math.floor(hours / 24);
|
||||
|
||||
// Calculate the remaining hours, minutes, and seconds
|
||||
hours = hours % 24;
|
||||
minutes = minutes % 60;
|
||||
seconds = seconds % 60;
|
||||
|
||||
// Create the formatted string
|
||||
let result = '';
|
||||
if (days > 0) {
|
||||
result += days + ' day' + (days > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
if (hours > 0) {
|
||||
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
if (minutes > 0) {
|
||||
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
result += seconds + ' second' + (seconds > 1 ? 's' : '');
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
<Metrics slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">System Metrics</span>
|
||||
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={heapChartElement} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-52"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={filesystemChartElement} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-52"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={temperatureChartElement} />
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import SystemStatus from './SystemStatus.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="mx-0 my-1 flex flex-col space-y-4
|
||||
sm:mx-8 sm:my-8"
|
||||
>
|
||||
<SystemStatus />
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
return { title: 'System Status' };
|
||||
}) satisfies PageLoad;
|
||||
@@ -0,0 +1,364 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { openModal, closeModal } from 'svelte-modals';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { page } from '$app/stores';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import CPU from '~icons/tabler/cpu';
|
||||
import CPP from '~icons/tabler/binary';
|
||||
import Power from '~icons/tabler/reload';
|
||||
import Sleep from '~icons/tabler/zzz';
|
||||
import FactoryReset from '~icons/tabler/refresh-dot';
|
||||
import Speed from '~icons/tabler/activity';
|
||||
import Flash from '~icons/tabler/device-sd-card';
|
||||
import Pyramid from '~icons/tabler/pyramid';
|
||||
import Sketch from '~icons/tabler/chart-pie';
|
||||
import Folder from '~icons/tabler/folder';
|
||||
import Heap from '~icons/tabler/box-model';
|
||||
import Cancel from '~icons/tabler/x';
|
||||
import Temperature from '~icons/tabler/temperature';
|
||||
import Health from '~icons/tabler/stethoscope';
|
||||
import Stopwatch from '~icons/tabler/24-hours';
|
||||
import SDK from '~icons/tabler/sdk';
|
||||
import type { SystemInformation, Analytics } from '$lib/types/models';
|
||||
import { socket } from '$lib/stores/socket';
|
||||
|
||||
let systemInformation: SystemInformation;
|
||||
|
||||
async function getSystemStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/systemStatus', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
systemInformation = await response.json();
|
||||
} catch (error) {
|
||||
console.log('Error:', error);
|
||||
}
|
||||
return systemInformation;
|
||||
}
|
||||
|
||||
onMount(() => socket.on('analytics', handleSystemData));
|
||||
|
||||
onDestroy(() => socket.off('analytics', handleSystemData));
|
||||
|
||||
const handleSystemData = (data: Analytics) =>
|
||||
(systemInformation = { ...systemInformation, ...data });
|
||||
|
||||
async function postRestart() {
|
||||
const response = await fetch('/api/restart', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function confirmRestart() {
|
||||
openModal(ConfirmDialog, {
|
||||
title: 'Confirm Restart',
|
||||
message: 'Are you sure you want to restart the device?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Restart', icon: Power }
|
||||
},
|
||||
onConfirm: () => {
|
||||
closeModal();
|
||||
postRestart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function postFactoryReset() {
|
||||
const response = await fetch('/api/factoryReset', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function confirmReset() {
|
||||
openModal(ConfirmDialog, {
|
||||
title: 'Confirm Factory Reset',
|
||||
message: 'Are you sure you want to reset the device to its factory defaults?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Factory Reset', icon: FactoryReset }
|
||||
},
|
||||
onConfirm: () => {
|
||||
closeModal();
|
||||
postFactoryReset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function postSleep() {
|
||||
const response = await fetch('/api/sleep', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function confirmSleep() {
|
||||
openModal(ConfirmDialog, {
|
||||
title: 'Confirm Going to Sleep',
|
||||
message: 'Are you sure you want to put the device into sleep?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Sleep', icon: Sleep }
|
||||
},
|
||||
onConfirm: () => {
|
||||
closeModal();
|
||||
postSleep();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function convertSeconds(seconds: number) {
|
||||
// Calculate the number of seconds, minutes, hours, and days
|
||||
let minutes = Math.floor(seconds / 60);
|
||||
let hours = Math.floor(minutes / 60);
|
||||
let days = Math.floor(hours / 24);
|
||||
|
||||
// Calculate the remaining hours, minutes, and seconds
|
||||
hours = hours % 24;
|
||||
minutes = minutes % 60;
|
||||
seconds = seconds % 60;
|
||||
|
||||
// Create the formatted string
|
||||
let result = '';
|
||||
if (days > 0) {
|
||||
result += days + ' day' + (days > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
if (hours > 0) {
|
||||
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
if (minutes > 0) {
|
||||
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
result += seconds + ' second' + (seconds > 1 ? 's' : '');
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
<Health slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">System Status</span>
|
||||
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getSystemStatus()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||
<CPU class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Chip</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.cpu_type} Rev {systemInformation.cpu_rev}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||
<SDK class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">SDK Version</div>
|
||||
<div class="text-sm opacity-75">
|
||||
ESP-IDF {systemInformation.sdk_version} / Arduino {systemInformation.arduino_version}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||
<CPP class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Firmware Version</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.firmware_version}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||
<Speed class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">CPU Frequency</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.cpu_freq_mhz} MHz {systemInformation.cpu_cores == 2
|
||||
? 'Dual Core'
|
||||
: 'Single Core'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||
<Heap class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Heap (Free / Max Alloc)</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.free_heap.toLocaleString('en-US')} / {systemInformation.max_alloc_heap.toLocaleString(
|
||||
'en-US'
|
||||
)} bytes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||
<Pyramid class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">PSRAM (Size / Free)</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.psram_size.toLocaleString('en-US')} / {systemInformation.psram_size.toLocaleString(
|
||||
'en-US'
|
||||
)} bytes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||
<Sketch class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Sketch (Used / Free)</div>
|
||||
<div class="flex flex-wrap justify-start gap-1 text-sm opacity-75">
|
||||
<span>
|
||||
{(
|
||||
(systemInformation.sketch_size / systemInformation.free_sketch_space) *
|
||||
100
|
||||
).toFixed(1)} % of
|
||||
{(systemInformation.free_sketch_space / 1000000).toLocaleString('en-US')} MB used
|
||||
</span>
|
||||
|
||||
<span>
|
||||
({(
|
||||
(systemInformation.free_sketch_space - systemInformation.sketch_size) /
|
||||
1000000
|
||||
).toLocaleString('en-US')} MB free)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||
<Flash class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Flash Chip (Size / Speed)</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{(systemInformation.flash_chip_size / 1000000).toLocaleString('en-US')} MB / {(
|
||||
systemInformation.flash_chip_speed / 1000000
|
||||
).toLocaleString('en-US')} MHz
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||
<Folder class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">File System (Used / Total)</div>
|
||||
<div class="flex flex-wrap justify-start gap-1 text-sm opacity-75">
|
||||
<span
|
||||
>{((systemInformation.fs_used / systemInformation.fs_total) * 100).toFixed(1)} % of {(
|
||||
systemInformation.fs_total / 1000000
|
||||
).toLocaleString('en-US')} MB used</span
|
||||
>
|
||||
|
||||
<span
|
||||
>({(
|
||||
(systemInformation.fs_total - systemInformation.fs_used) /
|
||||
1000000
|
||||
).toLocaleString('en-US')}
|
||||
MB free)</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||
<Temperature class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Core Temperature</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.core_temp == 53.33
|
||||
? 'NaN'
|
||||
: systemInformation.core_temp.toFixed(2) + ' °C'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||
<Stopwatch class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Uptime</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{convertSeconds(systemInformation.uptime)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||
<Power class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Reset Reason</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.cpu_reset_reason}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-wrap justify-end gap-2">
|
||||
{#if $page.data.features.sleep}
|
||||
<button class="btn btn-primary inline-flex items-center" on:click={confirmSleep}
|
||||
><Sleep class="mr-2 h-5 w-5" /><span>Sleep</span></button
|
||||
>
|
||||
{/if}
|
||||
{#if !$page.data.features.security || $user.admin}
|
||||
<button class="btn btn-primary inline-flex items-center" on:click={confirmRestart}
|
||||
><Power class="mr-2 h-5 w-5" /><span>Restart</span></button
|
||||
>
|
||||
<button class="btn btn-secondary inline-flex items-center" on:click={confirmReset}
|
||||
><FactoryReset class="mr-2 h-5 w-5" /><span>Factory Reset</span></button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user