Case Study: Як я створив професійну IoT платформу – від проектування плати до production-ready backend та web-інтерфейсу
Що створено:
- ✅ Власна електронна плата (схема + PCB дизайн)
- ✅ Firmware з dual-core обробкою та пріоритетною чергою команд
- ✅ Backend з MQTT брокером та real-time обробкою
- ✅ Next.js admin-панель з моніторингом
- ✅ Production-ready Docker інфраструктура
- ✅ Node-RED для візуальної автоматизації
Tech Stack: Raspberry Pi Pico → C++ → MQTT → Node.js/Express → PostgreSQL → Next.js/React → Docker
Результат: Повнофункціональна IoT платформа для керування промисловими об’єктами через GSM/GPRS з реальним часовим моніторингом
🎯 Проблема і рішення
Контекст
Промислові об’єкти (теплиці, насосні станції, склади) мають специфічні потреби:
- 🔌 Керування потужним обладнанням без прямого доступу
- 📊 Моніторинг датчиків 24/7 з аналітикою
- 🚨 Система алертів при критичних ситуаціях
- 📱 Доступ через інтернет навіть при відсутності WiFi
- 💾 Історія даних для аналізу тенденцій
- 🤖 Автоматизація процесів без програмування
Чому не готові рішення?
| Рішення | Проблема |
|---|---|
| Промислові PLC | $$$$ дорого, складно налаштовувати |
| Arduino + WiFi | Нестійкий зв’язок, не працює далеко від маршрутизатора |
| Online сервіси (IoT.io) | Залежність від хмари, витрати на трафік |
| Готові GSM системи | Не гнучкі, дорогі абонентські послуги |
Моє рішення
IoT PowerHub – DIY промислова IoT система з повним контролем, розумною ціною та гнучкістю.
🔧 Hardware: Від ідеї до плати
Проектування
Я спроектував плату в EasyEDA з наступними вимогами.
📸 PCB Макет
Ось як виглядає плата перед друком:

Вид зверху (компоненти):
- Велика розетка для Pi Pico посередині
- SIM800L модуль справа
- 4 реле по краях
- 74HC595 shift register ліворуч
- Конденсатори та резистори для стабілізації
- Клеммники для підключення датчиків/обладнання
Вид знизу (розводка):

