Extending the GroupLoop System
This guide explains how to extend the GroupLoop system by adding new services, clients, features, and integrations.
Extension Overview
The GroupLoop system is designed for extensibility at multiple levels:
graph TB
subgraph "Extension Points"
FIRMWARE[Firmware Extensions]
SERVICES[Service Extensions]
CLIENTS[Client Extensions]
PROTOCOL[Protocol Extensions]
end
subgraph "Firmware Extensions"
PROCESSES[New Processes]
COMMANDS[New Commands]
HARDWARE[Hardware Support]
BEHAVIORS[New Behaviors]
end
subgraph "Service Extensions"
NEW_SERVICES[New Services]
MIDDLEWARE[Middleware]
INTEGRATIONS[External Integrations]
DATABASE[Data Storage]
end
subgraph "Client Extensions"
NEW_CLIENTS[New Clients]
UI_COMPONENTS[UI Components]
VISUALIZATIONS[Visualizations]
MOBILE[Mobile Apps]
end
Firmware Extensions
Adding New Hardware Support
1. Create Hardware Process
// include/processes/NewHardwareProcess.h
#ifndef NEW_HARDWARE_PROCESS_H
#define NEW_HARDWARE_PROCESS_H
#include "Process.h"
#include "config.h"
#include "CommandRegistry.h"
class NewHardwareProcess : public Process {
public:
NewHardwareProcess() : Process() {
// Initialize hardware-specific variables
}
void setup() override {
// Initialize hardware
initializeHardware();
registerCommands();
}
void update() override {
if (!isProcessRunning()) return;
// Read from hardware
readHardwareData();
// Process data
processData();
}
private:
void initializeHardware() {
// Hardware initialization code
pinMode(hardwarePin, INPUT);
Serial.println("New hardware initialized");
}
void readHardwareData() {
// Read sensor data
int value = analogRead(hardwarePin);
// Process and store data
}
void registerCommands() {
commandRegistry.registerCommand("newhardware", [this](const String& params) {
handleCommand(params);
});
}
void handleCommand(const String& params) {
// Command implementation
}
};
#endif
2. Integrate with Data Publishing
// In PublishProcess.h, add new data to sensor frame
void PublishProcess::formatSensorFrame() {
// Get data from new hardware process
NewHardwareProcess* newHardware = static_cast<NewHardwareProcess*>(
processManager->getProcess("newhardware")
);
if (newHardware) {
// Add new hardware data to frame
int hardwareValue = newHardware->getValue();
// Include in hex frame
}
}
Adding New Behaviors
1. Create Behavior Interface
// include/behaviors/NewBehavior.h
#ifndef NEW_BEHAVIOR_H
#define NEW_BEHAVIOR_H
#include "Behavior.h"
class NewBehavior : public Behavior {
public:
void setup() override {
// Initialize behavior
}
void update() override {
// Behavior logic
}
void setParameter(const String& param, const String& value) override {
if (param == "speed") {
speed = value.toInt();
} else if (param == "direction") {
direction = value.toInt();
}
}
private:
int speed = 100;
int direction = 1;
};
#endif
2. Register Behavior
// In process that uses the behavior
void MyProcess::registerCommands() {
commandRegistry.registerCommand("behavior", [this](const String& params) {
if (params == "new") {
setBehavior(&newBehavior);
}
});
}
Service Extensions
Adding New Services
1. Create Service Structure
# Create new service directory
mkdir -p new-service/app
cd new-service
# Create basic files
touch app/__init__.py
touch app/app.py
touch app/requirements.txt
touch Dockerfile
2. Implement Service
# new-service/app/app.py
from flask import Flask, render_template, request, jsonify
import os
import asyncio
import websockets
app = Flask(__name__)
# Configuration
WS_URL = os.environ.get("WS_DEFAULT_URL", "ws://localhost:5003")
CDN_URL = os.environ.get("CDN_BASE_URL", "http://localhost:5008")
@app.route("/")
def index():
return render_template("index.html",
ws_url=WS_URL,
cdn_url=CDN_URL)
@app.route("/api/data")
def get_data():
# Your API implementation
return jsonify({"status": "ok", "data": []})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
3. Create Dockerfile
# new-service/Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY app/requirements.txt .
RUN pip install -r requirements.txt
COPY app/ .
EXPOSE 5000
CMD ["python", "app.py"]
4. Add to Docker Compose
# docker-compose.yml
services:
new_service:
build:
context: ./new-service
dockerfile: Dockerfile
ports:
- "5010:5000"
environment:
- WS_DEFAULT_URL=${WS_DEFAULT_URL:-ws://feib.nl:5003}
- CDN_BASE_URL=${CDN_BASE_URL:-http://cdn.hitloop.feib.nl}
volumes:
- ./new-service:/app
Adding Middleware
1. WebSocket Middleware
# middleware/websocket_middleware.py
import asyncio
import json
from typing import Dict, List
class WebSocketMiddleware:
def __init__(self):
self.message_handlers = []
self.connection_handlers = []
def add_message_handler(self, handler):
self.message_handlers.append(handler)
def add_connection_handler(self, handler):
self.connection_handlers.append(handler)
async def handle_message(self, message: str, websocket, path: str):
# Process message through handlers
for handler in self.message_handlers:
message = await handler(message, websocket, path)
if message is None:
return
return message
async def handle_connection(self, websocket, path: str):
# Process connection through handlers
for handler in self.connection_handlers:
await handler(websocket, path)
2. Authentication Middleware
# middleware/auth_middleware.py
import jwt
from functools import wraps
class AuthMiddleware:
def __init__(self, secret_key: str):
self.secret_key = secret_key
def require_auth(self, f):
@wraps(f)
def decorated_function(*args, **kwargs):
token = request.headers.get('Authorization')
if not token:
return jsonify({'message': 'No token provided'}), 401
try:
data = jwt.decode(token, self.secret_key, algorithms=['HS256'])
request.user = data
except:
return jsonify({'message': 'Invalid token'}), 401
return f(*args, **kwargs)
return decorated_function
Adding External Integrations
1. Database Integration
# integrations/database.py
import sqlite3
import json
from datetime import datetime
class DatabaseIntegration:
def __init__(self, db_path: str):
self.db_path = db_path
self.init_database()
def init_database(self):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS device_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
data TEXT NOT NULL
)
''')
conn.commit()
conn.close()
def store_device_data(self, device_id: str, data: dict):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute(
'INSERT INTO device_data (device_id, data) VALUES (?, ?)',
(device_id, json.dumps(data))
)
conn.commit()
conn.close()
def get_device_data(self, device_id: str, limit: int = 100):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute(
'SELECT * FROM device_data WHERE device_id = ? ORDER BY timestamp DESC LIMIT ?',
(device_id, limit)
)
results = cursor.fetchall()
conn.close()
return results
2. MQTT Integration
# integrations/mqtt.py
import paho.mqtt.client as mqtt
import json
class MQTTIntegration:
def __init__(self, broker_host: str, broker_port: int = 1883):
self.broker_host = broker_host
self.broker_port = broker_port
self.client = mqtt.Client()
self.setup_callbacks()
def setup_callbacks(self):
self.client.on_connect = self.on_connect
self.client.on_message = self.on_message
def on_connect(self, client, userdata, flags, rc):
print(f"Connected to MQTT broker with result code {rc}")
client.subscribe("grouploop/devices/+/data")
def on_message(self, client, userdata, msg):
topic = msg.topic
payload = json.loads(msg.payload.decode())
# Process MQTT message
self.process_message(topic, payload)
def process_message(self, topic: str, payload: dict):
# Extract device ID from topic
device_id = topic.split('/')[2]
# Process device data
print(f"Received data from device {device_id}: {payload}")
def publish_command(self, device_id: str, command: str, params: str = ""):
topic = f"grouploop/devices/{device_id}/commands"
message = {
"command": command,
"parameters": params,
"timestamp": datetime.now().isoformat()
}
self.client.publish(topic, json.dumps(message))
def connect(self):
self.client.connect(self.broker_host, self.broker_port, 60)
self.client.loop_start()
Client Extensions
Creating New Web Clients
1. Basic Client Structure
<!-- new-client/templates/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>New GroupLoop Client</title>
<script src="{{ cdn_url }}/js/HitloopDevice.js"></script>
<script src="{{ cdn_url }}/js/HitloopDeviceManager.js"></script>
</head>
<body>
<div id="app">
<h1>New GroupLoop Client</h1>
<div id="devices"></div>
<div id="controls"></div>
</div>
<script>
// Initialize device manager
const deviceManager = new HitloopDeviceManager('{{ ws_url }}');
// Connect to WebSocket server
deviceManager.connect();
// Handle device updates
deviceManager.onDeviceUpdate = (device) => {
updateDeviceDisplay(device);
};
// Handle new devices
deviceManager.onDeviceAdded = (device) => {
addDeviceToDisplay(device);
};
function updateDeviceDisplay(device) {
// Update device display
}
function addDeviceToDisplay(device) {
// Add device to display
}
</script>
</body>
</html>
2. Custom UI Components
// new-client/static/js/components/DeviceCard.js
class DeviceCard {
constructor(device) {
this.device = device;
this.element = this.createElement();
}
createElement() {
const card = document.createElement('div');
card.className = 'device-card';
card.innerHTML = `
<div class="device-header">
<h3>Device ${this.device.id}</h3>
<span class="status ${this.device.connected ? 'connected' : 'disconnected'}"></span>
</div>
<div class="device-data">
<div class="sensor-data">
<div>X: ${this.device.ax}</div>
<div>Y: ${this.device.ay}</div>
<div>Z: ${this.device.az}</div>
</div>
<div class="controls">
<button onclick="this.sendCommand('led', 'ff0000')">Red LED</button>
<button onclick="this.sendCommand('vibrate', '500')">Vibrate</button>
</div>
</div>
`;
return card;
}
update(device) {
this.device = device;
this.updateDisplay();
}
updateDisplay() {
const status = this.element.querySelector('.status');
status.className = `status ${this.device.connected ? 'connected' : 'disconnected'}`;
const sensorData = this.element.querySelector('.sensor-data');
sensorData.innerHTML = `
<div>X: ${this.device.ax}</div>
<div>Y: ${this.device.ay}</div>
<div>Z: ${this.device.az}</div>
`;
}
sendCommand(command, params) {
this.device.sendCommand(command, params);
}
}
Creating Mobile Apps
1. React Native App
// mobile-app/App.js
import React, { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, FlatList } from 'react-native';
import io from 'socket.io-client';
export default function App() {
const [devices, setDevices] = useState([]);
const [socket, setSocket] = useState(null);
useEffect(() => {
// Connect to WebSocket server
const newSocket = io('ws://your-server:5003');
setSocket(newSocket);
newSocket.on('connect', () => {
console.log('Connected to server');
});
newSocket.on('deviceUpdate', (device) => {
setDevices(prev =>
prev.map(d => d.id === device.id ? device : d)
);
});
newSocket.on('deviceAdded', (device) => {
setDevices(prev => [...prev, device]);
});
return () => newSocket.close();
}, []);
const sendCommand = (deviceId, command, params) => {
if (socket) {
socket.emit('cmd', `${deviceId}:${command}:${params}`);
}
};
const renderDevice = ({ item }) => (
<View style={styles.deviceCard}>
<Text style={styles.deviceId}>Device {item.id}</Text>
<Text>X: {item.ax}, Y: {item.ay}, Z: {item.az}</Text>
<TouchableOpacity
onPress={() => sendCommand(item.id, 'led', 'ff0000')}
style={styles.button}
>
<Text>Red LED</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => sendCommand(item.id, 'vibrate', '500')}
style={styles.button}
>
<Text>Vibrate</Text>
</TouchableOpacity>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.title}>GroupLoop Mobile</Text>
<FlatList
data={devices}
renderItem={renderDevice}
keyExtractor={item => item.id}
/>
</View>
);
}
Protocol Extensions
Adding New Message Types
1. Extend WebSocket Protocol
# extensions/protocol.py
class ExtendedProtocol:
def __init__(self):
self.message_handlers = {
'cmd': self.handle_command,
'config': self.handle_config,
'query': self.handle_query, # New message type
'subscribe': self.handle_subscribe, # New message type
}
def handle_query(self, message: str, websocket, path: str):
# Handle query messages
parts = message.split(':', 2)
if len(parts) >= 2:
query_type = parts[0]
query_params = parts[1] if len(parts) > 1 else ""
if query_type == "devices":
return self.query_devices(websocket)
elif query_type == "status":
return self.query_status(websocket)
return message
def handle_subscribe(self, message: str, websocket, path: str):
# Handle subscription messages
parts = message.split(':', 1)
if len(parts) >= 1:
subscription_type = parts[0]
self.add_subscription(websocket, subscription_type)
return message
2. Extend Command Protocol
{
"commands": {
"led": {
"handler": "led",
"parameters": ["color"],
"description": "Set LED color"
},
"newcommand": {
"handler": "newcommand",
"parameters": ["param1", "param2"],
"description": "New command with multiple parameters",
"examples": ["newcommand:value1:value2"]
}
}
}
Testing Extensions
Unit Testing
# tests/test_extensions.py
import unittest
from unittest.mock import Mock, patch
from extensions.protocol import ExtendedProtocol
class TestExtensions(unittest.TestCase):
def setUp(self):
self.protocol = ExtendedProtocol()
self.mock_websocket = Mock()
def test_query_devices(self):
result = self.protocol.query_devices(self.mock_websocket)
self.assertIsNotNone(result)
def test_subscribe(self):
message = "subscribe:devices"
result = self.protocol.handle_subscribe(message, self.mock_websocket, "/")
self.assertIsNotNone(result)
Integration Testing
# tests/test_integration.py
import asyncio
import websockets
import json
async def test_websocket_connection():
uri = "ws://localhost:5003"
async with websockets.connect(uri) as websocket:
# Test basic connection
await websocket.send("ping")
response = await websocket.recv()
assert response == "pong"
# Test new message types
await websocket.send("query:devices")
response = await websocket.recv()
assert "devices" in response
if __name__ == "__main__":
asyncio.run(test_websocket_connection())
Best Practices
1. Code Organization
- Follow existing project structure
- Use consistent naming conventions
- Document all new features
- Write tests for new functionality
2. Error Handling
- Implement proper error handling
- Provide meaningful error messages
- Log errors appropriately
- Handle edge cases gracefully
3. Performance
- Optimize for real-time performance
- Minimize memory usage
- Use efficient data structures
- Profile and benchmark changes
4. Security
- Validate all inputs
- Implement proper authentication
- Use secure communication protocols
- Regular security audits
5. Maintenance
- Version control all changes
- Document configuration changes
- Plan for backward compatibility
- Regular code reviews
Deployment Considerations
1. Configuration
- Use environment variables for configuration
- Provide default values
- Document configuration options
- Validate configuration on startup
2. Monitoring
- Add health check endpoints
- Implement logging
- Monitor performance metrics
- Set up alerts for failures
3. Scaling
- Design for horizontal scaling
- Use stateless services
- Implement load balancing
- Plan for data persistence
4. Updates
- Plan for rolling updates
- Implement feature flags
- Test in staging environment
- Have rollback procedures