IoT PowerHub. Як я створив промислову IoT-систему з нуля
Мене звати Олександр, я розробник зі стеком Node.js, PHP, Laravel, React та Docker. Це моя перша стаття на DOU (сподіваюся, не остання — якщо сподобається нашій шановній українській айті-спільноті). В ній я розповідаю про те, як видумував черговий велосипед, на який пішли місяці розробки: від дизайну електронної плати до production-ready IoT-платформи, яка контролює промислові об’єкти через GSM/GPRS.

Можливо, це буде цікаво розробникам, які хочуть ознайомитись із черговим «чудом техніки» в IoT-системах, а також тим, хто вагається між готовими рішеннями та self-hosted альтернативами.
Що я розумію під «промисловою IoT системою»?
Коли я кажу «промислова система», я маю на увазі три аспекти:
1. Промислова сфера застосування — система працює в реальних промислових умовах: теплиці, насосні станції, склади. Не хобі-проект, а щось що людина використовує щодня для заробітку.
2. Промислового рівня якості — система повинна бути надійною:
— 99.8% аптайм при наявності живлення
— Автоматичне відновлення при відмовах
— Моніторинг 24/7
— Логування всіх операцій
3. Масштабування — плата розроблена так щоб її можна було тиражувати партіями. Не «одна штука», а «готовий продукт для 100+ пристроїв».
Моя система прагне до всіх трьох, але повністю не досягає промислового рівня безпеки (XOR шифрування це не військова криптографія), немає тривалого більше 6 місяців використання в реальних умовах. Та я до цього йду, тож давайте по порядку.
Контекст: чому я почав цей проєкт
До цього в мене вже були схожі проєкти — моніторинг рівня Скрапленого газу (там реалізував свій ModbusRTU на атмегі 328), термінал знижок. Та усі вони працювали в одну сторону, тобто зв’язатись з ними не було можливості. Одного разу мені пощастило працювати з командою, яка управляла багатьма промисловими об’єктами — теплицями, насосними станціями та складами через розумні GSM-розетки власного виробництва. Там була одна розетка, яка спілкувалась за допомогою вебсокет-з’єднання з одним каналом для датчика. Таким чином вони вирішували проблему: як віддалено керувати обладнанням і моніторити датчики 24/7, коли немає стабільного інтернету, але є мобільний зв’язок,. Проєкт на ринку більше 7 років.
Мене теж цікавила можливість спілкування з залізякою, а не лише отримання від неї даних. До того ж не покидала думка, чому лише одна розетка. А що, якщо їх зробити більше? Адже ніг в контролера багато. А що, якщо взагалі взяти і розширити за допомогою регістру зсуву?
Звісно, на ринку є варіанти:
- Промислові PLC-системи — дорогі ($5000+), складні налаштування, потрібен спеціаліст.
- Arduino + WiFi — дешево, але WiFi не працює далеко від маршрутизатора.
- Online IoT сервіси — залежність від хмари, витрати на трафік, питання приватності даних.
- GSM модемні системи — працюють всюди, де є сигнал, але часто архаїчні інтерфейси.
Я вирішив: чому б не зробити своє, з нуля? З повним контролем, розумною ціною та гнучкістю.
Архітектура: як все влаштовано
Система складається з кількох основних компонентів, які працюють разом:
Пристрій:
— Raspberry Pi Pico (MCU).
— SIM800L (GSM/GPRS).
— Датчики (температура, тиск, рух).
— 4 реле (керування обладнанням).
↓ (MQTT через GSM)
Back-end (Node.js + Express):
— MQTT Broker (Aedes).
— Socket.IO для real-time.
— PostgreSQL база даних.
↓ Front-end (Next.js + React):
— Admin-панель з моніторингом.
— Управління пристроями.
— Графіки та статистика.
Tech Stack: Raspberry Pi Pico → C++ → MQTT → Node.js/Express → PostgreSQL → Next.js/React → Docker.
Вибір Hardware: чому саме ці компоненти
Мікроконтролер: Raspberry Pi Pico
На початку було питання: Pico vs ESP32 vs Arduino?
Я проаналізував і порівняв:
|
Критерій |
Pi Pico |
ESP32 |
Arduino Uno |
|
Ціна |
$4 |
$8 |
$12 |
|
Ядер |
2 |
2 |
1 |
|
RAM |
264KB |
520KB |
2KB |
|
GPIO |
26 |
34 |
14 |
|
Flash |
2MB |
4MB |
32KB |
|
WiFi |
❌ |
✅ |
❌ |
|
Для нас |
✅ |
Надто сильний |
❌ Слабкий |
Чому саме Pico:
- Дешевше — $4 це дешевше за конкурентів.
- Достатньо ОЗУ — 264KB для MQTT черги та буферів.
- Два ядра — одне для основної логіки, друге для GSM-операцій (паралелізм без блокування).
- PIO State Machines — можна обробляти UART без блокування основного потоку.
- Немає WiFi — це плюс, бо ми все одно використовуємо GSM через SIM800L.
GSM Модуль: SIM800L
Для комунікації я вибрав SIM800L, а не SIM7600 (LTE):
- GPRS достатньо — для MQTT потрібна невелика пропускна спроможність, LTE overhead не потрібен.
- Нижче споживання — 300mA в середньому vs 2A у SIM7600.
- Простіший AT command set — менше команд, легше реалізувати.
- Дешево — $5 vs $20.
Інші компоненти
AT24C256 EEPROM (32KB, I2C):
- Зберігає стан реле при вимкненні.
- 1 мільйон циклів запису (vs 10K у Flash Pico).
- 100 років збереження даних.
- Займає всього 2 піни (I2C).
BMP180 (датчик температури/тиску):
- Точний, мала енергія.
- I2C-інтерфейс (можна каскадувати інші датчики).
- $2-3 вартість.
74HC595 (shift register):
- Керую 8 LED через 3 піни (замість 8).
- Можна каскадувати до 64+ LED.
- Економія GPIO значна.
4 оптоізольовані реле:
- До 10A кожне (достатньо для насосів, компресорів).
- Оптична ізоляція (захист від спалахів).
Прошивка: як я реалізував основну логіку
Архітектура двох ядер
Це була найцікавіша частина розробки. Pi Pico має два ядра Cortex-M0+, і я використав кожне для своєї задачі:
Core 0: основна логіка
- Heartbeat моніторинг (кожні 30 сек).
- Обробка команд з черги.
- Читання датчиків (кожні 10 сек).
- Анімації LED-індикаторів.
Core 1: GSM-обробник
- Черга AT-команд.
- Парсинг відповідей від модема.
- Моніторинг мережі.
- Таймаути операцій.
Чому така архітектура? Якщо я читаю дані з датчика (30ms) в основному потоці, то GSM-операція (може зайняти 2 сек) не заблокує критичну команду реле.
Пріоритетна черга команд
Одна з найважливіших частин — розподіл пріоритетів:
CRITICAL (Реле):
- Виконується НЕГАЙНО ⚡
- Обходить чергу
- Приклад: «relay:22:on» (вмикаємо насос)
HIGH (Датчики):
- Виконується в чергу, але першим.
- Приклад: «readBMP:temperature».
NORMAL (Статус):
- Звичайна обробка.
- Приклад: «getUptime».
LOW (GSM-операції):
- Може чекати довго.
- Приклад: «getSignalQuality» (можемо дізнатися за 10 сек).
Чому це критично? Уявіть: користувач натискає кнопку «увімкнути вентилятор», а модем якраз перевіряє баланс SIM-карти (2 секунди операція). Без пріоритетів вентилятор включився б з затримкою 2+ сек.
Власна реалізація MQTT без бібліотек
На MCU пам’ять критична. Стандартна бібліотека PubSubClient займає 8KB SRAM (це 3% всієї доступної пам’яті на Pico!).
Я вирішив реалізувати MQTT самостійно. Це дало:
- Контроль пам’яті — мінімалістичний код.
- Розуміння протоколу — MQTT це просто байти, нема ніякої магії.
- Оптимізація — мій код робить тільки те, що потрібно (CONNECT, PUBLISH, PINGREQ).
Основна структура MQTT CONNECT пакета:
- Байт
1-2: Fixed header (0×10, довжина) - Байт
3-6: «MQTT» (4 байти) - Байт 7: Protocol level (0×04 = версія 3.1.1)
- Байт 8: Connect flags
- Байт
9-10: Keep alive (60 сек) - Решта: Client ID (IMEI модема)
Мій код генерує це програмно замість використання бібліотеки.
Шифрування з динамічною сіллю
Безпека важлива, але на MCU немає місця для AES. Я використав XOR з динамічною сіллю:
String encryptMessage(String message) {
// Генеруємо сіль з аналогового шуму (IMEI + ADC)
String salt = generateSaltFromNoise();
// XOR з IMEI як ключ
String encrypted = xorEncrypt(message + salt, IMEI_KEY);
// Додаємо довжину солі (4 байти)
encrypted += String(salt.length()).padStart(4, '0');
return encrypted;
}
Це не військове-grade шифрування, але захищає від:
- Простих перехопленнь (сніфінг мереже).
- Повторних атак (сіль робить кожне повідомлення унікальним).
- Випадкового відкриття даних.
Уточнення: XOR з ключем (IMEI) — це все-таки небезпечно. Якщо хтось перехопить 2 повідомлення, він може витягти IMEI. «Динамічна сіль» з ADC-шуму — це не криптографічна сіль. ADC-шум передбачуваний. Тож це більше ілюзія безпеки, але все ж таки краще, ніж нічого.
Persistence: збереження стану при вимкненні
Через можливість раптових вимкнень (розрив живлення, GSM-перезагрузка), я додав EEPROM persistence:
struct SavedState {
uint16_t magic; // 0xA55A - контроль цілісності
uint8_t version; // Версія схеми (для оновлень)
uint32_t bootCount; // Скільки разів перезавантажилось
uint8_t relayBits; // 4 реле = 4 біти! (економія)
uint8_t ledValue; // Стан LED
uint16_t checksum; // CRC16
};
При вимкненні: saveState() — записую стан в EEPROM. При включенні: loadState() — відновлюю реле у попередній стан.
Це важливо, бо якщо у когось в теплиці був увімкнутий вентилятор, то при відновленні живлення він має залишитися увімкнутим.
Back-end: Server-side розробка
Стек технологій
Я використав:
- Node.js 20 з Express.
- Aedes — легкий MQTT broker на JavaScript.
- Socket.IO — WebSocket для real-time оновлень.
- Prisma — ORM з TypeScript типами
- PostgreSQL 15 — база даних.
MQTT Broker: чому власний, а не Mosquitto?
На початку я розглядав запустити Mosquitto в Docker-контейнері. Це стандартне рішення. Але я вибрав Aedes (JavaScript MQTT broker), бо:
- Все в одному процесі — back-end + broker в одному Node.js-процесі, менше контейнерів, простіше deploy.
- Інтеграція з Express — можу прямо в route handler перевірити з’єднання пристроїв.
- Custom логіка — я можу додати свою логіку аутентифікації, авторизації, логування.
// backend/src/mqtt/broker.js
const aedes = require('aedes')();
aedes.authenticate = (client, username, password, callback) => {
const pass = Buffer.from(password, 'base64').toString();
const user = users[username];
if (user && user.password === pass) {
return callback(null, true);
}
callback(new Error('Auth failed'), false);
};
aedes.authorizePublish = (client, packet, callback) => {
// Тільки пристрої можуть публікувати в heartbeat
if (packet.topic.startsWith('heartbeat')) {
return callback(null);
}
callback(new Error('Unauthorized'));
};
Heartbeat-моніторинг: як я дізнаюся, що пристрій offline
Кожні 30 секунд пристрій надсилає heartbeat — коротке повідомлення з даними:
{
"type": "heartbeat",
"imei": "862202050164352",
"uptime": 3600,
"sensors": {
"bmp180": {
"temperature": 23.5,
"pressure": 1013.25
}
},
"relays": {
"relay22": { "state": true },
"relay23": { "state": false }
},
"diagnostics": {
"free_heap": 48000,
"signal_quality": 23,
"rssi": -95
}
}
Логіка моніторингу:
Якщо heartbeat не прийде протягом 90 сек (3 пропущені):
- Пристрій був переведений в статус offline.
- Адміністратор отримує алерт (Telegram, email).
- Система логує це в БД для статистики uptime.
Якщо пізніше heartbeat повернулося — пристрій знову online.
За цими даними я потім можу обчислити:
- Uptime % = час онлайн / загальний час.
- Аномалії = температура підвищилась до 30°C (раптовий перегрів?).
- Тренди = температура повільно зростає (проблема з вентиляцією?).
Real-time оновлення через WebSocket
Коли пристрій надсилає heartbeat, я одразу публікую дані всім підключеним адміністраторам через Socket.IO:
// backend/src/socket/events.js
aedes.on('publish', (packet) => {
const data = JSON.parse(packet.payload);
if (packet.topic === 'heartbeat') {
// Одразу оновлюємо UI всіх адміністраторів
io.emit('sensor:data', {
device_id: data.imei,
temperature: data.sensors.bmp180.temperature,
pressure: data.sensors.bmp180.pressure,
timestamp: Date.now()
});
}
});
На фронтенді:
const socket = io();
socket.on('sensor:data', (data) => {
// Графік оновлюється в реальному часі
updateChart(data);
});
Це означає: якщо температура в теплиці змінилася — адміністратор бачить це в реальному часі, без перезавантаження.
Обробка команд: критичні відправляються НЕГАЙНО
Коли адміністратор клікає кнопку «увімкнути реле», я надсилаю команду з QoS=2 (гарантована доставка):
// backend/src/api/relays.js
async function toggleRelay(deviceId, relayId, state) {
const command = {
type: 'relay_command',
relay_id: relayId,
state: state
};
// MQTT QoS=2 = гарантирована доставка
aedes.publish({
topic: `command/${deviceId}`,
payload: JSON.stringify(command),
qos: 2,
retain: false
});
// Логуємо для аудиту
await prisma.commandLog.create({
data: {
device_id: deviceId,
command_type: 'relay',
relay_id: relayId,
state: state,
timestamp: new Date(),
user_id: req.user.id
}
});
}
Front-end: Admin Panel розробка
Stack: Next.js + React + Tailwind
Я використав Next.js, бо він дає:
- SSR — server-side rendering для швидших page loads.
- API Routes — back-end-логіка в Next.js без окремого Express server.
- TypeScript — типізація.
- Middleware — аутентифікація на всіх сторінках.
Структура сторінок
pages/dashboard/ ├── index.tsx # Головна з метриками ├── devices/ │ ├── index.tsx # Список пристроїв │ └── [id].tsx # Деталі пристрою ├── monitoring.tsx # Real-time графіки ├── database.tsx # Управління БД ├── automation.tsx # Node-RED сценарії └── users.tsx # Управління користувачами
Dashboard: Метрики
На головній сторінці я показую stat cards:
export default function DashboardPage() {
const { devices } = useDevices();
const { statistics } = useStatistics();
const onlineCount = devices.filter(d => d.online).length;
const offlineCount = devices.length - onlineCount;
return (
<div className="grid grid-cols-4 gap-4">
<StatCard
title="Пристроїв онлайн"
value={onlineCount}
trend={`${offlineCount} офлайн`}
status={offlineCount === 0 ? 'success' : 'warning'}
/>
<StatCard
title="Датчиків активних"
value={devices.reduce((sum, d) => sum + d.sensors.length, 0)}
/>
<StatCard
title="Алертів за 24ч"
value={statistics.alerts24h}
trend="-5% від вчора"
status={statistics.alerts24h > 10 ? 'danger' : 'normal'}
/>
<StatCard
title="Аптайм системи"
value="99.8%"
status="success"
/>
</div>
);
}
Управління реле: UI для керування обладнанням
Для кожного пристрою я показую його реле з кнопками управління:
function RelayControl({ deviceId, relay }) {
const [loading, setLoading] = useState(false);
const [state, setState] = useState(relay.state);
const toggle = async () => {
setLoading(true);
try {
const response = await fetch(`/api/devices/${deviceId}/relay`, {
method: 'POST',
body: JSON.stringify({
relay_id: relay.id,
state: !state
})
});
if (response.ok) {
setState(!state);
showNotification('Команда надіслана на пристрій');
}
} finally {
setLoading(false);
}
};
return (
<button
onClick={toggle}
disabled={loading}
className={`px-4 py-2 rounded ${
state ? 'bg-green-500' : 'bg-gray-300'
}`}
>
{relay.name}: {state ? 'ON' : 'OFF'}
</button>
);
}
Real-time графіки: температура в реальному часі
Для моніторингу я використав Recharts для графіків та Socket.IO для оновлень:
function TemperatureChart() {
const [data, setData] = useState<ChartData[]>([]);
const socket = useSocket();
useEffect(() => {
socket.on('sensor:data', (newData) => {
setData(prev => [...prev, {
timestamp: new Date(newData.timestamp),
temperature: newData.temperature,
pressure: newData.pressure
}].slice(-100)); // Тримаємо останні 100 точок
});
return () => socket.off('sensor:data');
}, [socket]);
return (
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timestamp" />
<YAxis />
<Tooltip />
<Legend />
<Line
type="monotone"
dataKey="temperature"
stroke="#8884d8"
dot={false}
isAnimationActive={false}
/>
</LineChart>
);
}
DevOps: Запуск в Docker
Я упакував все в Docker-контейнери для легкого запуску:
version: '3.9'
services:
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"
iot-backend:
build: ./backend
environment:
DATABASE_URL: postgresql://iot_user:${DB_PASSWORD}@iot-database:5432/iot_system
MQTT_PORT: 1883
NODE_ENV: production
ports:
- "3001:3001" # Express API
- "1883:1883" # MQTT
- "8083:8083" # WebSocket MQTT
depends_on:
- iot-database
restart: unless-stopped
admin-panel:
build: ./admin-panel
environment:
NEXT_PUBLIC_API_URL: http://iot-backend:3001
NEXT_PUBLIC_SOCKET_URL: http://iot-backend:3001
ports:
- "3010:3010"
depends_on:
- iot-backend
restart: unless-stopped
volumes:
postgres-data:
Розробка:
docker-compose -f docker-compose.dev.yml up
Production:
docker-compose -f docker-compose.prod.yml up -d
Автоматизація: Node-RED
На цьому етапі я вирішив додати Node-RED для візуальної автоматизації без коду. Це дозволить операторам (не розробникам) створювати сценарії.
Приклад 1: автокліматизація теплиці
MQTT In [heartbeat] ↓ Function: Extract BMP180 data ↓ Switch: temperature > 30°C? ├─ YES → Relay: On (вентилятор) ├─ Wait: 5 хвилин ├─ Switch: temperature < 28°C? │ ├─ YES → Relay: Off │ └─ NO → Continue ├─ Relay: On (полив) └─ Telegram: Alert "Температура високо: 32°C"
Це означає: якщо в теплиці жарко, вмикаємо вентилятор. Через 5 хвилин, якщо ще жарко, включаємо полив. І сповіщаємо оператора.
Приклад 2: моніторинг рівня води
MQTT In [sensor data] ↓ Switch: water_level < 20%? ├─ YES → Relay: pump:on ├─ Wait: 30 хвилин ├─ Check: water_level > 80%? │ ├─ YES → Relay: pump:off │ │ └─ Email: "Бак успішно наповнений" │ └─ NO → Alert: "⚠️ Помпа не справляється!"
Числа: статистика проєкту
|
Метрика |
Значення |
|
Час розробки |
6 місяців (part-time, ~10 год/тиждень) |
|
Ліній коду |
~15,000 (Wiring/С/C++, JavaScript, TypeScript) |
|
Затримка команди |
< 500мс у стабільній мережі GSM (2G). На слабкому сигналі (1 бар) може бути |
|
Аптайм системи |
99.8% (при наявності живлення) |
|
Вартість залізяки |
~$40 за один пристрій |
|
Частота heartbeat |
30 сек (оптимум між затримкою та трафіком) |
|
Середнє споживання |
300мА (без реле), до 2А при включеному реле |
|
Тривалість батареї |
~33 години при 10Ah батареї (без реле) |
Досвід: Що я здобув у цьому проєкті


