Skip to content

GroupLoop Client Libraries

This document describes the shared JavaScript libraries provided by the CDN server and how to use them in your applications.

Library Overview

The GroupLoop system provides several shared JavaScript libraries through the CDN server:

graph TB
    subgraph "CDN Server"
        CDN[CDN Server<br/>cdn-server:5008]
    end

    subgraph "Shared Libraries"
        DEVICE[HitloopDevice.js<br/>Device abstraction]
        MANAGER[HitloopDeviceManager.js<br/>Device management]
        UTILS[Utils.js<br/>Utility functions]
        P5[p5.min.js<br/>Graphics library]
        SOUND[p5.sound.min.js<br/>Audio library]
    end

    subgraph "Client Applications"
        CLIENT[Socket Client]
        DEMO[Socket Demo]
        CONTROL[Device Control]
        SIM[Simulator]
        EMU[Emulator]
    end

    CDN --> DEVICE
    CDN --> MANAGER
    CDN --> UTILS
    CDN --> P5
    CDN --> SOUND

    DEVICE --> CLIENT
    MANAGER --> CLIENT
    DEVICE --> DEMO
    MANAGER --> DEMO
    DEVICE --> CONTROL
    MANAGER --> CONTROL
    DEVICE --> SIM
    MANAGER --> SIM
    DEVICE --> EMU
    MANAGER --> EMU

HitloopDevice.js

The HitloopDevice class provides a high-level abstraction for individual devices.

Basic Usage

<script src="http://cdn.hitloop.feib.nl/js/HitloopDevice.js"></script>
<script>
    // Create a device instance
    const device = new HitloopDevice('1234', 'ws://localhost:5003');

    // Connect to the device
    device.connect();

    // Handle device updates
    device.onUpdate = (data) => {
        console.log('Device updated:', data);
    };

    // Send commands to the device
    device.sendCommand('led', 'ff0000');
    device.sendCommand('vibrate', '500');
</script>

Device Properties

class HitloopDevice {
    constructor(deviceId, wsUrl) {
        this.id = deviceId;           // Device identifier
        this.wsUrl = wsUrl;          // WebSocket server URL
        this.connected = false;      // Connection status
        this.lastUpdate = null;      // Last update timestamp

        // Sensor data
        this.ax = 0;                 // Accelerometer X
        this.ay = 0;                 // Accelerometer Y
        this.az = 0;                 // Accelerometer Z

        // BLE beacon data
        this.dNW = 0;                // Northwest beacon RSSI
        this.dNE = 0;                // Northeast beacon RSSI
        this.dSW = 0;                // Southwest beacon RSSI
        this.dSE = 0;                // Southeast beacon RSSI

        // Device state
        this.color = 0;              // Current LED color
        this.motor = 0;              // Motor state
    }
}

Device Methods

Connection Management

// Connect to the device
device.connect();

// Disconnect from the device
device.disconnect();

// Check connection status
if (device.isConnected()) {
    console.log('Device is connected');
}

Command Sending

// Send a command with parameters
device.sendCommand('led', 'ff0000');
device.sendCommand('vibrate', '500');
device.sendCommand('pattern', 'breathing');

// Send a command without parameters
device.sendCommand('status');

// Send a command with multiple parameters
device.sendCommand('spring_param', '10050A');

Event Handling

// Handle device updates
device.onUpdate = (data) => {
    console.log('Device data updated:', data);
    updateUI(data);
};

// Handle connection events
device.onConnect = () => {
    console.log('Device connected');
    showConnectionStatus(true);
};

device.onDisconnect = () => {
    console.log('Device disconnected');
    showConnectionStatus(false);
};

// Handle errors
device.onError = (error) => {
    console.error('Device error:', error);
    showError(error);
};

Data Parsing

// Parse sensor data from hex frame
device.parseSensorFrame('1234a1b2c3d4e5f678901234567890');

// The frame is parsed into:
// - Device ID: 1234
// - Accelerometer: ax=a1, ay=b2, az=c3
// - BLE RSSI: dNW=d4, dNE=e5, dSW=f6, dSE=78
// - State: color=90, motor=12
// - Reserved: 34567890

HitloopDeviceManager.js

The HitloopDeviceManager class manages multiple devices and handles WebSocket communication.

