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 +7 -0
- data/README.md +85 -0
- data/knx.gemspec +28 -0
- data/lib/knx/address.rb +150 -0
- data/lib/knx/cemi.rb +220 -0
- data/lib/knx/datagram.rb +150 -0
- data/lib/knx/header.rb +32 -0
- data/lib/knx/object_server/datagram.rb +98 -0
- data/lib/knx/object_server/object_header.rb +39 -0
- data/lib/knx/object_server/request_item.rb +33 -0
- data/lib/knx/object_server/status_item.rb +39 -0
- data/lib/knx/object_server.rb +62 -0
- data/lib/knx.rb +66 -0
- data/spec/knx_spec.rb +38 -0
- data/spec/object_server_spec.rb +34 -0
- metadata +119 -0
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
|
+
[](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
|
data/lib/knx/address.rb
ADDED
@@ -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
|
data/lib/knx/datagram.rb
ADDED
@@ -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:
|