BMS projects start the same way: the client shows a building diagram with controllers from Siemens Desigo CC, Schneider Electric EcoStruxure, or Johnson Controls Metasys and says, "We want all this on a phone." Behind that "all this" are dozens of protocols, polling cycles from 1 second to 15 minutes, a historical database going back years, and the requirement to work even when the main BMS server reboots. We take such projects turnkey: from protocol analysis to publishing in stores. Our experience: 10+ years in BMS integration and over 50 implemented sites. We evaluate the project in 2 days and guarantee stable operation under any load.
Protocols and Gateways
Industrial BMS systems speak BACnet/IP, Modbus TCP/RTU, KNX/IP, and LonWorks. They are not directly accessible from a mobile app—between the controllers and the REST/WebSocket API sits a gateway or middleware.
Typical integration stack:
| Layer | Technology |
|---|---|
| Controllers | BACnet/IP, Modbus TCP, KNX |
| Gateway | Node-RED, Niagara Framework 4, custom Python/Go service |
| Transport | MQTT over TLS, REST, WebSocket |
| Mobile client | Flutter / Swift / Kotlin |
Niagara Framework 4 (Tridium) is the de facto standard for large sites. It normalizes BACnet objects into a unified REST API (/haystack/api/read?filter=bacnet) and provides WebSocket streams of changes. Working with Haystack API via Dart:
class HaystackClient { final Dio _dio; final String _baseUrl; HaystackClient(this._baseUrl, String username, String password) : _dio = Dio(BaseOptions( baseUrl: _baseUrl, headers: { 'Authorization': 'Basic ${base64Encode(utf8.encode('$username:$password'))}', 'Accept': 'application/json', }, )); Future<List<HaystackRow>> read(String filter) async { final response = await _dio.get('/haystack/api/read', queryParameters: {'filter': filter}); final grid = HaystackGrid.fromJson(response.data); return grid.rows; } Future<Map<String, dynamic>> readPoint(String pointId) async { final response = await _dio.get('/haystack/api/hisRead', queryParameters: { 'id': '@$pointId', 'range': 'today', }); return response.data; } } For sites with an MQTT gateway (Node-RED converts BACnet → MQTT JSON), we use the mqtt_client in Flutter. Topics are organized by building hierarchy: building/{buildingId}/floor/{floor}/zone/{zone}/{parameter}.
How We Integrate with Existing BMS?
The process always starts with an audit of the controllers: we find out which protocols are used, which version of Niagara or other middleware is installed, whether a REST/WebSocket API already exists or a gateway needs to be deployed. Then we design the data flow scheme: which points are read, which are written, and at what interval. Certified engineers configure the gateway and perform integration testing. The result is a unified interface on the phone instead of multiple control panels.
Real-Time Data Architecture: Why a DataHub is Critical?
The most challenging part of a BMS app is not the connection but managing the data stream. Temperature in 200 zones updates every 30 seconds, lighting changes by event, energy consumption every minute. All this cannot be resubscribed on every UI redraw.
The solution is a centralized DataHub at the application level:
class BmsDataHub { final MqttClient _mqtt; final _streams = <String, BehaviorSubject<BmsPoint>>{}; Stream<BmsPoint> watchPoint(String pointId) { if (!_streams.containsKey(pointId)) { _streams[pointId] = BehaviorSubject(); _mqtt.subscribe('building/+/+/+/$pointId', MqttQos.atLeastOnce); } return _streams[pointId]!.stream; } void _onMessage(List<MqttReceivedMessage<MqttMessage>> events) { for (final event in events) { final topic = event.topic; final payload = MqttPublishPayload.bytesToStringAsString( (event.payload as MqttPublishMessage).payload.message); final point = BmsPoint.fromJson(jsonDecode(payload)); _streams[point.id]?.add(point); } } } BehaviorSubject from the rxdart package retains the last value—so a widget that subscribes after data arrives immediately gets the current state without waiting for the next polling cycle.
Interactive Floor Plan
Customers always want a building floor plan with live data. We convert DXF or SVG plans to SVG (via ODA File Converter for DXF), render them with flutter_svg and InteractiveViewer. Sensor points are overlaid onto the SVG using normalized coordinates:
class FloorPlanWidget extends StatelessWidget { final FloorPlan plan; final Map<String, BmsPoint> liveData; @override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, constraints) { return Stack(children: [ SvgPicture.asset('assets/floors/${plan.id}.svg', width: constraints.maxWidth), ...plan.sensors.map((sensor) => Positioned( left: sensor.x * constraints.maxWidth, top: sensor.y * constraints.maxHeight, child: SensorMarker( point: liveData[sensor.pointId], type: sensor.type, ), )), ]); }); } } Markers change color based on thresholds: green (normal), yellow (warning), red (alarm). Thresholds are fetched from the BMS configuration—no hardcoding.
Control: Writing Values to BACnet Points
Reading is easier than writing. To command BACnet points (temperature setpoint, light on/off) via the REST gateway:
Future<void> writePoint(String pointId, dynamic value) async { // Optimistic UI update _hub.updateLocally(pointId, value); try { await _api.put('/haystack/api/pointWrite', data: { 'id': '@$pointId', 'level': 8, // BACnet write priority (1-16, lower = higher priority) 'val': value, 'who': _authService.currentUser, 'duration': 'PT0S', // permanent }); } on DioException catch (e) { // Roll back on error _hub.revertLocally(pointId); rethrow; } } BACnet Priority Array is a detail often overlooked—leading to confusion why a setpoint won't change: the controller accepts commands but they are overridden by a higher priority from BMS scheduling (level 2-4). Level 8 is standard for manual operator commands.
Alerts and Event Log
Alarm events from the BMS come via MQTT or WebSocket. Local push notifications are generated with flutter_local_notifications, server push (when the app is closed) via FCM with high priority (priority: high, content_available: true).
Event log: SQLite via drift for offline storage of 30 days of history, paginated loading from the API for older entries.
Access Control
On real sites, different users see different floors and zones. Rights are stored on the backend; the mobile client requests the list of accessible objects on login and does not build routes to inaccessible resources. Attempting to write to a forbidden point returns HTTP 403, triggering a local rollback and user notification.
What Is Included in Development?
- Analysis of controller protocols and BMS architecture.
- Design of integration scheme and data flows.
- Implementation of mobile client (iOS/Android on Flutter or native stack).
- Gateway configuration and integration testing.
- Publication in App Store and Google Play.
- 30 days of technical support after release.
- API documentation and staff training (optional).
Timeline and Cost
| Stage | Duration |
|---|---|
| MVP (floor plan + real-time monitoring) | 8–12 weeks |
| Full system (multiple objects, charts, alerts, rights) | 4–6 months |
| Integration with non-standard protocols | +2–4 weeks |
Cost is calculated individually after analysis of your controllers and requirements. Contact us for an evaluation—we will prepare a commercial proposal within 2 business days.