Basic Usage

<script src="http://cdn.hitloop.feib.nl/js/HitloopDeviceManager.js"></script>
<script>
    // Create device manager
    const deviceManager = new HitloopDeviceManager('ws://localhost:5003');

    // Connect to WebSocket server
    deviceManager.connect();

    // Handle device events
    deviceManager.onDeviceAdded = (device) => {
        console.log('New device added:', device.id);
        addDeviceToUI(device);
    };

    deviceManager.onDeviceUpdate = (device) => {
        console.log('Device updated:', device.id);
        updateDeviceInUI(device);
    };

    deviceManager.onDeviceRemoved = (deviceId) => {
        console.log('Device removed:', deviceId);
        removeDeviceFromUI(deviceId);
    };
</script>

Manager Properties

class HitloopDeviceManager {
    constructor(wsUrl) {
        this.wsUrl = wsUrl;              // WebSocket server URL
        this.connected = false;          // Connection status
        this.devices = new Map();        // Map of device ID to device objects
        this.subscribers = new Set();    // Set of subscriber WebSockets

        // Event handlers
        this.onDeviceAdded = null;       // Called when device is added
        this.onDeviceUpdate = null;      // Called when device is updated
        this.onDeviceRemoved = null;     // Called when device is removed
        this.onConnect = null;           // Called when connected
        this.onDisconnect = null;        // Called when disconnected
        this.onError = null;             // Called on error
    }
}

Manager Methods

Connection Management

// Connect to WebSocket server
deviceManager.connect();

// Disconnect from server
deviceManager.disconnect();

// Check connection status
if (deviceManager.isConnected()) {
    console.log('Connected to server');
}

Device Management

// Get device by ID
const device = deviceManager.getDevice('1234');

// Get all devices
const allDevices = deviceManager.getAllDevices();

// Check if device exists
if (deviceManager.hasDevice('1234')) {
    console.log('Device exists');
}

// Get device count
const count = deviceManager.getDeviceCount();

Command Broadcasting

// Send command to specific device
deviceManager.sendCommand('1234', 'led', 'ff0000');

// Send command to all devices
deviceManager.broadcastCommand('vibrate', '500');

// Send command with callback
deviceManager.sendCommand('1234', 'status', '', (result) => {
    console.log('Command result:', result);
});

Subscription Management

// Subscribe to device updates
deviceManager.subscribe();

// Unsubscribe from updates
deviceManager.unsubscribe();

// Check subscription status
if (deviceManager.isSubscribed()) {
    console.log('Subscribed to updates');
}

Event Handling

// Device events
deviceManager.onDeviceAdded = (device) => {
    console.log('New device:', device.id);
    // Add device to UI
};

deviceManager.onDeviceUpdate = (device) => {
    console.log('Device updated:', device.id);
    // Update device in UI
};

deviceManager.onDeviceRemoved = (deviceId) => {
    console.log('Device removed:', deviceId);
    // Remove device from UI
};

// Connection events
deviceManager.onConnect = () => {
    console.log('Connected to server');
    // Update connection status
};

deviceManager.onDisconnect = () => {
    console.log('Disconnected from server');
    // Update connection status
};

// Error handling
deviceManager.onError = (error) => {
    console.error('Manager error:', error);
    // Show error message
};

Utility Functions

Data Conversion

// Convert hex string to RGB values
function hexToRgb(hex) {
    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
    } : null;
}

// Convert RGB values to hex string
function rgbToHex(r, g, b) {
    return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}

// Normalize sensor value (0-255 to -1 to 1)
function normalizeSensor(value) {
    return (value - 127.5) / 127.5;
}

Color Utilities

// Predefined colors
const COLORS = {
    RED: '#ff0000',
    GREEN: '#00ff00',
    BLUE: '#0000ff',
    YELLOW: '#ffff00',
    CYAN: '#00ffff',
    MAGENTA: '#ff00ff',
    WHITE: '#ffffff',
    BLACK: '#000000'
};

// Get random color
function getRandomColor() {
    const colors = Object.values(COLORS);
    return colors[Math.floor(Math.random() * colors.length)];
}

