פרויקטי BMS מתחילים באותה דרך: הלקוח מציג תרשים בניין עם בקרים של Siemens Desigo CC, Schneider Electric EcoStruxure או Johnson Controls Metasys ואומר, "אנחנו רוצים את כל זה בטלפון." מאחורי "כל זה" מסתתרים עשרות פרוטוקולים, מחזורי שאילתה משנייה אחת עד 15 דקות, מסד נתונים היסטורי שנמשך שנים אחורה, והדרישה לעבוד גם כששרת ה-BMS הראשי מאתחל. אנחנו לוקחים פרויקטים כאלה במפתח סגור: מניתוח פרוטוקולים ועד פרסום בחנויות. הניסיון שלנו: 10+ שנים באינטגרציית BMS ומעל 50 אתרים מיושמים. אנו מעריכים את הפרויקט תוך יומיים ומבטיחים פעילות יציבה תחת כל עומס.
פרוטוקולים ושערי גישה
מערכות BMS תעשייתיות מדברות BACnet/IP, Modbus TCP/RTU, KNX/IP ו-LonWorks. הן אינן נגישות ישירות מאפליקציה ניידת—בין הבקרים ל-REST/WebSocket API יושב שער גישה או תווכה.
מחסנית אינטגרציה טיפוסית:
| שכבה | טכנולוגיה |
|---|---|
| בקרים | BACnet/IP, Modbus TCP, KNX |
| שער גישה | Node-RED, Niagara Framework 4, שירות Python/Go מותאם אישית |
| תעבורה | MQTT over TLS, REST, WebSocket |
| לקוח נייד | Flutter / Swift / Kotlin |
Niagara Framework 4 (Tridium) הוא התקן דה-פקטו לאתרים גדולים. הוא מנרמל אובייקטי BACnet ל-REST API אחיד (/haystack/api/read?filter=bacnet) ומספק זרמי WebSocket של שינויים. עבודה עם Haystack API באמצעות 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;
}
}לאתרים עם שער MQTT (Node-RED ממיר BACnet → MQTT JSON), אנו משתמשים ב-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; } } ב-Flutter. נושאים מאורגנים לפי היררכיית הבניין: mqtt_client.
איך אנו משתלבים עם BMS קיים?
התהליך תמיד מתחיל בבדיקת הבקרים: אנו מגלים אילו פרוטוקולים בשימוש, איזו גרסת Niagara או תווכה אחרת מותקנת, האם קיים REST/WebSocket API או שצריך לפרוס שער גישה. לאחר מכן אנו מתכננים את תוכנית זרימת הנתונים: אילו נקודות נקראות, אילו נכתבות, ובאיזה מרווח. מהנדסים מוסמכים מגדירים את שער הגישה ומבצעים בדיקות אינטגרציה. התוצאה היא ממשק אחיד בטלפון במקום מספר לוחות בקרה.
ארכיטקטורת נתונים בזמן אמת: למה DataHub קריטי?
החלק המאתגר ביותר באפליקציית BMS אינו החיבור אלא ניהול זרם הנתונים. טמפרטורה ב-200 אזורים מתעדכנת כל 30 שניות, תאורה משתנה לפי אירוע, צריכת אנרגיה כל דקה. כל זה לא יכול להיות מנוי מחדש בכל רינדור ממשק.
הפתרון הוא DataHub מרכזי ברמת האפליקציה:
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);
}
}
}building/{buildingId}/floor/{floor}/zone/{zone}/{parameter} מחבילת 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); } } } שומר את הערך האחרון—כך שווידג'ט שנרשם לאחר שהנתונים הגיעו מקבל מיד את המצב הנוכחי מבלי לחכות למחזור השאילתה הבא.
תוכנית קומה אינטראקטיבית
לקוחות תמיד רוצים תוכנית קומה של הבניין עם נתונים חיים. אנו ממירים תוכניות DXF או SVG ל-SVG (באמצעות ODA File Converter עבור DXF), מציגים אותן עם BehaviorSubject ו-rxdart. נקודות חיישן מונחות על גבי ה-SVG באמצעות קואורדינטות מנורמלות:
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,
),
),
),
],
);
},
);
}
}סמנים משנים צבע לפי ספים: ירוק (נורמלי), צהוב (אזהרה), אדום (התראה). ספים נטענים מהגדרות ה-BMS—ללא קידוד קשיח.
שליטה: כתיבת ערכים לנקודות BACnet
קריאה קלה יותר מכתיבה. כדי לשלוט בנקודות BACnet (נקודת הגדרת טמפרטורה, הדלקת/כיבוי אור) דרך שער ה-REST:
Future<void> writePoint(String pointId, dynamic value) async {
// Оптимистичное обновление UI
_hub.updateLocally(pointId, value);
try {
await _api.put('/haystack/api/pointWrite', data: {
'id': '@$pointId',
'level': 8, // приоритет записи BACnet (1-16, ниже = выше приоритет)
'val': value,
'who': _authService.currentUser,
'duration': 'PT0S', // permanent
});
} on DioException catch (e) {
// Откат при ошибке
_hub.revertLocally(pointId);
rethrow;
}
}BACnet Priority Array הוא פרט שלעתים קרובות מתעלמים ממנו—מה שמוביל לבלבול מדוע נקודת הגדרה לא משתנה: הבקר מקבל פקודות אך הן נדרסות על ידי עדיפות גבוהה יותר מתזמון BMS (רמות 2-4). רמה 8 היא התקן לפקודות ידניות של מפעיל.
התראות ויומן אירועים
אירועי אזעקה מה-BMS מגיעים דרך MQTT או WebSocket. התראות push מקומיות נוצרות עם flutter_svg, push מהשרת (כשהאפליקציה סגורה) דרך FCM בעדיפות גבוהה (InteractiveViewer, 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, ), )), ]); }); } } ).
יומן אירועים: SQLite דרך Future<void> writePoint(String pointId, dynamic value) async { // Оптимистичное обновление UI _hub.updateLocally(pointId, value); try { await _api.put('/haystack/api/pointWrite', data: { 'id': '@$pointId', 'level': 8, // приоритет записи BACnet (1-16, ниже = выше приоритет) 'val': value, 'who': _authService.currentUser, 'duration': 'PT0S', // permanent }); } on DioException catch (e) { // Откат при ошибке _hub.revertLocally(pointId); rethrow; } } לאחסון לא מקוון של 30 ימי היסטוריה, טעינה מפולחת מה-API לערכים ישנים יותר.
בקרת גישה
באתרים אמיתיים, משתמשים שונים רואים קומות ואזורים שונים. הרשאות מאוחסנות בקצה האחורי; הלקוח הנייד מבקש את רשימת האובייקטים הנגישים בעת התחברות ואינו בונה נתיבים למשאבים לא נגישים. ניסיון כתיבה לנקודה אסורה מחזיר HTTP 403, מה שמפעיל גלגול לאחור מקומי והתראה למשתמש.
מה כלול בפיתוח?
- ניתוח פרוטוקולי בקרים וארכיטקטורת BMS.
- תכנון תוכנית אינטגרציה וזרימות נתונים.
- יישום לקוח נייד (iOS/Android ב-Flutter או מחסנית מקורית).
- הגדרת שער גישה ובדיקות אינטגרציה.
- פרסום ב-App Store וב-Google Play.
- 30 ימי תמיכה טכנית לאחר השחרור.
- תיעוד API והדרכת צוות (אופציונלי).
לוח זמנים ועלות
| שלב | משך |
|---|---|
| MVP (תוכנית קומה + ניטור בזמן אמת) | 8–12 שבועות |
| מערכת מלאה (מספר אובייקטים, גרפים, התראות, הרשאות) | 4–6 חודשים |
| אינטגרציה עם פרוטוקולים לא סטנדרטיים | +2–4 שבועות |
העלות מחושבת באופן אישי לאחר ניתוח הבקרים והדרישות שלכם. צרו קשר להערכה—נכין הצעה מסחרית תוך יומיים עסקיים.







