knx 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![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
|
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:
|