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();
|