Adds actuator service to sync angles
This commit is contained in:
@@ -2,6 +2,10 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
#nipple_0_0, #nipple_1_1 {
|
||||||
|
z-index: 10!important;
|
||||||
|
}
|
||||||
|
|
||||||
#three-gui-panel {
|
#three-gui-panel {
|
||||||
top: 64px;
|
top: 64px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const loadModelAsync = async (
|
|||||||
resolve(Result.err('Failed to load model', error));
|
resolve(Result.err('Failed to load model', error));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(error) => reject(error)
|
(error) => resolve(Result.err('Failed to load model', error))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,11 +17,10 @@
|
|||||||
|
|
||||||
export let data: LayoutData;
|
export let data: LayoutData;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(async () => {
|
||||||
if ($user.bearer_token !== '') {
|
if ($user.bearer_token !== '') {
|
||||||
validateUser($user);
|
validateUser($user);
|
||||||
}
|
}
|
||||||
menuOpen = false;
|
|
||||||
connectToEventSource();
|
connectToEventSource();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -29,10 +28,6 @@
|
|||||||
NotificationSource?.close();
|
NotificationSource?.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
NotificationSource.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function validateUser(userdata: userProfile) {
|
async function validateUser(userdata: userProfile) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/rest/verifyAuthorization', {
|
const response = await fetch('/rest/verifyAuthorization', {
|
||||||
@@ -186,15 +181,13 @@
|
|||||||
<Statusbar />
|
<Statusbar />
|
||||||
|
|
||||||
<!-- Main page content here -->
|
<!-- Main page content here -->
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<!-- Side Navigation -->
|
<!-- Side Navigation -->
|
||||||
<div class="drawer-side z-30 shadow-lg">
|
<div class="drawer-side z-30 shadow-lg">
|
||||||
<label for="main-menu" class="drawer-overlay" />
|
<label for="main-menu" class="drawer-overlay" />
|
||||||
<Menu
|
<Menu
|
||||||
on:menuClicked={() => {
|
on:menuClicked={() => menuOpen = false}
|
||||||
menuOpen = false;
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,17 +14,8 @@ const registerFetchIntercept = async () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const setup = async () => {
|
const loadModelFiles = async () => {
|
||||||
const outControllerData = (await import('$lib/stores/model-store')).outControllerData;
|
|
||||||
const mode = (await import('$lib/stores/model-store')).mode;
|
|
||||||
const socketLocation = (await import('$lib/utilities/location-utilities')).socketLocation;
|
|
||||||
const socketService = (await import('$lib/services/socket-service')).default;
|
|
||||||
socketService.connect(socketLocation);
|
|
||||||
socketService.addPublisher(outControllerData);
|
|
||||||
socketService.addPublisher(mode, 'mode');
|
|
||||||
await registerFetchIntercept();
|
|
||||||
const modelRes = await loadModelAsync('/spot_micro.urdf.xacro');
|
const modelRes = await loadModelAsync('/spot_micro.urdf.xacro');
|
||||||
|
|
||||||
if (modelRes.isOk()) {
|
if (modelRes.isOk()) {
|
||||||
const [urdf, JOINT_NAME] = modelRes.inner;
|
const [urdf, JOINT_NAME] = modelRes.inner;
|
||||||
jointNames.set(JOINT_NAME);
|
jointNames.set(JOINT_NAME);
|
||||||
@@ -34,8 +25,9 @@ const setup = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const load = async () => {
|
export const load = async ({ fetch }) => {
|
||||||
await setup();
|
await registerFetchIntercept();
|
||||||
|
await loadModelFiles();
|
||||||
const result = await fetch('/rest/features');
|
const result = await fetch('/rest/features');
|
||||||
const features = await result.json();
|
const features = await result.json();
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="hero bg-base-100 h-screen">
|
<div class="hero bg-base-100 h-screen">
|
||||||
<div class="card md:card-side bg-base-200 shadow-2xl">
|
<div class="card md:card-side bg-base-200 shadow-2xl flex justify-center items-center">
|
||||||
<div class="w-64 h-64">
|
<div class="w-64 h-64">
|
||||||
<Visualization sky={false} orbit={true} panel={false}/>
|
<Visualization sky={false} orbit={true} panel={false}/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,33 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { user } from '$lib/stores/user';
|
import { user } from '$lib/stores/user';
|
||||||
|
import { servoAngles } from '$lib/stores';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { notifications } from '$lib/components/toasts/notifications';
|
import { notifications } from '$lib/components/toasts/notifications';
|
||||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||||
import Light from '~icons/tabler/bulb';
|
import Light from '~icons/tabler/bulb';
|
||||||
import Info from '~icons/tabler/info-circle';
|
|
||||||
import Save from '~icons/tabler/device-floppy';
|
|
||||||
import Reload from '~icons/tabler/reload';
|
|
||||||
|
|
||||||
type LightState = {
|
|
||||||
led_on: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
let lightState: LightState = { led_on: false };
|
async function getActuatorState() {
|
||||||
|
|
||||||
let lightOn = false;
|
|
||||||
|
|
||||||
async function getLightstate() {
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/rest/lightState', {
|
const response = await fetch('/rest/actuators', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',
|
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const light = await response.json();
|
const actuators = await response.json();
|
||||||
lightOn = light.led_on;
|
servoAngles.set(actuators.state);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
}
|
}
|
||||||
@@ -36,13 +27,13 @@
|
|||||||
|
|
||||||
const ws_token = $page.data.features.security ? '?access_token=' + $user.bearer_token : '';
|
const ws_token = $page.data.features.security ? '?access_token=' + $user.bearer_token : '';
|
||||||
|
|
||||||
const lightStateSocket = new WebSocket('ws://' + $page.url.host + '/ws/lightState' + ws_token);
|
const socket = new WebSocket('ws://' + $page.url.host + '/ws' + ws_token);
|
||||||
|
|
||||||
lightStateSocket.onopen = (event) => {
|
socket.onopen = (event) => {
|
||||||
lightStateSocket.send('Hello');
|
// socket.send('Hello');
|
||||||
};
|
};
|
||||||
|
|
||||||
lightStateSocket.addEventListener('close', (event) => {
|
socket.addEventListener('close', (event) => {
|
||||||
const closeCode = event.code;
|
const closeCode = event.code;
|
||||||
const closeReason = event.reason;
|
const closeReason = event.reason;
|
||||||
console.log('WebSocket closed with code:', closeCode);
|
console.log('WebSocket closed with code:', closeCode);
|
||||||
@@ -50,88 +41,43 @@
|
|||||||
notifications.error('Websocket disconnected', 5000);
|
notifications.error('Websocket disconnected', 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
lightStateSocket.onmessage = (event) => {
|
socket.onmessage = (event) => {
|
||||||
const message = JSON.parse(event.data);
|
const message = JSON.parse(event.data);
|
||||||
if (message.type != 'id') {
|
if (message.type != 'id') {
|
||||||
lightState = message;
|
servoAngles.set(message.state);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onDestroy(() => lightStateSocket.close());
|
onDestroy(() => socket.close());
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => getActuatorState());
|
||||||
getLightstate();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function postLightstate() {
|
function updateAngle(index: number, value: number) {
|
||||||
try {
|
servoAngles.update(($servoAngles) => {
|
||||||
const response = await fetch('/rest/lightState', {
|
$servoAngles[index] = value;
|
||||||
method: 'POST',
|
socket.send(JSON.stringify({ state: $servoAngles }));
|
||||||
headers: {
|
return $servoAngles;
|
||||||
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',
|
});
|
||||||
'Content-Type': 'application/json'
|
}
|
||||||
},
|
|
||||||
body: JSON.stringify({ led_on: lightOn })
|
|
||||||
});
|
|
||||||
if (response.status == 200) {
|
|
||||||
notifications.success('Light state updated.', 3000);
|
|
||||||
const light = await response.json();
|
|
||||||
lightOn = light.led_on;
|
|
||||||
} else {
|
|
||||||
notifications.error('User not authorized.', 3000);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SettingsCard collapsible={false}>
|
<SettingsCard collapsible={false}>
|
||||||
<Light slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
<Light slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||||
<span slot="title">Light State</span>
|
<span slot="title">Light State</span>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<h1 class="text-xl font-semibold">REST Example</h1>
|
|
||||||
<div class="alert alert-info my-2 shadow-lg">
|
|
||||||
<Info class="h-6 w-6 flex-shrink-0 stroke-current" />
|
|
||||||
<span>The form below controls the LED via the RESTful service exposed by the ESP device.</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row flex-wrap justify-between gap-x-2">
|
|
||||||
<div class="form-control w-52">
|
|
||||||
<label class="label cursor-pointer">
|
|
||||||
<span class="mr-4">Light State?</span>
|
|
||||||
<input type="checkbox" bind:checked={lightOn} class="checkbox checkbox-primary" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex-grow" />
|
|
||||||
<button class="btn btn-primary inline-flex items-center" on:click={postLightstate}
|
|
||||||
><Save class="mr-2 h-5 w-5" /><span>Save</span></button
|
|
||||||
>
|
|
||||||
<button class="btn btn-primary inline-flex items-center" on:click={getLightstate}
|
|
||||||
><Reload class="mr-2 h-5 w-5" /><span>Reload</span></button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="divider" />
|
|
||||||
<h1 class="text-xl font-semibold">Websocket Example</h1>
|
<h1 class="text-xl font-semibold">Websocket Example</h1>
|
||||||
<div class="alert alert-info my-2 shadow-lg">
|
<div class="form-control">
|
||||||
<Info class="h-6 w-6 flex-shrink-0 stroke-current" />
|
<div class="w-full flex justify-between">
|
||||||
<span
|
{#each $servoAngles as angle, index}
|
||||||
>The switch below controls the LED via the WebSocket. It will automatically update whenever
|
<input
|
||||||
the LED state changes.</span
|
type="number"
|
||||||
>
|
class="input w-12"
|
||||||
</div>
|
id={`angle-${index}`}
|
||||||
<div class="form-control w-52">
|
value={angle}
|
||||||
<label class="label cursor-pointer">
|
on:input={(event) => updateAngle(index, parseFloat(event.target?.value))}
|
||||||
<span class="">Light State?</span>
|
/>
|
||||||
<input
|
{/each}
|
||||||
type="checkbox"
|
</div>
|
||||||
class="toggle toggle-primary"
|
|
||||||
bind:checked={lightState.led_on}
|
|
||||||
on:change={() => {
|
|
||||||
lightStateSocket.send(JSON.stringify(lightState));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|||||||
@@ -180,7 +180,7 @@
|
|||||||
<h1 class="px-4 text-2xl font-bold">{appName}</h1>
|
<h1 class="px-4 text-2xl font-bold">{appName}</h1>
|
||||||
</a>
|
</a>
|
||||||
<ul class="menu rounded-box menu-vertical flex-nowrap overflow-y-auto">
|
<ul class="menu rounded-box menu-vertical flex-nowrap overflow-y-auto">
|
||||||
{#each menuItems as menuItem, i (menuItem.title)}
|
{#each menuItems as menuItem (menuItem.title)}
|
||||||
{#if menuItem.feature}
|
{#if menuItem.feature}
|
||||||
{#if menuItem.submenu}
|
{#if menuItem.submenu}
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* ESP32 SvelteKit
|
||||||
|
*
|
||||||
|
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||||
|
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||||
|
* https://github.com/theelims/ESP32-sveltekit
|
||||||
|
*
|
||||||
|
* Copyright (C) 2018 - 2023 rjwats
|
||||||
|
* Copyright (C) 2023 theelims
|
||||||
|
*
|
||||||
|
* All Rights Reserved. This software may be modified and distributed under
|
||||||
|
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||||
|
**/
|
||||||
|
|
||||||
|
#include <ActuatorStateService.h>
|
||||||
|
|
||||||
|
ActuatorStateService::ActuatorStateService(
|
||||||
|
PsychicHttpServer *server,
|
||||||
|
SecurityManager *securityManager
|
||||||
|
) : _httpEndpoint(
|
||||||
|
ActuatorState::read,
|
||||||
|
ActuatorState::update,
|
||||||
|
this,
|
||||||
|
server,
|
||||||
|
ACTUATOR_SETTINGS_ENDPOINT_PATH,
|
||||||
|
securityManager,
|
||||||
|
AuthenticationPredicates::IS_AUTHENTICATED),
|
||||||
|
_webSocketServer(
|
||||||
|
ActuatorState::read,
|
||||||
|
ActuatorState::update,
|
||||||
|
this,
|
||||||
|
server,
|
||||||
|
ACTUATOR_SETTINGS_SOCKET_PATH,
|
||||||
|
securityManager,
|
||||||
|
AuthenticationPredicates::IS_AUTHENTICATED)
|
||||||
|
{
|
||||||
|
// Setup actuator hardware
|
||||||
|
|
||||||
|
addUpdateHandler([&](const String &originId){ onConfigUpdated(); }, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActuatorStateService::begin()
|
||||||
|
{
|
||||||
|
_httpEndpoint.begin();
|
||||||
|
_webSocketServer.begin();
|
||||||
|
onConfigUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActuatorStateService::onConfigUpdated()
|
||||||
|
{
|
||||||
|
log_i("Update hardware interface for actuators");
|
||||||
|
// UPDATE HARDWARE INTERFACE FOR ACTUATORS
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#ifndef LightStateService_h
|
||||||
|
#define LightStateService_h
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ESP32 SvelteKit
|
||||||
|
*
|
||||||
|
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||||
|
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||||
|
* https://github.com/theelims/ESP32-sveltekit
|
||||||
|
*
|
||||||
|
* Copyright (C) 2018 - 2023 rjwats
|
||||||
|
* Copyright (C) 2023 theelims
|
||||||
|
*
|
||||||
|
* All Rights Reserved. This software may be modified and distributed under
|
||||||
|
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||||
|
**/
|
||||||
|
|
||||||
|
#include <HttpEndpoint.h>
|
||||||
|
#include <WebSocketServer.h>
|
||||||
|
|
||||||
|
#define ACTUATOR_SETTINGS_ENDPOINT_PATH "/rest/actuators"
|
||||||
|
#define ACTUATOR_SETTINGS_SOCKET_PATH "/ws"
|
||||||
|
|
||||||
|
class ActuatorState
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
int16_t state[12] = {0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90};
|
||||||
|
|
||||||
|
static void read(ActuatorState &settings, JsonObject &root)
|
||||||
|
{
|
||||||
|
JsonArray array = root.createNestedArray("state");
|
||||||
|
for(int i = 0; i < 12; i++)
|
||||||
|
{
|
||||||
|
array.add(settings.state[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static StateUpdateResult update(JsonObject &root, ActuatorState &actuatorState)
|
||||||
|
{
|
||||||
|
Serial.print("New state array: [");
|
||||||
|
JsonArray array = root["state"];
|
||||||
|
bool changed = false;
|
||||||
|
for(int i = 0; i < 12; i++)
|
||||||
|
{
|
||||||
|
Serial.print(array[i].as<int16_t>());
|
||||||
|
Serial.print(", ");
|
||||||
|
if (actuatorState.state[i] != array[i].as<int16_t>())
|
||||||
|
{
|
||||||
|
actuatorState.state[i] = array[i];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Serial.println("]");
|
||||||
|
return changed ? StateUpdateResult::CHANGED : StateUpdateResult::UNCHANGED;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void homeAssistRead(ActuatorState &settings, JsonObject &root)
|
||||||
|
{
|
||||||
|
JsonArray array = root.createNestedArray("state");
|
||||||
|
for(int i = 0; i < 12; i++)
|
||||||
|
{
|
||||||
|
array.add(settings.state[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static StateUpdateResult homeAssistUpdate(JsonObject &root, ActuatorState &actuatorState)
|
||||||
|
{
|
||||||
|
JsonArray array = root["state"];
|
||||||
|
if(array.size() != 12) return StateUpdateResult::ERROR;
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
for(int i = 0; i < 12; i++)
|
||||||
|
{
|
||||||
|
if (actuatorState.state[i] != array[i].as<int16_t>())
|
||||||
|
{
|
||||||
|
actuatorState.state[i] = array[i];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed ? StateUpdateResult::CHANGED : StateUpdateResult::UNCHANGED;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ActuatorStateService : public StatefulService<ActuatorState> {
|
||||||
|
public:
|
||||||
|
ActuatorStateService(PsychicHttpServer *server, SecurityManager *securityManager);
|
||||||
|
void begin();
|
||||||
|
|
||||||
|
private:
|
||||||
|
HttpEndpoint<ActuatorState> _httpEndpoint;
|
||||||
|
WebSocketServer<ActuatorState> _webSocketServer;
|
||||||
|
|
||||||
|
void onConfigUpdated();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
+12
-4
@@ -1,18 +1,26 @@
|
|||||||
#include <ESP32SvelteKit.h>
|
#include <ESP32SvelteKit.h>
|
||||||
|
#include <ActuatorStateService.h>
|
||||||
#include <PsychicHttpServer.h>
|
#include <PsychicHttpServer.h>
|
||||||
|
|
||||||
#define SERIAL_BAUD_RATE 115200
|
#define SERIAL_BAUD_RATE 115200
|
||||||
|
|
||||||
PsychicHttpServer server;
|
DRAM_ATTR PsychicHttpServer server;
|
||||||
|
|
||||||
ESP32SvelteKit esp32sveltekit(&server, 120);
|
DRAM_ATTR ESP32SvelteKit esp32sveltekit(&server, 120);
|
||||||
|
|
||||||
void setup() {
|
ActuatorStateService actuatorStateService = ActuatorStateService(&server, esp32sveltekit.getSecurityManager());
|
||||||
|
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
Serial.begin(SERIAL_BAUD_RATE);
|
Serial.begin(SERIAL_BAUD_RATE);
|
||||||
|
|
||||||
esp32sveltekit.begin();
|
esp32sveltekit.begin();
|
||||||
|
|
||||||
|
actuatorStateService.begin();
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop()
|
||||||
|
{
|
||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user