Class: GsmModem

Inherits:
Object
  • Object
show all
Includes:
Timeout
Defined in:
lib/rubygsm/log.rb,
lib/rubygsm/core.rb,
lib/rubygsm/errors.rb

Overview

:title:Ruby GSM Errors – vim: noet ++

Defined Under Namespace

Classes: AutoDetectError, Error, ReadError, TimeoutError, WriteError

Constant Summary collapse

Bands =

The values accepted and returned by the AT+WMBS command, mapped to frequency bands, in MHz. Copied directly from the MultiTech AT command-set reference

{
	0 => "850",
	1 => "900",
	2 => "1800",
	3 => "1900",
	4 => "850/1900",
	5 => "900E/1800",
	6 => "900E/1900"
}
BandAreas =
{
	:usa     => 4,
	:africa  => 5,
	:europe  => 5,
	:asia    => 5,
	:mideast => 5
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(port = :auto, verbosity = :warn, baud = 9600, cmd_delay = 0.1) ⇒ GsmModem

call-seq:

GsmModem.new(port, verbosity=:warn)

Create a new instance, to initialize and communicate exclusively with a single modem device via the port (which is usually either /dev/ttyS0 or /dev/ttyUSB0), and start logging to rubygsm.log in the chdir.



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/rubygsm/core.rb', line 31

def initialize(port=:auto, verbosity=:warn, baud=9600, cmd_delay=0.1)
	
	# if no port was specified, we'll attempt to iterate
	# all of the serial ports that i've ever seen gsm
	# modems mounted on. this is kind of shaky, and
	# only works well with a single modem. for now,
	# we'll try: ttyS0, ttyUSB0, ttyACM0, ttyS1...
	if port == :auto
		@device, @port = catch(:found) do
			0.upto(8) do |n|
				["ttyS", "ttyUSB", "ttyACM"].each do |prefix|
					try_port = "/dev/#{prefix}#{n}"
		
					begin
						# serialport args: port, baud, data bits, stop bits, parity
						device = SerialPort.new(try_port, baud, 8, 1, SerialPort::NONE)
						throw :found, [device, try_port]
					
					rescue ArgumentError, Errno::ENOENT
						# do nothing, just continue to
						# try the next port in order
					end
				end
			end

			# tried all ports, nothing worked
			raise AutoDetectError
		end
		
	else
		@device = SerialPort.new(port, baud, 8, 1, SerialPort::NONE)
		@port = port
	end
	
	@cmd_delay = cmd_delay
	@verbosity = verbosity
	@read_timeout = 10
	@locked_to = false
	
	# keep track of the depth which each
	# thread is indented in the log
	@log_indents = {}
	@log_indents.default = 0
	
	# to keep multi-part messages until
	# the last part is delivered
	@multipart = {}
	
	# (re-) open the full log file
	@log = File.new "rubygsm.log", "w"
	
	# initialization message (yes, it's underlined)
	msg = "RubyGSM Initialized at: #{Time.now}"
	log msg + "\n" + ("=" * msg.length), :file
	
	# to store incoming messages
	# until they're dealt with by
	# someone else, like a commander
	@incoming = []
	
	# initialize the modem
	command "ATE0"      # echo off
	command "AT+CMEE=1" # useful errors
	command "AT+WIND=0" # no notifications
	command "AT+CMGF=1" # switch to text mode
end

Instance Attribute Details

#deviceObject (readonly)

Returns the value of attribute device.



23
24
25
# File 'lib/rubygsm/core.rb', line 23

def device
  @device
end

#portObject (readonly)

Returns the value of attribute port.



23
24
25
# File 'lib/rubygsm/core.rb', line 23

def port
  @port
end

#read_timeoutObject

Returns the value of attribute read_timeout.



22
23
24
# File 'lib/rubygsm/core.rb', line 22

def read_timeout
  @read_timeout
end

#verbosityObject

Returns the value of attribute verbosity.



22
23
24
# File 'lib/rubygsm/core.rb', line 22

def verbosity
  @verbosity
end

Instance Method Details

#bandObject

call-seq:

band => string

Returns a string containing the band currently selected for use by the modem.



491
492
493
494
495
496
497
498
499
500
501
# File 'lib/rubygsm/core.rb', line 491

def band
	data = query("AT+WMBS?")
	if m = data.match(/^\+WMBS: (\d+),/)
		return Bands[m.captures[0].to_i]
		
	else
		# Todo: Recover from this exception
		err = "Not WMBS data: #{data.inspect}"
		raise RuntimeError.new(err)
	end
end

#band=(new_band) ⇒ Object

call-seq:

band=(_numeric_band_) => string

Sets the band currently selected for use by the modem, using either a literal band number (passed directly to the modem, see GsmModem.Bands) or a named area from GsmModem.BandAreas:

m = GsmModem.new
m.band = :usa    => "850/1900"
m.band = :africa => "900E/1800"
m.band = :monkey => ArgumentError

(Note that as usual, the United States of America is wearing its ass backwards.)

Raises ArgumentError if an unrecognized band was given, or raises GsmModem::Error if the modem does not support the given band.



531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
# File 'lib/rubygsm/core.rb', line 531

def band=(new_band)
	
	# resolve named bands into numeric
	# (mhz values first, then band areas)
	unless new_band.is_a?(Numeric)
		
		if Bands.has_value?(new_band.to_s)
			new_band = Bands.index(new_band.to_s)
		
		elsif BandAreas.has_key?(new_band.to_sym)
			new_band = BandAreas[new_band.to_sym]
			
		else
			err = "Invalid band: #{new_band}"
			raise ArgumentError.new(err)
		end
	end
	
	# set the band right now (second wmbs
	# argument is: 0=NEXT-BOOT, 1=NOW). if it
	# fails, allowGsmModem::Error to propagate
	command("AT+WMBS=#{new_band},1")
end

#bands_availableObject

call-seq:

bands_available => array

Returns an array containing the bands supported by the modem.



465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
# File 'lib/rubygsm/core.rb', line 465

def bands_available
	data = query("AT+WMBS=?")
	
	# wmbs data is returned as something like:
	#  +WMBS: (0,1,2,3,4,5,6),(0-1)
	#  +WMBS: (0,3,4),(0-1)
	# extract the numbers with a regex, and
	# iterate each to resolve it to a more
	# readable description
	if m = data.match(/^\+WMBS: \(([\d,]+)\),/)
		return m.captures[0].split(",").collect do |index|
			Bands[index.to_i]
		end
	
	else
		# Todo: Recover from this exception
		err = "Not WMBS data: #{data.inspect}"
		raise RuntimeError.new(err)
	end
end

#hardwareObject

call-seq:

hardware => hash

Returns a hash of containing information about the physical modem. The contents of each value are entirely manufacturer dependant, and vary wildly between devices.

modem.hardware => { :manufacturer => "Multitech".
                    :model        => "MTCBA-G-F4", 
                    :revision     => "123456789",
                    :serial       => "ABCD" }


438
439
440
441
442
443
444
# File 'lib/rubygsm/core.rb', line 438

def hardware
	return {
		:manufacturer => query("AT+CGMI"),
		:model        => query("AT+CGMM"),
		:revision     => query("AT+CGMR"),
		:serial       => query("AT+CGSN") }
end

#pin_required?Boolean

call-seq:

pin_required? => true or false

Returns true if the modem is waiting for a SIM PIN. Some SIM cards will refuse to work until the correct four-digit PIN is provided via the use_pin method.

Returns:

  • (Boolean)


560
561
562
# File 'lib/rubygsm/core.rb', line 560

def pin_required?
	not command("AT+CPIN?").include?("+CPIN: READY")
end

#receive(callback, interval = 5, join_thread = false) ⇒ Object

call-seq:

receive(callback_method, interval=5, join_thread=false)

Starts a new thread, which polls the device every interval seconds to capture incoming SMS and call callback_method for each.

class Receiver
  def incoming(caller, datetime, message)
    puts "From #{caller} at #{datetime}:", message
  end
end

# create the instances,
# and start receiving
rcv = Receiver.new
m = GsmModem.new "/dev/ttyS0"
m.receive inst.method :incoming

# block until ctrl+c
while(true) { sleep 2 }

Note: New messages may arrive at any time, even if this method’s receiver thread isn’t waiting to process them. They are not lost, but cached in @incoming until this method is called.



719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
# File 'lib/rubygsm/core.rb', line 719

def receive(callback, interval=5, join_thread=false)
	@polled = 0
	
	@thr = Thread.new do
		Thread.current["name"] = "receiver"
		
		# keep on receiving forever
		while true
			command "AT"
			
			# enable new message notification mode
			# every ten intevals, in case the
			# modem "forgets" (power cycle, etc)
			if (@polled % 10) == 0
				command "AT+CNMI=2,2,0,0,0"
			end
			
			# if there are any new incoming messages,
			# iterate, and pass each to the receiver
			# in the same format that they were built
			# back in _parse_incoming_sms!_
			unless @incoming.empty?
				@incoming.each do |inc|
					begin
						callback.call *inc
					
					rescue StandardError => err
						log "Error in callback: #{err}"
					end
				end
				
				# we have dealt with all of the pending
				# messages. todo: this is a ridiculous
				# race condition, and i fail at ruby
				@incoming.clear
			end
			
			# re-poll every
			# five seconds
			sleep(interval)
			@polled += 1
		end
	end
	
	# it's sometimes handy to run single-
	# threaded (like debugging handsets)
	@thr.join if join_thread
end

#send(to, msg) ⇒ Object

call-seq:

send(recipient, message) => true or false

Sends an SMS message, and returns true if the network accepted it for delivery. We currently can’t handle read receipts, so have no way of confirming delivery.

Note: the recipient is passed directly to the modem, which in turn passes it straight to the SMSC (sms message center). for maximum compatibility, use phone numbers in international format, including the plus and *country code*.



644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
# File 'lib/rubygsm/core.rb', line 644

def send(to, msg)
	
	# the number must be in the international
	# format for some SMSCs (notably, the one
	# i'm on right now) so maybe add a PLUS
	#to = "+#{to}" unless(to[0,1]=="+")
	
	# 1..9 is a special number which does not
	# result in a real sms being sent (see inject.rb)
	if to == "+123456789"
		log "Not sending test message: #{msg}"
		return false
	end
	
	# block the receiving thread while
	# we're sending. it can take some time
	exclusive do
		log_incr "Sending SMS to #{to}: #{msg}"
		
		# initiate the sms, and wait for either
		# the text prompt or an error message
		command "AT+CMGS=\"#{to}\"", ["\r\n", "> "]
		
		begin
			# send the sms, and wait until
			# it is accepted or rejected
			write "#{msg}#{26.chr}"
			wait
			
		# if something went wrong, we are
		# be stuck in entry mode (which will
		# result in someone getting a bunch
		# of AT commands via sms!) so send
		# an escpae, to... escape
		rescue Exception, Timeout::Error => err
			log "Rescued #{err.desc}"
			return false
			#write 27.chr
			#wait
		end
		
		log_decr
	end
			
	# if no error was raised,
	# then the message was sent
	return true
end

#signal_strengthObject

call-seq:

signal => fixnum or nil

Returns an fixnum between 1 and 99, representing the current signal strength of the GSM network, or nil if we don’t know.



595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
# File 'lib/rubygsm/core.rb', line 595

def signal_strength
	data = query("AT+CSQ")
	if m = data.match(/^\+CSQ: (\d+),/)
		
		# 99 represents "not known or not detectable",
		# but we'll use nil for that, since it's a bit
		# more ruby-ish to test for boolean equality
		csq = m.captures[0].to_i
		return (csq<99) ? csq : nil
		
	else
		# Todo: Recover from this exception
		err = "Not CSQ data: #{data.inspect}"
		raise RuntimeError.new(err)
	end
end

#use_pin(pin) ⇒ Object

call-seq:

use_pin(pin) => true or false

Provide a SIM PIN to the modem, and return true if it was accepted.



569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
# File 'lib/rubygsm/core.rb', line 569

def use_pin(pin)
	
	# if the sim is already ready,
	# this method isn't necessary
	if pin_required?
		begin
			command "AT+CPIN=#{pin}"
	
		# if the command failed, then
		# the pin was not accepted
		rescue GsmModem::Error
			return false
		end
	end
	
	# no error = SIM
	# PIN accepted!
	true
end

#wait_for_networkObject

call-seq:

wait_for_network

Blocks until the signal strength indicates that the device is active on the GSM network. It’s a good idea to call this before trying to send or receive anything.



619
620
621
622
623
624
625
626
627
628
629
630
# File 'lib/rubygsm/core.rb', line 619

def wait_for_network
	
	# keep retrying until the
	# network comes up (if ever)
	until csq = signal_strength
		sleep 1
	end
	
	# return the last
	# signal strength
	return csq
end