Added actions endpoint multiple clients can use to simultaneously control actions

This commit is contained in:
2026-03-20 11:23:51 +01:00
parent 28b120b88e
commit 2cc91c1d5d
15 changed files with 630 additions and 156 deletions

View File

@@ -1,57 +1,51 @@
<script setup lang="ts">
import RobotViewport from './components/RobotViewport.vue'
import { computed } from 'vue'
import { connectionStatus } from './stores/connection-status'
import { ros } from './ts/ros_client'
import { rosBackend } from './ts/robot_bridge'
import { ROS2TFClient, Transform } from 'roslib'
const store = connectionStatus()
const connected = computed(() => {
return store.backendConnected && store.rosbridgeConnected
})
const reconnect = () => {
ros.manualConnect()
rosBackend.manualConnect()
}
const rosout = ros.ros.Topic({
name: '/rosout',
messageType: 'rcl_interfaces/msg/Log',
})
rosout.subscribe((msg) => {
console.log(msg)
})
</script>
<template>
<div class="container">
<div class="row-container">
<div class="box box1"></div>
<div class="box box2">
<RobotViewport />
</div>
<div class="box box3"></div>
<div>
<div v-if="!connected" class="box-container">
Connecting...
<button id="reconnect" @click="reconnect">Reconnect</button>
</div>
<div class="box-container">
<h3>Current position:</h3>
</div>
</div>
</template>
<style scoped>
.container {
display: flex;
justify-content: stretch;
align-items: start;
width: 100vw;
height: calc(100vh - 1.5rem);
padding-top: 1.5rem;
.box-container {
margin: 30px;
padding: 10px;
border: 2px solid white;
}
.row-container {
width: 100vw;
max-height: 100%;
flex-shrink: 1;
display: flex;
margin: 0;
padding: 0;
justify-content: center;
flex-direction: row;
}
.box1 {
background-color: pink;
width: 3.5rem;
}
.box2 {
background-color: lightblue;
aspect-ratio: 3/2;
max-width: calc(150vh - 13.5rem);
max-height: calc((100vw - 13.5) * 2 / 3);
flex-grow: 2;
}
.box3 {
flex-grow: 1;
min-width: 10rem;
max-width: 20rem;
background-color: darkblue;
#reconnect {
margin-left: 2rem;
background-color: green;
}
</style>

View File

@@ -6,4 +6,5 @@ body,
min-height: 100vh;
max-width: 100vw;
background-color: #242130;
color: #ffffff;
}

View File

@@ -0,0 +1,13 @@
<script lang="ts"></script>
<template>
<div>
<button>Send</button>
<label for="angle">Angle: </label>
<input type="text" name="angle" id="" />
<label for="angle">Min Angle: </label>
<input type="text" name="min_angle" id="" value="0" />
</div>
</template>
<style scoped></style>

View File

@@ -3,34 +3,11 @@ import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import { ros } from './ros_client'
import '@/assets/scss/global.scss'
import '@/ts/robot_bridge'
import '@/ts/ros_client'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
ros.connect('http://localhost:3000')
ros.on('error', (error) => {
console.log(error)
setTimeout(() => {
ros.connect('http://localhost:3000').catch()
}, 1000)
})
ros.on('close', () => {
setTimeout(() => {
if (!ros.isConnected) {
ros.connect('http://localhost:3000').catch()
}
}, 1000)
})
ros.on('connection', () => {
console.log('Connection made!')
})

View File

@@ -1,9 +0,0 @@
import * as ROSLIB from 'roslib'
interface BehaviorTreeList {
tree_ids: Array<string>
}
const ros = new ROSLIB.Ros()
export { ros, type BehaviorTreeList }

View File

@@ -0,0 +1,8 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
export const connectionStatus = defineStore('connstatus', () => {
const rosbridgeConnected = ref(false)
const backendConnected = ref(false)
return { rosbridgeConnected, backendConnected }
})

View File

@@ -0,0 +1,81 @@
import { connectionStatus } from '@/stores/connection-status'
class BackendBridge {
private static instance: BackendBridge
private readonly url: string = 'http://localhost:8000/action/ws'
private socket: WebSocket | null = null
private timeout: number | null = null
private reconnectAttempts = 0
private readonly maxAttempts = 10
private readonly baseDelay = 100
private constructor() {
this.connect()
}
public static getInstance(): BackendBridge {
if (!BackendBridge.instance) {
BackendBridge.instance = new BackendBridge()
}
return this.instance
}
public manualConnect() {
if (this.timeout) clearTimeout(this.timeout)
this.connect()
}
private connect(): void {
console.log(`Connecting to ${this.url}...`)
if (this.socket) {
connectionStatus().backendConnected = false
this.socket.onclose = null
this.socket.close()
}
this.socket = new WebSocket(this.url)
this.socket.onopen = () => {
connectionStatus().backendConnected = true
console.log('Connected successfully!')
this.reconnectAttempts = 0
}
this.socket.onclose = (event) => {
connectionStatus().backendConnected = false
console.warn('Socket closed:', event)
this.handleReconnect()
}
this.socket.onerror = (error) => {
console.error('Socket error:', error)
}
}
// Backoff algorithm
private handleReconnect(): void {
if (this.reconnectAttempts < this.maxAttempts) {
this.reconnectAttempts++
const delay = this.baseDelay * Math.pow(2, this.reconnectAttempts - 1)
console.log(
`Retrying in ${delay}ms... (Attempt ${this.reconnectAttempts}/${this.maxAttempts})`,
)
this.timeout = setTimeout(() => {
this.timeout = null
console.log(`Starting attempt [${this.reconnectAttempts}/${this.maxAttempts}]`)
this.connect()
}, delay)
} else {
console.error('Max reconnection attempts reached. Please check your connection.')
}
}
}
const rosBackend = BackendBridge.getInstance()
export { rosBackend }
export type { BackendBridge }

View File

@@ -0,0 +1,84 @@
import * as ROSLIB from 'roslib'
import { connectionStatus } from '@/stores/connection-status'
interface BehaviorTreeList {
tree_ids: Array<string>
}
class Ros {
private static instance: Ros
readonly ros: ROSLIB.Ros = new ROSLIB.Ros()
private readonly url: string = 'http://localhost:3000'
private timeout: number | null = null
private reconnectAttempts = 0
private readonly maxAttempts = 10
private readonly baseDelay = 100 // 1 second
private constructor() {
this.connect()
}
public static getInstance(): Ros {
if (!Ros.instance) {
Ros.instance = new Ros()
}
return Ros.instance
}
public reconnect() {
this.ros.connect(this.url)
}
public manualConnect() {
if (this.timeout) clearTimeout(this.timeout)
this.connect()
}
private connect(): void {
console.log(`Connecting to ${this.url}...`)
this.ros.on('connection', () => {
connectionStatus().rosbridgeConnected = true
console.log('Connected successfully!')
this.reconnectAttempts = 0
})
this.ros.on('close', (event: ROSLIB.TransportEvent) => {
connectionStatus().rosbridgeConnected = false
console.warn('Socket closed:', event)
this.handleReconnect()
})
this.ros.on('error', (error) => {
console.error('Socket error:', error)
})
this.ros.connect(this.url)
}
// Backoff algorithm
private handleReconnect(): void {
if (this.reconnectAttempts < this.maxAttempts) {
this.reconnectAttempts++
const delay = this.baseDelay * Math.pow(2, this.reconnectAttempts - 1)
console.log(
`Retrying in ${delay}ms... (Attempt ${this.reconnectAttempts}/${this.maxAttempts})`,
)
this.timeout = setTimeout(() => {
this.timeout = null
console.log(`Starting attempt [${this.reconnectAttempts}/${this.maxAttempts}]`)
this.ros.connect(this.url)
}, delay)
} else {
console.error('Max reconnection attempts reached. Please check your connection.')
}
}
}
// Usage:
const ros = Ros.getInstance()
export { ros, type BehaviorTreeList }