Fleet management is like having a command center for all your vehicles! π― It helps track locations, monitor driver behavior, optimize routes, and keep vehicles running smoothly. Letβs build a comprehensive fleet management system on Alpine Linux! π
What is Fleet Management? π€
Fleet management includes:
- Vehicle tracking - Real-time GPS location
- Route optimization - Find the best paths
- Driver monitoring - Safety and performance
- Maintenance scheduling - Preventive care
- Fuel management - Track consumption
Think of it as a smart assistant for your entire vehicle fleet! π
Installing Core Components π¦
Set up the fleet management infrastructure:
# Update package list
sudo apk update
# Install database systems
sudo apk add postgresql postgresql-client
sudo apk add redis
# Install message broker
sudo apk add rabbitmq-server
# Install web server and runtime
sudo apk add nginx
sudo apk add nodejs npm
sudo apk add python3 py3-pip python3-dev
# Install geo libraries
sudo apk add proj geos gdal
sudo apk add postgis
# Install monitoring tools
sudo apk add prometheus grafana
GPS Tracking System π
Create the vehicle tracking backend:
# Create project structure
mkdir -p ~/fleet-management/{gps,api,dashboard,analytics}
cd ~/fleet-management
# GPS data receiver
cat > gps/gps_receiver.py << 'EOF'
#!/usr/bin/env python3
import asyncio
import json
import random
import math
from datetime import datetime
import asyncpg
import aioredis
import pika
class VehicleSimulator:
def __init__(self, vehicle_id, route_coords):
self.vehicle_id = vehicle_id
self.route_coords = route_coords
self.current_index = 0
self.speed = random.uniform(40, 60) # km/h
self.fuel_level = random.uniform(50, 100) # percentage
self.engine_temp = random.uniform(80, 95) # celsius
self.odometer = random.uniform(10000, 50000) # km
def get_next_position(self):
"""Simulate vehicle movement along route"""
if self.current_index >= len(self.route_coords) - 1:
self.current_index = 0 # Loop back to start
# Get current and next point
current = self.route_coords[self.current_index]
next_point = self.route_coords[self.current_index + 1]
# Interpolate between points
progress = random.uniform(0.1, 0.3)
lat = current[0] + (next_point[0] - current[0]) * progress
lon = current[1] + (next_point[1] - current[1]) * progress
# Move to next segment occasionally
if random.random() > 0.7:
self.current_index += 1
# Update vehicle stats
self.speed = max(0, self.speed + random.uniform(-5, 5))
self.fuel_level = max(0, self.fuel_level - random.uniform(0.01, 0.1))
self.engine_temp = min(120, max(70, self.engine_temp + random.uniform(-2, 2)))
self.odometer += self.speed / 3600 # Add distance based on speed
return lat, lon
def get_telemetry(self):
"""Get current vehicle telemetry"""
lat, lon = self.get_next_position()
return {
"vehicle_id": self.vehicle_id,
"timestamp": datetime.utcnow().isoformat(),
"location": {
"lat": round(lat, 6),
"lon": round(lon, 6),
"speed": round(self.speed, 1),
"heading": random.randint(0, 359),
"altitude": random.randint(100, 300)
},
"engine": {
"rpm": int(self.speed * 40), # Simplified RPM calculation
"temperature": round(self.engine_temp, 1),
"oil_pressure": random.uniform(30, 60),
"fuel_level": round(self.fuel_level, 1)
},
"vehicle": {
"odometer": round(self.odometer, 1),
"battery_voltage": round(random.uniform(12.5, 14.5), 1),
"tire_pressure": [
random.uniform(32, 35) for _ in range(4)
]
},
"driver": {
"id": f"DRV{random.randint(100, 999)}",
"status": "active",
"harsh_braking": random.random() < 0.05,
"harsh_acceleration": random.random() < 0.05,
"speeding": self.speed > 80
}
}
class GPSReceiver:
def __init__(self):
self.db_pool = None
self.redis = None
self.rabbitmq = None
self.vehicles = {}
async def init(self):
"""Initialize connections"""
# PostgreSQL connection
self.db_pool = await asyncpg.create_pool(
'postgresql://fleet_user:password@localhost/fleet_db'
)
# Redis connection
self.redis = await aioredis.create_redis_pool('redis://localhost')
# RabbitMQ connection
connection = pika.BlockingConnection(
pika.ConnectionParameters('localhost')
)
self.channel = connection.channel()
self.channel.queue_declare(queue='gps_data')
# Initialize database schema
await self.init_database()
async def init_database(self):
"""Create database tables"""
async with self.db_pool.acquire() as conn:
await conn.execute('''
CREATE TABLE IF NOT EXISTS vehicles (
id VARCHAR(50) PRIMARY KEY,
make VARCHAR(50),
model VARCHAR(50),
year INTEGER,
license_plate VARCHAR(20),
vin VARCHAR(50),
status VARCHAR(20) DEFAULT 'active'
);
CREATE TABLE IF NOT EXISTS gps_history (
id SERIAL PRIMARY KEY,
vehicle_id VARCHAR(50) REFERENCES vehicles(id),
timestamp TIMESTAMPTZ DEFAULT NOW(),
location GEOGRAPHY(POINT),
speed FLOAT,
heading INTEGER,
telemetry JSONB
);
CREATE INDEX idx_gps_vehicle_time ON gps_history(vehicle_id, timestamp DESC);
CREATE TABLE IF NOT EXISTS trips (
id SERIAL PRIMARY KEY,
vehicle_id VARCHAR(50) REFERENCES vehicles(id),
driver_id VARCHAR(50),
start_time TIMESTAMPTZ,
end_time TIMESTAMPTZ,
start_location GEOGRAPHY(POINT),
end_location GEOGRAPHY(POINT),
distance_km FLOAT,
duration_minutes INTEGER,
fuel_used_liters FLOAT,
route_points JSONB
);
CREATE TABLE IF NOT EXISTS alerts (
id SERIAL PRIMARY KEY,
vehicle_id VARCHAR(50) REFERENCES vehicles(id),
alert_type VARCHAR(50),
severity VARCHAR(20),
message TEXT,
data JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
resolved BOOLEAN DEFAULT FALSE
);
CREATE TABLE IF NOT EXISTS maintenance (
id SERIAL PRIMARY KEY,
vehicle_id VARCHAR(50) REFERENCES vehicles(id),
service_type VARCHAR(100),
scheduled_date DATE,
completed_date DATE,
mileage_at_service INTEGER,
cost DECIMAL(10,2),
notes TEXT
);
''')
async def process_gps_data(self, telemetry):
"""Process incoming GPS data"""
vehicle_id = telemetry['vehicle_id']
location = telemetry['location']
# Store in database
async with self.db_pool.acquire() as conn:
await conn.execute('''
INSERT INTO gps_history (vehicle_id, location, speed, heading, telemetry)
VALUES ($1, ST_MakePoint($2, $3)::geography, $4, $5, $6)
''', vehicle_id, location['lon'], location['lat'],
location['speed'], location['heading'], json.dumps(telemetry))
# Update real-time cache
await self.redis.setex(
f"vehicle:{vehicle_id}:location",
60, # TTL 60 seconds
json.dumps(telemetry)
)
# Check for alerts
await self.check_alerts(telemetry)
# Publish to message queue
self.channel.basic_publish(
exchange='',
routing_key='gps_data',
body=json.dumps(telemetry)
)
async def check_alerts(self, telemetry):
"""Check for alert conditions"""
vehicle_id = telemetry['vehicle_id']
alerts = []
# Speeding alert
if telemetry['location']['speed'] > 80:
alerts.append({
'type': 'speeding',
'severity': 'warning',
'message': f"Vehicle {vehicle_id} exceeding speed limit: {telemetry['location']['speed']} km/h"
})
# Low fuel alert
if telemetry['engine']['fuel_level'] < 20:
alerts.append({
'type': 'low_fuel',
'severity': 'warning',
'message': f"Vehicle {vehicle_id} low fuel: {telemetry['engine']['fuel_level']}%"
})
# Engine temperature alert
if telemetry['engine']['temperature'] > 100:
alerts.append({
'type': 'engine_temp',
'severity': 'critical',
'message': f"Vehicle {vehicle_id} engine overheating: {telemetry['engine']['temperature']}Β°C"
})
# Harsh driving alerts
if telemetry['driver']['harsh_braking'] or telemetry['driver']['harsh_acceleration']:
alerts.append({
'type': 'harsh_driving',
'severity': 'info',
'message': f"Vehicle {vehicle_id} harsh driving detected"
})
# Store alerts
async with self.db_pool.acquire() as conn:
for alert in alerts:
await conn.execute('''
INSERT INTO alerts (vehicle_id, alert_type, severity, message, data)
VALUES ($1, $2, $3, $4, $5)
''', vehicle_id, alert['type'], alert['severity'],
alert['message'], json.dumps(telemetry))
async def simulate_fleet(self):
"""Simulate a fleet of vehicles"""
# Sample routes (latitude, longitude pairs)
routes = [
# Route 1: City center loop
[(37.7749, -122.4194), (37.7849, -122.4094), (37.7949, -122.4194),
(37.7849, -122.4294), (37.7749, -122.4194)],
# Route 2: Highway route
[(37.7749, -122.4194), (37.8049, -122.4494), (37.8349, -122.4794),
(37.8649, -122.5094), (37.8349, -122.4794), (37.7749, -122.4194)],
# Route 3: Suburban route
[(37.7549, -122.4394), (37.7349, -122.4594), (37.7149, -122.4794),
(37.7349, -122.4594), (37.7549, -122.4394)]
]
# Create vehicle simulators
for i in range(10):
vehicle_id = f"VEH{1000 + i}"
route = routes[i % len(routes)]
self.vehicles[vehicle_id] = VehicleSimulator(vehicle_id, route)
# Add vehicle to database
async with self.db_pool.acquire() as conn:
await conn.execute('''
INSERT INTO vehicles (id, make, model, year, license_plate)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (id) DO NOTHING
''', vehicle_id, 'Ford', 'Transit', 2022, f"ABC{1000 + i}")
# Start simulation
while True:
tasks = []
for vehicle in self.vehicles.values():
telemetry = vehicle.get_telemetry()
tasks.append(self.process_gps_data(telemetry))
await asyncio.gather(*tasks)
await asyncio.sleep(5) # Update every 5 seconds
async def main():
receiver = GPSReceiver()
await receiver.init()
await receiver.simulate_fleet()
if __name__ == "__main__":
asyncio.run(main())
EOF
chmod +x gps/gps_receiver.py
Fleet Management API π
Build the REST API for fleet operations:
# API server
cd api
npm init -y
npm install express cors body-parser pg redis socket.io amqplib
npm install bcrypt jsonwebtoken
npm install @turf/turf # For geo calculations
cat > server.js << 'EOF'
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const { Pool } = require('pg');
const redis = require('redis');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const socketIo = require('socket.io');
const amqp = require('amqplib');
const turf = require('@turf/turf');
const app = express();
const server = require('http').createServer(app);
const io = socketIo(server, { cors: { origin: '*' } });
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Database connection
const pool = new Pool({
user: 'fleet_user',
host: 'localhost',
database: 'fleet_db',
password: 'password',
port: 5432,
});
// Redis connection
const redisClient = redis.createClient();
redisClient.connect();
// RabbitMQ connection
let channel;
amqp.connect('amqp://localhost').then(conn => {
return conn.createChannel();
}).then(ch => {
channel = ch;
return channel.assertQueue('gps_data');
}).then(() => {
// Consume GPS data and broadcast via WebSocket
channel.consume('gps_data', (msg) => {
if (msg) {
const data = JSON.parse(msg.content.toString());
io.emit('gps_update', data);
channel.ack(msg);
}
});
});
// Authentication middleware
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, 'your-secret-key');
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
};
// Routes
// Get all vehicles
app.get('/api/vehicles', authenticate, async (req, res) => {
try {
const result = await pool.query(`
SELECT v.*,
ST_AsGeoJSON(h.location) as last_location,
h.speed as last_speed,
h.timestamp as last_update
FROM vehicles v
LEFT JOIN LATERAL (
SELECT location, speed, timestamp
FROM gps_history
WHERE vehicle_id = v.id
ORDER BY timestamp DESC
LIMIT 1
) h ON true
ORDER BY v.id
`);
const vehicles = await Promise.all(result.rows.map(async (vehicle) => {
// Get real-time data from Redis
const realtimeData = await redisClient.get(`vehicle:${vehicle.id}:location`);
if (realtimeData) {
const telemetry = JSON.parse(realtimeData);
vehicle.realtime = telemetry;
}
if (vehicle.last_location) {
vehicle.last_location = JSON.parse(vehicle.last_location);
}
return vehicle;
}));
res.json(vehicles);
} catch (error) {
console.error('Error fetching vehicles:', error);
res.status(500).json({ error: 'Failed to fetch vehicles' });
}
});
// Get vehicle details
app.get('/api/vehicles/:id', authenticate, async (req, res) => {
try {
const { id } = req.params;
// Get vehicle info
const vehicleResult = await pool.query(
'SELECT * FROM vehicles WHERE id = $1',
[id]
);
if (vehicleResult.rows.length === 0) {
return res.status(404).json({ error: 'Vehicle not found' });
}
const vehicle = vehicleResult.rows[0];
// Get latest position
const positionResult = await pool.query(`
SELECT ST_AsGeoJSON(location) as location, speed, heading, timestamp, telemetry
FROM gps_history
WHERE vehicle_id = $1
ORDER BY timestamp DESC
LIMIT 1
`, [id]);
if (positionResult.rows.length > 0) {
vehicle.current_position = {
...positionResult.rows[0],
location: JSON.parse(positionResult.rows[0].location)
};
}
// Get today's route
const routeResult = await pool.query(`
SELECT ST_AsGeoJSON(location) as location, timestamp, speed
FROM gps_history
WHERE vehicle_id = $1 AND timestamp > NOW() - INTERVAL '24 hours'
ORDER BY timestamp
`, [id]);
vehicle.todays_route = routeResult.rows.map(point => ({
...point,
location: JSON.parse(point.location)
}));
// Get active alerts
const alertsResult = await pool.query(`
SELECT * FROM alerts
WHERE vehicle_id = $1 AND resolved = false
ORDER BY created_at DESC
`, [id]);
vehicle.active_alerts = alertsResult.rows;
res.json(vehicle);
} catch (error) {
console.error('Error fetching vehicle details:', error);
res.status(500).json({ error: 'Failed to fetch vehicle details' });
}
});
// Get vehicle history
app.get('/api/vehicles/:id/history', authenticate, async (req, res) => {
try {
const { id } = req.params;
const { start_date, end_date } = req.query;
let query = `
SELECT ST_AsGeoJSON(location) as location, speed, heading, timestamp
FROM gps_history
WHERE vehicle_id = $1
`;
const params = [id];
if (start_date) {
query += ' AND timestamp >= $2';
params.push(start_date);
}
if (end_date) {
query += ` AND timestamp <= $${params.length + 1}`;
params.push(end_date);
}
query += ' ORDER BY timestamp DESC LIMIT 1000';
const result = await pool.query(query, params);
const history = result.rows.map(point => ({
...point,
location: JSON.parse(point.location)
}));
res.json(history);
} catch (error) {
console.error('Error fetching vehicle history:', error);
res.status(500).json({ error: 'Failed to fetch vehicle history' });
}
});
// Calculate route statistics
app.get('/api/vehicles/:id/stats', authenticate, async (req, res) => {
try {
const { id } = req.params;
const { date = new Date().toISOString().split('T')[0] } = req.query;
// Get all points for the day
const result = await pool.query(`
SELECT ST_X(location::geometry) as lon,
ST_Y(location::geometry) as lat,
speed, timestamp, telemetry
FROM gps_history
WHERE vehicle_id = $1
AND DATE(timestamp) = $2
ORDER BY timestamp
`, [id, date]);
if (result.rows.length < 2) {
return res.json({
date,
total_distance: 0,
total_time: 0,
average_speed: 0,
max_speed: 0,
fuel_consumed: 0,
idle_time: 0
});
}
// Calculate statistics
let totalDistance = 0;
let maxSpeed = 0;
let idleTime = 0;
let fuelStart = null;
let fuelEnd = null;
for (let i = 1; i < result.rows.length; i++) {
const prev = result.rows[i - 1];
const curr = result.rows[i];
// Calculate distance
const from = turf.point([prev.lon, prev.lat]);
const to = turf.point([curr.lon, curr.lat]);
const distance = turf.distance(from, to, { units: 'kilometers' });
totalDistance += distance;
// Track max speed
maxSpeed = Math.max(maxSpeed, curr.speed);
// Track idle time
if (curr.speed < 5) {
const timeDiff = (new Date(curr.timestamp) - new Date(prev.timestamp)) / 1000 / 60;
idleTime += timeDiff;
}
// Track fuel
if (i === 1 && prev.telemetry?.engine?.fuel_level) {
fuelStart = prev.telemetry.engine.fuel_level;
}
if (i === result.rows.length - 1 && curr.telemetry?.engine?.fuel_level) {
fuelEnd = curr.telemetry.engine.fuel_level;
}
}
const firstPoint = result.rows[0];
const lastPoint = result.rows[result.rows.length - 1];
const totalTime = (new Date(lastPoint.timestamp) - new Date(firstPoint.timestamp)) / 1000 / 60; // minutes
res.json({
date,
total_distance: Math.round(totalDistance * 100) / 100,
total_time: Math.round(totalTime),
average_speed: totalDistance / (totalTime / 60),
max_speed: maxSpeed,
fuel_consumed: fuelStart && fuelEnd ? fuelStart - fuelEnd : 0,
idle_time: Math.round(idleTime),
stops: result.rows.filter(p => p.speed < 1).length
});
} catch (error) {
console.error('Error calculating stats:', error);
res.status(500).json({ error: 'Failed to calculate statistics' });
}
});
// Get alerts
app.get('/api/alerts', authenticate, async (req, res) => {
try {
const { vehicle_id, severity, resolved } = req.query;
let query = 'SELECT * FROM alerts WHERE 1=1';
const params = [];
if (vehicle_id) {
params.push(vehicle_id);
query += ` AND vehicle_id = $${params.length}`;
}
if (severity) {
params.push(severity);
query += ` AND severity = $${params.length}`;
}
if (resolved !== undefined) {
params.push(resolved === 'true');
query += ` AND resolved = $${params.length}`;
}
query += ' ORDER BY created_at DESC LIMIT 100';
const result = await pool.query(query, params);
res.json(result.rows);
} catch (error) {
console.error('Error fetching alerts:', error);
res.status(500).json({ error: 'Failed to fetch alerts' });
}
});
// Resolve alert
app.put('/api/alerts/:id/resolve', authenticate, async (req, res) => {
try {
const { id } = req.params;
await pool.query(
'UPDATE alerts SET resolved = true WHERE id = $1',
[id]
);
res.json({ success: true });
} catch (error) {
console.error('Error resolving alert:', error);
res.status(500).json({ error: 'Failed to resolve alert' });
}
});
// Get trips
app.get('/api/trips', authenticate, async (req, res) => {
try {
const { vehicle_id, driver_id, date } = req.query;
let query = `
SELECT t.*,
ST_AsGeoJSON(t.start_location) as start_location,
ST_AsGeoJSON(t.end_location) as end_location,
v.make, v.model, v.license_plate
FROM trips t
JOIN vehicles v ON t.vehicle_id = v.id
WHERE 1=1
`;
const params = [];
if (vehicle_id) {
params.push(vehicle_id);
query += ` AND t.vehicle_id = $${params.length}`;
}
if (driver_id) {
params.push(driver_id);
query += ` AND t.driver_id = $${params.length}`;
}
if (date) {
params.push(date);
query += ` AND DATE(t.start_time) = $${params.length}`;
}
query += ' ORDER BY t.start_time DESC LIMIT 100';
const result = await pool.query(query, params);
const trips = result.rows.map(trip => ({
...trip,
start_location: trip.start_location ? JSON.parse(trip.start_location) : null,
end_location: trip.end_location ? JSON.parse(trip.end_location) : null
}));
res.json(trips);
} catch (error) {
console.error('Error fetching trips:', error);
res.status(500).json({ error: 'Failed to fetch trips' });
}
});
// Schedule maintenance
app.post('/api/maintenance', authenticate, async (req, res) => {
try {
const { vehicle_id, service_type, scheduled_date, notes } = req.body;
const result = await pool.query(`
INSERT INTO maintenance (vehicle_id, service_type, scheduled_date, notes)
VALUES ($1, $2, $3, $4)
RETURNING *
`, [vehicle_id, service_type, scheduled_date, notes]);
res.json(result.rows[0]);
} catch (error) {
console.error('Error scheduling maintenance:', error);
res.status(500).json({ error: 'Failed to schedule maintenance' });
}
});
// Get maintenance schedule
app.get('/api/maintenance', authenticate, async (req, res) => {
try {
const { vehicle_id, upcoming } = req.query;
let query = `
SELECT m.*, v.make, v.model, v.license_plate
FROM maintenance m
JOIN vehicles v ON m.vehicle_id = v.id
WHERE 1=1
`;
const params = [];
if (vehicle_id) {
params.push(vehicle_id);
query += ` AND m.vehicle_id = $${params.length}`;
}
if (upcoming === 'true') {
query += ' AND m.completed_date IS NULL AND m.scheduled_date >= CURRENT_DATE';
}
query += ' ORDER BY m.scheduled_date';
const result = await pool.query(query, params);
res.json(result.rows);
} catch (error) {
console.error('Error fetching maintenance:', error);
res.status(500).json({ error: 'Failed to fetch maintenance' });
}
});
// WebSocket connection for real-time updates
io.on('connection', (socket) => {
console.log('Client connected:', socket.id);
// Join vehicle-specific rooms
socket.on('track_vehicle', (vehicleId) => {
socket.join(`vehicle:${vehicleId}`);
});
socket.on('untrack_vehicle', (vehicleId) => {
socket.leave(`vehicle:${vehicleId}`);
});
socket.on('disconnect', () => {
console.log('Client disconnected:', socket.id);
});
});
// Start server
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Fleet Management API running on port ${PORT}`);
});
EOF
Fleet Dashboard Interface π₯οΈ
Create the web dashboard:
# Dashboard frontend
cd ../dashboard
cat > index.html << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fleet Management Dashboard</title>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f5f5;
}
.header {
background: #2c3e50;
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.container {
display: grid;
grid-template-columns: 300px 1fr;
height: calc(100vh - 60px);
}
.sidebar {
background: white;
border-right: 1px solid #e0e0e0;
overflow-y: auto;
}
.main {
position: relative;
overflow: hidden;
}
#map {
width: 100%;
height: 100%;
}
.vehicle-list {
padding: 1rem;
}
.vehicle-item {
background: #f8f8f8;
padding: 1rem;
margin-bottom: 0.5rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
}
.vehicle-item:hover {
background: #e8e8e8;
}
.vehicle-item.selected {
border-color: #3498db;
background: #e3f2fd;
}
.vehicle-status {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 0.5rem;
}
.status-active { background: #27ae60; }
.status-idle { background: #f39c12; }
.status-offline { background: #e74c3c; }
.vehicle-info {
margin-top: 0.5rem;
font-size: 0.85rem;
color: #666;
}
.stats-panel {
position: absolute;
top: 1rem;
right: 1rem;
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
width: 300px;
max-height: 80vh;
overflow-y: auto;
}
.stat-item {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #eee;
}
.stat-label {
font-size: 0.85rem;
color: #666;
margin-bottom: 0.25rem;
}
.stat-value {
font-size: 1.5rem;
font-weight: bold;
color: #2c3e50;
}
.alert-badge {
background: #e74c3c;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
margin-left: 0.5rem;
}
.vehicle-marker {
background: #3498db;
color: white;
padding: 0.5rem;
border-radius: 50%;
text-align: center;
font-weight: bold;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.route-info {
background: #f8f8f8;
padding: 1rem;
border-radius: 8px;
margin-top: 1rem;
}
.tabs {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
border-bottom: 1px solid #e0e0e0;
}
.tab {
padding: 0.5rem 1rem;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.tab.active {
color: #3498db;
border-bottom-color: #3498db;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
#alerts-list {
max-height: 300px;
overflow-y: auto;
}
.alert-item {
background: #fff3cd;
border: 1px solid #ffeaa7;
padding: 0.75rem;
margin-bottom: 0.5rem;
border-radius: 4px;
font-size: 0.85rem;
}
.alert-item.critical {
background: #f8d7da;
border-color: #f5c6cb;
}
.btn {
background: #3498db;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
}
.btn:hover {
background: #2980b9;
}
.btn-small {
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
}
</style>
</head>
<body>
<header class="header">
<h1>π Fleet Management System</h1>
<div>
<span id="total-vehicles">0</span> Vehicles |
<span id="active-vehicles">0</span> Active |
<span id="total-alerts">0</span> Alerts
</div>
</header>
<div class="container">
<aside class="sidebar">
<div class="tabs">
<div class="tab active" onclick="switchTab('vehicles')">Vehicles</div>
<div class="tab" onclick="switchTab('alerts')">Alerts</div>
</div>
<div id="vehicles-tab" class="tab-content active">
<div class="vehicle-list" id="vehicle-list">
<!-- Vehicles will be loaded here -->
</div>
</div>
<div id="alerts-tab" class="tab-content">
<div id="alerts-list">
<!-- Alerts will be loaded here -->
</div>
</div>
</aside>
<main class="main">
<div id="map"></div>
<div class="stats-panel" id="stats-panel" style="display: none;">
<h3 id="selected-vehicle">No Vehicle Selected</h3>
<div class="stat-item">
<div class="stat-label">Current Speed</div>
<div class="stat-value" id="current-speed">0 km/h</div>
</div>
<div class="stat-item">
<div class="stat-label">Location</div>
<div id="current-location">Unknown</div>
</div>
<div class="stat-item">
<div class="stat-label">Driver</div>
<div id="current-driver">Unknown</div>
</div>
<div class="stat-item">
<div class="stat-label">Fuel Level</div>
<div class="stat-value" id="fuel-level">0%</div>
</div>
<div class="stat-item">
<div class="stat-label">Today's Distance</div>
<div class="stat-value" id="todays-distance">0 km</div>
</div>
<div class="route-info">
<h4>Today's Route</h4>
<canvas id="speed-chart" width="300" height="150"></canvas>
</div>
<button class="btn" onclick="showVehicleHistory()">View History</button>
<button class="btn" onclick="scheduleMainenance()">Schedule Maintenance</button>
</div>
</main>
</div>
<script>
// Initialize map
const map = L.map('map').setView([37.7749, -122.4194], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'Β© OpenStreetMap contributors'
}).addTo(map);
// WebSocket connection
const socket = io('http://localhost:3000');
// Data storage
let vehicles = {};
let vehicleMarkers = {};
let selectedVehicle = null;
let routePolylines = {};
let speedChart = null;
// Custom vehicle icon
const vehicleIcon = L.divIcon({
className: 'vehicle-marker',
html: '<div>π</div>',
iconSize: [30, 30]
});
// Initialize
async function init() {
await loadVehicles();
await loadAlerts();
setupSocketListeners();
initSpeedChart();
}
// Load vehicles
async function loadVehicles() {
const response = await fetch('/api/vehicles', {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
});
const vehicleList = await response.json();
document.getElementById('total-vehicles').textContent = vehicleList.length;
const vehicleListEl = document.getElementById('vehicle-list');
vehicleListEl.innerHTML = '';
let activeCount = 0;
vehicleList.forEach(vehicle => {
vehicles[vehicle.id] = vehicle;
// Add to sidebar
const item = document.createElement('div');
item.className = 'vehicle-item';
item.onclick = () => selectVehicle(vehicle.id);
const isActive = vehicle.realtime &&
(Date.now() - new Date(vehicle.realtime.timestamp).getTime()) < 60000;
if (isActive) activeCount++;
item.innerHTML = `
<div>
<span class="vehicle-status ${isActive ? 'status-active' : 'status-offline'}"></span>
<strong>${vehicle.id}</strong>
${vehicle.realtime?.driver?.speeding ? '<span class="alert-badge">β‘</span>' : ''}
</div>
<div class="vehicle-info">
${vehicle.make} ${vehicle.model} - ${vehicle.license_plate}<br>
${vehicle.realtime ?
`${vehicle.realtime.location.speed.toFixed(1)} km/h` :
'Offline'}
</div>
`;
vehicleListEl.appendChild(item);
// Add to map
if (vehicle.realtime) {
addVehicleToMap(vehicle);
}
});
document.getElementById('active-vehicles').textContent = activeCount;
}
// Add vehicle to map
function addVehicleToMap(vehicle) {
const location = vehicle.realtime.location;
if (vehicleMarkers[vehicle.id]) {
vehicleMarkers[vehicle.id].setLatLng([location.lat, location.lon]);
} else {
const marker = L.marker([location.lat, location.lon], { icon: vehicleIcon })
.bindPopup(`
<strong>${vehicle.id}</strong><br>
${vehicle.make} ${vehicle.model}<br>
Speed: ${location.speed.toFixed(1)} km/h
`)
.addTo(map);
vehicleMarkers[vehicle.id] = marker;
}
}
// Select vehicle
async function selectVehicle(vehicleId) {
selectedVehicle = vehicleId;
// Update UI
document.querySelectorAll('.vehicle-item').forEach(item => {
item.classList.remove('selected');
});
event.currentTarget.classList.add('selected');
// Show stats panel
document.getElementById('stats-panel').style.display = 'block';
document.getElementById('selected-vehicle').textContent = vehicleId;
// Load vehicle details
const response = await fetch(`/api/vehicles/${vehicleId}`, {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
});
const vehicle = await response.json();
// Update stats
if (vehicle.current_position) {
const telemetry = vehicle.current_position.telemetry;
document.getElementById('current-speed').textContent =
`${telemetry.location.speed.toFixed(1)} km/h`;
document.getElementById('current-location').textContent =
`${telemetry.location.lat.toFixed(4)}, ${telemetry.location.lon.toFixed(4)}`;
document.getElementById('current-driver').textContent =
telemetry.driver.id;
document.getElementById('fuel-level').textContent =
`${telemetry.engine.fuel_level.toFixed(1)}%`;
}
// Load today's stats
const statsResponse = await fetch(`/api/vehicles/${vehicleId}/stats`, {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
});
const stats = await statsResponse.json();
document.getElementById('todays-distance').textContent =
`${stats.total_distance.toFixed(1)} km`;
// Draw route
if (vehicle.todays_route && vehicle.todays_route.length > 1) {
drawRoute(vehicleId, vehicle.todays_route);
updateSpeedChart(vehicle.todays_route);
}
// Center map on vehicle
if (vehicleMarkers[vehicleId]) {
map.setView(vehicleMarkers[vehicleId].getLatLng(), 15);
}
// Subscribe to real-time updates
socket.emit('track_vehicle', vehicleId);
}
// Draw route on map
function drawRoute(vehicleId, route) {
// Remove existing route
if (routePolylines[vehicleId]) {
map.removeLayer(routePolylines[vehicleId]);
}
const coordinates = route.map(point => [
point.location.coordinates[1],
point.location.coordinates[0]
]);
const polyline = L.polyline(coordinates, {
color: '#3498db',
weight: 4,
opacity: 0.7
}).addTo(map);
routePolylines[vehicleId] = polyline;
}
// Initialize speed chart
function initSpeedChart() {
const ctx = document.getElementById('speed-chart').getContext('2d');
speedChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Speed (km/h)',
data: [],
borderColor: '#3498db',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 120
}
}
}
});
}
// Update speed chart
function updateSpeedChart(route) {
const labels = route.map(point =>
new Date(point.timestamp).toLocaleTimeString()
);
const speeds = route.map(point => point.speed);
speedChart.data.labels = labels;
speedChart.data.datasets[0].data = speeds;
speedChart.update();
}
// Load alerts
async function loadAlerts() {
const response = await fetch('/api/alerts?resolved=false', {
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
});
const alerts = await response.json();
document.getElementById('total-alerts').textContent = alerts.length;
const alertsList = document.getElementById('alerts-list');
alertsList.innerHTML = '';
alerts.forEach(alert => {
const item = document.createElement('div');
item.className = `alert-item ${alert.severity}`;
item.innerHTML = `
<strong>${alert.vehicle_id}</strong> - ${alert.alert_type}<br>
${alert.message}<br>
<small>${new Date(alert.created_at).toLocaleString()}</small>
<button class="btn btn-small" onclick="resolveAlert(${alert.id})">Resolve</button>
`;
alertsList.appendChild(item);
});
}
// Resolve alert
async function resolveAlert(alertId) {
await fetch(`/api/alerts/${alertId}/resolve`, {
method: 'PUT',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
});
loadAlerts();
}
// Setup socket listeners
function setupSocketListeners() {
socket.on('gps_update', (data) => {
// Update vehicle data
if (vehicles[data.vehicle_id]) {
vehicles[data.vehicle_id].realtime = data;
addVehicleToMap(vehicles[data.vehicle_id]);
// Update sidebar
loadVehicles();
// Update selected vehicle stats
if (selectedVehicle === data.vehicle_id) {
document.getElementById('current-speed').textContent =
`${data.location.speed.toFixed(1)} km/h`;
document.getElementById('fuel-level').textContent =
`${data.engine.fuel_level.toFixed(1)}%`;
}
}
});
}
// Switch tabs
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => {
t.classList.remove('active');
});
document.querySelectorAll('.tab-content').forEach(c => {
c.classList.remove('active');
});
event.target.classList.add('active');
document.getElementById(`${tab}-tab`).classList.add('active');
}
// Show vehicle history
function showVehicleHistory() {
// Implement history view
alert('Vehicle history view coming soon!');
}
// Schedule maintenance
function scheduleMainenance() {
// Implement maintenance scheduling
alert('Maintenance scheduling coming soon!');
}
// Set demo token (in production, implement proper login)
localStorage.setItem('token', 'demo-token');
// Initialize app
init();
</script>
</body>
</html>
EOF
Route Optimization Engine πΊοΈ
Optimize delivery routes:
# Route optimization
cat > analytics/route_optimizer.py << 'EOF'
#!/usr/bin/env python3
import numpy as np
from scipy.spatial.distance import cdist
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
import asyncpg
import json
class RouteOptimizer:
def __init__(self, db_config):
self.db_config = db_config
async def get_delivery_points(self, date):
"""Get delivery points for a specific date"""
conn = await asyncpg.connect(**self.db_config)
points = await conn.fetch('''
SELECT id, customer_name,
ST_X(location::geometry) as lon,
ST_Y(location::geometry) as lat,
delivery_window_start,
delivery_window_end,
package_weight,
priority
FROM deliveries
WHERE delivery_date = $1
ORDER BY priority DESC
''', date)
await conn.close()
return points
def calculate_distance_matrix(self, locations):
"""Calculate distance matrix between all points"""
coords = np.array([[loc['lat'], loc['lon']] for loc in locations])
# Calculate Euclidean distances (in production, use real road distances)
distances = cdist(coords, coords) * 111 # Convert to km (approximation)
return distances.astype(int)
def optimize_routes(self, locations, num_vehicles, vehicle_capacity):
"""Optimize routes using OR-Tools"""
# Create routing model
manager = pywrapcp.RoutingIndexManager(
len(locations), num_vehicles, 0
)
routing = pywrapcp.RoutingModel(manager)
# Distance callback
distance_matrix = self.calculate_distance_matrix(locations)
def distance_callback(from_index, to_index):
from_node = manager.IndexToNode(from_index)
to_node = manager.IndexToNode(to_index)
return distance_matrix[from_node][to_node]
transit_callback_index = routing.RegisterTransitCallback(distance_callback)
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
# Capacity constraint
def demand_callback(from_index):
from_node = manager.IndexToNode(from_index)
return locations[from_node].get('package_weight', 0)
demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback)
routing.AddDimensionWithVehicleCapacity(
demand_callback_index,
0, # null capacity slack
[vehicle_capacity] * num_vehicles,
True, # start cumul to zero
'Capacity'
)
# Time windows
time_dimension_name = 'Time'
routing.AddDimension(
transit_callback_index,
30, # allow waiting time
7200, # maximum time per vehicle
False, # Don't force start cumul to zero
time_dimension_name
)
time_dimension = routing.GetDimensionOrDie(time_dimension_name)
# Add time window constraints
for location_idx, location in enumerate(locations):
if location_idx == 0: # Skip depot
continue
index = manager.NodeToIndex(location_idx)
time_dimension.CumulVar(index).SetRange(
int(location.get('delivery_window_start', 0)),
int(location.get('delivery_window_end', 7200))
)
# Set search parameters
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
search_parameters.first_solution_strategy = (
routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC
)
search_parameters.local_search_metaheuristic = (
routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH
)
search_parameters.time_limit.FromSeconds(30)
# Solve
solution = routing.SolveWithParameters(search_parameters)
if solution:
return self.extract_routes(manager, routing, solution, locations)
return None
def extract_routes(self, manager, routing, solution, locations):
"""Extract routes from solution"""
routes = []
for vehicle_id in range(routing.vehicles()):
route = []
index = routing.Start(vehicle_id)
while not routing.IsEnd(index):
node_index = manager.IndexToNode(index)
route.append({
'location': locations[node_index],
'arrival_time': solution.Min(
routing.GetDimensionOrDie('Time').CumulVar(index)
)
})
index = solution.Value(routing.NextVar(index))
if len(route) > 1: # Exclude empty routes
routes.append({
'vehicle_id': vehicle_id,
'route': route,
'total_distance': routing.GetArcCostForVehicle(
routing.Start(vehicle_id),
routing.End(vehicle_id),
vehicle_id
)
})
return routes
def generate_turn_by_turn_directions(self, route):
"""Generate navigation instructions"""
directions = []
for i in range(len(route) - 1):
current = route[i]['location']
next_loc = route[i + 1]['location']
# Calculate bearing
lat1, lon1 = current['lat'], current['lon']
lat2, lon2 = next_loc['lat'], next_loc['lon']
bearing = self.calculate_bearing(lat1, lon1, lat2, lon2)
distance = self.calculate_distance(lat1, lon1, lat2, lon2)
# Generate instruction
turn = self.get_turn_instruction(bearing)
directions.append({
'instruction': f"{turn} and continue for {distance:.1f} km",
'distance': distance,
'destination': next_loc['customer_name']
})
return directions
def calculate_bearing(self, lat1, lon1, lat2, lon2):
"""Calculate bearing between two points"""
lat1, lon1, lat2, lon2 = map(np.radians, [lat1, lon1, lat2, lon2])
dlon = lon2 - lon1
x = np.sin(dlon) * np.cos(lat2)
y = np.cos(lat1) * np.sin(lat2) - np.sin(lat1) * np.cos(lat2) * np.cos(dlon)
bearing = np.arctan2(x, y)
bearing = np.degrees(bearing)
bearing = (bearing + 360) % 360
return bearing
def get_turn_instruction(self, bearing):
"""Convert bearing to turn instruction"""
directions = [
(0, 45, "Head north"),
(45, 135, "Turn right"),
(135, 225, "Turn around"),
(225, 315, "Turn left"),
(315, 360, "Head north")
]
for start, end, instruction in directions:
if start <= bearing < end:
return instruction
return "Continue straight"
def calculate_distance(self, lat1, lon1, lat2, lon2):
"""Calculate distance between two points (Haversine formula)"""
R = 6371 # Earth's radius in km
lat1, lon1, lat2, lon2 = map(np.radians, [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = np.sin(dlat/2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2)**2
c = 2 * np.arcsin(np.sqrt(a))
return R * c
# Example usage
async def main():
optimizer = RouteOptimizer({
'host': 'localhost',
'database': 'fleet_db',
'user': 'fleet_user',
'password': 'password'
})
# Get delivery points
locations = await optimizer.get_delivery_points('2024-01-15')
# Add depot as first location
depot = {
'id': 0,
'customer_name': 'Depot',
'lat': 37.7749,
'lon': -122.4194,
'package_weight': 0
}
locations = [depot] + list(locations)
# Optimize routes
routes = optimizer.optimize_routes(
locations,
num_vehicles=5,
vehicle_capacity=1000
)
# Display results
for route in routes:
print(f"\nVehicle {route['vehicle_id']}:")
print(f"Total distance: {route['total_distance']} km")
for stop in route['route']:
print(f" - {stop['location']['customer_name']} "
f"(Arrival: {stop['arrival_time']} min)")
if __name__ == "__main__":
import asyncio
asyncio.run(main())
EOF
Maintenance Scheduler π§
Automate vehicle maintenance:
# Maintenance scheduler
cat > analytics/maintenance_scheduler.py << 'EOF'
#!/usr/bin/env python3
import asyncio
import asyncpg
from datetime import datetime, timedelta
import smtplib
from email.mime.text import MIMEText
class MaintenanceScheduler:
def __init__(self, db_config):
self.db_config = db_config
self.maintenance_intervals = {
'oil_change': {'mileage': 5000, 'days': 90},
'tire_rotation': {'mileage': 10000, 'days': 180},
'brake_inspection': {'mileage': 20000, 'days': 365},
'transmission_service': {'mileage': 50000, 'days': 730},
'annual_inspection': {'mileage': None, 'days': 365}
}
async def check_maintenance_due(self):
"""Check which vehicles need maintenance"""
conn = await asyncpg.connect(**self.db_config)
# Get all vehicles with their current mileage and last service dates
vehicles = await conn.fetch('''
SELECT v.id, v.make, v.model, v.license_plate,
h.odometer as current_mileage,
m.service_type,
m.mileage_at_service,
m.completed_date
FROM vehicles v
LEFT JOIN LATERAL (
SELECT telemetry->>'vehicle'->>'odometer' as odometer
FROM gps_history
WHERE vehicle_id = v.id
ORDER BY timestamp DESC
LIMIT 1
) h ON true
LEFT JOIN maintenance m ON v.id = m.vehicle_id
WHERE v.status = 'active'
''')
maintenance_due = []
for vehicle in vehicles:
for service_type, intervals in self.maintenance_intervals.items():
if self.is_service_due(vehicle, service_type, intervals):
maintenance_due.append({
'vehicle_id': vehicle['id'],
'vehicle': f"{vehicle['make']} {vehicle['model']} ({vehicle['license_plate']})",
'service_type': service_type,
'current_mileage': vehicle['current_mileage'],
'reason': self.get_service_reason(vehicle, service_type, intervals)
})
await conn.close()
return maintenance_due
def is_service_due(self, vehicle, service_type, intervals):
"""Check if specific service is due"""
# Check mileage-based maintenance
if intervals['mileage'] and vehicle['current_mileage']:
last_service_mileage = 0
for service in vehicle:
if service.get('service_type') == service_type and service.get('mileage_at_service'):
last_service_mileage = max(last_service_mileage, service['mileage_at_service'])
if vehicle['current_mileage'] - last_service_mileage >= intervals['mileage']:
return True
# Check time-based maintenance
last_service_date = None
for service in vehicle:
if service.get('service_type') == service_type and service.get('completed_date'):
if not last_service_date or service['completed_date'] > last_service_date:
last_service_date = service['completed_date']
if last_service_date:
days_since_service = (datetime.now().date() - last_service_date).days
if days_since_service >= intervals['days']:
return True
elif intervals['days']: # No previous service record
return True
return False
def get_service_reason(self, vehicle, service_type, intervals):
"""Get reason why service is due"""
reasons = []
if intervals['mileage']:
reasons.append(f"Every {intervals['mileage']:,} km")
if intervals['days']:
reasons.append(f"Every {intervals['days']} days")
return " or ".join(reasons)
async def schedule_maintenance(self, vehicle_id, service_type, scheduled_date):
"""Schedule maintenance for a vehicle"""
conn = await asyncpg.connect(**self.db_config)
await conn.execute('''
INSERT INTO maintenance (vehicle_id, service_type, scheduled_date)
VALUES ($1, $2, $3)
''', vehicle_id, service_type, scheduled_date)
await conn.close()
async def send_maintenance_alerts(self, maintenance_due):
"""Send email alerts for due maintenance"""
if not maintenance_due:
return
# Group by service type
by_service = {}
for item in maintenance_due:
service = item['service_type']
if service not in by_service:
by_service[service] = []
by_service[service].append(item)
# Create email content
html_content = """
<html>
<body>
<h2>Fleet Maintenance Alert</h2>
<p>The following vehicles require maintenance:</p>
"""
for service_type, vehicles in by_service.items():
html_content += f"<h3>{service_type.replace('_', ' ').title()}</h3><ul>"
for vehicle in vehicles:
html_content += f"<li>{vehicle['vehicle']} - {vehicle['reason']}</li>"
html_content += "</ul>"
html_content += """
<p>Please schedule maintenance appointments as soon as possible.</p>
</body>
</html>
"""
# Send email (configure SMTP settings)
msg = MIMEText(html_content, 'html')
msg['Subject'] = f'Fleet Maintenance Alert - {len(maintenance_due)} vehicles need service'
msg['From'] = '[email protected]'
msg['To'] = '[email protected]'
# In production, configure proper SMTP
# smtp = smtplib.SMTP('localhost')
# smtp.send_message(msg)
# smtp.quit()
print(f"Maintenance alert sent for {len(maintenance_due)} vehicles")
async def run_daily_check(self):
"""Run daily maintenance check"""
print(f"Running maintenance check at {datetime.now()}")
maintenance_due = await self.check_maintenance_due()
if maintenance_due:
print(f"Found {len(maintenance_due)} vehicles needing maintenance:")
for item in maintenance_due:
print(f" - {item['vehicle']}: {item['service_type']}")
await self.send_maintenance_alerts(maintenance_due)
# Auto-schedule maintenance for next week
next_week = datetime.now().date() + timedelta(days=7)
for item in maintenance_due:
await self.schedule_maintenance(
item['vehicle_id'],
item['service_type'],
next_week
)
else:
print("No vehicles require maintenance at this time")
# Run scheduler
async def main():
scheduler = MaintenanceScheduler({
'host': 'localhost',
'database': 'fleet_db',
'user': 'fleet_user',
'password': 'password'
})
# Run once
await scheduler.run_daily_check()
# Or run continuously
# while True:
# await scheduler.run_daily_check()
# await asyncio.sleep(86400) # Check daily
if __name__ == "__main__":
asyncio.run(main())
EOF
System Configuration π§
Set up the complete system:
# Database setup
cat > setup_database.sql << 'EOF'
-- Create database and user
CREATE DATABASE fleet_db;
CREATE USER fleet_user WITH PASSWORD 'password';
GRANT ALL PRIVILEGES ON DATABASE fleet_db TO fleet_user;
-- Connect to fleet_db
\c fleet_db
-- Enable PostGIS extension
CREATE EXTENSION IF NOT EXISTS postgis;
-- Grant permissions
GRANT ALL ON ALL TABLES IN SCHEMA public TO fleet_user;
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO fleet_user;
EOF
# System startup script
cat > start_fleet_system.sh << 'EOF'
#!/bin/sh
# Start Fleet Management System
echo "Starting Fleet Management System..."
# Start PostgreSQL
sudo rc-service postgresql start
# Start Redis
redis-server --daemonize yes
# Start RabbitMQ
sudo rc-service rabbitmq-server start
# Start GPS receiver (simulator)
cd ~/fleet-management
python3 gps/gps_receiver.py &
# Start API server
cd api && npm start &
# Start maintenance scheduler
python3 analytics/maintenance_scheduler.py &
# Serve dashboard
cd ../dashboard
python3 -m http.server 8080 &
echo "Fleet Management System started!"
echo "Dashboard: http://localhost:8080"
echo "API: http://localhost:3000"
EOF
chmod +x start_fleet_system.sh
Best Practices π
- Data retention - Archive old GPS data
- Real-time accuracy - Use quality GPS hardware
- Driver privacy - Implement data protection
- Scalability - Design for fleet growth
- Redundancy - Backup critical systems
Troubleshooting π§
GPS Data Not Updating
# Check GPS receiver
ps aux | grep gps_receiver
# Check MQTT messages
mosquitto_sub -t "gps/#" -v
# Check database connection
psql -U fleet_user -d fleet_db -c "SELECT COUNT(*) FROM gps_history"
Performance Issues
# Optimize PostGIS queries
CREATE INDEX idx_gps_history_geography ON gps_history USING GIST(location);
# Partition large tables
CREATE TABLE gps_history_2024_01 PARTITION OF gps_history
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
Quick Commands π
# Check vehicle locations
curl http://localhost:3000/api/vehicles -H "Authorization: Bearer token"
# Get vehicle route
curl http://localhost:3000/api/vehicles/VEH1001/history?date=2024-01-15
# Schedule maintenance
curl -X POST http://localhost:3000/api/maintenance \
-H "Content-Type: application/json" \
-d '{"vehicle_id":"VEH1001","service_type":"oil_change","scheduled_date":"2024-01-20"}'
# View real-time tracking
open http://localhost:8080
Conclusion π―
Youβve built a comprehensive fleet management system on Alpine Linux! From real-time GPS tracking to route optimization and automated maintenance scheduling, you now have all the tools to manage a modern vehicle fleet efficiently. This system helps reduce costs, improve safety, and maximize fleet productivity. Happy tracking! πβ¨