Repository navigation
Expand file tree
/
Copy pathProgram.cs
More file actions
937 lines (883 loc) · 51.4 KB
/
Copy pathProgram.cs
File metadata and controls
937 lines (883 loc) · 51.4 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
// 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-SS (BACnet Smart Sensor) - C#
//
// This example shows the *minimum* needed to implement the BACnet "B-SS"
// (Smart Sensor) device profile with the CAS BACnet Stack.
//
// "A B-SS is a simple sensing device with very limited resources." (ANSI/ASHRAE
// 135-2024, Annex L.4.5). Its Profile Function is the one Data Sharing line of
// that clause: "Ability to provide values for any of its BACnet objects upon
// request" - a client reads the sensor and gets what it is measuring NOW. Its
// only mandatory service is:
//
// DS-RP-B - respond to ReadProperty requests
//
// plus the ability to be discovered: it answers Who-Is with I-Am (handled
// automatically by the stack) and announces itself with an unsolicited I-Am on
// start-up.
//
// SIMULATION. This example has no real sensor, so section 2 below is a sensor
// Simulation that stands in for the physical world: the temperature ramps up and
// down, the binary input toggles and the multi-state input steps through its
// states, all on a timer. It is NOT part of the BACnet Surface - on a real device
// you replace SensorSimulationTick() with your hardware sampling. The Simulation
// also has one Failure Mode: a broken sensor wire, which this device reports the
// BACnet way, as Reliability OPEN_LOOP and the FAULT bit of Status_Flags
// (cl. 12.2.7, 12.2.9). A Wire Test breaks the "wire" through a test-only hook
// (SENSOR_FAULT_FILE_VARIABLE below); nothing on the network can.
//
// A B-SS does NOT require WriteProperty, COV, alarms/events, scheduling, or
// trending - so this example deliberately does NOT enable any of those. That is
// the whole point of a profile example: implement exactly what the profile
// asks for, nothing more.
//
// This is the C# edition of BACnetProfileExample-B-SS-CPP / -Node - same
// objects, same names, same section layout, so you can diff the three side by
// side. The systematic difference here is the binding mechanism: C# calls the
// native CAS BACnet Stack library directly through P/Invoke (DllImport
// declarations in the stack's adapter, compiled in place from submodules/cas-bacnet-stack/adapters/csharp/),
// not an embedded/compiled-from-source link (C++) or a native Node addon
// (Node). See README.md "How the C# binding differs" for what that means in
// practice - most importantly, THERE IS NO LoadBACnetFunctions() STEP: the
// C# adapter's functions are plain `[DllImport] static extern` declarations,
// resolved by the .NET runtime the first time one is called, not a runtime
// GetProcAddress dance the application drives itself. A missing native library
// would therefore fail as a DllNotFoundException on first use, not a checked
// LoadBACnetFunctions() return value - so Main() checks for the library file in
// the folder it runs from before any stack call, and prints what is missing and
// how to get it (the library is licensed separately and not in any release).
//
// The device exposes three read-only sensor objects plus the Network Port
// object that every BACnet device must have. Each object is given a colour name
// (a convention shared across this example series):
//
// Device 389001 "Chipkin Example B-SS" (instance configurable with --deviceID)
// Analog Input 1 "Bronze" (REAL, degrees Celsius; ramps 18.0 .. 26.0)
// Binary Input 1 "Emerald" (active / inactive; toggles every 3 s)
// Multi-State Input 1 "Hot Pink" (an enumerated state, 1..3; steps every 4 s)
// Network Port 1 "Vermilion" (the BACnet/IP port - required)
//
// To be a conformant BACnet device (Protocol_Revision 30) each object must
// expose its full set of REQUIRED properties. Most are generated by the stack
// (Object_Identifier, Object_Type, Status_Flags, Object_List, Protocol_*).
// Event_State is NOT generated - with no alarming configured it simply reads its
// datatype default of normal(0), correct by coincidence, not because the stack
// computes it. The handful that the application must supply are served by the
// Get*Property callbacks below, and a few are turned on with SetPropertyEnabled.
//
// Interactive keys (handled by the shared helper's console polling below):
// h = help, q = quit, up/down = nudge the simulated Analog Input 1 reading by
// +/-1.1. 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.
// =============================================================================
using System;
using System.Text;
using System.Runtime.InteropServices;
using CASBACnetStack;
using BACnetProfileExampleBSSCS.Common;
namespace BACnetProfileExampleBSSCS
{
internal static unsafe class Program
{
// -------------------------------------------------------------------
// 1. Example + device configuration
// -------------------------------------------------------------------
private const string APP_NAME = "BACnet B-SS (Smart Sensor) Example - C#";
private const string APP_VERSION = "1.1.0";
// The device instance. BACnet requires this to be configurable, so it
// defaults to 389001 and can be overridden on the command line with
// --deviceID. Keep it configurable in your product: it must be unique
// across the internetwork.
private static uint g_deviceInstance = 389001;
// ---- 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.
// ---------------------------------------------------------------------
// 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.
private const uint 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-SS" - 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.
private const string DEVICE_NAME = "Chipkin Example B-SS";
// The Device object's Description. Change it to what YOUR device
// actually is; this string describes this tutorial.
private const string DEVICE_DESCRIPTION =
"Chipkin CAS BACnet Stack example - B-SS (Smart Sensor) profile. " +
"Demonstrates DS-RP-B: ReadProperty plus Who-Is/I-Am with read-only sensor objects.";
// 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.
// APPLICATION_SOFTWARE_VERSION - this example's own real version;
// sourced directly from APP_VERSION above, so it can't
// drift from what --version reports.
// g_firmwareRevision - the underlying CAS BACnet Stack's REAL
// version (it names the platform underneath this app,
// not the app itself). Built once at start-up, right
// after the native library is confirmed loaded - see
// Main(). Not a compile-time constant because it is
// read from the stack at runtime via the same
// BACnetStack_GetAPI*Version() calls
// CASExampleHelper.PrintVersion() uses for the
// start-up banner.
private const string VENDOR_NAME = "Chipkin Automation Systems";
private const string MODEL_NAME = "CAS BACnet Stack Example - B-SS";
private const string APPLICATION_SOFTWARE_VERSION = APP_VERSION;
private static string g_firmwareRevision = "0.0.0.0";
// The sensor objects (all instance 1) and their colour names.
private const uint ANALOG_INPUT_INSTANCE = 1; // "Bronze"
private const uint BINARY_INPUT_INSTANCE = 1; // "Emerald"
private const uint MULTI_STATE_INPUT_INSTANCE = 1; // "Hot Pink"
private const uint MULTI_STATE_INPUT_NUMBER_OF_STATES = 3;
private static readonly string[] MULTI_STATE_TEXT = { "On", "Off", "Auto" };
// The Network Port object - every BACnet device must have one. It
// represents the BACnet/IP port this device communicates on.
private const uint NETWORK_PORT_INSTANCE = 1; // "Vermilion"
private const uint MAX_APDU_LENGTH = 1476; // BACnet/IP APDU length
// BACnet/IP addressing the Network Port reports. Filled in at start-up
// from the host's primary interface; the gateway is left unset
// (0.0.0.0) for this example.
private static byte[] g_ipAddress = { 0, 0, 0, 0 };
private static byte[] g_ipSubnetMask = { 0, 0, 0, 0 };
private static readonly byte[] g_ipDefaultGateway = { 0, 0, 0, 0 };
private static ushort g_bacnetIpUdpPort = 47808;
// The sensor objects' live values. The callbacks in section 3 only ever
// hand these back; section 2 (the Simulation) is the only thing that
// changes them. On a real device your hardware sampling updates them.
private static float g_analogInput1Value = 21.5f; // degrees Celsius
private static uint g_binaryInput1Value = 0; // 0 = inactive, 1 = active
private static uint g_multiStateInput1Value = 1; // 1..MULTI_STATE_INPUT_NUMBER_OF_STATES
// Analog Input 1's Reliability (cl. 12.2.9). NO_FAULT_DETECTED while the
// sensor is healthy; OPEN_LOOP while its (simulated) wire is broken. The
// stack builds the FAULT bit of Status_Flags from this property by itself
// (cl. 12.2.7: FAULT is "Logical TRUE (1) if the Reliability property is
// present and does not have a value of NO_FAULT_DETECTED") - the example
// only has to serve it.
private const uint RELIABILITY_NO_FAULT_DETECTED = 0;
private const uint RELIABILITY_OPEN_LOOP = 4;
private static uint g_analogInput1Reliability = RELIABILITY_NO_FAULT_DETECTED;
private static readonly SimpleUDP udp = new SimpleUDP();
// -------------------------------------------------------------------
// 2. The sensor Simulation (NOT part of the BACnet Surface)
//
// Stands in for the physical world. Once a second:
// - Analog Input 1 moves 0.5 C, ramping up to 26.0 and back down to 18.0
// - Binary Input 1 toggles every 3 s
// - Multi-State Input 1 steps 1 -> 2 -> 3 -> 1 every 4 s
// and the sensor wire is checked.
//
// The broken wire is a TEST-ONLY hook, not a feature: when the
// environment variable named by SENSOR_FAULT_FILE_VARIABLE names a file,
// the wire counts as broken for as long as that file exists. A Wire Test
// creates and deletes the file to break and repair the sensor. While the
// wire is broken the reading freezes at its last good value (a dead wire
// measures nothing new) and Reliability reads OPEN_LOOP; once the file is
// gone the reading resumes and Reliability returns to NO_FAULT_DETECTED.
// On a real device, set Reliability from your hardware's own diagnostics
// (an ADC reading on the rails, a sensor that stopped answering, ...).
// -------------------------------------------------------------------
private const string SENSOR_FAULT_FILE_VARIABLE = "CAS_EXAMPLE_TEST_SENSOR_FAULT_FILE";
private static string g_sensorFaultFile = ""; // read once at start-up; empty = no hook
private const float ANALOG_INPUT_LOWEST = 18.0f;
private const float ANALOG_INPUT_HIGHEST = 26.0f;
private const float ANALOG_INPUT_STEP = 0.5f;
private const long BINARY_INPUT_TOGGLE_SECONDS = 3;
private const long MULTI_STATE_INPUT_STEP_SECONDS = 4;
private static readonly System.Diagnostics.Stopwatch g_simulationClock = System.Diagnostics.Stopwatch.StartNew();
private static long g_simulationLastSecond = 0;
private static float g_analogInputDirection = 1.0f;
private static bool SensorWireBroken()
{
return g_sensorFaultFile.Length > 0 && System.IO.File.Exists(g_sensorFaultFile);
}
private static void SensorSimulationTick()
{
long secondsRunning = (long)g_simulationClock.Elapsed.TotalSeconds;
if (secondsRunning == g_simulationLastSecond)
{
return;
}
g_simulationLastSecond = secondsRunning;
uint reliability = SensorWireBroken() ? RELIABILITY_OPEN_LOOP : RELIABILITY_NO_FAULT_DETECTED;
if (reliability != g_analogInput1Reliability)
{
g_analogInput1Reliability = reliability;
Console.WriteLine("Simulation: Analog Input 1 (Bronze) sensor wire " +
(reliability == RELIABILITY_OPEN_LOOP ? "broken - Reliability OPEN_LOOP" : "repaired - Reliability NO_FAULT_DETECTED"));
}
if (g_analogInput1Reliability == RELIABILITY_NO_FAULT_DETECTED)
{
if (g_analogInput1Value >= ANALOG_INPUT_HIGHEST)
{
g_analogInputDirection = -1.0f;
}
else if (g_analogInput1Value <= ANALOG_INPUT_LOWEST)
{
g_analogInputDirection = 1.0f;
}
g_analogInput1Value += g_analogInputDirection * ANALOG_INPUT_STEP;
}
if (secondsRunning % BINARY_INPUT_TOGGLE_SECONDS == 0)
{
g_binaryInput1Value = g_binaryInput1Value != 0 ? 0u : 1u;
}
if (secondsRunning % MULTI_STATE_INPUT_STEP_SECONDS == 0)
{
g_multiStateInput1Value = g_multiStateInput1Value % MULTI_STATE_INPUT_NUMBER_OF_STATES + 1;
}
}
// -------------------------------------------------------------------
// 3. 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 a trailing
// uint* 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.
// 2. return false and SET *errorCode -> "This read fails,
// with THIS BACnet error." The client gets exactly that Error-PDU.
//
// WHAT false-WITHOUT-AN-ERROR-CODE ACTUALLY DOES. 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. 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. 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. 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? See TUTORIAL.md "Add a second analog input" before
// you do - a half-added object does NOT fail loudly (see above).
// -------------------------------------------------------------------
// REAL (floating point) - the Analog Input's Present_Value.
private static bool GetPropertyReal(
uint deviceInstance, ushort objectType, uint objectInstance, uint propertyIdentifier,
float* value, bool useArrayIndex, uint propertyArrayIndex, uint* errorCode)
{
if (deviceInstance == g_deviceInstance &&
objectType == CASBACnetStackAdapter.OBJECT_TYPE_ANALOG_INPUT &&
objectInstance == ANALOG_INPUT_INSTANCE &&
propertyIdentifier == CASBACnetStackAdapter.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() call, so blocking it delays all BACnet
// processing. Sample the sensor on a timer/another thread and
// just hand back the latest value from here.
*value = g_analogInput1Value;
return true;
}
return false;
}
// ENUMERATED - the Binary Input's Present_Value (0 = inactive, 1 =
// active), the Analog Input's Units (degrees Celsius), and the Network
// Port's BACnet_IP_Mode.
private static bool GetPropertyEnumerated(
uint deviceInstance, ushort objectType, uint objectInstance, uint propertyIdentifier,
uint* value, bool useArrayIndex, uint propertyArrayIndex, uint* errorCode)
{
if (deviceInstance != g_deviceInstance)
{
return false;
}
if (objectType == CASBACnetStackAdapter.OBJECT_TYPE_BINARY_INPUT &&
objectInstance == BINARY_INPUT_INSTANCE)
{
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_PRESENT_VALUE)
{
// ON REAL HARDWARE: return your cached input state here -
// the same rule as GetPropertyReal above applies.
*value = g_binaryInput1Value;
return true;
}
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_POLARITY)
{
*value = CASBACnetStackExampleConstants.POLARITY_NORMAL; // required property of a Binary Input
return true;
}
}
if (objectType == CASBACnetStackAdapter.OBJECT_TYPE_ANALOG_INPUT &&
objectInstance == ANALOG_INPUT_INSTANCE)
{
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_UNITS)
{
*value = CASBACnetStackExampleConstants.ENGINEERING_UNITS_DEGREES_CELSIUS;
return true;
}
// Reliability (enabled in Main()). The stack also reads it through
// this callback to build Status_Flags, so once enabled it must
// always answer: if it declines, the stack silently substitutes 0
// (no-fault-detected), and the sensor reports itself healthy forever.
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_RELIABILITY)
{
*value = g_analogInput1Reliability;
return true;
}
}
if (objectType == CASBACnetStackAdapter.OBJECT_TYPE_NETWORK_PORT &&
objectInstance == NETWORK_PORT_INSTANCE &&
propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_BACNETIPMODE)
{
*value = CASBACnetStackExampleConstants.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).
private static bool GetPropertyUnsignedInteger(
uint deviceInstance, ushort objectType, uint objectInstance, uint propertyIdentifier,
uint* value, bool useArrayIndex, uint propertyArrayIndex, uint* errorCode)
{
if (deviceInstance != g_deviceInstance)
{
return false;
}
if (objectType == CASBACnetStackAdapter.OBJECT_TYPE_MULTI_STATE_INPUT &&
objectInstance == MULTI_STATE_INPUT_INSTANCE)
{
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_PRESENT_VALUE)
{
*value = g_multiStateInput1Value; // valid range is 1..Number_Of_States
return true;
}
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_NUMBEROFSTATES)
{
*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
// GetPropertyCharacterString.
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_STATETEXT &&
useArrayIndex && propertyArrayIndex == 0)
{
*value = MULTI_STATE_INPUT_NUMBER_OF_STATES;
return true;
}
}
if (objectType == CASBACnetStackAdapter.OBJECT_TYPE_DEVICE && objectInstance == g_deviceInstance &&
propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_VENDOR_IDENTIFIER)
{
*value = VENDOR_IDENTIFIER;
return true;
}
if (objectType == CASBACnetStackAdapter.OBJECT_TYPE_NETWORK_PORT && objectInstance == NETWORK_PORT_INSTANCE)
{
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_APDULENGTH)
{
*value = MAX_APDU_LENGTH;
return true;
}
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_REFERENCEPORT)
{
*value = CASBACnetStackExampleConstants.NETWORK_PORT_REFERENCE_PORT_NONE;
return true;
}
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_BACNETIPUDPPORT)
{
*value = g_bacnetIpUdpPort;
return true;
}
}
return false;
}
// BOOLEAN - Out_Of_Service is a required property of every input object
// and of the Network Port. This is a read-only sensor, so nothing is
// ever out of service: always false.
private static bool GetPropertyBool(
uint deviceInstance, ushort objectType, uint objectInstance, uint propertyIdentifier,
bool* value, bool useArrayIndex, uint propertyArrayIndex, uint* errorCode)
{
if (deviceInstance != g_deviceInstance)
{
return false;
}
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_OUT_OF_SERVICE &&
(objectType == CASBACnetStackAdapter.OBJECT_TYPE_ANALOG_INPUT ||
objectType == CASBACnetStackAdapter.OBJECT_TYPE_BINARY_INPUT ||
objectType == CASBACnetStackAdapter.OBJECT_TYPE_MULTI_STATE_INPUT ||
objectType == CASBACnetStackAdapter.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.
private static bool GetPropertyOctetString(
uint deviceInstance, ushort objectType, uint objectInstance, uint propertyIdentifier,
byte* value, uint* valueElementCount, uint maxElementCount,
bool useArrayIndex, uint propertyArrayIndex, uint* errorCode)
{
if (deviceInstance != g_deviceInstance ||
objectType != CASBACnetStackAdapter.OBJECT_TYPE_NETWORK_PORT ||
objectInstance != NETWORK_PORT_INSTANCE ||
maxElementCount < 4)
{
return false;
}
byte[] source;
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_IPADDRESS)
{
source = g_ipAddress;
}
else if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_IPSUBNETMASK)
{
source = g_ipSubnetMask;
}
else if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_IPDEFAULTGATEWAY)
{
source = g_ipDefaultGateway;
}
else
{
return false;
}
Marshal.Copy(source, 0, (IntPtr)value, 4);
*valueElementCount = 4;
return true;
}
// Small helper: copy a C# string into the stack's character-string
// buffer as UTF-8 and set the element count + encoding. Returns true
// (so callers can `return`).
private static bool ReturnCharacterString(
string text, byte* value, uint* valueElementCount, uint maxElementCount, byte* encodingType)
{
byte[] bytes = Encoding.UTF8.GetBytes(text);
uint length = (uint)bytes.Length;
if (length > maxElementCount)
{
// Truncate SILENTLY to fit the stack's buffer - see the C++
// edition's comment on MAX_CHARACTER_STRING_SIZE for why this
// never trips for the strings this example serves, and why it
// would matter on an embedded build.
length = maxElementCount;
}
Marshal.Copy(bytes, 0, (IntPtr)value, (int)length);
*valueElementCount = length;
*encodingType = CASBACnetStackExampleConstants.CHARACTER_STRING_ENCODING_UTF8;
return true;
}
// CHARACTER STRING - Object_Name for each object, and the device Description.
private static bool GetPropertyCharacterString(
uint deviceInstance, ushort objectType, uint objectInstance, uint propertyIdentifier,
byte* value, uint* valueElementCount, uint maxElementCount, byte* encodingType,
bool useArrayIndex, uint propertyArrayIndex, uint* 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 == CASBACnetStackAdapter.OBJECT_TYPE_MULTI_STATE_INPUT &&
objectInstance == MULTI_STATE_INPUT_INSTANCE &&
propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_STATETEXT && useArrayIndex)
{
if (propertyArrayIndex >= 1 && propertyArrayIndex <= MULTI_STATE_INPUT_NUMBER_OF_STATES)
{
return ReturnCharacterString(MULTI_STATE_TEXT[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. (At this Stack Pin the stack
// also checks n against the length GetPropertyUnsignedInteger
// serves for index 0 and answers invalid-array-index before it
// gets here; this line is what still answers correctly if that
// length and this table ever disagree - without it the client
// would silently receive an empty string.)
*errorCode = CASBACnetStackAdapter.ERROR_INVALID_ARRAY_INDEX;
return false;
}
// Object_Name - the colour name for each object.
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_OBJECT_NAME)
{
if (objectType == CASBACnetStackAdapter.OBJECT_TYPE_DEVICE && objectInstance == g_deviceInstance)
{
return ReturnCharacterString(DEVICE_NAME, value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == CASBACnetStackAdapter.OBJECT_TYPE_ANALOG_INPUT && objectInstance == ANALOG_INPUT_INSTANCE)
{
return ReturnCharacterString("Bronze", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == CASBACnetStackAdapter.OBJECT_TYPE_BINARY_INPUT && objectInstance == BINARY_INPUT_INSTANCE)
{
return ReturnCharacterString("Emerald", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == CASBACnetStackAdapter.OBJECT_TYPE_MULTI_STATE_INPUT && objectInstance == MULTI_STATE_INPUT_INSTANCE)
{
return ReturnCharacterString("Hot Pink", value, valueElementCount, maxElementCount, encodingType);
}
if (objectType == CASBACnetStackAdapter.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 == CASBACnetStackAdapter.OBJECT_TYPE_DEVICE && objectInstance == g_deviceInstance)
{
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_DESCRIPTION)
{
return ReturnCharacterString(DEVICE_DESCRIPTION, value, valueElementCount, maxElementCount, encodingType);
}
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_VENDOR_NAME)
{
return ReturnCharacterString(VENDOR_NAME, value, valueElementCount, maxElementCount, encodingType);
}
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_MODEL_NAME)
{
return ReturnCharacterString(MODEL_NAME, value, valueElementCount, maxElementCount, encodingType);
}
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_FIRMWAREREVISION)
{
return ReturnCharacterString(g_firmwareRevision, value, valueElementCount, maxElementCount, encodingType);
}
if (propertyIdentifier == CASBACnetStackAdapter.PROPERTY_IDENTIFIER_APPLICATIONSOFTWAREVERSION)
{
return ReturnCharacterString(APPLICATION_SOFTWARE_VERSION, value, valueElementCount, maxElementCount, encodingType);
}
}
return false;
}
// Delegate instances registered with the stack. Kept in static fields
// (rather than passed as bare method-group conversions) so the CLR
// cannot garbage-collect them while native code may still call back
// into their marshalling thunks - see the same note in CASExampleHelper.cs.
private static readonly CASBACnetStackAdapter.FPCallbackGetPropertyReal getPropertyRealDelegate = GetPropertyReal;
private static readonly CASBACnetStackAdapter.FPCallbackGetPropertyEnumerated getPropertyEnumeratedDelegate = GetPropertyEnumerated;
private static readonly CASBACnetStackAdapter.FPCallbackGetPropertyUnsignedInteger getPropertyUnsignedIntegerDelegate = GetPropertyUnsignedInteger;
private static readonly CASBACnetStackAdapter.FPCallbackGetPropertyBool getPropertyBoolDelegate = GetPropertyBool;
private static readonly CASBACnetStackAdapter.FPCallbackGetPropertyOctetString getPropertyOctetStringDelegate = GetPropertyOctetString;
private static readonly CASBACnetStackAdapter.FPCallbackGetPropertyCharacterString getPropertyCharacterStringDelegate = GetPropertyCharacterString;
// Printed (to stderr) when the CAS BACnet Stack library is missing or
// cannot be loaded. The example cannot do anything without it. The text is
// the same in every language port of this example.
private static void PrintStackLibraryMissing(string stackLibrary, string[] searched, string detail)
{
System.IO.TextWriter error = Console.Error;
error.WriteLine("ERROR: This example cannot run without the CAS BACnet Stack library.");
error.WriteLine();
error.WriteLine(" Missing file: " + stackLibrary);
for (int i = 0; i < searched.Length; i++)
{
error.WriteLine((i == 0 ? " Looked in: " : " ") + searched[i]);
}
error.WriteLine();
error.WriteLine("The CAS BACnet Stack is a commercially licensed (paid) library from Chipkin.");
error.WriteLine("It is not included in this release, so the example cannot start without it.");
error.WriteLine();
error.WriteLine(" - To license the CAS BACnet Stack: https://store.chipkin.com/contact-us");
error.WriteLine(" - Licensed customers: build the library from the cas-bacnet-stack submodule");
error.WriteLine(" (see README.md) and place " + stackLibrary + " next to this executable.");
if (detail != null)
{
error.WriteLine();
error.WriteLine("Details: " + detail);
}
}
// -------------------------------------------------------------------
// 4. Main
// -------------------------------------------------------------------
private static int Main(string[] args)
{
// --- --help / --version ----------------------------------------------
// These print and exit, and work without the native CAS BACnet Stack
// library (PrintVersion() then says the stack version is not available).
if (CASExampleHelper.HandleHelpAndVersionArgs(args, APP_NAME, APP_VERSION))
{
return 0;
}
ushort port = CASExampleHelper.ParsePortArg(args, 47808);
g_deviceInstance = CASExampleHelper.ParseDeviceIdArg(args, g_deviceInstance);
// --- Is the CAS BACnet Stack library here? -----------------------------
// Unlike the C++/Node editions there is no LoadBACnetFunctions() to
// call: the C# adapter's BACnetStack_* methods are plain
// `[DllImport] static extern` declarations, and the .NET runtime
// resolves the native library the first time one of them is called.
// The library is the separately licensed CAS BACnet Stack and is NOT
// part of this example or its releases, so check for it up front, in
// the folder this program runs from, before any stack call - and say
// plainly what is missing and how to get it.
string stackLibrary = CASExampleHelper.StackLibraryFileName();
string[] searched = { AppContext.BaseDirectory };
if (!System.IO.File.Exists(System.IO.Path.Combine(AppContext.BaseDirectory, stackLibrary)))
{
PrintStackLibraryMissing(stackLibrary, searched, null);
return 1;
}
// The first stack call. If the library is there but still cannot be
// loaded (the wrong architecture, a missing dependency, or it was found
// somewhere else and is not the right file), the runtime throws here,
// before anything else has been touched (no socket bound yet).
//
// Build the Device object's Firmware_Revision from the stack's own real
// version - it reports the underlying CAS BACnet Stack, not this app.
try
{
g_firmwareRevision =
CASBACnetStackAdapter.BACnetStack_GetAPIMajorVersion() + "." +
CASBACnetStackAdapter.BACnetStack_GetAPIMinorVersion() + "." +
CASBACnetStackAdapter.BACnetStack_GetAPIPatchVersion() + "." +
CASBACnetStackAdapter.BACnetStack_GetAPIBuildVersion();
}
catch (DllNotFoundException ex)
{
PrintStackLibraryMissing(stackLibrary, searched, ex.Message);
return 1;
}
catch (BadImageFormatException ex)
{
PrintStackLibraryMissing(stackLibrary, searched,
"the library found does not match this process's architecture (this build is x64): " + ex.Message);
return 1;
}
CASExampleHelper.PrintVersion(APP_NAME, APP_VERSION);
// --- Bind the BACnet/IP socket -------------------------------------
try
{
udp.Setup(port);
}
catch (Exception ex)
{
Console.Error.WriteLine("Error: could not bind UDP port " + port + ": " + ex.Message);
Console.Error.WriteLine("Is another BACnet device already running on this machine? Try --port.");
return 1;
}
// Capture the BACnet/IP addressing the Network Port object will report.
g_bacnetIpUdpPort = port;
CASExampleHelper.LocalIPv4 local = CASExampleHelper.GetLocalIPv4();
string[] ipParts = local.Address.Split('.');
string[] maskParts = local.Netmask.Split('.');
for (int i = 0; i < 4; i++)
{
g_ipAddress[i] = byte.Parse(ipParts[i]);
g_ipSubnetMask[i] = byte.Parse(maskParts[i]);
}
Console.WriteLine("FYI: listening on " + local.Address + ":" + g_bacnetIpUdpPort +
" (broadcast " + local.Broadcast + ")");
// --- Register callbacks ---------------------------------------------
// The transport + time callbacks are shared boilerplate, keyed to the
// Network Port instance this socket will become.
CASExampleHelper.RegisterCommonCallbacks(udp, NETWORK_PORT_INSTANCE);
// The property callbacks are specific to this example.
CASBACnetStackAdapter.BACnetStack_RegisterCallbackGetPropertyReal(getPropertyRealDelegate);
CASBACnetStackAdapter.BACnetStack_RegisterCallbackGetPropertyEnumerated(getPropertyEnumeratedDelegate);
CASBACnetStackAdapter.BACnetStack_RegisterCallbackGetPropertyUnsignedInteger(getPropertyUnsignedIntegerDelegate);
CASBACnetStackAdapter.BACnetStack_RegisterCallbackGetPropertyBool(getPropertyBoolDelegate);
CASBACnetStackAdapter.BACnetStack_RegisterCallbackGetPropertyOctetString(getPropertyOctetStringDelegate);
CASBACnetStackAdapter.BACnetStack_RegisterCallbackGetPropertyCharacterString(getPropertyCharacterStringDelegate);
// --- Create the device -----------------------------------------------
if (!CASBACnetStackAdapter.BACnetStack_AddDevice(g_deviceInstance))
{
Console.WriteLine("Error: Failed to add the Device " + g_deviceInstance + ".");
return 1;
}
// Enable ReadProperty (DS-RP-B) - the one mandatory service for B-SS.
// We set it explicitly to make the profile requirement obvious.
//
// We deliberately do NOT enable WriteProperty, ReadPropertyMultiple,
// SubscribeCOV, or any alarm/event service: a Smart Sensor does not
// require them, so a faithful B-SS example leaves them off.
if (!CASBACnetStackAdapter.BACnetStack_SetServiceEnabled(
g_deviceInstance, CASBACnetStackAdapter.SERVICES_SUPPORTED_READ_PROPERTY, true))
{
Console.WriteLine("Error: Failed to enable the ReadProperty service.");
return 1;
}
// Discovery: Who-Is/I-Am (DM-DDB-B) and Who-Has/I-Have (DM-DOB-B).
//
// These need enabling even though the device already ANSWERS them.
// The stack's service defaults are whoIs + whoHas + readProperty
// only - iAm and iHave are left FALSE. Who-Is is answered and the
// start-up I-Am is sent regardless, because neither is gated on the
// bit; but Protocol_Services_Supported is emitted verbatim from that
// bitstring, so without these calls the device DOES I-Am and I-Have
// while telling every client it supports neither.
if (!CASBACnetStackAdapter.BACnetStack_SetServiceEnabled(g_deviceInstance, CASBACnetStackAdapter.SERVICES_SUPPORTED_WHO_IS, true) ||
!CASBACnetStackAdapter.BACnetStack_SetServiceEnabled(g_deviceInstance, CASBACnetStackAdapter.SERVICES_SUPPORTED_I_AM, true) ||
!CASBACnetStackAdapter.BACnetStack_SetServiceEnabled(g_deviceInstance, CASBACnetStackAdapter.SERVICES_SUPPORTED_WHO_HAS, true) ||
!CASBACnetStackAdapter.BACnetStack_SetServiceEnabled(g_deviceInstance, CASBACnetStackAdapter.SERVICES_SUPPORTED_I_HAVE, true))
{
Console.WriteLine("Error: Failed to enable the discovery services (Who-Is/I-Am, Who-Has/I-Have).");
return 1;
}
// --- Add the read-only sensor objects -------------------------------
if (!CASBACnetStackAdapter.BACnetStack_AddObject(g_deviceInstance, CASBACnetStackAdapter.OBJECT_TYPE_ANALOG_INPUT, ANALOG_INPUT_INSTANCE))
{
Console.WriteLine("Error: Failed to add Analog Input " + ANALOG_INPUT_INSTANCE + " (Bronze).");
return 1;
}
if (!CASBACnetStackAdapter.BACnetStack_AddObject(g_deviceInstance, CASBACnetStackAdapter.OBJECT_TYPE_BINARY_INPUT, BINARY_INPUT_INSTANCE))
{
Console.WriteLine("Error: Failed to add Binary Input " + BINARY_INPUT_INSTANCE + " (Emerald).");
return 1;
}
if (!CASBACnetStackAdapter.BACnetStack_AddObject(g_deviceInstance, CASBACnetStackAdapter.OBJECT_TYPE_MULTI_STATE_INPUT, MULTI_STATE_INPUT_INSTANCE))
{
Console.WriteLine("Error: Failed to add Multi-State Input " + MULTI_STATE_INPUT_INSTANCE + " (Hot Pink).");
return 1;
}
// --- Add the Network Port object ------------------------------------
// Every BACnet device (Protocol_Revision 17+) must have at least one
// Network Port object describing the port it talks on. This one is
// the BACnet/IP application port; it is the lowest layer, so its
// reference port is "none". networkNumber 0 with quality "unknown"
// describes a local port that has not learned its network number -
// the right answer for a device that is not a router and has not
// been told one.
if (!CASBACnetStackAdapter.BACnetStack_AddNetworkPortObject(
g_deviceInstance, NETWORK_PORT_INSTANCE,
CASBACnetStackAdapter.NETWORK_PORT_OBJECT_NETWORK_TYPE_IPV4,
CASBACnetStackAdapter.PROTOCOL_LEVEL_BACNET_APPLICATION,
0, // networkNumber: not configured
CASBACnetStackAdapter.NETWORK_NUMBER_QUALITY_UNKNOWN,
CASBACnetStackExampleConstants.NETWORK_PORT_REFERENCE_PORT_NONE))
{
Console.WriteLine("Error: Failed to add Network Port 1 (Vermilion).");
return 1;
}
// --- Enable the OPTIONAL properties we choose to expose -------------
// The stack automatically enables an object's REQUIRED properties
// when the object is added - so Units, Polarity, Number_Of_States,
// Out_Of_Service, and the Network Port's BACnet/IP addressing are
// already enabled; our Get* callbacks just supply their values. Only
// OPTIONAL properties need SetPropertyEnabled. State_Text is
// optional on a Multi-State Input, so we enable it here (and serve
// it in GetPropertyCharacterString).
//
// The Device's Description is optional too, and it is an easy one to
// get wrong: serving it from a Get callback is NOT enough. The stack
// checks IsPropertyEnabled BEFORE it ever reaches the callbacks, and
// for an optional property that check falls back to "is it
// required?" - which is false. So a Description branch in the
// callback without this enable is DEAD CODE, and the client reads
// back Error: unknown-property.
if (!CASBACnetStackAdapter.BACnetStack_SetPropertyEnabled(
g_deviceInstance, CASBACnetStackAdapter.OBJECT_TYPE_DEVICE,
g_deviceInstance, CASBACnetStackAdapter.PROPERTY_IDENTIFIER_DESCRIPTION, true))
{
Console.WriteLine("Error: Failed to enable Description on the Device object.");
return 1;
}
if (!CASBACnetStackAdapter.BACnetStack_SetPropertyEnabled(
g_deviceInstance, CASBACnetStackAdapter.OBJECT_TYPE_MULTI_STATE_INPUT,
MULTI_STATE_INPUT_INSTANCE, CASBACnetStackAdapter.PROPERTY_IDENTIFIER_STATETEXT, true))
{
Console.WriteLine("Error: Failed to enable State_Text on Multi-State Input 1 (Hot Pink).");
return 1;
}
// Reliability is optional on an Analog Input; it is how a sensor says
// its reading cannot be trusted. Enabling it also gives the FAULT bit
// of Status_Flags its meaning - the stack derives that bit from it.
if (!CASBACnetStackAdapter.BACnetStack_SetPropertyEnabled(
g_deviceInstance, CASBACnetStackAdapter.OBJECT_TYPE_ANALOG_INPUT,
ANALOG_INPUT_INSTANCE, CASBACnetStackAdapter.PROPERTY_IDENTIFIER_RELIABILITY, true))
{
Console.WriteLine("Error: Failed to enable Reliability on Analog Input 1 (Bronze).");
return 1;
}
// The Simulation's test-only broken-wire hook (section 2).
g_sensorFaultFile = Environment.GetEnvironmentVariable(SENSOR_FAULT_FILE_VARIABLE) ?? "";
// Who-Is is answered automatically. The spec also requires a device
// to announce itself on start-up, so broadcast an unsolicited I-Am
// now (to the local subnet broadcast - the Network Port's own network).
if (!CASExampleHelper.SendIAm(g_deviceInstance, g_bacnetIpUdpPort, NETWORK_PORT_INSTANCE))
{
Console.WriteLine("Error: SendIAm failed.");
return 1;
}
Console.WriteLine("FYI: Device " + g_deviceInstance + " (\"" + DEVICE_NAME + "\") ready. Vendor ID " +
VENDOR_IDENTIFIER + ". Press 'h' for help.");
// --- Graceful Ctrl+C shutdown ----------------------------------------
bool running = true;
Console.CancelKeyPress += (sender, e) =>
{
e.Cancel = true; // handle it ourselves instead of the process dying mid-tick
running = false;
};
// --- Run the stack --------------------------------------------------
// BACnetStack_Tick() processes incoming messages and timers. Call it
// continuously, step the sensor Simulation beside it, and poll the
// keyboard for interactive commands (a real console's keys, or a 'q'
// written to a redirected stdin - see CASExampleHelper.PollKey()).
while (running)
{
CASBACnetStackAdapter.BACnetStack_Tick();
SensorSimulationTick();
switch (CASExampleHelper.PollKey())
{
case CASExampleHelper.KeyCommand.Help:
CASExampleHelper.PrintHelp(APP_NAME, APP_VERSION);
break;
case CASExampleHelper.KeyCommand.Quit:
running = false;
break;
case CASExampleHelper.KeyCommand.ArrowUp:
g_analogInput1Value += 1.1f;
Console.WriteLine("Analog Input 1 (Bronze) = " + g_analogInput1Value.ToString("0.0") + " C");
break;
case CASExampleHelper.KeyCommand.ArrowDown:
g_analogInput1Value -= 1.1f;
Console.WriteLine("Analog Input 1 (Bronze) = " + g_analogInput1Value.ToString("0.0") + " C");
break;
}
System.Threading.Thread.Sleep(1); // 1 ms - be a good citizen, don't spin the CPU
}
udp.Shutdown();
Console.WriteLine("FYI: stopped.");
return 0;
}
}
}