├── .gitignore ├── LICENSE ├── README.md ├── btypes ├── address.go ├── apdu.go ├── bacerr │ ├── bacerr.go │ ├── errorclass_string.go │ └── errorcode_string.go ├── bitstring.go ├── bvlc.go ├── const.go ├── datetime.go ├── device.go ├── marshal_test.go ├── ndpu │ ├── messagetypes.go │ └── networkmessagetype_string.go ├── npdu.go ├── null │ └── null.go ├── object.go ├── object_map.go ├── priority │ └── priority.go ├── property.go ├── segmentation │ ├── segmentation.go │ └── segmentedtype_string.go ├── services │ ├── services.go │ └── services_test.go └── units │ ├── unit_string.go │ └── units.go ├── cmd ├── cmd │ ├── discover.go │ ├── old │ │ └── readmultiprop.go │ ├── readmultiprop.go │ ├── readprop.go │ ├── root.go │ ├── whoIs.go │ └── writeprop.go └── main.go ├── const.go ├── datalink ├── datalink.go ├── mstp.go └── udp.go ├── device.go ├── encoding ├── apdu.go ├── appdata.go ├── appdata_test.go ├── bvlc.go ├── const.go ├── context_tag.go ├── date.go ├── decoder.go ├── encoder.go ├── error.go ├── general.go ├── iam.go ├── main_test.go ├── npdu.go ├── object.go ├── readmultiple.go ├── readmultipleack.go ├── readprop.go ├── service_test.go ├── string.go ├── types.go ├── validate.go ├── whois.go ├── writemultiple.go └── writeprop.go ├── go.mod ├── go.sum ├── helpers ├── data │ └── data.go ├── homedir │ └── homedir.go ├── ipbytes │ ├── ip.go │ └── ip_test.go ├── nils │ └── nil.go ├── print │ └── console.go ├── store │ └── init.go └── validation │ ├── vaildation.go │ └── validation_test.go ├── iam.go ├── iam_test.go ├── main_test.go ├── misc.go ├── network ├── base.go ├── client_test.go ├── device.go ├── discover.go ├── discover_test.go ├── err.go ├── objects.go ├── points.go ├── points_test.go ├── read.go ├── store.go ├── strings.go ├── strings_test.go ├── whois.go ├── whois_test.go ├── write.go └── write_test.go ├── objectlist.go ├── readmulti.go ├── readprop.go ├── tsm ├── transactions.go └── transactions_test.go ├── utsm ├── doc.go ├── main_test.go ├── subscriber.go └── utsm.go ├── whatnetwork.go ├── whois.go ├── whoisrouter.go ├── writemulti.go └── writeprop.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | .code/ 8 | .idea/ 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bacnet 2 | 3 | bacnet client written in go. 4 | 5 | # Installation 6 | 7 | learn some `golang` if you don't know it first :) 8 | 9 | ``` 10 | go mod tidy 11 | ``` 12 | 13 | For usage run: 14 | 15 | ``` 16 | go mod tidy 17 | cd cli 18 | go run main.go --help 19 | ``` 20 | 21 | ## examples 22 | 23 | change out the interface and device and so on 24 | 25 | ### whois 26 | 27 | ``` 28 | go run main.go whois --interface=wlp3s0 29 | ``` 30 | 31 | ``` 32 | go run main.go whois --interface=wlp3s0 33 | ``` 34 | 35 | ### read AO 36 | 37 | ``` 38 | go run main.go read --interface=wlp3s0 --device=202 --objectID=1 --objectType=1 --property=87 39 | ``` 40 | 41 | ### write to an AO 42 | 43 | ``` 44 | go run main.go write --interface=wlp3s0 --device=202 --address=192.168.15.20 --network=4 --mstp=1 --objectID=1 --objectType=1 --property=85 --priority=16 --value=21 45 | ``` 46 | 47 | ### write null to @16 48 | 49 | ``` 50 | go run main.go write --interface=wlp3s0 --device=202 --objectID=1 --objectType=1 --property=85 --priority=16 --null=true 51 | ``` 52 | 53 | ## over a bacnet to ms-tp network 54 | 55 | - router ip: 192.168.15.20 56 | - bacnet router network number: 4 57 | - bacnet mstp(rs485) mac address (between 0-255): 1 58 | 59 | ``` 60 | go run main.go read --interface=wlp3s0 --device=202 --address=192.168.15.20 --network=4 --mstp=1 --objectID=1 --objectType=1 --property=85 61 | ``` 62 | 63 | get device name 64 | 65 | ``` 66 | go run main.go read --interface=wlp3s0 --device=202 --address=192.168.15.20 --network=4 --mstp=1 --objectID=202 --objectType=8 --property=77 67 | ``` 68 | 69 | ### Max APDU Length 70 | 71 | `Max APDU Length is important on for read/write prop multiple` 72 | 73 | In the variable "Max APDU Length Accepted" the following are the values that can be returned: 74 | 75 | ``` 76 | mstp device: 480 77 | ip device: 1476 78 | ``` 79 | 80 | ### example same device getting the max APDU 81 | 82 | get device MaxApdu length over MSTP will return `480` 83 | 84 | ``` 85 | go run main.go read --interface=wlp3s0 --device=202 --address=192.168.15.20 --network=4 --mstp=1 --objectID=202 --objectType=8 --property=62 86 | ``` 87 | 88 | get device MaxApdu length and on the same device but over IP will return `1476` 89 | 90 | ``` 91 | go run main.go read --interface=wlp3s0 --device=202 --address=192.168.15.202 --network=0 --mstp=0 --objectID=202 --objectType=8 --property=62 92 | ``` 93 | 94 | ## Library 95 | 96 | - [x] Who Is 97 | - [x] Iam 98 | - [x] Read Property 99 | - [x] Read Multiple Property (beta) 100 | - [ ] Read Range 101 | - [x] Write Property 102 | - [x] Write Property Multiple (beta) 103 | - [ ] Who Has 104 | - [x] What Is Network Number (beta) 105 | - [x] Who Is Router To Network (beta) 106 | - [ ] Change of Value Notification 107 | - [ ] Event Notification 108 | - [ ] Subscribe Change of Value 109 | - [ ] Atomic Read File 110 | - [ ] Atomic Write File 111 | 112 | ## Command Line Interface 113 | 114 | - [x] Who Is 115 | - [x] Iam 116 | - [x] Read Property 117 | - [x] Read Multiple Property 118 | - [ ] Read Range 119 | - [ ] Write Property 120 | - [ ] Write Property Multiple 121 | - [ ] Who Has 122 | - [ ] What Is Network Number 123 | - [x] Who Is Router To Network 124 | - [ ] Atomic Read File 125 | - [ ] Atomic Write File 126 | 127 | # testing 128 | 129 | ## Tested on devices 130 | 131 | - [x] Johnson Controls (FEC) 132 | - [x] Easy-IO 30p, tested over IP and ms-tp 133 | - [ ] Delta Controls 134 | - [x] Reliable Controls 135 | - [x] Honeywell Spyder 136 | - [ ] Niagara N4 jace 137 | - [ ] Schneider 138 | 139 | ## tested with other bacnet-libs 140 | 141 | - [x] bacnet-stack 142 | - [ ] bacnet-4j 143 | - [x] bacpypes 144 | 145 | This library is heavily based on the BACnet-Stack library originally written by Steve Karg. 146 | 147 | - Ported and all credit to alex from https://github.com/alexbeltran/gobacnet 148 | - And ideas from https://github.com/noahtkeller/go-bacnet 149 | 150 | 151 | 152 | example whois 153 | ```go 154 | bytes := []byte{ 155 | 0x81, 0x0b, 0x00, 0x08, // BVLC 156 | 0x01, 0x00, // NPDU 157 | 0x10, 0x08, // APDU 158 | } 159 | 160 | pc, err := net.ListenPacket("udp4", ":47809") 161 | if err != nil { 162 | panic(err) 163 | } 164 | defer pc.Close() 165 | 166 | addr, err := net.ResolveUDPAddr("udp4", "255.255.255.255:47808") 167 | if err != nil { 168 | panic(err) 169 | } 170 | 171 | _, err = pc.WriteTo(bytes, addr) 172 | if err != nil { 173 | panic(err) 174 | } 175 | d := make([]byte, 1) 176 | 177 | a, b, c := pc.ReadFrom(d) 178 | fmt.Println(a) 179 | fmt.Println(b) 180 | fmt.Println(c) 181 | ``` 182 | 183 | 184 | -------------------------------------------------------------------------------- /btypes/address.go: -------------------------------------------------------------------------------- 1 | package btypes 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | type Address struct { 9 | Net uint16 // BACnet network number 10 | Len uint8 11 | MacLen uint8 // mac len 0 is a broadcast address 12 | Mac []uint8 //note: MAC for IP addresses uses 4 bytes for addr, 2 bytes for port 13 | Adr []uint8 // hardware addr (MAC) address of ms-tp devices 14 | } 15 | 16 | const GlobalBroadcast uint16 = 0xFFFF 17 | const broadcastNetwork uint16 = 0xFFFF 18 | 19 | // IsBroadcast returns if the address is a broadcast address 20 | func (a *Address) IsBroadcast() bool { 21 | if a.Net == broadcastNetwork || a.MacLen == 0 { 22 | return true 23 | } 24 | return false 25 | } 26 | 27 | //SetLength if device is of type ms-tp then set address len to 1 28 | func (a *Address) SetLength() { 29 | if len(a.Adr) > 0 { 30 | a.Len = 1 31 | } 32 | return 33 | } 34 | 35 | func (a *Address) SetBroadcast(b bool) { 36 | if b { 37 | a.MacLen = 0 38 | } else { 39 | a.MacLen = uint8(len(a.Mac)) 40 | } 41 | } 42 | 43 | // IsSubBroadcast checks to see if packet is meant to be a network 44 | // specific broadcast 45 | func (a *Address) IsSubBroadcast() bool { 46 | if a.Net > 0 && a.Len == 0 { 47 | return true 48 | } 49 | return false 50 | } 51 | 52 | // IsUnicast checks to see if packet is meant to be a unicast 53 | func (a *Address) IsUnicast() bool { 54 | if a.MacLen == 6 { 55 | return true 56 | } 57 | return false 58 | } 59 | 60 | // UDPAddr parses the mac address and returns a proper net.UDPAddr 61 | func (a *Address) UDPAddr() (net.UDPAddr, error) { 62 | if len(a.Mac) != 6 { 63 | return net.UDPAddr{}, fmt.Errorf("mac is too short at %d", len(a.Mac)) 64 | } 65 | port := uint(a.Mac[4])<<8 | uint(a.Mac[5]) 66 | ip := net.IPv4(a.Mac[0], a.Mac[1], a.Mac[2], a.Mac[3]) 67 | return net.UDPAddr{ 68 | IP: ip, 69 | Port: int(port), 70 | }, nil 71 | } 72 | -------------------------------------------------------------------------------- /btypes/apdu.go: -------------------------------------------------------------------------------- 1 | package btypes 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet/btypes/bacerr" 6 | ) 7 | 8 | /* 9 | Max ADPU sizes 10 | 0 = 50 11 | 1 = 128 12 | 2 = 206 13 | 3 = 480 14 | 4 = 1024 15 | 5 = 1476 16 | */ 17 | 18 | const MaxAPDU = 1476 19 | const MaxAPDU128 = 128 20 | const MaxAPDU206 = 206 21 | const MaxAPDU480 = 480 22 | const MaxAPDU1024 = 1024 23 | const MaxAPDU1476 = 1476 24 | 25 | type ServiceConfirmed uint8 26 | type ServiceUnconfirmed uint8 27 | 28 | const ( 29 | ServiceUnconfirmedIAm ServiceUnconfirmed = 0 30 | ServiceUnconfirmedIHave ServiceUnconfirmed = 1 31 | ServiceUnconfirmedCOVNotification ServiceUnconfirmed = 2 32 | ServiceUnconfirmedEventNotification ServiceUnconfirmed = 3 33 | ServiceUnconfirmedPrivateTransfer ServiceUnconfirmed = 4 34 | ServiceUnconfirmedTextMessage ServiceUnconfirmed = 5 35 | ServiceUnconfirmedTimeSync ServiceUnconfirmed = 6 36 | ServiceUnconfirmedWhoHas ServiceUnconfirmed = 7 37 | ServiceUnconfirmedWhoIs ServiceUnconfirmed = 8 38 | ServiceUnconfirmedUTCTimeSync ServiceUnconfirmed = 9 39 | ServiceUnconfirmedWriteGroup ServiceUnconfirmed = 10 40 | MaxServiceUnconfirmed ServiceUnconfirmed = 11 41 | 42 | /* Other services to be added as they are defined. */ 43 | /* All choice values in this production are reserved */ 44 | /* for definition by ASHRAE. */ 45 | /* Proprietary extensions are made by using the */ 46 | /* UnconfirmedPrivateTransfer service. See Clause 23. */ 47 | ) 48 | 49 | const ( 50 | /* Alarm and Event Services */ 51 | ServiceConfirmedAcknowledgeAlarm ServiceConfirmed = 0 52 | ServiceConfirmedCOVNotification ServiceConfirmed = 1 53 | ServiceConfirmedEventNotification ServiceConfirmed = 2 54 | ServiceConfirmedGetAlarmSummary ServiceConfirmed = 3 55 | ServiceConfirmedGetEnrollmentSummary ServiceConfirmed = 4 56 | ServiceConfirmedGetEventInformation ServiceConfirmed = 29 57 | ServiceConfirmedSubscribeCOV ServiceConfirmed = 5 58 | ServiceConfirmedSubscribeCOVProperty ServiceConfirmed = 28 59 | ServiceConfirmedLifeSafetyOperation ServiceConfirmed = 27 60 | /* File Access Services */ 61 | ServiceConfirmedAtomicReadFile ServiceConfirmed = 6 62 | ServiceConfirmedAtomicWriteFile ServiceConfirmed = 7 63 | /* Object Access Services */ 64 | ServiceConfirmedAddListElement ServiceConfirmed = 8 65 | ServiceConfirmedRemoveListElement ServiceConfirmed = 9 66 | ServiceConfirmedCreateObject ServiceConfirmed = 10 67 | ServiceConfirmedDeleteObject ServiceConfirmed = 11 68 | ServiceConfirmedReadProperty ServiceConfirmed = 12 69 | ServiceConfirmedReadPropConditional ServiceConfirmed = 13 70 | ServiceConfirmedReadPropMultiple ServiceConfirmed = 14 71 | ServiceConfirmedReadRange ServiceConfirmed = 26 72 | ServiceConfirmedWriteProperty ServiceConfirmed = 15 73 | ServiceConfirmedWritePropMultiple ServiceConfirmed = 16 74 | /* Remote Device Management Services */ 75 | ServiceConfirmedDeviceCommunicationControl ServiceConfirmed = 17 76 | ServiceConfirmedPrivateTransfer ServiceConfirmed = 18 77 | ServiceConfirmedTextMessage ServiceConfirmed = 19 78 | ServiceConfirmedReinitializeDevice ServiceConfirmed = 20 79 | /* Virtual Terminal Services */ 80 | ServiceConfirmedVTOpen ServiceConfirmed = 21 81 | ServiceConfirmedVTClose ServiceConfirmed = 22 82 | ServiceConfirmedVTData ServiceConfirmed = 23 83 | /* Security Services */ 84 | ServiceConfirmedAuthenticate ServiceConfirmed = 24 85 | ServiceConfirmedRequestKey ServiceConfirmed = 25 86 | /* Services added after 1995 */ 87 | /* readRange (26) see Object Access Services */ 88 | /* lifeSafetyOperation (27) see Alarm and Event Services */ 89 | /* subscribeCOVProperty (28) see Alarm and Event Services */ 90 | /* getEventInformation (29) see Alarm and Event Services */ 91 | maxBACnetConfirmedService ServiceConfirmed = 30 92 | ) 93 | 94 | // APDU - Application Protocol Data Unit 95 | type APDU struct { 96 | DataType PDUType 97 | SegmentedMessage bool 98 | MoreFollows bool 99 | SegmentedResponseAccepted bool 100 | MaxSegs uint 101 | MaxApdu uint 102 | InvokeId uint8 103 | Sequence uint8 104 | WindowNumber uint8 105 | Service ServiceConfirmed 106 | UnconfirmedService ServiceUnconfirmed 107 | Error struct { 108 | Class bacerr.ErrorClass 109 | Code bacerr.ErrorCode 110 | } 111 | 112 | // This is the raw data passed based on the service 113 | RawData []byte 114 | } 115 | 116 | // PDUType encompasses all valid pdus. 117 | type PDUType uint8 118 | 119 | // pdu requests 120 | const ( 121 | ConfirmedServiceRequest PDUType = 0 122 | UnconfirmedServiceRequest PDUType = 0x10 123 | SimpleAck PDUType = 0x20 124 | ComplexAck PDUType = 0x30 125 | SegmentAck PDUType = 0x40 126 | Error PDUType = 0x50 127 | Reject PDUType = 0x60 128 | Abort PDUType = 0x70 129 | ) 130 | 131 | // IsConfirmedServiceRequest checks to see if the APDU is in the list of known services 132 | func (a *APDU) IsConfirmedServiceRequest() bool { 133 | return (0xF0 & a.DataType) == ConfirmedServiceRequest 134 | } 135 | 136 | func (s *ServiceConfirmed) String() string { 137 | switch *s { 138 | default: 139 | return fmt.Sprintf("Unknown %d", uint(*s)) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /btypes/bacerr/errorclass_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ErrorClass"; DO NOT EDIT. 2 | 3 | package bacerr 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[DeviceError-0] 12 | _ = x[ObjectError-1] 13 | _ = x[PropertyError-2] 14 | _ = x[ResourcesError-3] 15 | _ = x[SecurityError-4] 16 | _ = x[ServicesError-5] 17 | _ = x[VTError-6] 18 | _ = x[CommunicationError-7] 19 | } 20 | 21 | const _ErrorClass_name = "DeviceErrorObjectErrorPropertyErrorResourcesErrorSecurityErrorServicesErrorVTErrorCommunicationError" 22 | 23 | var _ErrorClass_index = [...]uint8{0, 11, 22, 35, 49, 62, 75, 82, 100} 24 | 25 | func (i ErrorClass) String() string { 26 | if i >= ErrorClass(len(_ErrorClass_index)-1) { 27 | return "ErrorClass(" + strconv.FormatInt(int64(i), 10) + ")" 28 | } 29 | return _ErrorClass_name[_ErrorClass_index[i]:_ErrorClass_index[i+1]] 30 | } 31 | -------------------------------------------------------------------------------- /btypes/bitstring.go: -------------------------------------------------------------------------------- 1 | package btypes 2 | 3 | import "encoding/json" 4 | 5 | const MaxBitStringBytes = 15 6 | 7 | type BitString struct { 8 | BitUsed uint8 `json:"bit_used"` 9 | Value []byte `json:"value"` 10 | } 11 | 12 | func NewBitString(bufferSize int) *BitString { 13 | if bufferSize > MaxBitStringBytes { 14 | bufferSize = MaxBitStringBytes 15 | } 16 | return &BitString{ 17 | BitUsed: 0, 18 | Value: make([]byte, bufferSize), 19 | } 20 | } 21 | 22 | func (bs *BitString) GetValue() []bool { 23 | value := make([]bool, bs.BitUsed) 24 | for i := uint8(0); i < bs.BitUsed; i++ { 25 | if bs.Bit(i) { 26 | value[i] = true 27 | } else { 28 | value[i] = false 29 | } 30 | } 31 | return value 32 | } 33 | 34 | func (bs *BitString) String() string { 35 | bin, _ := json.Marshal(bs.GetValue()) 36 | return string(bin) 37 | } 38 | 39 | func (bs *BitString) SetBit(bitNumber uint8, value bool) { 40 | byteNumber := bitNumber / 8 41 | var bitMask uint8 = 1 42 | if byteNumber < MaxBitStringBytes { 43 | /* set max bits used */ 44 | if bs.BitUsed < (bitNumber + 1) { 45 | bs.BitUsed = bitNumber + 1 46 | } 47 | bitMask = bitMask << (bitNumber - (byteNumber * 8)) 48 | if value { 49 | bs.Value[byteNumber] |= bitMask 50 | } else { 51 | bs.Value[byteNumber] &= ^bitMask 52 | } 53 | } 54 | } 55 | 56 | func (bs *BitString) Bit(bitNumber uint8) bool { 57 | byteNumber := bitNumber / 8 58 | bitMask := uint8(1) 59 | if bitNumber < (MaxBitStringBytes * 8) { 60 | bitMask = bitMask << (bitNumber - (byteNumber * 8)) 61 | return (bs.Value[byteNumber] & bitMask) != 0 62 | } 63 | return false 64 | } 65 | 66 | func (bs *BitString) GetBitUsed() uint8 { 67 | return bs.BitUsed 68 | } 69 | 70 | func (bs *BitString) BytesUsed() uint8 { 71 | if bs != nil && bs.BitUsed > 0 { 72 | return (bs.BitUsed-1)/8 + 1 73 | } 74 | return 0 75 | } 76 | 77 | func (bs *BitString) Byte(index uint8) byte { 78 | if bs != nil && index < MaxBitStringBytes { 79 | return bs.Value[index] 80 | } 81 | return 0 82 | } 83 | 84 | func (bs *BitString) SetByte(index uint8, value byte) bool { 85 | if bs != nil && index < MaxBitStringBytes { 86 | bs.Value[index] = value 87 | return true 88 | } 89 | return false 90 | } 91 | 92 | func (bs *BitString) SetBitsUsed(byteUsed uint8, bitsUnused uint8) bool { 93 | if bs != nil { 94 | bs.BitUsed = byteUsed*8 - bitsUnused 95 | return true 96 | } 97 | return false 98 | } 99 | 100 | func (bs *BitString) BitsCapacity() uint8 { 101 | if bs != nil { 102 | return uint8(len(bs.Value) * 8) 103 | } 104 | return 0 105 | } 106 | -------------------------------------------------------------------------------- /btypes/bvlc.go: -------------------------------------------------------------------------------- 1 | package btypes 2 | 3 | // BACnet Virtual Link Control (BVLC) 4 | 5 | // BVLCTypeBacnetIP is the only valid type for the BVLC layer as of 2002. 6 | // Additional btypes may be added in the future 7 | const BVLCTypeBacnetIP = 0x81 8 | 9 | // BacFunc Function 10 | type BacFunc byte 11 | 12 | // List of possible BACnet functions 13 | const ( 14 | BacFuncResult BacFunc = 0 15 | BacFuncWriteBroadcastDistributionTable BacFunc = 1 16 | BacFuncBroadcastDistributionTable BacFunc = 2 17 | BacFuncBroadcastDistributionTableAck BacFunc = 3 18 | BacFuncForwardedNPDU BacFunc = 4 19 | BacFuncUnicast BacFunc = 10 20 | BacFuncBroadcast BacFunc = 11 21 | ) 22 | 23 | type BVLC struct { 24 | Type byte 25 | Function BacFunc 26 | 27 | // Length includes the length of Type, Function, and Length. (4 bytes) It also 28 | // has the length of the data field after 29 | Length uint16 30 | Data []byte 31 | } 32 | -------------------------------------------------------------------------------- /btypes/const.go: -------------------------------------------------------------------------------- 1 | package btypes 2 | 3 | const ( 4 | MaxInstance = 0x3FFFFF 5 | ) 6 | 7 | const ( 8 | // WhoIsAll is used when scanning a range. Using this as one of the two ranges, 9 | // will scan all available devices 10 | WhoIsAll = -1 11 | ArrayAll = 0xFFFFFFFF 12 | ) 13 | -------------------------------------------------------------------------------- /btypes/datetime.go: -------------------------------------------------------------------------------- 1 | package btypes 2 | 3 | type DayOfWeek int 4 | 5 | const ( 6 | None DayOfWeek = iota 7 | Monday DayOfWeek = iota 8 | Tuesday DayOfWeek = iota 9 | Wednesday DayOfWeek = iota 10 | Thursday DayOfWeek = iota 11 | Friday DayOfWeek = iota 12 | Saturday DayOfWeek = iota 13 | Sunday DayOfWeek = iota 14 | ) 15 | 16 | type Date struct { 17 | Year int 18 | Month int 19 | Day int 20 | // Bacnet has an option to only do operations on even or odd months 21 | EvenMonth bool 22 | OddMonth bool 23 | EvenDay bool 24 | OddDay bool 25 | LastDayOfMonth bool 26 | DayOfWeek DayOfWeek 27 | } 28 | 29 | type Time struct { 30 | Hour int 31 | Minute int 32 | Second int 33 | Millisecond int 34 | } 35 | 36 | type DataTime struct { 37 | Date 38 | Time 39 | } 40 | 41 | // UnspecifiedTime means that this time is triggered through out a period. An 42 | // example of this is 02:FF:FF:FF will trigger all through out 2 am 43 | const UnspecifiedTime = 0xFF 44 | 45 | const ( 46 | TimeStampTime = 0 47 | TimeStampSequence = 1 48 | TimeStampDatetime = 2 49 | ) 50 | -------------------------------------------------------------------------------- /btypes/device.go: -------------------------------------------------------------------------------- 1 | package btypes 2 | 3 | import ( 4 | "fmt" 5 | ip2bytes "github.com/NubeDev/bacnet/helpers/ipbytes" 6 | "github.com/NubeDev/bacnet/helpers/validation" 7 | ) 8 | 9 | type Enumerated uint32 10 | 11 | type IAm struct { 12 | ID ObjectID `json:"object_id"` 13 | MaxApdu uint32 `json:"max_apdu"` 14 | Segmentation Enumerated `json:"segmentation"` 15 | Vendor uint32 `json:"vendor"` 16 | Addr Address `json:"addr"` 17 | } 18 | 19 | type Device struct { 20 | ID ObjectID `json:"-"` 21 | DeviceID int `json:"device_id"` 22 | Ip string `json:"ip"` 23 | Port int `json:"port"` 24 | NetworkNumber int `json:"network_number"` 25 | MacMSTP int `json:"mac_mstp"` 26 | MaxApdu uint32 `json:"max_apdu"` //maxApduLengthAccepted 62 27 | Segmentation Enumerated `json:"segmentation"` 28 | Vendor uint32 `json:"vendor"` 29 | Addr Address `json:"address"` 30 | Objects ObjectMap `json:"-"` 31 | SupportsRPM bool `json:"supports_rpm"` //support read prob multiple 32 | SupportsWPM bool `json:"supports_wpm"` //support read prob multiple 33 | DeviceName string `json:"device_name"` 34 | VendorName string `json:"vendor_name"` 35 | } 36 | 37 | /* 38 | If the device doesn't support segmentation then we need to read for example the device object list in chunks of the array index 39 | Properties: []btypes.Property{ 40 | { 41 | Type: prop, 42 | ArrayIndex: bacnet.ArrayAll, So this needs to be changed as an example 0:returns AI:1, 1:returns AI:2 and so on 43 | }, 44 | }, 45 | 46 | BACnetSegmentation: 47 | segmented-both:0 48 | segmented-transmit:1 49 | segmented-receive:2 50 | no-segmentation: 3 51 | 52 | MaxApdu 53 | 0: 50 54 | 1: 128 55 | 2: 206 jci PCG 56 | 3: 480 honeywell spyder 57 | 4: 1024 58 | 5: 1476 easyIO-30p when over IP (same device when over MSTP is 480) 59 | 60 | */ 61 | 62 | // NewDevice returns a new instance of ta bacnet device 63 | func NewDevice(device *Device) (*Device, error) { 64 | 65 | port := device.Port 66 | //check ip 67 | ok := validation.ValidIP(device.Ip) 68 | if !ok { 69 | fmt.Println("fail ip") 70 | } 71 | //check port 72 | if port == 0 { 73 | port = 0xBAC0 74 | } 75 | ok = validation.ValidPort(port) 76 | if !ok { 77 | fmt.Println("fail port") 78 | } 79 | 80 | ip, err := ip2bytes.New(device.Ip, uint16(port)) 81 | if err != nil { 82 | fmt.Println("fail ip2bytes") 83 | return nil, err 84 | } 85 | addr := Address{ 86 | Net: uint16(device.NetworkNumber), 87 | Mac: ip, 88 | Adr: []uint8{uint8(device.MacMSTP)}, 89 | } 90 | object := ObjectID{ 91 | Type: DeviceType, 92 | Instance: device.ID.Instance, 93 | } 94 | device.ID = object 95 | device.Addr = addr 96 | return device, nil 97 | } 98 | 99 | // ObjectSlice returns all the objects in the device as a slice (not thread-safe) 100 | func (dev *Device) ObjectSlice() []Object { 101 | var objs []Object 102 | for _, objMap := range dev.Objects { 103 | for _, o := range objMap { 104 | objs = append(objs, o) 105 | } 106 | } 107 | return objs 108 | } 109 | 110 | //CheckADPU device max ADPU len (mstp can be > 480, and IP > 1476) 111 | func (dev *Device) CheckADPU() error { 112 | errMsg := "device.CheckADPU() incorrect ADPU size:" 113 | size := dev.MaxApdu 114 | if size == 0 { 115 | return fmt.Errorf("%s %d", errMsg, size) 116 | } 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /btypes/marshal_test.go: -------------------------------------------------------------------------------- 1 | package btypes 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | // TestMarshal tests encoding and decoding of the objectmap type. There is 10 | // custom logic in it so we want to make sure it works. 11 | func TestMarshal(t *testing.T) { 12 | test := ObjectMap{ 13 | AnalogInput: make(map[ObjectInstance]Object), 14 | BinaryOutput: make(map[ObjectInstance]Object), 15 | } 16 | test[AnalogInput][0] = Object{Name: "Pizza Sensor"} 17 | test[BinaryOutput][4] = Object{Name: "Should I Eat Pizza Sensor"} 18 | b, err := json.Marshal(test) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | out := make(ObjectMap, 0) 24 | err = json.Unmarshal(b, &out) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | if !reflect.DeepEqual(test, out) { 30 | t.Fatal("Encoding/decoding Object map is not equal") 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /btypes/ndpu/messagetypes.go: -------------------------------------------------------------------------------- 1 | package ndpu 2 | 3 | type NetworkMessageType uint8 4 | 5 | //go:generate stringer -type=NetworkMessageType 6 | const ( 7 | WhoIsRouterToNetwork NetworkMessageType = 0x00 8 | IamRouterToNetwork NetworkMessageType = 0x01 9 | WhatIsNetworkNumber NetworkMessageType = 0x12 10 | NetworkIs NetworkMessageType = 0x13 11 | ) 12 | -------------------------------------------------------------------------------- /btypes/ndpu/networkmessagetype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=NetworkMessageType"; DO NOT EDIT. 2 | 3 | package ndpu 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[WhoIsRouterToNetwork-0] 12 | _ = x[IamRouterToNetwork-1] 13 | _ = x[WhatIsNetworkNumber-18] 14 | _ = x[NetworkIs-19] 15 | } 16 | 17 | const ( 18 | _NetworkMessageType_name_0 = "WhoIsRouterToNetworkIamRouterToNetwork" 19 | _NetworkMessageType_name_1 = "WhatIsNetworkNumberNetworkIs" 20 | ) 21 | 22 | var ( 23 | _NetworkMessageType_index_0 = [...]uint8{0, 20, 38} 24 | _NetworkMessageType_index_1 = [...]uint8{0, 19, 28} 25 | ) 26 | 27 | func (i NetworkMessageType) String() string { 28 | switch { 29 | case i <= 1: 30 | return _NetworkMessageType_name_0[_NetworkMessageType_index_0[i]:_NetworkMessageType_index_0[i+1]] 31 | case 18 <= i && i <= 19: 32 | i -= 18 33 | return _NetworkMessageType_name_1[_NetworkMessageType_index_1[i]:_NetworkMessageType_index_1[i+1]] 34 | default: 35 | return "NetworkMessageType(" + strconv.FormatInt(int64(i), 10) + ")" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /btypes/npdu.go: -------------------------------------------------------------------------------- 1 | package btypes 2 | 3 | import "github.com/NubeDev/bacnet/btypes/ndpu" 4 | 5 | type NPDUPriority byte 6 | 7 | const ProtocolVersion uint8 = 1 8 | const DefaultHopCount uint8 = 255 9 | 10 | const ( 11 | LifeSafety NPDUPriority = 3 12 | CriticalEquipment NPDUPriority = 2 13 | Urgent NPDUPriority = 1 14 | Normal NPDUPriority = 0 15 | ) 16 | 17 | type NPDU struct { 18 | Version uint8 19 | 20 | // Destination (optional) 21 | Destination *Address 22 | 23 | // Source (optional) 24 | Source *Address 25 | 26 | VendorId uint16 27 | 28 | IsNetworkLayerMessage bool 29 | NetworkLayerMessageType ndpu.NetworkMessageType 30 | ExpectingReply bool 31 | Priority NPDUPriority 32 | HopCount uint8 33 | } 34 | -------------------------------------------------------------------------------- /btypes/null/null.go: -------------------------------------------------------------------------------- 1 | package null 2 | 3 | // Null is used when a value is empty. 4 | type Null struct{} 5 | 6 | func (n Null) String() string { 7 | return "" 8 | } 9 | -------------------------------------------------------------------------------- /btypes/object.go: -------------------------------------------------------------------------------- 1 | package btypes 2 | 3 | import "fmt" 4 | 5 | type ObjectType uint16 6 | 7 | const ( 8 | TypeAnalogInput = 0 9 | TypeAnalogOutput = 1 10 | TypeAnalogValue = 2 11 | TypeBinaryInput = 3 12 | TypeBinaryOutput = 4 13 | TypeBinaryValue = 5 14 | TypeDeviceType = 8 15 | TypeFile = 10 16 | TypeMultiStateInput = 13 17 | TypeMultiStateOutput = 14 18 | TypeNotificationClass = 15 19 | TypeMultiStateValue = 19 20 | TypeTrendLog = 20 21 | TypeCharacterString = 40 22 | ) 23 | 24 | const ( 25 | AnalogInput ObjectType = 0 26 | AnalogOutput ObjectType = 1 27 | AnalogValue ObjectType = 2 28 | BinaryInput ObjectType = 3 29 | BinaryOutput ObjectType = 4 30 | BinaryValue ObjectType = 5 31 | DeviceType ObjectType = 8 32 | File ObjectType = 10 33 | MultiStateInput ObjectType = 13 34 | MultiStateOutput ObjectType = 14 35 | NotificationClass ObjectType = 15 36 | MultiStateValue ObjectType = 19 37 | TrendLog ObjectType = 20 38 | CharacterString ObjectType = 40 39 | ) 40 | 41 | const ( 42 | AnalogInputStr = "AnalogInput" 43 | AnalogOutputStr = "AnalogOutput" 44 | AnalogValueStr = "AnalogValue" 45 | BinaryInputStr = "BinaryInput" 46 | BinaryOutputStr = "BinaryOutput" 47 | BinaryValueStr = "BinaryValue" 48 | Devicebtypestr = "Device" 49 | FileStr = "File" 50 | NotificationClassStr = "NotificationClass" 51 | MultiStateValueStr = "MultiStateValue" 52 | MultiStateInputStr = "MultiStateInput" 53 | MultiStateOutputStr = "MultiStateOutput" 54 | TrendLogStr = "TrendLog" 55 | CharacterStringStr = "CharacterString" 56 | ) 57 | 58 | var objTypeMap = map[ObjectType]string{ 59 | AnalogInput: AnalogInputStr, 60 | AnalogOutput: AnalogOutputStr, 61 | AnalogValue: AnalogValueStr, 62 | BinaryInput: BinaryInputStr, 63 | BinaryOutput: BinaryOutputStr, 64 | BinaryValue: BinaryValueStr, 65 | DeviceType: Devicebtypestr, 66 | File: FileStr, 67 | NotificationClass: NotificationClassStr, 68 | MultiStateValue: MultiStateValueStr, 69 | MultiStateInput: MultiStateInputStr, 70 | MultiStateOutput: MultiStateOutputStr, 71 | TrendLog: TrendLogStr, 72 | CharacterString: CharacterStringStr, 73 | } 74 | 75 | var objStrTypeMap = map[string]ObjectType{ 76 | AnalogInputStr: AnalogInput, 77 | AnalogOutputStr: AnalogOutput, 78 | AnalogValueStr: AnalogValue, 79 | BinaryInputStr: BinaryInput, 80 | BinaryOutputStr: BinaryOutput, 81 | BinaryValueStr: BinaryValue, 82 | Devicebtypestr: DeviceType, 83 | FileStr: File, 84 | NotificationClassStr: NotificationClass, 85 | MultiStateValueStr: MultiStateValue, 86 | MultiStateInputStr: MultiStateInput, 87 | MultiStateOutputStr: MultiStateOutput, 88 | TrendLogStr: TrendLog, 89 | CharacterStringStr: CharacterString, 90 | } 91 | 92 | func GetType(s string) ObjectType { 93 | t, ok := objStrTypeMap[s] 94 | if !ok { 95 | return 0 96 | } 97 | return t 98 | } 99 | 100 | func (t ObjectType) String() string { 101 | s, ok := objTypeMap[t] 102 | if !ok { 103 | return fmt.Sprintf("Unknown (%d)", t) 104 | } 105 | return fmt.Sprintf("%s", s) 106 | } 107 | 108 | type ObjectInstance uint32 109 | 110 | type ObjectID struct { 111 | Type ObjectType `json:"type"` 112 | Instance ObjectInstance `json:"instance"` 113 | } 114 | 115 | // String returns a pretty print of the ObjectID structure 116 | func (id ObjectID) String() string { 117 | return fmt.Sprintf("Instance: %d Type: %s", id.Instance, id.Type.String()) 118 | } 119 | 120 | type Object struct { 121 | Name string 122 | Description string 123 | ID ObjectID 124 | Properties []Property `json:",omitempty"` 125 | } 126 | -------------------------------------------------------------------------------- /btypes/object_map.go: -------------------------------------------------------------------------------- 1 | package btypes 2 | 3 | import "encoding/json" 4 | 5 | type ObjectMap map[ObjectType]map[ObjectInstance]Object 6 | 7 | // Len returns the total number of entries within the object map. 8 | func (o ObjectMap) Len() int { 9 | counter := 0 10 | for _, t := range o { 11 | for _ = range t { 12 | counter++ 13 | } 14 | 15 | } 16 | return counter 17 | } 18 | 19 | func (om ObjectMap) MarshalJSON() ([]byte, error) { 20 | m := make(map[string]map[ObjectInstance]Object) 21 | for typ, sub := range om { 22 | key := typ.String() 23 | if m[key] == nil { 24 | m[key] = make(map[ObjectInstance]Object) 25 | } 26 | for inst, obj := range sub { 27 | m[key][inst] = obj 28 | } 29 | } 30 | return json.Marshal(m) 31 | } 32 | 33 | func (om ObjectMap) UnmarshalJSON(data []byte) error { 34 | m := make(map[string]map[ObjectInstance]Object, 0) 35 | err := json.Unmarshal(data, &m) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | for t, sub := range m { 41 | key := GetType(t) 42 | if om[key] == nil { 43 | om[key] = make(map[ObjectInstance]Object) 44 | } 45 | for inst, obj := range sub { 46 | om[key][inst] = obj 47 | } 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /btypes/priority/priority.go: -------------------------------------------------------------------------------- 1 | package priority 2 | 3 | import ( 4 | "github.com/NubeDev/bacnet/btypes" 5 | "github.com/NubeDev/bacnet/helpers/data" 6 | "github.com/NubeDev/bacnet/helpers/nils" 7 | "reflect" 8 | ) 9 | 10 | func BuildFloat32(in btypes.PropertyData, objType btypes.ObjectType) (pri *Float32) { 11 | pri = &Float32{} 12 | _, arr := data.ToArr(in) 13 | for i, value := range arr { 14 | var returnValue *float32 15 | typeOf := reflect.TypeOf(value) 16 | if typeOf.Name() != "Null" { 17 | if objType == btypes.BinaryOutput || objType == btypes.BinaryValue { //convert from uint32 18 | f := value.(uint32) 19 | flt := float32(f) 20 | returnValue = nils.NewFloat32(flt) 21 | } else { 22 | f := value.(uint32) 23 | flt := float32(f) 24 | returnValue = nils.NewFloat32(flt) 25 | } 26 | } 27 | switch i { 28 | case 0: 29 | pri.P1 = returnValue 30 | case 1: 31 | pri.P2 = returnValue 32 | case 2: 33 | pri.P3 = returnValue 34 | case 3: 35 | pri.P4 = returnValue 36 | case 4: 37 | pri.P5 = returnValue 38 | case 5: 39 | pri.P6 = returnValue 40 | case 6: 41 | pri.P7 = returnValue 42 | case 7: 43 | pri.P8 = returnValue 44 | case 8: 45 | pri.P9 = returnValue 46 | case 9: 47 | pri.P10 = returnValue 48 | case 10: 49 | pri.P11 = returnValue 50 | case 11: 51 | pri.P12 = returnValue 52 | case 12: 53 | pri.P13 = returnValue 54 | case 13: 55 | pri.P14 = returnValue 56 | case 14: 57 | pri.P15 = returnValue 58 | case 15: 59 | pri.P16 = returnValue 60 | default: 61 | } 62 | } 63 | return 64 | } 65 | 66 | type Float32 struct { 67 | P1 *float32 `json:"_1"` 68 | P2 *float32 `json:"_2"` 69 | P3 *float32 `json:"_3"` 70 | P4 *float32 `json:"_4"` 71 | P5 *float32 `json:"_5"` 72 | P6 *float32 `json:"_6"` 73 | P7 *float32 `json:"_7"` 74 | P8 *float32 `json:"_8"` 75 | P9 *float32 `json:"_9"` 76 | P10 *float32 `json:"_10"` 77 | P11 *float32 `json:"_11"` 78 | P12 *float32 `json:"_12"` 79 | P13 *float32 `json:"_13"` 80 | P14 *float32 `json:"_14"` 81 | P15 *float32 `json:"_15"` 82 | P16 *float32 `json:"_16"` 83 | } 84 | 85 | func (p *Float32) HighestFloat32() *float32 { 86 | if p.P1 != nil { 87 | return p.P1 88 | } 89 | if p.P2 != nil { 90 | return p.P2 91 | } 92 | if p.P3 != nil { 93 | return p.P3 94 | } 95 | if p.P4 != nil { 96 | return p.P4 97 | } 98 | if p.P5 != nil { 99 | return p.P5 100 | } 101 | if p.P6 != nil { 102 | return p.P6 103 | } 104 | if p.P7 != nil { 105 | return p.P7 106 | } 107 | if p.P8 != nil { 108 | return p.P8 109 | } 110 | if p.P9 != nil { 111 | return p.P9 112 | } 113 | if p.P10 != nil { 114 | return p.P10 115 | } 116 | if p.P11 != nil { 117 | return p.P11 118 | } 119 | if p.P12 != nil { 120 | return p.P12 121 | } 122 | if p.P13 != nil { 123 | return p.P13 124 | } 125 | if p.P14 != nil { 126 | return p.P14 127 | } 128 | if p.P15 != nil { 129 | return p.P15 130 | } 131 | if p.P16 != nil { 132 | return p.P16 133 | } 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /btypes/segmentation/segmentation.go: -------------------------------------------------------------------------------- 1 | package segmentation 2 | 3 | //BACnetSegmentation: 4 | //segmented-both:0 5 | //segmented-transmit:1 6 | //segmented-receive:2 7 | //no-segmentation: 3 8 | 9 | type SegmentedType uint8 10 | 11 | //go:generate stringer -type=SegmentedType 12 | const ( 13 | SegmentedBoth SegmentedType = 0x00 14 | SegmentedTransmit SegmentedType = 0x01 15 | SegmentedReceive SegmentedType = 0x02 16 | NoSegmentation SegmentedType = 0x03 17 | ) 18 | -------------------------------------------------------------------------------- /btypes/segmentation/segmentedtype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=SegmentedType"; DO NOT EDIT. 2 | 3 | package segmentation 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[SegmentedBoth-0] 12 | _ = x[SegmentedTransmit-1] 13 | _ = x[SegmentedReceive-2] 14 | _ = x[NoSegmentation-3] 15 | } 16 | 17 | const _SegmentedType_name = "SegmentedBothSegmentedTransmitSegmentedReceiveNoSegmentation" 18 | 19 | var _SegmentedType_index = [...]uint8{0, 13, 30, 46, 60} 20 | 21 | func (i SegmentedType) String() string { 22 | if i >= SegmentedType(len(_SegmentedType_index)-1) { 23 | return "SegmentedType(" + strconv.FormatInt(int64(i), 10) + ")" 24 | } 25 | return _SegmentedType_name[_SegmentedType_index[i]:_SegmentedType_index[i+1]] 26 | } 27 | -------------------------------------------------------------------------------- /btypes/services/services_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | pprint "github.com/NubeDev/bacnet/helpers/print" 8 | ) 9 | 10 | func TestSupported(t *testing.T) { 11 | 12 | //Object to store name and supported values for sorting 13 | type supportedObject struct { 14 | Name string 15 | Supported bool 16 | } 17 | 18 | ss := Supported{} 19 | //Imported array goes here - change name & references 20 | arrayTest := []bool{false, false, false, false, false, false, false, false, false, false, false, false, true, false, true, true, false, false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false} 21 | 22 | //Creating array of services map in the correct order & size 23 | var servicesSize = len(ss.ListAll()) 24 | orderedArray := make([]supportedObject, servicesSize) 25 | 26 | //Sorting services map to array 27 | for supported := range ss.ListAll() { 28 | //Assigning supported value from bool array 29 | supported.Supported = arrayTest[supported.Index] 30 | 31 | //Adding objects to sorted array 32 | obj := new(supportedObject) 33 | obj.Name = supported.Name 34 | obj.Supported = supported.Supported 35 | orderedArray[supported.Index] = *obj 36 | } 37 | //Printing sorted array of objects 38 | for i, v := range orderedArray { 39 | var supportedStatus string 40 | 41 | if v.Supported == true { 42 | supportedStatus = "Supported" 43 | } 44 | if v.Supported == false { 45 | supportedStatus = "Not Supported" 46 | } 47 | fmt.Println(i, v.Name+":", supportedStatus) 48 | } 49 | 50 | pprint.PrintJOSN(orderedArray) 51 | } 52 | -------------------------------------------------------------------------------- /cmd/cmd/discover.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/NubeDev/bacnet" 6 | "github.com/NubeDev/bacnet/btypes" 7 | "github.com/sirupsen/logrus" 8 | "github.com/spf13/cobra" 9 | "os" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | var runDiscover bool 15 | var scanSize uint32 16 | var printStdout bool 17 | var verbose bool 18 | var concurrency int 19 | var output string 20 | 21 | var discoverCmd = &cobra.Command{ 22 | Use: "discover", 23 | Short: "discover finds all devices on the network saves results", 24 | Long: `discover finds all devices on the network saves results`, 25 | 26 | Run: discover, 27 | } 28 | 29 | func save(outfile string, stdout bool, results interface{}) error { 30 | var file *os.File 31 | var err error 32 | if printStdout { 33 | file = os.Stdout 34 | } else { 35 | file, err = os.Create(outfile) 36 | 37 | if err != nil { 38 | return err 39 | } 40 | defer file.Close() 41 | } 42 | enc := json.NewEncoder(file) 43 | enc.SetIndent("", " ") 44 | return enc.Encode(results) 45 | } 46 | 47 | func discover(cmd *cobra.Command, args []string) { 48 | log := logrus.New() 49 | log.Formatter = &logrus.TextFormatter{} 50 | log.Out = os.Stdout 51 | log.SetLevel(logrus.DebugLevel) 52 | var err error 53 | 54 | wh := &bacnet.WhoIsOpts{} 55 | 56 | cb := &bacnet.ClientBuilder{ 57 | Interface: Interface, 58 | Port: Port, 59 | } 60 | c, _ := bacnet.NewClient(cb) 61 | defer c.Close() 62 | go c.ClientRun() 63 | 64 | log.Printf("Discovering on interface %s and port %d", Interface, Port) 65 | start := time.Now() 66 | 67 | var wg sync.WaitGroup 68 | wg.Add(concurrency) 69 | scan := make(chan []btypes.Device, concurrency) 70 | merge := make(chan btypes.Device, concurrency) 71 | 72 | // Further discovers new points found in who is 73 | for i := 0; i < concurrency; i++ { 74 | go func() { 75 | for devs := range scan { 76 | for _, d := range devs { 77 | log.Infof("Found device: %d", d.ID.Instance) 78 | dev, err := c.Objects(d) 79 | 80 | if err != nil { 81 | log.Error(err) 82 | continue 83 | } 84 | merge <- dev 85 | } 86 | } 87 | wg.Done() 88 | }() 89 | 90 | } 91 | 92 | // combine results 93 | var results []btypes.Device 94 | repeats := make(map[btypes.ObjectInstance]struct{}) 95 | counter := 0 96 | total := 0 97 | go func() { 98 | for dev := range merge { 99 | if _, ok := repeats[dev.ID.Instance]; ok { 100 | log.Errorf("Receive repeated device %d", dev.ID.Instance) 101 | continue 102 | } 103 | log.Infof("Merged: %d", dev.ID.Instance) 104 | repeats[dev.ID.Instance] = struct{}{} 105 | if len(dev.Objects) > 0 { 106 | counter++ 107 | } 108 | total++ 109 | results = append(results, dev) 110 | } 111 | }() 112 | 113 | // Initiates who is 114 | var startRange, endRange, i int 115 | incr := int(scanSize) 116 | 117 | min := func(a, b int) int { 118 | if a < b { 119 | return a 120 | } 121 | return b 122 | } 123 | for i = 0; i < btypes.MaxInstance/int(scanSize); i++ { 124 | startRange = i * incr 125 | endRange = min((i+1)*incr-1, btypes.MaxInstance) 126 | log.Infof("Scanning %d to %d", startRange, endRange) 127 | wh.Low = startRange 128 | wh.High = startRange 129 | scanned, err := c.WhoIs(wh) 130 | if err != nil { 131 | log.Error(err) 132 | continue 133 | } 134 | scan <- scanned 135 | } 136 | close(scan) 137 | wg.Wait() 138 | close(merge) 139 | 140 | err = save(output, printStdout, results) 141 | if err != nil { 142 | log.Errorf("unable to save document: %v", err) 143 | } 144 | delta := time.Now().Sub(start) 145 | log.Infof("Discovery completed in %s", delta) 146 | if !printStdout { 147 | log.Infof("Results saved in %s", output) 148 | } 149 | log.Infof("%d/%d has values", counter, total) 150 | } 151 | 152 | func init() { 153 | scanSizeDescription := `scan size limits 154 | the number of devices that are being read at once` 155 | 156 | RootCmd.AddCommand(discoverCmd) 157 | discoverCmd.Flags().Uint32VarP(&scanSize, "size", "s", 1000, scanSizeDescription) 158 | discoverCmd.Flags().BoolVar(&printStdout, "stdout", false, "Print to stdout") 159 | discoverCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Print to additional debugging information") 160 | discoverCmd.Flags().IntVarP(&concurrency, "concurency", "c", 5, `Number of 161 | concurrent threads used for scanning the network. A higher number of 162 | concurrent workers can result in an oversaturate network but will result in 163 | a faster scan. Concurrency must be greater then 2.`) 164 | discoverCmd.Flags().StringVarP(&output, "output", "o", "out.json", "Save data to output filename. This field is ignored if stdout is true") 165 | } 166 | -------------------------------------------------------------------------------- /cmd/cmd/old/readmultiprop.go: -------------------------------------------------------------------------------- 1 | package cmd2 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet" 6 | "github.com/NubeDev/bacnet/btypes" 7 | "github.com/spf13/cobra" 8 | "log" 9 | ) 10 | 11 | // readMultiCmd represents the readMultiCmd command 12 | var readMultiCmd = &cobra.Command{ 13 | Use: "multi", 14 | Short: "A brief description of your command", 15 | Long: `A longer description that spans multiple lines and likely contains examples 16 | and usage of using your command. For example: 17 | 18 | Cobra is a CLI library for Go that empowers applications. 19 | This application is a tool to generate the needed files 20 | to quickly create a Cobra application.`, 21 | Run: readMulti, 22 | } 23 | 24 | var ( 25 | deviceID = 1 26 | listProperties = false 27 | startRange = 1 28 | endRange = 1 29 | ) 30 | 31 | func readMulti(cmd *cobra.Command, args []string) { 32 | if listProperties { 33 | btypes.PrintAllProperties() 34 | return 35 | } 36 | cb := &bacnet.ClientBuilder{ 37 | Interface: "", 38 | Port: 2, 39 | } 40 | c, _ := bacnet.NewClient(cb) 41 | defer c.Close() 42 | go c.ClientRun() 43 | wh := &bacnet.WhoIsOpts{} 44 | wh.Low = startRange 45 | wh.High = endRange 46 | // We need the actual address of the device first. 47 | resp, err := c.WhoIs(wh) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | 52 | if len(resp) == 0 { 53 | log.Fatal("Device id was not found on the network.") 54 | } 55 | 56 | for _, d := range resp { 57 | dest := d 58 | 59 | rp := btypes.PropertyData{ 60 | Object: btypes.Object{ 61 | ID: btypes.ObjectID{ 62 | Type: 8, 63 | Instance: btypes.ObjectInstance(deviceID), 64 | }, 65 | Properties: []btypes.Property{ 66 | btypes.Property{ 67 | Type: btypes.PropObjectList, 68 | ArrayIndex: bacnet.ArrayAll, 69 | }, 70 | }, 71 | }, 72 | } 73 | 74 | out, err := c.ReadProperty(dest, rp) 75 | if err != nil { 76 | log.Fatal(err) 77 | return 78 | } 79 | ids, ok := out.Object.Properties[0].Data.([]interface{}) 80 | if !ok { 81 | fmt.Println("unable to get object list") 82 | return 83 | } 84 | 85 | rpm := btypes.MultiplePropertyData{} 86 | rpm.Objects = make([]btypes.Object, len(ids)) 87 | for i, raw_id := range ids { 88 | id, ok := raw_id.(btypes.ObjectID) 89 | if !ok { 90 | log.Printf("unable to read object id %v\n", raw_id) 91 | return 92 | } 93 | rpm.Objects[i].ID = id 94 | 95 | rpm.Objects[i].Properties = []btypes.Property{ 96 | btypes.Property{ 97 | Type: btypes.PropObjectName, 98 | ArrayIndex: bacnet.ArrayAll, 99 | }, 100 | btypes.Property{ 101 | Type: btypes.PropDescription, 102 | ArrayIndex: bacnet.ArrayAll, 103 | }, 104 | } 105 | } 106 | 107 | x, err := c.ReadMultiProperty(dest, rpm) 108 | if err != nil { 109 | log.Println(err) 110 | } 111 | fmt.Println(x) 112 | } 113 | } 114 | 115 | func init() { 116 | //RootCmd.AddCommand(readMultiCmd) 117 | //readCmd.AddCommand(readMultiCmd) 118 | //readMultiCmd.Flags().IntVarP(&startRange, "start", "s", -1, "Start range of discovery") 119 | //readMultiCmd.Flags().IntVarP(&endRange, "end", "e", int(0xBAC0), "End range of discovery") 120 | 121 | } 122 | -------------------------------------------------------------------------------- /cmd/cmd/readmultiprop.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet" 6 | "github.com/NubeDev/bacnet/btypes" 7 | "github.com/NubeDev/bacnet/helpers/data" 8 | ip2bytes "github.com/NubeDev/bacnet/helpers/ipbytes" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // readMultiCmd represents the readMultiCmd command 14 | var readMultiCmd = &cobra.Command{ 15 | Use: "multi", 16 | Short: "A brief description of your command", 17 | Long: `A longer description that spans multiple lines and likely contains examples 18 | and usage of using your command. For example: 19 | 20 | Cobra is a CLI library for Go that empowers applications. 21 | This application is a tool to generate the needed files 22 | to quickly create a Cobra application.`, 23 | Run: readMulti, 24 | } 25 | 26 | func readMulti(cmd *cobra.Command, args []string) { 27 | if listProperties { 28 | btypes.PrintAllProperties() 29 | return 30 | } 31 | cb := &bacnet.ClientBuilder{ 32 | Interface: Interface, 33 | Port: Port, 34 | } 35 | c, _ := bacnet.NewClient(cb) 36 | defer c.Close() 37 | go c.ClientRun() 38 | 39 | ip, err := ip2bytes.New(deviceIP, uint16(devicePort)) 40 | if err != nil { 41 | return 42 | } 43 | 44 | addr := btypes.Address{ 45 | Net: uint16(networkNumber), 46 | Mac: ip, 47 | Adr: []uint8{uint8(deviceHardwareMac)}, 48 | } 49 | 50 | object := btypes.ObjectID{ 51 | Type: btypes.DeviceType, 52 | Instance: btypes.ObjectInstance(deviceID), 53 | } 54 | 55 | //get max adpu len 56 | rp := btypes.PropertyData{ 57 | Object: btypes.Object{ 58 | ID: btypes.ObjectID{ 59 | Type: btypes.DeviceType, 60 | Instance: btypes.ObjectInstance(deviceID), 61 | }, 62 | Properties: []btypes.Property{ 63 | btypes.Property{ 64 | Type: btypes.PropMaxAPDU, 65 | ArrayIndex: bacnet.ArrayAll, 66 | }, 67 | }, 68 | }, 69 | } 70 | 71 | dest := btypes.Device{ 72 | ID: object, 73 | Addr: addr, 74 | } 75 | // get the device MaxApdu 76 | out, err := c.ReadProperty(dest, rp) 77 | if err != nil { 78 | log.Fatal(err) 79 | return 80 | } 81 | 82 | _, dest.MaxApdu = data.ToUint32(out) 83 | 84 | fmt.Println("MaxApdu", dest.MaxApdu) 85 | 86 | //get object list 87 | rp = btypes.PropertyData{ 88 | Object: btypes.Object{ 89 | ID: btypes.ObjectID{ 90 | Type: 8, 91 | Instance: btypes.ObjectInstance(deviceID), 92 | }, 93 | Properties: []btypes.Property{ 94 | btypes.Property{ 95 | Type: btypes.PropObjectList, 96 | ArrayIndex: bacnet.ArrayAll, 97 | }, 98 | }, 99 | }, 100 | } 101 | 102 | // get the device object list 103 | out, err = c.ReadProperty(dest, rp) 104 | if err != nil { 105 | log.Fatal(err) 106 | return 107 | } 108 | 109 | rpm := btypes.MultiplePropertyData{} 110 | 111 | rpm.Objects = []btypes.Object{ 112 | btypes.Object{ 113 | ID: btypes.ObjectID{ 114 | Type: btypes.AnalogOutput, 115 | Instance: 1, 116 | }, 117 | Properties: []btypes.Property{ 118 | { 119 | Type: btypes.PropObjectName, 120 | ArrayIndex: bacnet.ArrayAll, 121 | }, 122 | }, 123 | }, 124 | btypes.Object{ 125 | ID: btypes.ObjectID{ 126 | Type: btypes.AnalogOutput, 127 | Instance: 1, 128 | }, 129 | Properties: []btypes.Property{ 130 | { 131 | Type: btypes.PropPresentValue, 132 | ArrayIndex: bacnet.ArrayAll, 133 | }, 134 | }, 135 | }, 136 | btypes.Object{ 137 | ID: btypes.ObjectID{ 138 | Type: btypes.AnalogOutput, 139 | Instance: 2, 140 | }, 141 | Properties: []btypes.Property{ 142 | { 143 | Type: btypes.PropObjectName, 144 | ArrayIndex: bacnet.ArrayAll, 145 | }, 146 | }, 147 | }, 148 | btypes.Object{ 149 | ID: btypes.ObjectID{ 150 | Type: btypes.AnalogOutput, 151 | Instance: 2, 152 | }, 153 | Properties: []btypes.Property{ 154 | { 155 | Type: btypes.PropPresentValue, 156 | ArrayIndex: bacnet.ArrayAll, 157 | }, 158 | }, 159 | }, 160 | } 161 | 162 | //fmt.Println(rpm) 163 | rpmRes, err := c.ReadMultiProperty(dest, rpm) 164 | if err != nil { 165 | log.Println(err) 166 | } 167 | fmt.Println(rpmRes) 168 | 169 | } 170 | 171 | func init() { 172 | RootCmd.AddCommand(readMultiCmd) 173 | readMultiCmd.PersistentFlags().IntVarP(&deviceID, "device", "d", 1234, "device id") 174 | readMultiCmd.Flags().StringVarP(&deviceIP, "address", "", "192.168.15.202", "device ip") 175 | readMultiCmd.Flags().IntVarP(&devicePort, "dport", "", 47808, "device port") 176 | readMultiCmd.Flags().IntVarP(&networkNumber, "network", "", 0, "bacnet network number") 177 | readMultiCmd.Flags().IntVarP(&deviceHardwareMac, "mstp", "", 0, "device hardware mstp addr") 178 | 179 | } 180 | -------------------------------------------------------------------------------- /cmd/cmd/readprop.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | 8 | "github.com/NubeDev/bacnet" 9 | "github.com/NubeDev/bacnet/btypes" 10 | "github.com/NubeDev/bacnet/btypes/services" 11 | pprint "github.com/NubeDev/bacnet/helpers/print" 12 | "github.com/NubeDev/bacnet/network" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // Flags 17 | var ( 18 | networkNumber int 19 | deviceID int 20 | deviceIP string 21 | devicePort int 22 | deviceHardwareMac int 23 | objectID int 24 | objectType int 25 | arrayIndex uint32 26 | propertyType string 27 | listProperties bool 28 | segmentation int 29 | maxADPU int 30 | getDevicePoints bool 31 | ) 32 | 33 | // readCmd represents the read command 34 | var readCmd = &cobra.Command{ 35 | Use: "read", 36 | Short: "Prints out a device's object's property", 37 | Long: ` 38 | Given a device's object instance and selected property, we print the value 39 | stored there. There are some autocomplete features to try and minimize the 40 | amount of arguments that need to be passed, but do take into consideration 41 | this discovery process may cause longer reads. 42 | `, 43 | Run: readProp, 44 | } 45 | 46 | func readProp(cmd *cobra.Command, args []string) { 47 | 48 | localDevice, err := network.New(&network.Network{Interface: Interface, Port: Port}) 49 | if err != nil { 50 | fmt.Println("ERR-client", err) 51 | return 52 | } 53 | defer localDevice.NetworkClose() 54 | go localDevice.NetworkRun() 55 | 56 | device, err := network.NewDevice(localDevice, &network.Device{Ip: deviceIP, DeviceID: deviceID, NetworkNumber: networkNumber, MacMSTP: deviceHardwareMac, MaxApdu: uint32(maxADPU), Segmentation: uint32(segmentation)}) 57 | if err != nil { 58 | return 59 | } 60 | 61 | if getDevicePoints { 62 | points, err := device.GetDevicePoints(btypes.ObjectInstance(deviceID)) 63 | if err != nil { 64 | return 65 | } 66 | for _, p := range points { 67 | fmt.Println("pnt----------pnt----------", p.Name) 68 | pprint.PrintJOSN(p) 69 | //fmt.Println(p.ObjectType) 70 | } 71 | return 72 | } 73 | 74 | var propInt btypes.PropertyType 75 | // Check to see if an int was passed 76 | if i, err := strconv.Atoi(propertyType); err == nil { 77 | propInt = btypes.PropertyType(uint32(i)) 78 | } else { 79 | propInt, err = btypes.Get(propertyType) 80 | } 81 | 82 | obj := &network.Object{ 83 | ObjectID: btypes.ObjectInstance(objectID), 84 | ObjectType: btypes.ObjectType(objectType), 85 | Prop: propInt, 86 | ArrayIndex: arrayIndex, //btypes.ArrayAll 87 | 88 | } 89 | read, err := device.Read(obj) 90 | pprint.PrintJOSN(read) 91 | fmt.Println(read.Object.Properties[0].Data) 92 | 93 | arr := read.Object.Properties[0].Data.(*btypes.BitString) 94 | for i, aa := range arr.GetValue() { 95 | fmt.Println(i, aa) 96 | fmt.Println("TYPE", reflect.TypeOf(aa)) 97 | } 98 | fmt.Println("TYPE", reflect.TypeOf(read.Object.Properties[0].Data)) 99 | //fmt.Println(1111, arr) 100 | //for i, a := range arr { 101 | // fmt.Println(i, a) 102 | //} 103 | ss := services.Supported{} 104 | ss.ListAll() 105 | } 106 | func init() { 107 | // Descriptions are kept separate for legibility purposes. 108 | propertyTypeDescr := `type of read that will be done. Support both the 109 | property type as an integer or as a string. e.g. PropObjectName or 77 are both 110 | support. Run --list to see available properties.` 111 | listPropertiesDescr := `list all string versions of properties that are 112 | support by property flag` 113 | 114 | RootCmd.AddCommand(readCmd) 115 | 116 | // Pass flags to children 117 | readCmd.PersistentFlags().IntVarP(&deviceID, "device", "", 0, "device id") 118 | readCmd.Flags().StringVarP(&deviceIP, "address", "", "192.168.15.202", "device ip") 119 | readCmd.Flags().IntVarP(&devicePort, "dport", "", 47808, "device port") 120 | readCmd.Flags().IntVarP(&networkNumber, "network", "", 0, "bacnet network number") 121 | readCmd.Flags().IntVarP(&deviceHardwareMac, "mstp", "", 0, "device hardware mstp addr") 122 | readCmd.Flags().IntVarP(&maxADPU, "adpu", "", 0, "device max adpu") 123 | readCmd.Flags().IntVarP(&segmentation, "seg", "", 0, "device segmentation") 124 | readCmd.Flags().IntVarP(&objectID, "objectID", "", -1, "object ID") 125 | readCmd.Flags().IntVarP(&objectType, "objectType", "", 8, "object type") 126 | readCmd.Flags().StringVarP(&propertyType, "property", "", btypes.ObjectNameStr, propertyTypeDescr) 127 | readCmd.Flags().Uint32Var(&arrayIndex, "index", bacnet.ArrayAll, "Which position to return.") 128 | 129 | readCmd.PersistentFlags().BoolVarP(&listProperties, "list", "l", false, listPropertiesDescr) 130 | 131 | readCmd.PersistentFlags().BoolVarP(&getDevicePoints, "device-points", "", false, "get device points list") 132 | } 133 | -------------------------------------------------------------------------------- /cmd/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet/helpers/homedir" 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/viper" 8 | "os" 9 | ) 10 | 11 | var cfgFile string 12 | var Interface string 13 | var Port int 14 | 15 | // RootCmd represents the base command when called without any subcommands 16 | var RootCmd = &cobra.Command{ 17 | Use: "baccli", 18 | Short: "description", 19 | Long: `description`, 20 | } 21 | 22 | // Execute adds all child commands to the root command and sets flags appropriately. 23 | // This is called by main.main(). It only needs to happen once to the rootCmd. 24 | func Execute() { 25 | if err := RootCmd.Execute(); err != nil { 26 | fmt.Println(err) 27 | os.Exit(1) 28 | } 29 | } 30 | 31 | func init() { 32 | cobra.OnInitialize(initConfig) 33 | 34 | RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.baccli.yaml)") 35 | RootCmd.PersistentFlags().StringVarP(&Interface, "interface", "i", "eth0", "Interface e.g. eth0") 36 | RootCmd.PersistentFlags().IntVarP(&Port, "port", "p", int(0xBAC0), "Port") 37 | 38 | RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 39 | 40 | // We want to allow this to be accessed 41 | viper.BindPFlag("interface", RootCmd.PersistentFlags().Lookup("interface")) 42 | viper.BindPFlag("port", RootCmd.PersistentFlags().Lookup("port")) 43 | } 44 | 45 | // initConfig reads in config file and ENV variables if set. 46 | func initConfig() { 47 | if cfgFile != "" { 48 | // Use config file from the flag. 49 | viper.SetConfigFile(cfgFile) 50 | } else { 51 | // Find home directory. 52 | home, err := homedir.Dir() 53 | if err != nil { 54 | fmt.Println(err) 55 | os.Exit(1) 56 | } 57 | 58 | // Search config in home directory with name ".baccli" (without extension). 59 | viper.AddConfigPath(home) 60 | viper.SetConfigName(".baccli") 61 | } 62 | 63 | viper.AutomaticEnv() // read in environment variables that match 64 | 65 | // If a config file is found, read it in. 66 | if err := viper.ReadInConfig(); err == nil { 67 | fmt.Println("Using config file:", viper.ConfigFileUsed()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /cmd/cmd/whoIs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet" 6 | pprint "github.com/NubeDev/bacnet/helpers/print" 7 | "github.com/NubeDev/bacnet/network" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // Flags 12 | var startRange int 13 | var endRange int 14 | 15 | var outputFilename string 16 | 17 | // whoIsCmd represents the whoIs command 18 | var whoIsCmd = &cobra.Command{ 19 | Use: "whois", 20 | Short: "BACnet device discovery", 21 | Long: `whoIs does a bacnet network discovery to find devices in the network 22 | given the provided range.`, 23 | Run: main, 24 | } 25 | 26 | func main(cmd *cobra.Command, args []string) { 27 | 28 | client, err := network.New(&network.Network{Interface: Interface, Port: Port}) 29 | if err != nil { 30 | fmt.Println("ERR-client", err) 31 | return 32 | } 33 | defer client.NetworkClose(false) 34 | go client.NetworkRun() 35 | 36 | if runDiscover { 37 | //device, err := network.NewDevice(client, &network.Device{Ip: deviceIP, Port: Port}) 38 | //err = device.DeviceDiscover() 39 | fmt.Println(err) 40 | return 41 | } 42 | 43 | wi := &bacnet.WhoIsOpts{ 44 | High: endRange, 45 | Low: startRange, 46 | GlobalBroadcast: true, 47 | NetworkNumber: uint16(networkNumber), 48 | } 49 | 50 | whoIs, err := client.Whois(wi) 51 | if err != nil { 52 | fmt.Println("ERR-whoIs", err) 53 | return 54 | } 55 | 56 | pprint.PrintJOSN(whoIs) 57 | 58 | whoIs, err = client.Whois(wi) 59 | if err != nil { 60 | fmt.Println("ERR-whoIs", err) 61 | return 62 | } 63 | fmt.Println("whois 2nd") 64 | pprint.PrintJOSN(whoIs) 65 | } 66 | 67 | func init() { 68 | RootCmd.AddCommand(whoIsCmd) 69 | whoIsCmd.Flags().BoolVar(&runDiscover, "discover", false, "run network discover") 70 | whoIsCmd.Flags().IntVarP(&startRange, "start", "s", -1, "Start range of discovery") 71 | whoIsCmd.Flags().IntVarP(&endRange, "end", "e", int(0xBAC0), "End range of discovery") 72 | whoIsCmd.Flags().IntVarP(&networkNumber, "network", "", 0, "network number") 73 | whoIsCmd.Flags().StringVarP(&outputFilename, "out", "o", "", "Output results into the given filename in json structure.") 74 | } 75 | -------------------------------------------------------------------------------- /cmd/cmd/writeprop.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet" 6 | "github.com/NubeDev/bacnet/btypes" 7 | "github.com/NubeDev/bacnet/btypes/null" 8 | ip2bytes "github.com/NubeDev/bacnet/helpers/ipbytes" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/spf13/cobra" 11 | "strconv" 12 | ) 13 | 14 | // write represents the write command 15 | var writeCmd = &cobra.Command{ 16 | Use: "write", 17 | Short: "A brief description of your command", 18 | Long: `A longer description that spans multiple lines and likely contains examples 19 | and usage of using your command. For example: 20 | 21 | Cobra is a CLI library for Go that empowers applications. 22 | This application is a tool to generate the needed files 23 | to quickly create a Cobra application.`, 24 | Run: writeProp, 25 | } 26 | 27 | // Flags 28 | var ( 29 | targetValue string 30 | priority uint 31 | isNull bool 32 | ) 33 | 34 | func init() { 35 | // Descriptions are kept separate for legibility purposes. 36 | propertyTypeDescr := `type of read that will be done. Support both the 37 | property type as an integer or as a string. e.g. PropObjectName or 77 are both 38 | support. Run --list to see available properties.` 39 | listPropertiesDescr := `list all string versions of properties that are 40 | support by property flag` 41 | 42 | RootCmd.AddCommand(writeCmd) 43 | // Pass flags to children 44 | writeCmd.PersistentFlags().IntVarP(&deviceID, "device", "d", 1234, "device id") 45 | writeCmd.Flags().StringVarP(&deviceIP, "address", "", "192.168.15.202", "device ip") 46 | writeCmd.Flags().IntVarP(&devicePort, "dport", "", 47808, "device port") 47 | writeCmd.Flags().IntVarP(&networkNumber, "network", "", 0, "bacnet network number") 48 | writeCmd.Flags().IntVarP(&deviceHardwareMac, "mstp", "", 0, "device hardware mstp addr") 49 | writeCmd.Flags().IntVarP(&objectID, "objectID", "o", 1234, "object ID") 50 | writeCmd.Flags().IntVarP(&objectType, "objectType", "j", 8, "object type") 51 | writeCmd.Flags().StringVarP(&propertyType, "property", "t", 52 | btypes.ObjectNameStr, propertyTypeDescr) 53 | writeCmd.Flags().StringVarP(&targetValue, "value", "v", 54 | "", "value that will be set") 55 | 56 | writeCmd.Flags().UintVar(&priority, "priority", 0, "default is the lowest priority") 57 | writeCmd.Flags().Uint32Var(&arrayIndex, "index", bacnet.ArrayAll, "Which position to return.") 58 | writeCmd.PersistentFlags().BoolVarP(&listProperties, "list", "l", false, 59 | listPropertiesDescr) 60 | 61 | writeCmd.PersistentFlags().BoolVar(&isNull, "null", false, 62 | "clear value by writting null to it.") 63 | } 64 | 65 | func writeProp(cmd *cobra.Command, args []string) { 66 | if listProperties { 67 | btypes.PrintAllProperties() 68 | return 69 | } 70 | cb := &bacnet.ClientBuilder{ 71 | Interface: Interface, 72 | Port: Port, 73 | } 74 | c, _ := bacnet.NewClient(cb) 75 | defer c.Close() 76 | go c.ClientRun() 77 | 78 | ip, err := ip2bytes.New(deviceIP, uint16(devicePort)) 79 | if err != nil { 80 | return 81 | } 82 | 83 | addr := btypes.Address{ 84 | Net: uint16(networkNumber), 85 | Mac: ip, 86 | Adr: []uint8{uint8(deviceHardwareMac)}, 87 | } 88 | object := btypes.ObjectID{ 89 | Type: 8, 90 | Instance: 1103, 91 | } 92 | 93 | dest := btypes.Device{ 94 | ID: object, 95 | Addr: addr, 96 | } 97 | 98 | var propInt btypes.PropertyType 99 | // Check to see if an int was passed 100 | if i, err := strconv.Atoi(propertyType); err == nil { 101 | propInt = btypes.PropertyType(uint32(i)) 102 | } else { 103 | propInt, err = btypes.Get(propertyType) 104 | } 105 | 106 | if btypes.IsDeviceProperty(propInt) { 107 | objectType = 8 108 | } 109 | 110 | if err != nil { 111 | log.Fatal(err) 112 | } 113 | 114 | rp := btypes.PropertyData{ 115 | Object: btypes.Object{ 116 | ID: btypes.ObjectID{ 117 | Type: btypes.ObjectType(objectType), 118 | Instance: btypes.ObjectInstance(objectID), 119 | }, 120 | Properties: []btypes.Property{ 121 | { 122 | Type: propInt, 123 | ArrayIndex: arrayIndex, 124 | Priority: btypes.NPDUPriority(priority), 125 | }, 126 | }, 127 | }, 128 | } 129 | 130 | var wp interface{} 131 | if isNull { 132 | wp = null.Null{} 133 | } else { 134 | out, err := c.ReadProperty(dest, rp) 135 | if err != nil { 136 | if rp.Object.Properties[0].Type == btypes.PropObjectList { 137 | log.Error("Note: PropObjectList reads may need to be broken up into multiple reads due to length. Read index 0 for array length") 138 | } 139 | log.Fatal(err) 140 | } 141 | if len(out.Object.Properties) == 0 { 142 | fmt.Println("No value returned") 143 | return 144 | } 145 | 146 | rd := out.Object.Properties[0].Data 147 | log.Infof("Current value %v, type %T", rd, rd) 148 | 149 | if targetValue == "" { 150 | log.Fatal("nothing was written") 151 | return 152 | } 153 | 154 | switch rd.(type) { 155 | case uint32: 156 | var f float64 157 | f, err = strconv.ParseFloat(targetValue, 32) 158 | wp = uint32(f) 159 | case float32: 160 | var f float64 161 | f, err = strconv.ParseFloat(targetValue, 32) 162 | wp = float32(f) 163 | case float64: 164 | wp, err = strconv.ParseFloat(targetValue, 64) 165 | case string: 166 | wp = targetValue 167 | default: 168 | err = fmt.Errorf("unable to handle a type %T", rd) 169 | } 170 | if err != nil { 171 | log.Printf("Expects a %T", rp.Object.Properties[0].Data) 172 | } 173 | } 174 | 175 | rp.Object.Properties[0].Data = wp 176 | // log.Printf("Writting: %v", wp) 177 | err = c.WriteProperty(dest, rp) 178 | if err != nil { 179 | log.Println(err) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/NubeDev/bacnet/cmd/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | package bacnet 2 | 3 | //fail read or write retry count 4 | const retryCount = 1 5 | 6 | // MTSP 7 | const defaultMTSPBAUD = 38400 8 | const defaultMTSPMAC = 127 9 | 10 | // General Bacnet 11 | const defaultMaxMaster = 127 12 | const defaultMaxInfoFrames = 1 13 | 14 | // ArrayAll is used when reading/writing to a property to read/write the entire 15 | // array 16 | const ArrayAll = 0xFFFFFFFF 17 | const maxStandardBacnetType = 128 18 | -------------------------------------------------------------------------------- /datalink/datalink.go: -------------------------------------------------------------------------------- 1 | package datalink 2 | 3 | import ( 4 | "github.com/NubeDev/bacnet/btypes" 5 | ) 6 | 7 | type DataLink interface { 8 | GetMyAddress() *btypes.Address 9 | GetBroadcastAddress() *btypes.Address 10 | Send(data []byte, npdu *btypes.NPDU, dest *btypes.Address) (int, error) 11 | Receive(data []byte) (*btypes.Address, int, error) 12 | Close() error 13 | } 14 | -------------------------------------------------------------------------------- /datalink/mstp.go: -------------------------------------------------------------------------------- 1 | package datalink 2 | 3 | ////go:build MSTP 4 | //// +build MSTP 5 | 6 | // 7 | // 8 | //import ( 9 | // "fmt" 10 | // "github.com/NubeDev/bacnet/btypes" 11 | // "os" 12 | // "time" 13 | // "unsafe" 14 | //) 15 | // 16 | ////libbacnet is compiled from github.com/bacnet-stack/bacnet-stack 17 | // 18 | //// #cgo CFLAGS: -DBACDL_MSTP=1 -I./include 19 | //// #cgo LDFLAGS: -L./lib -lbacnet -lWinmm 20 | //// #include 21 | //// #include 22 | //import "C" 23 | // 24 | //func cAddrToGoAddr(addr *C.BACNET_ADDRESS) *btypes.Address { 25 | // var result btypes.Address 26 | // for i := 0; i < len(result.Mac) && i < len(addr.mac); i++ { 27 | // result.Mac[i] = uint8(addr.mac[i]) 28 | // } 29 | // result.MacLen = uint8(addr.mac_len) 30 | // for i := 0; i < len(result.Adr) && i < len(addr.adr); i++ { 31 | // result.Adr[i] = uint8(addr.adr[i]) 32 | // } 33 | // result.Len = uint8(addr.len) 34 | // result.Net = uint16(addr.net) 35 | // return &result 36 | //} 37 | // 38 | //type mstpDataLink int 39 | // 40 | //func NewMSTPDataLink(ifname string, baudrate uint32) DataLink { 41 | // os.Setenv("BACNET_DATALINK ", "mstp") 42 | // name := C.CString(ifname) 43 | // defer C.free(unsafe.Pointer(name)) 44 | // C.dlmstp_set_baud_rate(C.uint(baudrate)) 45 | // C.dlmstp_init(name) 46 | // return mstpDataLink(0) 47 | //} 48 | // 49 | //func (M mstpDataLink) GetMyAddress() *btypes.Address { 50 | // var addr C.BACNET_ADDRESS 51 | // C.dlmstp_get_my_address(&addr) 52 | // return cAddrToGoAddr(&addr) 53 | //} 54 | // 55 | //func (M mstpDataLink) GetBroadcastAddress() *btypes.Address { 56 | // var addr C.BACNET_ADDRESS 57 | // C.dlmstp_get_broadcast_address(&addr) 58 | // return cAddrToGoAddr(&addr) 59 | //} 60 | // 61 | //func (M mstpDataLink) Send(data []byte, npdu *btypes.NPDU, dest *btypes.Address) (int, error) { 62 | // var addr C.BACNET_ADDRESS 63 | // for i := 0; i < len(dest.Mac) && i < len(addr.mac); i++ { 64 | // addr.mac[i] = C.uchar(dest.Mac[i]) 65 | // } 66 | // addr.mac_len = C.uchar(dest.MacLen) 67 | // for i := 0; i < len(dest.Adr) && i < len(addr.adr); i++ { 68 | // addr.adr[i] = C.uchar(dest.Adr[i]) 69 | // } 70 | // addr.len = C.uchar(dest.Len) 71 | // addr.net = C.ushort(dest.Net) 72 | // 73 | // var cnpdu C.struct_bacnet_npdu_data_t 74 | // cnpdu.protocol_version = C.uchar(npdu.Version) 75 | // cnpdu.data_expecting_reply = C.bool(npdu.ExpectingReply) 76 | // cnpdu.network_layer_message = C.bool(npdu.IsNetworkLayerMessage) 77 | // cnpdu.priority = C.BACNET_MESSAGE_PRIORITY(C.uchar(npdu.Priority)) 78 | // cnpdu.network_message_type = C.BACNET_NETWORK_MESSAGE_TYPE(C.uchar(npdu.NetworkLayerMessageType)) 79 | // cnpdu.vendor_id = C.ushort(npdu.VendorId) 80 | // cnpdu.hop_count = C.uchar(npdu.HopCount) 81 | // 82 | // n := C.dlmstp_send_pdu(&addr, &cnpdu, (*C.uchar)(&data[0]), C.uint(len(data))) 83 | // return int(n), nil 84 | //} 85 | // 86 | //func (M mstpDataLink) Receive(data []byte) (*btypes.Address, int, error) { 87 | // var addr C.BACNET_ADDRESS 88 | // n := C.dlmstp_receive(&addr, (*C.uchar)(&data[0]), C.ushort(len(data)), C.uint(time.Minute/1000000)) 89 | // if uint16(n) == 0 { 90 | // fmt.Errorf("timeout, no data received") 91 | // } 92 | // return cAddrToGoAddr(&addr), int(uint16(n)), nil 93 | //} 94 | // 95 | //func (M mstpDataLink) Close() error { 96 | // C.dlmstp_cleanup() 97 | // return nil 98 | //} 99 | -------------------------------------------------------------------------------- /datalink/udp.go: -------------------------------------------------------------------------------- 1 | package datalink 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet/btypes" 6 | "net" 7 | "strings" 8 | ) 9 | 10 | // DefaultPort that BacnetIP will use if a port is not given. Valid ports for 11 | // the bacnet protocol is between 0xBAC0 and 0xBAC9 12 | const DefaultPort = 0xBAC0 //47808 13 | 14 | type udpDataLink struct { 15 | netInterface *net.Interface 16 | myAddress, broadcastAddress *btypes.Address 17 | port int 18 | listener *net.UDPConn 19 | } 20 | 21 | /* 22 | NewUDPDataLink returns udp listener 23 | pass in your iface port by name, see an alternative NewUDPDataLinkFromIP if you wish to pass in by ip and subnet 24 | - inter: eth0 25 | - addr: 47808 26 | */ 27 | func NewUDPDataLink(inter string, port int) (link DataLink, err error) { 28 | if port == 0 { 29 | port = DefaultPort 30 | } 31 | addr := inter 32 | if !strings.ContainsRune(inter, '/') { 33 | addr, err = FindCIDRAddress(inter) 34 | if err != nil { 35 | return nil, err 36 | } 37 | } 38 | link, err = dataLink(addr, port) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return link, nil 43 | } 44 | 45 | /* 46 | NewUDPDataLinkFromIP returns udp listener 47 | - addr: 192.168.15.10 48 | - subNet: 24 49 | - addr: 47808 50 | */ 51 | func NewUDPDataLinkFromIP(addr string, subNet, port int) (link DataLink, err error) { 52 | addr = fmt.Sprintf("%s/%d", addr, subNet) 53 | link, err = dataLink(addr, port) 54 | if err != nil { 55 | return nil, err 56 | } 57 | return link, nil 58 | } 59 | 60 | func dataLink(ipAddr string, port int) (DataLink, error) { 61 | if port == 0 { 62 | port = DefaultPort 63 | } 64 | 65 | ip, ipNet, err := net.ParseCIDR(ipAddr) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | broadcast := net.IP(make([]byte, 4)) 71 | for i := range broadcast { 72 | broadcast[i] = ipNet.IP[i] | ^ipNet.Mask[i] 73 | } 74 | 75 | udp, _ := net.ResolveUDPAddr("udp4", fmt.Sprintf(":%d", port)) 76 | conn, err := net.ListenUDP("udp", udp) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | return &udpDataLink{ 82 | listener: conn, 83 | myAddress: IPPortToAddress(ip, port), 84 | broadcastAddress: IPPortToAddress(broadcast, DefaultPort), 85 | }, nil 86 | } 87 | 88 | func (c *udpDataLink) Close() error { 89 | if c.listener != nil { 90 | return c.listener.Close() 91 | } 92 | return nil 93 | } 94 | 95 | func (c *udpDataLink) Receive(data []byte) (*btypes.Address, int, error) { 96 | n, adr, err := c.listener.ReadFromUDP(data) 97 | if err != nil { 98 | return nil, n, err 99 | } 100 | adr.IP = adr.IP.To4() 101 | udpAddr := UDPToAddress(adr) 102 | return udpAddr, n, nil 103 | } 104 | 105 | func (c *udpDataLink) GetMyAddress() *btypes.Address { 106 | return c.myAddress 107 | } 108 | 109 | // GetBroadcastAddress uses the given address with subnet to return the broadcast address 110 | func (c *udpDataLink) GetBroadcastAddress() *btypes.Address { 111 | return c.broadcastAddress 112 | } 113 | 114 | func (c *udpDataLink) Send(data []byte, npdu *btypes.NPDU, dest *btypes.Address) (int, error) { 115 | // Get IP Address 116 | d, err := dest.UDPAddr() 117 | if err != nil { 118 | return 0, err 119 | } 120 | return c.listener.WriteTo(data, &d) 121 | } 122 | 123 | // IPPortToAddress converts a given udp address into a bacnet address 124 | func IPPortToAddress(ip net.IP, port int) *btypes.Address { 125 | return UDPToAddress(&net.UDPAddr{ 126 | IP: ip.To4(), 127 | Port: port, 128 | }) 129 | } 130 | 131 | // UDPToAddress converts a given udp address into a bacnet address 132 | func UDPToAddress(n *net.UDPAddr) *btypes.Address { 133 | a := &btypes.Address{} 134 | p := uint16(n.Port) 135 | // Length of IP plus the port 136 | length := net.IPv4len + 2 137 | a.Mac = make([]uint8, length) 138 | //Encode ip 139 | for i := 0; i < net.IPv4len; i++ { 140 | a.Mac[i] = n.IP[i] 141 | } 142 | // Encode port 143 | a.Mac[net.IPv4len+0] = uint8(p >> 8) 144 | a.Mac[net.IPv4len+1] = uint8(p & 0x00FF) 145 | 146 | a.MacLen = uint8(length) 147 | return a 148 | } 149 | 150 | // FindCIDRAddress find out CIDR address from net interface 151 | func FindCIDRAddress(inter string) (string, error) { 152 | i, err := net.InterfaceByName(inter) 153 | if err != nil { 154 | return "", err 155 | } 156 | 157 | uni, err := i.Addrs() 158 | if err != nil { 159 | return "", err 160 | } 161 | 162 | if len(uni) == 0 { 163 | return "", fmt.Errorf("interface %s has no addresses", inter) 164 | } 165 | 166 | // Find the first IP4 ip 167 | for _, adr := range uni { 168 | IP, _, _ := net.ParseCIDR(adr.String()) 169 | 170 | // To4 is non nil when the type is ip4 171 | if IP.To4() != nil { 172 | return adr.String(), nil 173 | } 174 | } 175 | // We couldn't find a interface or all of them are ip6 176 | return "", fmt.Errorf("no valid broadcasting address was found on interface %s", inter) 177 | } 178 | -------------------------------------------------------------------------------- /encoding/appdata.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet/btypes" 6 | "github.com/NubeDev/bacnet/btypes/null" 7 | ) 8 | 9 | const ( 10 | tagNull uint8 = 0 11 | tagBool uint8 = 1 12 | tagUint uint8 = 2 13 | tagInt uint8 = 3 14 | tagReal uint8 = 4 15 | tagDouble uint8 = 5 16 | tagOctetString uint8 = 6 17 | tagCharacterString uint8 = 7 18 | tagBitString uint8 = 8 19 | tagEnumerated uint8 = 9 20 | tagDate uint8 = 10 21 | tagTime uint8 = 11 22 | tagObjectID uint8 = 12 23 | tagReserve1 uint8 = 13 24 | tagReserve2 uint8 = 14 25 | tagReserve3 uint8 = 15 26 | maxTag uint8 = 16 27 | ) 28 | 29 | // Other values omitted here can have variable length 30 | const ( 31 | realLen uint32 = 4 32 | doubleLen uint32 = 8 33 | dateLen uint32 = 4 34 | timeLen uint32 = 4 35 | objectIDLen uint32 = 4 36 | ) 37 | 38 | // All app layer is non-context specific 39 | const appLayerContext = false 40 | 41 | func (e *Encoder) boolean(x bool) { 42 | // Boolean information is stored into the length field 43 | var length uint32 44 | if x { 45 | length = 1 46 | } else { 47 | length = 0 48 | } 49 | e.tag(tagInfo{ID: tagBool, Context: appLayerContext, Value: length}) 50 | } 51 | 52 | func (e *Encoder) real(x float32) { 53 | e.write(x) 54 | } 55 | 56 | func (d *Decoder) real(x *float32) { 57 | d.decode(x) 58 | } 59 | 60 | func (e *Encoder) double(x float64) { 61 | e.write(x) 62 | } 63 | 64 | func (d *Decoder) double(x *float64) { 65 | d.decode(x) 66 | } 67 | 68 | func (e *Encoder) AppData(i interface{}, typeBVBO bool) error { 69 | //if null used for sending null on point write to prop 87 70 | switch i.(type) { 71 | case null.Null: 72 | e.tag(tagInfo{ID: tagNull, Context: appLayerContext}) 73 | return nil 74 | } 75 | //if point type is a BO or BV then set the type to type enum (data type 9) 76 | if typeBVBO { 77 | v := i.(uint32) 78 | v, ok := i.(uint32) 79 | if ok { 80 | length := valueLength(v) 81 | e.tag(tagInfo{ID: tagEnumerated, Context: appLayerContext, Value: uint32(length)}) 82 | e.enumerated(v) 83 | return nil 84 | } else { 85 | err := fmt.Errorf("error in type convertion for boolean output or boolean value %T", i) 86 | // Set global error 87 | e.err = err 88 | return err 89 | } 90 | } 91 | switch val := i.(type) { 92 | case float32: 93 | e.tag(tagInfo{ID: tagReal, Context: appLayerContext, Value: realLen}) 94 | e.real(val) 95 | case float64: 96 | e.tag(tagInfo{ID: tagDouble, Context: appLayerContext, Value: realLen}) 97 | e.double(val) 98 | case bool: 99 | e.boolean(val) 100 | case string: 101 | // Add 1 to length to account for the encoding byte 102 | e.tag(tagInfo{ID: tagCharacterString, Context: appLayerContext, Value: uint32(len(val) + 1)}) 103 | e.string(val) 104 | case uint32: 105 | length := valueLength(val) 106 | e.tag(tagInfo{ID: tagUint, Context: appLayerContext, Value: uint32(length)}) 107 | e.unsigned(val) 108 | case int32: 109 | v := uint32(val) 110 | length := valueLength(v) 111 | e.tag(tagInfo{ID: tagInt, Context: appLayerContext, Value: uint32(length)}) 112 | e.unsigned(v) 113 | // Enumerated is pretty much a wrapper for a uint32 with an enumerated associated with it. 114 | case btypes.Enumerated: 115 | v := uint32(val) 116 | length := valueLength(v) 117 | e.tag(tagInfo{ID: tagEnumerated, Context: appLayerContext, Value: uint32(length)}) 118 | e.enumerated(v) 119 | case btypes.ObjectID: 120 | e.tag(tagInfo{ID: tagObjectID, Context: appLayerContext, Value: objectIDLen}) 121 | e.objectId(val.Type, val.Instance) 122 | case null.Null: 123 | e.tag(tagInfo{ID: tagNull, Context: appLayerContext}) 124 | default: 125 | err := fmt.Errorf("Unknown type %T", i) 126 | // Set global error 127 | e.err = err 128 | return err 129 | } 130 | return nil 131 | } 132 | 133 | func (d *Decoder) AppDataOfTag(tag uint8, len int) (interface{}, error) { 134 | switch tag { 135 | case tagNull: 136 | return null.Null{}, nil 137 | case tagBool: 138 | // Originally this was in C so non 0 values are considered 139 | // true 140 | return len > 0, d.Error() 141 | case tagUint: 142 | return d.unsigned(len), d.Error() 143 | case tagInt: 144 | return d.signed(len), d.Error() 145 | case tagReal: 146 | var x float32 147 | d.real(&x) 148 | return x, d.Error() 149 | case tagDouble: 150 | var x float64 151 | d.double(&x) 152 | return x, d.Error() 153 | case tagOctetString: 154 | var b []byte 155 | d.octetstring(&b, len) 156 | return b, d.Error() 157 | 158 | case tagCharacterString: 159 | var s string 160 | // Subtract 1 to length to account for the encoding byte 161 | err := d.string(&s, len-1) 162 | return s, err 163 | case tagBitString: 164 | return d.bitString(len), d.Error() 165 | case tagEnumerated: 166 | return d.enumerated(len), d.Error() 167 | case tagDate: 168 | var date btypes.Date 169 | d.date(&date, len) 170 | return date, d.Error() 171 | case tagTime: 172 | var t btypes.Time 173 | d.time(&t, len) 174 | return t, d.Error() 175 | case tagObjectID: 176 | objType, objInstance := d.objectId() 177 | return btypes.ObjectID{ 178 | Type: btypes.ObjectType(objType), 179 | Instance: objInstance, 180 | }, d.Error() 181 | default: 182 | return nil, fmt.Errorf("Unsupported tag: %d", tag) 183 | } 184 | } 185 | func (d *Decoder) AppData() (interface{}, error) { 186 | tag, _, lenvalue := d.tagNumberAndValue() 187 | return d.AppDataOfTag(tag, int(lenvalue)) 188 | } 189 | -------------------------------------------------------------------------------- /encoding/appdata_test.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/NubeDev/bacnet/btypes" 8 | ) 9 | 10 | func subTestSimpleData(t *testing.T, d *Decoder, x interface{}) func(t *testing.T) { 11 | return func(t *testing.T) { 12 | y, err := d.AppData() 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | if !reflect.DeepEqual(x, y) { 17 | t.Errorf("Mismatch between decrypted values. Received %v, expected %v", y, x) 18 | } 19 | } 20 | } 21 | 22 | func TestSimpleDatabtypes(t *testing.T) { 23 | t.Run("Specific function based encoding", generalSimpleDatabtypes(t, false)) 24 | t.Run("General Interface Encoding", generalSimpleDatabtypes(t, true)) 25 | } 26 | 27 | // generic states if we plan to use specific or generic encoding 28 | func generalSimpleDatabtypes(t *testing.T, generic bool) func(t *testing.T) { 29 | return func(t *testing.T) { 30 | enc := NewEncoder() 31 | var real float32 = 3.14 32 | 33 | // Default float 64 34 | double := 1234567.890 35 | boolean := false 36 | // Will be stored as an 8 bit uint 37 | var small uint32 = 12 38 | 39 | // Stored as 16 40 | var medium uint32 = 0xABF0 41 | 42 | // Stored as 24 43 | var wtf uint32 = 0x302010 44 | 45 | // Stored as 32 46 | var large uint32 = 0xFFFFFFF0 47 | 48 | str := "my pizza pizza" 49 | objID := btypes.ObjectID{93, 42} 50 | 51 | if generic { 52 | values := []interface{}{real, double, boolean, !boolean, small, medium, wtf, large, str, objID} 53 | for _, v := range values { 54 | enc.AppData(v, false) 55 | } 56 | 57 | } else { 58 | enc.tag(tagInfo{ID: tagReal, Context: appLayerContext, Value: realLen}) 59 | enc.real(real) 60 | 61 | enc.tag(tagInfo{ID: tagDouble, Context: appLayerContext, Value: doubleLen}) 62 | enc.double(double) 63 | 64 | enc.boolean(boolean) 65 | enc.boolean(!boolean) 66 | 67 | enc.tag(tagInfo{ID: tagUint, Context: appLayerContext, Value: size8}) 68 | enc.unsigned(small) 69 | 70 | enc.tag(tagInfo{ID: tagUint, Context: appLayerContext, Value: size16}) 71 | enc.unsigned(medium) 72 | 73 | enc.tag(tagInfo{ID: tagUint, Context: appLayerContext, Value: size24}) 74 | enc.unsigned(wtf) 75 | 76 | enc.tag(tagInfo{ID: tagUint, Context: appLayerContext, Value: size32}) 77 | enc.unsigned(large) 78 | 79 | enc.tag(tagInfo{ID: tagCharacterString, Context: appLayerContext, Value: uint32(len(str) + 1)}) 80 | enc.string(str) 81 | 82 | enc.tag(tagInfo{ID: tagObjectID, Context: appLayerContext, Value: objectIDLen}) 83 | enc.objectId(objID.Type, objID.Instance) 84 | } 85 | 86 | if err := enc.Error(); err != nil { 87 | t.Fatal(err) 88 | } 89 | 90 | dec := NewDecoder(enc.Bytes()) 91 | t.Run("Encoding Real", subTestSimpleData(t, dec, real)) 92 | t.Run("Encoding Double", subTestSimpleData(t, dec, double)) 93 | t.Run("Encoding Boolean", subTestSimpleData(t, dec, boolean)) 94 | t.Run("Encoding Flipped Boolean", subTestSimpleData(t, dec, !boolean)) 95 | t.Run("Encoding Uint8", subTestSimpleData(t, dec, small)) 96 | t.Run("Encoding Uint16", subTestSimpleData(t, dec, medium)) 97 | t.Run("Encoding uint24", subTestSimpleData(t, dec, wtf)) 98 | t.Run("Encoding uint32", subTestSimpleData(t, dec, large)) 99 | t.Run("Encoding string", subTestSimpleData(t, dec, str)) 100 | t.Run("Encoding object id", subTestSimpleData(t, dec, objID)) 101 | 102 | if err := dec.Error(); err != nil { 103 | t.Fatal(err) 104 | } 105 | } 106 | } 107 | 108 | func TestRandomData(t *testing.T) { 109 | b := []byte{196, 2, 3, 208, 91, 34, 9, 96, 101, 6, 10, 20, 0, 47, 186, 192, 196, 2, 3, 110 | 180, 113, 34, 9, 96, 101, 6, 10, 20, 0, 204, 186, 192, 196, 2, 0, 81, 65, 34, 9, 96, 101, 6, 10, 20, 0, 111 | 208, 186, 192} 112 | dec := NewDecoder(b) 113 | for dec.len() > 0 { 114 | res, _ := dec.AppData() 115 | t.Logf("%v", res) 116 | 117 | res, _ = dec.AppData() 118 | t.Logf("%v", res) 119 | 120 | res, _ = dec.AppData() 121 | t.Logf("%v", res) 122 | } 123 | 124 | } 125 | 126 | func TestPropertyList(t *testing.T) { 127 | b := []byte{196, 225, 0, 0, 1, 196, 2, 128, 0, 109, 196, 2, 128, 0, 94, 196, 128 | 2, 3, 180, 141, 196, 2, 128, 0, 1, 196, 2, 128, 0, 2, 196, 2, 128, 0, 3, 129 | 196, 2, 128, 0, 99, 196, 2, 128, 0, 98, 196, 2, 128, 0, 97, 196, 2, 128, 0, 130 | 96, 196, 3, 192, 0, 1, 196, 3, 192, 0, 2, 196, 3, 192, 0, 3, 196, 3, 192, 0, 131 | 4, 196, 3, 192, 0, 5, 196, 1, 128, 0, 1, 196, 1, 128, 0, 2, 196, 1, 128, 0, 132 | 3, 196, 1, 128, 0, 4, 196, 1, 128, 0, 5, 196, 1, 125, 9, 0, 196, 1, 125, 9, 133 | 1, 196, 1, 125, 9, 2, 196, 1, 125, 9, 3, 196, 1, 125, 9, 4, 196, 1, 125, 9, 134 | 5, 196, 1, 125, 9, 6, 196, 1, 125, 12, 232, 196, 2, 128, 0, 101, 196, 2, 135 | 128, 0, 102, 196, 2, 128, 0, 104, 196, 4, 0, 0, 1, 196, 0, 0, 0, 3, 196, 5, 136 | 0, 0, 3, 196, 5, 0, 0, 8, 196, 0, 64, 0, 1, 196, 5, 0, 0, 4, 196, 1, 64, 0, 137 | 6, 196, 1, 64, 0, 5, 196, 1, 64, 0, 1, 196, 0, 128, 0, 6, 196, 1, 64, 0, 7, 138 | 196, 1, 64, 0, 8, 196, 1, 64, 0, 9, 196, 1, 64, 0, 10, 196, 0, 128, 0, 9, 139 | 196, 5, 0, 0, 10, 196, 4, 64, 0, 1, 196, 0, 128, 0, 11, 196, 0, 0, 0, 1, 140 | 196, 192, 0, 0, 1, 196, 5, 0, 0, 1, 196, 5, 0, 0, 7, 196, 5, 0, 0, 12, 196, 141 | 5, 0, 0, 5, 196, 1, 64, 0, 4, 196, 1, 64, 0, 3, 196, 0, 128, 0, 4, 196, 0, 142 | 128, 0, 2, 196, 1, 64, 0, 11, 196, 0, 128, 0, 12, 196, 5, 0, 0, 13, 196, 0, 143 | 128, 0, 10, 196, 5, 0, 0, 9, 196, 0, 0, 0, 2, 196, 5, 0, 0, 2, 196, 5, 0, 0, 144 | 11, 196, 5, 0, 0, 6} 145 | dec := NewDecoder(b) 146 | for dec.len() > 0 { 147 | res, _ := dec.AppData() 148 | t.Logf("%v", res) 149 | } 150 | 151 | } 152 | 153 | // Testing for bug where a unicode is sometimes appending to the front of the string returned. 154 | func TestStringUnicode(t *testing.T) { 155 | s := "there is there such thing as too much pizza?" 156 | enc := NewEncoder() 157 | enc.AppData(s, false) 158 | 159 | dec := NewDecoder(enc.Bytes()) 160 | out, err := dec.AppData() 161 | if err != nil { 162 | t.Errorf("decoding string failed: %v", err) 163 | } 164 | 165 | outStr := out.(string) 166 | if outStr[0] != s[0] { 167 | t.Fatal("an unknown code was prepending to output") 168 | } 169 | } 170 | 171 | func TestReverseBitsInByte(t *testing.T) { 172 | var a byte = 0x0F 173 | a = byteReverseBits(a) 174 | if a != 0xF0 { 175 | t.Errorf("byteReverseBits error") 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /encoding/bvlc.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "github.com/NubeDev/bacnet/btypes" 5 | ) 6 | 7 | // Bacnet Virtual Layer Control 8 | 9 | func (e *Encoder) BVLC(b btypes.BVLC) error { 10 | // Set packet type 11 | e.write(b.Type) 12 | e.write(b.Function) 13 | e.write(b.Length) 14 | e.write(b.Data) 15 | return e.Error() 16 | } 17 | 18 | func (d *Decoder) BVLC(b *btypes.BVLC) error { 19 | d.decode(&b.Type) 20 | d.decode(&b.Function) 21 | d.decode(&b.Length) 22 | d.decode(&b.Data) 23 | return d.Error() 24 | } 25 | -------------------------------------------------------------------------------- /encoding/const.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | const MaxInstance = 0x3FFFFF 4 | const InstanceBits = 22 5 | const MaxPropertyID = 4194303 6 | 7 | const MaxAPDUOverIP = 1476 8 | const MaxAPDU = MaxAPDUOverIP 9 | 10 | const initialTagPos = 0 11 | 12 | const ( 13 | size8 = 1 14 | size16 = 2 15 | size24 = 3 16 | size32 = 4 17 | ) 18 | 19 | const ( 20 | flag16bit uint8 = 254 21 | flag32bit uint8 = 255 22 | ) 23 | 24 | // ArrayAll is an argument typically passed during a read to signify where to 25 | // read 26 | const ArrayAll uint32 = ^uint32(0) 27 | -------------------------------------------------------------------------------- /encoding/context_tag.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import "github.com/NubeDev/bacnet/btypes" 4 | 5 | /* refer to https://github.com/bacnet-stack/bacnet-stack/blob/bacnet-stack-0.9.1/src/bacapp.c#L583 */ 6 | /* returns the fixed tag type for certain context tagged properties */ 7 | func tagTypeInContext(property btypes.PropertyType, tagNumber uint8) uint8 { 8 | tag := tagNumber 9 | switch property { 10 | case btypes.PROP_ACTUAL_SHED_LEVEL: 11 | case btypes.PROP_REQUESTED_SHED_LEVEL: 12 | case btypes.PROP_EXPECTED_SHED_LEVEL: 13 | switch tagNumber { 14 | case 0, 1: 15 | tag = tagUint 16 | case 2: 17 | tag = tagReal 18 | } 19 | case btypes.PROP_ACTION: 20 | switch tagNumber { 21 | case 0, 1: 22 | tag = tagObjectID 23 | case 2: 24 | tag = tagEnumerated 25 | case 3, 5, 6: 26 | tag = tagUint 27 | case 7, 8: 28 | tag = tagBool 29 | case 4: /* propertyValue: abstract syntax */ 30 | } 31 | case btypes.PROP_LIST_OF_GROUP_MEMBERS: 32 | /* Sequence of ReadAccessSpecification */ 33 | switch tagNumber { 34 | case 0: 35 | tag = tagObjectID 36 | } 37 | case btypes.PROP_EXCEPTION_SCHEDULE: 38 | switch tagNumber { 39 | case 1: 40 | tag = tagObjectID 41 | case 3: 42 | tag = tagUint 43 | case 0: /* calendarEntry: abstract syntax + context */ 44 | case 2: /* list of BACnetTimeValue: abstract syntax */ 45 | } 46 | break 47 | case btypes.PROP_LOG_DEVICE_OBJECT_PROPERTY: 48 | switch tagNumber { 49 | case 0: /* Object ID */ 50 | fallthrough 51 | case 3: /* Device ID */ 52 | tag = tagObjectID 53 | case 1: /* Property ID */ 54 | tag = tagEnumerated 55 | case 2: /* Array index */ 56 | tag = tagUint 57 | } 58 | break 59 | case btypes.PROP_SUBORDINATE_LIST: 60 | /* BACnetARRAY[N] of BACnetDeviceObjectReference */ 61 | switch tagNumber { 62 | case 0: /* Optional Device ID */ 63 | fallthrough 64 | case 1: /* Object ID */ 65 | tag = tagObjectID 66 | } 67 | case btypes.PROP_RECIPIENT_LIST: 68 | /* List of BACnetDestination */ 69 | switch tagNumber { 70 | case 0: /* Device Object ID */ 71 | tag = tagObjectID 72 | case 1: 73 | /* 2015.08.22 EKH 135-2012 pg 708 74 | todo - Context 1 in Recipient list would be a BACnetAddress, not coded yet... 75 | BACnetRecipient::= CHOICE { 76 | device [0] BACnetObjectIdentifier, 77 | address [1] BACnetAddress 78 | } 79 | */ 80 | } 81 | break 82 | case btypes.PROP_ACTIVE_COV_SUBSCRIPTIONS: 83 | /* BACnetCOVSubscription */ 84 | switch tagNumber { 85 | case 0: /* BACnetRecipientProcess */ 86 | case 1: /* BACnetObjectPropertyReference */ 87 | case 2: /* issueConfirmedNotifications */ 88 | tag = tagBool 89 | case 3: /* timeRemaining */ 90 | tag = tagUint 91 | case 4: /* covIncrement */ 92 | tag = tagReal 93 | } 94 | } 95 | 96 | return tag 97 | } 98 | -------------------------------------------------------------------------------- /encoding/date.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import "github.com/NubeDev/bacnet/btypes" 4 | 5 | // epochYear is an increment to all non-stored values. This year is chosen in 6 | // the standard. Why? No idea. God help us all if bacnet hits the 255 + 1990 7 | // limit 8 | const epochYear = 1990 9 | 10 | // If the values == 0XFF, that means it is not specified. We will take that to 11 | const notDefined = 0xff 12 | 13 | func IsOddMonth(month int) bool { 14 | return month == 13 15 | } 16 | 17 | func IsEvenMonth(month int) bool { 18 | return month == 14 19 | } 20 | 21 | func IsLastDayOfMonth(day int) bool { 22 | return day == 32 23 | } 24 | 25 | func IsEvenDayOfMonth(day int) bool { 26 | return day == 33 27 | } 28 | 29 | func IsOddDayOfMonth(day int) bool { 30 | return day == 32 31 | } 32 | 33 | func (e *Encoder) date(dt btypes.Date) { 34 | // We don't want to override an unspecified time date 35 | if dt.Year != btypes.UnspecifiedTime { 36 | e.write(uint8(dt.Year - epochYear)) 37 | } else { 38 | e.write(uint8(dt.Year)) 39 | } 40 | e.write(uint8(dt.Month)) 41 | e.write(uint8(dt.Day)) 42 | e.write(uint8(dt.DayOfWeek)) 43 | } 44 | 45 | func (d *Decoder) date(dt *btypes.Date, length int) { 46 | if length <= 0 { 47 | return 48 | } 49 | data := make([]byte, length) 50 | _, d.err = d.Read(data) 51 | if d.err != nil { 52 | return 53 | } 54 | if len(data) < 4 { 55 | return 56 | } 57 | 58 | if dt.Year != btypes.UnspecifiedTime { 59 | dt.Year = int(data[0]) + epochYear 60 | } else { 61 | dt.Year = int(data[0]) 62 | } 63 | 64 | dt.Month = int(data[1]) 65 | dt.Day = int(data[2]) 66 | dt.DayOfWeek = btypes.DayOfWeek(data[3]) 67 | } 68 | 69 | func (e *Encoder) time(t btypes.Time) { 70 | e.write(uint8(t.Hour)) 71 | e.write(uint8(t.Minute)) 72 | e.write(uint8(t.Second)) 73 | 74 | // Stored as 1/100 of a second 75 | e.write(uint8(t.Millisecond / 10)) 76 | } 77 | func (d *Decoder) time(t *btypes.Time, length int) { 78 | if length <= 0 { 79 | return 80 | } 81 | data := make([]byte, length) 82 | if _, d.err = d.Read(data); d.err != nil { 83 | return 84 | } 85 | 86 | t.Hour = int(data[0]) 87 | t.Minute = int(data[1]) 88 | t.Second = int(data[2]) 89 | t.Millisecond = int(data[3]) * 10 90 | 91 | } 92 | -------------------------------------------------------------------------------- /encoding/decoder.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "github.com/NubeDev/bacnet/btypes" 7 | ) 8 | 9 | // Decoder used 10 | type Decoder struct { 11 | buff *bytes.Buffer 12 | err error 13 | } 14 | 15 | func (d *Decoder) len() int { 16 | return d.buff.Len() 17 | } 18 | func NewDecoder(b []byte) *Decoder { 19 | return &Decoder{ 20 | bytes.NewBuffer(b), 21 | nil, 22 | } 23 | } 24 | 25 | func (d *Decoder) Error() error { 26 | return d.err 27 | } 28 | 29 | func (d *Decoder) Bytes() []byte { 30 | return d.buff.Bytes() 31 | } 32 | 33 | func (d *Decoder) ReadByte() (byte, error) { 34 | return d.buff.ReadByte() 35 | } 36 | 37 | func (d *Decoder) Read(data []byte) (int, error) { 38 | return d.buff.Read(data) 39 | } 40 | 41 | func (d *Decoder) Skip(n uint32) error { 42 | _, d.err = d.buff.Read(make([]byte, n)) 43 | return d.err 44 | } 45 | 46 | func (d *Decoder) UnreadByte() error { 47 | return d.buff.UnreadByte() 48 | } 49 | 50 | func (d *Decoder) decode(data interface{}) { 51 | // Only decode if there have been no errors so far 52 | if d.err != nil { 53 | return 54 | } 55 | d.err = binary.Read(d.buff, EncodingEndian, data) 56 | } 57 | 58 | // contexTag decoder 59 | 60 | // Returns both a tag and additional metadata stored in this byte. If it is of 61 | // extended type, then that means that the entire first byte is metadata, else 62 | // the firrst 4 bytes store the tag 63 | func (d *Decoder) tagNumber() (tag uint8, meta tagMeta) { 64 | // Read the first value 65 | d.decode(&meta) 66 | if meta.isExtendedTagNumber() { 67 | d.decode(&tag) 68 | return tag, meta 69 | } 70 | return uint8(meta) >> 4, meta 71 | } 72 | 73 | func (d *Decoder) value(meta tagMeta) (value uint32) { 74 | if meta.isExtendedValue() { 75 | var val uint8 76 | d.decode(&val) 77 | // Tagged as an uint32 78 | if val == flag32bit { 79 | var parse uint32 80 | d.decode(&parse) 81 | return parse 82 | 83 | // Tagged as a uint16 84 | } else if val == flag16bit { 85 | var parse uint16 86 | d.decode(&parse) 87 | return uint32(parse) 88 | 89 | // No tag, it must be a uint8 90 | } else { 91 | return uint32(val) 92 | } 93 | } else if meta.isOpening() || meta.isClosing() { 94 | return 0 95 | } 96 | return uint32(meta & 0x07) 97 | } 98 | func (d *Decoder) tagNumberAndValue() (tag uint8, meta tagMeta, value uint32) { 99 | tag, meta = d.tagNumber() 100 | // It must be a non extended/small value 101 | // Note this is a mask of the last 3 bits 102 | return tag, meta, d.value(meta) 103 | } 104 | 105 | func (d *Decoder) objectId() (objectType btypes.ObjectType, instance btypes.ObjectInstance) { 106 | var value uint32 107 | d.decode(&value) 108 | objectType = btypes.ObjectType((value >> InstanceBits) & MaxObject) 109 | instance = btypes.ObjectInstance(value & MaxInstance) 110 | return 111 | } 112 | 113 | func (d *Decoder) enumerated(len int) uint32 { 114 | return d.unsigned(len) 115 | } 116 | 117 | func (d *Decoder) unsigned24() uint32 { 118 | var a, b, c uint8 119 | d.decode(&a) 120 | d.decode(&b) 121 | d.decode(&c) 122 | 123 | var x uint32 124 | x = uint32((uint32(a) << 16) & 0x00ff0000) 125 | x |= uint32((uint32(b) << 8) & 0x0000ff00) 126 | x |= uint32(uint32(c) & 0x000000ff) 127 | return x 128 | } 129 | 130 | func (d *Decoder) unsigned(length int) uint32 { 131 | switch length { 132 | case size8: //1 133 | var val uint8 134 | d.decode(&val) 135 | return uint32(val) 136 | case size16: //2 137 | var val uint16 138 | d.decode(&val) 139 | return uint32(val) 140 | case size24: //3 141 | return d.unsigned24() 142 | case size32: //4 143 | var val uint32 144 | d.decode(&val) 145 | return val 146 | default: 147 | return 0 148 | } 149 | } 150 | 151 | func (d *Decoder) signed24() int32 { 152 | var a, b, c int8 153 | d.decode(&a) 154 | d.decode(&b) 155 | d.decode(&c) 156 | 157 | var x int32 158 | x = int32((int32(a) << 16) & 0x00ff0000) 159 | x |= int32((int32(b) << 8) & 0x0000ff00) 160 | x |= int32(int32(c) & 0x000000ff) 161 | return x 162 | } 163 | 164 | func (d *Decoder) signed(length int) int32 { 165 | switch length { 166 | case size8: 167 | var val int8 168 | d.decode(&val) 169 | return int32(val) 170 | case size16: 171 | var val int16 172 | d.decode(&val) 173 | return int32(val) 174 | case size24: 175 | return d.signed24() 176 | case size32: 177 | var val int32 178 | d.decode(&val) 179 | return val 180 | default: 181 | return 0 182 | } 183 | } 184 | 185 | func (d *Decoder) bitString(length int) *btypes.BitString { 186 | if length <= 0 { 187 | return nil 188 | } 189 | data := make([]uint8, length) 190 | d.decode(data) 191 | //refer to https://github.com/bacnet-stack/bacnet-stack/blob/bacnet-stack-0.9.1/src/bacdcode.c#L672 192 | bs := btypes.NewBitString(length - 1) 193 | /* the lower 3 bits of the first byte contains the unused bits in the remain bytes and the remain bytes contain the bit masks*/ 194 | bytesUsed := uint8(length - 1) 195 | if bytesUsed <= btypes.MaxBitStringBytes { 196 | for i := uint8(0); i < bytesUsed; i++ { 197 | //index of data start from 1 198 | bs.SetByte(i, byteReverseBits(data[i+1])) 199 | } 200 | /*the lower 3 bits of the first byte store the number of unused bits , that is, less than 8*/ 201 | bs.SetBitsUsed(bytesUsed, data[0]&0x07) 202 | } 203 | return bs 204 | } 205 | -------------------------------------------------------------------------------- /encoding/encoder.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "github.com/NubeDev/bacnet/btypes" 7 | ) 8 | 9 | var EncodingEndian binary.ByteOrder = binary.BigEndian 10 | 11 | type Encoder struct { 12 | buff *bytes.Buffer 13 | err error 14 | } 15 | 16 | func NewEncoder() *Encoder { 17 | e := Encoder{ 18 | buff: new(bytes.Buffer), 19 | err: nil, 20 | } 21 | return &e 22 | } 23 | 24 | func (e *Encoder) Error() error { 25 | return e.err 26 | } 27 | 28 | func (e *Encoder) Bytes() []byte { 29 | return e.buff.Bytes() 30 | } 31 | 32 | func (e *Encoder) write(p interface{}) { 33 | if e.err != nil { 34 | return 35 | } 36 | e.err = binary.Write(e.buff, EncodingEndian, p) 37 | } 38 | 39 | func (e *Encoder) contextObjectID(tagNum uint8, objectType btypes.ObjectType, instance btypes.ObjectInstance) { 40 | /* length of object id is 4 octets, as per 20.2.14 */ 41 | e.tag(tagInfo{ID: tagNum, Context: true, Value: 4}) 42 | e.objectId(objectType, instance) 43 | } 44 | 45 | // Write opening tag to the system 46 | func (e *Encoder) openingTag(num uint8) { 47 | var meta tagMeta 48 | meta.setOpening() 49 | e.tagNum(meta, num) 50 | } 51 | 52 | func (e *Encoder) closingTag(num uint8) { 53 | var meta tagMeta 54 | meta.setClosing() 55 | e.tagNum(meta, num) 56 | } 57 | 58 | //tagNum pre-tags 59 | func (e *Encoder) tagNum(meta tagMeta, num uint8) { 60 | t := uint8(meta) 61 | if num <= 14 { 62 | t |= num << 4 63 | e.write(t) 64 | // We don't have enough space so make it in a new byte 65 | } else { 66 | t |= 0xF0 67 | e.write(t) 68 | e.write(num) 69 | } 70 | } 71 | 72 | func (e *Encoder) tag(tg tagInfo) { 73 | var t uint8 74 | var meta tagMeta 75 | if tg.Context { 76 | meta.setContextSpecific() 77 | } 78 | if tg.Opening { 79 | meta.setOpening() 80 | } 81 | if tg.Closing { 82 | meta.setClosing() 83 | } 84 | 85 | t = uint8(meta) 86 | if tg.Value <= 4 { 87 | t |= uint8(tg.Value) 88 | } else { 89 | t |= 5 90 | } 91 | 92 | // We have enough room to put it with the last value 93 | if tg.ID <= 14 { 94 | t |= tg.ID << 4 95 | e.write(t) 96 | 97 | // We don't have enough space so make it in a new byte 98 | } else { 99 | t |= 0xF0 100 | e.write(t) 101 | e.write(tg.ID) 102 | } 103 | if tg.Value > 4 { 104 | // Depending on the length, we will either write it as an 8 bit, 32 bit, or 64-bit integer 105 | if tg.Value <= 253 { 106 | e.write(uint8(tg.Value)) 107 | } else if tg.Value <= 65535 { 108 | e.write(flag16bit) 109 | e.write(uint16(tg.Value)) 110 | } else { 111 | e.write(flag32bit) 112 | e.write(tg.Value) 113 | } 114 | } 115 | } 116 | 117 | /* from clause 20.2.14 Encoding of an Object Identifier Value 118 | returns the number of apdu bytes consumed */ 119 | func (e *Encoder) objectId(objectType btypes.ObjectType, instance btypes.ObjectInstance) { 120 | var value uint32 121 | value = ((uint32(objectType) & MaxObject) << InstanceBits) | (uint32(instance) & MaxInstance) 122 | e.write(value) 123 | } 124 | 125 | func (e *Encoder) contextEnumerated(tagNumber uint8, value uint32) { 126 | e.contextUnsigned(tagNumber, value) 127 | } 128 | 129 | func (e *Encoder) contextUnsigned(tagNumber uint8, value uint32) { 130 | e.tag(tagInfo{ID: tagNumber, Context: true, Value: uint32(valueLength(value))}) 131 | e.unsigned(value) 132 | } 133 | 134 | func (e *Encoder) enumerated(value uint32) { 135 | e.unsigned(value) 136 | } 137 | 138 | // weird, huh? 139 | func (e *Encoder) unsigned24(value uint32) { 140 | e.write(uint8((value & 0xFF0000) >> 16)) 141 | e.write(uint8((value & 0x00FF00) >> 8)) 142 | e.write(uint8(value & 0x0000FF)) 143 | 144 | } 145 | 146 | func (e *Encoder) unsigned(value uint32) { 147 | if value < 0x100 { 148 | e.write(uint8(value)) 149 | } else if value < 0x10000 { 150 | e.write(uint16(value)) 151 | } else if value < 0x1000000 { 152 | // Really!? 24 bits? 153 | e.unsigned24(value) 154 | } else { 155 | e.write(value) 156 | } 157 | } 158 | 159 | func (e *Encoder) objects(objects []btypes.Object, write bool) error { 160 | var tag uint8 161 | for _, obj := range objects { 162 | tag = 0 163 | e.contextObjectID(tag, obj.ID.Type, obj.ID.Instance) 164 | // Tag 1 - Opening Tag 165 | tag = 1 166 | e.openingTag(tag) 167 | e.properties(obj.Properties, write) 168 | // Tag 1 - Closing Tag 169 | e.closingTag(tag) 170 | } 171 | return nil 172 | } 173 | 174 | func (e *Encoder) properties(properties []btypes.Property, write bool) error { 175 | // for each property 176 | var tag uint8 177 | for _, prop := range properties { 178 | // Tag 0 - Property ID 179 | tag = 0 180 | e.contextEnumerated(tag, uint32(prop.Type)) 181 | 182 | // Tag 1 (OPTIONAL) - Array Length 183 | if prop.ArrayIndex != ArrayAll { 184 | tag = 1 185 | e.contextUnsigned(tag, prop.ArrayIndex) 186 | } 187 | 188 | if write { 189 | // Tag 2 - Opening Tag 190 | tag = 2 191 | e.openingTag(tag) 192 | e.AppData(prop.Data, false) 193 | // Tag 2 - Closing Tag 194 | e.closingTag(tag) 195 | if prop.Priority != btypes.Normal { 196 | e.contextUnsigned(tag, uint32(prop.Priority)) 197 | } 198 | } 199 | } 200 | return nil 201 | } 202 | -------------------------------------------------------------------------------- /encoding/error.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type TagType string 8 | 9 | const ( 10 | ContextTag TagType = "context" 11 | OpeningTag TagType = "opening" 12 | ClosingTag TagType = "closing" 13 | ) 14 | 15 | // ErrorWrongTagType is given when a certain tag type is expected but not given when encoding/decoding 16 | type ErrorWrongTagType struct { 17 | Type TagType 18 | } 19 | 20 | func (e *ErrorWrongTagType) Error() string { 21 | return fmt.Sprintf("Tag should be a %s tag", e.Type) 22 | } 23 | -------------------------------------------------------------------------------- /encoding/general.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | // valueLength caclulates how large the necessary value needs to be to fit in the appropriate 4 | // packet length 5 | func valueLength(value uint32) int { 6 | /* length of enumerated is variable, as per 20.2.11 */ 7 | if value < 0x100 { 8 | return size8 9 | } else if value < 0x10000 { 10 | return size16 11 | } else if value < 0x1000000 { 12 | return size24 13 | } 14 | return size32 15 | } 16 | 17 | /* from clause 20.2.1.3.2 Constructed Data */ 18 | /* true if the tag is an opening tag */ 19 | func isOpeningTag(x uint8) bool { 20 | return ((x & 0x07) == 6) 21 | } 22 | 23 | /* from clause 20.2.1.3.2 Constructed Data */ 24 | /* true if the tag is a closing tag */ 25 | func isClosingTag(x uint8) bool { 26 | return ((x & 0x07) == 7) 27 | } 28 | 29 | type tagMeta uint8 30 | 31 | const tagMask tagMeta = 7 32 | const openingMask tagMeta = 6 33 | const closingMask tagMeta = 7 34 | 35 | const extendValueBits tagMeta = 5 36 | 37 | const contextSpecificBit = 0x08 38 | 39 | func (t *tagMeta) setClosing() { 40 | t.setContextSpecific() 41 | *t = *t | tagMeta(closingMask) 42 | } 43 | 44 | func (t *tagMeta) isClosing() bool { 45 | return ((*t & closingMask) == closingMask) 46 | } 47 | 48 | func (t *tagMeta) setOpening() { 49 | t.setContextSpecific() 50 | *t = *t | tagMeta(openingMask) 51 | } 52 | 53 | func (t *tagMeta) isOpening() bool { 54 | return ((*t & openingMask) == openingMask) 55 | } 56 | 57 | func (t *tagMeta) Clear() { 58 | *t = 0 59 | } 60 | 61 | func (t *tagMeta) setContextSpecific() { 62 | *t = *t | contextSpecificBit 63 | } 64 | 65 | func (t *tagMeta) isContextSpecific() bool { 66 | return ((*t & contextSpecificBit) > 0) 67 | } 68 | 69 | func (t *tagMeta) isExtendedValue() bool { 70 | return (*t & tagMask) == extendValueBits 71 | } 72 | func (t *tagMeta) isExtendedTagNumber() bool { 73 | return ((*t & 0xF0) == 0xF0) 74 | } 75 | 76 | // setInfoMask takes an input in, and make a bit either 0, or 1 depending on the 77 | // input boolean and mask 78 | func setInfoMask(in byte, b bool, mask byte) byte { 79 | if b { 80 | return in | mask 81 | } else { 82 | var m byte = 0xFF 83 | m = m - mask 84 | return in & m 85 | } 86 | } 87 | 88 | /* from clause 20.1.2.4 max-segments-accepted and clause 20.1.2.5 max-APDU-length-accepted 89 | returns the encoded octet */ 90 | func (e *Encoder) maxSegsMaxApdu(maxSegs uint, maxApdu uint) { 91 | x := encodeMaxSegsMaxApdu(maxSegs, maxApdu) 92 | e.write(x) 93 | } 94 | 95 | func encodeMaxSegsMaxApdu(maxSegs uint, maxApdu uint) uint8 { 96 | var octet uint8 97 | 98 | // 6 is chosen since 2^6 is 64 at which point we hit special cases 99 | var i uint 100 | for i = 0; i < 6; i++ { 101 | if maxSegs < 1<<(i+1) { 102 | octet = uint8(i << 4) 103 | break 104 | } 105 | } 106 | 107 | if maxSegs == 64 { 108 | octet = 0x60 109 | } else if maxSegs > 64 { 110 | octet = 0x70 111 | } 112 | 113 | /* max_apdu must be 50 octets minimum */ 114 | if maxApdu <= 50 { 115 | octet |= 0x00 116 | } else if maxApdu <= 128 { 117 | octet |= 0x01 118 | /*fits in a LonTalk frame */ 119 | } else if maxApdu <= 206 { 120 | octet |= 0x02 121 | /*fits in an ARCNET or MS/TP frame */ 122 | } else if maxApdu <= 480 { 123 | octet |= 0x03 124 | } else if maxApdu <= 1024 { 125 | octet |= 0x04 126 | /* fits in an ISO 8802-3 frame */ 127 | } else if maxApdu <= 1476 { 128 | octet |= 0x05 129 | } 130 | return octet 131 | } 132 | 133 | func (d *Decoder) maxSegsMaxApdu() (maxSegs uint, maxApdu uint) { 134 | var b uint8 135 | d.decode(&b) 136 | return decodeMaxSegs(b), decodeMaxApdu(b) 137 | } 138 | 139 | func decodeMaxApdu(a uint8) uint { 140 | switch s := a & 0x0F; s { 141 | case 0: 142 | return 50 143 | case 1: 144 | return 128 145 | case 2: 146 | return 206 147 | case 3: 148 | return 480 149 | case 4: 150 | return 1024 151 | case 5: 152 | return 1476 153 | default: 154 | return 0 155 | } 156 | } 157 | 158 | func decodeMaxSegs(a uint8) uint { 159 | a = a >> 4 160 | // Special case 161 | if a >= 0x07 { 162 | return 65 163 | } 164 | return 1 << (a) 165 | } 166 | 167 | func byteReverseBits(c byte) byte { 168 | c = (c&0xaa)>>1 | (c&0x55)<<1 169 | c = (c&0xcc)>>2 | (c&0x33)<<2 170 | c = (c&0xf0)>>4 | (c&0x0f)<<4 171 | return c 172 | } 173 | -------------------------------------------------------------------------------- /encoding/iam.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "github.com/NubeDev/bacnet/btypes" 5 | ) 6 | 7 | func (e *Encoder) IAm(id btypes.IAm) error { 8 | apdu := btypes.APDU{ 9 | DataType: btypes.UnconfirmedServiceRequest, 10 | UnconfirmedService: btypes.ServiceUnconfirmedIAm, 11 | } 12 | e.write(apdu.DataType) 13 | e.write(apdu.UnconfirmedService) 14 | 15 | e.AppData(id.ID, false) 16 | e.AppData(id.MaxApdu, false) 17 | e.AppData(id.Segmentation, false) 18 | e.AppData(id.Vendor, false) 19 | return e.Error() 20 | } 21 | 22 | func (d *Decoder) IAm(id *btypes.IAm) error { 23 | objID, err := d.AppData() 24 | if err != nil { 25 | return err 26 | } 27 | if i, ok := objID.(btypes.ObjectID); ok { 28 | id.ID = i 29 | } 30 | maxapdu, _ := d.AppData() 31 | if m, ok := maxapdu.(uint32); ok { 32 | id.MaxApdu = m 33 | } 34 | segmentation, _ := d.AppData() 35 | if m, ok := segmentation.(uint32); ok { 36 | id.Segmentation = btypes.Enumerated(m) 37 | } 38 | vendor, err := d.AppData() 39 | if v, ok := vendor.(uint32); ok { 40 | id.Vendor = v 41 | } 42 | return d.Error() 43 | } 44 | -------------------------------------------------------------------------------- /encoding/npdu.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "github.com/NubeDev/bacnet/btypes" 5 | "github.com/NubeDev/bacnet/btypes/ndpu" 6 | ) 7 | 8 | //https://github.com/bacnet-stack/bacnet-stack/blob/master/src/bacnet/npdu.c#L391 9 | 10 | // NPDU encodes the network layer control message 11 | func (e *Encoder) NPDU(n *btypes.NPDU) { 12 | e.write(n.Version) 13 | 14 | // Prepare metadata into the second byte 15 | meta := NPDUMetadata(0) 16 | meta.SetNetworkLayerMessage(n.IsNetworkLayerMessage) 17 | meta.SetExpectingReply(n.ExpectingReply) 18 | meta.SetPriority(n.Priority) 19 | 20 | // Check to see if we have a net address. If so set destination true 21 | if n.Destination != nil { 22 | if n.Destination.Net != 0 { 23 | meta.SetDestination(true) 24 | } 25 | } 26 | 27 | // Repeat for source 28 | if n.Source != nil { 29 | if n.Source.Net != 0 { 30 | meta.SetSource(true) 31 | } 32 | } 33 | e.write(meta) 34 | if meta.HasDestination() { 35 | e.write(n.Destination.Net) 36 | 37 | // Address 38 | e.write(n.Destination.Len) 39 | e.write(n.Destination.Adr) 40 | } 41 | 42 | if meta.HasSource() { 43 | e.write(n.Source.Net) 44 | 45 | // Address 46 | e.write(n.Source.Len) 47 | e.write(n.Source.Adr) 48 | } 49 | 50 | // Hop count is after source 51 | if meta.HasDestination() { 52 | e.write(n.HopCount) 53 | } 54 | 55 | if meta.IsNetworkLayerMessage() { 56 | e.write(n.NetworkLayerMessageType) 57 | 58 | // If the network value is above 0x80, then it should have a vendor id 59 | if n.NetworkLayerMessageType >= 0x80 { 60 | e.write(n.VendorId) 61 | } 62 | } 63 | } 64 | 65 | func (d *Decoder) Address(a *btypes.Address) { 66 | d.decode(&a.Net) //decode the network address 67 | d.decode(&a.Len) 68 | // Make space for address 69 | a.Adr = make([]uint8, a.Len) //decode the device hardware mac addr 70 | d.decode(a.Adr) 71 | 72 | } 73 | 74 | type RouterToNetworkList struct { 75 | Source []btypes.Address 76 | } 77 | 78 | // NPDU encodes the network layer control message 79 | func (d *Decoder) NPDU(n *btypes.NPDU) (addr []btypes.Address, err error) { 80 | d.decode(&n.Version) 81 | 82 | // Prepare metadata into the second byte 83 | meta := NPDUMetadata(0) 84 | d.decode(&meta) 85 | n.ExpectingReply = meta.ExpectingReply() 86 | n.IsNetworkLayerMessage = meta.IsNetworkLayerMessage() 87 | n.Priority = meta.Priority() 88 | 89 | if meta.HasDestination() { 90 | n.Destination = &btypes.Address{} 91 | d.Address(n.Destination) 92 | } 93 | 94 | if meta.HasSource() { 95 | n.Source = &btypes.Address{} 96 | d.Address(n.Source) 97 | } 98 | 99 | if meta.HasDestination() { 100 | d.decode(&n.HopCount) 101 | } else { 102 | n.HopCount = 0 103 | } 104 | 105 | if meta.IsNetworkLayerMessage() { 106 | d.decode(&n.NetworkLayerMessageType) 107 | if n.NetworkLayerMessageType > 0x80 { 108 | d.decode(&n.VendorId) 109 | } 110 | if n.NetworkLayerMessageType == ndpu.NetworkIs { //used for decoding a bacnet network number on a What-Is-Network-Number 0x12 111 | n.Source = &btypes.Address{} 112 | d.decode(&n.Source.Net) 113 | } 114 | if n.NetworkLayerMessageType == ndpu.IamRouterToNetwork { //used for decoding a bacnet network number on a What-Is-Network-Number 0x12 115 | n.Source = &btypes.Address{} 116 | var nets []btypes.Address 117 | d.decode(&n.Source.Net) //decode the first network 118 | nets = append(nets, *n.Source) 119 | size := d.len() 120 | for i := d.len(); i <= size; i++ { 121 | d.decode(&n.Source.Net) 122 | for _, adr := range nets { 123 | if adr.Net != n.Source.Net { //make sure that a network is only added once 124 | nets = append(nets, *n.Source) 125 | } 126 | } 127 | } 128 | addr = nets 129 | } 130 | } 131 | return addr, d.Error() 132 | } 133 | 134 | // NPDUMetadata includes additional metadata about npdu message 135 | type NPDUMetadata byte 136 | 137 | const maskNetworkLayerMessage = 1 << 7 138 | const maskDestination = 1 << 5 139 | const maskSource = 1 << 3 140 | const maskExpectingReply = 1 << 2 141 | 142 | // General setter for the info bits using the mask 143 | func (meta *NPDUMetadata) setInfoMask(b bool, mask byte) { 144 | *meta = NPDUMetadata(setInfoMask(byte(*meta), b, mask)) 145 | } 146 | 147 | // CheckMask uses mask to check bit position 148 | func (meta *NPDUMetadata) checkMask(mask byte) bool { 149 | return (*meta & NPDUMetadata(mask)) > 0 150 | 151 | } 152 | 153 | // IsNetworkLayerMessage returns true if it is a network layer message 154 | func (n *NPDUMetadata) IsNetworkLayerMessage() bool { 155 | return n.checkMask(maskNetworkLayerMessage) 156 | } 157 | 158 | func (n *NPDUMetadata) SetNetworkLayerMessage(b bool) { 159 | n.setInfoMask(b, maskNetworkLayerMessage) 160 | } 161 | 162 | // Priority returns priority 163 | func (n *NPDUMetadata) Priority() btypes.NPDUPriority { 164 | // Encoded in bit 0 and 1 165 | return btypes.NPDUPriority(byte(*n) & 3) 166 | } 167 | 168 | // SetPriority for NPDU 169 | func (n *NPDUMetadata) SetPriority(p btypes.NPDUPriority) { 170 | // Clear the first two bits 171 | //*n &= (0xF - 3) 172 | *n |= NPDUMetadata(p) 173 | } 174 | 175 | func (n *NPDUMetadata) HasDestination() bool { 176 | return n.checkMask(maskDestination) 177 | } 178 | 179 | func (n *NPDUMetadata) SetDestination(b bool) { 180 | n.setInfoMask(b, maskDestination) 181 | } 182 | 183 | func (n *NPDUMetadata) HasSource() bool { 184 | return n.checkMask(maskSource) 185 | } 186 | 187 | func (n *NPDUMetadata) SetSource(b bool) { 188 | n.setInfoMask(b, maskSource) 189 | } 190 | 191 | func (n *NPDUMetadata) ExpectingReply() bool { 192 | return n.checkMask(maskExpectingReply) 193 | } 194 | 195 | func (n *NPDUMetadata) SetExpectingReply(b bool) { 196 | n.setInfoMask(b, maskExpectingReply) 197 | } 198 | -------------------------------------------------------------------------------- /encoding/object.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | const MaxObject = 0x3FF 4 | -------------------------------------------------------------------------------- /encoding/readmultiple.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "github.com/NubeDev/bacnet/btypes" 5 | ) 6 | 7 | func (e *Encoder) ReadMultipleProperty(invokeID uint8, data btypes.MultiplePropertyData) error { 8 | a := btypes.APDU{ 9 | DataType: btypes.ConfirmedServiceRequest, 10 | Service: btypes.ServiceConfirmedReadPropMultiple, 11 | MaxSegs: 0, 12 | MaxApdu: MaxAPDU, 13 | InvokeId: invokeID, 14 | SegmentedMessage: false, 15 | } 16 | e.APDU(a) 17 | err := e.objects(data.Objects, false) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | return e.Error() 23 | } 24 | -------------------------------------------------------------------------------- /encoding/readprop.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/NubeDev/bacnet/btypes" 7 | ) 8 | 9 | func (e *Encoder) readPropertyHeader(tagPos uint8, data *btypes.PropertyData) (uint8, error) { 10 | // Validate data first 11 | if err := isValidObjectType(data.Object.ID.Type); err != nil { 12 | return 0, err 13 | } 14 | if err := isValidPropertyType(uint32(data.Object.Properties[0].Type)); err != nil { 15 | return 0, err 16 | } 17 | 18 | // Tag - Object Type and Instance 19 | e.contextObjectID(tagPos, data.Object.ID.Type, data.Object.ID.Instance) 20 | tagPos++ 21 | 22 | // Get first property 23 | prop := data.Object.Properties[0] 24 | e.contextEnumerated(tagPos, uint32(prop.Type)) 25 | tagPos++ 26 | 27 | // Optional Tag - Array Index 28 | if prop.ArrayIndex != ArrayAll { 29 | e.contextUnsigned(tagPos, prop.ArrayIndex) 30 | } 31 | tagPos++ 32 | return tagPos, nil 33 | } 34 | 35 | // ReadProperty is a service request to read a property that is passed. 36 | func (e *Encoder) ReadProperty(invokeID uint8, data btypes.PropertyData) error { 37 | // PDU Type 38 | a := btypes.APDU{ 39 | DataType: btypes.ConfirmedServiceRequest, 40 | Service: btypes.ServiceConfirmedReadProperty, 41 | MaxSegs: 0, 42 | MaxApdu: MaxAPDU, 43 | InvokeId: invokeID, 44 | SegmentedMessage: false, 45 | } 46 | e.APDU(a) 47 | e.readPropertyHeader(initialTagPos, &data) 48 | return e.Error() 49 | } 50 | 51 | // ReadPropertyAck is the response made to a ReadProperty service request. 52 | func (e *Encoder) ReadPropertyAck(invokeID uint8, data btypes.PropertyData) error { 53 | if len(data.Object.Properties) != 1 { 54 | return fmt.Errorf("Property length length must be 1 not %d", len(data.Object.Properties)) 55 | } 56 | // PDU Type 57 | a := btypes.APDU{ 58 | DataType: btypes.ComplexAck, 59 | Service: btypes.ServiceConfirmedReadProperty, 60 | MaxSegs: 0, 61 | MaxApdu: MaxAPDU, 62 | InvokeId: invokeID, 63 | } 64 | e.APDU(a) 65 | 66 | tagID, err := e.readPropertyHeader(initialTagPos, &data) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | e.openingTag(tagID) 72 | tagID++ 73 | prop := data.Object.Properties[0] 74 | e.AppData(prop.Data, false) 75 | e.closingTag(tagID) 76 | return e.Error() 77 | } 78 | 79 | func (d *Decoder) ReadProperty(data *btypes.PropertyData) error { 80 | // Must have at least 7 bytes 81 | if d.buff.Len() < 7 { 82 | return fmt.Errorf("missing parameters") 83 | } 84 | 85 | // Tag 0: Object ID 86 | tag, meta := d.tagNumber() 87 | 88 | var expectedTag uint8 89 | if tag != expectedTag { 90 | return &ErrorIncorrectTag{expectedTag, tag} 91 | } 92 | expectedTag++ 93 | 94 | var objectType btypes.ObjectType 95 | var instance btypes.ObjectInstance 96 | if !meta.isContextSpecific() { 97 | return fmt.Errorf("tag %d should be context specific. %x", tag, meta) 98 | } 99 | objectType, instance = d.objectId() 100 | data.Object.ID.Type = objectType 101 | data.Object.ID.Instance = instance 102 | 103 | // Tag 1: Property ID 104 | tag, meta = d.tagNumber() 105 | if tag != expectedTag { 106 | return &ErrorIncorrectTag{expectedTag, tag} 107 | } 108 | expectedTag++ 109 | 110 | lenValue := d.value(meta) 111 | 112 | var prop btypes.Property 113 | prop.Type = btypes.PropertyType(d.enumerated(int(lenValue))) 114 | 115 | if d.len() != 0 { 116 | tag, meta = d.tagNumber() 117 | } 118 | 119 | // Check to see if we still have bytes to read. 120 | if d.buff.Len() != 0 || tag >= 2 { 121 | // If we do then that means we are reading the optional argument, 122 | // arra length 123 | 124 | // Tag 2: Array Length (OPTIONAL) 125 | var lenValue uint32 126 | lenValue = d.value(meta) 127 | 128 | var openTag uint8 129 | // I tried to not use magic numbers but it doesn't look like it can be avoided 130 | // If the attag we receive is a tag of 2 then set the value 131 | if tag == 2 { 132 | prop.ArrayIndex = d.unsigned(int(lenValue)) 133 | if d.len() > 0 { 134 | openTag, meta = d.tagNumber() 135 | } 136 | } else { 137 | openTag = tag 138 | prop.ArrayIndex = ArrayAll 139 | } 140 | 141 | if openTag == 3 { 142 | var err error 143 | // We subtract one to ignore the closing tag. 144 | datalist := make([]interface{}, 0) 145 | 146 | // There is a closing tag of size 1 byte that we ignore which is why we are 147 | // looping until the length is greater than 1 148 | for i := 0; d.buff.Len() > 1; i++ { 149 | data, err := d.AppData() 150 | if err != nil { 151 | d.err = err 152 | return err 153 | } 154 | datalist = append(datalist, data) 155 | } 156 | prop.Data = datalist 157 | 158 | // If we only have one value in the list, lets just return that value 159 | if len(datalist) == 1 { 160 | prop.Data = datalist[0] 161 | } 162 | if err != nil { 163 | d.err = err 164 | return err 165 | } 166 | } 167 | } else { 168 | prop.ArrayIndex = ArrayAll 169 | } 170 | 171 | // We now assemble all the values that we have read above 172 | data.Object.ID.Instance = instance 173 | data.Object.ID.Type = objectType 174 | data.Object.Properties = []btypes.Property{prop} 175 | 176 | return d.Error() 177 | } 178 | -------------------------------------------------------------------------------- /encoding/string.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "fmt" 5 | "golang.org/x/text/encoding/unicode" 6 | ) 7 | 8 | type stringType uint8 9 | 10 | // Supported String btypes 11 | const ( 12 | //https://github.com/stargieg/bacnet-stack/blob/master/include/bacenum.h#L1261 13 | stringUTF8 stringType = 0 //same as ANSI_X34 14 | characterUCS2 stringType = 4 //johnson controllers use this 15 | ) 16 | 17 | func (e *Encoder) string(s string) { 18 | e.write(stringUTF8) 19 | e.write([]byte(s)) 20 | } 21 | 22 | func decodeUCS2(s string) (string, error) { 23 | dec := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder() 24 | out, err := dec.String(s) 25 | if err != nil { 26 | return "", err 27 | } 28 | return out, err 29 | 30 | } 31 | 32 | func (d *Decoder) string(s *string, len int) error { 33 | var t stringType 34 | d.decode(&t) 35 | switch t { 36 | case stringUTF8: 37 | case characterUCS2: 38 | default: 39 | return fmt.Errorf("unsupported string format %d", t) 40 | } 41 | b := make([]byte, len) 42 | d.decode(b) 43 | 44 | if t == characterUCS2 { 45 | out, err := decodeUCS2(string(b)) 46 | if err != nil { 47 | return fmt.Errorf("unable to decode string format characterUCS2%d", t) 48 | } 49 | *s = out 50 | } else { 51 | *s = string(b) 52 | } 53 | 54 | return d.Error() 55 | 56 | } 57 | func (e *Encoder) octetstring(b []byte) { 58 | e.write([]byte(b)) 59 | } 60 | func (d *Decoder) octetstring(b *[]byte, len int) { 61 | *b = make([]byte, len) 62 | d.decode(b) 63 | } 64 | -------------------------------------------------------------------------------- /encoding/types.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import "fmt" 4 | 5 | type ErrorIncorrectTag struct { 6 | Expected uint8 7 | Given uint8 8 | } 9 | 10 | func (e *ErrorIncorrectTag) Error() string { 11 | return fmt.Sprintf("Incorrect tag %d, expected %d.", e.Given, e.Expected) 12 | } 13 | 14 | type tagInfo struct { 15 | // Tag id. Typically sequential, except when it is not... 16 | ID uint8 17 | Context bool 18 | // Either has a value or length of the next value 19 | Value uint32 20 | Opening bool 21 | Closing bool 22 | } 23 | -------------------------------------------------------------------------------- /encoding/validate.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/NubeDev/bacnet/btypes" 7 | ) 8 | 9 | func isValidObjectType(idType btypes.ObjectType) error { 10 | if idType > MaxObject { 11 | return fmt.Errorf("Object btypes is %d which must be less then %d", idType, MaxObject) 12 | } 13 | return nil 14 | } 15 | 16 | func isValidPropertyType(propType uint32) error { 17 | if propType > MaxPropertyID { 18 | return fmt.Errorf("Object btypes is %d which must be less then %d", propType, MaxPropertyID) 19 | } 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /encoding/whois.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "github.com/NubeDev/bacnet/btypes" 5 | ) 6 | 7 | func (e *Encoder) WhoIs(low, high int32) error { 8 | apdu := btypes.APDU{ 9 | DataType: btypes.UnconfirmedServiceRequest, 10 | UnconfirmedService: btypes.ServiceUnconfirmedWhoIs, 11 | } 12 | e.write(apdu.DataType) 13 | e.write(apdu.UnconfirmedService) 14 | 15 | // The range is optional. A scan for all objects is done when either low/high 16 | // are negative or when we are scanning above the max instance 17 | if low >= 0 && high >= 0 && low < btypes.MaxInstance && high < 18 | btypes.MaxInstance { 19 | // Tag 0 20 | e.contextUnsigned(0, uint32(low)) 21 | 22 | // Tag 1 23 | e.contextUnsigned(1, uint32(high)) 24 | } 25 | return e.Error() 26 | } 27 | 28 | func (d *Decoder) WhoIs(low, high *int32) error { 29 | // APDU read in a higher level 30 | if d.len() == 0 { 31 | *low = btypes.WhoIsAll 32 | *high = btypes.WhoIsAll 33 | return nil 34 | } 35 | // Tag 0 - Low Value 36 | var expectedTag uint8 37 | tag, _, value := d.tagNumberAndValue() 38 | if tag != expectedTag { 39 | return &ErrorIncorrectTag{Expected: expectedTag, Given: tag} 40 | } 41 | l := d.unsigned(int(value)) 42 | *low = int32(l) 43 | 44 | // Tag 1 - High Value 45 | expectedTag = 1 46 | tag, _, value = d.tagNumberAndValue() 47 | if tag != expectedTag { 48 | return &ErrorIncorrectTag{Expected: expectedTag, Given: tag} 49 | } 50 | h := d.unsigned(int(value)) 51 | *high = int32(h) 52 | 53 | return d.Error() 54 | } 55 | -------------------------------------------------------------------------------- /encoding/writemultiple.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import "github.com/NubeDev/bacnet/btypes" 4 | 5 | // WriteMultiProperty encodes a writes property request 6 | func (e *Encoder) WriteMultiProperty(invokeID uint8, data btypes.MultiplePropertyData) error { 7 | a := btypes.APDU{ 8 | DataType: btypes.ConfirmedServiceRequest, 9 | Service: btypes.ServiceConfirmedWritePropMultiple, 10 | MaxSegs: 0, 11 | MaxApdu: MaxAPDU, 12 | InvokeId: invokeID, 13 | } 14 | e.APDU(a) 15 | 16 | err := e.objects(data.Objects, true) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | return e.Error() 22 | } 23 | -------------------------------------------------------------------------------- /encoding/writeprop.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "github.com/NubeDev/bacnet/btypes" 5 | ) 6 | 7 | // WriteProperty encodes a write request 8 | func (e *Encoder) WriteProperty(invokeID uint8, data btypes.PropertyData) error { 9 | a := btypes.APDU{ 10 | DataType: btypes.ConfirmedServiceRequest, 11 | Service: btypes.ServiceConfirmedWriteProperty, 12 | MaxSegs: 0, 13 | MaxApdu: MaxAPDU, 14 | InvokeId: invokeID, 15 | } 16 | e.APDU(a) 17 | 18 | tagID, err := e.readPropertyHeader(0, &data) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | prop := data.Object.Properties[0] 24 | 25 | if data.Object.ID.Type == 1 { 26 | 27 | } 28 | 29 | // Tag 3 - the value (unlike other values, this is just a raw byte array) 30 | e.openingTag(tagID) 31 | e.AppData(prop.Data, pointTypeBOBV(data)) 32 | e.closingTag(tagID) 33 | tagID++ 34 | // Tag 4 - Optional priority tag 35 | // Priority set 36 | if prop.Priority != btypes.Normal { 37 | e.contextUnsigned(tagID, uint32(prop.Priority)) 38 | } 39 | return e.Error() 40 | } 41 | 42 | //pointTypeBOBV if point type is bv or bo then we need to set the data type to enum 43 | func pointTypeBOBV(data btypes.PropertyData) (isBool bool) { 44 | pointType := data.Object.ID.Type 45 | property := 0 46 | if len(data.Object.Properties) > 0 { 47 | property = int(data.Object.Properties[0].Type) 48 | } 49 | if (pointType == btypes.TypeBinaryValue || pointType == btypes.TypeBinaryOutput) && property == 85 { 50 | return true 51 | } 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/NubeDev/bacnet 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/kr/pretty v0.2.0 7 | github.com/patrickmn/go-cache v2.1.0+incompatible 8 | github.com/pkg/errors v0.9.1 9 | github.com/sirupsen/logrus v1.8.1 10 | github.com/spf13/cobra v1.4.0 11 | github.com/spf13/viper v1.11.0 12 | golang.org/x/text v0.3.7 13 | ) 14 | 15 | require ( 16 | github.com/fsnotify/fsnotify v1.5.1 // indirect 17 | github.com/hashicorp/hcl v1.0.0 // indirect 18 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 19 | github.com/kr/text v0.1.0 // indirect 20 | github.com/magiconair/properties v1.8.6 // indirect 21 | github.com/mitchellh/mapstructure v1.4.3 // indirect 22 | github.com/pelletier/go-toml v1.9.4 // indirect 23 | github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect 24 | github.com/spf13/afero v1.8.2 // indirect 25 | github.com/spf13/cast v1.4.1 // indirect 26 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 27 | github.com/spf13/pflag v1.0.5 // indirect 28 | github.com/subosito/gotenv v1.2.0 // indirect 29 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect 30 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 31 | gopkg.in/ini.v1 v1.66.4 // indirect 32 | gopkg.in/yaml.v2 v2.4.0 // indirect 33 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /helpers/data/data.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet/btypes" 6 | ) 7 | 8 | func ToBitString(d btypes.PropertyData) (ok bool, out *btypes.BitString) { 9 | out, ok = d.Object.Properties[0].Data.(*btypes.BitString) 10 | 11 | if !ok { 12 | fmt.Println("unable to get object list") 13 | return ok, out 14 | } 15 | return 16 | } 17 | 18 | func ToArr(d btypes.PropertyData) (ok bool, out []interface{}) { 19 | out, ok = d.Object.Properties[0].Data.([]interface{}) 20 | if !ok { 21 | fmt.Println("unable to get object list") 22 | return ok, out 23 | } 24 | return 25 | } 26 | 27 | func ToInt(d btypes.PropertyData) (ok bool, out int) { 28 | if len(d.Object.Properties) == 0 { 29 | fmt.Println("No value returned") 30 | return ok, out 31 | } 32 | out, ok = d.Object.Properties[0].Data.(int) 33 | return ok, out 34 | } 35 | 36 | func ToFloat32(d btypes.PropertyData) (ok bool, out float32) { 37 | if len(d.Object.Properties) == 0 { 38 | fmt.Println("No value returned") 39 | return ok, out 40 | } 41 | out, ok = d.Object.Properties[0].Data.(float32) 42 | return ok, out 43 | } 44 | 45 | func ToFloat64(d btypes.PropertyData) (ok bool, out float64) { 46 | if len(d.Object.Properties) == 0 { 47 | fmt.Println("No value returned") 48 | return ok, out 49 | } 50 | out, ok = d.Object.Properties[0].Data.(float64) 51 | return ok, out 52 | } 53 | 54 | func ToBool(d btypes.PropertyData) (ok bool, out bool) { 55 | if len(d.Object.Properties) == 0 { 56 | fmt.Println("No value returned") 57 | return ok, out 58 | } 59 | out, ok = d.Object.Properties[0].Data.(bool) 60 | return ok, out 61 | } 62 | 63 | func ToStr(d btypes.PropertyData) (ok bool, out string) { 64 | if len(d.Object.Properties) == 0 { 65 | fmt.Println("No value returned") 66 | return ok, out 67 | } 68 | out, ok = d.Object.Properties[0].Data.(string) 69 | return ok, out 70 | } 71 | 72 | func ToUint32(d btypes.PropertyData) (ok bool, out uint32) { 73 | if len(d.Object.Properties) == 0 { 74 | fmt.Println("No value returned") 75 | return ok, out 76 | } 77 | out, ok = d.Object.Properties[0].Data.(uint32) 78 | return ok, out 79 | } 80 | -------------------------------------------------------------------------------- /helpers/homedir/homedir.go: -------------------------------------------------------------------------------- 1 | package homedir 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "runtime" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | // DisableCache will disable caching of the home directory. Caching is enabled 16 | // by default. 17 | var DisableCache bool 18 | 19 | var homedirCache string 20 | var cacheLock sync.RWMutex 21 | 22 | // Dir returns the home directory for the executing user. 23 | // 24 | // This uses an OS-specific method for discovering the home directory. 25 | // An error is returned if a home directory cannot be detected. 26 | func Dir() (string, error) { 27 | if !DisableCache { 28 | cacheLock.RLock() 29 | cached := homedirCache 30 | cacheLock.RUnlock() 31 | if cached != "" { 32 | return cached, nil 33 | } 34 | } 35 | 36 | cacheLock.Lock() 37 | defer cacheLock.Unlock() 38 | 39 | var result string 40 | var err error 41 | if runtime.GOOS == "windows" { 42 | result, err = dirWindows() 43 | } else { 44 | // Unix-like system, so just assume Unix 45 | result, err = dirUnix() 46 | } 47 | 48 | if err != nil { 49 | return "", err 50 | } 51 | homedirCache = result 52 | return result, nil 53 | } 54 | 55 | // Expand expands the path to include the home directory if the path 56 | // is prefixed with `~`. If it isn't prefixed with `~`, the path is 57 | // returned as-is. 58 | func Expand(path string) (string, error) { 59 | if len(path) == 0 { 60 | return path, nil 61 | } 62 | 63 | if path[0] != '~' { 64 | return path, nil 65 | } 66 | 67 | if len(path) > 1 && path[1] != '/' && path[1] != '\\' { 68 | return "", errors.New("cannot expand user-specific home dir") 69 | } 70 | 71 | dir, err := Dir() 72 | if err != nil { 73 | return "", err 74 | } 75 | 76 | return filepath.Join(dir, path[1:]), nil 77 | } 78 | 79 | // Reset clears the store, forcing the next call to Dir to re-detect 80 | // the home directory. This generally never has to be called, but can be 81 | // useful in tests if you're modifying the home directory via the HOME 82 | // env var or something. 83 | func Reset() { 84 | cacheLock.Lock() 85 | defer cacheLock.Unlock() 86 | homedirCache = "" 87 | } 88 | 89 | func dirUnix() (string, error) { 90 | homeEnv := "HOME" 91 | if runtime.GOOS == "plan9" { 92 | // On plan9, env vars are lowercase. 93 | homeEnv = "home" 94 | } 95 | 96 | // First prefer the HOME environmental variable 97 | if home := os.Getenv(homeEnv); home != "" { 98 | return home, nil 99 | } 100 | 101 | var stdout bytes.Buffer 102 | 103 | // If that fails, try OS specific commands 104 | if runtime.GOOS == "darwin" { 105 | cmd := exec.Command("sh", "-c", `dscl -q . -read /Users/"$(whoami)" NFSHomeDirectory | sed 's/^[^ ]*: //'`) 106 | cmd.Stdout = &stdout 107 | if err := cmd.Run(); err == nil { 108 | result := strings.TrimSpace(stdout.String()) 109 | if result != "" { 110 | return result, nil 111 | } 112 | } 113 | } else { 114 | cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid())) 115 | cmd.Stdout = &stdout 116 | if err := cmd.Run(); err != nil { 117 | // If the error is ErrNotFound, we ignore it. Otherwise, return it. 118 | if err != exec.ErrNotFound { 119 | return "", err 120 | } 121 | } else { 122 | if passwd := strings.TrimSpace(stdout.String()); passwd != "" { 123 | // username:password:uid:gid:gecos:home:shell 124 | passwdParts := strings.SplitN(passwd, ":", 7) 125 | if len(passwdParts) > 5 { 126 | return passwdParts[5], nil 127 | } 128 | } 129 | } 130 | } 131 | 132 | // If all else fails, try the shell 133 | stdout.Reset() 134 | cmd := exec.Command("sh", "-c", "cd && pwd") 135 | cmd.Stdout = &stdout 136 | if err := cmd.Run(); err != nil { 137 | return "", err 138 | } 139 | 140 | result := strings.TrimSpace(stdout.String()) 141 | if result == "" { 142 | return "", errors.New("blank output when reading home directory") 143 | } 144 | 145 | return result, nil 146 | } 147 | 148 | func dirWindows() (string, error) { 149 | // First prefer the HOME environmental variable 150 | if home := os.Getenv("HOME"); home != "" { 151 | return home, nil 152 | } 153 | 154 | // Prefer standard environment variable USERPROFILE 155 | if home := os.Getenv("USERPROFILE"); home != "" { 156 | return home, nil 157 | } 158 | 159 | drive := os.Getenv("HOMEDRIVE") 160 | path := os.Getenv("HOMEPATH") 161 | home := drive + path 162 | if drive == "" || path == "" { 163 | return "", errors.New("HOMEDRIVE, HOMEPATH, or USERPROFILE are blank") 164 | } 165 | 166 | return home, nil 167 | } 168 | -------------------------------------------------------------------------------- /helpers/ipbytes/ip.go: -------------------------------------------------------------------------------- 1 | package ip2bytes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | log "github.com/sirupsen/logrus" 8 | "math/big" 9 | "net" 10 | ) 11 | 12 | func ip4toInt(ip4Address net.IP) int64 { 13 | IPv4Int := big.NewInt(0) 14 | IPv4Int.SetBytes(ip4Address.To4()) 15 | return IPv4Int.Int64() 16 | } 17 | 18 | func pack32BinaryIP4(ip4Address string) []byte { 19 | ipv4Decimal := ip4toInt(net.ParseIP(ip4Address)) 20 | buf := new(bytes.Buffer) 21 | err := binary.Write(buf, binary.BigEndian, uint32(ipv4Decimal)) 22 | if err != nil { 23 | log.Errorln("helpers.mac.Pack32BinaryIP4() unable to write to buffer:", err) 24 | } 25 | return buf.Bytes() 26 | } 27 | 28 | /*New ip in byte format 29 | - ip with no subnet 30 | - port 31 | - returns uint8[192 168 15 10 186 192] 32 | */ 33 | func New(ip string, port uint16) ([]uint8, error) { 34 | buf := new(bytes.Buffer) 35 | err := binary.Write(buf, binary.BigEndian, port) 36 | if err != nil { 37 | log.Errorln("helpers.mac.BuildMac() unable to write to binary:", err) 38 | return nil, errors.New("helpers.mac.BuildMac() unable to write to binary") 39 | } 40 | return append(pack32BinaryIP4(ip), buf.Bytes()...), nil 41 | } 42 | -------------------------------------------------------------------------------- /helpers/ipbytes/ip_test.go: -------------------------------------------------------------------------------- 1 | package ip2bytes 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestIP(t *testing.T) { 9 | 10 | mac, err := New("192.168.15.10", 47808) 11 | if err != nil { 12 | return 13 | } 14 | 15 | fmt.Println(mac) 16 | 17 | } 18 | -------------------------------------------------------------------------------- /helpers/nils/nil.go: -------------------------------------------------------------------------------- 1 | package nils 2 | 3 | func NewString(value string) *string { 4 | return &value 5 | } 6 | 7 | func StringIsNil(b *string) string { 8 | if b == nil { 9 | return "" 10 | } else { 11 | return *b 12 | } 13 | } 14 | func StringNilCheck(b *string) bool { 15 | if b == nil { 16 | return true 17 | } else { 18 | return false 19 | } 20 | } 21 | 22 | func Float64IsNil(b *float64) float64 { 23 | if b == nil { 24 | return 0 25 | } else { 26 | return *b 27 | } 28 | } 29 | 30 | func NewInt(value int) *int { 31 | return &value 32 | } 33 | 34 | func NewBool(value bool) *bool { 35 | return &value 36 | } 37 | 38 | func BoolIsNil(value *bool) bool { 39 | if value == nil { 40 | return false 41 | } 42 | return *value 43 | } 44 | 45 | func NewTrue() *bool { 46 | b := true 47 | return &b 48 | } 49 | 50 | func NewFalse() *bool { 51 | b := false 52 | return &b 53 | } 54 | 55 | func NewUint16(value uint16) *uint16 { 56 | return &value 57 | } 58 | 59 | func NewUint32(value uint32) *uint32 { 60 | return &value 61 | } 62 | 63 | func NewFloat32(value float32) *float32 { 64 | return &value 65 | } 66 | 67 | func NewFloat64(value float64) *float64 { 68 | return &value 69 | } 70 | 71 | func IntIsNil(b *int) int { 72 | if b == nil { 73 | return 0 74 | } else { 75 | return *b 76 | } 77 | } 78 | 79 | func BoolNilCheck(b *bool) bool { 80 | if b == nil { 81 | return true 82 | } else { 83 | return false 84 | } 85 | } 86 | 87 | func IntNilCheck(b *int) bool { 88 | if b == nil { 89 | return true 90 | } else { 91 | return false 92 | } 93 | } 94 | 95 | func Float32IsNil(b *float32) float32 { 96 | if b == nil { 97 | return 0 98 | } else { 99 | return *b 100 | } 101 | } 102 | 103 | func UnitIsNil(b *uint) uint { 104 | if b == nil { 105 | return 0 106 | } else { 107 | return *b 108 | } 109 | } 110 | 111 | func Unit16IsNil(b *uint16) uint16 { 112 | if b == nil { 113 | return 0 114 | } else { 115 | return *b 116 | } 117 | } 118 | 119 | func Unit32IsNil(b *uint32) uint32 { 120 | if b == nil { 121 | return 0 122 | } else { 123 | return *b 124 | } 125 | } 126 | 127 | func Unit32NilCheck(b *uint32) bool { 128 | if b == nil { 129 | return true 130 | } else { 131 | return false 132 | } 133 | } 134 | 135 | func FloatIsNilCheck(b *float64) bool { 136 | if b == nil { 137 | return true 138 | } else { 139 | return false 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /helpers/print/console.go: -------------------------------------------------------------------------------- 1 | package pprint 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func Print(i interface{}) { 10 | fmt.Printf("%+v\n", i) 11 | return 12 | } 13 | 14 | func Log(i interface{}) string { 15 | 16 | return fmt.Sprintf("%+v\n", i) 17 | } 18 | func PrintJOSN(x interface{}) { 19 | ioWriter := os.Stdout 20 | w := json.NewEncoder(ioWriter) 21 | w.SetIndent("", " ") 22 | w.Encode(x) 23 | } 24 | -------------------------------------------------------------------------------- /helpers/store/init.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "github.com/patrickmn/go-cache" 5 | "time" 6 | ) 7 | 8 | //var store *cache.Cache 9 | 10 | type Handler struct { 11 | Store *cache.Cache 12 | } 13 | 14 | //Init init store 15 | func Init() *Handler { 16 | newStore := cache.New(cache.NoExpiration, cache.DefaultExpiration) 17 | store := &Handler{Store: newStore} 18 | return store 19 | } 20 | 21 | // Get an item from the store. Returns the item or nil, and a bool indicating 22 | // whether the key was found. 23 | func (l *Handler) Get(key string) (interface{}, bool) { 24 | value, found := l.Store.Get(key) 25 | return value, found 26 | } 27 | 28 | // Set an item to the store, replacing any existing item. If the duration is 0 29 | // (DefaultExpiration), the store's default expiration time is used. If it is -1 30 | // (NoExpiration), the item never expires. 31 | func (l *Handler) Set(key string, value interface{}, d time.Duration) { 32 | l.Store.Set(key, value, d) 33 | } 34 | -------------------------------------------------------------------------------- /helpers/validation/vaildation.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | func ValidCIDR(ip string, cidr int) (ok bool) { 11 | ip = fmt.Sprintf("%s/%d", ip, cidr) 12 | _, _, err := net.ParseCIDR(ip) 13 | if err != nil { 14 | return 15 | } 16 | ok = true 17 | return 18 | } 19 | 20 | func ValidPort(port int) bool { 21 | t := fmt.Sprintf("%d", port) 22 | re, _ := regexp.Compile(`^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$`) 23 | if re.MatchString(t) { 24 | return true 25 | } 26 | return false 27 | } 28 | 29 | // ValidIP return true if string ip contains a valid representation of an IPv4 or IPv6 address 30 | func ValidIP(ip string) bool { 31 | ipaddr := net.ParseIP(NormaliseIPAddr(ip)) 32 | return ipaddr != nil 33 | } 34 | 35 | // NormaliseIPAddr return ip address without /32 (IPv4 or /128 (IPv6) 36 | func NormaliseIPAddr(ip string) string { 37 | if strings.HasSuffix(ip, "/32") && strings.Contains(ip, ".") { // single host (IPv4) 38 | ip = strings.TrimSuffix(ip, "/32") 39 | } else { 40 | if strings.HasSuffix(ip, "/128") { // single host (IPv6) 41 | ip = strings.TrimSuffix(ip, "/128") 42 | } 43 | } 44 | 45 | return ip 46 | } 47 | -------------------------------------------------------------------------------- /helpers/validation/validation_test.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestPort(t *testing.T) { 9 | 10 | port := 8080 11 | ok := ValidPort(port) 12 | fmt.Println("port:", port, "is ok", ok) 13 | 14 | port = -1 15 | ok = ValidPort(port) 16 | fmt.Println("port:", port, "is ok", ok) 17 | 18 | port = 0 19 | ok = ValidPort(port) 20 | fmt.Println("port:", port, "is ok", ok) 21 | 22 | port = 47808 23 | ok = ValidPort(port) 24 | fmt.Println("port:", port, "is ok", ok) 25 | 26 | port = 24 27 | ok = ValidCIDR("192.168.15.1", port) 28 | fmt.Println("ValidCIDR:", port, "is ok", ok) 29 | 30 | port = 2000 31 | ok = ValidCIDR("192.168.15.1", port) 32 | fmt.Println("ValidCIDR:", port, "is ok", ok) 33 | } 34 | -------------------------------------------------------------------------------- /iam.go: -------------------------------------------------------------------------------- 1 | package bacnet 2 | 3 | import ( 4 | "github.com/NubeDev/bacnet/btypes" 5 | "github.com/NubeDev/bacnet/encoding" 6 | ) 7 | 8 | /* 9 | not working 10 | */ 11 | 12 | func (c *client) IAm(dest btypes.Address, iam btypes.IAm) error { 13 | npdu := &btypes.NPDU{ 14 | Version: btypes.ProtocolVersion, 15 | Destination: &dest, 16 | //IsNetworkLayerMessage: true, 17 | //NetworkLayerMessageType: 0x12, 18 | //Source: c.dataLink.GetMyAddress(), 19 | ExpectingReply: false, 20 | Priority: btypes.Normal, 21 | HopCount: btypes.DefaultHopCount, 22 | } 23 | enc := encoding.NewEncoder() 24 | enc.NPDU(npdu) 25 | enc.IAm(iam) 26 | _, err := c.Send(dest, npdu, enc.Bytes(), nil) 27 | return err 28 | } 29 | -------------------------------------------------------------------------------- /iam_test.go: -------------------------------------------------------------------------------- 1 | package bacnet 2 | 3 | import ( 4 | "fmt" 5 | pprint "github.com/NubeDev/bacnet/helpers/print" 6 | "go/build" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | var iface = "enp0s31f6" 12 | 13 | func TestIam(t *testing.T) { 14 | 15 | gopath := os.Getenv("GOPATH") 16 | if gopath == "" { 17 | gopath = build.Default.GOPATH 18 | } 19 | fmt.Println(gopath) 20 | 21 | cb := &ClientBuilder{ 22 | Interface: iface, 23 | } 24 | c, _ := NewClient(cb) 25 | defer c.Close() 26 | go c.ClientRun() 27 | 28 | //resp := c.WhatIsNetworkNumber() 29 | 30 | resp := c.WhoIsRouterToNetwork() 31 | fmt.Println("WhoIsRouterToNetwork") 32 | pprint.Print(resp) 33 | 34 | } 35 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package bacnet 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/NubeDev/bacnet/btypes" 7 | "github.com/NubeDev/bacnet/datalink" 8 | "github.com/NubeDev/bacnet/encoding" 9 | "log" 10 | "testing" 11 | ) 12 | 13 | const interfaceName = "eth0" 14 | const testServer = 1234 15 | 16 | // TestMain are general test 17 | func TestUdpDataLink(t *testing.T) { 18 | c, _ := NewClient(&ClientBuilder{Interface: interfaceName}) 19 | c.Close() 20 | 21 | _, err := datalink.NewUDPDataLink("pizzainterfacenotreal", 0) 22 | if err == nil { 23 | t.Fatal("Successfully passed a false interface.") 24 | } 25 | } 26 | 27 | func TestMac(t *testing.T) { 28 | var mac []byte 29 | json.Unmarshal([]byte("\"ChQAzLrA\""), &mac) 30 | l := len(mac) 31 | p := uint16(mac[l-1])<<8 | uint16(mac[l-1]) 32 | log.Printf("%d", p) 33 | } 34 | 35 | func TestServices(t *testing.T) { 36 | c, _ := NewClient(&ClientBuilder{Interface: interfaceName}) 37 | defer c.Close() 38 | 39 | t.Run("Read Property", func(t *testing.T) { 40 | testReadPropertyService(c, t) 41 | }) 42 | 43 | t.Run("Who Is", func(t *testing.T) { 44 | testWhoIs(c, t) 45 | }) 46 | 47 | t.Run("WriteProperty", func(t *testing.T) { 48 | testWritePropertyService(c, t) 49 | }) 50 | 51 | } 52 | 53 | func testReadPropertyService(c Client, t *testing.T) { 54 | wh := &WhoIsOpts{ 55 | GlobalBroadcast: false, 56 | NetworkNumber: 0, 57 | } 58 | wh.Low = testServer 59 | wh.High = testServer 60 | dev, err := c.WhoIs(wh) 61 | read := btypes.PropertyData{ 62 | Object: btypes.Object{ 63 | ID: btypes.ObjectID{ 64 | Type: btypes.AnalogValue, 65 | Instance: 1, 66 | }, 67 | Properties: []btypes.Property{ 68 | btypes.Property{ 69 | Type: btypes.PropObjectName, // Present value 70 | ArrayIndex: ArrayAll, 71 | }, 72 | }, 73 | }, 74 | } 75 | if len(dev) == 0 { 76 | t.Fatalf("Unable to find device id %d", testServer) 77 | } 78 | 79 | resp, err := c.ReadProperty(dev[0], read) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | t.Logf("Response: %v", resp.Object.Properties[0].Data) 84 | } 85 | 86 | func testWhoIs(c Client, t *testing.T) { 87 | wh := &WhoIsOpts{ 88 | GlobalBroadcast: false, 89 | NetworkNumber: 0, 90 | } 91 | wh.Low = testServer - 1 92 | wh.High = testServer + 1 93 | dev, err := c.WhoIs(wh) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | if len(dev) == 0 { 98 | t.Fatalf("Unable to find device id %d", testServer) 99 | } 100 | } 101 | 102 | // This test will first cconver the name of an analogue sensor to a different 103 | // value, read the property to make sure the name was changed, revert back, and 104 | // ensure that the revert was successful 105 | func testWritePropertyService(c Client, t *testing.T) { 106 | const targetName = "Hotdog" 107 | wh := &WhoIsOpts{ 108 | GlobalBroadcast: false, 109 | NetworkNumber: 0, 110 | } 111 | wh.Low = testServer 112 | wh.High = testServer 113 | dev, err := c.WhoIs(wh) 114 | wp := btypes.PropertyData{ 115 | Object: btypes.Object{ 116 | ID: btypes.ObjectID{ 117 | Type: btypes.AnalogValue, 118 | Instance: 1, 119 | }, 120 | Properties: []btypes.Property{ 121 | btypes.Property{ 122 | Type: btypes.PropObjectName, // Present value 123 | ArrayIndex: ArrayAll, 124 | Priority: btypes.Normal, 125 | }, 126 | }, 127 | }, 128 | } 129 | 130 | if len(dev) == 0 { 131 | t.Fatalf("Unable to find device id %d", testServer) 132 | } 133 | resp, err := c.ReadProperty(dev[0], wp) 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | // Store the original response since we plan to put it back in after 138 | org := resp.Object.Properties[0].Data 139 | t.Logf("original name is: %d", org) 140 | 141 | wp.Object.Properties[0].Data = targetName 142 | err = c.WriteProperty(dev[0], wp) 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | 147 | resp, err = c.ReadProperty(dev[0], wp) 148 | if err != nil { 149 | t.Fatal(err) 150 | } 151 | 152 | d := resp.Object.Properties[0].Data 153 | s, ok := d.(string) 154 | if !ok { 155 | log.Fatalf("unexpected return type %T", d) 156 | } 157 | 158 | if s != targetName { 159 | log.Fatalf("write to name %s did not successed, name was %s", targetName, s) 160 | } 161 | 162 | // Revert Changes 163 | wp.Object.Properties[0].Data = org 164 | err = c.WriteProperty(dev[0], wp) 165 | if err != nil { 166 | t.Fatal(err) 167 | } 168 | 169 | resp, err = c.ReadProperty(dev[0], wp) 170 | if err != nil { 171 | t.Fatal(err) 172 | } 173 | 174 | if resp.Object.Properties[0].Data != org { 175 | t.Fatalf("unable to revert name back to original value %v: name is %v", org, resp.Object.Properties[0].Data) 176 | } 177 | } 178 | 179 | func TestDeviceClient(t *testing.T) { 180 | c, _ := NewClient(&ClientBuilder{Interface: interfaceName}) 181 | go c.ClientRun() 182 | wh := &WhoIsOpts{ 183 | GlobalBroadcast: false, 184 | NetworkNumber: 0, 185 | } 186 | wh.Low = testServer - 1 187 | wh.High = testServer - 1 188 | devs, err := c.WhoIs(wh) 189 | if err != nil { 190 | fmt.Println(err) 191 | return 192 | } 193 | fmt.Printf("%+v\n", devs) 194 | // c.Objects(devs[0]) 195 | 196 | prop, err := c.ReadProperty( 197 | devs[0], 198 | btypes.PropertyData{ 199 | Object: btypes.Object{ 200 | ID: btypes.ObjectID{ 201 | Type: btypes.AnalogInput, 202 | Instance: 0, 203 | }, 204 | Properties: []btypes.Property{{ 205 | Type: 85, 206 | ArrayIndex: encoding.ArrayAll, 207 | }}, 208 | }, 209 | ErrorClass: 0, 210 | ErrorCode: 0, 211 | }) 212 | if err != nil { 213 | fmt.Println(err) 214 | return 215 | } 216 | fmt.Println(prop.Object.Properties) 217 | 218 | props, err := c.ReadMultiProperty(devs[0], btypes.MultiplePropertyData{Objects: []btypes.Object{ 219 | { 220 | ID: btypes.ObjectID{ 221 | Type: btypes.AnalogInput, 222 | Instance: 0, 223 | }, 224 | Properties: []btypes.Property{ 225 | { 226 | Type: 8, 227 | ArrayIndex: encoding.ArrayAll, 228 | }, 229 | /* { 230 | Type: 85, 231 | ArrayIndex: encoding.ArrayAll, 232 | },*/ 233 | }, 234 | }, 235 | }}) 236 | 237 | fmt.Println(props) 238 | if err != nil { 239 | fmt.Println(err) 240 | return 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /misc.go: -------------------------------------------------------------------------------- 1 | package bacnet 2 | 3 | // From: 4 | // https://stackoverflow.com/questions/6878590/the-maximum-value-for-an-int-type-in-go 5 | const ( 6 | maxUint = ^uint(0) 7 | minUint = 0 8 | // based on 2's complement structure of max int 9 | maxInt = int(maxUint >> 1) 10 | minInt = -maxInt - 1 11 | ) 12 | 13 | func max(a, b int) int { 14 | if a > b { 15 | return a 16 | } 17 | return b 18 | } 19 | 20 | func min(a, b int) int { 21 | if a < b { 22 | return a 23 | } 24 | return b 25 | } 26 | -------------------------------------------------------------------------------- /network/base.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "errors" 5 | "github.com/NubeDev/bacnet" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | type Network struct { 10 | Interface string 11 | Ip string 12 | Port int 13 | SubnetCIDR int 14 | StoreID string 15 | bacnet bacnet.Client 16 | Store *Store 17 | } 18 | 19 | // New returns a new instance of bacnet network 20 | func New(net *Network) (*Network, error) { 21 | cb := &bacnet.ClientBuilder{ 22 | Interface: net.Interface, 23 | Ip: net.Ip, 24 | Port: net.Port, 25 | SubnetCIDR: net.SubnetCIDR, 26 | } 27 | bc, err := bacnet.NewClient(cb) 28 | if err != nil { 29 | return nil, err 30 | } 31 | net.bacnet = bc 32 | if net.Store.BacStore != nil { 33 | net.Store.BacStore.Set(net.StoreID, net, -1) 34 | } else { 35 | return nil, errors.New("failed to set bacnet store, bacnet store is empty") 36 | } 37 | return net, nil 38 | } 39 | 40 | func (net *Network) NetworkClose(closeLogs bool) error { 41 | if net.bacnet != nil { 42 | log.Infof("close bacnet network") 43 | err := net.bacnet.ClientClose(closeLogs) 44 | if err != nil { 45 | log.Errorf("close bacnet network err:%s", err.Error()) 46 | return err 47 | } 48 | } 49 | return nil 50 | 51 | } 52 | 53 | func (net *Network) NetworkRun() { 54 | if net.bacnet != nil { 55 | net.bacnet.ClientRun() 56 | } 57 | 58 | } 59 | 60 | func (net *Network) store() { 61 | 62 | } 63 | -------------------------------------------------------------------------------- /network/client_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet" 6 | "github.com/NubeDev/bacnet/btypes" 7 | segmentation2 "github.com/NubeDev/bacnet/btypes/segmentation" 8 | 9 | //"github.com/NubeDev/bacnet" 10 | 11 | "testing" 12 | ) 13 | 14 | var iface = "wlp0s20f3" 15 | var NetworkDevicePort = 47808 16 | var deviceIP = "192.168.15.189" 17 | var deviceID = 1122 18 | var networkNumber = 0 19 | var macMSTP = 0 20 | var segmentation = segmentation2.SegmentedBoth 21 | var MaxApdu uint32 = btypes.MaxAPDU1476 22 | var storeID = "my-id" 23 | 24 | /* 25 | MaxApdu 26 | 0 = 50 27 | 1 = 128 28 | 2 = 206 jci PCG 29 | 3 = 480 honeywell spyder 30 | 4 = 1024 31 | 5 = 1476 easyIO-30p when over IP 32 | 33 | BACnetSegmentation: 34 | segmented-both:0 35 | segmented-transmit:1 36 | segmented-receive:2 37 | no-segmentation: 3 38 | */ 39 | 40 | func TestWhoIs(t *testing.T) { 41 | 42 | client, err := New(&Network{Interface: iface, Port: NetworkDevicePort}) 43 | if err != nil { 44 | fmt.Println("ERR-client", err) 45 | return 46 | } 47 | defer client.NetworkClose(false) 48 | go client.NetworkRun() 49 | 50 | wi := &bacnet.WhoIsOpts{ 51 | High: 0, 52 | Low: 0, 53 | GlobalBroadcast: true, 54 | NetworkNumber: 0, 55 | } 56 | 57 | whoIs, err := client.Whois(wi) 58 | if err != nil { 59 | fmt.Println("ERR-whoIs", err) 60 | return 61 | } 62 | 63 | for _, dev := range whoIs { 64 | fmt.Println(dev.ID) 65 | fmt.Println(dev.Vendor) 66 | } 67 | 68 | } 69 | 70 | func TestReadObj(t *testing.T) { 71 | 72 | NetworkDevice, err := New(&Network{Interface: iface, Port: NetworkDevicePort}) 73 | if err != nil { 74 | fmt.Println("ERR-client", err) 75 | return 76 | } 77 | defer NetworkDevice.NetworkClose(false) 78 | go NetworkDevice.NetworkRun() 79 | 80 | device, err := NewDevice(NetworkDevice, &Device{Ip: deviceIP, DeviceID: deviceID, NetworkNumber: networkNumber, MacMSTP: macMSTP, MaxApdu: MaxApdu, Segmentation: uint32(segmentation)}) 81 | if err != nil { 82 | return 83 | } 84 | 85 | obj := &Object{ 86 | ObjectID: 1, 87 | ObjectType: btypes.AnalogInput, 88 | Prop: btypes.PropUnits, 89 | ArrayIndex: btypes.ArrayAll, //btypes.ArrayAll 90 | 91 | } 92 | 93 | out, err := device.Read(obj) 94 | fmt.Println(err) 95 | fmt.Println(out) 96 | //fmt.Println("DATA", out.Object.Properties[0].Data) 97 | 98 | } 99 | 100 | func TestWriteObj(t *testing.T) { 101 | 102 | NetworkDevice, err := New(&Network{Interface: iface, Port: NetworkDevicePort}) 103 | if err != nil { 104 | fmt.Println("ERR-client", err) 105 | return 106 | } 107 | defer NetworkDevice.NetworkClose(false) 108 | go NetworkDevice.NetworkRun() 109 | 110 | device, err := NewDevice(NetworkDevice, &Device{Ip: deviceIP, DeviceID: deviceID}) 111 | if err != nil { 112 | return 113 | } 114 | 115 | device.Write(&Write{ObjectID: 1234, ObjectType: btypes.DeviceType, Prop: btypes.PropObjectName, WriteValue: "aidan test"}) 116 | 117 | } 118 | -------------------------------------------------------------------------------- /network/device.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet" 6 | "github.com/NubeDev/bacnet/btypes" 7 | ) 8 | 9 | type Device struct { 10 | Ip string 11 | Port int 12 | DeviceID int 13 | NetworkNumber int 14 | MacMSTP int 15 | MaxApdu uint32 16 | Segmentation uint32 17 | StoreID string 18 | DeviceName string 19 | VendorName string 20 | dev btypes.Device 21 | network bacnet.Client 22 | } 23 | 24 | // NewDevice returns a new instance of ta bacnet device 25 | func NewDevice(net *Network, device *Device) (*Device, error) { 26 | var err error 27 | if net == nil { 28 | fmt.Println("network can not be nil") 29 | return nil, err 30 | } 31 | dev := &btypes.Device{ 32 | Ip: device.Ip, 33 | DeviceID: device.DeviceID, 34 | NetworkNumber: device.NetworkNumber, 35 | MacMSTP: device.MacMSTP, 36 | MaxApdu: device.MaxApdu, 37 | Segmentation: btypes.Enumerated(device.Segmentation), 38 | } 39 | dev, err = btypes.NewDevice(dev) 40 | if err != nil { 41 | return nil, err 42 | } 43 | if dev == nil { 44 | fmt.Println("dev is nil") 45 | return nil, err 46 | } 47 | device.network = net.bacnet 48 | device.dev = *dev 49 | if net.Store.BacStore != nil { 50 | net.Store.BacStore.Set(device.StoreID, device, -1) 51 | } 52 | return device, nil 53 | } 54 | -------------------------------------------------------------------------------- /network/discover.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet" 6 | "github.com/NubeDev/bacnet/btypes" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type DevicePoints struct { 11 | Name string `json:"name"` 12 | MaxApdu uint32 `json:"max_apdu"` 13 | VendorName string `json:"vendor_name"` 14 | Segmentation uint32 `json:"segmentation"` 15 | ProtocolServicesSupported *btypes.BitString `json:"protocol_services_supported"` 16 | } 17 | 18 | //GetDevicePoints build a device points list 19 | //first read device and see what it supports and get the name and so on 20 | //try and get the object list if it's an error then loop through the arrayIndex to build the object list 21 | //with the object list do a point's discovery, get the name, units and so on 22 | func (device *Device) GetDevicePoints(deviceID btypes.ObjectInstance) (resp []*PointDetails, err error) { 23 | resp = []*PointDetails{} 24 | list, err := device.DeviceObjects(deviceID, true) 25 | if err != nil { 26 | return nil, err 27 | } 28 | pntDetails := &Point{} 29 | for _, obj := range list { 30 | 31 | if obj.Type != 8 { 32 | pntDetails = &Point{ 33 | ObjectID: obj.Instance, 34 | ObjectType: obj.Type, 35 | } 36 | details, _ := device.PointDetails(pntDetails) 37 | resp = append(resp, details) 38 | } 39 | } 40 | return resp, nil 41 | 42 | } 43 | 44 | type DeviceDetails struct { 45 | Name string `json:"name"` 46 | MaxApdu uint32 `json:"max_apdu"` 47 | VendorName string `json:"vendor_name"` 48 | Segmentation uint32 `json:"segmentation"` 49 | ProtocolServicesSupported *btypes.BitString `json:"protocol_services_supported"` 50 | } 51 | 52 | //GetDeviceDetails get the device name, max adpu and so on 53 | //first read device and see what it supports and get the name and so on 54 | //try and get the object list if it's an error then loop through the arrayIndex to build the object list 55 | func (device *Device) GetDeviceDetails(deviceID btypes.ObjectInstance) (resp *DeviceDetails, err error) { 56 | resp = &DeviceDetails{} 57 | obj := &Object{ 58 | ObjectID: deviceID, 59 | ObjectType: btypes.TypeDeviceType, 60 | Prop: btypes.PropObjectName, 61 | ArrayIndex: bacnet.ArrayAll, 62 | } 63 | props := []btypes.PropertyType{btypes.PropObjectName, btypes.PropMaxAPDU, btypes.PropVendorName, btypes.PropSegmentationSupported} 64 | for _, prop := range props { 65 | obj.Prop = prop 66 | read, err := device.Read(obj) 67 | if err != nil { 68 | log.Errorln("bacnet-master-GetDeviceDetails()", err.Error()) 69 | } 70 | switch prop { 71 | case btypes.PropObjectName: 72 | resp.Name = device.toStr(read) 73 | case btypes.PropMaxAPDU: 74 | resp.MaxApdu = device.toUint32(read) 75 | case btypes.PropVendorName: 76 | resp.VendorName = device.toStr(read) 77 | case btypes.PropSegmentationSupported: 78 | resp.Segmentation = device.toUint32(read) 79 | case btypes.ProtocolServicesSupported: 80 | resp.ProtocolServicesSupported = device.ToBitString(read) 81 | } 82 | } 83 | log.Infoln("bacnet-device name:", resp.Name) 84 | log.Infoln("bacnet-device vendor-name:", resp.VendorName) 85 | return resp, nil 86 | } 87 | 88 | func (device *Device) DeviceDiscover(options *bacnet.WhoIsOpts) ([]*Device, error) { 89 | whois, err := device.Whois(options) 90 | var devices []*Device 91 | if err != nil { 92 | return devices, err 93 | } 94 | for _, dev := range whois { 95 | if len(dev.Addr.Adr) > 0 { 96 | device.MacMSTP = int(dev.Addr.Adr[0]) 97 | } 98 | host, _ := dev.Addr.UDPAddr() 99 | device.DeviceID = int(dev.ID.Instance) 100 | device.Ip = host.IP.String() 101 | device.NetworkNumber = int(dev.Addr.Net) 102 | device.MaxApdu = dev.MaxApdu 103 | device.Segmentation = uint32(dev.Segmentation) 104 | details, err := device.GetDeviceDetails(dev.ID.Instance) 105 | if err != nil { 106 | fmt.Println("discover err", err) 107 | } 108 | device.DeviceName = details.Name 109 | device.VendorName = details.VendorName 110 | devices = append(devices, device) 111 | 112 | } 113 | count := 1 114 | for _, d := range devices { 115 | fmt.Println("devices found", count) 116 | count++ 117 | fmt.Println(d.VendorName, d.DeviceName, d.Ip) 118 | } 119 | return devices, err 120 | } 121 | -------------------------------------------------------------------------------- /network/discover_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | pprint "github.com/NubeDev/bacnet/helpers/print" 6 | 7 | //"github.com/NubeDev/bacnet" 8 | 9 | "testing" 10 | ) 11 | 12 | func TestDiscover(t *testing.T) { 13 | 14 | localDevice, err := New(&Network{Interface: iface, Port: 47808}) 15 | if err != nil { 16 | fmt.Println("ERR-client", err) 17 | return 18 | } 19 | defer localDevice.NetworkClose(false) 20 | go localDevice.NetworkRun() 21 | 22 | device, err := NewDevice(localDevice, &Device{Ip: deviceIP, DeviceID: deviceID}) 23 | if err != nil { 24 | return 25 | } 26 | 27 | objects, err := device.DeviceObjects(12, true) 28 | if err != nil { 29 | return 30 | } 31 | pprint.PrintJOSN(objects) 32 | 33 | } 34 | 35 | func TestGetPointsList(t *testing.T) { 36 | 37 | localDevice, err := New(&Network{Interface: iface, Port: 47808}) 38 | if err != nil { 39 | fmt.Println("ERR-client", err) 40 | return 41 | } 42 | defer localDevice.NetworkClose(false) 43 | go localDevice.NetworkRun() 44 | 45 | device, err := NewDevice(localDevice, &Device{Ip: deviceIP, DeviceID: deviceID}) 46 | if err != nil { 47 | return 48 | } 49 | 50 | objects, err := device.GetDevicePoints(12) 51 | if err != nil { 52 | return 53 | } 54 | pprint.PrintJOSN(objects) 55 | 56 | } 57 | -------------------------------------------------------------------------------- /network/err.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import "errors" 4 | 5 | var ( 6 | ObjectNil = errors.New("object list can not be empty") 7 | ) 8 | -------------------------------------------------------------------------------- /network/objects.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet" 6 | "github.com/NubeDev/bacnet/btypes" 7 | "github.com/NubeDev/bacnet/helpers/data" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | //DeviceObjects get device objects 12 | func (device *Device) DeviceObjects(deviceID btypes.ObjectInstance, checkAPDU bool) (objectList []btypes.ObjectID, err error) { 13 | if checkAPDU { //check set the maxADPU and Segmentation 14 | whoIs, err := device.Whois(&bacnet.WhoIsOpts{ 15 | High: int(deviceID), 16 | Low: int(deviceID), 17 | GlobalBroadcast: true, 18 | }) 19 | if err != nil { 20 | return nil, err 21 | } 22 | for _, dev := range whoIs { 23 | if dev.ID.Instance == deviceID { 24 | device.MaxApdu = dev.MaxApdu 25 | device.Segmentation = uint32(dev.Segmentation) 26 | } 27 | } 28 | log.Infoln("bacnet.DeviceObjects() do whois on deviceID:", deviceID, " maxADPU:", device.MaxApdu, " Segmentation:", device.Segmentation) 29 | } 30 | 31 | //get object list 32 | obj := &Object{ 33 | ObjectID: deviceID, 34 | ObjectType: btypes.DeviceType, 35 | Prop: btypes.PropObjectList, 36 | ArrayIndex: btypes.ArrayAll, //btypes.ArrayAll 37 | 38 | } 39 | out, err := device.Read(obj) 40 | if err != nil { //this is a device that would have a low maxADPU 41 | fmt.Println("DeviceObjects, now read here", err) 42 | return device.deviceObjectsBuilder(deviceID) 43 | 44 | } 45 | if len(out.Object.Properties) == 0 { 46 | fmt.Println("No value returned") 47 | return nil, nil 48 | } 49 | _, ids := data.ToArr(out) 50 | for _, id := range ids { 51 | objectID := id.(btypes.ObjectID) 52 | objectList = append(objectList, objectID) 53 | } 54 | return objectList, nil 55 | } 56 | 57 | //DeviceObjectsBuilder this is used when a device can't send the object list in the fully ArrayIndex 58 | //it first reads the size of the object list and then loops the list to build an object list 59 | func (device *Device) deviceObjectsBuilder(deviceID btypes.ObjectInstance) (objectList []btypes.ObjectID, err error) { 60 | //get object list 61 | obj := &Object{ 62 | ObjectID: deviceID, 63 | ObjectType: btypes.DeviceType, 64 | Prop: btypes.PropObjectList, 65 | ArrayIndex: 0, //start at 0 and then loop through 66 | } 67 | out, err := device.Read(obj) 68 | if err != nil { 69 | log.Errorln("failed to read object list in deviceObjectsBuilder() err:", err) 70 | return nil, err 71 | } 72 | _, o := data.ToUint32(out) 73 | log.Println("size of object-list", o) 74 | var listLen = int(o) 75 | for i := 1; i <= listLen; i++ { 76 | obj.ArrayIndex = uint32(i) 77 | out, _ := device.Read(obj) 78 | objectID := out.Object.Properties[0].Data.(btypes.ObjectID) 79 | objectList = append(objectList, objectID) 80 | } 81 | return objectList, err 82 | 83 | } 84 | -------------------------------------------------------------------------------- /network/points_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet/btypes" 6 | pprint "github.com/NubeDev/bacnet/helpers/print" 7 | "testing" 8 | ) 9 | 10 | func TestPointDetails(t *testing.T) { 11 | 12 | localDevice, err := New(&Network{Interface: iface, Port: 47808}) 13 | if err != nil { 14 | fmt.Println("ERR-client", err) 15 | return 16 | } 17 | defer localDevice.NetworkClose(false) 18 | go localDevice.NetworkRun() 19 | 20 | device, err := NewDevice(localDevice, &Device{Ip: deviceIP, DeviceID: deviceID}) 21 | if err != nil { 22 | return 23 | } 24 | 25 | pnt := &Point{ 26 | ObjectID: 2, 27 | ObjectType: btypes.AnalogInput, 28 | } 29 | 30 | readFloat64, err := device.PointDetails(pnt) 31 | if err != nil { 32 | //return 33 | } 34 | 35 | fmt.Println(readFloat64, err) 36 | 37 | } 38 | 39 | func TestRead(t *testing.T) { 40 | 41 | localDevice, err := New(&Network{Interface: iface, Port: 47808}) 42 | if err != nil { 43 | fmt.Println("ERR-client", err) 44 | return 45 | } 46 | defer localDevice.NetworkClose(false) 47 | go localDevice.NetworkRun() 48 | 49 | device, err := NewDevice(localDevice, &Device{Ip: deviceIP, DeviceID: deviceID}) 50 | if err != nil { 51 | return 52 | } 53 | 54 | pnt := &Point{ 55 | ObjectID: 1, 56 | ObjectType: btypes.AnalogOutput, 57 | } 58 | 59 | readFloat64, err := device.PointReadFloat32(pnt) 60 | if err != nil { 61 | return 62 | } 63 | 64 | fmt.Println(readFloat64, err) 65 | 66 | } 67 | 68 | func TestReadWrite(t *testing.T) { 69 | 70 | localDevice, err := New(&Network{Interface: iface, Port: 47809}) 71 | if err != nil { 72 | fmt.Println("ERR-client", err) 73 | return 74 | } 75 | defer localDevice.NetworkClose(false) 76 | go localDevice.NetworkRun() 77 | 78 | device, err := NewDevice(localDevice, &Device{Ip: deviceIP, DeviceID: deviceID}) 79 | if err != nil { 80 | return 81 | } 82 | 83 | pnt := &Point{ 84 | ObjectID: 1, 85 | ObjectType: btypes.AnalogOutput, 86 | WriteValue: nil, 87 | WriteNull: false, 88 | WritePriority: 15, 89 | ReadPresentValue: false, 90 | ReadPriority: false, 91 | } 92 | 93 | err = device.PointWriteAnalogue(pnt, 11) 94 | if err != nil { 95 | //return 96 | } 97 | fmt.Println(err) 98 | readFloat64, err := device.PointReadFloat32(pnt) 99 | if err != nil { 100 | return 101 | } 102 | 103 | fmt.Println(readFloat64, err) 104 | 105 | } 106 | 107 | func TestPointReleasePriority(t *testing.T) { 108 | 109 | localDevice, err := New(&Network{Interface: iface, Port: 47808}) 110 | if err != nil { 111 | fmt.Println("ERR-client", err) 112 | return 113 | } 114 | defer localDevice.NetworkClose(false) 115 | go localDevice.NetworkRun() 116 | 117 | device, err := NewDevice(localDevice, &Device{Ip: deviceIP, DeviceID: deviceID}) 118 | if err != nil { 119 | return 120 | } 121 | 122 | pnt := &Point{ 123 | ObjectID: 1, 124 | ObjectType: btypes.AnalogOutput, 125 | WriteValue: nil, 126 | WriteNull: false, 127 | WritePriority: 15, 128 | ReadPresentValue: false, 129 | ReadPriority: false, 130 | } 131 | 132 | err = device.PointReleasePriority(pnt, 15) 133 | if err != nil { 134 | //return 135 | } 136 | 137 | } 138 | 139 | func TestReadPri(t *testing.T) { 140 | 141 | localDevice, err := New(&Network{Interface: iface, Port: 47808}) 142 | if err != nil { 143 | fmt.Println("ERR-client", err) 144 | return 145 | } 146 | defer localDevice.NetworkClose(false) 147 | 148 | device, err := NewDevice(localDevice, &Device{Ip: deviceIP, DeviceID: deviceID}) 149 | if err != nil { 150 | return 151 | } 152 | 153 | pnt := &Point{ 154 | ObjectID: 1, 155 | ObjectType: btypes.AnalogOutput, 156 | } 157 | 158 | fmt.Println(err) 159 | readFloat64, err := device.PointReadPriority(pnt) 160 | if err != nil { 161 | return 162 | } 163 | pprint.PrintJOSN(readFloat64) 164 | 165 | } 166 | -------------------------------------------------------------------------------- /network/read.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "github.com/NubeDev/bacnet/btypes" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | type Object struct { 9 | ObjectID btypes.ObjectInstance `json:"object_id"` 10 | ObjectType btypes.ObjectType `json:"object_type"` 11 | Prop btypes.PropertyType `json:"prop"` 12 | ArrayIndex uint32 `json:"array_index"` 13 | } 14 | 15 | func (device *Device) Read(obj *Object) (out btypes.PropertyData, err error) { 16 | if obj == nil { 17 | return out, ObjectNil 18 | } 19 | rp := btypes.PropertyData{ 20 | Object: btypes.Object{ 21 | ID: btypes.ObjectID{ 22 | Type: obj.ObjectType, 23 | Instance: obj.ObjectID, 24 | }, 25 | Properties: []btypes.Property{ 26 | { 27 | Type: obj.Prop, 28 | ArrayIndex: obj.ArrayIndex, //bacnet.ArrayAll 29 | }, 30 | }, 31 | }, 32 | } 33 | out, err = device.network.ReadProperty(device.dev, rp) 34 | if err != nil { 35 | if rp.Object.Properties[0].Type == btypes.PropObjectList { 36 | log.Errorln("network.Read(): PropObjectList reads may need to be broken up into multiple reads due to length. Read index 0 for array length err:", err) 37 | } else { 38 | log.Errorln("network.Read(): err:", err) 39 | } 40 | return out, err 41 | } 42 | if len(out.Object.Properties) == 0 { 43 | log.Errorln("network.Read(): no values returned") 44 | return out, nil 45 | } 46 | return out, nil 47 | } 48 | -------------------------------------------------------------------------------- /network/store.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet" 6 | "github.com/NubeDev/bacnet/btypes" 7 | "github.com/NubeDev/bacnet/helpers/store" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | //var memDb *store.Handler 12 | 13 | type Store struct { 14 | BacStore *store.Handler 15 | } 16 | 17 | func NewStore() *Store { 18 | s := &Store{ 19 | BacStore: store.Init(), 20 | } 21 | return s 22 | } 23 | 24 | //NewNetwork updated a cached 25 | func (store *Store) NewNetwork(storeID, iface, ip string, port, subnet int) error { 26 | cb := &bacnet.ClientBuilder{ 27 | Interface: iface, 28 | Ip: ip, 29 | Port: port, 30 | SubnetCIDR: subnet, 31 | } 32 | bc, err := bacnet.NewClient(cb) 33 | if err != nil { 34 | return err 35 | } 36 | bacnetNet := &Network{ 37 | Interface: iface, 38 | Port: port, 39 | bacnet: bc, 40 | } 41 | if store.BacStore != nil { 42 | store.BacStore.Set(storeID, bacnetNet, -1) 43 | return nil 44 | } else { 45 | return errors.New("new network, failed to set bacnet store, bacnet store is empty") 46 | } 47 | } 48 | 49 | func (store *Store) GetNetwork(uuid string) (*Network, error) { 50 | cli, ok := store.BacStore.Get(uuid) 51 | if !ok { 52 | return nil, errors.New(fmt.Sprintf("bacnet: no network found with uuid:%s", uuid)) 53 | } 54 | parse := cli.(*Network) 55 | return parse, nil 56 | } 57 | 58 | //UpdateDevice updated a cached device 59 | func (store *Store) UpdateDevice(storeID string, net *Network, device *Device) error { 60 | var err error 61 | dev := &btypes.Device{ 62 | Ip: device.Ip, 63 | DeviceID: device.DeviceID, 64 | NetworkNumber: device.NetworkNumber, 65 | MacMSTP: device.MacMSTP, 66 | MaxApdu: device.MaxApdu, 67 | Segmentation: btypes.Enumerated(device.Segmentation), 68 | } 69 | dev, err = btypes.NewDevice(dev) 70 | if err != nil { 71 | return err 72 | } 73 | if dev == nil { 74 | fmt.Println("dev is nil") 75 | return err 76 | } 77 | device.network = net.bacnet 78 | device.dev = *dev 79 | if store.BacStore != nil { 80 | store.BacStore.Set(storeID, device, -1) 81 | } 82 | return nil 83 | } 84 | 85 | func (store *Store) GetDevice(uuid string) (*Device, error) { 86 | cli, ok := store.BacStore.Get(uuid) 87 | if !ok { 88 | return nil, errors.New(fmt.Sprintf("bacnet: no device found with uuid:%s", uuid)) 89 | } 90 | parse := cli.(*Device) 91 | return parse, nil 92 | } 93 | -------------------------------------------------------------------------------- /network/strings.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "github.com/NubeDev/bacnet" 5 | "github.com/NubeDev/bacnet/btypes" 6 | ) 7 | 8 | //ReadString to read a string like objectName 9 | func (device *Device) ReadString(obj *Object) (string, error) { 10 | read, err := device.Read(obj) 11 | if err != nil { 12 | return "", err 13 | } 14 | return device.toStr(read), nil 15 | } 16 | 17 | func (device *Device) ReadDeviceName(ObjectID btypes.ObjectInstance) (string, error) { 18 | obj := &Object{ 19 | ObjectID: ObjectID, 20 | ObjectType: btypes.DeviceType, 21 | Prop: btypes.PropObjectName, 22 | ArrayIndex: bacnet.ArrayAll, 23 | } 24 | read, err := device.Read(obj) 25 | if err != nil { 26 | return "", err 27 | } 28 | return device.toStr(read), nil 29 | } 30 | 31 | func (device *Device) WriteDeviceName(ObjectID btypes.ObjectInstance, value string) error { 32 | write := &Write{ 33 | ObjectID: ObjectID, 34 | ObjectType: btypes.DeviceType, 35 | Prop: btypes.PropObjectName, 36 | WriteValue: value, 37 | } 38 | err := device.Write(write) 39 | if err != nil { 40 | return err 41 | } 42 | return nil 43 | } 44 | 45 | func (device *Device) ReadPointName(pnt *Point) (string, error) { 46 | obj := &Object{ 47 | ObjectID: pnt.ObjectID, 48 | ObjectType: pnt.ObjectType, 49 | Prop: btypes.PropObjectName, 50 | ArrayIndex: bacnet.ArrayAll, 51 | } 52 | read, err := device.Read(obj) 53 | if err != nil { 54 | return "", err 55 | } 56 | return device.toStr(read), nil 57 | } 58 | 59 | func (device *Device) WritePointName(pnt *Point, value string) error { 60 | write := &Write{ 61 | ObjectID: pnt.ObjectID, 62 | ObjectType: pnt.ObjectType, 63 | Prop: btypes.PropObjectName, 64 | WriteValue: value, 65 | } 66 | err := device.Write(write) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /network/strings_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet/btypes" 6 | "testing" 7 | ) 8 | 9 | func TestDevice_ReadPointName(t *testing.T) { 10 | 11 | localDevice, err := New(&Network{Interface: iface, Port: 47808}) 12 | if err != nil { 13 | fmt.Println("ERR-client", err) 14 | return 15 | } 16 | defer localDevice.NetworkClose(false) 17 | go localDevice.NetworkRun() 18 | 19 | device, err := NewDevice(localDevice, &Device{Ip: deviceIP, DeviceID: deviceID}) 20 | if err != nil { 21 | return 22 | } 23 | 24 | pnt := &Point{ 25 | ObjectID: 1, 26 | ObjectType: btypes.AnalogOutput, 27 | } 28 | read, err := device.ReadPointName(pnt) 29 | fmt.Println(err) 30 | fmt.Println(read, err) 31 | 32 | } 33 | 34 | func TestDevice_WritePointName(t *testing.T) { 35 | 36 | localDevice, err := New(&Network{Interface: iface, Port: 47808}) 37 | if err != nil { 38 | fmt.Println("ERR-client", err) 39 | return 40 | } 41 | defer localDevice.NetworkClose(false) 42 | go localDevice.NetworkRun() 43 | 44 | device, err := NewDevice(localDevice, &Device{Ip: deviceIP, DeviceID: deviceID}) 45 | if err != nil { 46 | return 47 | } 48 | 49 | pnt := &Point{ 50 | ObjectID: 1, 51 | ObjectType: btypes.AnalogOutput, 52 | } 53 | 54 | err = device.WritePointName(pnt, "new-name") 55 | fmt.Println(err) 56 | if err != nil { 57 | //return 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /network/whois.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "github.com/NubeDev/bacnet" 5 | "github.com/NubeDev/bacnet/btypes" 6 | "github.com/NubeDev/bacnet/helpers/data" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func (device *Device) Whois(options *bacnet.WhoIsOpts) ([]btypes.Device, error) { 11 | go device.network.ClientRun() 12 | resp, err := device.network.WhoIs(options) 13 | return resp, err 14 | } 15 | 16 | func (net *Network) Whois(options *bacnet.WhoIsOpts) ([]btypes.Device, error) { 17 | go net.NetworkRun() 18 | resp, err := net.bacnet.WhoIs(options) 19 | return resp, err 20 | } 21 | 22 | func (net *Network) NetworkDiscover(options *bacnet.WhoIsOpts) ([]btypes.Device, error) { 23 | go net.NetworkRun() 24 | resp, err := net.bacnet.WhoIs(options) 25 | var devices []btypes.Device 26 | for _, device := range resp { 27 | buildDevice := &btypes.Device{ 28 | DeviceID: device.DeviceID, 29 | Ip: device.Ip, 30 | Port: device.Port, 31 | NetworkNumber: device.NetworkNumber, 32 | MacMSTP: device.MacMSTP, 33 | MaxApdu: device.MaxApdu, 34 | Segmentation: device.Segmentation, 35 | } 36 | dev, err := btypes.NewDevice(buildDevice) 37 | details, err := net.GetDeviceDetails(*dev) 38 | if err != nil { 39 | return nil, err 40 | } 41 | device.DeviceName = details.Name 42 | device.VendorName = details.VendorName 43 | device.MaxApdu = details.MaxApdu 44 | device.Segmentation = btypes.Enumerated(details.Segmentation) 45 | devices = append(devices, device) 46 | } 47 | return devices, err 48 | } 49 | 50 | func (net *Network) GetDeviceDetails(device btypes.Device) (resp *DeviceDetails, err error) { 51 | resp = &DeviceDetails{} 52 | props := []btypes.PropertyType{btypes.PropObjectName, btypes.PropMaxAPDU, btypes.PropVendorName, btypes.PropSegmentationSupported} 53 | for _, prop := range props { 54 | property, err := net.bacnet.ReadProperty(device, buildObj(device.DeviceID, prop)) 55 | if err != nil { 56 | log.Errorln("bacnet-master-GetDeviceDetails()", err.Error()) 57 | } 58 | switch prop { 59 | case btypes.PropObjectName: 60 | _, read := data.ToStr(property) 61 | resp.Name = read 62 | case btypes.PropMaxAPDU: 63 | _, read := data.ToUint32(property) 64 | resp.MaxApdu = read 65 | case btypes.PropVendorName: 66 | _, read := data.ToStr(property) 67 | resp.VendorName = read 68 | case btypes.PropSegmentationSupported: 69 | _, read := data.ToUint32(property) 70 | resp.Segmentation = read 71 | case btypes.ProtocolServicesSupported: 72 | _, read := data.ToBitString(property) 73 | resp.ProtocolServicesSupported = read 74 | } 75 | } 76 | log.Infoln("bacnet-device name:", resp.Name) 77 | log.Infoln("bacnet-device vendor-name:", resp.VendorName) 78 | return resp, nil 79 | } 80 | 81 | func buildObj(id int, propertyType btypes.PropertyType) btypes.PropertyData { 82 | rp := btypes.PropertyData{ 83 | Object: btypes.Object{ 84 | ID: btypes.ObjectID{ 85 | Type: btypes.TypeDeviceType, 86 | Instance: btypes.ObjectInstance(id), 87 | }, 88 | Properties: []btypes.Property{ 89 | { 90 | Type: propertyType, 91 | ArrayIndex: bacnet.ArrayAll, //bacnet.ArrayAll 92 | }, 93 | }, 94 | }, 95 | } 96 | 97 | return rp 98 | } 99 | -------------------------------------------------------------------------------- /network/whois_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet" 6 | pprint "github.com/NubeDev/bacnet/helpers/print" 7 | "github.com/kr/pretty" 8 | "testing" 9 | ) 10 | 11 | func TestNetwork_Whois(t *testing.T) { 12 | localDevice, err := New(&Network{Interface: iface, Port: 47808}) 13 | if err != nil { 14 | fmt.Println("ERR-client", err) 15 | return 16 | } 17 | defer localDevice.NetworkClose(false) 18 | go localDevice.NetworkRun() 19 | 20 | whois, err := localDevice.Whois(&bacnet.WhoIsOpts{ 21 | Low: 0, 22 | High: 0, 23 | GlobalBroadcast: true, 24 | NetworkNumber: 0, 25 | }) 26 | fmt.Println(err) 27 | if err != nil { 28 | return 29 | } 30 | 31 | pretty.Print(whois) 32 | } 33 | 34 | func TestNetwork_DeviceDiscover(t *testing.T) { 35 | localDevice, err := New(&Network{Interface: iface, Port: 47808}) 36 | if err != nil { 37 | fmt.Println(err) 38 | return 39 | } 40 | defer localDevice.NetworkClose(false) 41 | go localDevice.NetworkRun() 42 | 43 | devices, err := localDevice.NetworkDiscover(&bacnet.WhoIsOpts{ 44 | Low: 0, 45 | High: 0, 46 | GlobalBroadcast: true, 47 | NetworkNumber: 0, 48 | }) 49 | if err != nil { 50 | fmt.Println(err) 51 | return 52 | } 53 | pprint.PrintJOSN(devices) 54 | fmt.Println(devices) 55 | } 56 | -------------------------------------------------------------------------------- /network/write.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet" 6 | "github.com/NubeDev/bacnet/btypes" 7 | "github.com/NubeDev/bacnet/btypes/null" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type Write struct { 12 | ObjectID btypes.ObjectInstance 13 | ObjectType btypes.ObjectType 14 | Prop btypes.PropertyType 15 | WriteValue interface{} 16 | WriteNull bool 17 | WritePriority uint8 18 | } 19 | 20 | func (device *Device) Write(write *Write) error { 21 | var err error 22 | writeValue := write.WriteValue 23 | 24 | rp := btypes.PropertyData{ 25 | Object: btypes.Object{ 26 | ID: btypes.ObjectID{ 27 | Type: write.ObjectType, 28 | Instance: btypes.ObjectInstance(write.ObjectID), 29 | }, 30 | Properties: []btypes.Property{ 31 | { 32 | Type: write.Prop, 33 | ArrayIndex: bacnet.ArrayAll, 34 | Priority: btypes.NPDUPriority(write.WritePriority), 35 | }, 36 | }, 37 | }, 38 | } 39 | 40 | if write.WriteNull { 41 | writeValue = null.Null{} 42 | } else { 43 | switch writeValue.(type) { 44 | case uint32: 45 | out, _ := writeValue.(uint32) 46 | writeValue = out 47 | case float32: 48 | out, _ := writeValue.(float32) 49 | writeValue = out 50 | case float64: 51 | out, _ := writeValue.(float64) 52 | writeValue = out 53 | case string: 54 | writeValue = fmt.Sprintf("%s", writeValue) 55 | default: 56 | err = fmt.Errorf("unable to handle a type %T", writeValue) 57 | return err 58 | } 59 | if err != nil { 60 | log.Printf("Expects a %T", rp.Object.Properties[0].Data) 61 | return err 62 | } 63 | } 64 | 65 | rp.Object.Properties[0].Data = writeValue 66 | // log.Printf("Writting: %v", writeValue) 67 | err = device.network.WriteProperty(device.dev, rp) 68 | if err != nil { 69 | return err 70 | } 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /network/write_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet/btypes" 6 | "testing" 7 | ) 8 | 9 | func TestDevice_Write(t *testing.T) { 10 | localDevice, err := New(&Network{Interface: iface, Port: 47809}) 11 | if err != nil { 12 | fmt.Println("ERR-client", err) 13 | return 14 | } 15 | defer localDevice.NetworkClose(false) 16 | go localDevice.NetworkRun() 17 | 18 | device, err := NewDevice(localDevice, &Device{Ip: deviceIP, DeviceID: deviceID}) 19 | if err != nil { 20 | return 21 | } 22 | 23 | err = device.Write(&Write{ 24 | ObjectID: 0, 25 | ObjectType: btypes.MultiStateOutput, 26 | Prop: 85, 27 | WriteValue: 1, 28 | WriteNull: false, 29 | WritePriority: 16, 30 | }) 31 | fmt.Println(err) 32 | if err != nil { 33 | return 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /objectlist.go: -------------------------------------------------------------------------------- 1 | package bacnet 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet/btypes" 6 | ) 7 | 8 | func (c *client) objectListLen(dev btypes.Device) (int, error) { 9 | rp := btypes.PropertyData{ 10 | Object: btypes.Object{ 11 | ID: dev.ID, 12 | Properties: []btypes.Property{ 13 | btypes.Property{ 14 | Type: btypes.PropObjectList, 15 | ArrayIndex: 0, 16 | }, 17 | }, 18 | }, 19 | } 20 | 21 | resp, err := c.ReadProperty(dev, rp) 22 | if err != nil { 23 | return 0, fmt.Errorf("reading property failed: %v", err) 24 | } 25 | 26 | if len(resp.Object.Properties) == 0 { 27 | return 0, fmt.Errorf("no data was returned") 28 | } 29 | 30 | data, ok := resp.Object.Properties[0].Data.(uint32) 31 | if !ok { 32 | return 0, fmt.Errorf("unable to get object length") 33 | } 34 | return int(data), nil 35 | } 36 | 37 | func (c *client) objectsRange(dev btypes.Device, start, end int) ([]btypes.Object, error) { 38 | rpm := btypes.MultiplePropertyData{ 39 | Objects: []btypes.Object{ 40 | btypes.Object{ 41 | ID: dev.ID, 42 | }, 43 | }, 44 | } 45 | 46 | for i := start; i <= end; i++ { 47 | rpm.Objects[0].Properties = append(rpm.Objects[0].Properties, btypes.Property{ 48 | Type: btypes.PropObjectList, 49 | ArrayIndex: uint32(i), 50 | }) 51 | } 52 | resp, err := c.ReadMultiProperty(dev, rpm) 53 | if err != nil { 54 | return nil, fmt.Errorf("unable to read multiple properties: %v", err) 55 | } 56 | if len(resp.Objects) == 0 { 57 | return nil, fmt.Errorf("no data was returned") 58 | } 59 | 60 | objs := make([]btypes.Object, len(resp.Objects[0].Properties)) 61 | 62 | for i, prop := range resp.Objects[0].Properties { 63 | id, ok := prop.Data.(btypes.ObjectID) 64 | if !ok { 65 | return nil, fmt.Errorf("expected type Object ID, got %T", prop.Data) 66 | } 67 | objs[i].ID = id 68 | } 69 | 70 | return objs, nil 71 | } 72 | 73 | const readPropRequestSize = 20 74 | 75 | func objectCopy(dest btypes.ObjectMap, src []btypes.Object) { 76 | for _, o := range src { 77 | if dest[o.ID.Type] == nil { 78 | dest[o.ID.Type] = make(map[btypes.ObjectInstance]btypes.Object) 79 | } 80 | dest[o.ID.Type][o.ID.Instance] = o 81 | } 82 | 83 | } 84 | 85 | func (c *client) objectList(dev *btypes.Device) error { 86 | dev.Objects = make(btypes.ObjectMap) 87 | 88 | l, err := c.objectListLen(*dev) 89 | if err != nil { 90 | return fmt.Errorf("unable to get list length: %v", err) 91 | } 92 | 93 | // Scan size is broken 94 | scanSize := int(dev.MaxApdu) / readPropRequestSize 95 | i := 0 96 | for i = 0; i < l/scanSize; i++ { 97 | start := i*scanSize + 1 98 | end := (i + 1) * scanSize 99 | 100 | objs, err := c.objectsRange(*dev, start, end) 101 | if err != nil { 102 | return fmt.Errorf("unable to retrieve objects between %d and %d: %v", start, end, err) 103 | } 104 | objectCopy(dev.Objects, objs) 105 | } 106 | start := i*scanSize + 1 107 | end := l 108 | if start <= end { 109 | objs, err := c.objectsRange(*dev, start, end) 110 | if err != nil { 111 | return fmt.Errorf("unable to retrieve objects between %d and %d: %v", start, end, err) 112 | } 113 | objectCopy(dev.Objects, objs) 114 | } 115 | return nil 116 | } 117 | 118 | func (c *client) objectInformation(dev *btypes.Device, objs []btypes.Object) error { 119 | // Often times the map will re-arrange the order it spits out, 120 | // so we need to keep track since the response will be in the 121 | // same order we issue the commands. 122 | keys := make([]btypes.ObjectID, len(objs)) 123 | counter := 0 124 | rpm := btypes.MultiplePropertyData{ 125 | Objects: []btypes.Object{}, 126 | } 127 | 128 | for _, o := range objs { 129 | if o.ID.Type > maxStandardBacnetType { 130 | continue 131 | } 132 | keys[counter] = o.ID 133 | counter++ 134 | rpm.Objects = append(rpm.Objects, btypes.Object{ 135 | ID: o.ID, 136 | Properties: []btypes.Property{ 137 | btypes.Property{ 138 | Type: btypes.PropObjectName, 139 | ArrayIndex: btypes.ArrayAll, 140 | }, 141 | btypes.Property{ 142 | Type: btypes.PropDescription, 143 | ArrayIndex: btypes.ArrayAll, 144 | }, 145 | }, 146 | }) 147 | 148 | } 149 | resp, err := c.ReadMultiProperty(*dev, rpm) 150 | if err != nil { 151 | return fmt.Errorf("unable to read multiple property :%v", err) 152 | } 153 | var name, description string 154 | var ok bool 155 | for i, r := range resp.Objects { 156 | name, ok = r.Properties[0].Data.(string) 157 | if !ok { 158 | return fmt.Errorf("expecting string got %T", r.Properties[0].Data) 159 | } 160 | description, ok = r.Properties[1].Data.(string) 161 | if !ok { 162 | return fmt.Errorf("expecting string got %T", r.Properties[1].Data) 163 | } 164 | obj := dev.Objects[keys[i].Type][keys[i].Instance] 165 | obj.Name = name 166 | obj.Description = description 167 | dev.Objects[keys[i].Type][keys[i].Instance] = obj 168 | } 169 | return nil 170 | } 171 | 172 | func (c *client) allObjectInformation(dev *btypes.Device) error { 173 | objs := dev.ObjectSlice() 174 | incrSize := 5 175 | 176 | var err error 177 | for i := 0; i < len(objs); i += incrSize { 178 | subset := objs[i:min(i+incrSize, len(objs))] 179 | err = c.objectInformation(dev, subset) 180 | if err != nil { 181 | return err 182 | } 183 | } 184 | 185 | return nil 186 | } 187 | 188 | // Objects retrieves all the objects within the given device and returns a 189 | // device with these objects. Along with the list of objects, it will also 190 | // gather additional information from the object such as the name and 191 | // description of the objects. The device returned contains all the name and 192 | // description fields for all objects 193 | func (c *client) Objects(dev btypes.Device) (btypes.Device, error) { 194 | err := c.objectList(&dev) 195 | if err != nil { 196 | return dev, fmt.Errorf("unable to get object list: %v", err) 197 | } 198 | err = c.allObjectInformation(&dev) 199 | if err != nil { 200 | return dev, fmt.Errorf("unable to get object's information: %v", err) 201 | } 202 | return dev, nil 203 | } 204 | -------------------------------------------------------------------------------- /readmulti.go: -------------------------------------------------------------------------------- 1 | package bacnet 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/NubeDev/bacnet/btypes" 7 | "time" 8 | 9 | "github.com/NubeDev/bacnet/encoding" 10 | ) 11 | 12 | const maxReattempt = 2 13 | 14 | // ReadMultiProperty uses the given device and read property request to read 15 | // from a device. Along with being able to read multiple properties from a 16 | // device, it can also read these properties from multiple objects. This is a 17 | // good feature to read all present values of every object in the device. This 18 | // is a batch operation compared to a ReadProperty and should be used in place 19 | // when reading more than two objects/properties. 20 | func (c *client) ReadMultiProperty(device btypes.Device, rp btypes.MultiplePropertyData) (btypes.MultiplePropertyData, error) { 21 | var out btypes.MultiplePropertyData 22 | 23 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 24 | defer cancel() 25 | id, err := c.tsm.ID(ctx) 26 | if err != nil { 27 | return out, fmt.Errorf("unable to get transaction id: %v", err) 28 | } 29 | defer c.tsm.Put(id) 30 | device.Addr.SetLength() 31 | err = device.CheckADPU() 32 | if err != nil { 33 | return btypes.MultiplePropertyData{}, err 34 | } 35 | 36 | npdu := &btypes.NPDU{ 37 | Version: btypes.ProtocolVersion, 38 | Destination: &device.Addr, 39 | Source: c.dataLink.GetMyAddress(), 40 | IsNetworkLayerMessage: false, 41 | ExpectingReply: true, 42 | Priority: btypes.Normal, 43 | HopCount: btypes.DefaultHopCount, 44 | } 45 | 46 | enc := encoding.NewEncoder() 47 | enc.NPDU(npdu) 48 | err = enc.ReadMultipleProperty(uint8(id), rp) 49 | if enc.Error() != nil || err != nil { 50 | return out, fmt.Errorf("encoding read multiple property failed: %v", err) 51 | } 52 | 53 | pack := enc.Bytes() 54 | if device.MaxApdu < uint32(len(pack)) { 55 | return out, fmt.Errorf("read multiple property is too large (max: %d given: %d)", device.MaxApdu, len(pack)) 56 | } 57 | // the value filled doesn't matter. it just needs to be non nil 58 | err = fmt.Errorf("go") 59 | 60 | for count := 0; err != nil && count < maxReattempt; count++ { 61 | out, err = c.sendReadMultipleProperty(id, device, npdu, pack) 62 | if err == nil { 63 | return out, nil 64 | } 65 | } 66 | return out, fmt.Errorf("failed %d tries: %v", maxReattempt, err) 67 | } 68 | 69 | func (c *client) sendReadMultipleProperty(id int, dev btypes.Device, npdu *btypes.NPDU, request []byte) (btypes.MultiplePropertyData, error) { 70 | var out btypes.MultiplePropertyData 71 | _, err := c.Send(dev.Addr, npdu, request, nil) 72 | if err != nil { 73 | return out, err 74 | } 75 | 76 | raw, err := c.tsm.Receive(id, time.Duration(5)*time.Second) 77 | if err != nil { 78 | return out, fmt.Errorf("unable to receive id %d: %v", id, err) 79 | } 80 | 81 | var b []byte 82 | switch v := raw.(type) { 83 | case error: 84 | return out, v 85 | case []byte: 86 | b = v 87 | default: 88 | return out, fmt.Errorf("received unknown datatype %T", raw) 89 | } 90 | 91 | dec := encoding.NewDecoder(b) 92 | 93 | var apdu btypes.APDU 94 | if err = dec.APDU(&apdu); err != nil { 95 | return out, err 96 | } 97 | if apdu.Error.Class != 0 || apdu.Error.Code != 0 { 98 | err = fmt.Errorf("received error, class: %d, code: %d", apdu.Error.Class, apdu.Error.Code) 99 | return out, err 100 | } 101 | err = dec.ReadMultiplePropertyAck(&out) 102 | if err != nil { 103 | c.log.Debugf("WEIRD PACKET: %v: %v", err, b) 104 | return out, err 105 | } 106 | return out, err 107 | } 108 | -------------------------------------------------------------------------------- /readprop.go: -------------------------------------------------------------------------------- 1 | package bacnet 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/NubeDev/bacnet/btypes" 7 | "github.com/NubeDev/bacnet/encoding" 8 | "log" 9 | "time" 10 | ) 11 | 12 | // ReadProperty reads a single property from a single object in the given device. 13 | func (c *client) ReadProperty(device btypes.Device, rp btypes.PropertyData) (btypes.PropertyData, error) { 14 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 15 | defer cancel() 16 | id, err := c.tsm.ID(ctx) 17 | if err != nil { 18 | return btypes.PropertyData{}, fmt.Errorf("unable to get an transaction id: %v", err) 19 | } 20 | defer c.tsm.Put(id) 21 | enc := encoding.NewEncoder() 22 | device.Addr.SetLength() 23 | npdu := &btypes.NPDU{ 24 | Version: btypes.ProtocolVersion, 25 | Destination: &device.Addr, 26 | Source: c.dataLink.GetMyAddress(), 27 | IsNetworkLayerMessage: false, 28 | ExpectingReply: true, 29 | Priority: btypes.Normal, 30 | HopCount: btypes.DefaultHopCount, 31 | } 32 | enc.NPDU(npdu) 33 | err = enc.ReadProperty(uint8(id), rp) 34 | if enc.Error() != nil || err != nil { 35 | return btypes.PropertyData{}, err 36 | } 37 | // the value filled doesn't matter. it just needs to be non nil 38 | err = fmt.Errorf("go") 39 | for count := 0; err != nil && count < retryCount; count++ { 40 | var b []byte 41 | var out btypes.PropertyData 42 | _, err = c.Send(device.Addr, npdu, enc.Bytes(), nil) 43 | if err != nil { 44 | log.Print(err) 45 | continue 46 | } 47 | 48 | var raw interface{} 49 | raw, err = c.tsm.Receive(id, time.Duration(5)*time.Second) 50 | if err != nil { 51 | continue 52 | } 53 | switch v := raw.(type) { 54 | case error: 55 | return out, v 56 | case []byte: 57 | b = v 58 | default: 59 | return out, fmt.Errorf("received unknown datatype %T", raw) 60 | } 61 | dec := encoding.NewDecoder(b) 62 | 63 | var apdu btypes.APDU 64 | if err = dec.APDU(&apdu); err != nil { 65 | continue 66 | } 67 | if apdu.Error.Class != 0 || apdu.Error.Code != 0 { 68 | err = fmt.Errorf("received error, class: %d, code: %d", apdu.Error.Class, apdu.Error.Code) 69 | continue 70 | } 71 | if err = dec.ReadProperty(&out); err != nil { 72 | continue 73 | } 74 | return out, dec.Error() 75 | } 76 | return btypes.PropertyData{}, err 77 | } 78 | -------------------------------------------------------------------------------- /tsm/transactions.go: -------------------------------------------------------------------------------- 1 | package tsm 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // MaxTransaction is the default max number of transactions that can occur 11 | // concurrently 12 | const MaxTransaction = 255 13 | const invalidID = 0 14 | 15 | const ( 16 | idle = iota 17 | ) 18 | 19 | type state struct { 20 | id int 21 | state int 22 | requestTimer int 23 | data chan interface{} 24 | } 25 | 26 | // TSM is the transaction state manager. It handles passing data to other 27 | // processes and keeping track of what transactions are currently processed 28 | type TSM struct { 29 | mutex sync.Mutex 30 | states map[int]*state 31 | pool sync.Pool 32 | free struct { 33 | id chan int 34 | space chan struct{} 35 | } 36 | } 37 | 38 | // New creates a new transaction manager 39 | func New(size int) *TSM { 40 | t := &TSM{ 41 | states: make(map[int]*state), pool: sync.Pool{ 42 | // Operation doesn't include a new channel. We want that done when a get is 43 | // done since we close all channels when putting into the pool. 44 | New: func() interface{} { 45 | s := new(state) 46 | return s 47 | }, 48 | }, 49 | } 50 | 51 | // Generate free ids. 52 | t.free.id = make(chan int, MaxTransaction) 53 | for i := invalidID + 1; i < MaxTransaction; i++ { 54 | t.free.id <- i 55 | } 56 | 57 | // Generate free space 58 | t.free.space = make(chan struct{}, size) 59 | for i := 0; i < size; i++ { 60 | t.free.space <- struct{}{} 61 | } 62 | 63 | return t 64 | } 65 | 66 | // Send data to invoked id 67 | func (t *TSM) Send(id int, b interface{}) error { 68 | t.mutex.Lock() 69 | s, ok := t.states[id] 70 | t.mutex.Unlock() 71 | 72 | if !ok { 73 | return fmt.Errorf("id %d is not receiving", id) 74 | } 75 | s.data <- b 76 | return nil 77 | } 78 | 79 | // Receive attempts to receive a byte array from the invoked id. If a time out 80 | // period has passed then an error is returned 81 | func (t *TSM) Receive(id int, timeout time.Duration) (interface{}, error) { 82 | t.mutex.Lock() 83 | s, ok := t.states[id] 84 | t.mutex.Unlock() 85 | 86 | if !ok { 87 | return nil, fmt.Errorf("id %d is not sending", id) 88 | } 89 | 90 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 91 | defer cancel() 92 | 93 | // Wait for data 94 | select { 95 | case b := <-s.data: 96 | return b, nil 97 | case <-ctx.Done(): 98 | return nil, fmt.Errorf("receive timed out (%v)", timeout) 99 | } 100 | 101 | } 102 | 103 | // ID returns the invoke id that was used to save the state of this connection. 104 | func (t *TSM) ID(ctx context.Context) (int, error) { 105 | var id int 106 | select { 107 | case <-t.free.space: 108 | // got a free spot, lets try and get a free id 109 | select { 110 | case id = <-t.free.id: 111 | case err := <-ctx.Done(): 112 | t.free.space <- struct{}{} 113 | return 0, fmt.Errorf("unable to get a free id: %v", err) 114 | } 115 | case err := <-ctx.Done(): 116 | return 0, fmt.Errorf("no free space: %v", err) 117 | } 118 | 119 | // skip error checking, since we control new generation and what is put in the pool. 120 | s := t.pool.Get().(*state) 121 | s.state = idle 122 | s.requestTimer = 0 // TODO: apdu_timeout 123 | s.data = make(chan interface{}) 124 | 125 | t.mutex.Lock() 126 | defer t.mutex.Unlock() 127 | t.states[id] = s 128 | return id, nil 129 | } 130 | 131 | // Put allows the id to be reused in the transaction manager 132 | func (t *TSM) Put(id int) error { 133 | t.mutex.Lock() 134 | defer t.mutex.Unlock() 135 | 136 | s, ok := t.states[id] 137 | if !ok { 138 | return fmt.Errorf("id %d does not exist in the transactions", id) 139 | } 140 | 141 | close(s.data) 142 | t.pool.Put(s) 143 | t.free.id <- id 144 | t.free.space <- struct{}{} 145 | delete(t.states, id) 146 | return nil 147 | } 148 | -------------------------------------------------------------------------------- /tsm/transactions_test.go: -------------------------------------------------------------------------------- 1 | package tsm 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestTSM(t *testing.T) { 10 | size := 3 11 | tsm := New(size) 12 | ctx := context.Background() 13 | var err error 14 | for i := 0; i < size-1; i++ { 15 | _, err = tsm.ID(ctx) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | } 20 | 21 | id, err := tsm.ID(ctx) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | // The buffer should be full at this point. 27 | ctx, cancel := context.WithTimeout(ctx, time.Millisecond) 28 | defer cancel() 29 | _, err = tsm.ID(ctx) 30 | if err == nil { 31 | t.Fatal("Buffer was full but an id was given ") 32 | } 33 | 34 | // Free an ID 35 | err = tsm.Put(id) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | // Now we should be able to get a new id since we free id 41 | _, err = tsm.ID(context.Background()) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | } 47 | 48 | func TestDataTransaction(t *testing.T) { 49 | size := 2 50 | tsm := New(size) 51 | ids := make([]int, size) 52 | var err error 53 | 54 | for i := 0; i < size; i++ { 55 | ids[i], err = tsm.ID(context.Background()) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | } 60 | 61 | go func() { 62 | err = tsm.Send(ids[0], "Hello First ID") 63 | if err != nil { 64 | t.Error(err) 65 | } 66 | }() 67 | 68 | go func() { 69 | err = tsm.Send(ids[1], "Hello Second ID") 70 | if err != nil { 71 | t.Error(err) 72 | } 73 | }() 74 | 75 | go func() { 76 | b, err := tsm.Receive(ids[0], time.Duration(5)*time.Second) 77 | if err != nil { 78 | t.Error(err) 79 | } 80 | s, ok := b.(string) 81 | if !ok { 82 | t.Errorf("type was not preseved") 83 | return 84 | } 85 | t.Log(s) 86 | }() 87 | 88 | b, err := tsm.Receive(ids[1], time.Duration(5)*time.Second) 89 | if err != nil { 90 | t.Error(err) 91 | } 92 | 93 | s, ok := b.(string) 94 | if !ok { 95 | t.Errorf("type was not preseved") 96 | return 97 | } 98 | t.Log(s) 99 | } 100 | -------------------------------------------------------------------------------- /utsm/doc.go: -------------------------------------------------------------------------------- 1 | package utsm 2 | 3 | /* 4 | utsm is the Unconfirmed Transaction State Manager. These types of 5 | transactions do not necessarily have a single destination but rather multiple 6 | destinations. Using this library, we set up a simple pub-sub model. 7 | */ 8 | -------------------------------------------------------------------------------- /utsm/main_test.go: -------------------------------------------------------------------------------- 1 | package utsm 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func sub(t *testing.T, m *Manager, start, end int) { 10 | b, err := m.Subscribe(start, end) 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | 15 | t.Logf("[%d, %d] %v", start, end, b) 16 | } 17 | 18 | func publisher(t *testing.T, m *Manager) { 19 | for i := 0; i < 5; i++ { 20 | go m.Publish(20, fmt.Sprintf("HI!%d", i)) 21 | time.Sleep(time.Duration(100) * time.Millisecond) 22 | } 23 | } 24 | func TestUTSM(t *testing.T) { 25 | opts := []ManagerOption{ 26 | DefaultSubscriberTimeout(time.Duration(2) * time.Second), 27 | DefaultSubscriberLastReceivedTimeout(time.Duration(300) * time.Millisecond), 28 | } 29 | m := NewManager(opts...) 30 | 31 | go publisher(t, m) 32 | go sub(t, m, 9, 20) 33 | go sub(t, m, 0, 2) 34 | sub(t, m, 10, 30) 35 | } 36 | -------------------------------------------------------------------------------- /utsm/subscriber.go: -------------------------------------------------------------------------------- 1 | package utsm 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type subscriber struct { 10 | // Start and End is the range that this object is subscribed to 11 | start int 12 | end int 13 | timeout time.Duration 14 | lastReceivedTimeout time.Duration 15 | lastReceived time.Time 16 | // Data channel is used for data transfer between subscriber and publisher 17 | data chan interface{} 18 | mutex *sync.Mutex 19 | } 20 | 21 | // SubscriberOption are options passed to a particular subscribe function 22 | type SubscriberOption func(s *subscriber) 23 | 24 | // Timeout is the overall timeout for subscribing. 25 | func (s *subscriber) Timeout(d time.Duration) SubscriberOption { 26 | return func(s *subscriber) { 27 | s.mutex.Lock() 28 | defer s.mutex.Unlock() 29 | s.timeout = d 30 | } 31 | } 32 | 33 | // LastReceivedTimeout is a timeout between the last time we have heard from a 34 | // publisher 35 | func (s *subscriber) LastReceivedTimeout(d time.Duration) SubscriberOption { 36 | return func(s *subscriber) { 37 | s.mutex.Lock() 38 | defer s.mutex.Unlock() 39 | s.lastReceivedTimeout = d 40 | } 41 | } 42 | 43 | // getTimeout returns the expiration time based on when we last received a message 44 | func (s *subscriber) getTimeout() time.Duration { 45 | s.mutex.Lock() 46 | // Deadline is x seconds after the last packet we received. 47 | timeout := s.lastReceived.Add(s.lastReceivedTimeout).Sub(time.Now()) 48 | s.mutex.Unlock() 49 | return timeout 50 | } 51 | 52 | // Subscribe receives data meant for ids that fall between the start and end range. 53 | func (m *Manager) Subscribe(start int, end int, options ...SubscriberOption) ([]interface{}, error) { 54 | var store []interface{} 55 | s := m.newSubscriber(start, end, options) 56 | defer m.removeSubscriber(s) 57 | 58 | ctx, cancel := context.WithTimeout(context.Background(), s.timeout) 59 | defer cancel() 60 | 61 | for { 62 | c, can := context.WithTimeout(ctx, s.getTimeout()) 63 | defer can() 64 | 65 | select { 66 | case <-c.Done(): 67 | return store, nil 68 | case b := <-s.data: 69 | store = append(store, b) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /utsm/utsm.go: -------------------------------------------------------------------------------- 1 | package utsm 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | const ( 9 | defaultOverallTimeout = time.Duration(10) * time.Second 10 | 11 | // defaultSubTimeout is how long in between publish packages to a subscriber 12 | // before we timeout waiting for additional data. 13 | defaultSubTimeout = time.Duration(1) * time.Second 14 | ) 15 | 16 | // Manager handles subscriptions and publications. Each manager is thread-safe 17 | type Manager struct { 18 | subs []*subscriber 19 | mutex *sync.Mutex 20 | subTimeout time.Duration 21 | subOverallTimeout time.Duration 22 | } 23 | 24 | // NewManager initializes a manager's internals. Do not allocate a struct of the 25 | // manager directly. 26 | func NewManager(options ...ManagerOption) *Manager { 27 | m := &Manager{ 28 | subTimeout: defaultSubTimeout, 29 | subOverallTimeout: defaultOverallTimeout, 30 | mutex: &sync.Mutex{}, 31 | } 32 | for _, op := range options { 33 | op(m) 34 | } 35 | return m 36 | } 37 | 38 | // ManagerOption are function passed to NewManager to configure the manager 39 | type ManagerOption func(m *Manager) 40 | 41 | // DefaultSubscriberTimeout option sets a a timeout period when we have not 42 | // received any packages to a subscriber for the timeout period 43 | func DefaultSubscriberTimeout(timeout time.Duration) ManagerOption { 44 | return func(m *Manager) { 45 | m.mutex.Lock() 46 | m.subOverallTimeout = timeout 47 | m.mutex.Unlock() 48 | } 49 | } 50 | 51 | // DefaultSubscriberLastReceivedTimeout option sets a a timeout period when we have not 52 | // received any packages to a subscriber for the timeout period 53 | func DefaultSubscriberLastReceivedTimeout(timeout time.Duration) ManagerOption { 54 | return func(m *Manager) { 55 | m.mutex.Lock() 56 | m.subTimeout = timeout 57 | m.mutex.Unlock() 58 | } 59 | } 60 | 61 | func (m *Manager) Publish(id int, data interface{}) { 62 | m.mutex.Lock() 63 | defer m.mutex.Unlock() 64 | 65 | for _, s := range m.subs { 66 | if id >= s.start && id <= s.end { 67 | s.mutex.Lock() 68 | s.lastReceived = time.Now() 69 | s.data <- data 70 | s.mutex.Unlock() 71 | } 72 | } 73 | } 74 | 75 | func (m *Manager) newSubscriber(start int, end int, options []SubscriberOption) *subscriber { 76 | s := &subscriber{ 77 | start: start, 78 | end: end, 79 | lastReceived: time.Now(), 80 | data: make(chan interface{}, 1), 81 | mutex: &sync.Mutex{}, 82 | } 83 | m.mutex.Lock() 84 | m.subs = append(m.subs, s) 85 | 86 | s.mutex.Lock() 87 | s.timeout = m.subOverallTimeout 88 | s.lastReceivedTimeout = m.subTimeout 89 | s.mutex.Unlock() 90 | 91 | m.mutex.Unlock() 92 | 93 | for _, opt := range options { 94 | opt(s) 95 | } 96 | 97 | return s 98 | } 99 | 100 | func (m *Manager) removeSubscriber(sub *subscriber) { 101 | m.mutex.Lock() 102 | defer m.mutex.Unlock() 103 | 104 | for i, s := range m.subs { 105 | if s == sub { 106 | // https://github.com/golang/go/wiki/SliceTricks 107 | // Prevents a memory leak that may occur when deleting 108 | 109 | // Shift 110 | copy(m.subs[i:], m.subs[i+1:]) 111 | 112 | // Set last value nil 113 | m.subs[len(m.subs)-1] = nil 114 | 115 | // Remove last value 116 | m.subs = m.subs[:len(m.subs)-1] 117 | return 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /whatnetwork.go: -------------------------------------------------------------------------------- 1 | package bacnet 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet/btypes" 6 | "github.com/NubeDev/bacnet/btypes/ndpu" 7 | "github.com/NubeDev/bacnet/encoding" 8 | ) 9 | 10 | /* 11 | Is in beta, works but needs a decoder 12 | 13 | in bacnet.Send() need to set the header.Function as btypes.BacFuncBroadcast 14 | 15 | in bacnet.handleMsg() the npdu.IsNetworkLayerMessage is always rejected so this needs to be updated 16 | 17 | */ 18 | 19 | func (c *client) WhatIsNetworkNumber() (resp []*btypes.Address) { 20 | var err error 21 | dest := *c.dataLink.GetBroadcastAddress() 22 | enc := encoding.NewEncoder() 23 | npdu := &btypes.NPDU{ 24 | Version: btypes.ProtocolVersion, 25 | Destination: &dest, 26 | Source: c.dataLink.GetMyAddress(), 27 | IsNetworkLayerMessage: true, 28 | NetworkLayerMessageType: ndpu.WhatIsNetworkNumber, 29 | // We are not expecting a direct reply from a single destination 30 | ExpectingReply: false, 31 | Priority: btypes.Normal, 32 | HopCount: btypes.DefaultHopCount, 33 | } 34 | enc.NPDU(npdu) 35 | // Run in parallel 36 | errChan := make(chan error) 37 | broadcast := &SetBroadcastType{Set: true, BacFunc: btypes.BacFuncBroadcast} 38 | go func() { 39 | _, err = c.Send(dest, npdu, enc.Bytes(), broadcast) 40 | errChan <- err 41 | }() 42 | values, err := c.utsm.Subscribe(1, 65534) //65534 is the max number a network can be 43 | if err != nil { 44 | fmt.Println(`err`, err) 45 | } 46 | err = <-errChan 47 | if err != nil { 48 | 49 | } 50 | 51 | for _, v := range values { 52 | r, ok := v.(btypes.NPDU) 53 | if r.Source != nil { 54 | resp = append(resp, r.Source) 55 | } 56 | if !ok { 57 | continue 58 | } 59 | } 60 | return resp 61 | 62 | } 63 | -------------------------------------------------------------------------------- /whois.go: -------------------------------------------------------------------------------- 1 | package bacnet 2 | 3 | import "C" 4 | import ( 5 | "github.com/NubeDev/bacnet/btypes" 6 | "github.com/NubeDev/bacnet/encoding" 7 | ) 8 | 9 | type WhoIsOpts struct { 10 | Low int `json:"low"` 11 | High int `json:"high"` 12 | GlobalBroadcast bool `json:"global_broadcast"` 13 | NetworkNumber uint16 `json:"network_number"` 14 | } 15 | 16 | // WhoIs finds all devices with ids between the provided low and high values. 17 | // Use constant ArrayAll for both fields to scan the entire network at once. 18 | // Using ArrayAll is highly discouraged for most networks since it can lead 19 | // to a high congested network. 20 | func (c *client) WhoIs(wh *WhoIsOpts) ([]btypes.Device, error) { 21 | dest := *c.dataLink.GetBroadcastAddress() 22 | enc := encoding.NewEncoder() 23 | low := wh.Low 24 | high := wh.High 25 | if wh.GlobalBroadcast { 26 | wh.NetworkNumber = btypes.GlobalBroadcast //65535 27 | } 28 | if low <= 0 { 29 | low = 0 30 | } 31 | if high <= 0 { 32 | high = 4194304 //max dev id 33 | } 34 | 35 | dest.Net = wh.NetworkNumber 36 | npdu := &btypes.NPDU{ 37 | Version: btypes.ProtocolVersion, 38 | Destination: &dest, 39 | Source: c.dataLink.GetMyAddress(), 40 | IsNetworkLayerMessage: false, 41 | // We are not expecting a direct reply from a single destination 42 | ExpectingReply: false, 43 | Priority: btypes.Normal, 44 | HopCount: btypes.DefaultHopCount, 45 | } 46 | enc.NPDU(npdu) 47 | err := enc.WhoIs(int32(low), int32(high)) 48 | if err != nil { 49 | return nil, err 50 | } 51 | // Subscribe to any changes in the range. If it is a broadcast, 52 | var start, end int 53 | if low == -1 || high == -1 { 54 | start = 0 55 | end = maxInt 56 | } else { 57 | start = low 58 | end = high 59 | } 60 | // Run in parallel 61 | errChan := make(chan error) 62 | go func() { 63 | _, err = c.Send(dest, npdu, enc.Bytes(), nil) 64 | errChan <- err 65 | }() 66 | values, err := c.utsm.Subscribe(start, end) 67 | if err != nil { 68 | return nil, err 69 | } 70 | err = <-errChan 71 | if err != nil { 72 | return nil, err 73 | } 74 | // Weed out values that are not important such as non object type 75 | // and that are not 76 | uniqueMap := make(map[btypes.ObjectInstance]btypes.Device) 77 | uniqueList := make([]btypes.Device, len(uniqueMap)) 78 | 79 | for _, v := range values { 80 | r, ok := v.(btypes.IAm) 81 | // Skip non I AM responses 82 | if !ok { 83 | continue 84 | } 85 | // Check to see if we are in the map before inserting 86 | macMSTP := 0 87 | if len(r.Addr.Adr) > 0 { 88 | macMSTP = int(r.Addr.Adr[0]) 89 | } 90 | networkNumber := 0 91 | if r.Addr.Net > 0 { 92 | networkNumber = int(r.Addr.Net) 93 | } 94 | if _, ok := uniqueMap[r.ID.Instance]; !ok { 95 | dev := btypes.Device{ 96 | DeviceID: int(r.ID.Instance), 97 | Addr: r.Addr, 98 | ID: r.ID, 99 | MaxApdu: r.MaxApdu, 100 | Segmentation: r.Segmentation, 101 | Vendor: r.Vendor, 102 | MacMSTP: macMSTP, 103 | NetworkNumber: networkNumber, 104 | } 105 | ip, err := r.Addr.UDPAddr() 106 | if err == nil { 107 | dev.Ip = ip.IP.String() 108 | dev.Port = ip.Port 109 | } 110 | uniqueMap[r.ID.Instance] = dev 111 | uniqueList = append(uniqueList, dev) 112 | } 113 | } 114 | return uniqueList, err 115 | } 116 | -------------------------------------------------------------------------------- /whoisrouter.go: -------------------------------------------------------------------------------- 1 | package bacnet 2 | 3 | import ( 4 | "fmt" 5 | "github.com/NubeDev/bacnet/btypes" 6 | "github.com/NubeDev/bacnet/btypes/ndpu" 7 | "github.com/NubeDev/bacnet/encoding" 8 | ) 9 | 10 | /* 11 | Is in beta 12 | */ 13 | 14 | func (c *client) WhoIsRouterToNetwork() (resp *[]btypes.Address) { 15 | var err error 16 | dest := *c.dataLink.GetBroadcastAddress() 17 | enc := encoding.NewEncoder() 18 | npdu := &btypes.NPDU{ 19 | Version: btypes.ProtocolVersion, 20 | Destination: &dest, 21 | Source: c.dataLink.GetMyAddress(), 22 | IsNetworkLayerMessage: true, 23 | NetworkLayerMessageType: ndpu.WhoIsRouterToNetwork, 24 | // We are not expecting a direct reply from a single destination 25 | ExpectingReply: false, 26 | Priority: btypes.Normal, 27 | HopCount: btypes.DefaultHopCount, 28 | } 29 | enc.NPDU(npdu) 30 | // Run in parallel 31 | errChan := make(chan error) 32 | broadcast := &SetBroadcastType{Set: true, BacFunc: btypes.BacFuncBroadcast} 33 | go func() { 34 | _, err = c.Send(dest, npdu, enc.Bytes(), broadcast) 35 | errChan <- err 36 | }() 37 | values, err := c.utsm.Subscribe(1, 65534) //65534 is the max number a network can be 38 | if err != nil { 39 | fmt.Println(`err`, err) 40 | } 41 | err = <-errChan 42 | if err != nil { 43 | 44 | } 45 | var list []btypes.Address 46 | for _, addresses := range values { 47 | r, ok := addresses.([]btypes.Address) 48 | if !ok { 49 | continue 50 | } 51 | for _, addr := range r { 52 | list = append(list, addr) 53 | } 54 | } 55 | return &list 56 | 57 | } 58 | -------------------------------------------------------------------------------- /writemulti.go: -------------------------------------------------------------------------------- 1 | package bacnet 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/NubeDev/bacnet/btypes" 9 | "github.com/NubeDev/bacnet/encoding" 10 | ) 11 | 12 | func (c *client) WriteMultiProperty(dev btypes.Device, wp btypes.MultiplePropertyData) error { 13 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 14 | defer cancel() 15 | id, err := c.tsm.ID(ctx) 16 | if err != nil { 17 | return fmt.Errorf("unable to get an transaction id: %v", err) 18 | } 19 | defer c.tsm.Put(id) 20 | 21 | npdu := &btypes.NPDU{ 22 | Version: btypes.ProtocolVersion, 23 | Destination: &dev.Addr, 24 | Source: c.dataLink.GetMyAddress(), 25 | IsNetworkLayerMessage: false, 26 | ExpectingReply: true, 27 | Priority: btypes.Normal, 28 | HopCount: btypes.DefaultHopCount, 29 | } 30 | enc := encoding.NewEncoder() 31 | enc.NPDU(npdu) 32 | 33 | enc.WriteMultiProperty(uint8(id), wp) 34 | if enc.Error() != nil { 35 | return enc.Error() 36 | } 37 | 38 | pack := enc.Bytes() 39 | if dev.MaxApdu < uint32(len(pack)) { 40 | return fmt.Errorf("read multiple property is too large (max: %d given: %d)", dev.MaxApdu, len(pack)) 41 | } 42 | 43 | // the value filled doesn't matter. it just needs to be non nil 44 | err = fmt.Errorf("go") 45 | 46 | for count := 0; err != nil && count < maxReattempt; count++ { 47 | err = c.sendWriteMultipleProperty(id, dev, npdu, pack) 48 | if err == nil { 49 | return nil 50 | } 51 | } 52 | return fmt.Errorf("failed %d tries: %v", maxReattempt, err) 53 | } 54 | 55 | func (c *client) sendWriteMultipleProperty(id int, dev btypes.Device, npdu *btypes.NPDU, request []byte) error { 56 | _, err := c.Send(dev.Addr, npdu, request, nil) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | raw, err := c.tsm.Receive(id, time.Duration(5)*time.Second) 62 | if err != nil { 63 | return fmt.Errorf("unable to receive id %d: %v", id, err) 64 | } 65 | 66 | var b []byte 67 | switch v := raw.(type) { 68 | case error: 69 | return v 70 | case []byte: 71 | b = v 72 | default: 73 | return fmt.Errorf("received unknown datatype %T", raw) 74 | } 75 | 76 | dec := encoding.NewDecoder(b) 77 | 78 | var apdu btypes.APDU 79 | if err = dec.APDU(&apdu); err != nil { 80 | return err 81 | } 82 | if apdu.Error.Class != 0 || apdu.Error.Code != 0 { 83 | return fmt.Errorf("received error, class: %d, code: %d", apdu.Error.Class, apdu.Error.Code) 84 | } 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /writeprop.go: -------------------------------------------------------------------------------- 1 | package bacnet 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/NubeDev/bacnet/btypes" 7 | "github.com/NubeDev/bacnet/encoding" 8 | "time" 9 | ) 10 | 11 | func (c *client) WriteProperty(device btypes.Device, wp btypes.PropertyData) error { 12 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 13 | defer cancel() 14 | id, err := c.tsm.ID(ctx) 15 | if err != nil { 16 | return fmt.Errorf("unable to get an transaction id: %v", err) 17 | } 18 | defer c.tsm.Put(id) 19 | device.Addr.SetLength() 20 | npdu := &btypes.NPDU{ 21 | Version: btypes.ProtocolVersion, 22 | Destination: &device.Addr, 23 | Source: c.dataLink.GetMyAddress(), 24 | IsNetworkLayerMessage: false, 25 | ExpectingReply: true, 26 | Priority: btypes.Normal, 27 | HopCount: btypes.DefaultHopCount, 28 | } 29 | enc := encoding.NewEncoder() 30 | enc.NPDU(npdu) 31 | enc.WriteProperty(uint8(id), wp) 32 | if enc.Error() != nil { 33 | return enc.Error() 34 | } 35 | // the value filled doesn't matter. it just needs to be non nil 36 | err = fmt.Errorf("go") 37 | for count := 0; err != nil && count < 2; count++ { 38 | var b []byte 39 | var raw interface{} 40 | _, err = c.Send(device.Addr, npdu, enc.Bytes(), nil) 41 | if err != nil { 42 | continue 43 | } 44 | raw, err = c.tsm.Receive(id, time.Duration(5)*time.Second) 45 | if err != nil { 46 | continue 47 | } 48 | switch v := raw.(type) { 49 | case error: 50 | if err == nil { 51 | err = raw.(error) 52 | } 53 | return err 54 | case []byte: 55 | b = v 56 | default: 57 | return fmt.Errorf("received unknown datatype %T", raw) 58 | } 59 | 60 | dec := encoding.NewDecoder(b) 61 | var apdu btypes.APDU 62 | if err = dec.APDU(&apdu); err != nil { 63 | continue 64 | } 65 | if apdu.Error.Class != 0 || apdu.Error.Code != 0 { 66 | err = fmt.Errorf("received error, class: %d, code: %d", apdu.Error.Class, apdu.Error.Code) 67 | continue 68 | } 69 | 70 | return err 71 | } 72 | return err 73 | } 74 | --------------------------------------------------------------------------------