Skip to content

Commit

Permalink
feat: improved CEMI parsing
Browse files Browse the repository at this point in the history
and add helpers for making tunnel requests
  • Loading branch information
stakach committed Jul 19, 2024
1 parent 5f29012 commit fe8c3ca
Show file tree
Hide file tree
Showing 12 changed files with 230 additions and 270 deletions.
2 changes: 1 addition & 1 deletion shard.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: knx
version: 1.1.1
version: 1.2.0
crystal: ">= 0.36.1"

dependencies:
Expand Down
45 changes: 40 additions & 5 deletions spec/knx_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,57 @@ describe "knx protocol helper" do

it "should generate single bit action requests" do
datagram = knx.action("1/2/0", false)
datagram.to_slice.should eq(Bytes[6, 16, 5, 48, 0, 17, 41, 0, 188, 224, 0, 1, 10, 0, 1, 0, 128])
datagram.to_slice.should eq(Bytes[6, 16, 5, 48, 0, 17, 41, 0, 188, 224, 0, 0, 10, 0, 1, 0, 128])

datagram = knx.action("1/2/0", true)
datagram.to_slice.should eq(Bytes[6, 16, 5, 48, 0, 17, 41, 0, 188, 224, 0, 1, 10, 0, 1, 0, 129])
datagram.to_slice.should eq(Bytes[6, 16, 5, 48, 0, 17, 41, 0, 188, 224, 0, 0, 10, 0, 1, 0, 129])
end

it "should generate byte action requests" do
datagram = knx.action("1/2/0", 20)
datagram.to_slice.should eq(Bytes[6, 16, 5, 48, 0, 17, 41, 0, 188, 224, 0, 1, 10, 0, 1, 0, 148])
datagram.to_slice.should eq(Bytes[6, 16, 5, 48, 0, 17, 41, 0, 188, 224, 0, 0, 10, 0, 1, 0, 148])

datagram = knx.action("1/2/0", 240)
datagram.to_slice.should eq(Bytes[6, 16, 5, 48, 0, 18, 41, 0, 188, 224, 0, 1, 10, 0, 1, 0, 128, 240])
datagram.to_slice.should eq(Bytes[6, 16, 5, 48, 0, 18, 41, 0, 188, 224, 0, 0, 10, 0, 2, 0, 128, 240])
end

it "should generate status requests" do
datagram = knx.status("1/2/1")
datagram.to_slice.should eq(Bytes[6, 16, 5, 48, 0, 17, 41, 0, 188, 224, 0, 1, 10, 1, 0, 0, 0])
datagram.to_slice.should eq(Bytes[6, 16, 5, 48, 0, 17, 17, 0, 188, 224, 0, 0, 10, 1, 1, 0, 0])
end

# examples from
# https://github.com/uptimedk/knxnet_ip/blob/master/test/knxnet_ip/telegram_test.exs

it "should encode / decode a Group Read" do
datagram = knx.status("0/0/3", source: "1.0.3", msg_code: :data_indicator)
cemi_raw = "2900bce010030003010000".hexbytes
datagram.cemi.to_slice.should eq(cemi_raw)

input = IO::Memory.new(cemi_raw)
cemi = input.read_bytes(KNX::CEMI)
cemi.to_slice.should eq cemi_raw
end

it "should encode / decode a Group Write" do
datagram = knx.action("0/0/3", 0x1917, source: "1.1.1")
cemi_raw = "2900bce0110100030300801917".hexbytes
datagram.cemi.to_slice.should eq(cemi_raw)

input = IO::Memory.new(cemi_raw)
cemi = input.read_bytes(KNX::CEMI)
cemi.to_slice.should eq cemi_raw
end

it "should encode / decode a Group Respnse" do
# source: "1.1.4",
# destination: "0/0/2",
# service: :group_response,
# value: <<0x41, 0x46, 0x8F, 0x5C>>

cemi_raw = "2900bce01104000205004041468F5C".hexbytes
input = IO::Memory.new(cemi_raw)
cemi = input.read_bytes(KNX::CEMI)
cemi.to_slice.should eq cemi_raw
end
end
22 changes: 0 additions & 22 deletions spec/routing_indication_spec.cr

This file was deleted.

45 changes: 45 additions & 0 deletions spec/tunnel_client_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,49 @@ describe KNX::TunnelClient do

client.queue_size.should eq 0
end

it "should work with helper methods" do
client = KNX::TunnelClient.new(
Socket::IPAddress.new("10.9.78.59", 48907),
knx: ::KNX.new(
priority: :alarm,
broadcast: true,
no_repeat: true
)
)
client.connected?.should eq false

is_connected = nil
last_error = nil
last_trans = nil
last_cemi = nil

client.on_state_change do |connected, error|
is_connected = connected
last_error = error
end
client.on_transmit { |bytes| last_trans = bytes }
client.on_message { |cemi| last_cemi = cemi }

# check connection flow
client.connect
last_trans.should eq "06100205001a08010a094e3bbf0b08010a094e3bbf0b04040200".hexbytes
client.process "0610020600143d0008010a094e510e5704041103".hexbytes

is_connected.should eq true
last_error.should eq KNX::ConnectionError::NoError
client.waiting?.should eq false

# make some requests
client.action("0/0/2", true)
last_trans.try(&.hexstring).should eq "061004200015043d00002900b4e000000002010081"
client.process "0610020800083d00".hexbytes

client.action("0/0/2", 2)
last_trans.try(&.hexstring).should eq "061004200015043d01002900b4e000000002010082"
client.process "0610020800083d00".hexbytes

client.status("0/0/2")
last_trans.try(&.hexstring).should eq "061004200015043d02001100b4e000000002010000"
end
end
2 changes: 1 addition & 1 deletion spec/tunnel_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe KNX::TunnelRequest do
req.channel_id.should eq(1)
req.sequence.should eq(23)
req.cemi.is_group_address.should eq(true)
req.destination_address.should eq("9/0/8")
req.destination_address.to_s.should eq("9/0/8")

raw = "061004200015043d00001100b4e000000002010000".hexbytes
input = IO::Memory.new(raw)
Expand Down
23 changes: 13 additions & 10 deletions src/knx.cr
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ require "./knx/connection/connect_request"
require "./knx/connection/connect_state_request"
require "./knx/connection/disconnect_request"
require "./knx/connection/tunnel_request"
require "./knx/routing/indication_request"

# Discovery and negotiation: http://knxer.net/?p=78

Expand All @@ -21,29 +20,30 @@ class KNX
property? no_repeat : Bool
property? broadcast : Bool
property hop_count : UInt8
property msg_code : MsgCode
property cmac_key : Bytes?
property source : String

def initialize(
@priority = Priority::LOW,
@no_repeat = true,
@broadcast = true,
@hop_count = 6_u8,
@msg_code = MsgCode::DataIndicator,
@two_level_group = false,
@cmac_key = nil
@cmac_key = nil,
@source = "0.0.0"
)
end

def action(
address : String,
data,
msg_code : MsgCode = @msg_code,
msg_code : MsgCode = MsgCode::DataIndicator,
no_repeat : Bool = @no_repeat,
broadcast : Bool = @broadcast,
priority : Priority = @priority,
hop_count : UInt8 = @hop_count,
request_type : RequestTypes = RequestTypes::RoutingIndication
request_type : RequestTypes = RequestTypes::RoutingIndication,
source : String = @source
) : ActionDatagram
raw = case data
when Bool
Expand Down Expand Up @@ -76,18 +76,20 @@ class KNX
broadcast: broadcast,
priority: priority,
hop_count: hop_count,
request_type: request_type
request_type: request_type,
source: source
)
end

def status(
address : String,
msg_code : MsgCode = @msg_code,
msg_code : MsgCode = MsgCode::DataRequest,
no_repeat : Bool = @no_repeat,
broadcast : Bool = @broadcast,
priority : Priority = @priority,
hop_count : UInt8 = @hop_count,
request_type : RequestTypes = RequestTypes::RoutingIndication
request_type : RequestTypes = RequestTypes::RoutingIndication,
source : String = @source
)
StatusDatagram.new(
address,
Expand All @@ -96,7 +98,8 @@ class KNX
broadcast: broadcast,
priority: priority,
hop_count: hop_count,
request_type: request_type
request_type: request_type,
source: source
)
end

Expand Down
43 changes: 30 additions & 13 deletions src/knx/address.cr
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,37 @@ class KNX
abstract class Address < BinData
endian :big

def self.parse(input)
case input
when Int
io = IO::Memory.new(2)
io.write_bytes input.to_u16, IO::ByteFormat::BigEndian
io.rewind
io.read_bytes(self, IO::ByteFormat::BigEndian)
when String
addr = parse_friendly(input)
raise "address parsing failed" unless addr
addr
def self.parse(input : Int)
io = IO::Memory.new(2)
io.write_bytes input.to_u16, IO::ByteFormat::BigEndian
io.rewind
io.read_bytes(self, IO::ByteFormat::BigEndian)
end

def self.parse(bytes : Bytes)
io = IO::Memory.new(bytes)
io.read_bytes(self)
end

def self.parse(io : IO)
io.read_bytes(self)
end

def self.parse(address : String) : Address
count = address.count('/')
case count
when 2
GroupAddress.parse_friendly(address)
when 1
GroupAddress2Level.parse_friendly(address)
else
io = IO::Memory.new(input.to_slice)
io.read_bytes(self)
IndividualAddress.parse_friendly(address)
end
end

macro inherited
def self.parse(address : String) : self
parse_friendly address
end
end

Expand Down
Loading

0 comments on commit fe8c3ca

Please sign in to comment.