Class: GsmModem
- Inherits:
-
Object
- Object
- GsmModem
- 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
-
#device ⇒ Object
readonly
Returns the value of attribute device.
-
#port ⇒ Object
readonly
Returns the value of attribute port.
-
#read_timeout ⇒ Object
Returns the value of attribute read_timeout.
-
#verbosity ⇒ Object
Returns the value of attribute verbosity.
Instance Method Summary collapse
-
#band ⇒ Object
call-seq: band => string.
-
#band=(new_band) ⇒ Object
call-seq: band=(numeric_band) => string.
-
#bands_available ⇒ Object
call-seq: bands_available => array.
-
#hardware ⇒ Object
call-seq: hardware => hash.
-
#initialize(port = :auto, verbosity = :warn, baud = 9600, cmd_delay = 0.1) ⇒ GsmModem
constructor
call-seq: GsmModem.new(port, verbosity=:warn).
-
#pin_required? ⇒ Boolean
call-seq: pin_required? => true or false.
-
#receive(callback, interval = 5, join_thread = false) ⇒ Object
call-seq: receive(callback_method, interval=5, join_thread=false).
-
#send(to, msg) ⇒ Object
call-seq: send(recipient, message) => true or false.
-
#signal_strength ⇒ Object
call-seq: signal => fixnum or nil.
-
#use_pin(pin) ⇒ Object
call-seq: use_pin(pin) => true or false.
-
#wait_for_network ⇒ Object
call-seq: wait_for_network.
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
#device ⇒ Object (readonly)
Returns the value of attribute device.
23 24 25 |
# File 'lib/rubygsm/core.rb', line 23 def device @device end |
#port ⇒ Object (readonly)
Returns the value of attribute port.
23 24 25 |
# File 'lib/rubygsm/core.rb', line 23 def port @port end |
#read_timeout ⇒ Object
Returns the value of attribute read_timeout.
22 23 24 |
# File 'lib/rubygsm/core.rb', line 22 def read_timeout @read_timeout end |
#verbosity ⇒ Object
Returns the value of attribute verbosity.
22 23 24 |
# File 'lib/rubygsm/core.rb', line 22 def verbosity @verbosity end |
Instance Method Details
#band ⇒ Object
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_available ⇒ Object
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 |
#hardware ⇒ Object
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.
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_strength ⇒ Object
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_network ⇒ Object
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 |