// Interpolate between colors
function interpolateColor(color1, color2, factor) {
    const rgb1 = hexToRgb(color1);
    const rgb2 = hexToRgb(color2);

    return rgbToHex(
        Math.round(rgb1.r + (rgb2.r - rgb1.r) * factor),
        Math.round(rgb1.g + (rgb2.g - rgb1.g) * factor),
        Math.round(rgb1.b + (rgb2.b - rgb1.b) * factor)
    );
}

Animation Utilities

// Easing functions
const Easing = {
    linear: t => t,
    easeInQuad: t => t * t,
    easeOutQuad: t => t * (2 - t),
    easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
    easeInCubic: t => t * t * t,
    easeOutCubic: t => (--t) * t * t + 1,
    easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
};

// Animation class
class Animation {
    constructor(duration, easing = Easing.linear) {
        this.duration = duration;
        this.easing = easing;
        this.startTime = null;
        this.isRunning = false;
    }

    start() {
        this.startTime = Date.now();
        this.isRunning = true;
    }

    stop() {
        this.isRunning = false;
    }

    getProgress() {
        if (!this.isRunning) return 0;

        const elapsed = Date.now() - this.startTime;
        const progress = Math.min(elapsed / this.duration, 1);

        if (progress >= 1) {
            this.isRunning = false;
        }

        return this.easing(progress);
    }
}

Integration Examples

React Component

import React, { useState, useEffect } from 'react';

function DeviceList() {
    const [devices, setDevices] = useState([]);
    const [deviceManager, setDeviceManager] = useState(null);

    useEffect(() => {
        const manager = new HitloopDeviceManager('ws://localhost:5003');

        manager.onDeviceAdded = (device) => {
            setDevices(prev => [...prev, device]);
        };

        manager.onDeviceUpdate = (device) => {
            setDevices(prev => 
                prev.map(d => d.id === device.id ? device : d)
            );
        };

        manager.onDeviceRemoved = (deviceId) => {
            setDevices(prev => prev.filter(d => d.id !== deviceId));
        };

        manager.connect();
        setDeviceManager(manager);

        return () => manager.disconnect();
    }, []);

    const sendCommand = (deviceId, command, params) => {
        if (deviceManager) {
            deviceManager.sendCommand(deviceId, command, params);
        }
    };

    return (
        <div>
            <h2>Connected Devices</h2>
            {devices.map(device => (
                <div key={device.id} className="device-card">
                    <h3>Device {device.id}</h3>
                    <p>X: {device.ax}, Y: {device.ay}, Z: {device.az}</p>
                    <button onClick={() => sendCommand(device.id, 'led', 'ff0000')}>
                        Red LED
                    </button>
                    <button onClick={() => sendCommand(device.id, 'vibrate', '500')}>
                        Vibrate
                    </button>
                </div>
            ))}
        </div>
    );
}

Vue.js Component

<template>
    <div>
        <h2>Connected Devices</h2>
        <div v-for="device in devices" :key="device.id" class="device-card">
            <h3>Device {{ device.id }}</h3>
            <p>X: {{ device.ax }}, Y: {{ device.ay }}, Z: {{ device.az }}</p>
            <button @click="sendCommand(device.id, 'led', 'ff0000')">
                Red LED
            </button>
            <button @click="sendCommand(device.id, 'vibrate', '500')">
                Vibrate
            </button>
        </div>
    </div>
</template>

<script>
export default {
    data() {
        return {
            devices: [],
            deviceManager: null
        };
    },

    mounted() {
        this.deviceManager = new HitloopDeviceManager('ws://localhost:5003');

        this.deviceManager.onDeviceAdded = (device) => {
            this.devices.push(device);
        };

        this.deviceManager.onDeviceUpdate = (device) => {
            const index = this.devices.findIndex(d => d.id === device.id);
            if (index !== -1) {
                this.devices.splice(index, 1, device);
            }
        };

        this.deviceManager.onDeviceRemoved = (deviceId) => {
            const index = this.devices.findIndex(d => d.id === deviceId);
            if (index !== -1) {
                this.devices.splice(index, 1);
            }
        };

        this.deviceManager.connect();
    },

    beforeDestroy() {
        if (this.deviceManager) {
            this.deviceManager.disconnect();
        }
    },

    methods: {
        sendCommand(deviceId, command, params) {
            if (this.deviceManager) {
                this.deviceManager.sendCommand(deviceId, command, params);
            }
        }
    }
};
</script>

