knx 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: da3886255a22b8e72fe4843c9d572b8f55d0f24f
4
+ data.tar.gz: f13f7c99aa45592be334abe8d5fda818ed4a03a5
5
+ SHA512:
6
+ metadata.gz: 1edd50db73644cc37edb56cdf2ac87375f39af1945df6ad957eb5dea19976a5c4d1545297fc754b51d66bc5e5a8c19544a4fe35645d1565e9b203a95e30df39f
7
+ data.tar.gz: 9146bc461808c8f06c56a5b3e256052ff95cb0ef0bb62d7eeac31e1b76e188f6f1ea560962a5a9a4efacab2b9ac4543ddcdc8bf6438c833894941cb0cc5e2ad7
data/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # Ruby KNX
2
+
3
+ Constructs [KNX standard](https://en.wikipedia.org/wiki/KNX_(standard)) datagrams that make it easy to communicate with devices on KNX networks.
4
+ It does not implement the transport layer so you can use it with naitive ruby, eventmachine, celluloid or the like.
5
+
6
+ [![Build Status](https://travis-ci.org/acaprojects/ruby-knx.svg?branch=master)](https://travis-ci.org/acaprojects/ruby-knx)
7
+
8
+ You'll need a gateway. I recommend one that supports TCP/IP such as [MDT Interfaces](http://www.mdt.de/EN_Interfaces.html) however you can use multicast groups if your network is configured to allow this.
9
+
10
+
11
+ ## Install the gem
12
+
13
+ Install it with [RubyGems](https://rubygems.org/)
14
+
15
+ gem install knx
16
+
17
+ or add this to your Gemfile if you use [Bundler](http://gembundler.com/):
18
+
19
+ gem 'knx'
20
+
21
+
22
+
23
+ ## Usage
24
+
25
+ ```ruby
26
+ require 'knx'
27
+
28
+ knx = KNX.new
29
+ datagram = knx.read(byte_string)
30
+
31
+ datagram.source_address.to_s
32
+ # => '2.3.4'
33
+
34
+ datagram.destination_address.to_s
35
+ # => '3/4/5'
36
+
37
+ datagram.data # Returns a byte array
38
+ # => [1]
39
+
40
+ # ...
41
+
42
+ request = knx.action('1/2/0', true)
43
+ byte_string = request.to_binary_s
44
+
45
+ request = knx.action('1/2/3', 150)
46
+ byte_string = request.to_binary_s
47
+
48
+ # Send byte_string to KNX network to execute the request
49
+ # Supports multicast, unicast and TCP/IP tunneling (when supported)
50
+
51
+ ```
52
+
53
+ We also support [KNX BAOS devices](http://www.weinzierl.de/index.php/en/all-knx/knx-devices-en) devices:
54
+
55
+
56
+ ```ruby
57
+ require 'knx/object_server'
58
+
59
+ os = KNX::ObjectServer.new
60
+ datagram = os.read(byte_string)
61
+
62
+ # Can return multiple values
63
+ datagram.data.length #=> 1
64
+
65
+ # Get the item index we are reading
66
+ datagram.data[0].id
67
+ # => 12
68
+
69
+ datagram.data[0].value # Returns a binary string
70
+ # => "\x01"
71
+
72
+ # ...
73
+
74
+ request = os.action(1, true)
75
+ byte_string = request.to_binary_s
76
+
77
+ # Send byte_string to KNX BAOS server to execute the request
78
+ # This protocol was designed to be sent over TCP/IP
79
+
80
+ ```
81
+
82
+
83
+ ## License and copyright
84
+
85
+ MIT
data/knx.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "knx"
5
+ s.version = '1.0.0'
6
+ s.authors = ["Stephen von Takach"]
7
+ s.email = ["steve@cotag.me"]
8
+ s.licenses = ["MIT"]
9
+ s.homepage = "https://github.com/acaprojects/ruby-knx"
10
+ s.summary = "KNX protocol on Ruby"
11
+ s.description = <<-EOF
12
+ Constructs KNX standard datagrams that make it easy to communicate with devices on KNX networks
13
+ EOF
14
+
15
+
16
+ s.add_dependency 'bindata', '~> 2.3'
17
+
18
+ s.add_development_dependency 'rspec', '~> 3.5'
19
+ s.add_development_dependency 'yard', '~> 0'
20
+ s.add_development_dependency 'rake', '~> 11'
21
+
22
+
23
+ s.files = Dir["{lib}/**/*"] + %w(knx.gemspec README.md)
24
+ s.test_files = Dir["spec/**/*"]
25
+ s.extra_rdoc_files = ["README.md"]
26
+
27
+ s.require_paths = ["lib"]
28
+ end
@@ -0,0 +1,150 @@
1
+ #encoding: ASCII-8BIT
2
+
3
+ class KNX
4
+ # ------------------------
5
+ # Address Processing
6
+ # ------------------------
7
+ # +-----------------------------------------------+
8
+ # 16 bits | INDIVIDUAL ADDRESS |
9
+ # +-----------------------+-----------------------+
10
+ # | OCTET 0 (high byte) | OCTET 1 (low byte) |
11
+ # +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
12
+ # bits | 7| 6| 5| 4| 3| 2| 1| 0| 7| 6| 5| 4| 3| 2| 1| 0|
13
+ # +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
14
+ # | Subnetwork Address | |
15
+ # +-----------+-----------+ Device Address |
16
+ # |(Area Adrs)|(Line Adrs)| |
17
+ # +-----------------------+-----------------------+
18
+
19
+ # +-----------------------------------------------+
20
+ # 16 bits | GROUP ADDRESS (3 level) |
21
+ # +-----------------------+-----------------------+
22
+ # | OCTET 0 (high byte) | OCTET 1 (low byte) |
23
+ # +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
24
+ # bits | 7| 6| 5| 4| 3| 2| 1| 0| 7| 6| 5| 4| 3| 2| 1| 0|
25
+ # +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
26
+ # | | Main Grp | Midd G | Sub Group |
27
+ # +--+--------------------+-----------------------+
28
+
29
+ # +-----------------------------------------------+
30
+ # 16 bits | GROUP ADDRESS (2 level) |
31
+ # +-----------------------+-----------------------+
32
+ # | OCTET 0 (high byte) | OCTET 1 (low byte) |
33
+ # +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
34
+ # bits | 7| 6| 5| 4| 3| 2| 1| 0| 7| 6| 5| 4| 3| 2| 1| 0|
35
+ # +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
36
+ # | | Main Grp | Sub Group |
37
+ # +--+--------------------+-----------------------+
38
+ module Address
39
+ module ClassMethods
40
+ def parse(input)
41
+ address = @address_class.new
42
+ klass = input.class
43
+
44
+ if klass == Array
45
+ address.read(input.pack('n'))
46
+ elsif [Integer, Fixnum].include? klass
47
+ address.read([input].pack('n'))
48
+ elsif klass == String
49
+ tmp = parse_friendly(input)
50
+ if tmp.nil?
51
+ address.read(input)
52
+ else
53
+ address = tmp
54
+ end
55
+ else
56
+ raise 'address parsing failed'
57
+ end
58
+
59
+ address
60
+ end
61
+ end
62
+
63
+ def self.included(base)
64
+ base.instance_variable_set(:@address_class, base)
65
+ base.extend(ClassMethods)
66
+ end
67
+
68
+ def to_i
69
+ # 16-bit unsigned, network (big-endian)
70
+ to_binary_s.unpack('n')[0]
71
+ end
72
+
73
+ def is_group?; true; end
74
+ end
75
+
76
+ class GroupAddress < ::BinData::Record
77
+ include Address
78
+ endian :big
79
+
80
+ bit1 :_reserved_, value: 0
81
+ bit4 :main_group
82
+ bit3 :middle_group
83
+ uint8 :sub_group
84
+
85
+
86
+ def to_s
87
+ "#{main_group}/#{middle_group}/#{sub_group}"
88
+ end
89
+
90
+ def self.parse_friendly(str)
91
+ result = str.split('/')
92
+ if result.length == 3
93
+ address = GroupAddress.new
94
+ address.main_group = result[0].to_i
95
+ address.middle_group = result[1].to_i
96
+ address.sub_group = result[2].to_i
97
+ address
98
+ end
99
+ end
100
+ end
101
+
102
+ class GroupAddress2Level < ::BinData::Record
103
+ include Address
104
+ endian :big
105
+
106
+ bit1 :_reserved_, value: 0
107
+ bit4 :main_group
108
+ bit11 :sub_group
109
+
110
+
111
+ def to_s
112
+ "#{main_group}/#{sub_group}"
113
+ end
114
+
115
+ def self.parse_friendly(str)
116
+ result = str.split('/')
117
+ if result.length == 2
118
+ address = GroupAddress2Level.new
119
+ address.main_group = result[0].to_i
120
+ address.sub_group = result[1].to_i
121
+ address
122
+ end
123
+ end
124
+ end
125
+
126
+ class IndividualAddress < ::BinData::Record
127
+ include Address
128
+ endian :big
129
+
130
+ bit4 :area_address
131
+ bit4 :line_address
132
+ uint8 :device_address
133
+
134
+ def to_s
135
+ "#{area_address}.#{line_address}.#{device_address}"
136
+ end
137
+
138
+ def is_group?; false; end
139
+
140
+ def self.parse_friendly(str)
141
+ result = str.split('.')
142
+ if result.length == 3
143
+ address = IndividualAddress.new
144
+ address.area_address = result[0].to_i
145
+ address.line_address = result[1].to_i
146
+ address.device_address = result[2].to_i
147
+ end
148
+ end
149
+ end
150
+ end
data/lib/knx/cemi.rb ADDED
@@ -0,0 +1,220 @@
1
+ #encoding: ASCII-8BIT
2
+
3
+ class KNX
4
+ # APCI type
5
+ ActionType = {
6
+ group_read: 0,
7
+ group_resp: 1,
8
+ group_write: 2,
9
+
10
+ individual_write: 3,
11
+ individual_read: 4,
12
+ individual_resp: 5,
13
+
14
+ adc_read: 6,
15
+ adc_resp: 7,
16
+
17
+ memory_read: 8,
18
+ memory_resp: 9,
19
+ memory_write: 10,
20
+
21
+ user_msg: 11,
22
+
23
+ descriptor_read: 12,
24
+ descriptor_resp: 13,
25
+
26
+ restart: 14,
27
+ escape: 15
28
+ }
29
+
30
+ TpciType = {
31
+ unnumbered_data: 0b00,
32
+ numbered_data: 0b01,
33
+ unnumbered_control: 0b10,
34
+ numbered_control: 0b11
35
+ }
36
+
37
+ MsgCode = {
38
+ send_datagram: 0x29
39
+ }
40
+
41
+ Priority = {
42
+ system: 0,
43
+ alarm: 1,
44
+ high: 2,
45
+ low: 3
46
+ }
47
+
48
+
49
+ # CEMI == Common External Message Interface
50
+ # +--------+--------+--------+--------+----------------+----------------+--------+----------------+
51
+ # | Msg |Add.Info| Ctrl 1 | Ctrl 2 | Source Address | Dest. Address | Data | APDU |
52
+ # | Code | Length | | | | | Length | |
53
+ # +--------+--------+--------+--------+----------------+----------------+--------+----------------+
54
+ # 1 byte 1 byte 1 byte 1 byte 2 bytes 2 bytes 1 byte 2 bytes
55
+ #
56
+ # Message Code = 0x11 - a L_Data.req primitive
57
+ # COMMON EMI MESSAGE CODES FOR DATA LINK LAYER PRIMITIVES
58
+ # FROM NETWORK LAYER TO DATA LINK LAYER
59
+ # +---------------------------+--------------+-------------------------+---------------------+------------------+
60
+ # | Data Link Layer Primitive | Message Code | Data Link Layer Service | Service Description | Common EMI Frame |
61
+ # +---------------------------+--------------+-------------------------+---------------------+------------------+
62
+ # | L_Raw.req | 0x10 | | | |
63
+ # +---------------------------+--------------+-------------------------+---------------------+------------------+
64
+ # | | | | Primitive used for | Sample Common |
65
+ # | L_Data.req | 0x11 | Data Service | transmitting a data | EMI frame |
66
+ # | | | | frame | |
67
+ # +---------------------------+--------------+-------------------------+---------------------+------------------+
68
+ # | L_Poll_Data.req | 0x13 | Poll Data Service | | |
69
+ # +---------------------------+--------------+-------------------------+---------------------+------------------+
70
+ # | L_Raw.req | 0x10 | | | |
71
+ # +---------------------------+--------------+-------------------------+---------------------+------------------+
72
+ # FROM DATA LINK LAYER TO NETWORK LAYER
73
+ # +---------------------------+--------------+-------------------------+---------------------+
74
+ # | Data Link Layer Primitive | Message Code | Data Link Layer Service | Service Description |
75
+ # +---------------------------+--------------+-------------------------+---------------------+
76
+ # | L_Poll_Data.con | 0x25 | Poll Data Service | |
77
+ # +---------------------------+--------------+-------------------------+---------------------+
78
+ # | | | | Primitive used for |
79
+ # | L_Data.ind | 0x29 | Data Service | receiving a data |
80
+ # | | | | frame |
81
+ # +---------------------------+--------------+-------------------------+---------------------+
82
+ # | L_Busmon.ind | 0x2B | Bus Monitor Service | |
83
+ # +---------------------------+--------------+-------------------------+---------------------+
84
+ # | L_Raw.ind | 0x2D | | |
85
+ # +---------------------------+--------------+-------------------------+---------------------+
86
+ # | | | | Primitive used for |
87
+ # | | | | local confirmation |
88
+ # | L_Data.con | 0x2E | Data Service | that a frame was |
89
+ # | | | | sent (does not mean |
90
+ # | | | | successful receive) |
91
+ # +---------------------------+--------------+-------------------------+---------------------+
92
+ # | L_Raw.con | 0x2F | | |
93
+ # +---------------------------+--------------+-------------------------+---------------------+
94
+
95
+ # Add.Info Length = 0x00 - no additional info
96
+ # Control Field 1 = see the bit structure above
97
+ # Control Field 2 = see the bit structure above
98
+ # Source Address = 0x0000 - filled in by router/gateway with its source address which is
99
+ # part of the KNX subnet
100
+ # Dest. Address = KNX group or individual address (2 byte)
101
+ # Data Length = Number of bytes of data in the APDU excluding the TPCI/APCI bits
102
+ # APDU = Application Protocol Data Unit - the actual payload including transport
103
+ # protocol control information (TPCI), application protocol control
104
+ # information (APCI) and data passed as an argument from higher layers of
105
+ # the KNX communication stack
106
+ #
107
+ class CEMI < BinData::Record
108
+ endian :big
109
+
110
+ uint8 :msg_code
111
+ uint8 :info_length
112
+
113
+
114
+ # ---------------------
115
+ # Control Fields
116
+ # ---------------------
117
+
118
+ # Bit order
119
+ # +---+---+---+---+---+---+---+---+
120
+ # | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
121
+ # +---+---+---+---+---+---+---+---+
122
+
123
+ # Control Field 1
124
+
125
+ # Bit |
126
+ # ------+---------------------------------------------------------------
127
+ # 7 | Frame Type - 0x0 for extended frame
128
+ # | 0x1 for standard frame
129
+ # ------+---------------------------------------------------------------
130
+ # 6 | Reserved
131
+ # |
132
+ # ------+---------------------------------------------------------------
133
+ # 5 | Repeat Flag - 0x0 repeat frame on medium in case of an error
134
+ # | 0x1 do not repeat
135
+ # ------+---------------------------------------------------------------
136
+ # 4 | System Broadcast - 0x0 system broadcast
137
+ # | 0x1 broadcast
138
+ # ------+---------------------------------------------------------------
139
+ # 3 | Priority - 0x0 system
140
+ # | 0x1 normal (also called alarm priority)
141
+ # ------+ 0x2 urgent (also called high priority)
142
+ # 2 | 0x3 low
143
+ # |
144
+ # ------+---------------------------------------------------------------
145
+ # 1 | Acknowledge Request - 0x0 no ACK requested
146
+ # | (L_Data.req) 0x1 ACK requested
147
+ # ------+---------------------------------------------------------------
148
+ # 0 | Confirm - 0x0 no error
149
+ # | (L_Data.con) - 0x1 error
150
+ # ------+---------------------------------------------------------------
151
+ bit1 :is_standard_frame
152
+ bit1 :_reserved_, value: 0
153
+ bit1 :no_repeat
154
+ bit1 :broadcast
155
+ bit2 :priority # 2 bits
156
+ bit1 :ack_requested
157
+ bit1 :is_error
158
+
159
+ # Control Field 2
160
+
161
+ # Bit |
162
+ # ------+---------------------------------------------------------------
163
+ # 7 | Destination Address Type - 0x0 individual address
164
+ # | - 0x1 group address
165
+ # ------+---------------------------------------------------------------
166
+ # 6-4 | Hop Count (0-7)
167
+ # ------+---------------------------------------------------------------
168
+ # 3-0 | Extended Frame Format - 0x0 standard frame
169
+ # ------+---------------------------------------------------------------
170
+ bit1 :is_group_address
171
+ bit3 :hop_count
172
+ bit4 :extended_frame_format
173
+
174
+ uint16 :source_address
175
+ uint16 :destination_address
176
+
177
+ uint8 :data_length
178
+
179
+
180
+ # In the Common EMI frame, the APDU payload is defined as follows:
181
+
182
+ # +--------+--------+--------+--------+--------+
183
+ # | TPCI + | APCI + | Data | Data | Data |
184
+ # | APCI | Data | | | |
185
+ # +--------+--------+--------+--------+--------+
186
+ # byte 1 byte 2 byte 3 ... byte 16
187
+
188
+ # For data that is 6 bits or less in length, only the first two bytes are used in a Common EMI
189
+ # frame. Common EMI frame also carries the information of the expected length of the Protocol
190
+ # Data Unit (PDU). Data payload can be at most 14 bytes long. <p>
191
+
192
+ # The first byte is a combination of transport layer control information (TPCI) and application
193
+ # layer control information (APCI). First 6 bits are dedicated for TPCI while the two least
194
+ # significant bits of first byte hold the two most significant bits of APCI field, as follows:
195
+
196
+ # Bit 1 Bit 2 Bit 3 Bit 4 Bit 5 Bit 6 Bit 7 Bit 8 Bit 1 Bit 2
197
+ # +--------+--------+--------+--------+--------+--------+--------+--------++--------+----....
198
+ # | | | | | | | | || |
199
+ # | TPCI | TPCI | TPCI | TPCI | TPCI | TPCI | APCI | APCI || APCI |
200
+ # | | | | | | |(bit 1) |(bit 2) ||(bit 3) |
201
+ # +--------+--------+--------+--------+--------+--------+--------+--------++--------+----....
202
+ # + B Y T E 1 || B Y T E 2
203
+ # +-----------------------------------------------------------------------++-------------....
204
+
205
+ # Total number of APCI control bits can be either 4 or 10. The second byte bit structure is as follows:
206
+
207
+ # Bit 1 Bit 2 Bit 3 Bit 4 Bit 5 Bit 6 Bit 7 Bit 8 Bit 1 Bit 2
208
+ # +--------+--------+--------+--------+--------+--------+--------+--------++--------+----....
209
+ # | | | | | | | | || |
210
+ # | APCI | APCI | APCI/ | APCI/ | APCI/ | APCI/ | APCI/ | APCI/ || Data | Data
211
+ # |(bit 3) |(bit 4) | Data | Data | Data | Data | Data | Data || |
212
+ # +--------+--------+--------+--------+--------+--------+--------+--------++--------+----....
213
+ # + B Y T E 2 || B Y T E 3
214
+ # +-----------------------------------------------------------------------++-------------....
215
+ bit2 :tpci # transport protocol control information
216
+ bit4 :tpci_seq_num # Sequence number when tpci is sequenced
217
+ bit4 :apci # application protocol control information (What we trying to do: Read, write, respond etc)
218
+ bit6 :data # Or the tail end of APCI depending on the message type
219
+ end
220
+ end
@@ -0,0 +1,150 @@
1
+ #encoding: ASCII-8BIT
2
+
3
+ class KNX
4
+ DatagramBuilder = Struct.new(:header, :cemi, :source_address, :destination_address, :data) do
5
+
6
+ def to_binary_s
7
+ data_array = self.data
8
+
9
+ resp = if present? data_array
10
+ @cemi.data_length = data_array.length
11
+
12
+ if data_array[0] <= 0b111111
13
+ @cemi.data = data_array[0]
14
+ if data_array.length > 1
15
+ data_array[1..-1].pack('C')
16
+ else
17
+ String.new
18
+ end
19
+ else
20
+ @cemi.data = 0
21
+ data_array.pack('C')
22
+ end
23
+ else
24
+ @cemi.data = 0
25
+ @cemi.data_length = 0
26
+ String.new
27
+ end
28
+
29
+ @cemi.source_address = self.source_address.to_i
30
+ @cemi.destination_address = self.destination_address.to_i
31
+
32
+ # 17 == header + cemi
33
+ @header.request_length = resp.bytesize + 17
34
+ "#{@header.to_binary_s}#{@cemi.to_binary_s}#{resp}"
35
+ end
36
+
37
+
38
+ protected
39
+
40
+
41
+ def present?(data)
42
+ !(data.nil? || data.empty?)
43
+ end
44
+
45
+ def initialize(address = nil, options = nil)
46
+ super()
47
+ return unless address
48
+
49
+ @address = parse(address)
50
+
51
+ @cemi = CEMI.new
52
+ @cemi.msg_code = MsgCode[options[:msg_code]]
53
+ @cemi.is_standard_frame = true
54
+ @cemi.no_repeat = options[:no_repeat]
55
+ @cemi.broadcast = options[:broadcast]
56
+ @cemi.priority = Priority[options[:priority]]
57
+
58
+ @cemi.is_group_address = @address.is_group?
59
+ @cemi.hop_count = options[:hop_count]
60
+
61
+ @header = Header.new
62
+ if options[:request_type]
63
+ @header.request_type = RequestTypes[options[:request_type]]
64
+ else
65
+ @header.request_type = RequestTypes[:routing_indication]
66
+ end
67
+
68
+ self.header = @header
69
+ self.cemi = @cemi
70
+ self.source_address = IndividualAddress.parse_friendly('0.0.1')
71
+ self.destination_address = @address
72
+
73
+ @cemi.source_address = self.source_address.to_i
74
+ @cemi.destination_address = self.destination_address.to_i
75
+ end
76
+
77
+ def parse(address)
78
+ result = address.split('/')
79
+ if result.length > 1
80
+ if result.length == 3
81
+ GroupAddress.parse_friendly(address)
82
+ else
83
+ GroupAddress2Level.parse_friendly(address)
84
+ end
85
+ else
86
+ IndividualAddress.parse_friendly(address)
87
+ end
88
+ end
89
+ end
90
+
91
+ class ActionDatagram < DatagramBuilder
92
+ def initialize(address, data_array, options)
93
+ super(address, options)
94
+
95
+ # Set the protocol control information
96
+ @cemi.apci = @address.is_group? ? ActionType[:group_write] : ActionType[:individual_write]
97
+ @cemi.tpci = TpciType[:unnumbered_data]
98
+
99
+ # To attempt save a byte we try to cram the first byte into the APCI field
100
+ if present? data_array
101
+ if data_array[0] <= 0b111111
102
+ @cemi.data = data_array[0]
103
+ end
104
+
105
+ @cemi.data_length = data_array.length
106
+ self.data = data_array
107
+ end
108
+ end
109
+ end
110
+
111
+ class StatusDatagram < DatagramBuilder
112
+ def initialize(address, options)
113
+ super(address, options)
114
+
115
+ # Set the protocol control information
116
+ @cemi.apci = @address.is_group? ? ActionType[:group_read] : ActionType[:individual_read]
117
+ @cemi.tpci = TpciType[:unnumbered_data]
118
+ end
119
+ end
120
+
121
+ class ResponseDatagram < DatagramBuilder
122
+ def initialize(raw_data, options)
123
+ super()
124
+
125
+ @header = Header.new
126
+ @header.read(raw_data[0..5])
127
+
128
+ @cemi = CEMI.new
129
+ @cemi.read(raw_data[6..16])
130
+
131
+ self.header = @header
132
+ self.cemi = @cemi
133
+
134
+ self.data = raw_data[17..(@header.request_length - 1)].bytes
135
+ if @cemi.data_length > self.data.length
136
+ self.data.unshift @cemi.data
137
+ end
138
+
139
+ self.source_address = IndividualAddress.parse(@cemi.source_address.to_i)
140
+
141
+ if @cemi.is_group_address == 0
142
+ self.destination_address = IndividualAddress.parse(@cemi.destination_address.to_i)
143
+ elsif options[:two_level_group]
144
+ self.destination_address = GroupAddress2Level.parse(@cemi.destination_address.to_i)
145
+ else
146
+ self.destination_address = GroupAddress.parse(@cemi.destination_address.to_i)
147
+ end
148
+ end
149
+ end
150
+ end
data/lib/knx/header.rb ADDED
@@ -0,0 +1,32 @@
1
+ #encoding: ASCII-8BIT
2
+
3
+ class KNX
4
+ RequestTypes = {
5
+ search_request: 0x0201,
6
+ search_response: 0x0202,
7
+ description_request: 0x0203,
8
+ description_response: 0x0204,
9
+ connect_request: 0x0205,
10
+ connect_response: 0x0206,
11
+ connectionstate_request: 0x0207,
12
+ connectionstate_response: 0x0208,
13
+ disconnect_request: 0x0209,
14
+ disconnect_response: 0x020A,
15
+ device_configuration_request: 0x0310,
16
+ device_configuration_ack: 0x0311,
17
+ tunnelling_request: 0x0420,
18
+ tunnelling_ack: 0x0421,
19
+ routing_indication: 0x0530,
20
+ routing_lost_message: 0x0531
21
+ }
22
+
23
+ # http://www.openremote.org/display/forums/KNX+IP+Connection+Headers
24
+ class Header < BinData::Record
25
+ endian :big
26
+
27
+ uint8 :header_length, value: 0x06 # Length 6 (always for version 1)
28
+ uint8 :version, value: 0x10 # Version 1
29
+ uint16 :request_type
30
+ uint16 :request_length
31
+ end
32
+ end
@@ -0,0 +1,98 @@
1
+ #encoding: ASCII-8BIT
2
+
3
+ class KNX
4
+ class ObjectServer
5
+ Errors = {
6
+ 0 => :no_error,
7
+ 1 => :device_internal_error,
8
+ 2 => :no_item_found,
9
+ 3 => :buffer_is_too_small,
10
+ 4 => :item_not_writeable,
11
+ 5 => :service_not_supported,
12
+ 6 => :bad_service_param,
13
+ 7 => :wrong_datapoint_id,
14
+ 8 => :bad_datapoint_command,
15
+ 9 => :bad_datapoint_length,
16
+ 10 => :message_inconsistent,
17
+ 11 => :object_server_busy
18
+ }
19
+
20
+ Datagram = Struct.new(:knx_header, :connection, :header) do
21
+ def initialize(raw_data = nil)
22
+ super(Header.new, ConnectionHeader.new, ObjectHeader.new)
23
+ # These values are unique to the KNX Object Server
24
+ self.knx_header.version = 0x20
25
+ self.knx_header.request_type = 0xF080
26
+ @data = []
27
+
28
+ if raw_data
29
+ self.knx_header.read(raw_data[0..5])
30
+ self.connection.read(raw_data[6..9])
31
+ self.header.read(raw_data[10..15])
32
+
33
+ # Check for error
34
+ if self.header.item_count == 0
35
+ @error_code = raw_data[16].getbyte(0)
36
+ @error = Errors[@error_code]
37
+ else
38
+ @error_code = 0
39
+ @error = :no_error
40
+
41
+ # Read the response
42
+ index = 16
43
+ self.header.item_count.times do
44
+ next_index = index + 4
45
+ item = StatusItem.new
46
+ item.read(raw_data[index...next_index])
47
+
48
+ index = next_index + item.value_length
49
+ item.value = raw_data[next_index...index]
50
+
51
+ @data << item
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+
58
+ attr_reader :error, :error_code, :data
59
+
60
+
61
+ def error?
62
+ @error_code != 0
63
+ end
64
+
65
+ def to_binary_s
66
+ self.header.item_count = @data.length if @data.length > 0
67
+ resp = "#{self.connection.to_binary_s}#{self.header.to_binary_s}"
68
+
69
+ @data.each do |item|
70
+ resp << item.to_binary_s
71
+ end
72
+
73
+ self.knx_header.request_length = resp.length + 6
74
+ "#{self.knx_header.to_binary_s}#{resp}"
75
+ end
76
+
77
+ def add_action(index, data: nil, **options)
78
+ req = RequestItem.new
79
+ req.id = index.to_i
80
+ req.command = Commands[options[:command]] || :set_value
81
+ if not data.nil?
82
+ if data == true || data == false
83
+ data = data ? 1 : 0
84
+ end
85
+
86
+ if data.is_a? String
87
+ req.value = data
88
+ else
89
+ req.value = ''
90
+ req.value << data
91
+ end
92
+ end
93
+ @data << req
94
+ self
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,39 @@
1
+ #encoding: ASCII-8BIT
2
+
3
+ class KNX
4
+ class ObjectServer
5
+ class ConnectionHeader < BinData::Record
6
+ endian :big
7
+
8
+ uint8 :header_length, value: 0x04
9
+ uint8 :reserved1, value: 0x00
10
+ uint8 :reserved2, value: 0x00
11
+ uint8 :reserved3, value: 0x00
12
+ end
13
+
14
+
15
+ Filters = {
16
+ 0 => :all_values,
17
+ 1 => :valid_values,
18
+ 2 => :updated_values
19
+ }
20
+ Filters.merge!(Filters.invert)
21
+
22
+ class ObjectHeader < BinData::Record
23
+ endian :big
24
+
25
+ uint8 :main_service, value: 0xF0
26
+ uint8 :sub_service
27
+ uint16 :start_item
28
+ uint16 :item_count
29
+
30
+ attr_accessor :filter
31
+
32
+ def to_binary_s
33
+ resp = super()
34
+ resp << @filter if @filter
35
+ resp
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,33 @@
1
+ #encoding: ASCII-8BIT
2
+
3
+ class KNX
4
+ class ObjectServer
5
+ Commands = {
6
+ 0 => :no_command,
7
+ 1 => :set_value,
8
+ 2 => :send_value,
9
+ 3 => :set_value_and_send,
10
+ 4 => :read_value,
11
+ 5 => :clear_transmission_state
12
+ }
13
+ Commands.merge!(Commands.invert)
14
+
15
+ class RequestItem < BinData::Record
16
+ endian :big
17
+
18
+ uint16 :id
19
+ bit4 :reserved
20
+ bit4 :command
21
+ uint8 :value_length
22
+
23
+
24
+ attr_accessor :value
25
+
26
+
27
+ def to_binary_s
28
+ self.value_length = @value ? @value.length : 0
29
+ "#{super()}#{@value}"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,39 @@
1
+ #encoding: ASCII-8BIT
2
+
3
+ class KNX
4
+ class ObjectServer
5
+ Status = {
6
+ 0 => :idle_ok,
7
+ 1 => :idle_error,
8
+ 2 => :transmission_in_progress,
9
+ 3 => :transmission_request
10
+ }
11
+
12
+ class StatusItem < BinData::Record
13
+ endian :big
14
+
15
+ uint16 :id
16
+
17
+ bit3 :reserved
18
+ bit1 :valid
19
+ bit1 :update_from_bus
20
+ bit1 :data_request
21
+ bit2 :status
22
+
23
+ uint8 :value_length
24
+
25
+
26
+ attr_accessor :value
27
+
28
+
29
+ def to_binary_s
30
+ self.value_length = @value ? @value.length : 0
31
+ "#{super()}#{@value}"
32
+ end
33
+
34
+ def transmission_status
35
+ ::KNX::ObjectServer::Status[self.status]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,62 @@
1
+ #encoding: ASCII-8BIT
2
+
3
+ require 'bindata'
4
+
5
+ require 'knx/header'
6
+ require 'knx/object_server/object_header'
7
+ require 'knx/object_server/status_item'
8
+ require 'knx/object_server/request_item'
9
+ require 'knx/object_server/datagram'
10
+
11
+
12
+ class KNX
13
+ class ObjectServer
14
+ Defaults = {
15
+ filter: :valid_values,
16
+ item_count: 1,
17
+ command: :set_value_and_send
18
+ }
19
+
20
+ def initialize(options = {})
21
+ @options = Defaults.merge(options)
22
+ end
23
+
24
+ # Builds an Object Server command datagram for setting an index to a value
25
+ #
26
+ # @param index [Integer, Fixnum] the object address or index as defined in the object server
27
+ # @param data [String, Integer, Fixnum, Array<Integer, Fixnum>] the value to be set at the address
28
+ # @return [Datagram] a ruby object representing the request that can be modified further
29
+ def action(index, data = nil, **options)
30
+ options = @options.merge(options)
31
+
32
+ cmd = Datagram.new
33
+ cmd.add_action(index.to_i, data: data, **options)
34
+ cmd.header.sub_service = 0x06
35
+ cmd.header.start_item = index.to_i
36
+ cmd
37
+ end
38
+
39
+ # Builds an Object Server request datagram for querying an index value
40
+ #
41
+ # @param index [Integer, Fixnum] the object address or index as defined in the object server
42
+ # @return [Datagram] a ruby object representing the request that can be modified further
43
+ def status(index, options = {})
44
+ options = @options.merge(options)
45
+
46
+ data = Datagram.new
47
+ data.header.sub_service = 0x05
48
+ data.header.start_item = index.to_i
49
+ data.header.item_count = options[:item_count].to_i
50
+ data.header.filter = Filters[options[:filter]]
51
+ data
52
+ end
53
+
54
+ # Returns a KNX Object Server datagram as a ruby object for easy inspection
55
+ #
56
+ # @param data [String] a binary string containing the datagram
57
+ # @return [Datagram] a ruby object representing the data
58
+ def read(raw_data)
59
+ Datagram.new(raw_data)
60
+ end
61
+ end
62
+ end
data/lib/knx.rb ADDED
@@ -0,0 +1,66 @@
1
+ #encoding: ASCII-8BIT
2
+
3
+ require 'bindata'
4
+
5
+ require 'knx/header'
6
+ require 'knx/cemi'
7
+ require 'knx/address'
8
+ require 'knx/datagram'
9
+
10
+
11
+ class KNX
12
+ Defaults = {
13
+ priority: :low,
14
+ no_repeat: true,
15
+ broadcast: true,
16
+ hop_count: 6,
17
+ msg_code: :send_datagram
18
+ }
19
+
20
+ def initialize(options = {})
21
+ @options = Defaults.merge(options)
22
+ end
23
+
24
+ # Builds a KNX command datagram for setting an address to a value
25
+ #
26
+ # @param address [String] the object address in group or individual format
27
+ # @param data [String, Integer, Fixnum, Array<Integer, Fixnum>] the value to be set at the address
28
+ # @return [ActionDatagram] a ruby object representing the request that can be modified further
29
+ def action(address, data, options = {})
30
+ if data == true || data == false
31
+ data = data ? 1 : 0
32
+ end
33
+
34
+ klass = data.class
35
+
36
+ raw = if klass == String
37
+ data.bytes
38
+ elsif [Integer, Fixnum].include? klass
39
+ # Assume this is a byte
40
+ [data]
41
+ elsif klass == Array
42
+ # We assume this is a byte array
43
+ data
44
+ else
45
+ raise ArgumentError, "Unknown data type for #{data}"
46
+ end
47
+
48
+ ActionDatagram.new(address, raw, @options.merge(options))
49
+ end
50
+
51
+ # Builds a KNX status request datagram for querying an address value
52
+ #
53
+ # @param address [String] the object address in group or individual format
54
+ # @return [StatusDatagram] a ruby object representing the request that can be modified further
55
+ def status(address, options = {})
56
+ StatusDatagram.new(address, @options.merge(options))
57
+ end
58
+
59
+ # Represents a KNX datagram as a ruby object for easy inspection
60
+ #
61
+ # @param data [String] a binary string containing the datagram
62
+ # @return [ResponseDatagram] a ruby object representing the data
63
+ def read(data, options = {})
64
+ ResponseDatagram.new(data, @options.merge(options))
65
+ end
66
+ end
data/spec/knx_spec.rb ADDED
@@ -0,0 +1,38 @@
1
+ #encoding: ASCII-8BIT
2
+
3
+ require 'knx'
4
+
5
+
6
+ describe "knx protocol helper" do
7
+ before :each do
8
+ @knx = KNX.new
9
+ end
10
+
11
+ it "should parse and generate the same string" do
12
+ datagram = @knx.read("\x06\x10\x05\x30\0\x11\x29\0\xbc\xe0\0\x01\x0a\0\x01\0\x80")
13
+ expect(datagram.to_binary_s).to eq("\x06\x10\x05\x30\0\x11\x29\0\xbc\xe0\0\x01\x0a\0\x01\0\x80")
14
+
15
+ datagram = @knx.read("\x06\x10\x05\x30\0\x11\x29\0\xbc\xe0\0\x01\x0a\0\x01\0\x81")
16
+ expect(datagram.to_binary_s).to eq("\x06\x10\x05\x30\0\x11\x29\0\xbc\xe0\0\x01\x0a\0\x01\0\x81")
17
+
18
+ expect(datagram.data).to eq([1])
19
+ expect(datagram.source_address.to_s).to eq("0.0.1")
20
+ expect(datagram.destination_address.to_s).to eq("1/2/0")
21
+ end
22
+
23
+ it "should generate single bit action requests" do
24
+ datagram = @knx.action('1/2/0', false)
25
+ expect(datagram.to_binary_s).to eq("\x06\x10\x05\x30\0\x11\x29\0\xbc\xe0\0\x01\x0a\0\x01\0\x80")
26
+
27
+ datagram = @knx.action('1/2/0', true)
28
+ expect(datagram.to_binary_s).to eq("\x06\x10\x05\x30\0\x11\x29\0\xbc\xe0\0\x01\x0a\0\x01\0\x81")
29
+ end
30
+
31
+ it "should generate byte action requests" do
32
+ datagram = @knx.action('1/2/0', 20)
33
+ expect(datagram.to_binary_s).to eq("\x06\x10\x050\x00\x11)\x00\xBC\xE0\x00\x01\n\x00\x01\x00\x94")
34
+
35
+ datagram = @knx.action('1/2/0', 240)
36
+ expect(datagram.to_binary_s).to eq("\x06\x10\x050\x00\x12)\x00\xBC\xE0\x00\x01\n\x00\x01\x00\x80\xF0")
37
+ end
38
+ end
@@ -0,0 +1,34 @@
1
+ #encoding: ASCII-8BIT
2
+
3
+ require 'knx/object_server'
4
+
5
+
6
+ describe "object server protocol helper" do
7
+ before :each do
8
+ @knx = KNX::ObjectServer.new
9
+ end
10
+
11
+ it "should parse and generate the same string" do
12
+ datagram = @knx.read("\x06\x10\xF0\x80\x00\x15\x04\x00\x00\x00\xF0\x06\x00\x02\x00\x01\x00\x02\x03\x01\x01")
13
+ expect(datagram.to_binary_s).to eq("\x06\x10\xF0\x80\x00\x15\x04\x00\x00\x00\xF0\x06\x00\x02\x00\x01\x00\x02\x03\x01\x01")
14
+
15
+ expect(datagram.data[0].id).to eq(2)
16
+ expect(datagram.data[0].value).to eq("\x01")
17
+ end
18
+
19
+ it "should generate single bit action requests" do
20
+ datagram = @knx.action(1, false)
21
+ expect(datagram.to_binary_s).to eq("\x06\x10\xF0\x80\x00\x15\x04\x00\x00\x00\xF0\x06\x00\x01\x00\x01\x00\x01\x03\x01\x00")
22
+
23
+ datagram = @knx.action(2, true)
24
+ expect(datagram.to_binary_s).to eq("\x06\x10\xF0\x80\x00\x15\x04\x00\x00\x00\xF0\x06\x00\x02\x00\x01\x00\x02\x03\x01\x01")
25
+ end
26
+
27
+ it "should generate byte action requests" do
28
+ datagram = @knx.action(3, 20)
29
+ expect(datagram.to_binary_s).to eq("\x06\x10\xF0\x80\x00\x15\x04\x00\x00\x00\xF0\x06\x00\x03\x00\x01\x00\x03\x03\x01\x14")
30
+
31
+ datagram = @knx.action(4, 240)
32
+ expect(datagram.to_binary_s).to eq("\x06\x10\xF0\x80\x00\x15\x04\x00\x00\x00\xF0\x06\x00\x04\x00\x01\x00\x04\x03\x01\xF0")
33
+ end
34
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: knx
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Stephen von Takach
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-07-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bindata
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: yard
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '11'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '11'
69
+ description: " Constructs KNX standard datagrams that make it easy to communicate
70
+ with devices on KNX networks\n"
71
+ email:
72
+ - steve@cotag.me
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files:
76
+ - README.md
77
+ files:
78
+ - README.md
79
+ - knx.gemspec
80
+ - lib/knx.rb
81
+ - lib/knx/address.rb
82
+ - lib/knx/cemi.rb
83
+ - lib/knx/datagram.rb
84
+ - lib/knx/header.rb
85
+ - lib/knx/object_server.rb
86
+ - lib/knx/object_server/datagram.rb
87
+ - lib/knx/object_server/object_header.rb
88
+ - lib/knx/object_server/request_item.rb
89
+ - lib/knx/object_server/status_item.rb
90
+ - spec/knx_spec.rb
91
+ - spec/object_server_spec.rb
92
+ homepage: https://github.com/acaprojects/ruby-knx
93
+ licenses:
94
+ - MIT
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.5.1
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: KNX protocol on Ruby
116
+ test_files:
117
+ - spec/knx_spec.rb
118
+ - spec/object_server_spec.rb
119
+ has_rdoc: