Repository navigation
Expand file tree
/
Copy pathmain.cpp
More file actions
782 lines (731 loc) · 40.5 KB
/
Copy pathmain.cpp
File metadata and controls
782 lines (731 loc) · 40.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
// SPDX-License-Identifier: CC0-1.0
// Public-domain example code (CC0) - see LICENSE. The CAS BACnet Stack itself is
// a separate, commercially licensed product and is not covered by CC0.
// =============================================================================
// BACnet Profile Example - B-AS (Authorization Server) - C++
//
// This example shows the CAS BACnet Stack customer-facing surface needed for the
// BACnet "B-AS" (Authorization Server) device profile's DS/DM base:
//
// DS-RP-B - respond to ReadProperty requests
// DM-DDB-B - respond to Who-Is with I-Am, and announce on start-up
// DM-DOB-B - respond to Who-Has with I-Have
// DM-DCC-B - respond to DeviceCommunicationControl messages: a management
// station can tell this device to stop, or resume, communicating.
//
// BLOCKED EXAMPLE. AA-AS-B (Authorization Server - B, ASHRAE 135-2024 K.9.5:
// issuing access tokens with the AuthRequest service, Clause 17) is the B-AS
// Profile Function, and it is NOT implemented here. It is not faked: see TODO.md
// for the stack gap and its evidence (stack issue #2043). Short version - at the
// Stack Pin (6.x @ 69b6249f) the AuthRequest handler and the authorization
// policy exports (BACnetStack_AddAuthorizationPolicy and friends) compile only
// into the stack's test-tool build, not the customer-facing DLL surface this
// repo is restricted to, so AA-AS-B cannot be executed here.
//
// The device exposes the same three read-only sensor objects plus the Network
// Port object every base-series example does. Each object is given a colour name
// (a convention shared across this example series):
//
// Device 389023 "Chipkin Example B-AS" (instance configurable with --deviceID)
// Analog Input 1 "Bronze" (REAL, degrees Celsius; starts at 21.5)
// Binary Input 1 "Emerald" (active / inactive)
// Multi-State Input 1 "Hot Pink" (an enumerated state, 1..3)
// Network Port 1 "Vermilion" (the BACnet/IP port - required)
//
// To be a conformant BACnet device (Protocol_Revision 30) each object must
// expose its full set of REQUIRED properties. Most are generated by the stack
// (Object_Identifier, Object_Type, Status_Flags, Object_List, Protocol_*).
// Event_State is NOT generated - with no alarming configured it simply reads its
// datatype default of normal(0), correct by coincidence, not because the stack
// computes it. The handful that the application must supply are served by the
// Get*Property callbacks below, and a few are turned on with SetPropertyEnabled.
//
// Interactive keys (handled by the shared helper): h = help, q = quit,
// up/down = nudge Analog Input 1 by +/-1.1. Command line: --port <n>,
// --deviceID <n>, --deviceName <text>, --xml.
//
// All the UDP/stack plumbing lives in common/CASExampleHelper so this file can
// stay focused on the BACnet logic.
// =============================================================================
#include "CASExampleHelper.h"
#include "CASBACnetStackExampleConstants.h"
#include "CASBACnetStackAdapter.h" // the CAS BACnet Stack C API (BACnetStack_*); call
// LoadBACnetFunctions() before any BACnetStack_* call -
// see the top of main() below.
#include <stdio.h>
#include <string.h>
#include <string>
#if defined(_WIN32)
#include <windows.h> // Sleep()
#else
#include <unistd.h> // usleep()
#endif
using namespace CASBACnetStackExampleConstants;
// -----------------------------------------------------------------------------
// 1. Example + device configuration
// -----------------------------------------------------------------------------
static const char* APP_NAME = "BACnet Authorization Server (B-AS) Example - C++";
static const char* APP_VERSION = "1.1.0";
// The device instance. BACnet requires this to be configurable, so it defaults
// to 389023 and can be overridden on the command line with --deviceID. Keep it
// configurable in your product: it must be unique across the internetwork.
static uint32_t g_deviceInstance = 389023;
// ---- Device identity: CHANGE ALL OF THIS BEFORE YOU SHIP --------------------
// Everything in this block is read by clients and shown to the operator in every
// discovery tool on the network. Left as-is, your product will appear on a real
// site announcing itself as a Chipkin demo. None of it is cosmetic:
// Object_Name must be unique across the BACnet internetwork, and Model_Name /
// Vendor_Identifier are what a building operator uses to identify your device.
//
// This block is the ship checklist. Every constant below has a note saying what
// to change it to; nothing here is safe to leave at its example value.
// -----------------------------------------------------------------------------
// Your BACnet Vendor Identifier. 389 = Chipkin Automation Systems; change this
// to YOUR company's vendor ID before shipping a product. Vendor IDs are assigned
// by ASHRAE - request one (free) at https://bacnet.org/assigned-vendor-ids/.
// Update VENDOR_NAME below to match.
static const uint32_t VENDOR_IDENTIFIER = 389;
// The Device object's Object_Name - the DEFAULT one.
//
// THIS IS THE ONE THAT WILL BITE YOU. Object_Name must be unique across the
// whole BACnet internetwork (cl. 12.1.1.4), so it must be per-unit configurable,
// exactly like the instance. This example takes it at start-up with
// --deviceName <text> (common/ ParseDeviceNameArg, read into g_deviceName in
// main()); DEVICE_NAME is only what a unit announces when nobody configured it.
// Ship two units without --deviceName and BOTH announce the same Object_Name -
// a spec violation, and a hard BTL failure. In your product, configure the name
// per unit (a serial number, DIP switches, a config file, or this argument) and
// change the default below to something that says "not configured yet".
static const char* DEVICE_NAME = "Chipkin Example B-AS";
static const char* g_deviceName = DEVICE_NAME; // --deviceName, or the default
// The Device object's Description. Change it to what YOUR device actually is;
// this string describes this tutorial.
static const char* DEVICE_DESCRIPTION =
"Chipkin CAS BACnet Stack example - B-AS (Authorization Server) profile. "
"Demonstrates DS-RP-B + DM-DDB-B + DM-DOB-B + DM-DCC-B with read-only "
"sensor inputs. AA-AS-B not implemented - see TODO.md.";
// Device identity strings (read by clients, and used to populate I-Am).
// VENDOR_NAME - your company name; it must match VENDOR_IDENTIFIER above.
// MODEL_NAME - your model designation. This is what a building operator reads
// to identify your device in a discovery tool.
static const char* VENDOR_NAME = "Chipkin Automation Systems";
static const char* MODEL_NAME = "CAS BACnet Stack Example - B-AS";
// Application_Software_Version (12) is just APP_VERSION - one source of
// truth, so it can never drift from what --version/the startup banner
// prints (it did drift: this used to be a separate hardcoded "1.0.0"
// constant nobody updated across patch releases).
//
// Firmware_Revision (44) is meant to name the underlying platform/stack,
// not this example's own version - built at runtime from the CAS BACnet
// Stack's own BACnetStack_GetAPIMajorVersion()/etc. (the same 4 calls
// common/CASExampleHelper.cpp's PrintVersion() already uses for the
// startup banner's "CAS BACnet Stack version: X.Y.Z.W" line), so it can
// never go stale either - see g_firmwareRevision below, populated once
// right after LoadBACnetFunctions() succeeds (those functions are what
// the version getters themselves are, so they must be loaded first).
static std::string g_firmwareRevision;
// The sensor objects (all instance 1) and their colour names.
static const uint32_t ANALOG_INPUT_INSTANCE = 1; // "Bronze"
static const uint32_t BINARY_INPUT_INSTANCE = 1; // "Emerald"
static const uint32_t MULTI_STATE_INPUT_INSTANCE = 1; // "Hot Pink"
static const uint32_t MULTI_STATE_INPUT_NUMBER_OF_STATES = 3;
// The Network Port object - every BACnet device must have one. It represents
// the BACnet/IP port this device communicates on.
static const uint32_t NETWORK_PORT_INSTANCE = 1; // "Vermilion"
static const uint32_t MAX_APDU_LENGTH = 1476; // BACnet/IP APDU length
// BACnet/IP addressing the Network Port reports. The IP address and subnet mask
// are filled in at start-up from the host's primary interface; the gateway is
// left unset (0.0.0.0) for this example. The stack also uses IP_Address +
// BACnet_IP_UDP_Port to build the port's MAC_Address automatically.
static uint8_t g_ipAddress[4] = { 0, 0, 0, 0 };
static uint8_t g_ipSubnetMask[4] = { 0, 0, 0, 0 };
static uint8_t g_ipDefaultGateway[4] = { 0, 0, 0, 0 };
static uint16_t g_bacnetIpUdpPort = 47808;
// Analog Input 1's live present value (degrees Celsius). Starts at 21.5 and is
// nudged by the up/down arrow keys. A real sensor would update this from
// hardware instead.
static float g_analogInput1Value = 21.5f;
// DeviceCommunicationControl password. A management station may include a password
// with a DeviceCommunicationControl (or ReinitializeDevice) request; the device
// requires it to match if configured, or accepts any request if left "". A real
// deployment should set a non-empty password here.
static const char* DCC_PASSWORD = ""; // "" = no password required
// -----------------------------------------------------------------------------
// 2. Property "get" callbacks
//
// The stack calls these when a client reads a property. For each data type the
// stack uses a separate callback. We return true (and fill *value) when we
// recognise the (object, property) pair, and false otherwise.
//
// THE errorCode OUT-PARAMETER. Every Get callback ends with uint32_t* errorCode.
// The stack PRESETS it to success (84) before the call, and reads it only if you
// return false. That gives a declining callback two distinct meanings:
//
// 1. return false and LEAVE errorCode ALONE -> "I have no opinion on this
// property." The stack falls back to its own handling (see below).
// 2. return false and SET *errorCode -> "This read fails, with THIS
// BACnet error." The client gets exactly that Error-PDU.
//
// Option 2 is new (CAS BACnet Stack issue #974); before it, a Get callback had
// no way to name an error at all. Do not reach for it reflexively - option 1 is
// still the right answer most of the time, for the reason in the next paragraph.
//
// WHAT false-WITHOUT-AN-ERROR-CODE ACTUALLY DOES - the most important paragraph
// in this file, and the opposite of what most people assume. It does NOT
// reliably produce a BACnet error. The stack errors only for the handful of
// properties it refuses to invent: Present_Value, Number_Of_States,
// Relinquish_Default, Local_Date, Local_Time, and a Network Port's APDU_Length
// (declining one of those now reads back as Error: read-access-denied, where
// older stack versions said value-not-initialized).
// For EVERYTHING ELSE, a false return means the stack SILENTLY SUBSTITUTES a
// default:
// Object_Name -> the literal string "undefined"
// Units -> no-units (95)
// otherwise -> a datatype zero-value
//
// AND THAT FALLBACK IS LORE-BEARING, WHICH IS WHY WE DO NOT "FIX" IT HERE.
// It is tempting to end every callback with *errorCode = unknown-property so
// nothing is ever silently invented. That breaks the device. The stack relies on
// the decline-and-fabricate path to answer required properties the application
// is not expected to serve - the Device's Max_APDU_Length_Accepted, APDU_Timeout
// and Number_Of_APDU_Retries among them. Name an error on the catch-all return
// and those required properties start failing instead of answering.
// So: set *errorCode ONLY where THIS device knows the read is wrong. There is
// exactly one such case below (State_Text with an out-of-range array index); the
// catch-all `return false` at the end of each callback deliberately leaves
// errorCode alone.
// -----------------------------------------------------------------------------
// REAL (floating point) - the Analog Input's Present_Value.
bool GetPropertyReal(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
float* value, const bool useArrayIndex,
const uint32_t propertyArrayIndex, uint32_t* errorCode) {
(void)useArrayIndex;
(void)errorCode; // see "THE errorCode OUT-PARAMETER" above: we decline without naming an error
(void)propertyArrayIndex;
if (deviceInstance == g_deviceInstance &&
objectType == OBJECT_TYPE_ANALOG_INPUT &&
objectInstance == ANALOG_INPUT_INSTANCE &&
propertyIdentifier == PROPERTY_IDENTIFIER_PRESENT_VALUE) {
// ON REAL HARDWARE: return the live sensor reading here. Read it from a
// cached variable that your hardware updates (as g_analogInput1Value is),
// NOT directly from a slow/blocking device (I2C, SPI, ADC conversion):
// this callback runs on the BACnetStack_Tick() thread, so blocking it
// delays all BACnet processing. Sample the sensor on a timer/another
// thread and just hand back the latest value from here.
*value = g_analogInput1Value;
return true;
}
return false;
}
// ENUMERATED - the Binary Input's Present_Value (0 = inactive, 1 = active) and
// the Analog Input's Units (degrees Celsius).
bool GetPropertyEnumerated(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
uint32_t* value, const bool useArrayIndex,
const uint32_t propertyArrayIndex, uint32_t* errorCode) {
(void)useArrayIndex;
(void)propertyArrayIndex;
(void)errorCode;
if (deviceInstance != g_deviceInstance) {
return false;
}
if (objectType == OBJECT_TYPE_BINARY_INPUT &&
objectInstance == BINARY_INPUT_INSTANCE) {
if (propertyIdentifier == PROPERTY_IDENTIFIER_PRESENT_VALUE) {
// ON REAL HARDWARE: return your cached input state here - the same rule
// as GetPropertyReal above applies (never block this callback on slow
// I/O; sample on a timer/another thread and hand back the latest).
*value = 0; // inactive - the series-wide starting value
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_POLARITY) {
*value = POLARITY_NORMAL; // required property of a Binary Input
return true;
}
}
if (objectType == OBJECT_TYPE_ANALOG_INPUT &&
objectInstance == ANALOG_INPUT_INSTANCE &&
propertyIdentifier == PROPERTY_IDENTIFIER_UNITS) {
*value = ENGINEERING_UNITS_DEGREES_CELSIUS;
return true;
}
if (objectType == OBJECT_TYPE_NETWORK_PORT &&
objectInstance == NETWORK_PORT_INSTANCE &&
propertyIdentifier == PROPERTY_IDENTIFIER_BACNET_IP_MODE) {
*value = BACNET_IP_MODE_NORMAL; // not foreign-device, not BBMD
return true;
}
return false;
}
// UNSIGNED INTEGER - the Multi-State Input's Present_Value, and the Device's
// Vendor_Identifier (the stack also uses Vendor_Identifier to build I-Am).
bool GetPropertyUnsignedInteger(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
uint32_t* value, const bool useArrayIndex,
const uint32_t propertyArrayIndex, uint32_t* errorCode) {
(void)errorCode;
if (deviceInstance != g_deviceInstance) {
return false;
}
if (objectType == OBJECT_TYPE_MULTI_STATE_INPUT &&
objectInstance == MULTI_STATE_INPUT_INSTANCE) {
if (propertyIdentifier == PROPERTY_IDENTIFIER_PRESENT_VALUE) {
*value = 1; // state 1 (valid range is 1..Number_Of_States)
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_NUMBER_OF_STATES) {
*value = MULTI_STATE_INPUT_NUMBER_OF_STATES; // required property
return true;
}
// State_Text is an array. The stack asks for its LENGTH here (array
// index 0) before reading each element via GetPropertyCharString.
if (propertyIdentifier == PROPERTY_IDENTIFIER_STATE_TEXT &&
useArrayIndex && propertyArrayIndex == 0) {
*value = MULTI_STATE_INPUT_NUMBER_OF_STATES;
return true;
}
}
if (objectType == OBJECT_TYPE_DEVICE && objectInstance == g_deviceInstance &&
propertyIdentifier == PROPERTY_IDENTIFIER_VENDOR_IDENTIFIER) {
*value = VENDOR_IDENTIFIER;
return true;
}
if (objectType == OBJECT_TYPE_NETWORK_PORT && objectInstance == NETWORK_PORT_INSTANCE) {
if (propertyIdentifier == PROPERTY_IDENTIFIER_APDU_LENGTH) {
*value = MAX_APDU_LENGTH;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_REFERENCE_PORT) {
*value = NETWORK_PORT_REFERENCE_PORT_NONE;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_BACNET_IP_UDP_PORT) {
*value = g_bacnetIpUdpPort;
return true;
}
}
return false;
}
// BOOLEAN - Out_Of_Service is a required property of every input object and of
// the Network Port. This is a read-only sensor, so nothing is ever out of
// service: always false.
bool GetPropertyBool(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
bool* value, const bool useArrayIndex,
const uint32_t propertyArrayIndex, uint32_t* errorCode) {
(void)objectInstance;
(void)errorCode;
(void)useArrayIndex;
(void)propertyArrayIndex;
if (deviceInstance != g_deviceInstance) {
return false;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_OUT_OF_SERVICE &&
(objectType == OBJECT_TYPE_ANALOG_INPUT ||
objectType == OBJECT_TYPE_BINARY_INPUT ||
objectType == OBJECT_TYPE_MULTI_STATE_INPUT ||
objectType == OBJECT_TYPE_NETWORK_PORT)) {
*value = false;
return true;
}
return false;
}
// OCTET STRING - the Network Port's BACnet/IP addressing. The stack cannot know
// the host's IP, so the application must supply IP_Address and IP_Subnet_Mask
// (and IP_Default_Gateway). Each is four octets. The stack also reads IP_Address
// (with BACnet_IP_UDP_Port) to build the port's six-octet MAC_Address.
bool GetPropertyOctetString(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
uint8_t* value, uint32_t* valueElementCount,
const uint32_t maxElementCount, const bool useArrayIndex,
const uint32_t propertyArrayIndex, uint32_t* errorCode) {
(void)useArrayIndex;
(void)errorCode;
(void)propertyArrayIndex;
if (deviceInstance != g_deviceInstance ||
objectType != OBJECT_TYPE_NETWORK_PORT ||
objectInstance != NETWORK_PORT_INSTANCE ||
maxElementCount < 4) {
return false;
}
const uint8_t* source = NULL;
switch (propertyIdentifier) {
case PROPERTY_IDENTIFIER_IP_ADDRESS: source = g_ipAddress; break;
case PROPERTY_IDENTIFIER_IP_SUBNET_MASK: source = g_ipSubnetMask; break;
case PROPERTY_IDENTIFIER_IP_DEFAULT_GATEWAY: source = g_ipDefaultGateway; break;
default: return false;
}
memcpy(value, source, 4);
*valueElementCount = 4;
return true;
}
// Small helper: copy a C string into the stack's character-string buffer and
// set the element count + encoding. Returns true (so callers can `return`).
static bool ReturnCharacterString(const char* text, char* value,
uint32_t* valueElementCount,
const uint32_t maxElementCount,
uint8_t* encodingType) {
uint32_t length = (uint32_t)strlen(text);
if (length > maxElementCount) {
// Truncate SILENTLY to fit the stack's buffer. maxElementCount is
// MAX_CHARACTER_STRING_SIZE (256 in this build), and our longest string
// (DEVICE_DESCRIPTION) fits with room to spare - so this never trips
// here. But if you build with STACK_OPTION_TARGET_EMBEDDED, that limit drops to
// 64, and a long Object_Name or Description would be clipped mid-word
// with nothing on the wire or console to tell you. If you lengthen any
// served string, check it against MAX_CHARACTER_STRING_SIZE for your
// target, or make this truncation loud.
length = maxElementCount;
}
memcpy(value, text, length);
*valueElementCount = length;
*encodingType = CHARACTER_STRING_ENCODING_UTF8;
return true;
}
// CHARACTER STRING - Object_Name for each object, and the device Description.
bool GetPropertyCharString(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
char* value, uint32_t* valueElementCount,
const uint32_t maxElementCount, uint8_t* encodingType,
const bool useArrayIndex, const uint32_t propertyArrayIndex,
uint32_t* errorCode) {
if (deviceInstance != g_deviceInstance) {
return false;
}
// State_Text (optional) - one label per state of the Multi-State Input. It is
// a BACnet array, so the stack asks for one element at a time by index
// (1..Number_Of_States). Present_Value 1 -> "On", 2 -> "Off", 3 -> "Auto".
if (objectType == OBJECT_TYPE_MULTI_STATE_INPUT &&
objectInstance == MULTI_STATE_INPUT_INSTANCE &&
propertyIdentifier == PROPERTY_IDENTIFIER_STATE_TEXT && useArrayIndex) {
static const char* const stateText[] = { "On", "Off", "Auto" };
if (propertyArrayIndex >= 1 && propertyArrayIndex <= MULTI_STATE_INPUT_NUMBER_OF_STATES) {
return ReturnCharacterString(stateText[propertyArrayIndex - 1], value,
valueElementCount, maxElementCount, encodingType);
}
// The one place in this file where naming an error is clearly right: the
// client asked for State_Text[n] and this object has no element n. That
// is not "no opinion" - it is a wrong read, and the spec has a code for
// it. Without this the client would silently receive an empty string.
*errorCode = ERROR_CODE_INVALID_ARRAY_INDEX;
return false;
}
// Object_Name - the colour name for each object.
if (propertyIdentifier == PROPERTY_IDENTIFIER_OBJECT_NAME) {
if (objectType == OBJECT_TYPE_DEVICE && objectInstance == g_deviceInstance) {
return ReturnCharacterString(g_deviceName, value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_ANALOG_INPUT && objectInstance == ANALOG_INPUT_INSTANCE) {
return ReturnCharacterString("Bronze", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_BINARY_INPUT && objectInstance == BINARY_INPUT_INSTANCE) {
return ReturnCharacterString("Emerald", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_MULTI_STATE_INPUT && objectInstance == MULTI_STATE_INPUT_INSTANCE) {
return ReturnCharacterString("Hot Pink", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_NETWORK_PORT && objectInstance == NETWORK_PORT_INSTANCE) {
return ReturnCharacterString("Vermilion", value, valueElementCount, maxElementCount, encodingType);
}
}
// The remaining strings are all on the Device object - its identity, read
// by clients and used to populate the device's I-Am / object list.
if (objectType == OBJECT_TYPE_DEVICE && objectInstance == g_deviceInstance) {
switch (propertyIdentifier) {
case PROPERTY_IDENTIFIER_DESCRIPTION:
return ReturnCharacterString(DEVICE_DESCRIPTION, value, valueElementCount, maxElementCount, encodingType);
case PROPERTY_IDENTIFIER_VENDOR_NAME:
return ReturnCharacterString(VENDOR_NAME, value, valueElementCount, maxElementCount, encodingType);
case PROPERTY_IDENTIFIER_MODEL_NAME:
return ReturnCharacterString(MODEL_NAME, value, valueElementCount, maxElementCount, encodingType);
case PROPERTY_IDENTIFIER_FIRMWARE_REVISION:
return ReturnCharacterString(g_firmwareRevision.c_str(), value, valueElementCount, maxElementCount, encodingType);
case PROPERTY_IDENTIFIER_APPLICATION_SOFTWARE_VERSION:
return ReturnCharacterString(APP_VERSION, value, valueElementCount, maxElementCount, encodingType);
default:
break;
}
}
return false;
}
// -----------------------------------------------------------------------------
// 2b. DeviceCommunicationControl callback (DM-DCC-B)
//
// A management station sends DeviceCommunicationControl to tell a device to stop
// or resume communicating - useful to quiet a noisy device during commissioning.
// The CAS BACnet Stack runs the actual enable/disable state machine (and the
// optional re-enable timer) for us; this callback's job is to (a) validate the
// optional password and (b) let the application know what was asked.
//
// enableDisable: 0 = enable (resume), 1 = disable (stop initiating AND
// responding), 2 = disable-initiation (keep responding).
// useTimeDuration/timeDuration: if set, the device auto-re-enables after
// timeDuration minutes. The stack handles that timer.
//
// Return true to accept (the stack then applies the new communication state), or
// false with *errorCode = password-failure to reject a bad password.
//
// NOTE (Protocol_Revision >= 20): the plain "disable" value (1) is DEPRECATED.
// Even if this callback accepts it, the stack rejects the request with
// service-request-denied - the standard now expects "disable-initiation" (2)
// (the device keeps answering reads but stops initiating). So at rev 30 only
// enable (0) and disable-initiation (2) actually take effect.
// -----------------------------------------------------------------------------
bool DeviceCommunicationControl(const uint32_t deviceInstance, const uint8_t enableDisable,
const char* password, const uint8_t passwordLength,
const bool useTimeDuration, const uint16_t timeDuration,
uint32_t* errorCode) {
if (deviceInstance != g_deviceInstance) {
// Not our device. Set *errorCode even here - a false return with
// *errorCode unset ships "Error Code = success(84)", which is
// meaningless on the wire.
*errorCode = ERROR_CODE_OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED;
return false;
}
// Check the password if this device requires one. A device with no configured
// password (DCC_PASSWORD == "") accepts any request.
//
// Compare by LENGTH FIRST, then bytes: a BACnet CharacterString may legitimately
// contain embedded NULs, and strcmp would silently compare only up to the
// first one. Never strcmp a wire string.
const size_t requiredLength = strlen(DCC_PASSWORD);
if (requiredLength > 0) {
bool matches = (password != NULL) && (passwordLength == requiredLength);
if (matches) {
for (size_t i = 0; i < requiredLength; ++i) {
if (password[i] != DCC_PASSWORD[i]) {
matches = false;
break;
}
}
}
if (!matches) {
printf("DeviceCommunicationControl: REJECTED (password failure)\n");
*errorCode = ERROR_CODE_PASSWORD_FAILURE;
return false;
}
}
const char* action = (enableDisable == DCC_ENABLE) ? "enable (resume communication)" :
(enableDisable == DCC_DISABLE) ? "disable (1) - DEPRECATED, the stack will reject this" :
(enableDisable == DCC_DISABLE_INITIATION) ? "disable-initiation (keep responding)" :
"unknown";
if (useTimeDuration) {
printf("DeviceCommunicationControl: %s for %u minute(s)\n", action, timeDuration);
} else {
printf("DeviceCommunicationControl: %s (indefinitely)\n", action);
}
// Accept. Nothing to write to *errorCode on the success path.
//
// IMPORTANT: this callback MUST set *errorCode on EVERY `false` return. The
// DCC path has no default - the stack pre-initialises errorCode to
// BACnetErrorCode::success (84, NOT 0) and only names an error class from
// what we set. This differs from the Get* callbacks above, whose catch-all
// `false` deliberately leaves errorCode alone; do not carry that habit here.
return true;
}
// -----------------------------------------------------------------------------
// 3. main()
// -----------------------------------------------------------------------------
int main(int argc, char** argv) {
// Show printf output immediately, even when stdout is piped to a file.
setvbuf(stdout, NULL, _IONBF, 0);
// --- Load the CAS BACnet Stack -------------------------------------------
// Required in every link mode (source/static/DLL) before any other
// BACnetStack_* call - see CASBACnetStackAdapter.h. In DLL mode this is the
// step that actually resolves the symbols; skipping it there is a null-pointer
// call, not a silent no-op, so it comes before even --version (which calls
// BACnetStack_GetAPIMajorVersion() to print the linked stack's version).
if (!LoadBACnetFunctions()) {
fprintf(stderr, "Error: failed to load the CAS BACnet Stack: %s\n",
CASBACnetStackAdapter_LastError());
return 1;
}
// g_firmwareRevision (Device object property 44) - see its own doc
// comment above for why this is the STACK's version, not this example's
// own (that's Application_Software_Version/APP_VERSION instead). Must
// happen after LoadBACnetFunctions() (these getters ARE some of the
// functions it loads) and before the Device object is ever readable.
{
char buf[32];
snprintf(buf, sizeof(buf), "%u.%u.%u.%u",
BACnetStack_GetAPIMajorVersion(), BACnetStack_GetAPIMinorVersion(),
BACnetStack_GetAPIPatchVersion(), BACnetStack_GetAPIBuildVersion());
g_firmwareRevision = buf;
}
// --- Command line + version --------------------------------------------
// --help / --version print and exit, so handle them before we bind a socket.
// The help lists only the options this example reads: no --dcc-password (the
// DeviceCommunicationControl password is the DCC_PASSWORD constant above, not
// a command-line option), and --deviceName (read below).
if (CASExampleHelper::HandleHelpAndVersionArgs(argc, argv, APP_NAME, APP_VERSION,
false, // no --dcc-password
true)) { // --deviceName
return 0;
}
const uint16_t port = CASExampleHelper::ParsePortArg(argc, argv, 47808);
g_deviceInstance = CASExampleHelper::ParseDeviceIdArg(argc, argv, g_deviceInstance);
g_deviceName = CASExampleHelper::ParseDeviceNameArg(argc, argv, DEVICE_NAME);
CASExampleHelper::ParseXmlLogArg(argc, argv); // --xml: dump every RX/TX frame as XML instead of one line (off by default)
CASExampleHelper::PrintVersion(APP_NAME, APP_VERSION);
// --- Bind the BACnet/IP socket -----------------------------------------
if (!CASExampleHelper::SetupUDP(port)) {
return 1;
}
// Capture the BACnet/IP addressing the Network Port object will report.
g_bacnetIpUdpPort = port;
if (!CASExampleHelper::GetLocalIPv4(g_ipAddress, g_ipSubnetMask)) {
printf("FYI: could not read a local IPv4 address; Network Port IP_Address "
"will report 0.0.0.0.\n");
}
// --- Register callbacks -------------------------------------------------
// Tell the helper which Network Port object owns the socket it just bound.
// The stack identifies a link by its Network Port INSTANCE, so the transport
// callbacks (and the start-up I-Am) have to name the one added below.
CASExampleHelper::SetNetworkPortInstance(NETWORK_PORT_INSTANCE);
// The transport + time callbacks are shared boilerplate.
CASExampleHelper::RegisterCommonCallbacks();
// The property callbacks are specific to this example.
BACnetStack_RegisterCallbackGetPropertyReal(GetPropertyReal);
BACnetStack_RegisterCallbackGetPropertyEnumerated(GetPropertyEnumerated);
BACnetStack_RegisterCallbackGetPropertyUnsignedInteger(GetPropertyUnsignedInteger);
BACnetStack_RegisterCallbackGetPropertyCharacterString(GetPropertyCharString);
BACnetStack_RegisterCallbackGetPropertyBool(GetPropertyBool);
BACnetStack_RegisterCallbackGetPropertyOctetString(GetPropertyOctetString);
// The DeviceCommunicationControl callback makes this a B-AS (DM-DCC-B).
BACnetStack_RegisterCallbackDeviceCommunicationControl(DeviceCommunicationControl);
// --- Create the device --------------------------------------------------
if (!BACnetStack_AddDevice(g_deviceInstance)) {
printf("Error: Failed to add the Device %u.\n", g_deviceInstance);
return 1;
}
// Enable the services a B-AS's DS/DM base must execute: ReadProperty
// (DS-RP-B) and DeviceCommunicationControl (DM-DCC-B). We set them
// explicitly to make the profile requirements obvious.
//
// We deliberately do NOT enable WriteProperty, ReadPropertyMultiple,
// SubscribeCOV, or any alarm/event service: they are not part of this
// profile's required set.
if (!BACnetStack_SetServiceEnabled(g_deviceInstance, SERVICE_READ_PROPERTY, true)) {
printf("Error: Failed to enable the ReadProperty service.\n");
return 1;
}
if (!BACnetStack_SetServiceEnabled(g_deviceInstance, SERVICE_DEVICE_COMMUNICATION_CONTROL, true)) {
printf("Error: Failed to enable the DeviceCommunicationControl service.\n");
return 1;
}
// Discovery: Who-Is/I-Am (DM-DDB-B) and Who-Has/I-Have (DM-DOB-B).
//
// These need enabling even though the device already ANSWERS them. The
// stack's service defaults are whoIs + whoHas + readProperty only
// (BACnetDBDevice.cpp) - iAm and iHave are left FALSE. Who-Is is answered and
// the start-up I-Am is sent regardless, because neither is gated on the bit;
// but Protocol_Services_Supported is emitted verbatim from that bitstring, so
// without these calls the device DOES I-Am and I-Have while telling every
// client it supports neither. The README claims DM-DDB-B and DM-DOB-B; this
// is what makes the claim true on the wire.
if (!BACnetStack_SetServiceEnabled(g_deviceInstance, SERVICE_WHO_IS, true) ||
!BACnetStack_SetServiceEnabled(g_deviceInstance, SERVICE_I_AM, true) ||
!BACnetStack_SetServiceEnabled(g_deviceInstance, SERVICE_WHO_HAS, true) ||
!BACnetStack_SetServiceEnabled(g_deviceInstance, SERVICE_I_HAVE, true)) {
printf("Error: Failed to enable the discovery services (Who-Is/I-Am, Who-Has/I-Have).\n");
return 1;
}
// --- Add the read-only sensor objects -----------------------------------
// Every stack setup call returns a bool; a real device should always check
// it, so this example does too.
if (!BACnetStack_AddObject(g_deviceInstance, OBJECT_TYPE_ANALOG_INPUT, ANALOG_INPUT_INSTANCE)) {
printf("Error: Failed to add Analog Input %u (Bronze).\n", ANALOG_INPUT_INSTANCE);
return 1;
}
if (!BACnetStack_AddObject(g_deviceInstance, OBJECT_TYPE_BINARY_INPUT, BINARY_INPUT_INSTANCE)) {
printf("Error: Failed to add Binary Input %u (Emerald).\n", BINARY_INPUT_INSTANCE);
return 1;
}
if (!BACnetStack_AddObject(g_deviceInstance, OBJECT_TYPE_MULTI_STATE_INPUT, MULTI_STATE_INPUT_INSTANCE)) {
printf("Error: Failed to add Multi-State Input %u (Hot Pink).\n", MULTI_STATE_INPUT_INSTANCE);
return 1;
}
// --- Add the Network Port object ----------------------------------------
// Every BACnet device (Protocol_Revision 17+) must have at least one Network
// Port object describing the port it talks on. This one is the BACnet/IP
// application port; it is the lowest layer, so its reference port is "none".
// networkNumber 0 with quality "unknown" describes a local port that has not
// learned its network number - the right answer for a device that is not a
// router and has not been told one.
if (!BACnetStack_AddNetworkPortObject(
g_deviceInstance, NETWORK_PORT_INSTANCE,
NETWORK_PORT_NETWORK_TYPE_IPV4,
NETWORK_PORT_PROTOCOL_LEVEL_BACNET_APPLICATION,
0, // networkNumber: not configured
NETWORK_NUMBER_QUALITY_UNKNOWN,
NETWORK_PORT_REFERENCE_PORT_NONE)) {
printf("Error: Failed to add Network Port 1 (Vermilion).\n");
return 1;
}
// --- Enable the OPTIONAL properties we choose to expose ------------------
// The stack automatically enables an object's REQUIRED properties when the
// object is added (AddObject / AddNetworkPortObject) - so Units, Polarity,
// Number_Of_States, Out_Of_Service, and the Network Port's BACnet/IP
// addressing (IP_Address, IP_Subnet_Mask, BACnet_IP_UDP_Port, ...) are
// already enabled; our Get* callbacks just supply their values. Only
// OPTIONAL properties need SetPropertyEnabled. State_Text is optional on a
// Multi-State Input, so we enable it here (and serve it in GetPropertyCharString).
//
// The Device's Description is optional too, and it is an easy one to get
// wrong: serving it from a Get callback is NOT enough. The stack checks
// IsPropertyEnabled BEFORE it ever reaches the callbacks, and for an optional
// property that check falls back to "is it required?" - which is false. So a
// Description branch in the callback without this enable is DEAD CODE, and
// the client reads back Error: unknown-property.
if (!BACnetStack_SetPropertyEnabled(g_deviceInstance, OBJECT_TYPE_DEVICE,
g_deviceInstance, PROPERTY_IDENTIFIER_DESCRIPTION, true)) {
printf("Error: Failed to enable Description on the Device object.\n");
return 1;
}
if (!BACnetStack_SetPropertyEnabled(g_deviceInstance, OBJECT_TYPE_MULTI_STATE_INPUT,
MULTI_STATE_INPUT_INSTANCE, PROPERTY_IDENTIFIER_STATE_TEXT, true)) {
printf("Error: Failed to enable State_Text on Multi-State Input 1 (Hot Pink).\n");
return 1;
}
// Who-Is is answered automatically. The spec also requires a device to
// announce itself on start-up, so broadcast an unsolicited I-Am now (to the
// local subnet broadcast - the Network Port's own network).
CASExampleHelper::SendIAm(g_deviceInstance);
printf("FYI: Device %u (\"%s\") ready. Vendor ID %u. Press 'h' for help.\n",
g_deviceInstance, g_deviceName, VENDOR_IDENTIFIER);
// --- Run the stack ------------------------------------------------------
// BACnetStack_Tick() processes incoming messages and timers. Call it
// continuously, and poll the keyboard for interactive commands.
bool running = true;
while (running) {
BACnetStack_Tick();
switch (CASExampleHelper::PollKey()) {
case CASExampleHelper::KeyCommand::Help:
CASExampleHelper::PrintHelp(APP_NAME, APP_VERSION);
break;
case CASExampleHelper::KeyCommand::Quit:
running = false;
break;
case CASExampleHelper::KeyCommand::ArrowUp:
g_analogInput1Value += 1.1f;
printf("Analog Input 1 (Bronze) = %.1f C\n", g_analogInput1Value);
break;
case CASExampleHelper::KeyCommand::ArrowDown:
g_analogInput1Value -= 1.1f;
printf("Analog Input 1 (Bronze) = %.1f C\n", g_analogInput1Value);
break;
case CASExampleHelper::KeyCommand::None:
default:
break;
}
#if defined(_WIN32)
Sleep(1); // 1 ms - be a good citizen, don't spin the CPU
#else
usleep(1000);
#endif
}
CASExampleHelper::RestoreInput();
CASExampleHelper::ShutdownUDP();
return 0;
}