Angular Service

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

@Injectable({
    providedIn: 'root'
})
export class DeviceService {
    private deviceManager: any;
    private devicesSubject = new BehaviorSubject<any[]>([]);
    public devices$ = this.devicesSubject.asObservable();

    constructor() {
        this.deviceManager = new (window as any).HitloopDeviceManager('ws://localhost:5003');

        this.deviceManager.onDeviceAdded = (device: any) => {
            const devices = this.devicesSubject.value;
            this.devicesSubject.next([...devices, device]);
        };

        this.deviceManager.onDeviceUpdate = (device: any) => {
            const devices = this.devicesSubject.value;
            const index = devices.findIndex(d => d.id === device.id);
            if (index !== -1) {
                devices[index] = device;
                this.devicesSubject.next([...devices]);
            }
        };

        this.deviceManager.onDeviceRemoved = (deviceId: string) => {
            const devices = this.devicesSubject.value;
            this.devicesSubject.next(devices.filter(d => d.id !== deviceId));
        };

        this.deviceManager.connect();
    }

    sendCommand(deviceId: string, command: string, params: string = '') {
        if (this.deviceManager) {
            this.deviceManager.sendCommand(deviceId, command, params);
        }
    }

    broadcastCommand(command: string, params: string = '') {
        if (this.deviceManager) {
            this.deviceManager.broadcastCommand(command, params);
        }
    }
}

Best Practices

1. Error Handling

// Always handle connection errors
deviceManager.onError = (error) => {
    console.error('Connection error:', error);
    // Implement retry logic
    setTimeout(() => {
        deviceManager.connect();
    }, 5000);
};

// Handle command errors
device.sendCommand('led', 'ff0000', (result) => {
    if (result.error) {
        console.error('Command failed:', result.error);
    } else {
        console.log('Command successful:', result);
    }
});

2. Performance Optimization

// Throttle device updates
let lastUpdate = 0;
const UPDATE_THROTTLE = 100; // 100ms

deviceManager.onDeviceUpdate = (device) => {
    const now = Date.now();
    if (now - lastUpdate > UPDATE_THROTTLE) {
        lastUpdate = now;
        updateUI(device);
    }
};

// Use requestAnimationFrame for smooth animations
function animateDevice(device) {
    requestAnimationFrame(() => {
        updateDeviceAnimation(device);
    });
}

3. Memory Management

// Clean up event listeners
deviceManager.onDeviceAdded = null;
deviceManager.onDeviceUpdate = null;
deviceManager.onDeviceRemoved = null;

// Disconnect when component unmounts
useEffect(() => {
    return () => {
        deviceManager.disconnect();
    };
}, []);

4. Configuration

// Use environment variables for configuration
const WS_URL = process.env.REACT_APP_WS_URL || 'ws://localhost:5003';
const CDN_URL = process.env.REACT_APP_CDN_URL || 'http://localhost:5008';

// Create device manager with configuration
const deviceManager = new HitloopDeviceManager(WS_URL);

Troubleshooting

Common Issues

1. Connection Failures

// Check WebSocket URL
console.log('Connecting to:', deviceManager.wsUrl);

// Handle connection errors
deviceManager.onError = (error) => {
    console.error('Connection error:', error);
    // Check if server is running
    // Verify URL format
    // Check firewall settings
};

2. Device Not Found

// Check if device exists
if (deviceManager.hasDevice('1234')) {
    const device = deviceManager.getDevice('1234');
    device.sendCommand('led', 'ff0000');
} else {
    console.log('Device 1234 not found');
}

3. Command Failures

// Check command format
device.sendCommand('led', 'ff0000', (result) => {
    if (result.error) {
        console.error('Command error:', result.error);
        // Check command syntax
        // Verify device is connected
        // Check command registry
    }
});

Debug Mode

// Enable debug logging
deviceManager.debug = true;

// Log all events
deviceManager.onDeviceAdded = (device) => {
    console.log('Device added:', device);
};

deviceManager.onDeviceUpdate = (device) => {
    console.log('Device updated:', device);
};