Initial commit: Portfolio Website
Vollständige Next.js 15 Portfolio-Website mit: - Blog-System mit 100+ Artikeln - Supabase-Integration - Responsive Design mit Tailwind CSS - TypeScript-Konfiguration - Testing-Setup mit Vitest und Playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,766 @@
|
||||
# Raspberry Pi IoT Projekte mit Node.js
|
||||
|
||||
**Meta-Description:** IoT-Projekte mit Raspberry Pi und Node.js. GPIO-Steuerung, Sensor-Integration, MQTT und Web Dashboards.
|
||||
|
||||
**Keywords:** Raspberry Pi, Node.js, GPIO, IoT, Sensors, Home Automation, MQTT, Embedded Linux
|
||||
|
||||
---
|
||||
|
||||
## Einführung
|
||||
|
||||
Der **Raspberry Pi** ist der perfekte Single-Board Computer für IoT-Projekte. Mit **Node.js** kombiniert man die Hardware-Nähe mit dem mächtigen npm-Ökosystem für Web-basierte IoT-Anwendungen.
|
||||
|
||||
---
|
||||
|
||||
## Raspberry Pi Setup
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ RASPBERRY PI 5 SPECIFICATIONS │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ CPU: Broadcom BCM2712 quad-core Arm Cortex-A76 @ 2.4GHz │
|
||||
│ RAM: 4GB / 8GB LPDDR4X-4267 │
|
||||
│ │
|
||||
│ Connectivity: │
|
||||
│ ├── Gigabit Ethernet │
|
||||
│ ├── Dual-band WiFi 802.11ac │
|
||||
│ ├── Bluetooth 5.0 / BLE │
|
||||
│ ├── 2× USB 3.0 + 2× USB 2.0 │
|
||||
│ └── PCIe 2.0 x1 │
|
||||
│ │
|
||||
│ GPIO: │
|
||||
│ ├── 40-pin GPIO Header │
|
||||
│ ├── 26 GPIO Pins │
|
||||
│ ├── I2C, SPI, UART │
|
||||
│ └── PWM (Hardware) │
|
||||
│ │
|
||||
│ Storage: microSD / NVMe (via HAT) │
|
||||
│ Power: 5V/5A USB-C │
|
||||
│ │
|
||||
│ GPIO Pinout (Subset): │
|
||||
│ ┌────┬────┬────────────────────────┐ │
|
||||
│ │ 1 │ 2 │ 3.3V | 5V │ │
|
||||
│ │ 3 │ 4 │ GPIO2 | 5V │ │
|
||||
│ │ 5 │ 6 │ GPIO3 | GND │ │
|
||||
│ │ 7 │ 8 │ GPIO4 | GPIO14(TX) │ │
|
||||
│ │ 9 │ 10 │ GND | GPIO15(RX) │ │
|
||||
│ └────┴────┴────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Node.js Installation
|
||||
|
||||
```bash
|
||||
# Node.js via NodeSource (empfohlen)
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
|
||||
# Alternativ: NVM (Node Version Manager)
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
|
||||
source ~/.bashrc
|
||||
nvm install 22
|
||||
nvm use 22
|
||||
|
||||
# Verify
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
# Build Tools für native Addons
|
||||
sudo apt-get install -y build-essential python3
|
||||
|
||||
# GPIO Berechtigungen (ohne sudo)
|
||||
sudo usermod -aG gpio $USER
|
||||
sudo usermod -aG i2c $USER
|
||||
sudo usermod -aG spi $USER
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GPIO Control mit Node.js
|
||||
|
||||
```typescript
|
||||
// lib/gpio.ts
|
||||
import { Gpio } from 'onoff';
|
||||
|
||||
// LED Control
|
||||
const LED_PIN = 17;
|
||||
const led = new Gpio(LED_PIN, 'out');
|
||||
|
||||
// LED einschalten
|
||||
led.writeSync(1);
|
||||
|
||||
// LED ausschalten
|
||||
led.writeSync(0);
|
||||
|
||||
// Asynchrone Steuerung
|
||||
async function blinkLED(times: number, intervalMs: number) {
|
||||
for (let i = 0; i < times; i++) {
|
||||
await led.write(1);
|
||||
await sleep(intervalMs);
|
||||
await led.write(0);
|
||||
await sleep(intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
// Button mit Interrupt
|
||||
const BUTTON_PIN = 18;
|
||||
const button = new Gpio(BUTTON_PIN, 'in', 'both', { debounceTimeout: 50 });
|
||||
|
||||
button.watch((err, value) => {
|
||||
if (err) {
|
||||
console.error('Button error:', err);
|
||||
return;
|
||||
}
|
||||
console.log(`Button: ${value === 1 ? 'pressed' : 'released'}`);
|
||||
});
|
||||
|
||||
// Cleanup bei Programmende
|
||||
process.on('SIGINT', () => {
|
||||
led.unexport();
|
||||
button.unexport();
|
||||
process.exit();
|
||||
});
|
||||
|
||||
// Helper
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Für Raspberry Pi 5: gpiod verwenden
|
||||
// lib/gpio-pi5.ts
|
||||
import { Chip, Line } from '@iiot2k/gpiox';
|
||||
|
||||
const chip = new Chip(4); // Pi5 verwendet Chip 4
|
||||
|
||||
// LED Setup
|
||||
const ledLine = chip.getLine(17);
|
||||
ledLine.requestOutput('led');
|
||||
|
||||
// LED steuern
|
||||
ledLine.setValue(1); // On
|
||||
ledLine.setValue(0); // Off
|
||||
|
||||
// Button Setup mit Events
|
||||
const buttonLine = chip.getLine(18);
|
||||
buttonLine.requestInput('button');
|
||||
buttonLine.requestBothEdges();
|
||||
|
||||
buttonLine.addEventListener('value', (event) => {
|
||||
console.log(`Button value: ${event.value}`);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sensor-Integration
|
||||
|
||||
```typescript
|
||||
// lib/sensors/dht22.ts
|
||||
import sensor from 'node-dht-sensor';
|
||||
|
||||
const DHT_TYPE = 22; // DHT22
|
||||
const DHT_PIN = 4;
|
||||
|
||||
interface Reading {
|
||||
temperature: number;
|
||||
humidity: number;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export async function readDHT22(): Promise<Reading> {
|
||||
return new Promise((resolve, reject) => {
|
||||
sensor.read(DHT_TYPE, DHT_PIN, (err, temperature, humidity) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({
|
||||
temperature: Math.round(temperature * 10) / 10,
|
||||
humidity: Math.round(humidity * 10) / 10,
|
||||
timestamp: new Date()
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Retry Logic für zuverlässigere Messungen
|
||||
export async function readDHT22WithRetry(maxRetries = 3): Promise<Reading> {
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const reading = await readDHT22();
|
||||
|
||||
// Plausibilitätsprüfung
|
||||
if (reading.temperature > -40 && reading.temperature < 80 &&
|
||||
reading.humidity >= 0 && reading.humidity <= 100) {
|
||||
return reading;
|
||||
}
|
||||
} catch (err) {
|
||||
lastError = err as Error;
|
||||
await sleep(2000); // DHT braucht Zeit zwischen Messungen
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('Failed to read DHT22');
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// lib/sensors/bme280.ts
|
||||
import BME280 from 'bme280-sensor';
|
||||
|
||||
const bme280 = new BME280({ i2cBusNo: 1, i2cAddress: 0x76 });
|
||||
|
||||
interface EnvironmentData {
|
||||
temperature: number;
|
||||
humidity: number;
|
||||
pressure: number;
|
||||
}
|
||||
|
||||
export async function initBME280(): Promise<void> {
|
||||
await bme280.init();
|
||||
console.log('BME280 initialized');
|
||||
}
|
||||
|
||||
export async function readBME280(): Promise<EnvironmentData> {
|
||||
const data = await bme280.readSensorData();
|
||||
|
||||
return {
|
||||
temperature: Math.round(data.temperature_C * 10) / 10,
|
||||
humidity: Math.round(data.humidity * 10) / 10,
|
||||
pressure: Math.round(data.pressure_hPa * 10) / 10
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// lib/sensors/ultrasonic.ts
|
||||
import { Gpio } from 'onoff';
|
||||
|
||||
const TRIG_PIN = 23;
|
||||
const ECHO_PIN = 24;
|
||||
|
||||
const trigger = new Gpio(TRIG_PIN, 'out');
|
||||
const echo = new Gpio(ECHO_PIN, 'in', 'both');
|
||||
|
||||
export function measureDistance(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let startTime: bigint;
|
||||
let endTime: bigint;
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
// Timeout nach 1 Sekunde
|
||||
timeout = setTimeout(() => {
|
||||
reject(new Error('Measurement timeout'));
|
||||
}, 1000);
|
||||
|
||||
echo.watch((err, value) => {
|
||||
if (err) {
|
||||
clearTimeout(timeout);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === 1) {
|
||||
startTime = process.hrtime.bigint();
|
||||
} else {
|
||||
endTime = process.hrtime.bigint();
|
||||
|
||||
clearTimeout(timeout);
|
||||
|
||||
// Distanz berechnen
|
||||
const duration = Number(endTime - startTime) / 1e9; // Sekunden
|
||||
const distance = (duration * 34300) / 2; // cm
|
||||
|
||||
resolve(Math.round(distance * 10) / 10);
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger Puls
|
||||
trigger.writeSync(1);
|
||||
setTimeout(() => trigger.writeSync(0), 10); // 10µs Puls
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MQTT Sensor Node
|
||||
|
||||
```typescript
|
||||
// apps/sensor-node.ts
|
||||
import mqtt from 'mqtt';
|
||||
import { readDHT22WithRetry } from './lib/sensors/dht22';
|
||||
import { measureDistance } from './lib/sensors/ultrasonic';
|
||||
import os from 'os';
|
||||
|
||||
interface SensorNodeConfig {
|
||||
brokerUrl: string;
|
||||
deviceId: string;
|
||||
readIntervalMs: number;
|
||||
}
|
||||
|
||||
class SensorNode {
|
||||
private client: mqtt.MqttClient;
|
||||
private config: SensorNodeConfig;
|
||||
private baseTopic: string;
|
||||
private running = false;
|
||||
|
||||
constructor(config: SensorNodeConfig) {
|
||||
this.config = config;
|
||||
this.baseTopic = `sensors/${config.deviceId}`;
|
||||
|
||||
this.client = mqtt.connect(config.brokerUrl, {
|
||||
clientId: config.deviceId,
|
||||
will: {
|
||||
topic: `${this.baseTopic}/status`,
|
||||
payload: Buffer.from('offline'),
|
||||
qos: 1,
|
||||
retain: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
await this.waitForConnection();
|
||||
|
||||
// Online Status
|
||||
this.client.publish(
|
||||
`${this.baseTopic}/status`,
|
||||
'online',
|
||||
{ retain: true }
|
||||
);
|
||||
|
||||
// Device Info
|
||||
this.client.publish(
|
||||
`${this.baseTopic}/info`,
|
||||
JSON.stringify({
|
||||
device_id: this.config.deviceId,
|
||||
hostname: os.hostname(),
|
||||
platform: os.platform(),
|
||||
arch: os.arch(),
|
||||
uptime: os.uptime(),
|
||||
memory: {
|
||||
total: os.totalmem(),
|
||||
free: os.freemem()
|
||||
}
|
||||
}),
|
||||
{ retain: true }
|
||||
);
|
||||
|
||||
// Commands abonnieren
|
||||
this.client.subscribe(`${this.baseTopic}/command`);
|
||||
this.client.on('message', (topic, payload) => {
|
||||
this.handleCommand(payload.toString());
|
||||
});
|
||||
|
||||
this.running = true;
|
||||
this.sensorLoop();
|
||||
}
|
||||
|
||||
private async sensorLoop(): Promise<void> {
|
||||
while (this.running) {
|
||||
try {
|
||||
// Alle Sensoren lesen
|
||||
const [dht, distance] = await Promise.allSettled([
|
||||
readDHT22WithRetry(),
|
||||
measureDistance()
|
||||
]);
|
||||
|
||||
const data: Record<string, any> = {
|
||||
device_id: this.config.deviceId,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (dht.status === 'fulfilled') {
|
||||
data.temperature = dht.value.temperature;
|
||||
data.humidity = dht.value.humidity;
|
||||
}
|
||||
|
||||
if (distance.status === 'fulfilled') {
|
||||
data.distance = distance.value;
|
||||
}
|
||||
|
||||
// System Metrics
|
||||
data.system = {
|
||||
cpu_temp: await this.getCPUTemperature(),
|
||||
memory_usage: (1 - os.freemem() / os.totalmem()) * 100,
|
||||
load_average: os.loadavg()[0]
|
||||
};
|
||||
|
||||
// Publish
|
||||
this.client.publish(
|
||||
`${this.baseTopic}/data`,
|
||||
JSON.stringify(data),
|
||||
{ qos: 1 }
|
||||
);
|
||||
|
||||
console.log('Published:', data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Sensor read error:', error);
|
||||
}
|
||||
|
||||
await this.sleep(this.config.readIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
private handleCommand(payload: string): void {
|
||||
try {
|
||||
const command = JSON.parse(payload);
|
||||
|
||||
switch (command.action) {
|
||||
case 'restart':
|
||||
console.log('Restart command received');
|
||||
process.exit(0); // Systemd wird neustarten
|
||||
break;
|
||||
|
||||
case 'setInterval':
|
||||
this.config.readIntervalMs = command.value;
|
||||
console.log(`Interval set to ${command.value}ms`);
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
this.publishStatus();
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Command parse error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async getCPUTemperature(): Promise<number> {
|
||||
try {
|
||||
const { readFile } = await import('fs/promises');
|
||||
const temp = await readFile('/sys/class/thermal/thermal_zone0/temp', 'utf8');
|
||||
return parseInt(temp) / 1000;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private publishStatus(): void {
|
||||
this.client.publish(
|
||||
`${this.baseTopic}/status/detailed`,
|
||||
JSON.stringify({
|
||||
running: this.running,
|
||||
uptime: process.uptime(),
|
||||
memory: process.memoryUsage(),
|
||||
interval: this.config.readIntervalMs
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private waitForConnection(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.on('connect', () => {
|
||||
console.log('Connected to MQTT broker');
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.client.on('error', reject);
|
||||
|
||||
setTimeout(() => reject(new Error('Connection timeout')), 30000);
|
||||
});
|
||||
}
|
||||
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.running = false;
|
||||
this.client.publish(
|
||||
`${this.baseTopic}/status`,
|
||||
'offline',
|
||||
{ retain: true }
|
||||
);
|
||||
this.client.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Verwendung
|
||||
const node = new SensorNode({
|
||||
brokerUrl: process.env.MQTT_URL || 'mqtt://localhost:1883',
|
||||
deviceId: process.env.DEVICE_ID || `rpi-${os.hostname()}`,
|
||||
readIntervalMs: 30000
|
||||
});
|
||||
|
||||
node.start();
|
||||
|
||||
// Graceful Shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log('Shutting down...');
|
||||
node.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Web Dashboard
|
||||
|
||||
```typescript
|
||||
// apps/dashboard/server.ts
|
||||
import express from 'express';
|
||||
import { createServer } from 'http';
|
||||
import { Server as SocketServer } from 'socket.io';
|
||||
import mqtt from 'mqtt';
|
||||
|
||||
const app = express();
|
||||
const server = createServer(app);
|
||||
const io = new SocketServer(server);
|
||||
|
||||
// MQTT Client
|
||||
const mqttClient = mqtt.connect(process.env.MQTT_URL || 'mqtt://localhost:1883');
|
||||
|
||||
// Sensor Data speichern
|
||||
const sensorData: Map<string, any[]> = new Map();
|
||||
const MAX_HISTORY = 100;
|
||||
|
||||
mqttClient.on('connect', () => {
|
||||
console.log('MQTT connected');
|
||||
mqttClient.subscribe('sensors/+/data');
|
||||
mqttClient.subscribe('sensors/+/status');
|
||||
});
|
||||
|
||||
mqttClient.on('message', (topic, payload) => {
|
||||
const parts = topic.split('/');
|
||||
const deviceId = parts[1];
|
||||
const type = parts[2];
|
||||
|
||||
if (type === 'data') {
|
||||
const data = JSON.parse(payload.toString());
|
||||
|
||||
// History speichern
|
||||
if (!sensorData.has(deviceId)) {
|
||||
sensorData.set(deviceId, []);
|
||||
}
|
||||
|
||||
const history = sensorData.get(deviceId)!;
|
||||
history.push(data);
|
||||
|
||||
if (history.length > MAX_HISTORY) {
|
||||
history.shift();
|
||||
}
|
||||
|
||||
// An WebSocket Clients senden
|
||||
io.emit('sensorData', { deviceId, data });
|
||||
}
|
||||
|
||||
if (type === 'status') {
|
||||
io.emit('deviceStatus', {
|
||||
deviceId,
|
||||
status: payload.toString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Static Files
|
||||
app.use(express.static('public'));
|
||||
|
||||
// API Endpoints
|
||||
app.get('/api/devices', (req, res) => {
|
||||
const devices = Array.from(sensorData.keys()).map(id => ({
|
||||
id,
|
||||
lastReading: sensorData.get(id)?.slice(-1)[0]
|
||||
}));
|
||||
|
||||
res.json(devices);
|
||||
});
|
||||
|
||||
app.get('/api/devices/:id/history', (req, res) => {
|
||||
const history = sensorData.get(req.params.id) || [];
|
||||
res.json(history);
|
||||
});
|
||||
|
||||
// WebSocket Events
|
||||
io.on('connection', (socket) => {
|
||||
console.log('Client connected');
|
||||
|
||||
// Initial Data senden
|
||||
sensorData.forEach((history, deviceId) => {
|
||||
socket.emit('initialData', {
|
||||
deviceId,
|
||||
history: history.slice(-20)
|
||||
});
|
||||
});
|
||||
|
||||
// Command an Gerät senden
|
||||
socket.on('command', ({ deviceId, action, payload }) => {
|
||||
mqttClient.publish(
|
||||
`sensors/${deviceId}/command`,
|
||||
JSON.stringify({ action, ...payload })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(3000, () => {
|
||||
console.log('Dashboard running on http://localhost:3000');
|
||||
});
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- public/index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Pi Sensor Dashboard</title>
|
||||
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 20px; background: #1a1a2e; color: #eee; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
|
||||
.card { background: #16213e; border-radius: 12px; padding: 20px; }
|
||||
.value { font-size: 48px; font-weight: bold; color: #00d9ff; }
|
||||
.label { color: #888; margin-top: 5px; }
|
||||
canvas { max-height: 200px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Pi Sensor Dashboard</h1>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="value" id="temperature">--</div>
|
||||
<div class="label">Temperature (°C)</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="value" id="humidity">--</div>
|
||||
<div class="label">Humidity (%)</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<canvas id="chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const socket = io();
|
||||
|
||||
const ctx = document.getElementById('chart').getContext('2d');
|
||||
const chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Temperature',
|
||||
data: [],
|
||||
borderColor: '#00d9ff',
|
||||
tension: 0.4
|
||||
}, {
|
||||
label: 'Humidity',
|
||||
data: [],
|
||||
borderColor: '#ff6b6b',
|
||||
tension: 0.4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: { y: { beginAtZero: false } }
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('sensorData', ({ deviceId, data }) => {
|
||||
document.getElementById('temperature').textContent = data.temperature?.toFixed(1) || '--';
|
||||
document.getElementById('humidity').textContent = data.humidity?.toFixed(1) || '--';
|
||||
|
||||
// Chart Update
|
||||
const time = new Date(data.timestamp).toLocaleTimeString();
|
||||
chart.data.labels.push(time);
|
||||
chart.data.datasets[0].data.push(data.temperature);
|
||||
chart.data.datasets[1].data.push(data.humidity);
|
||||
|
||||
if (chart.data.labels.length > 20) {
|
||||
chart.data.labels.shift();
|
||||
chart.data.datasets.forEach(ds => ds.data.shift());
|
||||
}
|
||||
|
||||
chart.update('none');
|
||||
});
|
||||
|
||||
socket.on('initialData', ({ deviceId, history }) => {
|
||||
history.forEach(data => {
|
||||
const time = new Date(data.timestamp).toLocaleTimeString();
|
||||
chart.data.labels.push(time);
|
||||
chart.data.datasets[0].data.push(data.temperature);
|
||||
chart.data.datasets[1].data.push(data.humidity);
|
||||
});
|
||||
chart.update();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Systemd Service
|
||||
|
||||
```ini
|
||||
# /etc/systemd/system/sensor-node.service
|
||||
[Unit]
|
||||
Description=Raspberry Pi Sensor Node
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
WorkingDirectory=/home/pi/sensor-node
|
||||
ExecStart=/usr/bin/node dist/apps/sensor-node.js
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
Environment=NODE_ENV=production
|
||||
Environment=MQTT_URL=mqtt://localhost:1883
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
```bash
|
||||
# Service aktivieren
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable sensor-node
|
||||
sudo systemctl start sensor-node
|
||||
|
||||
# Status prüfen
|
||||
sudo systemctl status sensor-node
|
||||
journalctl -u sensor-node -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fazit
|
||||
|
||||
Raspberry Pi mit Node.js bietet:
|
||||
|
||||
1. **Full Linux**: Komplettes OS mit npm-Ökosystem
|
||||
2. **GPIO Access**: Direkte Hardware-Steuerung
|
||||
3. **Networking**: WiFi, Ethernet, Bluetooth
|
||||
4. **Web Stack**: Express, Socket.io, React
|
||||
|
||||
Perfekt für IoT-Gateways und Edge Computing.
|
||||
|
||||
---
|
||||
|
||||
## Bildprompts
|
||||
|
||||
1. "Raspberry Pi with connected sensors and wires, IoT prototype"
|
||||
2. "Web dashboard showing sensor data graphs, real-time monitoring"
|
||||
3. "Smart home hub with Pi and multiple sensor nodes, IoT network"
|
||||
|
||||
---
|
||||
|
||||
## Quellen
|
||||
|
||||
- [Node.js on Raspberry Pi Guide](https://fleetstack.io/blog/nodejs-raspberry-pi-complete-guide)
|
||||
- [InfluxDB + Node-RED + Raspberry Pi](https://www.influxdata.com/blog/node-red-influxdb-raspberry-pi-iot-sensor-tutorial/)
|
||||
- [onoff GPIO Library](https://www.npmjs.com/package/onoff)
|
||||
- [SitePoint GPIO Tutorial](https://www.sitepoint.com/getting-started-with-the-raspberry-pi-gpio-pins-in-node-js/)
|
||||
Reference in New Issue
Block a user