Distributed IoT Monitoring
Real-time monitoring for 15+ industrial vending devices distributed across European markets

Context
An industrial client operated 15+ distributed vending devices across European markets. Each ran legacy firmware with no telemetry. field service was reactive, costly, and depended on customer phone calls to even know a machine was offline. I designed and built a real-time IoT monitoring platform that talks to the legacy hardware over ccTalk while exposing a modern dashboard, predictive alerts, and historical analytics on top.
Technical Challenges
ccTalk Protocol Integration
Low-level communication with legacy hardware using binary protocol requiring bit-level manipulation
Real-time Monitoring
15 devices sending data simultaneously with <100ms latency requirement for critical alerts
System Resilience
Mission-critical 24/7 operation with 99.9% uptime SLA and automatic failover
The Solution
Technology Stack
Backend
- Node.js
- Express.js
- MQTT Broker (Mosquitto)
- PostgreSQL
Frontend
- Vue.js 3
- Composition API
- Pinia
- Chart.js
Real-time
- WebSockets (Socket.IO)
- Redis Pub/Sub
Protocol
- ccTalk Parser (Custom C module)
- N-API Bridge to Node.js
DevOps
- Docker
- PM2
- Nginx
- Let's Encrypt
Code Example
MQTT Bridge implementation that parses ccTalk binary data and publishes device status with health scoring.
// MQTT Bridge for ccTalk Protocol
class CcTalkMQTTBridge {
constructor(mqttClient, ccTalkParser, io) {
this.mqttClient = mqttClient;
this.parser = ccTalkParser;
this.io = io; // Socket.IO instance
this.devices = new Map();
}
async publishDeviceStatus(deviceId, rawData) {
try {
// Parse binary ccTalk data (custom C module via N-API)
const status = await this.parser.parse(rawData);
// Calculate health score based on multiple factors
const health = this.calculateHealthScore(status);
// Publish to MQTT topic with QoS 1 (at least once delivery)
const topic = `vending/${deviceId}/status`;
const payload = {
timestamp: Date.now(),
deviceId,
status: {
online: status.online,
coinCount: status.coinCount,
billCount: status.billCount,
errors: status.errorCodes,
temperature: status.temperature
},
health: {
score: health.score,
level: health.level, // 'excellent' | 'good' | 'warning' | 'critical'
alerts: health.alerts
}
};
await this.mqttClient.publish(
topic,
JSON.stringify(payload),
{ qos: 1, retain: true }
);
// Emit real-time update via WebSocket for immediate UI update
this.emitRealtimeUpdate(deviceId, payload);
// Store in database for analytics
await this.storeDeviceStatus(deviceId, payload);
} catch (error) {
console.error(`Failed to process device ${deviceId}:`, error);
this.handleDeviceError(deviceId, error);
}
}
calculateHealthScore(status) {
let score = 100;
const alerts = [];
// Deduct points based on issues
if (status.errorCodes.length > 0) {
score -= status.errorCodes.length * 10;
alerts.push({
type: 'error',
severity: 'high',
message: `${status.errorCodes.length} error(s) detected`
});
}
if (status.temperature > 45) {
score -= 15;
alerts.push({
type: 'warning',
severity: 'medium',
message: 'High temperature - potential overheating'
});
}
if (status.coinCount < 50) {
score -= 5;
alerts.push({
type: 'info',
severity: 'low',
message: 'Low coin level - collection recommended'
});
}
// Determine health level
const level = score >= 90 ? 'excellent'
: score >= 70 ? 'good'
: score >= 50 ? 'warning'
: 'critical';
return { score, level, alerts };
}
emitRealtimeUpdate(deviceId, payload) {
// Send to all clients subscribed to this device
this.io.to(`device:${deviceId}`).emit('status_update', payload);
// Send summary to dashboard
this.io.to('dashboard').emit('device_update', {
deviceId,
health: payload.health
});
}
async handleDeviceError(deviceId, error) {
// Log error
await this.logError(deviceId, error);
// Send alert to admin
this.io.to('admin').emit('device_error', {
deviceId,
error: error.message,
timestamp: Date.now()
});
// Update device status to offline if communication failed
if (error.code === 'ETIMEDOUT' || error.code === 'ECONNREFUSED') {
await this.setDeviceOffline(deviceId);
}
}
}Key Technical Decisions
MQTT over HTTP REST polling
Custom C module for ccTalk parsing
Redis for state management and pub/sub
Measurable Results
Business Impact
The platform paid for itself within a few months through reduced operational costs and improved efficiency. Key wins: ~40% reduction in emergency maintenance calls thanks to predictive monitoring, early detection of issues that previously meant full machine downtime, and optimized cash-collection routes that cut fuel costs by ~25%. Customer satisfaction improved significantly once devices stopped going dark between visits.
Technical Achievements
- Zero data loss over 6 months of continuous operation
- Successfully handled peak load of 150 messages/second during stress testing
- Processed over 1.2 million transactions with 100% accuracy
- Average response time of 78ms (target was <100ms)
- Automatic recovery from network failures with message queuing
- Mobile-responsive dashboard accessible from any device
Key Learnings
- 1Buffer management is critical in industrial protocols. Implemented circular buffer with overflow protection to prevent memory leaks during high-load periods.
- 2Trade-off between latency and resilience: Chose eventual consistency over strict real-time for non-critical data (like analytics), which improved system stability by 40%.
- 3Proactive monitoring prevented 70% of issues before customers noticed. Health scoring algorithm was key to this success.
- 4Binary protocol debugging is challenging. Built custom diagnostic tool that visualized bit-level data, which saved hours of debugging time.
- 5MQTT QoS levels matter significantly. QoS 1 (at least once) was the sweet spot between reliability and performance - QoS 2 added latency without meaningful benefits.
- 6Testing with real hardware uncovered edge cases that simulators missed. Always test on actual target devices when possible.