Class: Gsm::Modem

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

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) ⇒ Modem

call-seq:

Gsm::Modem.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
97
98
# 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 = {}
	
	# start logging to file
	log_init
	
	# to store incoming messages
	# until they're dealt with by
	# someone else, like a commander
	@incoming = []
	
	# initialize the modem; rubygsm is (supposed to be) robust enough to function
	# without these working (hence the "try_"), but they make different modems more
	# consistant, and the logs a bit more sane.
	try_command "ATE0"      # echo off
	try_command "AT+CMEE=1" # useful errors
	try_command "AT+WIND=0" # no notifications
	
	# PDU mode isn't supported right now (although
	# it should be, because it's quite simple), so
	# switching to text mode (mode 1) is MANDATORY
	command "AT+CMGF=1"
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.



527
528
529
530
531
532
533
534
535
536
537
# File 'lib/rubygsm/core.rb', line 527

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 Gsm::Modem.Bands) or a named area from Gsm::Modem.BandAreas:

m = Gsm::Modem.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 Gsm::Error if the modem does not support the given band.



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

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, allow Gsm::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.



501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
# File 'lib/rubygsm/core.rb', line 501

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

#fetch_stored_messagesObject



862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
# File 'lib/rubygsm/core.rb', line 862

def fetch_stored_messages
	
	# fetch all/unread (see constant) messages
	lines = command('AT+CMGL="%s"' % CMGL_STATUS)
	n = 0
	
	# if the last line returned is OK
	# (and it SHOULD BE), remove it
	lines.pop if lines[-1] == "OK"
	
	# keep on iterating the data we received,
	# until there's none left. if there were no
	# stored messages waiting, this done nothing!
 	while n < lines.length
 		
 		# attempt to parse the CMGL line (we're skipping
 		# two lines at a time in this loop, so we will
 		# always land at a CMGL line here) - they look like:
		#   +CMGL: 0,"REC READ","+13364130840",,"09/03/04,21:59:31-20"
		unless m = lines[n].match(/^\+CMGL: (\d+),"(.+?)","(.+?)",*?,"(.+?)".*?$/)
			err = "Couldn't parse CMGL data: #{lines[n]}"
			raise RuntimeError.new(err)
		end
		
		# find the index of the next
		# CMGL line, or the end
		nn = n+1
		nn += 1 until\
			nn >= lines.length ||\
			lines[nn][0,6] == "+CMGL:"
		
		# extract the meta-info from the CMGL line, and the
		# message text from the lines between _n_ and _nn_
		index, status, from, timestamp = *m.captures
		msg_text = lines[(n+1)..(nn-1)].join("\n").strip
 		
		# log the incoming message
		log "Fetched stored message from #{from}: #{msg_text.inspect}"
 		
		# store the incoming data to be picked up
		# from the attr_accessor as a tuple (this
		# is kind of ghetto, and WILL change later)
		sent = parse_incoming_timestamp(timestamp)
		msg = Gsm::Incoming.new(self, from, sent, msg_text)
		@incoming.push(msg)
 		
 		# skip over the messge line(s),
 		# on to the next CMGL line
 		n = nn
 	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" }


474
475
476
477
478
479
480
# File 'lib/rubygsm/core.rb', line 474

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)


596
597
598
# File 'lib/rubygsm/core.rb', line 596

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, and polls the device’s internal storage for incoming SMS that we weren’t notified about (some modems don’t support that).

class Receiver
  def incoming(msg)
    puts "From #{msg.from} at #{msg.sent}:", msg.text
  end
end

# create the instances,
# and start receiving
rcv = Receiver.new
m = Gsm::Modem.new "/dev/ttyS0"
m.receive rcv.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.



807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
# File 'lib/rubygsm/core.rb', line 807

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
				try_command("AT+CNMI=2,2,0,0,0")
			end
			
			# check for new messages lurking in the device's
			# memory (in case we missed them (yes, it happens))
			if (@polled % 4) == 0
				fetch_stored_messages
			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 |msg|
					begin
						callback.call(msg)
						
					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_sms(*args) ⇒ Object

call-seq:

send_sms(message) => true or false
send_sms(recipient, text) => true or false

Sends an SMS message via send_sms!, but traps any exceptions raised, and returns false instead. Use this when you don’t really care if the message was sent, which is… never.



677
678
679
680
681
682
683
684
685
686
# File 'lib/rubygsm/core.rb', line 677

def send_sms(*args)
	begin
		send_sms!(*args)
		return true
	
	# something went wrong
	rescue Gsm::Error
		return false
	end
end

#send_sms!(*args) ⇒ Object

call-seq:

send_sms!(message) => true or raises Gsm::Error
send_sms!(receipt, text) => true or raises Gsm::Error

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. If the device or network rejects the message, a Gsm::Error is raised containing (hopefully) information about what went wrong.

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*.



704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
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
767
768
769
770
771
772
773
774
775
776
777
# File 'lib/rubygsm/core.rb', line 704

def send_sms!(*args)
	
	# extract values from Outgoing object.
	# for now, this does not offer anything
	# in addition to the recipient/text pair,
	# but provides an upgrade path for future
	# features (like FLASH and VALIDITY TIME)
	if args.length == 1\
	and args[0].is_a? Gsm::Outgoing
		to = args[0].recipient
		msg = args[0].text
	
	# the < v0.4 arguments. maybe
	# deprecate this one day
	elsif args.length == 2
		to, msg = *args
	
	else
		raise ArgumentError,\
			"The Gsm::Modem#send_sms method accepts" +\
			"a single Gsm::Outgoing instance, " +\
			"or recipient and text strings"
	end
	
	# 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 notm
	# 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}"
			write 27.chr
			
			# allow the error to propagate,
			# so the application can catch
			# it for more useful info
			raise
			
		ensure
			log_decr
		end
	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.



631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
# File 'lib/rubygsm/core.rb', line 631

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.



605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
# File 'lib/rubygsm/core.rb', line 605

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 Gsm::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.



655
656
657
658
659
660
661
662
663
664
665
666
# File 'lib/rubygsm/core.rb', line 655

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