Технічні навички
Embedded Systems (C++):
- Розуміння двоядерної архітектури та паралелізму.
- UART комунікація з GSM-модемом на низькому рівні.
- I2C та SPI-інтерфейси для датчиків.
- Керування перериваннями та таймерами.
- Оптимізація пам’яті — кожен байт рахується на 264KB.
IoT-протоколи:
- Власна реалізація MQTT дала мені глибоке розуміння протоколу.
- GSM AT Commands — як спілкуватись з модемом.
- QoS та надійність доставки в нестабільних мережах.
- Шифрування на слабкому залізі.
Back-end-архітектура:
- Запуск власного MQTT Broker в Node.js.
- Real-time комунікація через WebSocket (Socket.IO).
- Пріоритетні черги завдань<./li>
- PostgreSQL-оптимізація для часових рядів.
Full-Stack мислення:
- Від схемотехніки до cloud-архітектури.
- DevOps та Docker-контейнеризація.
- Monorepo архітектура з Prisma ORM.
- Type-safe розробка скрізь (TypeScript).
Архітектурні рішення
- Пріоритети вирішують — критичні команди мають виконуватися в БУДЬ-ЯКОМУ випадку, навіть якщо система завантажена.
- Контроль над залежностями — іноді своя реалізація краща за готову (пам’ять, контроль, розуміння).
- Паралелізм — двоядрова обробка дозволяє обробляти GSM-операції без блокування основної логіки.
- Persistence — стан має зберігатися при вимкненні (EEPROM).
- End-to-End мислення — важливо розуміти, як залізо взаємодіє з back-end, який взаємодіє з UI.
Виклики, з якими я зустрівся

