├── .gitignore ├── Gemfile ├── LICENSE.md ├── NEWS.md ├── README.md ├── Rakefile ├── bin └── em-mqtt ├── em-mqtt.gemspec ├── examples ├── eventmachine_publish.rb ├── eventmachine_subscribe.rb └── eventmachine_subscribe_auth.rb ├── lib └── em │ ├── mqtt.rb │ └── mqtt │ ├── client_connection.rb │ ├── connection.rb │ ├── server.rb │ ├── server_connection.rb │ └── version.rb └── spec ├── em_mqtt_connection_spec.rb ├── em_mqtt_version_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | .ruby-version 3 | .yardoc 4 | *.gem 5 | coverage 6 | doc 7 | Gemfile.lock 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Gem's dependencies are specified in em-mqtt.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2009-2013 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. 23 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | EventMachine MQTT NEWS 2 | ====================== 3 | 4 | EventMachine MQTT Version 0.0.5 (2016-04-15) 5 | -------------------------------------------- 6 | 7 | * Prevent errors from getting swallowed inside of EM 8 | 9 | EventMachine MQTT Version 0.0.4 (2015-08-07) 10 | -------------------------------------------- 11 | 12 | * Fix for using version 0.3 of MQTT gem 13 | * Added support for client authentication 14 | * Changed license from 'Ruby' to 'MIT' 15 | * Various other fixes and improvements 16 | 17 | 18 | EventMachine MQTT Version 0.0.1 (2012-02-04) 19 | -------------------------------------------- 20 | 21 | * Initial Release. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ruby-em-mqtt 2 | ============ 3 | 4 | This gem adds MQTT (Message Queue Telemetry Transport) protocol support to EventMachine, 5 | an event-processing library for Ruby. 6 | 7 | 8 | Installing 9 | ---------- 10 | 11 | You may get the latest stable version from rubygems.org: 12 | 13 | $ gem install em-mqtt 14 | 15 | It depends upon the mqtt gem to perform packet parsing and serialising. 16 | 17 | 18 | Synopsis 19 | -------- 20 | 21 | ```ruby 22 | require 'rubygems' 23 | require 'em/mqtt' 24 | 25 | # Publish example 26 | EventMachine.run do 27 | c = EventMachine::MQTT::ClientConnection.connect('test.mosquitto.org') 28 | EventMachine::PeriodicTimer.new(1.0) do 29 | puts "-- Publishing time" 30 | c.publish('test', "The time is #{Time.now}") 31 | end 32 | end 33 | 34 | # Subscribe example 35 | EventMachine.run do 36 | EventMachine::MQTT::ClientConnection.connect('test.mosquitto.org') do |c| 37 | c.subscribe('test') 38 | c.receive_callback do |message| 39 | p message 40 | end 41 | end 42 | end 43 | ``` 44 | 45 | Resources 46 | --------- 47 | 48 | * MQTT Homepage: http://www.mqtt.org/ 49 | * GitHub Project: http://github.com/njh/ruby-em-mqtt 50 | * Documentation: http://rubydoc.info/gems/em-mqtt/frames 51 | 52 | License 53 | ------- 54 | 55 | The em-mqtt gem is licensed under the terms of the MIT license. 56 | See the file LICENSE for details. 57 | 58 | 59 | Contact 60 | ------- 61 | 62 | * Author: Nicholas J Humfrey 63 | * Email: njh@aelius.com 64 | * Home Page: http://www.aelius.com/njh/ 65 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.push File.expand_path("../lib", __FILE__) 4 | 5 | require 'rubygems' 6 | require 'yard' 7 | require 'rspec/core/rake_task' 8 | require "bundler/gem_tasks" 9 | 10 | RSpec::Core::RakeTask.new(:spec) 11 | 12 | namespace :spec do 13 | desc 'Run RSpec code examples in specdoc mode' 14 | RSpec::Core::RakeTask.new(:doc) do |t| 15 | t.rspec_opts = %w(--backtrace --colour --format doc) 16 | end 17 | end 18 | 19 | namespace :doc do 20 | YARD::Rake::YardocTask.new 21 | 22 | desc "Generate HTML report specs" 23 | RSpec::Core::RakeTask.new("spec") do |spec| 24 | spec.rspec_opts = ["--format", "html", "-o", "doc/spec.html"] 25 | end 26 | end 27 | 28 | task :specs => :spec 29 | task :default => :spec 30 | -------------------------------------------------------------------------------- /bin/em-mqtt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", 'lib'))) 4 | require 'em/mqtt' 5 | 6 | EventMachine::MQTT::Server.new(ARGV).run 7 | -------------------------------------------------------------------------------- /em-mqtt.gemspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -rubygems 2 | # -*- encoding: utf-8 -*- 3 | $:.push File.expand_path("../lib", __FILE__) 4 | require "em/mqtt/version" 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = 'em-mqtt' 8 | gem.version = EventMachine::MQTT::VERSION 9 | gem.author = 'Nicholas J Humfrey' 10 | gem.email = 'njh@aelius.com' 11 | gem.homepage = 'http://github.com/njh/ruby-em-mqtt' 12 | gem.summary = 'MQTT for EventMachine' 13 | gem.description = 'This gem adds MQTT (Message Queue Telemetry Transport) protocol support to EventMachine.' 14 | gem.license = 'MIT' if gem.respond_to?(:license=) 15 | 16 | gem.files = %w(README.md LICENSE.md NEWS.md) + Dir.glob('lib/**/*.rb') 17 | gem.test_files = Dir.glob('spec/*_spec.rb') 18 | gem.executables = %w(em-mqtt) 19 | gem.require_paths = %w(lib) 20 | 21 | gem.add_runtime_dependency 'eventmachine' 22 | gem.add_runtime_dependency 'mqtt', '>= 0.3.0' 23 | 24 | if Gem.ruby_version > Gem::Version.new('1.9') 25 | gem.add_development_dependency 'bundler', '>= 1.5.0' 26 | gem.add_development_dependency 'rake', '>= 0.10.0' 27 | gem.add_development_dependency 'yard', '>= 0.8.0' 28 | gem.add_development_dependency 'rspec', '~> 3.0.0' 29 | gem.add_development_dependency 'simplecov' 30 | elsif Gem.ruby_version > Gem::Version.new('1.8') 31 | gem.add_development_dependency 'bundler', '>= 1.1.0' 32 | gem.add_development_dependency 'rake', '~> 0.9.0' 33 | gem.add_development_dependency 'yard', '~> 0.8.0' 34 | gem.add_development_dependency 'rspec', '~> 3.0.0' 35 | else 36 | raise "#{Gem.ruby_version} is an unsupported version of ruby" 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /examples/eventmachine_publish.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.dirname(__FILE__)+'/../lib' 4 | 5 | require 'rubygems' 6 | require 'em/mqtt' 7 | 8 | include EventMachine::MQTT 9 | 10 | EventMachine::error_handler { |e| puts "#{e}: #{e.backtrace.first}" } 11 | 12 | EventMachine.run do 13 | c = ClientConnection.connect('test.mosquitto.org') 14 | EventMachine::PeriodicTimer.new(1.0) do 15 | puts "-- Publishing time" 16 | c.publish('test', "The time is #{Time.now}") 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /examples/eventmachine_subscribe.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.dirname(__FILE__)+'/../lib' 4 | 5 | require 'rubygems' 6 | require 'em/mqtt' 7 | 8 | EventMachine::error_handler { |e| puts "#{e}: #{e.backtrace.first}" } 9 | 10 | EventMachine.run do 11 | EventMachine::MQTT::ClientConnection.connect('test.mosquitto.org') do |c| 12 | c.subscribe('test') 13 | c.receive_callback do |message| 14 | p message 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /examples/eventmachine_subscribe_auth.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.dirname(__FILE__)+'/../lib' 4 | 5 | require 'rubygems' 6 | require 'em/mqtt' 7 | 8 | EventMachine::error_handler { |e| puts "#{e}: #{e.backtrace.first}" } 9 | 10 | EventMachine.run do 11 | EventMachine::MQTT::ClientConnection.connect( 12 | :host => 'localhost', 13 | :username => 'myuser', 14 | :password => 'mypass' 15 | ) do |c| 16 | c.subscribe('test') 17 | c.receive_callback do |message| 18 | p message 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/em/mqtt.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'eventmachine' 4 | require 'logger' 5 | require 'mqtt' 6 | 7 | require "em/mqtt/version" 8 | 9 | module EventMachine::MQTT 10 | 11 | autoload :ClientConnection, 'em/mqtt/client_connection' 12 | autoload :Connection, 'em/mqtt/connection' 13 | autoload :Server, 'em/mqtt/server' 14 | autoload :ServerConnection, 'em/mqtt/server_connection' 15 | 16 | end 17 | -------------------------------------------------------------------------------- /lib/em/mqtt/client_connection.rb: -------------------------------------------------------------------------------- 1 | 2 | class EventMachine::MQTT::ClientConnection < EventMachine::MQTT::Connection 3 | include EventMachine::Deferrable 4 | 5 | attr_reader :client_id 6 | attr_reader :keep_alive 7 | attr_reader :clean_session 8 | attr_reader :username 9 | attr_reader :password 10 | attr_reader :packet_id 11 | attr_reader :ack_timeout 12 | attr_reader :timer 13 | 14 | # Connect to an MQTT server 15 | # 16 | # Examples: 17 | # ClientConnection.connect('localhost', 1883) 18 | # ClientConnection.connect(:host => 'localhost', :username => 'user', :password => 'pass') 19 | # 20 | def self.connect(*args, &blk) 21 | hash = { 22 | :host => 'localhost', 23 | :port => MQTT::DEFAULT_PORT 24 | } 25 | 26 | i = 0 27 | args.each do |arg| 28 | if arg.is_a?(Hash) 29 | hash.merge!(arg) 30 | else 31 | if i == 0 32 | hash[:host] = arg 33 | elsif i == 1 34 | hash[:port] = arg 35 | end 36 | i += 1 37 | end 38 | end 39 | 40 | ::EventMachine.connect( hash.delete(:host), hash.delete(:port), self, hash, &blk ) 41 | end 42 | 43 | # Initialize connection 44 | # @param args [Hash] Arguments for connection 45 | # @option args [String] :client_id A unique identifier for this client 46 | # @option args [Integer] :keep_alive How often to send keep-alive pings (in seconds) 47 | # @option args [Boolean] :clean_session Start a clean session with server or resume old one (default true) 48 | # @option args [String] :username Username to authenticate with the server 49 | # @option args [String] :password Password to authenticate with the server 50 | def initialize(args={}) 51 | @client_id = MQTT::Client.generate_client_id 52 | @keep_alive = 10 53 | @clean_session = true 54 | @packet_id = 0 55 | @ack_timeout = 5 56 | @username = nil 57 | @password = nil 58 | @timer = nil 59 | 60 | if args.is_a?(Hash) 61 | args.each_pair do |k,v| 62 | instance_variable_set("@#{k}", v) 63 | end 64 | end 65 | end 66 | 67 | def post_init 68 | super 69 | @state = :connecting 70 | end 71 | 72 | def connection_completed 73 | # TCP socket established: send Connect packet 74 | packet = MQTT::Packet::Connect.new( 75 | :client_id => @client_id, 76 | :clean_session => @clean_session, 77 | :keep_alive => @keep_alive, 78 | :username => @username, 79 | :password => @password 80 | ) 81 | 82 | send_packet(packet) 83 | 84 | @state = :connect_sent 85 | end 86 | 87 | # Disconnect from the MQTT broker. 88 | # If you don't want to say goodbye to the broker, set send_msg to false. 89 | def disconnect(send_msg=true) 90 | # FIXME: only close if we aren't waiting for any acknowledgements 91 | if connected? 92 | send_packet(MQTT::Packet::Disconnect.new) if send_msg 93 | end 94 | @state = :disconnecting 95 | end 96 | 97 | def receive_callback(&block) 98 | @receive_callback = block 99 | end 100 | 101 | def receive_msg(packet) 102 | # Alternatively, subclass this method 103 | @receive_callback.call(packet) unless @receive_callback.nil? 104 | end 105 | 106 | def unbind 107 | timer.cancel if timer 108 | unless state == :disconnecting 109 | # Re-throw any exceptions (if present) to avoid swallowed errors. 110 | raise $! || MQTT::NotConnectedException.new("Connection to server lost") 111 | end 112 | @state = :disconnected 113 | end 114 | 115 | # Publish a message on a particular topic to the MQTT broker. 116 | def publish(topic, payload, retain=false, qos=0) 117 | # Defer publishing until we are connected 118 | callback do 119 | send_packet( 120 | MQTT::Packet::Publish.new( 121 | :id => next_packet_id, 122 | :qos => qos, 123 | :retain => retain, 124 | :topic => topic, 125 | :payload => payload 126 | ) 127 | ) 128 | end 129 | end 130 | 131 | # Send a subscribe message for one or more topics on the MQTT broker. 132 | # The topics parameter should be one of the following: 133 | # * String: subscribe to one topic with QoS 0 134 | # * Array: subscribe to multiple topics with QoS 0 135 | # * Hash: subscribe to multiple topics where the key is the topic and the value is the QoS level 136 | # 137 | # For example: 138 | # cc.subscribe( 'a/b' ) 139 | # cc.subscribe( 'a/b', 'c/d' ) 140 | # cc.subscribe( ['a/b',0], ['c/d',1] ) 141 | # cc.subscribe( 'a/b' => 0, 'c/d' => 1 ) 142 | 143 | def subscribe(*topics) 144 | # Defer subscribing until we are connected 145 | callback do 146 | send_packet( 147 | MQTT::Packet::Subscribe.new( 148 | :id => next_packet_id, 149 | :topics => topics 150 | ) 151 | ) 152 | end 153 | end 154 | 155 | # Send a unsubscribe message for one or more topics on the MQTT broker 156 | def unsubscribe(*topics) 157 | # Defer unsubscribing until we are connected 158 | callback do 159 | send_packet( 160 | MQTT::Packet::Unsubscribe.new( 161 | :id => next_packet_id, 162 | :topics => topics 163 | ) 164 | ) 165 | end 166 | end 167 | 168 | 169 | 170 | private 171 | 172 | def process_packet(packet) 173 | if state == :connect_sent and packet.class == MQTT::Packet::Connack 174 | connect_ack(packet) 175 | elsif state == :connected and packet.class == MQTT::Packet::Pingresp 176 | # Pong! 177 | elsif state == :connected and packet.class == MQTT::Packet::Publish 178 | receive_msg(packet) 179 | elsif state == :connected and packet.class == MQTT::Packet::Suback 180 | # Subscribed! 181 | else 182 | # FIXME: deal with other packet types 183 | raise MQTT::ProtocolException.new( 184 | "Wasn't expecting packet of type #{packet.class} when in state #{state}" 185 | ) 186 | disconnect 187 | end 188 | end 189 | 190 | def connect_ack(packet) 191 | if packet.return_code != 0x00 192 | raise MQTT::ProtocolException.new(packet.return_msg) 193 | else 194 | @state = :connected 195 | end 196 | 197 | # Send a ping packet every X seconds 198 | if keep_alive > 0 199 | @timer = EventMachine::PeriodicTimer.new(keep_alive) do 200 | send_packet MQTT::Packet::Pingreq.new 201 | end 202 | end 203 | 204 | # We are now connected - can now execute deferred calls 205 | set_deferred_success 206 | end 207 | 208 | def next_packet_id 209 | @packet_id += 1 210 | end 211 | 212 | end 213 | -------------------------------------------------------------------------------- /lib/em/mqtt/connection.rb: -------------------------------------------------------------------------------- 1 | 2 | class EventMachine::MQTT::Connection < EventMachine::Connection 3 | 4 | attr_reader :state 5 | attr_reader :last_sent 6 | attr_reader :last_received 7 | 8 | def post_init 9 | @state = :connecting 10 | @last_sent = 0 11 | @last_received = 0 12 | @packet = nil 13 | @data = '' 14 | end 15 | 16 | # Checks whether a connection is full established 17 | def connected? 18 | state == :connected 19 | end 20 | 21 | def receive_data(data) 22 | @data << data 23 | 24 | # FIXME: limit maximum data / packet size 25 | 26 | # Are we at the start of a new packet? 27 | if @packet.nil? and @data.length >= 2 28 | @packet = MQTT::Packet.parse_header(@data) 29 | end 30 | 31 | # Do we have the the full packet body now? 32 | if @packet and @data.length >= @packet.body_length 33 | @packet.parse_body( 34 | @data.slice!(0...@packet.body_length) 35 | ) 36 | @last_received = Time.now 37 | process_packet(@packet) 38 | @packet = nil 39 | receive_data '' 40 | end 41 | end 42 | 43 | # The function needs to be sub-classed 44 | def process_packet(packet) 45 | end 46 | 47 | def send_packet(packet) 48 | # FIXME: Throw exception if we aren't connected? 49 | #unless packet.class == MQTT::Packet::Connect 50 | # raise MQTT::NotConnectedException if not connected? 51 | #end 52 | 53 | send_data(packet.to_s) 54 | @last_sent = Time.now 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /lib/em/mqtt/server.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | 3 | class EventMachine::MQTT::Server 4 | attr_accessor :address 5 | attr_accessor :port 6 | attr_accessor :logger 7 | 8 | def initialize(args=[]) 9 | # Set defaults 10 | self.address = "0.0.0.0" 11 | self.port = MQTT::DEFAULT_PORT 12 | self.logger = Logger.new(STDOUT) 13 | self.logger.level = Logger::INFO 14 | parse(args) unless args.empty? 15 | end 16 | 17 | def parse(args) 18 | OptionParser.new("", 24, ' ') do |opts| 19 | opts.banner = "Usage: #{File.basename $0} [options]" 20 | 21 | opts.separator "" 22 | opts.separator "Options:" 23 | 24 | opts.on("-D", "--debug", "turn on debug logging") do 25 | self.logger.level = Logger::DEBUG 26 | end 27 | 28 | opts.on("-a", "--address [HOST]", "bind to HOST address (default: #{address})") do |address| 29 | self.address = address 30 | end 31 | 32 | opts.on("-p", "--port [PORT]", "port number to run on (default: #{port})") do |port| 33 | self.port = port 34 | end 35 | 36 | opts.on_tail("-h", "--help", "show this message") do 37 | puts opts 38 | exit 39 | end 40 | 41 | opts.on_tail("--version", "show version") do 42 | puts EventMachine::MQTT::VERSION 43 | exit 44 | end 45 | 46 | opts.parse!(args) 47 | end 48 | end 49 | 50 | def run 51 | EventMachine.run do 52 | # hit Control + C to stop 53 | Signal.trap("INT") { EventMachine.stop } 54 | Signal.trap("TERM") { EventMachine.stop } 55 | 56 | logger.info("Starting MQTT server on #{address}:#{port}") 57 | EventMachine.start_server(address, port, EventMachine::MQTT::ServerConnection, logger) 58 | end 59 | end 60 | 61 | end 62 | -------------------------------------------------------------------------------- /lib/em/mqtt/server_connection.rb: -------------------------------------------------------------------------------- 1 | 2 | class EventMachine::MQTT::ServerConnection < EventMachine::MQTT::Connection 3 | 4 | @@clients = Array.new 5 | 6 | attr_accessor :client_id 7 | attr_accessor :last_packet 8 | attr_accessor :keep_alive 9 | attr_accessor :packet_id 10 | attr_accessor :subscriptions 11 | 12 | attr_reader :timer 13 | attr_reader :logger 14 | 15 | def initialize(logger) 16 | @logger = logger 17 | end 18 | 19 | def post_init 20 | super 21 | @state = :wait_connect 22 | @client_id = nil 23 | @keep_alive = 0 24 | @packet_id = 0 25 | @subscriptions = [] 26 | @timer = nil 27 | logger.debug("TCP connection opened") 28 | end 29 | 30 | def unbind 31 | @@clients.delete(self) 32 | @timer.cancel if @timer 33 | logger.debug("TCP connection closed") 34 | end 35 | 36 | def process_packet(packet) 37 | logger.debug("#{client_id}: #{packet.inspect}") 38 | 39 | if state == :wait_connect and packet.class == MQTT::Packet::Connect 40 | connect(packet) 41 | elsif state == :connected and packet.class == MQTT::Packet::Pingreq 42 | ping(packet) 43 | elsif state == :connected and packet.class == MQTT::Packet::Subscribe 44 | subscribe(packet) 45 | elsif state == :connected and packet.class == MQTT::Packet::Publish 46 | publish(packet) 47 | elsif packet.class == MQTT::Packet::Disconnect 48 | logger.info("#{client_id} has disconnected") 49 | disconnect 50 | else 51 | # FIXME: deal with other packet types 52 | raise MQTT::ProtocolException.new( 53 | "Wasn't expecting packet of type #{packet.class} when in state #{state}" 54 | ) 55 | disconnect 56 | end 57 | end 58 | 59 | def connect(packet) 60 | # FIXME: check the protocol name and version 61 | # FIXME: check the client id is between 1 and 23 charcters 62 | self.client_id = packet.client_id 63 | 64 | ## FIXME: disconnect old client with the same ID 65 | send_packet MQTT::Packet::Connack.new 66 | @state = :connected 67 | @@clients << self 68 | logger.info("#{client_id} is now connected") 69 | 70 | # Setup a keep-alive timer 71 | if packet.keep_alive 72 | @keep_alive = packet.keep_alive 73 | logger.debug("#{client_id}: Setting keep alive timer to #{@keep_alive} seconds") 74 | @timer = EventMachine::PeriodicTimer.new(@keep_alive / 2) do 75 | last_seen = Time.now - @last_received 76 | if last_seen > @keep_alive * 1.5 77 | logger.info("Disconnecting '#{client_id}' because it hasn't been seen for #{last_seen} seconds") 78 | disconnect 79 | end 80 | end 81 | end 82 | end 83 | 84 | def disconnect 85 | logger.debug("Closing connection to #{client_id}") 86 | @state = :disconnected 87 | close_connection 88 | end 89 | 90 | def ping(packet) 91 | send_packet MQTT::Packet::Pingresp.new 92 | end 93 | 94 | def subscribe(packet) 95 | packet.topics.each do |topic,qos| 96 | self.subscriptions << topic 97 | end 98 | logger.info("#{client_id} has subscriptions: #{self.subscriptions}") 99 | 100 | # FIXME: send subscribe acknowledgement 101 | end 102 | 103 | def publish(packet) 104 | @@clients.each do |client| 105 | if client.subscriptions.include?(packet.topic) or client.subscriptions.include?('#') 106 | client.send_packet(packet) 107 | end 108 | end 109 | end 110 | 111 | end 112 | -------------------------------------------------------------------------------- /lib/em/mqtt/version.rb: -------------------------------------------------------------------------------- 1 | module EventMachine 2 | module MQTT 3 | VERSION = "0.0.5" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/em_mqtt_connection_spec.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__)) 2 | 3 | require 'spec_helper' 4 | 5 | describe EventMachine::MQTT::Connection do 6 | 7 | let(:signature) { double('signature') } 8 | let(:subject) { EventMachine::MQTT::Connection.new(signature) } 9 | let(:packet) { MQTT::Packet::Publish.new( :topic => 'test', :payload => 'hello world' ) } 10 | 11 | describe "when receiving data" do 12 | it "should parse packets" do 13 | expect(subject).to receive(:process_packet).exactly(:once) 14 | subject.receive_data packet.to_s 15 | end 16 | 17 | it "should handle multiple packets at once" do 18 | expect(subject).to receive(:process_packet).exactly(:twice) 19 | subject.receive_data [packet, packet].join 20 | end 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /spec/em_mqtt_version_spec.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__)) 2 | 3 | require 'spec_helper' 4 | 5 | describe EventMachine::MQTT do 6 | 7 | describe "version number" do 8 | it "should be defined as a constant" do 9 | expect(defined?(EventMachine::MQTT::VERSION)).to eq('constant') 10 | end 11 | 12 | it "should be a string" do 13 | expect(EventMachine::MQTT::VERSION).to be_a(String) 14 | end 15 | 16 | it "should be in the format x.y.z" do 17 | expect(EventMachine::MQTT::VERSION).to match(/^\d{1,2}\.\d{1,2}\.\d{1,2}$/) 18 | end 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.join(File.dirname(__FILE__),'..','lib')) 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | require 'em/mqtt' 6 | 7 | Bundler.require(:default, :development) 8 | 9 | unless RUBY_VERSION =~ /^1\.8/ 10 | SimpleCov.start 11 | end 12 | --------------------------------------------------------------------------------