Разработка мобильного приложения для промышленного 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% к срокам из-за нативных платформенных каналов. Стоимость рассчитывается после детального анализа технологического стека и требований к надёжности.







