Jorge Morais — Senior Full-Stack Developer at the intersection of AI and Industrial IoT · Remote, EU

Distributed IoT Monitoring

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

3 months
Lead Developer
2023
Node.jsVue.js 3MQTTccTalk ProtocolWebSocketsCPostgreSQL
Distributed IoT Monitoring preview

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.

JavaScript
// 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

Rationale: Reduces bandwidth by 80%, enables true real-time updates with pub/sub pattern, and handles 15 concurrent devices efficiently
Trade-off: Requires persistent connections and more complex infrastructure (MQTT broker), but worth it for real-time requirements

Custom C module for ccTalk parsing

Rationale: Binary protocol requires low-level bit manipulation. C module is 10x faster than pure JavaScript implementation and handles complex binary operations more reliably
Trade-off: Platform-dependent compilation and harder to maintain, but critical for performance and reliability

Redis for state management and pub/sub

Rationale: Fast in-memory pub/sub for WebSocket fanout, persistent device state, and enables horizontal scaling
Trade-off: Additional infrastructure component to manage, but essential for real-time architecture

Measurable Results

-40%Maintenance Time
+25%Operational Efficiency
50kTransactions/Month
99.8%System Uptime
<80msAverage Latency
15Devices Monitored

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.