Repository navigation
Expand file tree
/
Copy pathmain.cpp
More file actions
2444 lines (2308 loc) · 133 KB
/
Copy pathmain.cpp
File metadata and controls
2444 lines (2308 loc) · 133 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-LS (BACnet Lighting Supervisor) - C++
//
// This example implements the BACnet "B-LS" (Lighting Supervisor) profile with
// the CAS BACnet Stack. It is seeded from the B-LD (Lighting Device) example -
// same base objects, same Lighting Output "Jade" - plus the three features that
// make a device a SUPERVISOR rather than a single light: Channel (to fan a write
// out to several targets, including a remote device), WriteGroup (the service a
// lighting system uses to command a whole scene at once), and Schedule/Calendar
// (to run a scene automatically, including writing to a remote device on a
// timer). This repo is the series' CANONICAL implementation of those three:
//
// F-CHANNEL - Channel object + AddObjectPropertyReferenceToChannel
// F-EXTWRITE - a remote (cross-device) object-property reference
// F-SCHED-E - a Schedule that executes a write (here, to Channel 1)
//
// ANSI/ASHRAE 135-2024 Annex L.11.1: "A B-LS is any device that implements
// Channel objects and optionally Lighting Output and/or Binary Lighting Output
// objects with the ability to forward channel writes to other BACnet devices."
// That forwarding - one WriteGroup reaching Jade AND device 389016, with
// Write_Status saying whether every write worked - is what tests/ proves.
// A B-LS must support:
//
// DS-RP-B - respond to ReadProperty requests,
// DS-WP-A - initiate WriteProperty requests (the A-side: the stack's
// WriteProperty to device 389016 when Channel 1 fans out),
// DS-WP-B - allow other devices to WriteProperty its objects,
// DS-WG-E-B - EXECUTE an incoming WriteGroup request (NEW - the defining
// BIBB): a client names a Group/Channel and this device fans
// the write out to every object in the matching Channel's
// List_Of_Object_Property_References.
// DS-ALO-A - initiate a write to another device's (lighting) output -
// satisfied by the same A-side WriteProperty as DS-WP-A.
// SCHED-E-B - EXECUTE a Schedule (NEW): a Schedule object drives a write,
// including to a remote device, on a time-of-day/exception basis.
// DM-DDB-A/B - initiate AND answer Who-Is/I-Am (the A-side is new: this
// device sends Who-Is itself to find the remote target - see
// the 'd' key),
// DM-DOB-B - answer Who-Has with I-Have (handled by the stack),
// DM-DCC-B - respond to DeviceCommunicationControl,
// DM-TS-B / DM-UTC-B - accept both local and UTC TimeSynchronization.
//
// It does NOT require COV, alarms, trending, or backup/restore, so this example
// leaves those off.
//
// HOW THE REMOTE WRITE ACTUALLY HAPPENS - READ THIS BEFORE COPYING THE PATTERN.
// Earlier revisions of the CAS BACnet Stack had a pair of exports,
// RegisterCallbackChannelSendWritePropertyToRemote and
// RegisterCallbackScheduleSendWritePropertyToRemote, that asked the APPLICATION
// to perform the outbound WriteProperty by hand. VERIFIED AGAINST THE PIN
// (submodules/cas-bacnet-stack/source/CASBACnetStackDLL.h, "Sprint 82" comment
// just above the debug-message section): both exports were REMOVED. The stack
// now does the remote write itself, automatically, via an internal resolver
// called the Device_Address_Binding (DAB): once AddObjectPropertyReferenceToChannel
// / AddScheduleObjectPropertyReference name a reference with refUseDeviceIdentifier
// = true and a remote refDeviceInstance, the stack fans a WriteGroup or a
// Schedule-driven write out to that remote device on its own, resolving its
// address from whatever I-Am traffic the DAB has seen (fed by a periodic
// Who-Is heartbeat AND by any Who-Is this application sends itself). THIS
// EXAMPLE DOES NOT REGISTER EITHER CALLBACK, because neither exists any more -
// registering a removed export would not compile. The 'd' key below sends a
// manual Who-Is so the DAB can resolve the remote device on demand, for the demo.
//
// The device keeps the same base objects as B-SS and adds a Lighting Output
// (from B-LD), a Channel, a Schedule, and a Calendar. Each object has a colour
// name (the convention shared across this example series):
//
// Device 389017 "Chipkin Example B-LS" (instance configurable with --deviceID)
// Analog Input 1 "Bronze" (REAL, degrees Celsius; read-only)
// Binary Input 1 "Emerald" (active / inactive; read-only)
// Multi-State Input 1 "Hot Pink" (state 1..3; read-only)
// Analog Output 1 "Chartreuse" (REAL setpoint; WRITABLE, commandable)
// Binary Output 1 "Fuchsia" (active / inactive; WRITABLE, commandable)
// Multi-State Output 1 "Indigo" (state 1..3; WRITABLE, commandable)
// Lighting Output 1 "Jade" (REAL 0-100%; WRITABLE, commandable)
// Channel 1 "Garnet" (BACnetChannelValue REAL; WRITABLE,
// commandable - the F-CHANNEL addition;
// fans WriteGroup out to Jade AND a
// remote object - the F-EXTWRITE addition)
// Schedule 1 "Saffron" (writes Channel 1 on a timer - the
// F-SCHED-E addition)
// Calendar 1 "Cream" (a real, independently-readable Calendar
// object - see the Date_List note in
// TODO.md for the one documented gap)
// Network Port 1 "Vermilion" (the BACnet/IP port - required)
//
// 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. Channel 1 uses this SAME mechanism, but
// dispatched differently - see the F-CHANNEL section for the exact callback shape.
//
// 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, w = manually fire a demo WriteGroup
// (writes Channel 1, which fans out to Jade + the remote reference), d = send a
// demo Who-Is so the DAB can resolve the remote device by address (F-EXTWRITE).
// Command line: --port <n>, --deviceID <n>.
//
// 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 "CASBACnetStackPackHelpers.h" // CASPropertyBuffer_* - packs the SendWriteProperty /
// SendWriteGroup buffers for the 'w'/'d' demo keys below.
#include <stdio.h>
#include <string.h>
#include <string>
#include <time.h> // F-TIMESYNC: InitSyncedDateTimeFromHost() seeds Local_Date/Local_Time
#if defined(_WIN32)
#include <windows.h> // Sleep()
#else
#include <unistd.h> // usleep()
#endif
using namespace CASBACnetStackExampleConstants;
// -----------------------------------------------------------------------------
// 0. Lighting constants used only by this example
//
// The shared common/CASBACnetStackExampleConstants.h carries the values every
// example needs. The lighting values below are specific to this profile, so they
// live here - same style, citing the stack header that owns the full enumeration.
// -----------------------------------------------------------------------------
// -- Lighting Output object type (Object_Type enumeration) -------------------
// Full list: submodules/cas-bacnet-stack/source/BACnetObjectType.h
static const uint16_t OBJECT_TYPE_LIGHTING_OUTPUT = 54;
// -- F-CHANNEL / F-SCHED-E object types - specific to THIS profile, so declared
// here rather than in the series-wide common/CASBACnetStackExampleConstants.h
// (verified against submodules/cas-bacnet-stack/source/BACnetObjectType.h at
// the pin: calendar = 6, channel = 53, schedule = 17).
static const uint16_t OBJECT_TYPE_CALENDAR = 6;
static const uint16_t OBJECT_TYPE_CHANNEL = 53;
static const uint16_t OBJECT_TYPE_SCHEDULE = 17;
// -- WriteGroup service (Services_Supported enumeration) - specific to this
// profile. Verified against source/BACnetServicesSupported.h at the pin.
static const uint32_t SERVICE_WRITE_GROUP = 40;
// -- Channel property identifiers not already in the series-wide constants ---
// Full list: submodules/cas-bacnet-stack/source/BACnetPropertyIdentifier.h
static const uint32_t PROPERTY_IDENTIFIER_CHANNEL_NUMBER = 366;
static const uint32_t PROPERTY_IDENTIFIER_CONTROL_GROUPS = 367;
static const uint32_t PROPERTY_IDENTIFIER_LAST_PRIORITY = 369;
static const uint32_t PROPERTY_IDENTIFIER_WRITE_STATUS = 370;
// -- Lighting Output property identifiers (Property_Identifier enumeration) ---
// Full list: submodules/cas-bacnet-stack/source/BACnetPropertyIdentifier.h
static const uint32_t PROPERTY_IDENTIFIER_TRACKING_VALUE = 164;
static const uint32_t PROPERTY_IDENTIFIER_BLINK_WARN_ENABLE = 373;
static const uint32_t PROPERTY_IDENTIFIER_DEFAULT_FADE_TIME = 374;
static const uint32_t PROPERTY_IDENTIFIER_DEFAULT_RAMP_RATE = 375;
static const uint32_t PROPERTY_IDENTIFIER_DEFAULT_STEP_INCREMENT = 376;
static const uint32_t PROPERTY_IDENTIFIER_EGRESS_TIME = 377;
static const uint32_t PROPERTY_IDENTIFIER_IN_PROGRESS = 378;
static const uint32_t PROPERTY_IDENTIFIER_LIGHTING_COMMAND = 380;
static const uint32_t PROPERTY_IDENTIFIER_LIGHTING_COMMAND_DEFAULT_PRIORITY = 381;
static const uint32_t PROPERTY_IDENTIFIER_EGRESS_ACTIVE = 386;
// -- BACnetLightingOperation - what a Lighting_Command tells the light to DO --
// Full list: submodules/cas-bacnet-stack/source/BACnetLightingOperation.h
static const uint32_t LIGHTING_OPERATION_NONE = 0;
static const uint32_t LIGHTING_OPERATION_FADE_TO = 1;
static const uint32_t LIGHTING_OPERATION_RAMP_TO = 2;
static const uint32_t LIGHTING_OPERATION_STEP_UP = 3;
static const uint32_t LIGHTING_OPERATION_STEP_DOWN = 4;
static const uint32_t LIGHTING_OPERATION_STOP = 10;
// -- BACnetLightingInProgress - what the light is doing right now -------------
// Full list: submodules/cas-bacnet-stack/source/BACnetLightingInProgress.h
static const uint32_t LIGHTING_IN_PROGRESS_IDLE = 0;
static const uint32_t LIGHTING_IN_PROGRESS_FADE_ACTIVE = 1;
static const uint32_t LIGHTING_IN_PROGRESS_RAMP_ACTIVE = 2;
// NOTE: ENGINEERING_UNITS_PERCENT and SERVICE_TIME_SYNCHRONIZATION are declared
// in common/CASBACnetStackExampleConstants.h, the series-wide union - declaring
// them here as well would be an ambiguous symbol. Only values genuinely specific
// to THIS profile belong in this block.
// -----------------------------------------------------------------------------
// 1. Example + device configuration
// -----------------------------------------------------------------------------
static const char* APP_NAME = "BACnet B-LS (Lighting Supervisor) Example - C++";
static const char* APP_VERSION = "1.2.0";
// The device instance. BACnet requires this to be configurable, so it defaults
// to 389017 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 = 389017;
// The remote device this example writes to, for F-EXTWRITE: Channel 1 (Garnet)
// carries a reference to an object on this device. In the Wire Tests it is a
// Python Peer (tests/channel_fanout_test.py). On a real network it is any device
// 389016 with a Lighting Output 1 - a BACnetProfileExample-B-LD-CPP instance is
// the natural target (389016 is its default instance) - on the SAME subnet and
// UDP port: a BACnet/IP network is the devices sharing one UDP port, so a device
// on another port never hears this one's Who-Is. This example's DAB
// (Device_Address_Binding - see the header comment above) resolves its address
// once it sees an I-Am: the answer to the DAB's own Who-Is at start-up, or to
// the 'd' (DiscoverRemote) key's manual one.
static const uint32_t REMOTE_DEVICE_INSTANCE = 389016; // B-LD's device instance
static const uint16_t REMOTE_OBJECT_TYPE = 54; // OBJECT_TYPE_LIGHTING_OUTPUT
static const uint32_t REMOTE_OBJECT_INSTANCE = 1; // B-LD's Lighting Output 1 (Jade)
// ---- 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.
//
// THIS IS THE ONE THAT WILL BITE YOU. Object_Name must be unique across the
// whole BACnet internetwork, and here it is a COMPILE-TIME constant. The device
// instance is runtime-configurable via --deviceID, so it is easy to ship two
// units, configure their instances correctly, and still have BOTH announce
// Object_Name "Chipkin Example B-LS" - a spec violation, and a hard BTL failure. In a real
// product Object_Name must be per-unit configurable too: derive it from a serial
// number, DIP switches, a config file, or add a --deviceName argument.
static const char* DEVICE_NAME = "Chipkin Example B-LS";
// The Device object's Description. Change it to what YOUR device actually is;
// this string describes this tutorial and its Channel/Schedule fan-out.
static const char* DEVICE_DESCRIPTION =
"Chipkin CAS BACnet Stack Example - B-LS (Lighting Supervisor) profile. Supervises "
"a scene of lights through a Channel object that fans an incoming WriteGroup out to "
"a local Lighting Output and a remote device's object, plus a Schedule that writes "
"the Channel on a timer.";
// 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-LS";
// DeviceCommunicationControl password. A management station may include a password
// with a DeviceCommunicationControl (or ReinitializeDevice) request; the device
// accepts the command only if it matches. Set to NULL/empty to accept any request
// (no password required). Change this to your device's secret before shipping -
// it crosses the wire in PLAINTEXT, so treat it as a guard against accidents,
// not a security boundary.
static const char* DCC_PASSWORD = ""; // "" = no password required
// 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;
// The commandable OUTPUT objects (all instance 1) and their colour names. These
// are what make this a B-SA actuator: clients drive them with WriteProperty.
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. A real
// device would map the resolved Present_Value onto its physical output; here we
// just store the commands. 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
// --- The Lighting Output (the B-LD addition) ---------------------------------
static const uint32_t LIGHTING_OUTPUT_INSTANCE = 1; // "Jade"
// Its Present_Value is commandable exactly like the Analog Output above - a level
// in percent. Relinquish_Default 0.0 means "when nothing commands this light, it
// is off", which is the safe resting state for a luminaire.
static Commandable g_lightingOutput = { { false }, { 0 }, 0.0 };
// The Lighting Output's REQUIRED scalar properties. A real luminaire would drive
// its dimming hardware from these; here they are the device's configuration.
// Default_Fade_Time - fade duration (ms) used when a fadeTo omits fade-time
// Default_Ramp_Rate - percent per second used when a rampTo omits ramp-rate
// Default_Step_Increment - percent used when a stepUp/stepDown omits step-increment
// Egress_Time - the blink-warn "get out of the room" period, in SECONDS
// Lighting_Command_Default_Priority - the priority a Lighting_Command writes at
// when it does not name one (BACnet default is 16)
static const uint32_t LIGHTING_DEFAULT_FADE_TIME = 3000; // 3.0 s, in milliseconds
static const float LIGHTING_DEFAULT_RAMP_RATE = 10.0f; // 10 % per second
static const float LIGHTING_DEFAULT_STEP_INCREMENT = 5.0f; // 5 %
static const uint32_t LIGHTING_EGRESS_TIME = 30; // 30 s
static const uint32_t LIGHTING_COMMAND_DEFAULT_PRIORITY = 16;
// --- Channel 1 "Garnet" (F-CHANNEL / DS-WG-E-B) -------------------------------
// Its Present_Value is commandable exactly like the other outputs above - see
// the F-CHANNEL section below for why its GET/SET dispatch looks different.
static const uint32_t CHANNEL_INSTANCE = 1; // "Garnet"
static Commandable g_channel = { { false }, { 0 }, 0.0 }; // default off (0.0)
// Channel_Number and Control_Groups are REQUIRED *and writable* on a Channel,
// and (per BACnetStack_AddChannelObject's doc) the WriteGroup service reads
// them back through our own callbacks to decide whether THIS Channel matches an
// incoming WriteGroup request. One group is enough for this demo.
static uint16_t g_channelNumber = 1;
static uint32_t g_channelControlGroups[1] = { 1 };
static const uint32_t CHANNEL_CONTROL_GROUPS_COUNT = 1;
// Last_Priority: the priority the most recent successful write to Channel 1
// landed at. Updated in SetPropertyReal / SetPropertyNull below. cl. 12.53.6:
// "The initial value of Last_Priority shall be 16."
static uint32_t g_channelLastPriority = 16;
// Write_Status (cl. 12.53.7), BACnetWriteStatus. The stack does not keep it: it
// performs the Channel's writes to its members and tells this application how
// each one went (the Set callbacks for the local member, the SimpleACK / Error /
// Reject / Abort / Timeout hooks for the remote one), so the application keeps
// it - see section 2e.
static const uint32_t WRITE_STATUS_IDLE = 0;
static const uint32_t WRITE_STATUS_IN_PROGRESS = 1;
static const uint32_t WRITE_STATUS_SUCCESSFUL = 2;
static const uint32_t WRITE_STATUS_FAILED = 3;
static uint32_t g_channelWriteStatus = WRITE_STATUS_IDLE; // "shall be set to IDLE initially"
// cl. 12.53.5: a WriteProperty of Present_Value while Write_Status is IN_PROGRESS
// "shall cause a Result(-) ... 'Error Class' of OBJECT and an 'Error Code' of BUSY".
// Verified against source/BACnetErrorCode.h at the pin: busy (82).
static const uint32_t ERROR_CODE_BUSY = 82;
// --- Schedule 1 "Saffron" and Calendar 1 "Cream" (F-SCHED-E) ------------------
static const uint32_t SCHEDULE_INSTANCE = 1; // "Saffron"
static const uint32_t CALENDAR_INSTANCE = 1; // "Cream"
// The last Lighting_Command a client wrote. Reading Lighting_Command reports back
// what the light was last told to do, so we remember it. A real device would ALSO
// be running the fade/ramp this describes; this example stores the command and
// reports the resulting level, which is what makes the read/write pair legible
// without simulating dimming hardware.
struct LightingCommandState {
uint32_t operation;
bool useTargetLevel; float targetLevel;
bool useRampRate; float rampRate;
bool useStepIncrement; float stepIncrement;
bool useFadeTime; uint32_t fadeTime;
bool usePriority; uint32_t priority;
};
// Starts as `none` - the light has not been commanded yet. `none` is a real
// BACnetLightingOperation value (0), not a placeholder: it is what a Lighting
// Output reports before anything has driven it.
static LightingCommandState g_lightingCommand = {
LIGHTING_OPERATION_NONE,
false, 0.0f, false, 0.0f, false, 0.0f, false, 0, false, 0
};
// Is the light mid-fade / mid-ramp? A real device would set this while its dimming
// hardware is moving and clear it on arrival. This example completes every command
// instantly, so it reports the honest answer: idle.
static uint32_t g_lightingInProgress = LIGHTING_IN_PROGRESS_IDLE;
// 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;
}
// The Lighting Output is commandable in exactly the same way as the Analog
// Output - a REAL level resolved from the Priority_Array. Adding it here is all
// it takes to give it the whole commandable mechanism.
if (objectType == OBJECT_TYPE_LIGHTING_OUTPUT && objectInstance == LIGHTING_OUTPUT_INSTANCE) {
return &g_lightingOutput;
}
// F-CHANNEL: Channel 1's Present_Value is ALSO a REAL resolved from a
// Priority_Array the application holds - see BACnetStack_AddChannelObject's
// doc. The GET side reads through this exact same
// GetCommandable()+ReadPrioritySlot() mechanism as every other commandable
// object; the SET side has its own arm in SetPropertyReal / SetPropertyNull
// below (two dispatch shapes, Write_Status, BUSY).
if (objectType == OBJECT_TYPE_CHANNEL && objectInstance == CHANNEL_INSTANCE) {
return &g_channel;
}
return NULL;
}
// Resolve the light's current level the way the stack does: highest-priority
// commanded slot, else Relinquish_Default. Used to serve Tracking_Value and to
// print the level - the stack serves Present_Value itself.
static float ResolveLightingLevel(const Commandable* c) {
for (uint32_t i = 0; i < BACNET_PRIORITY_ARRAY_SIZE; i++) {
if (c->isSet[i]) {
return (float)c->value[i];
}
}
return (float)c->relinquishDefault;
}
// A human-readable name for a BACnetLightingOperation, for the console log.
static const char* LightingOperationName(uint32_t operation) {
switch (operation) {
case LIGHTING_OPERATION_NONE: return "none";
case LIGHTING_OPERATION_FADE_TO: return "fadeTo";
case LIGHTING_OPERATION_RAMP_TO: return "rampTo";
case LIGHTING_OPERATION_STEP_UP: return "stepUp";
case LIGHTING_OPERATION_STEP_DOWN: return "stepDown";
case LIGHTING_OPERATION_STOP: return "stop";
default: return "(other)";
}
}
// 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;
}
// -----------------------------------------------------------------------------
// 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.
//
// WHAT false ACTUALLY DOES - the most important paragraph in this file, and the
// opposite of what most people assume. Returning false does NOT reliably produce
// a BACnet error. The stack only errors 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.
// 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
//
// 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.
// -----------------------------------------------------------------------------
// 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) {
// None of the arms below is an error condition (an unmatched (object, property)
// just means "let the stack substitute its default" - see the note at the top
// of this section) - so errorCode is deliberately never set in this callback.
(void)errorCode;
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);
// The Analog Output, Lighting Output, AND Channel all carry a REAL
// Present_Value, so all three serve their Priority_Array slots +
// Relinquish_Default through this one callback - the commandable mechanism
// does not care what the object is. (Channel's WRITE side is different -
// see SetPropertyReal below - but its READ side matches this exactly.)
if (c != NULL && (objectType == OBJECT_TYPE_ANALOG_OUTPUT ||
objectType == OBJECT_TYPE_LIGHTING_OUTPUT ||
objectType == OBJECT_TYPE_CHANNEL)) {
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;
}
}
// --- The Lighting Output's REQUIRED REAL properties ----------------------
if (objectType == OBJECT_TYPE_LIGHTING_OUTPUT && objectInstance == LIGHTING_OUTPUT_INSTANCE) {
switch (propertyIdentifier) {
case PROPERTY_IDENTIFIER_TRACKING_VALUE:
// Tracking_Value is what the light ACTUALLY is, as opposed to what it
// was commanded to be (Present_Value). They differ mid-fade on real
// hardware. This example completes commands instantly, so they agree -
// a real driver would report its measured//live level here.
*value = ResolveLightingLevel(&g_lightingOutput);
return true;
case PROPERTY_IDENTIFIER_DEFAULT_RAMP_RATE:
*value = LIGHTING_DEFAULT_RAMP_RATE;
return true;
case PROPERTY_IDENTIFIER_DEFAULT_STEP_INCREMENT:
*value = LIGHTING_DEFAULT_STEP_INCREMENT;
return true;
default:
break;
}
}
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) {
*value = 1; // active
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 not in the stack's
// valueShouldBeInitialized list and so falls through to a substituted default
// (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;
}
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;
}
// --- The Lighting Output's REQUIRED enumerated properties ----------------
if (objectType == OBJECT_TYPE_LIGHTING_OUTPUT && objectInstance == LIGHTING_OUTPUT_INSTANCE) {
if (propertyIdentifier == PROPERTY_IDENTIFIER_IN_PROGRESS) {
// What the light is doing right now: idle / fadeActive / rampActive.
// A real driver would report fadeActive while its hardware is moving and
// flip to idle on arrival; this example completes instantly, so idle.
*value = g_lightingInProgress;
return true;
}
// NB: a Lighting Output has NO Units property (its level is a dimensionless
// percent, and Units is not in the object's required or optional set), so
// there is deliberately no Units arm here - serving one would be dead code
// the stack never calls.
}
// --- Channel 1's REQUIRED Write_Status (F-CHANNEL) ------------------------
// BACnetWriteStatus: 0 = idle, 1 = in-progress, 2 = successful, 3 = failed
// (source/BACnetWriteStatus.h). Kept by section 2e from what happened to the
// Channel's last writes to its members, local and remote.
if (objectType == OBJECT_TYPE_CHANNEL && objectInstance == CHANNEL_INSTANCE &&
propertyIdentifier == PROPERTY_IDENTIFIER_WRITE_STATUS) {
*value = g_channelWriteStatus;
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;
}
}
// 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;
}
}
// --- The Lighting Output's REQUIRED unsigned properties ------------------
// These are the light's dimming defaults: what it does when a Lighting_Command
// names an operation but omits that operation's parameter (cl. 12.54.9). E.g. a
// fadeTo with no fade-time fades over Default_Fade_Time.
if (objectType == OBJECT_TYPE_LIGHTING_OUTPUT && objectInstance == LIGHTING_OUTPUT_INSTANCE) {
switch (propertyIdentifier) {
case PROPERTY_IDENTIFIER_DEFAULT_FADE_TIME:
*value = LIGHTING_DEFAULT_FADE_TIME; // milliseconds
return true;
case PROPERTY_IDENTIFIER_EGRESS_TIME:
*value = LIGHTING_EGRESS_TIME; // seconds - note: NOT milliseconds
return true;
case PROPERTY_IDENTIFIER_LIGHTING_COMMAND_DEFAULT_PRIORITY:
*value = LIGHTING_COMMAND_DEFAULT_PRIORITY;
return true;
default:
break;
}
}
// --- Channel 1's REQUIRED unsigned properties (F-CHANNEL) -----------------
// Channel_Number and Control_Groups are what let the WriteGroup service MATCH
// an incoming request to this Channel (see BACnetStack_AddChannelObject's doc
// comment) - an app that does not serve these two reports channel 0 / an
// empty group list and the Channel never matches. Last_Priority is the
// priority the most recent successful write landed at.
if (objectType == OBJECT_TYPE_CHANNEL && objectInstance == CHANNEL_INSTANCE) {
if (propertyIdentifier == PROPERTY_IDENTIFIER_CHANNEL_NUMBER) {
*value = g_channelNumber;
return true;
}
// Control_Groups is an array; the stack asks for element 0 (the count)
// first, then elements 1..N - the same convention as State_Text above.
if (propertyIdentifier == PROPERTY_IDENTIFIER_CONTROL_GROUPS && useArrayIndex) {
if (propertyArrayIndex == 0) {
*value = CHANNEL_CONTROL_GROUPS_COUNT;
return true;
}
if (propertyArrayIndex >= 1 && propertyArrayIndex <= CHANNEL_CONTROL_GROUPS_COUNT) {
*value = g_channelControlGroups[propertyArrayIndex - 1];
return true;
}
return false;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_LAST_PRIORITY) {
*value = g_channelLastPriority;
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_LIGHTING_OUTPUT ||
objectType == OBJECT_TYPE_CHANNEL ||
objectType == OBJECT_TYPE_SCHEDULE ||
objectType == OBJECT_TYPE_NETWORK_PORT)) {
*value = false;
return true;
}
// --- The Lighting Output's REQUIRED boolean properties -------------------
if (objectType == OBJECT_TYPE_LIGHTING_OUTPUT && objectInstance == LIGHTING_OUTPUT_INSTANCE) {
if (propertyIdentifier == PROPERTY_IDENTIFIER_BLINK_WARN_ENABLE) {
// Blink-warn is the "the lights are about to go out" flash that gives
// people Egress_Time to leave. Enabled here so the property is meaningful.
*value = true;
return true;
}
if (propertyIdentifier == PROPERTY_IDENTIFIER_EGRESS_ACTIVE) {
// True only while the egress timer is actually running (i.e. between a
// warn command and the lights going off). Nothing drives it in this
// example, so it is honestly false.
*value = false;
return true;
}
}
// --- Calendar 1's REQUIRED Present_Value (F-SCHED-E) ----------------------
// Present_Value is "is today in Date_List". See the TODO.md entry: this
// stack build has no customer-surface callback shape for Date_List itself
// (a BACnetLIST of BACnetCalendarEntry, a variable-length list of a CHOICE
// type - none of the typed Get callbacks fit it), so Date_List genuinely
// stays empty here, and Present_Value is honestly always false as a result.
if (objectType == OBJECT_TYPE_CALENDAR && objectInstance == CALENDAR_INSTANCE &&
propertyIdentifier == PROPERTY_IDENTIFIER_PRESENT_VALUE) {
*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) {
(void)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);
}
return false;