Class: Gsm::Modem
- Inherits:
-
Object
- Object
- Gsm::Modem
- 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
-
#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.
- #fetch_stored_messages ⇒ Object
-
#hardware ⇒ Object
call-seq: hardware => hash.
-
#initialize(port = :auto, verbosity = :warn, baud = 9600, cmd_delay = 0.1) ⇒ Modem
constructor
call-seq: Gsm::Modem.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_sms(*args) ⇒ Object
call-seq: send_sms(message) => true or false send_sms(recipient, text) => true or false.
-
#send_sms!(*args) ⇒ Object
call-seq: send_sms!(message) => true or raises Gsm::Error send_sms!(receipt, text) => true or raises Gsm::Error.
-
#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) ⇒ Modem
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
#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.
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_available ⇒ Object
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_messages ⇒ Object
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 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, = *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 = () 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 |
#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" }
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.
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 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() => 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!() => 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_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.
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_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.
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 |