289 lines
8.6 KiB
TypeScript
289 lines
8.6 KiB
TypeScript
![]() |
import { Hono } from '@hono/hono';
|
|||
|
import { serveStatic } from '@hono/hono/serve-static';
|
|||
|
import { html } from '@hono/hono/html';
|
|||
|
import { Context } from '@hono/hono';
|
|||
|
|
|||
|
// Create Hono app
|
|||
|
const app = new Hono();
|
|||
|
|
|||
|
// Store AGV positions
|
|||
|
const agvPositions: Map<string, { x: number; y: number; theta: number }> = new Map();
|
|||
|
let server: { shutdown: () => Promise<void> } | null = null;
|
|||
|
let isRunning = false;
|
|||
|
|
|||
|
// Serve static files
|
|||
|
app.use('/*', serveStatic({
|
|||
|
root: './',
|
|||
|
getContent: async (path, c) => {
|
|||
|
try {
|
|||
|
const file = await Deno.readFile(path);
|
|||
|
return file;
|
|||
|
} catch {
|
|||
|
return null;
|
|||
|
}
|
|||
|
}
|
|||
|
}));
|
|||
|
|
|||
|
// Main page with canvas
|
|||
|
app.get('/', (c: Context) => {
|
|||
|
return c.html(html`
|
|||
|
<!DOCTYPE html>
|
|||
|
<html>
|
|||
|
<head>
|
|||
|
<title>AGV Position Monitor</title>
|
|||
|
<style>
|
|||
|
body {
|
|||
|
margin: 0;
|
|||
|
padding: 20px;
|
|||
|
background-color: #f0f0f0;
|
|||
|
font-family: Arial, sans-serif;
|
|||
|
}
|
|||
|
canvas {
|
|||
|
background-color: white;
|
|||
|
border: 1px solid #ccc;
|
|||
|
margin: 20px 0;
|
|||
|
}
|
|||
|
.controls {
|
|||
|
margin: 20px 0;
|
|||
|
}
|
|||
|
button {
|
|||
|
padding: 8px 16px;
|
|||
|
margin-right: 10px;
|
|||
|
background-color: #4CAF50;
|
|||
|
color: white;
|
|||
|
border: none;
|
|||
|
border-radius: 4px;
|
|||
|
cursor: pointer;
|
|||
|
}
|
|||
|
button:hover {
|
|||
|
background-color: #45a049;
|
|||
|
}
|
|||
|
</style>
|
|||
|
</head>
|
|||
|
<body>
|
|||
|
<h1>AGV Position Monitor</h1>
|
|||
|
<div class="controls">
|
|||
|
<button onclick="startUpdates()">Start Updates</button>
|
|||
|
<button onclick="stopUpdates()">Stop Updates</button>
|
|||
|
</div>
|
|||
|
<canvas id="agvCanvas" width="800" height="800"></canvas>
|
|||
|
<script>
|
|||
|
const canvas = document.getElementById('agvCanvas');
|
|||
|
const ctx = canvas.getContext('2d');
|
|||
|
let updateInterval;
|
|||
|
|
|||
|
// Scale factor for visualization
|
|||
|
const SCALE = 2; // Scale adjusted for 400x400 meter area
|
|||
|
const AGV_SIZE = 8; // AGV representation size
|
|||
|
const GRID_SIZE = 400; // 400x400 meter grid
|
|||
|
const COLORS = ['#4CAF50', '#2196F3', '#FFC107', '#9C27B0', '#FF5722', '#607D8B'];
|
|||
|
|
|||
|
function getColor(id) {
|
|||
|
// Simple hash function to get consistent color for each AGV
|
|||
|
let hash = 0;
|
|||
|
for (let i = 0; i < id.length; i++) {
|
|||
|
hash = id.charCodeAt(i) + ((hash << 5) - hash);
|
|||
|
}
|
|||
|
return COLORS[Math.abs(hash) % COLORS.length];
|
|||
|
}
|
|||
|
|
|||
|
function drawAGV(x, y, theta, id, color) {
|
|||
|
ctx.save();
|
|||
|
// Center the canvas and flip Y axis to match coordinate system
|
|||
|
ctx.translate(canvas.width/2 + x * SCALE, canvas.height/2 - y * SCALE);
|
|||
|
ctx.rotate(-theta); // Negative theta to match coordinate system
|
|||
|
|
|||
|
// Draw AGV body
|
|||
|
ctx.fillStyle = color;
|
|||
|
ctx.fillRect(-AGV_SIZE/2, -AGV_SIZE/2, AGV_SIZE, AGV_SIZE);
|
|||
|
|
|||
|
// Draw direction indicator
|
|||
|
ctx.beginPath();
|
|||
|
ctx.moveTo(0, 0);
|
|||
|
ctx.lineTo(AGV_SIZE/2, 0);
|
|||
|
ctx.strokeStyle = 'white';
|
|||
|
ctx.lineWidth = 2;
|
|||
|
ctx.stroke();
|
|||
|
|
|||
|
ctx.restore();
|
|||
|
|
|||
|
// Draw AGV name
|
|||
|
ctx.save();
|
|||
|
ctx.translate(canvas.width/2 + x * SCALE, canvas.height/2 - y * SCALE - AGV_SIZE);
|
|||
|
ctx.fillStyle = color;
|
|||
|
ctx.font = '12px Arial';
|
|||
|
ctx.textAlign = 'center';
|
|||
|
ctx.fillText(id, 0, 0);
|
|||
|
ctx.restore();
|
|||
|
}
|
|||
|
|
|||
|
function clearCanvas() {
|
|||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|||
|
|
|||
|
// Draw grid
|
|||
|
ctx.strokeStyle = '#ddd';
|
|||
|
ctx.lineWidth = 1;
|
|||
|
|
|||
|
// Grid interval for labels (show every 20 units)
|
|||
|
const LABEL_INTERVAL = 20;
|
|||
|
|
|||
|
// Vertical lines
|
|||
|
for (let x = -GRID_SIZE/2; x <= GRID_SIZE/2; x++) {
|
|||
|
const drawX = canvas.width/2 + x * SCALE;
|
|||
|
ctx.beginPath();
|
|||
|
ctx.moveTo(drawX, 0);
|
|||
|
ctx.lineTo(drawX, canvas.height);
|
|||
|
ctx.stroke();
|
|||
|
|
|||
|
// Add X axis labels (every LABEL_INTERVAL units)
|
|||
|
if (x % LABEL_INTERVAL === 0) {
|
|||
|
ctx.fillStyle = '#666';
|
|||
|
ctx.font = '12px Arial';
|
|||
|
ctx.textAlign = 'center';
|
|||
|
ctx.fillText(x.toString(), drawX, canvas.height/2 + 20);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Horizontal lines
|
|||
|
for (let y = -GRID_SIZE/2; y <= GRID_SIZE/2; y++) {
|
|||
|
const drawY = canvas.height/2 + y * SCALE;
|
|||
|
ctx.beginPath();
|
|||
|
ctx.moveTo(0, drawY);
|
|||
|
ctx.lineTo(canvas.width, drawY);
|
|||
|
ctx.stroke();
|
|||
|
|
|||
|
// Add Y axis labels (every LABEL_INTERVAL units)
|
|||
|
if (y % LABEL_INTERVAL === 0) {
|
|||
|
ctx.fillStyle = '#666';
|
|||
|
ctx.font = '12px Arial';
|
|||
|
ctx.textAlign = 'right';
|
|||
|
ctx.fillText((-y).toString(), canvas.width/2 - 10, drawY + 4);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Draw axis lines
|
|||
|
ctx.strokeStyle = '#999';
|
|||
|
ctx.lineWidth = 2;
|
|||
|
|
|||
|
// X axis
|
|||
|
ctx.beginPath();
|
|||
|
ctx.moveTo(0, canvas.height/2);
|
|||
|
ctx.lineTo(canvas.width, canvas.height/2);
|
|||
|
ctx.stroke();
|
|||
|
|
|||
|
// Y axis
|
|||
|
ctx.beginPath();
|
|||
|
ctx.moveTo(canvas.width/2, 0);
|
|||
|
ctx.lineTo(canvas.width/2, canvas.height);
|
|||
|
ctx.stroke();
|
|||
|
|
|||
|
// Add axes labels
|
|||
|
ctx.fillStyle = '#444';
|
|||
|
ctx.font = 'bold 14px Arial';
|
|||
|
ctx.textAlign = 'center';
|
|||
|
ctx.fillText('X', canvas.width - 15, canvas.height/2 - 10);
|
|||
|
ctx.fillText('Y', canvas.width/2 + 15, 15);
|
|||
|
}
|
|||
|
|
|||
|
function updatePositions() {
|
|||
|
fetch('/positions')
|
|||
|
.then(response => response.json())
|
|||
|
.then(positions => {
|
|||
|
clearCanvas();
|
|||
|
positions.forEach(pos => {
|
|||
|
const color = getColor(pos.id);
|
|||
|
drawAGV(
|
|||
|
pos.position.x,
|
|||
|
pos.position.y,
|
|||
|
pos.position.theta,
|
|||
|
pos.id,
|
|||
|
color
|
|||
|
);
|
|||
|
});
|
|||
|
})
|
|||
|
.catch(error => console.error('Error fetching positions:', error));
|
|||
|
}
|
|||
|
|
|||
|
function startUpdates() {
|
|||
|
if (!updateInterval) {
|
|||
|
updateInterval = setInterval(updatePositions, 1000);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function stopUpdates() {
|
|||
|
if (updateInterval) {
|
|||
|
clearInterval(updateInterval);
|
|||
|
updateInterval = null;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Start updates automatically
|
|||
|
startUpdates();
|
|||
|
</script>
|
|||
|
</body>
|
|||
|
</html>
|
|||
|
`);
|
|||
|
});
|
|||
|
|
|||
|
// API endpoint to get current positions
|
|||
|
app.get('/positions', (c: Context) => {
|
|||
|
// Convert Map to array of objects with id and position
|
|||
|
const positions = Array.from(agvPositions.entries()).map(([id, pos]) => ({
|
|||
|
id,
|
|||
|
position: pos
|
|||
|
}));
|
|||
|
return c.json(positions);
|
|||
|
});
|
|||
|
|
|||
|
// Handle messages from main thread
|
|||
|
self.onmessage = (event) => {
|
|||
|
const message = event.data;
|
|||
|
|
|||
|
if (message.type === 'positionUpdate') {
|
|||
|
const { agvId, position } = message.data;
|
|||
|
// console.log("agvId", agvId, "position", position);
|
|||
|
agvPositions.set(`${agvId.manufacturer}/${agvId.serialNumber}`, position);
|
|||
|
} else if (message.type === 'shutdown') {
|
|||
|
stopServer();
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
// Start the server
|
|||
|
export async function startServer(port: number = 3001) {
|
|||
|
if (isRunning) {
|
|||
|
console.log("Web服务器已在运行中");
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
try {
|
|||
|
server = Deno.serve({ port }, app.fetch);
|
|||
|
isRunning = true;
|
|||
|
console.log(`Web服务器已启动,监听端口 ${port}`);
|
|||
|
return true;
|
|||
|
} catch (error) {
|
|||
|
console.error(`服务器启动失败: ${error}`);
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Stop the server
|
|||
|
export async function stopServer() {
|
|||
|
if (!isRunning || !server) {
|
|||
|
console.log("Web服务器未在运行");
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
try {
|
|||
|
await server.shutdown();
|
|||
|
isRunning = false;
|
|||
|
server = null;
|
|||
|
console.log('Web服务器已关闭');
|
|||
|
return true;
|
|||
|
} catch (error) {
|
|||
|
console.error(`服务器关闭失败: ${error}`);
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Start the server when the worker is initialized
|
|||
|
startServer();
|