- Доріжки для живлення (товстіші)
- Сигнальні доріжки для даних (тонші)
- Через-отвори (vias) для зв’язки шарів
- Контакти для припайки компонентів
Компоненти
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
┌─────────────────────────────────────┐ │ Raspberry Pi Pico (RP2040) │ │ - Dual-core (процесор + GSM) │ │ - 264KB RAM │ │ - 16 GPIO + 3 SPI/I2C интерфейса │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ SIM800L GSM/GPRS модуль │ │ - Quad-band (підтримує все) │ │ - 300mA при пік навантаженні │ │ - AT commands інтерфейс │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ Периферія: │ │ - 74HC595 shift register (8 LED) │ │ - AT24C256 EEPROM (32KB, I2C) │ │ - BMP180 датчик (темп/тиск) │ │ - 4x реле (10A 5V) │ │ - PIR датчик руху │ │ - Стабілізатори 5V/3.3V │ └─────────────────────────────────────┘ |
Архітектура плати
I2C Bus (GPIO 4-5):
- BMP180 (адреса 0x77)
- AT24C256 (адреса 0x50)
UART (GPIO 0-1):
- SIM800L (9600 baud, пряма комунікація)
SPI-like (GPIO 19-21):
- 74HC595 (Data, Clock, Latch) для керування LED
Digital GPIO (GPIO 22-29):
- 4x Relay управління
- PIR датчик
Power Rails:
- 5V: Реле + SIM800L (до 2A burst)
- 3.3V: Pico + EEPROM + датчики (стабілізовано)
Чому саме ці компоненти?
Pi Pico vs альтернативи:
|
1 2 3 4 5 6 7 8 9 |
┌──────────┬──────────┬─────────────┐ │ Критерій │ Pico │ ESP32 │ ├──────────┼──────────┼─────────────┤ │ Ціна │ $4 │ $8 │ │ Ядер │ 2 │ 2 │ │ RAM │ 264KB │ 520KB │ │ GPIO │ 26 │ 34 │ └──────────┴──────────┴─────────────┘ |
Вибір Pico:
- ✅ Дешевше ($4 vs $8)
- ✅ Достатньо ОЗУ для MQTT + черги команд
- ✅ Стабільніший GSM на окремому ядрі
- ✅ PIO state machines для UART без блокування
SIM800L vs SIM7600:
- ✅ GPRS достатньо (LTE overhead не потрібен)
- ✅ Нижче споживання (300mA vs 2A)
- ✅ Простіший AT command set
- ✅ $5 vs $20
💻 Firmware: Embedded-розробка
Архітектура прошивки
Вся система побудована на дво-ядерній архітектурі:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
┌──────────────────────────────────────┐ │ Core 0: Основна логіка │ ├──────────────────────────────────────┤ │ - Heartbeat моніторинг (30с) │ │ - Обробка команд з черги │ │ - Читання датчиків (10с) │ │ - Анімації LED │ └──────────────────────────────────────┘ ┌──────────────────────────────────────┐ │ Core 1: GSM обробник │ ├──────────────────────────────────────┤ │ - AT command очередь │ │ - Парсинг відповідей │ │ - Моніторинг мережі │ │ - Таймаути операцій │ └──────────────────────────────────────┘ |
Ключові компоненти
1️⃣ Пріоритетна черга команд
|
1 2 3 4 5 6 7 8 |
enum Priority { CRITICAL = 0, HIGH = 1, NORMAL = 2, LOW = 3 }; struct Command { String data; Priority priority; unsigned long timestamp; }; |
Логіка розподілу:
- CRITICAL (реле): Виконуються НЕГАЙНО, обходять чергу
- HIGH (датчики): Виконуються перед LOW
- NORMAL (статус): Звичайна обробка
- LOW (GSM інформація): Можуть чекати
Приклад:
|
1 2 3 4 |
Команда 1: "relay:on" (CRITICAL) → виконуюється НЕГАЙНО ⚡ Команда 2: "readBMP" (HIGH) → в чергу, виконається наступною Команда 3: "getSignal" (LOW) → останній пріоритет |
2️⃣ Власна реалізація MQTT без бібліотек
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void mqttConnect() { // Конструюємо CONNECT пакет (10 байт мінімум) String packet = "\x10"; // Fixed header packet += char(remainingLength); // Довжина packet += "\x00\x04MQTT"; // Protocol name packet += "\x04"; // Protocol level (3.1.1) packet += "\x02"; // Connect flags (clean session) packet += "\x00\x3C"; // Keep alive (60s) packet += clientId; // Client ID (IMEI) sendToServer(packet); } |
Чому БЕЗ бібліотеки?
- ✅ Контроль пам’яті (264KB – кожен байт в рахунку)
- ✅ PubSubClient займає 8KB SRAM (3% всієї ОЗУ!)
- ✅ Глибше розуміння протоколу
- ✅ Менше залежностей = менше багів
3️⃣ Шифрування з динамічною сіллю
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
String encryptMessage(String message) { // Генеруємо сіль з аналогового шуму String salt = generateSaltFromNoise(analogRead(A0)); // XOR шифрування з IMEI як ключ String encrypted = xorEncrypt(message + salt, IMEI_KEY); // Додаємо довжину солі (4 байти) encrypted += String(salt.length()).padStart(4, '0'); return encrypted; } |
Чому XOR + сіль?
- ✅ Швидкий на MCU без AES-модуля
- ✅ Динамічна сіль робить кожне повідомлення унікальним
- ✅ Запобігає атакам на розповсюджені паттерни
- ✅ 256 комбінацій зробити перебір важко
4️⃣ Persistence: EEPROM для збереження стану
|
1 2 3 4 5 6 7 8 |
struct SavedState { uint16_t magic; // 0xA55A - контрольна сума uint8_t version; // Версія схеми uint32_t bootCount; // Статистика перезавантажень uint8_t relayBits; // 4 реле = 4 біти! (économия) uint16_t checksum; // CRC16 для перевірки }; |
Результат:
- При вимкненні: зберігаємо стан реле
- При включенні: відновлюємо попередній стан
- Вісім років історія перезавантажень (за счет uint32_t)
🖥️ Backend: Server-side архітектура
Стек
|
1 2 3 4 5 6 7 8 9 |
┌─────────────────────────────────────┐ │ Node.js 20 + Express.js │ ├─────────────────────────────────────┤ │ - Aedes MQTT Broker │ │ - Socket.IO для WebSocket real-time │ │ - Prisma ORM з TypeScript │ │ - PostgreSQL 15 база даних │ └─────────────────────────────────────┘ |
Архітектура обробки даних
|
1 2 3 4 5 6 7 8 9 10 11 12 |
IoT Пристрій (Pi Pico) ↓ MQTT (коннект з SIM800L) Aedes MQTT Broker (1883 port) ↓ authenticate + authorize Command Processor ├─ Database saving (Prisma) ├─ Real-time broadcast (Socket.IO) ├─ Alert system (Telegram) └─ Logging ↓ PostgreSQL (storia + analytics) |
Heartbeat Моніторинг
Кожні 30 секунд пристрій відправляє heartbeat:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
{ "type": "heartbeat", "imei": "862202050164352", "uptime": 3600, "sensors": { "bmp180": { "temperature": 23.5, "pressure": 1013.25 } }, "relays": { "22": { "state": true }, "23": { "state": false } }, "diagnostics": { "free_heap": 48000, "signal_quality": 23 } } |
Backend логіка:
- Якщо heartbeat не прийде протягом 90 сек (3 пропущені) → пристрій offline
- Зберігаємо в БД для статистики uptime
- Фіксуємо аномалії (температура скочила до 30°C)
- Запускаємо автоматизацію (Node-RED)
Пріоритетне виконання команд
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// backend/src/services/commandQueue.js async executeCommand(deviceId, command, priority) { // CRITICAL: Реле виконуються негайно if (command.type === 'relay') { return publishMQTT(deviceId, command, { qos: 2 }); } // HIGH: Датчики отримують черга if (command.type === 'sensor') { return queue.add(command, { priority: 1 }); } // LOW: GSM операції можуть чекати if (command.type === 'gsm_info') { return queue.add(command, { priority: 3 }); } } |
Real-time оновлення через WebSocket
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// backend/src/socket/deviceEvents.js aedes.on('client:connected', (client) => { io.emit('device:online', { imei: client.id, timestamp: new Date(), signalQuality: client.handshake?.signal }); }); aedes.on('publish', (packet) => { const data = JSON.parse(packet.payload); // Broadcast до всіх підключених админів io.emit('sensor:data', { device_id: data.imei, data: data.sensors, timestamp: Date.now() }); }); |
📱 Frontend: Admin Panel
Структура
|
1 2 3 4 5 6 7 8 9 |
next.js/pages/dashboard/ ├── / # Головна (stat cards) ├── /devices # Список пристроїв ├── /devices/[id] # Деталі пристрою ├── /monitoring # Real-time графіки ├── /database # Управління БД ├── /automation # Node-RED сценарії └── /users # Управління користувачами |
Ключові сторінки
📊 Dashboard
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
export default function DashboardPage() { const { devices } = useDevices(); const { statistics } = useStatistics(); return ( <div className="grid grid-cols-4"> <StatCard title="Пристроїв онлайн" value={devices.filter(d => d.online).length} trend="+2" /> <StatCard title="Датчиків активних" value={devices.reduce((sum, d) => sum + d.sensors.length, 0)} status="normal" /> <StatCard title="Алертів за 24ч" value={statistics.alerts24h} trend="-5%" status="warning" /> <StatCard title="Аптайм системи" value="99.8%" status="success" /> </div> ); } |
🔌 Керування пристроями
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// Вмикаємо реле через UI const toggleRelay = async (deviceId: string, relayId: number) => { const response = await fetch(`/api/devices/${deviceId}/relay`, { method: 'POST', body: JSON.stringify({ relay_id: relayId, state: true }) }); // Backend надсилає MQTT команду // Pico отримує та виконує НЕГАЙНО (CRITICAL пріоритет) // Socket.IO оновлює UI в реальному часі }; |
📈 Real-time моніторинг
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Grafana-like графіки з Socket.IO const socket = useSocket(); useEffect(() => { socket.on('sensor:data', (data) => { setChartData(prev => [...prev, { timestamp: Date.now(), temperature: data.sensors.bmp180.temperature, pressure: data.sensors.bmp180.pressure }]); }); }, []); |
🗄️ Управління БД
Tab-based інтерфейс:
- Огляд: Розмір БД, кількість записів
- Таблиці: Редагування даних
- Здоров’я: Індекси, вакуум
- Обслуговування: Оптимізація
- Бекапи: Резервні копії
🐳 DevOps: Production deployment
Docker Compose
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
version: '3.9' services: # Database iot-database: image: postgres:15 environment: POSTGRES_USER: iot_user POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: iot_system volumes: - postgres-data:/var/lib/postgresql/data ports: - "5432:5432" # Backend + MQTT iot-backend: build: ./backend environment: DATABASE_URL: postgresql://... MQTT_PORT: 1883 NODE_ENV: production ports: - "3001:3001" # Express - "1883:1883" # MQTT - "8083:8083" # WebSocket depends_on: - iot-database restart: unless-stopped # Admin Panel admin-panel: build: ./admin-panel environment: NEXT_PUBLIC_API_URL: http://backend:3001 ports: - "3010:3010" depends_on: - iot-backend restart: unless-stopped # Node-RED для автоматизації node-red: image: nodered/node-red:latest ports: - "1880:1880" volumes: - node-red-data:/data environment: TZ: Europe/Kiev depends_on: - iot-backend restart: unless-stopped volumes: postgres-data: node-red-data: |
Запуск
|
1 2 3 4 5 6 |
# Розробка docker-compose -f docker-compose.dev.yml up # Production docker-compose -f docker-compose.prod.yml up -d |
🤖 Автоматизація: Node-RED
Приклади сценаріїв
🌡️ Автокліматизація теплиці
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
MQTT In (topic: heartbeat) ↓ Function: Extract BMP180 data ↓ Switch: temperature > 30? ├─ YES → MQTT Out: relay:relay22:on (вентилятор) └─ NO → MQTT Out: relay:relay22:off ↓ Switch: temperature < 28? (гістеріз) ├─ YES → MQTT Out: relay:relay23:off └─ NO → MQTT Out: relay:relay23:on (полив) ↓ Telegram: Alert "Température высокая! 32°C" |
💧 Моніторинг рівня води
|
1 2 3 4 5 6 7 8 9 |
MQTT In (PIR sensor data) ↓ Switch: level < 20%? ├─ YES → Relay: pump:on ├─ Wait: 30 min ├─ Check: level > 80%? │ ├─ YES → Relay: pump:off │ └─ NO → Alert: "Помпа не справляється!" |
📧 EMAIL сповіщення
|
1 2 3 4 5 6 7 8 9 |
MQTT In ↓ Function: Check if alert-worthy ↓ Email: Send to admin@company.com ├─ Subject: "IoT Alert: Device offline" ├─ Body: JSON details └─ Attachment: CSV of last 24h data |
🎓 Досвід: Що здобув
Технічні навички Embedded Systems (C++)
- ✅ Архітектура двоядерної системи (RP2040)
- ✅ UART комунікація з GSM модулем
- ✅ I2C Bus для датчиків та пам’яті
- ✅ SPI для керування LED (74HC595)
- ✅ Роботе з перериваннями та таймерами
- ✅ Оптимізація пам’яті (264KB)
IoT Протоколи
- ✅ Власна реалізація MQTT без бібліотек
- ✅ GSM AT Commands (SIM800L)
- ✅ QoS та надійність доставки
- ✅ Шифрування на маломовного контролері
Backend Architecture
- ✅ Власний MQTT Broker (Aedes)
- ✅ Real-time WebSocket комунікація (Socket.IO)
- ✅ Пріоритетні черги завдань
- ✅ PostgreSQL оптимізація
- ✅ Обробка великих об’ємів даних
Full-Stack розробка
- ✅ Від “залізяки” до cloud
- ✅ DevOps та Docker
- ✅ Monorepo архітектура з Prisma
- ✅ Type-safe розробка (TypeScript)
Архітектурні рішення
| Рішення | Причина |
|---|---|
| Власний MQTT | Контроль, оптимізація під Pico |
| Пріоритетна черга | Гарантія виконання критичних команд |
| Dual-core | Паралельна обробка GSM + логіки |
| EEPROM persistence | Збереження стану при вимкненні |
| XOR + сіль | Баланс безпеки та швидкості |
| Monorepo | Синхронізація типів скрізь |
📊 Результати
Числа
| Метрика | Значення |
|---|---|
| Час розробки | 6 місяців |
| Кількість ліній коду | ~15,000 (C++, JS, TS) |
| Затримка команди | < 500мс в мережі 2G |
| Аптайм | 99.8% (при наявності живлення) |
| Вартість заліза | ~$40 за один пристрій |
| Частота heartbeat | 30 сек (оптимальний баланс) |
Що покриває рішення
- ✅ Моніторинг температури/тиску 24/7
- ✅ Керування 4 електричними пристроями
- ✅ Датчик руху (PIR)
- ✅ SMS алерти через SIM800L
- ✅ Візуальні індикатори (8 LED)
- ✅ Графіки та аналітика за період
- ✅ Автоматизовані сценарії без коду
- ✅ Історія дій для аудиту
🚀 Висновок
Цей проект демонструє повний цикл розробки IoT системи – від концепції та схемотехніки до production-ready платформи.
Це не простий проект для новачків, але це може реалізувати розробник який має досвід у embedded та backend розробці.
Якщо у вас є запитання про IoT архітектуру, MQTT, GSM модеми або Pi Pico — із задоволенням відповім у коментарях!
І якщо ви роботодавець, який шукає розробника, здатного писати код від залізяки до cloud — радо розгляну пропозицію! 👉
Дякую за увагу! Цей проект навчив мене більше, ніж усі курси разом.
Якщо стаття була корисною — поділіться з колегами у коментарях чи в соцмережах. Маєте запитання або свій досвід — пишіть! 💬
#IoT #RaspberryPi #Embedded #FullStack #MQTT #ProductDesign #DevOps