em-mqtt-sn 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.md +22 -0
- data/NEWS.md +14 -0
- data/README.md +54 -0
- data/bin/em-mqtt-sn-gateway +6 -0
- data/lib/em/mqtt-sn.rb +25 -0
- data/lib/em/mqtt-sn/gateway.rb +81 -0
- data/lib/em/mqtt-sn/gateway_handler.rb +216 -0
- data/lib/em/mqtt-sn/packet.rb +348 -0
- data/lib/em/mqtt-sn/server_connection.rb +77 -0
- data/lib/em/mqtt-sn/version.rb +5 -0
- data/spec/em_mqtt-sn_packet_spec.rb +660 -0
- data/spec/em_mqtt-sn_version_spec.rb +21 -0
- metadata +171 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 89cbe9cba1cf7a7b09bf5bef4ec54c47a2953f4d
|
4
|
+
data.tar.gz: 5e1138b972b3ac617062a9dadfb2e6afffac34b2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ab1388f070cc1860fc8d73d30e3e1a5d33d9b1aafecaf369086b731ca710d7382cc1042500193e1e5e688fbfda41a186af8e0ce2a1d9578e67620a8a81de1372
|
7
|
+
data.tar.gz: bc7dabab0a5596af64424fedb78e785449518bddc5eeea86a1a4a4a5eb3298155a4537a2b79cebeada9b3f78633f66366710ed0d14d55b0ddc73602993d1257a
|
data/LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
=====================
|
3
|
+
|
4
|
+
Copyright (c) Nicholas J Humfrey
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
8
|
+
in the Software without restriction, including without limitation the rights
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
11
|
+
furnished to do so, subject to the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be included in
|
14
|
+
all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
THE SOFTWARE.
|
data/NEWS.md
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
EventMachine MQTT-SN NEWS
|
2
|
+
=========================
|
3
|
+
|
4
|
+
Version 0.0.2 (2015-03-03)
|
5
|
+
--------------------------
|
6
|
+
|
7
|
+
* Renamed gem from mqtts to mqtt-sn.
|
8
|
+
* Fixes for new version of MQTT gem
|
9
|
+
|
10
|
+
|
11
|
+
Version 0.0.1 (2013-04-20)
|
12
|
+
--------------------------
|
13
|
+
|
14
|
+
* Initial Release.
|
data/README.md
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
ruby-em-mqtt-sn
|
2
|
+
===============
|
3
|
+
|
4
|
+
This gem adds MQTT-SN (MQTT For Sensor Networks) protocol support to EventMachine,
|
5
|
+
an event-processing library for Ruby.
|
6
|
+
|
7
|
+
It also includes a MQTT-SN gateway, to connect MQTT-SN clients to a standard [MQTT] server.
|
8
|
+
|
9
|
+
Usage: em-mqtt-sn-gateway [options]
|
10
|
+
|
11
|
+
Options:
|
12
|
+
-D, --debug turn on debug logging
|
13
|
+
-a, --address [HOST] bind to HOST address (default: 0.0.0.0)
|
14
|
+
-p, --port [PORT] UDP port number to run on (default: 1883)
|
15
|
+
-A, --server-address [HOST] MQTT server address to connect to (default: 127.0.0.1)
|
16
|
+
-P, --server-port [PORT] MQTT server port to connect to (default: 1883)
|
17
|
+
-h, --help show this message
|
18
|
+
--version show version
|
19
|
+
|
20
|
+
|
21
|
+
Example
|
22
|
+
-------
|
23
|
+
|
24
|
+
$ sudo gem install em-mqtt-sn
|
25
|
+
$ em-mqtt-sn-gateway -A test.mosquitto.org
|
26
|
+
I, [2013-04-20T12:08:56.850572 #29588] INFO -- : Starting MQTT-SN gateway on UDP 0.0.0.0:1883
|
27
|
+
I, [2013-04-20T12:08:56.850646 #29588] INFO -- : Server address test.mosquitto.org:1883
|
28
|
+
I, [2013-04-20T12:09:00.577446 #29588] INFO -- : mqtt-sn-tools-29710 is now connected
|
29
|
+
I, [2013-04-20T12:09:00.578032 #29588] INFO -- : mqtt-sn-tools-29710 subscribing to 'test'
|
30
|
+
I, [2013-04-20T12:09:00.601937 #29588] INFO -- : mqtt-sn-tools-29710 recieved publish to 'test'
|
31
|
+
I, [2013-04-20T12:09:07.770269 #29588] INFO -- : mqtt-sn-tools-29713 is now connected
|
32
|
+
I, [2013-04-20T12:09:07.770733 #29588] INFO -- : mqtt-sn-tools-29713 publishing to 'test'
|
33
|
+
I, [2013-04-20T12:09:07.783940 #29588] INFO -- : mqtt-sn-tools-29710 recieved publish to 'test'
|
34
|
+
I, [2013-04-20T12:09:22.815726 #29588] INFO -- : Disconnected: mqtt-sn-tools-29713
|
35
|
+
|
36
|
+
|
37
|
+
License
|
38
|
+
-------
|
39
|
+
|
40
|
+
The em-mqtt-sn gem is licensed under the terms of the MIT license.
|
41
|
+
See the file LICENSE for details.
|
42
|
+
|
43
|
+
|
44
|
+
Contact
|
45
|
+
-------
|
46
|
+
|
47
|
+
* Author: Nicholas J Humfrey
|
48
|
+
* Email: njh@aelius.com
|
49
|
+
* Twitter: [@njh]
|
50
|
+
* Home Page: http://www.aelius.com/njh/
|
51
|
+
|
52
|
+
|
53
|
+
[MQTT]: http://mqtt.org/
|
54
|
+
[@njh]: http://twitter.com/njh
|
data/lib/em/mqtt-sn.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..')))
|
2
|
+
|
3
|
+
require 'eventmachine'
|
4
|
+
require 'logger'
|
5
|
+
require 'em/mqtt'
|
6
|
+
|
7
|
+
module EventMachine::MQTTSN
|
8
|
+
|
9
|
+
DEFAULT_HOST = 'localhost'
|
10
|
+
DEFAULT_PORT = 1883
|
11
|
+
|
12
|
+
class Exception < Exception
|
13
|
+
end
|
14
|
+
|
15
|
+
class ProtocolException < MQTT::Exception
|
16
|
+
end
|
17
|
+
|
18
|
+
require "em/mqtt-sn/version"
|
19
|
+
|
20
|
+
autoload :ServerConnection, 'em/mqtt-sn/server_connection'
|
21
|
+
autoload :Gateway, 'em/mqtt-sn/gateway'
|
22
|
+
autoload :GatewayHandler, 'em/mqtt-sn/gateway_handler'
|
23
|
+
autoload :Packet, 'em/mqtt-sn/packet'
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
class EventMachine::MQTTSN::Gateway
|
4
|
+
attr_accessor :local_address
|
5
|
+
attr_accessor :local_port
|
6
|
+
attr_accessor :server_address
|
7
|
+
attr_accessor :server_port
|
8
|
+
attr_accessor :logger
|
9
|
+
|
10
|
+
def initialize(args=[])
|
11
|
+
# Set defaults
|
12
|
+
self.local_address = "0.0.0.0"
|
13
|
+
self.local_port = EventMachine::MQTTSN::DEFAULT_PORT
|
14
|
+
self.server_address = "127.0.0.1"
|
15
|
+
self.server_port = MQTT::DEFAULT_PORT
|
16
|
+
self.logger = Logger.new(STDOUT)
|
17
|
+
self.logger.level = Logger::INFO
|
18
|
+
parse(args) unless args.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse(args)
|
22
|
+
OptionParser.new("", 28, ' ') do |opts|
|
23
|
+
opts.banner = "Usage: #{File.basename $0} [options]"
|
24
|
+
|
25
|
+
opts.separator ""
|
26
|
+
opts.separator "Options:"
|
27
|
+
|
28
|
+
opts.on("-D", "--debug", "turn on debug logging") do
|
29
|
+
self.logger.level = Logger::DEBUG
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.on("-a", "--address [HOST]", "bind to HOST address (default: #{local_address})") do |address|
|
33
|
+
self.local_address = address
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on("-p", "--port [PORT]", "UDP port number to run on (default: #{local_port})") do |port|
|
37
|
+
self.local_port = port
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on("-A", "--server-address [HOST]", "MQTT server address to connect to (default: #{server_address})") do |address|
|
41
|
+
self.server_address = address
|
42
|
+
end
|
43
|
+
|
44
|
+
opts.on("-P", "--server-port [PORT]", "MQTT server port to connect to (default: #{server_port})") do |port|
|
45
|
+
self.server_port = port
|
46
|
+
end
|
47
|
+
|
48
|
+
opts.on_tail("-h", "--help", "show this message") do
|
49
|
+
puts opts
|
50
|
+
exit
|
51
|
+
end
|
52
|
+
|
53
|
+
opts.on_tail("--version", "show version") do
|
54
|
+
puts EventMachine::MQTTSN::VERSION
|
55
|
+
exit
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.parse!(args)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def run
|
63
|
+
EventMachine.run do
|
64
|
+
# hit Control + C to stop
|
65
|
+
Signal.trap("INT") { EventMachine.stop }
|
66
|
+
Signal.trap("TERM") { EventMachine.stop }
|
67
|
+
|
68
|
+
logger.info("Starting MQTT-SN gateway on UDP #{local_address}:#{local_port}")
|
69
|
+
logger.info("MQTT server address #{server_address}:#{server_port}")
|
70
|
+
EventMachine.open_datagram_socket(
|
71
|
+
local_address,
|
72
|
+
local_port,
|
73
|
+
EventMachine::MQTTSN::GatewayHandler,
|
74
|
+
:logger => logger,
|
75
|
+
:server_address => server_address,
|
76
|
+
:server_port => server_port
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
#
|
2
|
+
# There is only a single instance of GatewayHandler which
|
3
|
+
# processes UDP packets from all MQTT-SN clients.
|
4
|
+
#
|
5
|
+
|
6
|
+
class EventMachine::MQTTSN::GatewayHandler < EventMachine::Connection
|
7
|
+
attr_reader :logger
|
8
|
+
attr_reader :connections
|
9
|
+
attr_reader :server_address
|
10
|
+
attr_reader :server_port
|
11
|
+
|
12
|
+
def initialize(attr)
|
13
|
+
@connections = {}
|
14
|
+
attr.each_pair do |k,v|
|
15
|
+
instance_variable_set("@#{k}", v)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Run the cleanup task periodically
|
19
|
+
EventMachine.add_periodic_timer(10) { cleanup }
|
20
|
+
end
|
21
|
+
|
22
|
+
# UDP packet received by gateway
|
23
|
+
def receive_data(data)
|
24
|
+
packet = EventMachine::MQTTSN::Packet.parse(data)
|
25
|
+
unless packet.nil?
|
26
|
+
process_packet(get_peername, packet)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Incoming packet received from client
|
31
|
+
def process_packet(peername, packet)
|
32
|
+
logger.debug("Received MQTT-SN: #{packet.class}")
|
33
|
+
|
34
|
+
if packet.class == EventMachine::MQTTSN::Packet::Connect
|
35
|
+
connect(peername, packet)
|
36
|
+
else
|
37
|
+
connection = @connections[peername]
|
38
|
+
unless connection.nil? or !connection.connected?
|
39
|
+
case packet
|
40
|
+
when EventMachine::MQTTSN::Packet::Register
|
41
|
+
register(connection, packet)
|
42
|
+
when EventMachine::MQTTSN::Packet::Publish
|
43
|
+
publish(connection, packet)
|
44
|
+
when EventMachine::MQTTSN::Packet::Subscribe
|
45
|
+
subscribe(connection, packet)
|
46
|
+
when EventMachine::MQTTSN::Packet::Pingreq
|
47
|
+
connection.send_packet MQTT::Packet::Pingreq.new
|
48
|
+
when EventMachine::MQTTSN::Packet::Pingresp
|
49
|
+
connection.send_packet MQTT::Packet::Pingresp.new
|
50
|
+
when EventMachine::MQTTSN::Packet::Disconnect
|
51
|
+
disconnect(connection)
|
52
|
+
else
|
53
|
+
logger.warn("Unable to handle MQTT-SN packet of type: #{packet.class}")
|
54
|
+
end
|
55
|
+
else
|
56
|
+
logger.warn("Received MQTT-SN packet of type: #{packet.class} while not connected")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# CONNECT received from client - establish connection to server
|
62
|
+
def connect(peername, packet)
|
63
|
+
# If connection already exists, disconnect first
|
64
|
+
if @connections.has_key?(peername)
|
65
|
+
logger.warn("Received CONNECT while already connected")
|
66
|
+
@connections[peername].disconnect
|
67
|
+
end
|
68
|
+
|
69
|
+
# Create a TCP connection to the server
|
70
|
+
client_port, client_address = Socket.unpack_sockaddr_in(peername)
|
71
|
+
connection = EventMachine::connect(
|
72
|
+
server_address, server_port,
|
73
|
+
EventMachine::MQTTSN::ServerConnection,
|
74
|
+
self, client_address, client_port
|
75
|
+
)
|
76
|
+
|
77
|
+
# Store the client ID
|
78
|
+
connection.client_id = packet.client_id
|
79
|
+
|
80
|
+
# Send a MQTT connect packet to the server
|
81
|
+
connection.send_packet MQTT::Packet::Connect.new(
|
82
|
+
:client_id => packet.client_id,
|
83
|
+
:keep_alive => packet.keep_alive,
|
84
|
+
:clean_session => packet.clean_session
|
85
|
+
)
|
86
|
+
|
87
|
+
# Add the connection to the table
|
88
|
+
@connections[peername] = connection
|
89
|
+
end
|
90
|
+
|
91
|
+
# Handle a MQTT packet coming back from the server
|
92
|
+
def relay_from_server(connection, packet)
|
93
|
+
logger.debug("Received MQTT: #{packet.inspect}")
|
94
|
+
case packet
|
95
|
+
when MQTT::Packet::Connack
|
96
|
+
# FIXME: re-map the return code
|
97
|
+
mqttsn_packet = EventMachine::MQTTSN::Packet::Connack.new(
|
98
|
+
:return_code => packet.return_code
|
99
|
+
)
|
100
|
+
if packet.return_code == 0
|
101
|
+
logger.info("#{connection.client_id} is now connected")
|
102
|
+
else
|
103
|
+
logger.info("#{connection.client_id} failed to connect: #{packet.return_msg}")
|
104
|
+
end
|
105
|
+
when MQTT::Packet::Suback
|
106
|
+
# Check that it is a response to a request we made
|
107
|
+
request = connection.remove_from_pending(packet.id)
|
108
|
+
if request
|
109
|
+
logger.debug("#{connection.client_id} now subscribed to '#{request.topic_name}'")
|
110
|
+
topic_id_type, topic_id = connection.get_topic_id(request.topic_name)
|
111
|
+
mqttsn_packet = EventMachine::MQTTSN::Packet::Suback.new(
|
112
|
+
:topic_id_type => topic_id_type,
|
113
|
+
:topic_id => topic_id,
|
114
|
+
:qos => packet.granted_qos.first,
|
115
|
+
:id => packet.id,
|
116
|
+
:return_code => 0x00
|
117
|
+
)
|
118
|
+
else
|
119
|
+
logger.warn("Received Suback from server for something we didn't request: #{packet.inspect}")
|
120
|
+
end
|
121
|
+
when MQTT::Packet::Publish
|
122
|
+
logger.info("#{connection.client_id} recieved publish to '#{packet.topic}'")
|
123
|
+
# FIXME: send register if this is a new topic
|
124
|
+
topic_id_type, topic_id = connection.get_topic_id(packet.topic)
|
125
|
+
mqttsn_packet = EventMachine::MQTTSN::Packet::Publish.new(
|
126
|
+
:duplicate => packet.duplicate,
|
127
|
+
:qos => packet.qos,
|
128
|
+
:retain => packet.retain,
|
129
|
+
:topic_id_type => topic_id_type,
|
130
|
+
:topic_id => topic_id,
|
131
|
+
:id => packet.id,
|
132
|
+
:data => packet.payload
|
133
|
+
)
|
134
|
+
when MQTT::Packet::Pingreq
|
135
|
+
mqttsn_packet = EventMachine::MQTTSN::Packet::Pingreq.new
|
136
|
+
when MQTT::Packet::Pingresp
|
137
|
+
mqttsn_packet = EventMachine::MQTTSN::Packet::Pingresp.new
|
138
|
+
else
|
139
|
+
logger.warn("Unable to handle MQTT packet of type: #{packet.class}")
|
140
|
+
end
|
141
|
+
|
142
|
+
unless mqttsn_packet.nil?
|
143
|
+
send_datagram(mqttsn_packet.to_s, connection.client_address, connection.client_port)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# REGISTER received from client
|
148
|
+
def register(connection, packet)
|
149
|
+
regack = EventMachine::MQTTSN::Packet::Regack.new(
|
150
|
+
:topic_id_type => :normal,
|
151
|
+
:id => packet.id
|
152
|
+
)
|
153
|
+
|
154
|
+
topic_id_type, topic_id = connection.get_topic_id(packet.topic_name)
|
155
|
+
unless topic_id.nil?
|
156
|
+
regack.return_code = 0x00 # Accepted
|
157
|
+
regack.topic_id = topic_id
|
158
|
+
else
|
159
|
+
regack.return_code = 0x02 # Rejected: invalid topic ID
|
160
|
+
end
|
161
|
+
send_data(regack.to_s)
|
162
|
+
end
|
163
|
+
|
164
|
+
# PUBLISH received from client - pass it on to the server
|
165
|
+
def publish(connection, packet)
|
166
|
+
if packet.topic_id_type == :short
|
167
|
+
topic_name = packet.topic_id
|
168
|
+
elsif packet.topic_id_type == :normal
|
169
|
+
topic_name = connection.get_topic_name(packet.topic_id)
|
170
|
+
end
|
171
|
+
|
172
|
+
if topic_name
|
173
|
+
logger.info("#{connection.client_id} publishing to '#{topic_name}'")
|
174
|
+
connection.send_packet MQTT::Packet::Publish.new(
|
175
|
+
:topic => topic_name,
|
176
|
+
:payload => packet.data,
|
177
|
+
:retain => packet.retain,
|
178
|
+
:qos => packet.qos
|
179
|
+
)
|
180
|
+
else
|
181
|
+
# FIXME: disconnect?
|
182
|
+
logger.warn("Invalid topic ID: #{packet.topic_id}")
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# SUBSCRIBE received from client - pass it on to the server
|
187
|
+
def subscribe(connection, packet)
|
188
|
+
logger.info("#{connection.client_id} subscribing to '#{packet.topic_name}'")
|
189
|
+
mqtt_packet = MQTT::Packet::Subscribe.new(
|
190
|
+
:id => packet.id,
|
191
|
+
:topics => packet.topic_name
|
192
|
+
)
|
193
|
+
connection.add_to_pending(packet)
|
194
|
+
connection.send_packet(mqtt_packet)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Disconnect client from server
|
198
|
+
def disconnect(connection)
|
199
|
+
if connection.connected?
|
200
|
+
logger.info("Disconnected: #{connection.client_id}")
|
201
|
+
mqttsn_packet = EventMachine::MQTTSN::Packet::Disconnect.new
|
202
|
+
send_datagram(mqttsn_packet.to_s, connection.client_address, connection.client_port)
|
203
|
+
connection.disconnect
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Periodic task to cleanup dead connections
|
208
|
+
def cleanup
|
209
|
+
connections.each_pair do |key,connection|
|
210
|
+
unless connection.connected?
|
211
|
+
logger.debug("Destroying connection: #{connection.client_id}")
|
212
|
+
@connections.delete(key)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,348 @@
|
|
1
|
+
module EventMachine::MQTTSN
|
2
|
+
|
3
|
+
# Class representing a MQTTSN Packet
|
4
|
+
# Performs binary encoding and decoding of headers
|
5
|
+
class Packet
|
6
|
+
attr_accessor :duplicate # Duplicate delivery flag
|
7
|
+
attr_accessor :qos # Quality of Service level
|
8
|
+
attr_accessor :retain # Retain flag
|
9
|
+
attr_accessor :request_will # Request that gateway prompts for Will
|
10
|
+
attr_accessor :clean_session # When true, subscriptions are deleted after disconnect
|
11
|
+
attr_accessor :topic_id_type # One of :normal, :predefined or :short
|
12
|
+
|
13
|
+
DEFAULTS = {}
|
14
|
+
|
15
|
+
# Parse buffer into new packet object
|
16
|
+
def self.parse(buffer)
|
17
|
+
# Parse the fixed header (length and type)
|
18
|
+
length,type_id,body = buffer.unpack('CCa*')
|
19
|
+
if length == 1
|
20
|
+
length,type_id,body = buffer.unpack('xnCa*')
|
21
|
+
end
|
22
|
+
|
23
|
+
# Double-check the length
|
24
|
+
if buffer.length != length
|
25
|
+
raise ProtocolException.new("Length of packet is not the same as the length header")
|
26
|
+
end
|
27
|
+
|
28
|
+
packet_class = PACKET_TYPES[type_id]
|
29
|
+
if packet_class.nil?
|
30
|
+
raise ProtocolException.new("Invalid packet type identifier: #{type_id}")
|
31
|
+
end
|
32
|
+
|
33
|
+
# Create a new packet object
|
34
|
+
packet = packet_class.new
|
35
|
+
packet.parse_body(body)
|
36
|
+
|
37
|
+
return packet
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create a new empty packet
|
41
|
+
def initialize(args={})
|
42
|
+
update_attributes(self.class::DEFAULTS.merge(args))
|
43
|
+
end
|
44
|
+
|
45
|
+
def update_attributes(attr={})
|
46
|
+
attr.each_pair do |k,v|
|
47
|
+
send("#{k}=", v)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Get the identifer for this packet type
|
52
|
+
def type_id
|
53
|
+
PACKET_TYPES.each_pair do |key, value|
|
54
|
+
return key if self.class == value
|
55
|
+
end
|
56
|
+
raise "Invalid packet type: #{self.class}"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Serialise the packet
|
60
|
+
def to_s
|
61
|
+
# Get the packet's variable header and payload
|
62
|
+
body = self.encode_body
|
63
|
+
|
64
|
+
# Build up the body length field bytes
|
65
|
+
body_length = body.length
|
66
|
+
if body_length > 65531
|
67
|
+
raise "Packet too big"
|
68
|
+
elsif body_length > 253
|
69
|
+
[0x01, body_length + 4, type_id].pack('CnC') + body
|
70
|
+
else
|
71
|
+
[body_length + 2, type_id].pack('CC') + body
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def parse_body(buffer)
|
76
|
+
end
|
77
|
+
|
78
|
+
protected
|
79
|
+
|
80
|
+
def parse_flags(flags)
|
81
|
+
self.duplicate = ((flags & 0x80) >> 7) == 0x01
|
82
|
+
self.qos = (flags & 0x60) >> 5
|
83
|
+
self.qos = -1 if self.qos == 3
|
84
|
+
self.retain = ((flags & 0x10) >> 4) == 0x01
|
85
|
+
self.request_will = ((flags & 0x08) >> 3) == 0x01
|
86
|
+
self.clean_session = ((flags & 0x04) >> 2) == 0x01
|
87
|
+
case (flags & 0x03)
|
88
|
+
when 0x0
|
89
|
+
self.topic_id_type = :normal
|
90
|
+
when 0x1
|
91
|
+
self.topic_id_type = :predefined
|
92
|
+
when 0x2
|
93
|
+
self.topic_id_type = :short
|
94
|
+
else
|
95
|
+
self.topic_id_type = nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Get serialisation of packet's body (variable header and payload)
|
100
|
+
def encode_body
|
101
|
+
'' # No body by default
|
102
|
+
end
|
103
|
+
|
104
|
+
def encode_flags
|
105
|
+
flags = 0x00
|
106
|
+
flags += 0x80 if duplicate
|
107
|
+
flags += 0x10 if retain
|
108
|
+
flags += 0x08 if request_will
|
109
|
+
flags += 0x04 if clean_session
|
110
|
+
case topic_id_type
|
111
|
+
when :normal
|
112
|
+
flags += 0x0
|
113
|
+
when :predefined
|
114
|
+
flags += 0x1
|
115
|
+
when :short
|
116
|
+
flags += 0x2
|
117
|
+
end
|
118
|
+
return flags
|
119
|
+
end
|
120
|
+
|
121
|
+
def encode_topic_id
|
122
|
+
if topic_id_type == :short
|
123
|
+
(topic_id[0].ord << 8) + topic_id[1].ord
|
124
|
+
else
|
125
|
+
topic_id
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def parse_topic_id(topic_id)
|
130
|
+
if topic_id_type == :short
|
131
|
+
int = topic_id.to_i
|
132
|
+
self.topic_id = [(int >> 8) & 0xFF, int & 0xFF].pack('CC')
|
133
|
+
else
|
134
|
+
self.topic_id = topic_id
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class Connect < Packet
|
139
|
+
attr_accessor :keep_alive
|
140
|
+
attr_accessor :client_id
|
141
|
+
|
142
|
+
DEFAULTS = {
|
143
|
+
:request_will => false,
|
144
|
+
:clean_session => true,
|
145
|
+
:keep_alive => 15
|
146
|
+
}
|
147
|
+
|
148
|
+
# Get serialisation of packet's body
|
149
|
+
def encode_body
|
150
|
+
if @client_id.nil? or @client_id.length < 1 or @client_id.length > 23
|
151
|
+
raise "Invalid client identifier when serialising packet"
|
152
|
+
end
|
153
|
+
|
154
|
+
[encode_flags, 0x01, keep_alive, client_id].pack('CCna*')
|
155
|
+
end
|
156
|
+
|
157
|
+
def parse_body(buffer)
|
158
|
+
flags, protocol_id, self.keep_alive, self.client_id = buffer.unpack('CCna*')
|
159
|
+
|
160
|
+
if protocol_id != 0x01
|
161
|
+
raise ProtocolException.new("Unsupported protocol ID number: #{protocol_id}")
|
162
|
+
end
|
163
|
+
|
164
|
+
parse_flags(flags)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
class Connack < Packet
|
169
|
+
attr_accessor :return_code
|
170
|
+
|
171
|
+
# Get a string message corresponding to a return code
|
172
|
+
def return_msg
|
173
|
+
case return_code
|
174
|
+
when 0x00
|
175
|
+
"Accepted"
|
176
|
+
when 0x01
|
177
|
+
"Rejected: congestion"
|
178
|
+
when 0x02
|
179
|
+
"Rejected: invalid topic ID"
|
180
|
+
when 0x03
|
181
|
+
"Rejected: not supported"
|
182
|
+
else
|
183
|
+
"Rejected: error code #{return_code}"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def encode_body
|
188
|
+
[return_code].pack('C')
|
189
|
+
end
|
190
|
+
|
191
|
+
def parse_body(buffer)
|
192
|
+
self.return_code = buffer.unpack('C')[0]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
class Register < Packet
|
197
|
+
attr_accessor :id
|
198
|
+
attr_accessor :topic_id
|
199
|
+
attr_accessor :topic_name
|
200
|
+
|
201
|
+
DEFAULTS = {
|
202
|
+
:id => 0x00,
|
203
|
+
:topic_id_type => :normal
|
204
|
+
}
|
205
|
+
|
206
|
+
def encode_body
|
207
|
+
[encode_topic_id, id, topic_name].pack('nna*')
|
208
|
+
end
|
209
|
+
|
210
|
+
def parse_body(buffer)
|
211
|
+
topic_id, self.id, self.topic_name = buffer.unpack('nna*')
|
212
|
+
parse_topic_id(topic_id)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
class Regack < Packet
|
217
|
+
attr_accessor :id
|
218
|
+
attr_accessor :topic_id
|
219
|
+
attr_accessor :return_code
|
220
|
+
|
221
|
+
DEFAULTS = {
|
222
|
+
:id => 0x00,
|
223
|
+
:topic_id_type => :normal
|
224
|
+
}
|
225
|
+
|
226
|
+
def encode_body
|
227
|
+
[encode_topic_id, id, return_code].pack('nnC')
|
228
|
+
end
|
229
|
+
|
230
|
+
def parse_body(buffer)
|
231
|
+
topic_id, self.id, self.return_code = buffer.unpack('nnC')
|
232
|
+
parse_topic_id(topic_id)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
class Publish < Packet
|
237
|
+
attr_accessor :topic_id
|
238
|
+
attr_accessor :id
|
239
|
+
attr_accessor :data
|
240
|
+
|
241
|
+
DEFAULTS = {
|
242
|
+
:id => 0x00,
|
243
|
+
:duplicate => false,
|
244
|
+
:qos => 0,
|
245
|
+
:retain => false,
|
246
|
+
:topic_id_type => :normal
|
247
|
+
}
|
248
|
+
|
249
|
+
def encode_body
|
250
|
+
[encode_flags, encode_topic_id, id, data].pack('Cnna*')
|
251
|
+
end
|
252
|
+
|
253
|
+
def parse_body(buffer)
|
254
|
+
flags, topic_id, self.id, self.data = buffer.unpack('Cnna*')
|
255
|
+
parse_flags(flags)
|
256
|
+
parse_topic_id(topic_id)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
class Subscribe < Packet
|
261
|
+
attr_accessor :id
|
262
|
+
attr_accessor :topic_id
|
263
|
+
attr_accessor :topic_name
|
264
|
+
|
265
|
+
DEFAULTS = {
|
266
|
+
:id => 0x00,
|
267
|
+
:topic_id_type => :normal
|
268
|
+
}
|
269
|
+
|
270
|
+
def encode_body
|
271
|
+
[encode_flags, id, topic_name].pack('Cna*')
|
272
|
+
end
|
273
|
+
|
274
|
+
def parse_body(buffer)
|
275
|
+
flags, self.id, self.topic_name = buffer.unpack('Cna*')
|
276
|
+
parse_flags(flags)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
class Suback < Packet
|
281
|
+
attr_accessor :id
|
282
|
+
attr_accessor :topic_id
|
283
|
+
attr_accessor :return_code
|
284
|
+
|
285
|
+
DEFAULTS = {
|
286
|
+
:qos => 0,
|
287
|
+
:id => 0x00,
|
288
|
+
:topic_id_type => :normal
|
289
|
+
}
|
290
|
+
|
291
|
+
def encode_body
|
292
|
+
[encode_flags, encode_topic_id, id, return_code].pack('CnnC')
|
293
|
+
end
|
294
|
+
|
295
|
+
def parse_body(buffer)
|
296
|
+
flags, topic_id, self.id, self.return_code = buffer.unpack('CnnC')
|
297
|
+
parse_flags(flags)
|
298
|
+
parse_topic_id(topic_id)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
class Pingreq < Packet
|
303
|
+
# No attributes
|
304
|
+
end
|
305
|
+
|
306
|
+
class Pingresp < Packet
|
307
|
+
# No attributes
|
308
|
+
end
|
309
|
+
|
310
|
+
class Disconnect < Packet
|
311
|
+
# No attributes
|
312
|
+
end
|
313
|
+
|
314
|
+
end
|
315
|
+
|
316
|
+
|
317
|
+
# An enumeration of the MQTT-SN packet types
|
318
|
+
PACKET_TYPES = {
|
319
|
+
# 0x00 => EventMachine::MQTTSN::Packet::Advertise,
|
320
|
+
# 0x01 => EventMachine::MQTTSN::Packet::Searchgw,
|
321
|
+
# 0x02 => EventMachine::MQTTSN::Packet::Gwinfo,
|
322
|
+
0x04 => EventMachine::MQTTSN::Packet::Connect,
|
323
|
+
0x05 => EventMachine::MQTTSN::Packet::Connack,
|
324
|
+
# 0x06 => EventMachine::MQTTSN::Packet::Willtopicreq,
|
325
|
+
# 0x07 => EventMachine::MQTTSN::Packet::Willtopic,
|
326
|
+
# 0x08 => EventMachine::MQTTSN::Packet::Willmsgreq,
|
327
|
+
# 0x09 => EventMachine::MQTTSN::Packet::Willmsg,
|
328
|
+
0x0a => EventMachine::MQTTSN::Packet::Register,
|
329
|
+
0x0b => EventMachine::MQTTSN::Packet::Regack,
|
330
|
+
0x0c => EventMachine::MQTTSN::Packet::Publish,
|
331
|
+
# 0x0d => EventMachine::MQTTSN::Packet::Puback,
|
332
|
+
# 0x0e => EventMachine::MQTTSN::Packet::Pubcomp,
|
333
|
+
# 0x0f => EventMachine::MQTTSN::Packet::Pubrec,
|
334
|
+
# 0x10 => EventMachine::MQTTSN::Packet::Pubrel,
|
335
|
+
0x12 => EventMachine::MQTTSN::Packet::Subscribe,
|
336
|
+
0x13 => EventMachine::MQTTSN::Packet::Suback,
|
337
|
+
# 0x14 => EventMachine::MQTTSN::Packet::Unsubscribe,
|
338
|
+
# 0x15 => EventMachine::MQTTSN::Packet::Unsuback,
|
339
|
+
0x16 => EventMachine::MQTTSN::Packet::Pingreq,
|
340
|
+
0x17 => EventMachine::MQTTSN::Packet::Pingresp,
|
341
|
+
0x18 => EventMachine::MQTTSN::Packet::Disconnect,
|
342
|
+
# 0x1a => EventMachine::MQTTSN::Packet::Willtopicupd,
|
343
|
+
# 0x1b => EventMachine::MQTTSN::Packet::Willtopicresp,
|
344
|
+
# 0x1c => EventMachine::MQTTSN::Packet::Willmsgupd,
|
345
|
+
# 0x1d => EventMachine::MQTTSN::Packet::Willmsgresp,
|
346
|
+
}
|
347
|
+
|
348
|
+
end
|