Repository navigation
Expand file tree
/
Copy pathmain.cpp
More file actions
1433 lines (1354 loc) · 75.8 KB
/
Copy pathmain.cpp
File metadata and controls
1433 lines (1354 loc) · 75.8 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
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// 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-ASC (BACnet Application Specific Controller) - C++
//
// This example implements the BACnet "B-ASC" (Application Specific Controller)
// device profile with the CAS BACnet Stack. It is the B-SA (Smart Actuator)
// example plus one device-management capability: it answers
// DeviceCommunicationControl.
//
// "A B-ASC is a controller with limited resources relative to a B-AAC. It is
// intended for use in a specific application and supports limited
// programmability." (ANSI/ASHRAE 135-2024, Annex L.4.3). What it adds to a B-SA
// is one Device and Network Management line of that clause: "Ability to respond
// to communication control messages". Its Profile Function: a controller that
// is commanded like a B-SA, and that a management station can silence and
// un-silence. The BIBBs:
//
// DS-RP-B - respond to ReadProperty requests,
// DS-WP-B - allow other devices to WriteProperty its objects, and
// DM-DCC-B - respond to DeviceCommunicationControl messages (NEW): a
// management station can tell the device to stop/resume talking,
// optionally for a time period and behind a password.
//
// plus discovery (Who-Is/I-Am, Who-Has/I-Have). It does NOT require COV, alarms,
// scheduling, or trending, so this example leaves those off.
//
// SOMETHING TO SILENCE. "Disable initiation" only means something if the device
// initiates. Answering Who-Is is not initiating (cl. 16.1.2 exempts it), so this
// example announces itself with an unsolicited I-Am every HEARTBEAT_SECONDS
// (section 2d) - I-Am is the one service DM-DDB-B has the device initiate, so the
// BACnet Surface does not grow. While a DeviceCommunicationControl
// DISABLE_INITIATION is in force the stack refuses to send it; the heartbeat
// stops, and resumes on ENABLE or when the time duration runs out.
//
// The device keeps the same objects as B-SA - three commandable outputs a client
// drives with WriteProperty, and three read-only inputs that are their FEEDBACK.
// Each object has a colour name (the convention shared across this example
// series):
//
// Device 389003 "Chipkin Example B-ASC" (instance configurable with --deviceID,
// Object_Name with --deviceName)
// Analog Input 1 "Bronze" (REAL, degrees Celsius; read-only - the temperature the valve produces)
// Binary Input 1 "Emerald" (active / inactive; read-only - the relay's feedback contact)
// Multi-State Input 1 "Hot Pink" (state 1..3; read-only - the speed the drive is running at)
// Analog Output 1 "Chartreuse" (REAL setpoint, 10.0 .. 30.0 C; WRITABLE, commandable)
// Binary Output 1 "Fuchsia" (active / inactive relay; WRITABLE, commandable)
// Multi-State Output 1 "Indigo" (speed 1..3; WRITABLE, commandable)
// Network Port 1 "Vermilion" (the BACnet/IP port - required)
//
// SIMULATION. As in B-SA, section 1b is an actuator Simulation that stands in for
// the equipment: a heating valve moves the temperature toward the winning Analog
// Output 1 command, a relay follows Binary Output 1, a drive follows Multi-State
// Output 1, and each reports back through its input. It is NOT part of the
// BACnet Surface. Its Failure Mode is a disconnected valve actuator (Analog
// Output 1 Reliability NO_OUTPUT, Status_Flags FAULT), reachable only through a
// test-only hook (ACTUATOR_FAULT_FILE_VARIABLE).
//
// Output objects are COMMANDABLE: their Present_Value is driven by a 16-slot
// BACnet Priority_Array. A WriteProperty(Present_Value, value, priority) sets a
// slot; writing NULL relinquishes it; the stack reports the highest-priority
// non-null slot (or Relinquish_Default) as the effective Present_Value. The app
// stores the priority array (see the Commandable struct below) and serves it; the
// stack computes Present_Value from it.
//
// 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 = disturb the simulated temperature (Analog Input 1) by +/-1.1; the
// valve then brings it back to the setpoint. Command line: --port <n>,
// --deviceID <n>, --deviceName <text>, --dcc-password <string>, --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 <chrono>
#include <fstream>
#include <stdio.h>
#include <stdlib.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 B-ASC (Application Specific Controller) Example - C++";
static const char* APP_VERSION = "1.5.0";
// The device instance. BACnet requires this to be configurable, so it defaults
// to 389003 and can be overridden on the command line with --deviceID.
static uint32_t g_deviceInstance = 389003;
// ---- 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-ASC";
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-ASC (Application Specific Controller) "
"profile. Demonstrates DS-RP-B + DS-WP-B + DM-DCC-B: ReadProperty, "
"WriteProperty, and DeviceCommunicationControl with read-only sensor inputs "
"and commandable outputs.";
// 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-ASC";
// DeviceCommunicationControl password. A management station may include a password
// with a DeviceCommunicationControl (or ReinitializeDevice) request; the device
// accepts the command only if it matches. Empty = accept any request (no password
// required). The default is empty; `--dcc-password <string>` sets it at start-up
// (main() below), so the password Failure Mode can be shown without a recompile. In
// your product, set it from your own configuration - and remember it crosses the
// wire in PLAINTEXT: it is a guard against accidents, not a security boundary. See
// the DeviceCommunicationControl callback below for the compare.
static const char* DEFAULT_DCC_PASSWORD = ""; // "" = no password required
static const char* g_dccPassword = DEFAULT_DCC_PASSWORD;
// 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 device instance below (g_deviceInstance, default 389003, --deviceID overrides)
// must also be unique on the internetwork; BACnet requires it to be configurable, so
// keep the --deviceID override rather than hard-coding it.
// 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;
// The input objects' live values - the actuators' feedback. The Get callbacks
// only ever hand these back; the Simulation (section 1b) is the only thing that
// changes them. On a real device your feedback sampling updates them instead.
static float g_analogInput1Value = 21.5f; // degrees Celsius
static uint32_t g_binaryInput1Value = 0; // 0 = inactive, 1 = active
static uint32_t g_multiStateInput1Value = 1; // 1..MULTI_STATE_INPUT_NUMBER_OF_STATES
// The commandable OUTPUT objects (all instance 1) and their colour names. These
// are inherited verbatim from B-SA, which defines this pattern for the series:
// clients drive them with WriteProperty (DS-WP-B).
static const uint32_t ANALOG_OUTPUT_INSTANCE = 1; // "Chartreuse"
static const uint32_t BINARY_OUTPUT_INSTANCE = 1; // "Fuchsia"
static const uint32_t MULTI_STATE_OUTPUT_INSTANCE = 1; // "Indigo"
static const uint32_t MULTI_STATE_OUTPUT_NUMBER_OF_STATES = 3;
static const uint32_t BACNET_PRIORITY_ARRAY_SIZE = 16;
// A BACnet commandable value: a 16-slot Priority_Array plus a Relinquish_Default.
// Each slot is either null (relinquished) or holds a commanded value. The
// resolved value (ResolvedCommand() below - the same answer the stack gives for
// Present_Value) is what drives the output; here that is the Simulation in
// section 1b. The values are kept as double and cast per object type (REAL for
// AO, 0/1 for BO, state number for MSO).
struct Commandable {
bool isSet[16]; // is slot i (1..16) commanded?
double value[16]; // the commanded value at slot i
double relinquishDefault; // used when every slot is null
};
// The { { false }, { 0 }, default } initializer zero-fills all 16 slots of isSet
// and value (C++ aggregate rules: the remaining elements are value-initialized),
// so every priority slot starts null and Present_Value reports relinquishDefault.
static Commandable g_analogOutput = { { false }, { 0 }, 20.0 }; // setpoint, default 20.0 C
static Commandable g_binaryOutput = { { false }, { 0 }, 0.0 }; // default inactive (0)
static Commandable g_multiStateOutput = { { false }, { 0 }, 1.0 }; // default state 1
// A WriteProperty to a commandable Present_Value carries a priority 1..16. When a
// client omits it, BACnet uses 16 (the lowest priority) - so normalise anything
// out of range to 16, matching the stack's own behaviour.
static uint8_t EffectivePriority(uint8_t priority) {
return (priority >= 1 && priority <= BACNET_PRIORITY_ARRAY_SIZE) ? priority : 16;
}
// Store a commanded value at a priority slot (a WriteProperty of a value).
static void CommandWrite(Commandable* c, uint8_t priority, double value) {
const uint8_t p = EffectivePriority(priority);
c->isSet[p - 1] = true;
c->value[p - 1] = value;
}
// Relinquish (clear) a priority slot - i.e. a WriteProperty of NULL.
static void CommandRelinquish(Commandable* c, uint8_t priority) {
const uint8_t p = EffectivePriority(priority);
c->isSet[p - 1] = false;
}
// Resolve which Commandable an (objectType, objectInstance) maps to, or NULL.
static Commandable* GetCommandable(uint16_t objectType, uint32_t objectInstance) {
if (objectType == OBJECT_TYPE_ANALOG_OUTPUT && objectInstance == ANALOG_OUTPUT_INSTANCE) {
return &g_analogOutput;
}
if (objectType == OBJECT_TYPE_BINARY_OUTPUT && objectInstance == BINARY_OUTPUT_INSTANCE) {
return &g_binaryOutput;
}
if (objectType == OBJECT_TYPE_MULTI_STATE_OUTPUT && objectInstance == MULTI_STATE_OUTPUT_INSTANCE) {
return &g_multiStateOutput;
}
return NULL;
}
// Is this read a single Priority_Array element (Priority_Array[1..16])? If so,
// report whether that slot is commanded (*slotIsSet) and its value (*slotValue).
// The typed Get callbacks use this to serve a commandable object's Priority_Array
// and to let the stack compute Present_Value from the highest non-null slot.
static bool ReadPrioritySlot(const Commandable* c, uint32_t propertyIdentifier,
bool useArrayIndex, uint32_t propertyArrayIndex,
bool* slotIsSet, double* slotValue) {
if (propertyIdentifier != PROPERTY_IDENTIFIER_PRIORITY_ARRAY || !useArrayIndex ||
propertyArrayIndex < 1 || propertyArrayIndex > BACNET_PRIORITY_ARRAY_SIZE) {
return false;
}
*slotIsSet = c->isSet[propertyArrayIndex - 1];
*slotValue = c->value[propertyArrayIndex - 1];
return true;
}
// The command that wins (cl. 19.2): the value at the highest-priority (lowest
// numbered) commanded slot, or Relinquish_Default when every slot is null. This
// is the value the stack reports as Present_Value, and the one an output obeys.
static double ResolvedCommand(const Commandable* c) {
for (uint32_t slot = 0; slot < BACNET_PRIORITY_ARRAY_SIZE; ++slot) {
if (c->isSet[slot]) {
return c->value[slot];
}
}
return c->relinquishDefault;
}
// Analog Output 1 accepts setpoints in this range, and serves it as the optional
// Min_Pres_Value / Max_Pres_Value (cl. 12.3.12 / 12.3.13: the lowest / highest
// number "that can be reliably used for the Present_Value"). A write outside it
// is refused with value-out-of-range.
static const uint32_t PROPERTY_IDENTIFIER_MAX_PRES_VALUE = 65;
static const uint32_t PROPERTY_IDENTIFIER_MIN_PRES_VALUE = 69;
static const float ANALOG_OUTPUT_MIN_PRES_VALUE = 10.0f;
static const float ANALOG_OUTPUT_MAX_PRES_VALUE = 30.0f;
// Analog Output 1's Reliability (cl. 12.3.9). NO_FAULT_DETECTED while the valve
// actuator is connected; NO_OUTPUT ("No physical device is connected to the
// Output object", cl. 12.1) while it is not. The stack builds the FAULT bit of
// Status_Flags from this property by itself (cl. 12.3.7) - the example only has
// to serve it.
static const uint32_t PROPERTY_IDENTIFIER_RELIABILITY = 103;
static const uint32_t RELIABILITY_NO_FAULT_DETECTED = 0;
static const uint32_t RELIABILITY_NO_OUTPUT = 6;
static uint32_t g_analogOutput1Reliability = RELIABILITY_NO_FAULT_DETECTED;
// -----------------------------------------------------------------------------
// 1b. The actuator Simulation (NOT part of the BACnet Surface)
//
// Stands in for the equipment the outputs drive. Once a second:
// - the heating valve (Analog Output 1) moves the temperature (Analog Input 1)
// up to 1.0 C toward the winning setpoint;
// - the relay's feedback contact (Binary Input 1) takes the winning Binary
// Output 1 command;
// - the drive's speed (Multi-State Input 1) takes the winning Multi-State
// Output 1 command;
// and the valve actuator's connection is checked. Feedback that lags its command
// by up to a second is what a real actuator gives you, too.
//
// The disconnected valve is a TEST-ONLY hook, not a feature: when the
// environment variable named by ACTUATOR_FAULT_FILE_VARIABLE names a file, the
// valve actuator counts as disconnected for as long as that file exists. A Wire
// Test creates and deletes the file. While disconnected, the valve moves nothing
// - the temperature stays where it is whatever is commanded - and Analog Output 1
// Reliability reads NO_OUTPUT; commands are still accepted into the
// Priority_Array, and once the file is gone the valve obeys the winning one. On a
// real device, set Reliability from your output driver's own diagnostics (an
// open-circuit detect, a position feedback that does not move, ...).
// -----------------------------------------------------------------------------
static const char* const ACTUATOR_FAULT_FILE_VARIABLE = "CAS_EXAMPLE_TEST_ACTUATOR_FAULT_FILE";
static std::string g_actuatorFaultFile; // read once at start-up; empty = no hook
static const float VALVE_MAX_CHANGE_PER_SECOND = 1.0f; // degrees Celsius
static std::string ReadEnvironmentVariable(const char* name) {
#if defined(_WIN32)
char* environmentValue = NULL;
size_t environmentValueLength = 0;
if (_dupenv_s(&environmentValue, &environmentValueLength, name) != 0 || environmentValue == NULL) {
return std::string();
}
std::string result(environmentValue);
free(environmentValue);
return result;
#else
const char* environmentValue = getenv(name);
return environmentValue != NULL ? std::string(environmentValue) : std::string();
#endif
}
static bool ValveActuatorDisconnected() {
return !g_actuatorFaultFile.empty() && std::ifstream(g_actuatorFaultFile.c_str()).good();
}
static void ActuatorSimulationTick() {
static const std::chrono::steady_clock::time_point started = std::chrono::steady_clock::now();
static unsigned lastSecond = 0;
const unsigned secondsRunning = (unsigned)std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::steady_clock::now() - started).count();
if (secondsRunning == lastSecond) {
return;
}
lastSecond = secondsRunning;
const uint32_t reliability = ValveActuatorDisconnected() ? RELIABILITY_NO_OUTPUT : RELIABILITY_NO_FAULT_DETECTED;
if (reliability != g_analogOutput1Reliability) {
g_analogOutput1Reliability = reliability;
printf("Simulation: Analog Output 1 (Chartreuse) valve actuator %s - Reliability %s\n",
reliability == RELIABILITY_NO_OUTPUT ? "disconnected" : "reconnected",
reliability == RELIABILITY_NO_OUTPUT ? "NO_OUTPUT" : "NO_FAULT_DETECTED");
}
if (g_analogOutput1Reliability == RELIABILITY_NO_FAULT_DETECTED) {
const float setpoint = (float)ResolvedCommand(&g_analogOutput);
const float difference = setpoint - g_analogInput1Value;
if (difference > VALVE_MAX_CHANGE_PER_SECOND) {
g_analogInput1Value += VALVE_MAX_CHANGE_PER_SECOND;
} else if (difference < -VALVE_MAX_CHANGE_PER_SECOND) {
g_analogInput1Value -= VALVE_MAX_CHANGE_PER_SECOND;
} else {
g_analogInput1Value = setpoint;
}
}
g_binaryInput1Value = (uint32_t)ResolvedCommand(&g_binaryOutput);
g_multiStateInput1Value = (uint32_t)ResolvedCommand(&g_multiStateOutput);
}
// -----------------------------------------------------------------------------
// 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.
//
// ADDING AN OBJECT? READ THIS FIRST.
// The consequence is the opposite of reassuring. These callbacks are not
// uniformly strict:
// - GetPropertyReal / GetPropertyEnumerated / GetPropertyUnsignedInteger match
// on object type AND INSTANCE (directly, or via GetCommandable(), which
// looks up the exact type+instance pair). A new instance falls through every
// one of those checks.
// - GetPropertyBool serves Out_Of_Service on object TYPE ONLY, so a new
// instance of an existing type gets Out_Of_Service for free.
// So a half-added object does NOT fail loudly. Its Present_Value errors (that
// one is in the list above) - but its Object_Name reads back as "undefined" and
// its Units as no-units, with no error at all. Add two objects that way and BOTH
// report Object_Name "undefined": duplicate object names within one device, which
// is a spec violation and a hard BTL failure, and which every scan tool will show
// you as a healthy object. The device looks fine and is non-conformant.
//
// So: when you add an instance, walk EVERY callback below, then read back every
// required property of the new object and DIFF IT against the existing one. Do
// not trust "it scanned OK" - that is exactly the failure mode. The README's
// "Extending the example" recipe lists the edits.
// -----------------------------------------------------------------------------
// 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)errorCode; // see "THE errorCode OUT-PARAMETER" above: we decline without naming an error
if (deviceInstance != g_deviceInstance) {
return false;
}
if (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;
}
// Analog Output (commandable): serve its Priority_Array slots and
// Relinquish_Default. The stack reads each slot to compute Present_Value and
// to answer a ReadProperty of the whole array. For a null (relinquished) slot
// we return false - the stack then takes the "slot is null" answer from
// GetPropertyBool below.
const Commandable* c = GetCommandable(objectType, objectInstance);
if (c != NULL && objectType == OBJECT_TYPE_ANALOG_OUTPUT) {
bool slotIsSet = false;
double slotValue = 0.0;
if (ReadPrioritySlot(c, propertyIdentifier, useArrayIndex, propertyArrayIndex,
&slotIsSet, &slotValue)) {
if (!slotIsSet) {
return false;
}
*value = (float)slotValue;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_RELINQUISH_DEFAULT) {
*value = (float)c->relinquishDefault;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_MIN_PRES_VALUE) {
*value = ANALOG_OUTPUT_MIN_PRES_VALUE;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_MAX_PRES_VALUE) {
*value = ANALOG_OUTPUT_MAX_PRES_VALUE;
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)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 = g_binaryInput1Value; // the relay's feedback contact
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_POLARITY) {
*value = POLARITY_NORMAL; // required property of a Binary Input
return true;
}
}
// Binary Output (commandable): Present_Value is an enumerated active/inactive
// driven through the Priority_Array. Serve the array slots and Relinquish_Default
// (plus its required Polarity).
const Commandable* c = GetCommandable(objectType, objectInstance);
if (c != NULL && objectType == OBJECT_TYPE_BINARY_OUTPUT) {
bool slotIsSet = false;
double slotValue = 0.0;
if (ReadPrioritySlot(c, propertyIdentifier, useArrayIndex, propertyArrayIndex,
&slotIsSet, &slotValue)) {
if (!slotIsSet) {
return false;
}
*value = (uint32_t)slotValue;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_RELINQUISH_DEFAULT) {
*value = (uint32_t)c->relinquishDefault;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_POLARITY) {
*value = POLARITY_NORMAL; // required property of a Binary Output
return true;
}
}
// Units is REQUIRED on an Analog Input AND on an Analog Output. Serve BOTH.
// If you only serve the input's, the output does not error - it silently
// reports no-units(95), because Units is one of the properties the stack
// substitutes a default for rather than erroring on (see the note at the top
// of this section). A setpoint that reads back "no units" next to a degC
// sensor is the kind of thing nobody notices until commissioning.
if (propertyIdentifier == PROPERTY_IDENTIFIER_UNITS &&
((objectType == OBJECT_TYPE_ANALOG_INPUT && objectInstance == ANALOG_INPUT_INSTANCE) ||
(objectType == OBJECT_TYPE_ANALOG_OUTPUT && objectInstance == ANALOG_OUTPUT_INSTANCE))) {
*value = ENGINEERING_UNITS_DEGREES_CELSIUS;
return true;
}
// Analog Output 1's Reliability (enabled in main()). The stack also reads it
// through this callback to build Status_Flags, so once enabled it must always
// answer.
if (objectType == OBJECT_TYPE_ANALOG_OUTPUT && objectInstance == ANALOG_OUTPUT_INSTANCE &&
propertyIdentifier == PROPERTY_IDENTIFIER_RELIABILITY) {
*value = g_analogOutput1Reliability;
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 = g_multiStateInput1Value; // the drive's speed; 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;
}
}
// Multi-State Output (commandable): Present_Value is an unsigned state number
// driven through the Priority_Array. Serve the array slots, Relinquish_Default,
// and the required Number_Of_States.
const Commandable* c = GetCommandable(objectType, objectInstance);
if (c != NULL && objectType == OBJECT_TYPE_MULTI_STATE_OUTPUT) {
bool slotIsSet = false;
double slotValue = 0.0;
if (ReadPrioritySlot(c, propertyIdentifier, useArrayIndex, propertyArrayIndex,
&slotIsSet, &slotValue)) {
if (!slotIsSet) {
return false;
}
*value = (uint32_t)slotValue;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_RELINQUISH_DEFAULT) {
*value = (uint32_t)c->relinquishDefault;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_NUMBER_OF_STATES) {
*value = MULTI_STATE_OUTPUT_NUMBER_OF_STATES;
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)errorCode;
if (deviceInstance != g_deviceInstance) {
return false;
}
// Commandable outputs: the stack asks "is this Priority_Array slot null?" with
// the boolean getter. Answer true (1) for a relinquished slot, false (0) for a
// commanded one. This is how the stack knows which slots to skip when computing
// Present_Value and how it encodes the NULLs in a ReadProperty of the array.
const Commandable* c = GetCommandable(objectType, objectInstance);
if (c != NULL && propertyIdentifier == PROPERTY_IDENTIFIER_PRIORITY_ARRAY &&
useArrayIndex && propertyArrayIndex >= 1 &&
propertyArrayIndex <= BACNET_PRIORITY_ARRAY_SIZE) {
*value = !c->isSet[propertyArrayIndex - 1];
return true;
}
// Out_Of_Service is a required property of every input and output object and of
// the Network Port. This example never takes anything out of service: 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_ANALOG_OUTPUT ||
objectType == OBJECT_TYPE_BINARY_OUTPUT ||
objectType == OBJECT_TYPE_MULTI_STATE_OUTPUT ||
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)propertyArrayIndex;
(void)errorCode;
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_ANALOG_OUTPUT && objectInstance == ANALOG_OUTPUT_INSTANCE) {
return ReturnCharacterString("Chartreuse", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_BINARY_OUTPUT && objectInstance == BINARY_OUTPUT_INSTANCE) {
return ReturnCharacterString("Fuchsia", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == OBJECT_TYPE_MULTI_STATE_OUTPUT && objectInstance == MULTI_STATE_OUTPUT_INSTANCE) {
return ReturnCharacterString("Indigo", 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. Property "set" callbacks - DS-WP-B (identical to B-SA, which defines them)
//
// The stack calls these when a client sends WriteProperty to a commandable
// output's Present_Value. The value arrives already decoded into the matching
// data type, together with the priority (1..16) the client wrote at. We store it
// in the object's Priority_Array; the stack recomputes Present_Value from the
// array on the next read. A WriteProperty of NULL relinquishes a slot and arrives
// through SetPropertyNull instead.
//
// Return true when we accept the write; return false (optionally setting
// *errorCode) to reject it, and the stack answers with a BACnet Error-PDU.
// -----------------------------------------------------------------------------
// REAL write - Analog Output 1 (Chartreuse) Present_Value.
bool SetPropertyReal(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
const float value, const bool useArrayIndex,
const uint32_t propertyArrayIndex, const uint8_t priority,
uint32_t* errorCode) {
(void)useArrayIndex;
(void)propertyArrayIndex;
if (deviceInstance != g_deviceInstance) {
return false;
}
Commandable* c = GetCommandable(objectType, objectInstance);
if (c != NULL && objectType == OBJECT_TYPE_ANALOG_OUTPUT &&
propertyIdentifier == PROPERTY_IDENTIFIER_PRESENT_VALUE) {
// The setpoint must lie in Min_Pres_Value .. Max_Pres_Value, the range this
// object serves. Reject anything outside it with value-out-of-range,
// exactly as the Binary and Multi-State Output setters below do for
// their fixed ranges - nothing is stored, so the output does not move.
if (value < ANALOG_OUTPUT_MIN_PRES_VALUE || value > ANALOG_OUTPUT_MAX_PRES_VALUE) {
*errorCode = ERROR_CODE_VALUE_OUT_OF_RANGE;
return false;
}
CommandWrite(c, priority, (double)value);
printf("WriteProperty: Analog Output %u (Chartreuse) <- %.2f @ priority %u\n",
objectInstance, value, EffectivePriority(priority));
return true;
}
return false;
}
// ENUMERATED write - Binary Output 1 (Fuchsia) Present_Value (0/1).
bool SetPropertyEnumerated(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
const uint32_t value, const bool useArrayIndex,
const uint32_t propertyArrayIndex, const uint8_t priority,
uint32_t* errorCode) {
(void)useArrayIndex;
(void)propertyArrayIndex;
if (deviceInstance != g_deviceInstance) {
return false;
}
Commandable* c = GetCommandable(objectType, objectInstance);
if (c != NULL && objectType == OBJECT_TYPE_BINARY_OUTPUT &&
propertyIdentifier == PROPERTY_IDENTIFIER_PRESENT_VALUE) {
// A Binary Output's Present_Value is 0 (inactive) or 1 (active). Reject
// anything else with value-out-of-range - validating the written value is
// part of being a conformant DS-WP-B device.
if (value > 1) {
*errorCode = ERROR_CODE_VALUE_OUT_OF_RANGE;
return false;
}
CommandWrite(c, priority, (double)value);
printf("WriteProperty: Binary Output %u (Fuchsia) <- %s @ priority %u\n",
objectInstance, value ? "active" : "inactive", EffectivePriority(priority));
return true;
}
return false;
}
// UNSIGNED write - Multi-State Output 1 (Indigo) Present_Value (state 1..3).
bool SetPropertyUnsignedInteger(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
const uint32_t value, const bool useArrayIndex,
const uint32_t propertyArrayIndex, const uint8_t priority,
uint32_t* errorCode) {
(void)useArrayIndex;
(void)propertyArrayIndex;
if (deviceInstance != g_deviceInstance) {
return false;
}
Commandable* c = GetCommandable(objectType, objectInstance);
if (c != NULL && objectType == OBJECT_TYPE_MULTI_STATE_OUTPUT &&
propertyIdentifier == PROPERTY_IDENTIFIER_PRESENT_VALUE) {
// A Multi-State Output's Present_Value is a state number in 1..Number_Of_States.
// Reject anything outside that range with value-out-of-range.
if (value < 1 || value > MULTI_STATE_OUTPUT_NUMBER_OF_STATES) {
*errorCode = ERROR_CODE_VALUE_OUT_OF_RANGE;
return false;
}
CommandWrite(c, priority, (double)value);
printf("WriteProperty: Multi-State Output %u (Indigo) <- state %u @ priority %u\n",
objectInstance, value, EffectivePriority(priority));
return true;
}
return false;
}
// NULL write - relinquish a commandable output's Present_Value at a priority. The
// stack routes a WriteProperty of NULL here (one callback for every data type).
bool SetPropertyNull(const uint32_t deviceInstance, const uint16_t objectType,
const uint32_t objectInstance, const uint32_t propertyIdentifier,
const bool useArrayIndex, const uint32_t propertyArrayIndex,
const uint8_t priority, uint32_t* errorCode) {
(void)useArrayIndex;
(void)propertyArrayIndex;
(void)errorCode;
if (deviceInstance != g_deviceInstance) {
return false;
}
Commandable* c = GetCommandable(objectType, objectInstance);
if (c != NULL && propertyIdentifier == PROPERTY_IDENTIFIER_PRESENT_VALUE) {
CommandRelinquish(c, priority);
printf("WriteProperty: relinquished %s %u @ priority %u\n",
objectType == OBJECT_TYPE_ANALOG_OUTPUT ? "Analog Output" :
objectType == OBJECT_TYPE_BINARY_OUTPUT ? "Binary Output" :
"Multi-State Output",
objectInstance, EffectivePriority(priority));
return true;
}
return false;
}
// -----------------------------------------------------------------------------
// 2c. DeviceCommunicationControl callback - the B-ASC addition (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,