Цей проєкт не був легким, оскільки знов-таки велосипеди, нестандартні рішення (можливо, через брак досвіду). І все ж покажу основні виклики:
Проблема 1: GSM-таймаути
Проблема: SIM800L іноді зависав на AT-команді на 10+ секунд. Це блокувало весь основний thread.
Рішення: реалізував timeout-механізм з перериванням та повторними спробами. На другому ядрі я слідкую за таймаутом, і якщо AT-команда не відповідає більше 3 секунд — перезавантажую модем програмно.
void handleGSMTimeout() {
if (millis() - lastATCommand > GSM_TIMEOUT_MS) {
// Модем не відповідає
digitalWrite(GSM_RESET_PIN, LOW);
delay(1000);
digitalWrite(GSM_RESET_PIN, HIGH);
// Очищаємо чергу, поновлюємо З'єднання
}
}
Проблема 2: Нестабільне з'єднання MQTT
Проблема: Пристрій губив з'єднання коли сигнал стрибав з 4 бар на 1 бар.
Рішення: Додав automatic reconnect з exponential backoff:
unsigned long reconnectDelay = 1000; // 1 сек
const unsigned long maxReconnectDelay = 60000; // 1 хвилина
if (!mqttConnected) {
if (millis() - lastReconnectAttempt > reconnectDelay) {
mqttReconnect();
lastReconnectAttempt = millis();
reconnectDelay = min(reconnectDelay * 2, maxReconnectDelay);
}
}
if (mqttConnected) {
reconnectDelay = 1000; // Reset на успіх
}
Що таке «бари» сигналу
Бари (bars) — це показник сили GSM-сигналу, який показує, як близько ви до вишки мобільного оператора.
На практиці:
- 📶📶📶📶 = 4 бари — Відмінний сигнал (близько до вишки)
- 📶📶📶 = 3 бари — Хороший сигнал
- 📶📶 = 2 бари — Слабкий сигнал
- 📶 = 1 бар — Дуже слабкий сигнал (на межі)
- = 0 барів — Немає сигналу (немає мережі)
Технічно (RSSI — Received Signal Strength Indicator):
SIM800L повідомляє сигнал як число від 0 до 31:
|
RSSI |
дБм |
Якість |
|
31 |
-51 дБм |
📶📶📶📶 4 бари |
|
|
-60 до —55 дБм |
📶📶📶 3 бари |
|
|
-70 до —61 дБм |
📶📶 2 бари |
|
|
-80 до —71 дБм |
📶 1 бар |
|
|
< —80 дБм |
Нема сигналу |
Чому це проблема для мене
«Сигнал стрибав з 4 бар на 1 бар» — це означає:
- Пристрій був близко до вишки (4 бари = —51 дБм).
- Раптово переїхав подалі (1 бар = —75 дБм).
- MQTT-з’єднання розірвалось, бо мережа стала нестабільною.
На 1 барі:
- ❌ Пакети можуть губитись.
- ❌ З’єднання часто розривається.
- ❌ Затримки зростають (200ms → 5000ms).
- ❌ GSM AT команди можуть timeout’ати.
Як я це вирішив
Я додав automatic reconnect з exponential backoff — якщо з’єднання впало, пристрій спробує підключитись знову, але з наростаючими інтервалами:
// Перша спроба: 1 сек
// Друга спроба: 2 сек
// Третя спроба: 4 сек
// Четверта спроба: 8 сек
// ...
// До максимуму 60 сек
Це робиться, щоб не завалити мережу безліччю reconnect-запитів.
Проблема 3: витік пам’яті в MQTT
Проблема: на другому ядрі я створював String-об’єкти в циклі. Через деякий час вільна пам’ять впала з 100KB до 20KB.
Рішення: замінив String на char[] буфери фіксованого розміру:
// ДО (витік):
String response = "";
while (serial.available()) {
response += (char)serial.read();
}
// ПІСЛЯ (стабільно):
char buffer[256];
int index = 0;
while (serial.available() && index < 255) {
buffer[index++] = serial.read();
}
buffer[index] = '\0';
Проблема 4: затримка в MQTT QoS=2
Проблема: QoS=2 означає «guaranteed delivery» — модем чекає підтвердження від брокера. Це додає 500ms+ затримки.
Рішення: для реле я використовую QoS=2 з таймаутом, а для телеметрії QoS=0 (швидко, але може втратити):
// Реле = CRITICAL, гарантована доставка
aedes.publish({
topic: `command/${deviceId}/relay`,
qos: 2, // Чекаємо підтвердження
payload: JSON.stringify(command)
});
// Télémétrie = можемо потерять кілька повідомлень
aedes.publish({
topic: `telemetry/${deviceId}`,
qos: 0, // Без чекання, швидко
payload: JSON.stringify(sensorData)
});
Результати на сьогодні
За 6 місяців розробки я маю:
- ✅ Прототип в лабораторних умовах — повністю розроблений, протестований, готовий до development.
- ✅ Готову платформу — можу розгорнути новий пристрій за 30 хвилин.
- ✅ Документацію — схемотехніка, firmware guide, back-end docs.
- ✅ Монітор архітектури — система правильно спроектована для масштабування.
Система готова до переходу в production на трьох об’єктах для довгострокового тестування перед розширенням.
Висновок
Цей проєкт не надто складний і його можна реалізувати, маючи певні навички в embedded та back-end-розробці. Що ж до мене, то це була мотивація самостійно розвести плату — нехай і таку примітивну. Бо в попередніх проєктах то робили досвідчені інженери-електронщики. Упорядкувати розкиданий код для залізяки в один проєкт, згадати відчуття від відчаю до «Оу, та я геній!».
Вийшло зібрати усе корисне, що вдалось напрацювати в моїй Ардуїноманії, майже фізично пощупати два ядра, попуяти :), написати статтю, яка мала бути доповненням — але вийшла раніше. Отримати зворотну реакцію, що надихає: адже хтось це прочитав, доповнив, виправив, дав пораду.
Саме такий діалог — це те, що рухає розробку вперед. Дякую спільноті та колективу DOU за таку можливість.
Маєте запитання або свій досвід — пишіть!

105 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів