Розробка мобільного додатка для промислового IoT (IIoT)
Промислений IoT відрізняється від потребительського трьома речами: надійність важливіше зручності, дані йдуть потоком 24/7, а ціна ошибки — зупинка виробництва. Мобільний додаток для IIoT — це не «включи світло», а інструмент оператора, який читає показання з ПЛК Siemens S7-1500 через OPC UA, слідить за вібрацією підшипників по даним IO-Link, отримує алерти коли температура пресс-форми вийшла за 195°C. Підхід до розробки відповідний.
Протоколи та шар комунікацій
Перший питання на брифінгу — по яких протоколах говорять пристрої. Найчастіші варіанти в промисловості:
| Протокол | Транспорт | Типове застосування |
|---|---|---|
| OPC UA | TCP, WebSocket | ПЛК, SCADA, станки з ЧПУ |
| MQTT | TCP/TLS | Датчики, шлюзи IoT |
| Modbus TCP | TCP | Старі ПЛК, перетворювачі |
| PROFINET | Ethernet | Промислові мережі Siemens |
| IO-Link | RS-232/SIO | Датчики на рівні поля |
Мобільний додаток напряму з Modbus TCP або OPC UA говорити не повинен — це надто низький рівень. Правильна архітектура: Edge Gateway збирає дані з пристроїв, нормалізує в єдиний формат (зазвичай MQTT або REST), мобільний додаток працює з Gateway через захищений канал.
ПЛК / датчики
↓ OPC UA / Modbus TCP
Edge Gateway (на виробництві)
↓ MQTT TLS / HTTPS
MQTT Broker / Backend (хмара або локальний сервер)
↓ WebSocket / REST
Мобільний додаток
MQTT у реальному часі: Android
Для промислових додатків на Android використовуємо Eclipse Paho MQTT або MQTT BLE-розновидності. Ключовий параметр — QoS. Для телеметрії (температура раз в секунду) достатньо QoS 0. Для команд управління та критичних алертів — QoS 2 з exactly-once delivery:
class MqttService : Service() {
private lateinit var client: MqttAndroidClient
fun connect(brokerUrl: String, credentials: MqttCredentials) {
client = MqttAndroidClient(applicationContext, brokerUrl, clientId)
client.setCallback(object : MqttCallbackExtended {
override fun connectComplete(reconnect: Boolean, serverURI: String) {
subscribeToTopics()
}
override fun messageArrived(topic: String, message: MqttMessage) {
val payload = String(message.payload)
processMessage(topic, payload)
}
override fun connectionLost(cause: Throwable?) {
// Логуємо, triggering reconnect через ExponentialBackoff
scheduleReconnect(cause)
}
override fun deliveryComplete(token: IMqttDeliveryToken) {}
})
val options = MqttConnectOptions().apply {
userName = credentials.username
password = credentials.password.toCharArray()
isCleanSession = false // Зберігаємо підписки між переподключеннями
keepAliveInterval = 30
connectionTimeout = 10
isAutomaticReconnect = true
socketFactory = credentials.sslSocketFactory
}
client.connect(options)
}
}
isCleanSession = false критично для промислових додатків: якщо телефон втратив зв'язок, після переподключення брокер доставить всі QoS 1/2 повідомлення, що пропустили.
Зберігання телеметрії локально
Зберігаємо дані з пристроїв локально — виробництво в підвалі без інтернету, операторський обхід без зв'язку. Room з Flow:
@Entity(tableName = "telemetry")
data class TelemetryRecord(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val deviceId: String,
val parameter: String,
val value: Double,
val unit: String,
val timestamp: Long,
val synced: Boolean = false
)
@Dao
interface TelemetryDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(record: TelemetryRecord)
@Query("SELECT * FROM telemetry WHERE deviceId = :deviceId ORDER BY timestamp DESC LIMIT :limit")
fun observeLatest(deviceId: String, limit: Int): Flow<List<TelemetryRecord>>
@Query("SELECT * FROM telemetry WHERE synced = 0")
suspend fun getUnsynced(): List<TelemetryRecord>
}
WorkManager синхронізує несинхронізовані записи при появленні мережі.
Алерти та критичні сповіщення
У промисловості FCM/APNs не підходять для критичних алертів — немає гарантії доставки. Для алертів класу «стоп-машина» потрібен прямий WebSocket або MQTT push з локальним будильником як резервом.
Реалізація на Android: WebSocket тримаємо в Foreground Service (тип dataSync), при отриманні критичної подієї викликаємо NotificationManager з IMPORTANCE_HIGH та Ringtone.play() на максимальній гучності:
fun showCriticalAlert(message: AlertMessage) {
val channel = NotificationChannel(
CRITICAL_CHANNEL_ID,
"Critical Alerts",
NotificationManager.IMPORTANCE_HIGH
).apply {
enableVibration(true)
vibrationPattern = longArrayOf(0, 500, 200, 500)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
}
notificationManager.createNotificationChannel(channel)
val notification = NotificationCompat.Builder(context, CRITICAL_CHANNEL_ID)
.setContentTitle("⚠ ${message.deviceName}")
.setContentText(message.description)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setCategory(NotificationCompat.CATEGORY_ALARM)
.setAutoCancel(true)
.build()
notificationManager.notify(message.id.hashCode(), notification)
}
На iOS — аналогічно через Critical Alerts (вимагає спеціального entitlement: com.apple.developer.usernotifications.critical-alerts), які воспроизводять навіть в режимі «Не беспокоить».
UX для оператора виробництва
Промислений UX — не Material Design. Оператор працює в робочих рукавичах, при поганому освітленню, з телефоном в одній руці. Вимоги:
- Кнопки не менше 48×48 dp, краще 64×64 dp
- Висококонтрастна тема з можливістю інверсії для прямого сонячного світла
- Мінімум навігації: потрібна інформація за 1-2 тапа
- Офлайн-режим з чітким індикатором «немає зв'язку»
Безпека та доступ
Аутентифікація в IIoT-додатку — LDAP/Active Directory через SAML або OAuth 2.0 з корпоративним IdP. Biometric unlock допустим для повторної аутентифікації, але не для першоначального входу. Всі команди управління логуються з timestamp та user_id — аудит обов'язковий.
Розробка IIoT-додатка з нуля (Android або iOS): 3-5 місяців залежно від числа протоколів, кількості типів пристроїв та вимог до офлайн-режиму. Кросс-платформа на Flutter — плюс 20-30% до строків через нативні платформенні каналів. Вартість розраховується після детального аналізу технологічного стека та вимог до надійності.







