knx 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: