Added actions endpoint multiple clients can use to simultaneously control actions
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -6,4 +6,5 @@ body,
|
||||
min-height: 100vh;
|
||||
max-width: 100vw;
|
||||
background-color: #242130;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
13
toid_frontend/src/components/RobotControl.vue
Normal file
13
toid_frontend/src/components/RobotControl.vue
Normal 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>
|
||||
@@ -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!')
|
||||
})
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import * as ROSLIB from 'roslib'
|
||||
|
||||
interface BehaviorTreeList {
|
||||
tree_ids: Array<string>
|
||||
}
|
||||
|
||||
const ros = new ROSLIB.Ros()
|
||||
|
||||
export { ros, type BehaviorTreeList }
|
||||
8
toid_frontend/src/stores/connection-status.ts
Normal file
8
toid_frontend/src/stores/connection-status.ts
Normal 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 }
|
||||
})
|
||||
81
toid_frontend/src/ts/robot_bridge.ts
Normal file
81
toid_frontend/src/ts/robot_bridge.ts
Normal 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 }
|
||||
84
toid_frontend/src/ts/ros_client.ts
Normal file
84
toid_frontend/src/ts/ros_client.ts
Normal 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 }
|
||||
Reference in New Issue
Block a user