mqtt_pipe 0.0.1
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/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +43 -0
- data/Rakefile +2 -0
- data/examples/receiver.rb +9 -0
- data/examples/sender.rb +13 -0
- data/lib/mqtt_pipe.rb +16 -0
- data/lib/mqtt_pipe/config.rb +31 -0
- data/lib/mqtt_pipe/listener.rb +57 -0
- data/lib/mqtt_pipe/packer.rb +83 -0
- data/lib/mqtt_pipe/pipe.rb +88 -0
- data/lib/mqtt_pipe/types.rb +11 -0
- data/lib/mqtt_pipe/types/array.rb +36 -0
- data/lib/mqtt_pipe/types/class.rb +9 -0
- data/lib/mqtt_pipe/types/color.rb +17 -0
- data/lib/mqtt_pipe/types/false.rb +17 -0
- data/lib/mqtt_pipe/types/float.rb +17 -0
- data/lib/mqtt_pipe/types/integer.rb +39 -0
- data/lib/mqtt_pipe/types/nil.rb +17 -0
- data/lib/mqtt_pipe/types/string.rb +30 -0
- data/lib/mqtt_pipe/types/time.rb +17 -0
- data/lib/mqtt_pipe/types/true.rb +17 -0
- data/lib/mqtt_pipe/types/type.rb +36 -0
- data/lib/mqtt_pipe/version.rb +3 -0
- data/mqtt_pipe.gemspec +26 -0
- data/spec/config_spec.rb +27 -0
- data/spec/listener_spec.rb +87 -0
- data/spec/packer_spec.rb +264 -0
- data/spec/pipe_spec.rb +9 -0
- metadata +135 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 93caadb951ff60325a712d56794e4431adbcf621
|
4
|
+
data.tar.gz: b9a8f6b3b8a31d2925601444bad95bb054e00c5e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bf62b6c097b52d0770b64b5bf63dc299a399120a525eb4682b03a92f74d74ce3734fe6221c6008c9620774d4c3c28e009b4196d1a0492f822886fbe83b53e645
|
7
|
+
data.tar.gz: 9e8cf7c06b4e8a625bff2944337bbf90bb8b180f16e898d1127417a47fc34641f030baec5fb0f0e860023eed030f268bfde664a318ed4de36695843efc30e292
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Sebastian Lindberg
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# MQTTPipe
|
2
|
+
|
3
|
+
This gem wraps the [MQTT gem](https://github.com/njh/ruby-mqtt) and adds a serializer for simple data structures.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'mqtt_pipe'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install mqtt_pipe
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
pipe = MQTTPipe.create do
|
25
|
+
on 'hello/world/#' do |message, id|
|
26
|
+
p message, id
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
pipe.open 'test.mosquitto.org', port: 1883 do
|
31
|
+
100.times do |i|
|
32
|
+
send "hello/world/#{i}", Time.now
|
33
|
+
end
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
## Contributing
|
38
|
+
|
39
|
+
1. Fork it ( https://github.com/[my-github-username]/mqtt_pipe/fork )
|
40
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
41
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
42
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
43
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/examples/sender.rb
ADDED
data/lib/mqtt_pipe.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'mqtt'
|
2
|
+
|
3
|
+
require 'mqtt_pipe/version'
|
4
|
+
require 'mqtt_pipe/types'
|
5
|
+
require 'mqtt_pipe/packer'
|
6
|
+
require 'mqtt_pipe/config'
|
7
|
+
require 'mqtt_pipe/listener'
|
8
|
+
require 'mqtt_pipe/pipe'
|
9
|
+
|
10
|
+
module MQTTPipe
|
11
|
+
extend self
|
12
|
+
|
13
|
+
def create &block
|
14
|
+
Pipe.new &block
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module MQTTPipe
|
2
|
+
|
3
|
+
##
|
4
|
+
# An instance of Config is used as the context in which
|
5
|
+
# the pipe is configured.
|
6
|
+
|
7
|
+
class Config
|
8
|
+
attr_reader :listeners
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@listeners = []
|
12
|
+
end
|
13
|
+
|
14
|
+
##
|
15
|
+
# Subscribe to a topic and attatch an action that will
|
16
|
+
# be called once a message with a matching topic is
|
17
|
+
# received.
|
18
|
+
|
19
|
+
def on topic, &action
|
20
|
+
raise ArgumentError, 'No block given' if action.nil?
|
21
|
+
@listeners << Listener.new(topic, &action)
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Subscribe to all topics
|
26
|
+
|
27
|
+
def on_anything &action
|
28
|
+
on '#', &action
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module MQTTPipe
|
2
|
+
|
3
|
+
##
|
4
|
+
# Used to store topics along with their actions.
|
5
|
+
# Contains conveniens methods for matching the topic to a
|
6
|
+
# given string as well as calling the action.
|
7
|
+
|
8
|
+
class Listener
|
9
|
+
attr_reader :topic, :pattern
|
10
|
+
|
11
|
+
##
|
12
|
+
# The listener requires a topic string and a callable
|
13
|
+
# action to initialize.
|
14
|
+
#
|
15
|
+
# An ArgumentError is raised if no action is given
|
16
|
+
|
17
|
+
def initialize topic, &action
|
18
|
+
raise ArgumentError, 'No block given' if action.nil?
|
19
|
+
|
20
|
+
@topic = topic
|
21
|
+
@action = action
|
22
|
+
|
23
|
+
pattern = topic.gsub('*', '([^/]+)').gsub('/#', '/?(.*)')
|
24
|
+
@pattern = %r{^#{pattern}$}
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Check if a given topic string matches the listener
|
29
|
+
# topic.
|
30
|
+
#
|
31
|
+
# Returns an array containing any matched sections of
|
32
|
+
# topic, if there was a match. False otherwise.
|
33
|
+
|
34
|
+
def match topic
|
35
|
+
m = @pattern.match topic
|
36
|
+
m.nil? ? false : m.captures
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Returns true if the topic matches listener topic
|
41
|
+
# Otherwise false.
|
42
|
+
|
43
|
+
def === topic
|
44
|
+
@pattern === topic
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Call the listener action
|
49
|
+
|
50
|
+
def call *args
|
51
|
+
#raise ArgumentError, 'No value provided' if args.empty?
|
52
|
+
@action.call *args
|
53
|
+
end
|
54
|
+
|
55
|
+
alias_method :run, :call
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module MQTTPipe
|
2
|
+
|
3
|
+
##
|
4
|
+
# The packer module is used to pack/unpack classes that
|
5
|
+
# supports it.
|
6
|
+
|
7
|
+
module Packer
|
8
|
+
extend self
|
9
|
+
|
10
|
+
##
|
11
|
+
# Raised when the packet being unpacked is badly
|
12
|
+
# formatted.
|
13
|
+
|
14
|
+
class FormatError < StandardError; end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Used to signal the end of a packet as it is being
|
18
|
+
# unpacked.
|
19
|
+
|
20
|
+
class EndOfPacket < StandardError; end
|
21
|
+
|
22
|
+
# Use the refinements made to the supported classes
|
23
|
+
|
24
|
+
using Types
|
25
|
+
|
26
|
+
##
|
27
|
+
# Packs the arguments acording to their type.
|
28
|
+
#
|
29
|
+
# An ArgumentError is raised if any given class does
|
30
|
+
# not support packing.
|
31
|
+
|
32
|
+
def pack *values
|
33
|
+
values.map{|value| value.to_packed }.join
|
34
|
+
rescue NoMethodError
|
35
|
+
raise ArgumentError, 'Unknown input format'
|
36
|
+
end
|
37
|
+
|
38
|
+
alias_method :[], :pack
|
39
|
+
|
40
|
+
|
41
|
+
##
|
42
|
+
# Unpacks a serialized object and returns an array of
|
43
|
+
# the original values.
|
44
|
+
|
45
|
+
def unpack raw, limit: nil
|
46
|
+
raw = StringIO.new raw unless raw.respond_to? :read
|
47
|
+
result = []
|
48
|
+
|
49
|
+
# Either loop infinately or the number of times
|
50
|
+
# specified by limit
|
51
|
+
|
52
|
+
(limit.nil? ? loop : limit.times).each do
|
53
|
+
result << unpack_single(raw)
|
54
|
+
end
|
55
|
+
|
56
|
+
return result
|
57
|
+
rescue EndOfPacket
|
58
|
+
return result
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# A simple helper method to read a given number of bytes
|
63
|
+
# +from+ IO object and format them +as+ anything
|
64
|
+
# supported by Array#unpack.
|
65
|
+
|
66
|
+
def read_packed_bytes n = 1, from:, as: 'C'
|
67
|
+
raw = from.read(n)
|
68
|
+
raise FormatError if raw.nil? or raw.length != n
|
69
|
+
|
70
|
+
raw.unpack(as).first
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def unpack_single raw
|
76
|
+
code = raw.read 1
|
77
|
+
raise EndOfPacket if code.nil?
|
78
|
+
|
79
|
+
type = code.unpack(?C).first
|
80
|
+
Types::Type.lookup(type).from_packed type, raw
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module MQTTPipe
|
2
|
+
|
3
|
+
##
|
4
|
+
# The actual wrapper class for MQTT
|
5
|
+
|
6
|
+
class Pipe
|
7
|
+
|
8
|
+
##
|
9
|
+
# Raised when the connection unexpectedly lost.
|
10
|
+
|
11
|
+
class ConnectionError < StandardError; end
|
12
|
+
|
13
|
+
|
14
|
+
def initialize &block
|
15
|
+
@config = Config.new
|
16
|
+
@config.instance_eval &block unless block.nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Open the pipe
|
21
|
+
|
22
|
+
def open host, port: 1883, &block
|
23
|
+
MQTT::Client.connect host: host, port: port do |client|
|
24
|
+
|
25
|
+
# Subscribe
|
26
|
+
topics = @config.listeners.map{|listener| listener.topic }
|
27
|
+
listener_thread = nil
|
28
|
+
|
29
|
+
unless topics.empty?
|
30
|
+
listener_thread = Thread.new do
|
31
|
+
client.get do |topic, data|
|
32
|
+
begin
|
33
|
+
unpacked_data = Packer.unpack data
|
34
|
+
|
35
|
+
@config.listeners.each do |listener|
|
36
|
+
if m = listener.match(topic)
|
37
|
+
listener.call unpacked_data, *m
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
rescue Packer::FormatError
|
42
|
+
# TODO: Handle more gracefully
|
43
|
+
puts 'Could not parse data!'
|
44
|
+
next
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
client.subscribe *topics
|
50
|
+
end
|
51
|
+
|
52
|
+
unless block.nil?
|
53
|
+
context = Context.new client
|
54
|
+
|
55
|
+
begin
|
56
|
+
context.instance_eval &block
|
57
|
+
rescue ConnectionError
|
58
|
+
|
59
|
+
puts 'Need to reconnect'
|
60
|
+
rescue Interrupt
|
61
|
+
ensure
|
62
|
+
listener_thread.exit unless topics.empty?
|
63
|
+
end
|
64
|
+
|
65
|
+
else
|
66
|
+
begin
|
67
|
+
listener_thread.join unless topics.empty?
|
68
|
+
rescue Interrupt
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
class Context
|
77
|
+
def initialize client
|
78
|
+
@client = client
|
79
|
+
end
|
80
|
+
|
81
|
+
def send topic, *data
|
82
|
+
raise ConnectionError unless @client.connected?
|
83
|
+
@client.publish topic, Packer.pack(*data)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require_relative 'types/string'
|
2
|
+
require_relative 'types/nil'
|
3
|
+
require_relative 'types/false'
|
4
|
+
require_relative 'types/true'
|
5
|
+
require_relative 'types/integer'
|
6
|
+
require_relative 'types/float'
|
7
|
+
require_relative 'types/time'
|
8
|
+
require_relative 'types/color'
|
9
|
+
require_relative 'types/array'
|
10
|
+
require_relative 'types/class'
|
11
|
+
require_relative 'types/type'
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module MQTTPipe
|
2
|
+
module Types
|
3
|
+
refine Array.singleton_class do
|
4
|
+
def packer_code; 0x80; end
|
5
|
+
|
6
|
+
def from_packed type, raw
|
7
|
+
length = if type == packer_code
|
8
|
+
Packer.read_packed_bytes(1, from: raw) + 31
|
9
|
+
else
|
10
|
+
type - packer_code
|
11
|
+
end
|
12
|
+
|
13
|
+
array = Packer.unpack raw, limit: length
|
14
|
+
raise Packer::FormatError, 'Badly formatted array' unless array.length == length
|
15
|
+
|
16
|
+
return array
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
refine Array do
|
21
|
+
def to_packed
|
22
|
+
header = case length
|
23
|
+
when 0 then return nil.to_packed
|
24
|
+
when 1..31
|
25
|
+
[self.class.packer_code + length].pack(?C)
|
26
|
+
when 32..288
|
27
|
+
[self.class.packer_code, length - 31].pack('C2')
|
28
|
+
else
|
29
|
+
raise ArgumentError, 'Array is too long'
|
30
|
+
end
|
31
|
+
|
32
|
+
header + map{|v| v.to_packed }.join
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module MQTTPipe
|
2
|
+
module Types
|
3
|
+
refine FalseClass.singleton_class do
|
4
|
+
def packer_code; 0xC2; end
|
5
|
+
|
6
|
+
def from_packed type, _
|
7
|
+
type == packer_code ? false : true
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
refine FalseClass do
|
12
|
+
def to_packed
|
13
|
+
[self.class.packer_code].pack ?C
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module MQTTPipe
|
2
|
+
module Types
|
3
|
+
refine Float.singleton_class do
|
4
|
+
def packer_code; 0xC7; end
|
5
|
+
|
6
|
+
def from_packed _, raw
|
7
|
+
Packer.read_packed_bytes 4, from: raw, as: 'e'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
refine Float do
|
12
|
+
def to_packed
|
13
|
+
[self.class.packer_code, self].pack 'Ce'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module MQTTPipe
|
2
|
+
module Types
|
3
|
+
refine Integer.singleton_class do
|
4
|
+
def packer_code; 0xC4; end
|
5
|
+
|
6
|
+
def from_packed type, raw
|
7
|
+
case type
|
8
|
+
when 0xC4
|
9
|
+
Packer.read_packed_bytes 1, from: raw
|
10
|
+
when 0xC5
|
11
|
+
Packer.read_packed_bytes 2, from: raw, as: 's<'
|
12
|
+
when 0xC6
|
13
|
+
Packer.read_packed_bytes 4, from: raw, as: 'l<'
|
14
|
+
when 0..0x7F, 0xD0..0xFF
|
15
|
+
[type].pack('C').unpack('c').first
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
refine Integer do
|
21
|
+
def to_packed
|
22
|
+
case self
|
23
|
+
when -48..127
|
24
|
+
[self].pack ?C
|
25
|
+
when 0..255
|
26
|
+
[self.class.packer_code, self].pack 'C2'
|
27
|
+
when -32_768..32_767
|
28
|
+
[self.class.packer_code + 1, self].pack 'Cs<'
|
29
|
+
when -2_147_483_648..2_147_483_647
|
30
|
+
[self.class.packer_code + 2, self].pack 'Cl<'
|
31
|
+
else
|
32
|
+
raise ArgumentError, 'Integer is larger than 32 bit signed'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module MQTTPipe
|
2
|
+
module Types
|
3
|
+
refine NilClass.singleton_class do
|
4
|
+
def packer_code; 0xC1; end
|
5
|
+
|
6
|
+
def from_packed _, _
|
7
|
+
return nil
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
refine NilClass do
|
12
|
+
def to_packed
|
13
|
+
[self.class.packer_code].pack ?C
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module MQTTPipe
|
2
|
+
module Types
|
3
|
+
refine String.singleton_class do
|
4
|
+
def packer_code; 0xA0; end
|
5
|
+
|
6
|
+
def from_packed type, raw
|
7
|
+
length = if type == packer_code
|
8
|
+
Packer.read_packed_bytes(1, from: raw) + 31
|
9
|
+
else
|
10
|
+
type - packer_code
|
11
|
+
end
|
12
|
+
Packer.read_packed_bytes length, from: raw, as: 'A*'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
refine String do
|
17
|
+
def to_packed
|
18
|
+
case length
|
19
|
+
when 0 then return nil.to_packed
|
20
|
+
when 1..31
|
21
|
+
[self.class.packer_code + length, self].pack('CA*')
|
22
|
+
when 32..288
|
23
|
+
[self.class.packer_code, length - 31, self].pack('C2A*')
|
24
|
+
else
|
25
|
+
raise ArgumentError, 'String is too long'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module MQTTPipe
|
2
|
+
module Types
|
3
|
+
refine Time.singleton_class do
|
4
|
+
def packer_code; 0xC8; end
|
5
|
+
|
6
|
+
def from_packed _, raw
|
7
|
+
at(Packer.read_packed_bytes 4, from: raw, as: 'L<')
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
refine Time do
|
12
|
+
def to_packed
|
13
|
+
[self.class.packer_code, to_i].pack 'CL<'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module MQTTPipe
|
2
|
+
module Types
|
3
|
+
refine TrueClass.singleton_class do
|
4
|
+
def packer_code; 0xC2; end
|
5
|
+
|
6
|
+
def from_packed type, _
|
7
|
+
type == packer_code ? false : true
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
refine TrueClass do
|
12
|
+
def to_packed
|
13
|
+
[self.class.packer_code + 1].pack ?C
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module MQTTPipe
|
2
|
+
module Types
|
3
|
+
module Type
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def packer_code
|
7
|
+
0xC0
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_packed
|
11
|
+
[packer_code, packer_code].pack 'C2'
|
12
|
+
end
|
13
|
+
|
14
|
+
def lookup type
|
15
|
+
case type
|
16
|
+
when 0x80..0x9F then Array
|
17
|
+
when 0xA0..0xBF then String
|
18
|
+
when 0xC0 then Type
|
19
|
+
when 0xC1 then NilClass
|
20
|
+
when 0xC2 then FalseClass
|
21
|
+
when 0xC3 then TrueClass
|
22
|
+
when 0xC7 then Float
|
23
|
+
when 0xC8 then Time
|
24
|
+
when 0xC9 then Color
|
25
|
+
when 0x00..0x7F,
|
26
|
+
0xD0..0xFF,
|
27
|
+
0xC4..0xC6 then Integer
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def from_packed _, raw
|
32
|
+
lookup(Packer.read_packed_bytes 1, from: raw)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/mqtt_pipe.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'mqtt_pipe/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "mqtt_pipe"
|
8
|
+
spec.version = MQTTPipe::VERSION
|
9
|
+
spec.authors = ["Sebastian Lindberg"]
|
10
|
+
spec.email = ["seb.lindberg@gmail.com"]
|
11
|
+
spec.summary = %q{A gem for sending a small set of objects via MQTT.}
|
12
|
+
spec.description = %q{This gem wraps the MQTT gem by njh (on Github) and adds a serializer for simple data structures.}
|
13
|
+
spec.homepage = "https://github.com/seblindberg/ruby-mqtt-pipe"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "mqtt", "~> 0.3"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "rspec", "~> 3.3"
|
26
|
+
end
|
data/spec/config_spec.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'mqtt_pipe'
|
2
|
+
|
3
|
+
describe MQTTPipe::Config do
|
4
|
+
let(:klass) { MQTTPipe::Config }
|
5
|
+
|
6
|
+
before :each do
|
7
|
+
@config = klass.new
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '#on' do
|
11
|
+
it 'requires one argument and a block' do
|
12
|
+
expect{@config.on}.to raise_error(ArgumentError)
|
13
|
+
expect{@config.on 'test'}.to raise_error(ArgumentError)
|
14
|
+
expect{@config.on('test') {}}.not_to raise_error
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'stores the listener' do
|
18
|
+
expect(@config.listeners.length).to eq(0)
|
19
|
+
|
20
|
+
@config.on('test') {}
|
21
|
+
|
22
|
+
expect(@config.listeners.length).to eq(1)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'mqtt_pipe'
|
2
|
+
|
3
|
+
describe MQTTPipe::Listener do
|
4
|
+
let(:klass) { MQTTPipe::Listener }
|
5
|
+
let(:topic) { 'test/*/topic/#' }
|
6
|
+
let(:matching_topic_1) { 'test/some/topic' }
|
7
|
+
let(:matching_topic_2) { 'test/some/topic/with/more/5' }
|
8
|
+
let(:non_matching_topic) { 'test/topic/with/8' }
|
9
|
+
|
10
|
+
describe '#new' do
|
11
|
+
it 'expects a topic and an action block' do
|
12
|
+
expect{klass.new}.to raise_error(ArgumentError)
|
13
|
+
expect{klass.new topic}.to raise_error(ArgumentError)
|
14
|
+
expect{klass.new(topic) {}}.not_to raise_error
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'Using the listener' do
|
19
|
+
before :each do
|
20
|
+
@listener = klass.new(topic) {|value, captures = 2| value * captures.to_i }
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '#topic' do
|
24
|
+
it 'returns the topic' do
|
25
|
+
expect(@listener.topic).to eq topic
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#pattern' do
|
30
|
+
it 'returns a regular expression mathing the pattern' do
|
31
|
+
pattern = @listener.pattern
|
32
|
+
|
33
|
+
expect(pattern).to be_a(Regexp)
|
34
|
+
expect(pattern === topic).to be true
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'leaves lopics without wildcards as is' do
|
38
|
+
listener = klass.new('test/of') {}
|
39
|
+
expect(listener.pattern === 'test/of').to be_truthy
|
40
|
+
expect(listener.pattern).to eq %r{^test/of$}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#match' do
|
45
|
+
it 'requires one argument' do
|
46
|
+
expect{@listener.match}.to raise_error ArgumentError
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'returns true for a matching topic' do
|
50
|
+
expect(@listener.match matching_topic_1).to be_truthy
|
51
|
+
expect(@listener.match matching_topic_2).to be_truthy
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'returns nil for a non matching topic' do
|
55
|
+
expect(@listener.match non_matching_topic).to be false
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'also responds to #===' do
|
59
|
+
expect(@listener === matching_topic_1).to be true
|
60
|
+
expect(@listener === non_matching_topic).to be false
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'captures wildcard match groups' do
|
64
|
+
m = @listener.match matching_topic_2
|
65
|
+
|
66
|
+
expect(m).not_to be false
|
67
|
+
expect(m[0]).to eq 'some'
|
68
|
+
expect(m[1]).to eq 'with/more/5'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#call' do
|
73
|
+
it 'runs the given callback' do
|
74
|
+
expect(@listener.call 42).to eq 84
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'accepts an optional second argument' do
|
78
|
+
expect(@listener.call 12, 3).to eq 12*3
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'also responds to #run' do
|
82
|
+
expect(@listener.run 42).to eq 84
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
data/spec/packer_spec.rb
ADDED
@@ -0,0 +1,264 @@
|
|
1
|
+
require 'mqtt_pipe'
|
2
|
+
|
3
|
+
describe MQTTPipe::Packer do
|
4
|
+
let(:klass) { MQTTPipe::Packer }
|
5
|
+
|
6
|
+
describe '#pack/#[]' do
|
7
|
+
describe 'Array' do
|
8
|
+
it 'serializes empty arrays to nil' do
|
9
|
+
expect(klass[[]]).to eq [0xC1].pack('C')
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'serializes short arrays' do
|
13
|
+
arr = (0..4).to_a
|
14
|
+
expect(klass[arr]).to eq [0x85, 0, 1, 2, 3, 4].pack('C*')
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'serializes long arrays' do
|
18
|
+
arr = (1..100).to_a
|
19
|
+
expect(klass[arr].length).to be 102
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'String' do
|
24
|
+
it 'serializes empty strings to nil' do
|
25
|
+
expect(klass['']).to eq [0xC1].pack('C')
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'serializes short strings' do
|
29
|
+
expect{klass['string']}.not_to raise_error
|
30
|
+
expect(klass['string']).to eq [0xA6, 'string'].pack('CA*')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'serializes long strings' do
|
34
|
+
long_string = '*' * 33
|
35
|
+
expect(klass[long_string]).to eq [0xA0, long_string.length - 31, long_string].pack('C2A*')
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'does not serialize string longer than 288 chars' do
|
39
|
+
too_long_string = '*' * 289
|
40
|
+
expect{klass[too_long_string]}.to raise_error ArgumentError
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'Type' do
|
45
|
+
it 'serializes classes' do
|
46
|
+
expect(klass[MQTTPipe::Types::Type]).to eq [0xC0, 0xC0].pack('C2')
|
47
|
+
|
48
|
+
expect(klass[NilClass]).to eq [0xC0, 0xC1].pack('C2')
|
49
|
+
|
50
|
+
expect(klass[FalseClass]).to eq [0xC0, 0xC2].pack('C2')
|
51
|
+
expect(klass[TrueClass]).to eq [0xC0, 0xC2].pack('C2')
|
52
|
+
|
53
|
+
expect(klass[Array]).to eq [0xC0, 0x80].pack('C2')
|
54
|
+
expect(klass[String]).to eq [0xC0, 0xA0].pack('C2')
|
55
|
+
|
56
|
+
expect(klass[Fixnum]).to eq [0xC0, 0xC4].pack('C2')
|
57
|
+
expect(klass[Integer]).to eq [0xC0, 0xC4].pack('C2')
|
58
|
+
|
59
|
+
expect(klass[Float]).to eq [0xC0, 0xC7].pack('C2')
|
60
|
+
expect(klass[Time]).to eq [0xC0, 0xC8].pack('C2')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe 'Nil' do
|
65
|
+
it 'serializes nil values' do
|
66
|
+
expect(klass[nil]).to eq [0xC1].pack(?C)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe 'Boolean' do
|
71
|
+
it 'serializes false values' do
|
72
|
+
expect(klass[false]).to eq [0xC2].pack(?C)
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'serializes true values' do
|
76
|
+
expect(klass[true]).to eq [0xC3].pack(?C)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe 'Integer' do
|
81
|
+
it 'serializes small integers' do
|
82
|
+
expect(klass[4]).to eq [4].pack(?c)
|
83
|
+
expect(klass[127]).to eq [127].pack(?c)
|
84
|
+
expect(klass[-48]).to eq [-48].pack(?c)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'serializes bytes' do
|
88
|
+
expect(klass[128]).to eq [0xC4, 128].pack('C2')
|
89
|
+
expect(klass[255]).to eq [0xC4, 255].pack('C2')
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'serializes short integers' do
|
93
|
+
expect(klass[32_767]).to eq [0xC5, 32_767].pack('Cs<')
|
94
|
+
expect(klass[-32_768]).to eq [0xC5, -32_768].pack('Cs<')
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'serializes integers' do
|
98
|
+
expect(klass[2_147_483_647]).to eq [0xC6, 2_147_483_647].pack('Cl<')
|
99
|
+
expect(klass[-2_147_483_648]).to eq [0xC6, -2_147_483_648].pack('Cl<')
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'does not serialize integers larger than 32 bit' do
|
103
|
+
expect{klass[2_147_483_648]}.to raise_error ArgumentError
|
104
|
+
expect{klass[-2_147_483_649]}.to raise_error ArgumentError
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe 'Float' do
|
109
|
+
it 'serializes 32 bit floats' do
|
110
|
+
expect(klass[0.2]).to eq [0xC7, 0.2].pack('Ce')
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe 'Time' do
|
115
|
+
it 'serializes time' do
|
116
|
+
timestamp = Time.now
|
117
|
+
expect(klass[timestamp]).to eq [0xC8, timestamp.to_i].pack('CL<')
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
describe '#unpack' do
|
124
|
+
it 'raises an error on malformated packets' do
|
125
|
+
# Make the list one item longer than what is written
|
126
|
+
raw = [0x87, *(1..6).to_a].pack 'CC*'
|
127
|
+
expect{klass.unpack(raw)}.to raise_error MQTTPipe::Packer::FormatError
|
128
|
+
|
129
|
+
# Strings
|
130
|
+
raw = [0xA7, 'string'].pack 'CA*'
|
131
|
+
expect{klass.unpack(raw)}.to raise_error MQTTPipe::Packer::FormatError
|
132
|
+
|
133
|
+
# Integers
|
134
|
+
raw = [0xC4].pack 'C'
|
135
|
+
expect{klass.unpack(raw)}.to raise_error MQTTPipe::Packer::FormatError
|
136
|
+
|
137
|
+
raw = [0xC5, 1].pack 'C*'
|
138
|
+
expect{klass.unpack(raw)}.to raise_error MQTTPipe::Packer::FormatError
|
139
|
+
|
140
|
+
raw = [0xC6, 1,2,3].pack 'C*'
|
141
|
+
expect{klass.unpack(raw)}.to raise_error MQTTPipe::Packer::FormatError
|
142
|
+
|
143
|
+
# Float
|
144
|
+
raw = [0xC7].pack 'C*'
|
145
|
+
expect{klass.unpack(raw)}.to raise_error MQTTPipe::Packer::FormatError
|
146
|
+
|
147
|
+
# Time
|
148
|
+
raw = [0xC8, 1].pack 'C*'
|
149
|
+
expect{klass.unpack(raw)}.to raise_error MQTTPipe::Packer::FormatError
|
150
|
+
end
|
151
|
+
|
152
|
+
describe 'Array' do
|
153
|
+
it 'deserializes short arrays' do
|
154
|
+
raw = [0x86, *(1..6).to_a].pack 'CC6'
|
155
|
+
expect(klass.unpack(raw).first).to eq (1..6).to_a
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe 'String' do
|
160
|
+
it 'deserializes short strings' do
|
161
|
+
raw = [0xA6, 'string'].pack 'CA*'
|
162
|
+
|
163
|
+
expect(klass.unpack(raw).first).to eq 'string'
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'deserializes long strings' do
|
167
|
+
long_string = '*' * 33
|
168
|
+
raw = [0xA0, long_string.length - 31, long_string].pack 'C2A*'
|
169
|
+
|
170
|
+
expect(klass.unpack(raw).first).to eq long_string
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
describe 'Type' do
|
175
|
+
it 'deserializes types' do
|
176
|
+
raw = [0xC0, 0xC0].pack 'C2'
|
177
|
+
expect(klass.unpack(raw).first).to be MQTTPipe::Types::Type
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
describe 'Nil' do
|
182
|
+
it 'deserializes nil' do
|
183
|
+
raw = [0xC1].pack 'C'
|
184
|
+
expect(klass.unpack(raw).first).to be nil
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
describe 'Boolean' do
|
189
|
+
it 'deserializes to false' do
|
190
|
+
raw = [0xC2].pack 'C'
|
191
|
+
expect(klass.unpack(raw).first).to be false
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'deserializes to true' do
|
195
|
+
raw = [0xC3].pack 'C'
|
196
|
+
expect(klass.unpack(raw).first).to be true
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
describe 'Integer' do
|
201
|
+
it 'deserializes tiny integers' do
|
202
|
+
raw = [127].pack ?C
|
203
|
+
expect(klass.unpack(raw).first).to eq 127
|
204
|
+
|
205
|
+
raw = [-48].pack ?C
|
206
|
+
expect(klass.unpack(raw).first).to eq -48
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'deserializes bytes' do
|
210
|
+
raw = [0xC4, 128].pack 'C2'
|
211
|
+
expect(klass.unpack(raw).first).to eq 128
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'deserializes short integers' do
|
215
|
+
raw = [0xC5, 32_767].pack 'Cs<'
|
216
|
+
expect(klass.unpack(raw).first).to eq 32_767
|
217
|
+
|
218
|
+
raw = [0xC5, -32_768].pack 'Cs<'
|
219
|
+
expect(klass.unpack(raw).first).to eq -32_768
|
220
|
+
end
|
221
|
+
|
222
|
+
it 'deserializes long integers' do
|
223
|
+
raw = [0xC6, 2_147_483_647].pack 'Cl<'
|
224
|
+
expect(klass.unpack(raw).first).to eq 2_147_483_647
|
225
|
+
|
226
|
+
raw = [0xC6, -2_147_483_648].pack 'Cl<'
|
227
|
+
expect(klass.unpack(raw).first).to eq -2_147_483_648
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
describe 'Float' do
|
232
|
+
it 'deserializes float' do
|
233
|
+
raw = [0xC7, 0.4].pack 'Ce'
|
234
|
+
expect(klass.unpack(raw).first).to be_within(0.0001).of(0.4)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
describe 'Time' do
|
239
|
+
it 'deserializes time' do
|
240
|
+
t = Time.now
|
241
|
+
raw = [0xC8, t.to_i].pack 'CL<'
|
242
|
+
expect(klass.unpack(raw).first).to be_a Time
|
243
|
+
expect(klass.unpack(raw).first - t).to be_within(1).of(0)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
describe 'End to end' do
|
249
|
+
it 'can serialize and deserialize an object' do
|
250
|
+
packet = ['Hello there!', 42, 42_531, ['hi', Time], true, []]
|
251
|
+
|
252
|
+
req = klass.pack *packet
|
253
|
+
res = klass.unpack req
|
254
|
+
|
255
|
+
expect(res[0]).to eq 'Hello there!'
|
256
|
+
expect(res[1]).to eq 42
|
257
|
+
expect(res[2]).to eq 42_531
|
258
|
+
expect(res[3][0]).to eq 'hi'
|
259
|
+
expect(res[3][1]).to eq Time
|
260
|
+
expect(res[4]).to eq true
|
261
|
+
expect(res[5]).to eq nil
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
data/spec/pipe_spec.rb
ADDED
metadata
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mqtt_pipe
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sebastian Lindberg
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: mqtt
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.3'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.3'
|
69
|
+
description: This gem wraps the MQTT gem by njh (on Github) and adds a serializer
|
70
|
+
for simple data structures.
|
71
|
+
email:
|
72
|
+
- seb.lindberg@gmail.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".gitignore"
|
78
|
+
- Gemfile
|
79
|
+
- LICENSE.txt
|
80
|
+
- README.md
|
81
|
+
- Rakefile
|
82
|
+
- examples/receiver.rb
|
83
|
+
- examples/sender.rb
|
84
|
+
- lib/mqtt_pipe.rb
|
85
|
+
- lib/mqtt_pipe/config.rb
|
86
|
+
- lib/mqtt_pipe/listener.rb
|
87
|
+
- lib/mqtt_pipe/packer.rb
|
88
|
+
- lib/mqtt_pipe/pipe.rb
|
89
|
+
- lib/mqtt_pipe/types.rb
|
90
|
+
- lib/mqtt_pipe/types/array.rb
|
91
|
+
- lib/mqtt_pipe/types/class.rb
|
92
|
+
- lib/mqtt_pipe/types/color.rb
|
93
|
+
- lib/mqtt_pipe/types/false.rb
|
94
|
+
- lib/mqtt_pipe/types/float.rb
|
95
|
+
- lib/mqtt_pipe/types/integer.rb
|
96
|
+
- lib/mqtt_pipe/types/nil.rb
|
97
|
+
- lib/mqtt_pipe/types/string.rb
|
98
|
+
- lib/mqtt_pipe/types/time.rb
|
99
|
+
- lib/mqtt_pipe/types/true.rb
|
100
|
+
- lib/mqtt_pipe/types/type.rb
|
101
|
+
- lib/mqtt_pipe/version.rb
|
102
|
+
- mqtt_pipe.gemspec
|
103
|
+
- spec/config_spec.rb
|
104
|
+
- spec/listener_spec.rb
|
105
|
+
- spec/packer_spec.rb
|
106
|
+
- spec/pipe_spec.rb
|
107
|
+
homepage: https://github.com/seblindberg/ruby-mqtt-pipe
|
108
|
+
licenses:
|
109
|
+
- MIT
|
110
|
+
metadata: {}
|
111
|
+
post_install_message:
|
112
|
+
rdoc_options: []
|
113
|
+
require_paths:
|
114
|
+
- lib
|
115
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
requirements: []
|
126
|
+
rubyforge_project:
|
127
|
+
rubygems_version: 2.4.5
|
128
|
+
signing_key:
|
129
|
+
specification_version: 4
|
130
|
+
summary: A gem for sending a small set of objects via MQTT.
|
131
|
+
test_files:
|
132
|
+
- spec/config_spec.rb
|
133
|
+
- spec/listener_spec.rb
|
134
|
+
- spec/packer_spec.rb
|
135
|
+
- spec/pipe_spec.rb
|