jeffrafter-rubygsm 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/bin/sms ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: noet
3
+
4
+ dir = File.dirname(__FILE__)
5
+ require "#{dir}/../lib/rubygsm.rb"
6
+
7
+
8
+ # expand args (TODO: optparser,
9
+ # which we need so very badly)
10
+ # or fail with USAGE message
11
+ if (ARGV.length == 2)
12
+ recipient, msg = *ARGV
13
+ port = :auto
14
+
15
+ elsif (ARGV.length == 3)
16
+ port, recipient, msg = *ARGV
17
+
18
+ else
19
+ puts "Usage: sms [PORT] RECIPIENT MESSAGE"
20
+ puts "(don't forget to quote the message)"
21
+ puts
22
+ puts "Examples:"
23
+ puts ' sms +13474201234 Hello'
24
+ puts ' sms /dev/ttyS0 +13474201234 "Hello from RubyGSM"'
25
+ exit
26
+ end
27
+
28
+ # initialize the modem, send the sms, and
29
+ # terminate. currently, rubygsm does a lot
30
+ # of things that aren't strictly required
31
+ # to get this done; maybe refactor
32
+ begin
33
+ modem = Gsm::Modem.new(port)
34
+ modem.send_sms!(recipient, msg)
35
+
36
+ rescue Gsm::Error => err
37
+ puts "Error: #{err.desc}"
38
+ end
@@ -18,8 +18,7 @@ module Gsm
18
18
  class Modem
19
19
  include Timeout
20
20
 
21
-
22
- attr_accessor :verbosity, :read_timeout
21
+ attr_accessor :verbosity, :read_timeout, :keep_inbox_empty
23
22
  attr_reader :device, :port
24
23
 
25
24
  # call-seq:
@@ -56,16 +55,25 @@ class Modem
56
55
  # tried all ports, nothing worked
57
56
  raise AutoDetectError
58
57
  end
59
-
60
- else
58
+
59
+ # if the port was a port number or file
60
+ # name, initialize a serialport object
61
+ elsif port.is_a?(String) or port.is_a?(Fixnum)
61
62
  @device = SerialPort.new(port, baud, 8, 1, SerialPort::NONE)
62
63
  @port = port
64
+
65
+ # otherwise, we'll assume that the object passed
66
+ # was an object ready to quack like a serial modem
67
+ else
68
+ @device = port
69
+ @port = nil
63
70
  end
64
71
 
65
72
  @cmd_delay = cmd_delay
66
73
  @verbosity = verbosity
67
74
  @read_timeout = 10
68
75
  @locked_to = false
76
+ @encoding = nil
69
77
 
70
78
  # keep track of the depth which each
71
79
  # thread is indented in the log
@@ -76,33 +84,35 @@ class Modem
76
84
  # the last part is delivered
77
85
  @multipart = {}
78
86
 
79
- # (re-) open the full log file
80
- @log = File.new "rubygsm.log", "w"
81
-
82
- # initialization message (yes, it's underlined)
83
- msg = "RubyGSM Initialized at: #{Time.now}"
84
- log msg + "\n" + ("=" * msg.length), :file
87
+ # start logging to file
88
+ log_init
85
89
 
86
90
  # to store incoming messages
87
91
  # until they're dealt with by
88
92
  # someone else, like a commander
89
93
  @incoming = []
90
94
 
91
- # initialize the modem
92
- command "ATE0" # echo off
93
- #COMPAT command "AT+CMEE=1" # useful errors
94
- #COMPAT command "AT+WIND=0" # no notifications
95
- command "AT+CMGF=1" # switch to text mode
95
+ # initialize the modem; rubygsm is (supposed to be) robust enough to function
96
+ # without these working (hence the "try_"), but they make different modems more
97
+ # consistant, and the logs a bit more sane.
98
+ try_command "ATE0" # echo off
99
+ try_command "AT+CMEE=1" # useful errors
100
+ try_command "AT+WIND=0" # no notifications
101
+
102
+ # PDU mode isn't supported right now (although
103
+ # it should be, because it's quite simple), so
104
+ # switching to text mode (mode 1) is MANDATORY
105
+ command "AT+CMGF=1"
96
106
  end
97
107
 
98
108
 
99
-
100
-
101
109
  private
102
110
 
103
111
 
104
112
  INCOMING_FMT = "%y/%m/%d,%H:%M:%S%Z" #:nodoc:
105
-
113
+ CMGL_STATUS = "REC UNREAD" #:nodoc:
114
+
115
+
106
116
  def parse_incoming_timestamp(ts)
107
117
  # extract the weirdo quarter-hour timezone,
108
118
  # convert it into a regular hourly offset
@@ -129,17 +139,17 @@ class Modem
129
139
  next
130
140
  end
131
141
 
132
- # since this line IS a CMT string (an incomming
142
+ # since this line IS a CMT string (an incoming
133
143
  # SMS), parse it and store it to deal with later
134
144
  unless m = lines[n].match(/^\+CMT: "(.+?)",.*?,"(.+?)".*?$/)
135
- err = "Couldn't parse CMT data: #{buf}"
145
+ err = "Couldn't parse CMT data: #{lines[n]}"
136
146
  raise RuntimeError.new(err)
137
147
  end
138
148
 
139
149
  # extract the meta-info from the CMT line,
140
150
  # and the message from the FOLLOWING line
141
151
  from, timestamp = *m.captures
142
- msg = lines[n+1].strip
152
+ msg_text = lines[n+1].strip
143
153
 
144
154
  # notify the network that we accepted
145
155
  # the incoming message (for read receipt)
@@ -157,12 +167,13 @@ class Modem
157
167
  log "Receipt acknowledgement (CNMA) was rejected"
158
168
  end
159
169
 
160
- # we might abort if this is
170
+ # we might abort if this part of a
171
+ # multi-part message, but not the last
161
172
  catch :skip_processing do
162
173
 
163
174
  # multi-part messages begin with ASCII char 130
164
- if (msg[0] == 130) and (msg[1].chr == "@")
165
- text = msg[7,999]
175
+ if (msg_text[0] == 130) and (msg_text[1].chr == "@")
176
+ text = msg_text[7,999]
166
177
 
167
178
  # ensure we have a place for the incoming
168
179
  # message part to live as they are delivered
@@ -178,24 +189,25 @@ class Modem
178
189
 
179
190
  # abort if this is not the last part
180
191
  throw :skip_processing\
181
- unless (msg[5] == 173)
192
+ unless (msg_text[5] == 173)
182
193
 
183
194
  # last part, so switch out the received
184
195
  # part with the whole message, to be processed
185
196
  # below (the sender and timestamp are the same
186
197
  # for all parts, so no change needed there)
187
- msg = @multipart[from].join("")
198
+ msg_text = @multipart[from].join("")
188
199
  @multipart.delete(from)
189
200
  end
190
201
 
191
202
  # just in case it wasn't already obvious...
192
- log "Received message from #{from}: #{msg}"
203
+ log "Received message from #{from}: #{msg_text.inspect}"
193
204
 
194
205
  # store the incoming data to be picked up
195
206
  # from the attr_accessor as a tuple (this
196
207
  # is kind of ghetto, and WILL change later)
197
208
  sent = parse_incoming_timestamp(timestamp)
198
- @incoming.push Gsm::Incoming.new(self, from, sent, msg)
209
+ msg = Gsm::Incoming.new(self, from, sent, msg_text)
210
+ @incoming.push(msg)
199
211
  end
200
212
 
201
213
  # drop the two CMT lines (meta-info and message),
@@ -313,7 +325,7 @@ class Modem
313
325
  # then automatically re-try the command after
314
326
  # a short delay. for others, propagate
315
327
  rescue Error => err
316
- log "Rescued: #{err.desc}"
328
+ log "Rescued (in #command): #{err.desc}"
317
329
 
318
330
  if (err.type == "CMS") and (err.code == 515)
319
331
  sleep 2
@@ -326,6 +338,24 @@ class Modem
326
338
  end
327
339
 
328
340
 
341
+ # proxy a single command to #command, but catch any
342
+ # Gsm::Error exceptions that are raised, and return
343
+ # nil. This should be used to issue commands which
344
+ # aren't vital - of which there are VERY FEW.
345
+ def try_command(cmd, *args)
346
+ begin
347
+ log_incr "Trying Command: #{cmd}"
348
+ out = command(cmd, *args)
349
+ log_decr "=#{out}"
350
+ return out
351
+
352
+ rescue Error => err
353
+ log_then_decr "Rescued (in #try_command): #{err.desc}"
354
+ return nil
355
+ end
356
+ end
357
+
358
+
329
359
  def query(cmd)
330
360
  log_incr "Query: #{cmd}"
331
361
  out = command cmd
@@ -352,7 +382,7 @@ class Modem
352
382
  while true do
353
383
  buf = read(term)
354
384
  buffer.push(buf)
355
-
385
+
356
386
  # some errors contain useful error codes,
357
387
  # so raise a proper error with a description
358
388
  if m = buf.match(/^\+(CM[ES]) ERROR: (\d+)$/)
@@ -646,15 +676,38 @@ class Modem
646
676
  # send_sms(message) => true or false
647
677
  # send_sms(recipient, text) => true or false
648
678
  #
679
+ # Sends an SMS message via _send_sms!_, but traps
680
+ # any exceptions raised, and returns false instead.
681
+ # Use this when you don't really care if the message
682
+ # was sent, which is... never.
683
+ def send_sms(*args)
684
+ begin
685
+ send_sms!(*args)
686
+ return true
687
+
688
+ # something went wrong
689
+ rescue Gsm::Error
690
+ return false
691
+ end
692
+ end
693
+
694
+
695
+ # call-seq:
696
+ # send_sms!(message) => true or raises Gsm::Error
697
+ # send_sms!(receipt, text) => true or raises Gsm::Error
698
+ #
649
699
  # Sends an SMS message, and returns true if the network
650
700
  # accepted it for delivery. We currently can't handle read
651
- # receipts, so have no way of confirming delivery.
701
+ # receipts, so have no way of confirming delivery. If the
702
+ # device or network rejects the message, a Gsm::Error is
703
+ # raised containing (hopefully) information about what went
704
+ # wrong.
652
705
  #
653
706
  # Note: the recipient is passed directly to the modem, which
654
707
  # in turn passes it straight to the SMSC (sms message center).
655
- # for maximum compatibility, use phone numbers in international
708
+ # For maximum compatibility, use phone numbers in international
656
709
  # format, including the *plus* and *country code*.
657
- def send_sms(*args)
710
+ def send_sms!(*args)
658
711
 
659
712
  # extract values from Outgoing object.
660
713
  # for now, this does not offer anything
@@ -699,6 +752,9 @@ class Modem
699
752
  # the text prompt or an error message
700
753
  command "AT+CMGS=\"#{to}\"", ["\r\n", "> "]
701
754
 
755
+ # encode the message using the setup encoding or the default
756
+ msg = encode(msg)
757
+
702
758
  begin
703
759
  # send the sms, and wait until
704
760
  # it is accepted or rejected
@@ -712,26 +768,51 @@ class Modem
712
768
  # an escpae, to... escape
713
769
  rescue Exception, Timeout::Error => err
714
770
  log "Rescued #{err.desc}"
715
- return false
716
- #write 27.chr
717
- #wait
771
+ write 27.chr
772
+
773
+ # allow the error to propagate,
774
+ # so the application can catch
775
+ # it for more useful info
776
+ raise
777
+
778
+ ensure
779
+ log_decr
718
780
  end
719
-
720
- log_decr
721
781
  end
722
782
 
723
783
  # if no error was raised,
724
784
  # then the message was sent
725
785
  return true
726
786
  end
727
-
728
-
787
+
788
+ # Encodes the message using the set encoding or, if no encoding is specified
789
+ # returns the msg unchange
790
+ def encode(msg)
791
+ if (@encoding == :ascii)
792
+ # TODO, use lucky sneaks here
793
+ msg
794
+ elsif (@encoding == :utf8)
795
+ # Unpacking and repacking supposedly cleans out bad (non-UTF-8) stuff
796
+ utf8 = msg.unpack("U*");
797
+ packed = utf8.pack("U*");
798
+ packed
799
+ elsif (@encoding == :ucs2)
800
+ ucs2 = Iconv.iconv("UCS-2", "UTF-8", msg).first
801
+ ucs2 = ucs2.unpack("H*").join
802
+ ucs2
803
+ else
804
+ msg
805
+ end
806
+ end
807
+
729
808
  # call-seq:
730
809
  # receive(callback_method, interval=5, join_thread=false)
731
810
  #
732
811
  # Starts a new thread, which polls the device every _interval_
733
812
  # seconds to capture incoming SMS and call _callback_method_
734
- # for each.
813
+ # for each, and polls the device's internal storage for incoming
814
+ # SMS that we weren't notified about (some modems don't support
815
+ # that).
735
816
  #
736
817
  # class Receiver
737
818
  # def incoming(msg)
@@ -760,15 +841,25 @@ class Modem
760
841
  # keep on receiving forever
761
842
  while true
762
843
  command "AT"
763
- check_for_inbox_messages
764
-
765
- # enable new message notification mode
766
- # every ten intevals, in case the
844
+
845
+ # enable new message notification mode every ten intevals, in case the
767
846
  # modem "forgets" (power cycle, etc)
768
847
  if (@polled % 10) == 0
769
- #COMPAT command "AT+CNMI=2,2,0,0,0"
848
+ try_command("AT+CNMI=2,2,0,0,0")
770
849
  end
850
+
851
+ # check for messages in the default mailbox (wether read or not)
852
+ # read them and then delete them
853
+ if (@keep_inbox_empty)
854
+ fetch_and_delete_stored_messages
855
+ end
771
856
 
857
+ # check for new messages lurking in the device's
858
+ # memory (in case we missed them (yes, it happens))
859
+ if (@polled % 4) == 0
860
+ fetch_stored_messages
861
+ end
862
+
772
863
  # if there are any new incoming messages,
773
864
  # iterate, and pass each to the receiver
774
865
  # in the same format that they were built
@@ -800,7 +891,32 @@ class Modem
800
891
  # threaded (like debugging handsets)
801
892
  @thr.join if join_thread
802
893
  end
894
+
803
895
 
896
+ def encodings?
897
+ command "AT+CSCS=?"
898
+ end
899
+
900
+ def encoding
901
+ @encoding
902
+ end
903
+
904
+ def encoding=(enc)
905
+ @encoding = enc
906
+ case enc
907
+ when :ascii
908
+ command "AT+CSCS=\"ASCII\""
909
+ when :utf8
910
+ command "AT+CSCS=\"UTF8\""
911
+ when :ucs2
912
+ command "AT+CSCS=\"UCS2\""
913
+ when :gsm
914
+ command "AT+CSCS=\"GSM\""
915
+ when :iso88591
916
+ command "AT+CSCS=\"8859-1\""
917
+ end
918
+ end
919
+
804
920
  def select_default_mailbox
805
921
  # Eventually we will select the first mailbox as the default
806
922
  result = command("AT+CPMS=?")
@@ -813,8 +929,8 @@ class Modem
813
929
  nil
814
930
  end
815
931
 
816
- # Could accomplish this with a CMGL=\"REC UNREAD\" too
817
- def check_for_inbox_messages
932
+ def fetch_and_delete_stored_messages
933
+ # If there is no way to select a default mailbox we can't continue
818
934
  return unless select_default_mailbox
819
935
 
820
936
  # Try to read the first message from the box (should this be 0?)
@@ -847,5 +963,56 @@ class Modem
847
963
  @incoming.push Gsm::Incoming.new(self, from, sent, msg)
848
964
  end
849
965
 
966
+ def fetch_stored_messages
967
+
968
+ # fetch all/unread (see constant) messages
969
+ lines = command('AT+CMGL="%s"' % CMGL_STATUS)
970
+ n = 0
971
+
972
+ # if the last line returned is OK
973
+ # (and it SHOULD BE), remove it
974
+ lines.pop if lines[-1] == "OK"
975
+
976
+ # keep on iterating the data we received,
977
+ # until there's none left. if there were no
978
+ # stored messages waiting, this done nothing!
979
+ while n < lines.length
980
+
981
+ # attempt to parse the CMGL line (we're skipping
982
+ # two lines at a time in this loop, so we will
983
+ # always land at a CMGL line here) - they look like:
984
+ # +CMGL: 0,"REC READ","+13364130840",,"09/03/04,21:59:31-20"
985
+ unless m = lines[n].match(/^\+CMGL: (\d+),"(.+?)","(.+?)",*?,"(.+?)".*?$/)
986
+ err = "Couldn't parse CMGL data: #{lines[n]}"
987
+ raise RuntimeError.new(err)
988
+ end
989
+
990
+ # find the index of the next
991
+ # CMGL line, or the end
992
+ nn = n+1
993
+ nn += 1 until\
994
+ nn >= lines.length ||\
995
+ lines[nn][0,6] == "+CMGL:"
996
+
997
+ # extract the meta-info from the CMGL line, and the
998
+ # message text from the lines between _n_ and _nn_
999
+ index, status, from, timestamp = *m.captures
1000
+ msg_text = lines[(n+1)..(nn-1)].join("\n").strip
1001
+
1002
+ # log the incoming message
1003
+ log "Fetched stored message from #{from}: #{msg_text.inspect}"
1004
+
1005
+ # store the incoming data to be picked up
1006
+ # from the attr_accessor as a tuple (this
1007
+ # is kind of ghetto, and WILL change later)
1008
+ sent = parse_incoming_timestamp(timestamp)
1009
+ msg = Gsm::Incoming.new(self, from, sent, msg_text)
1010
+ @incoming.push(msg)
1011
+
1012
+ # skip over the messge line(s),
1013
+ # on to the next CMGL line
1014
+ n = nn
1015
+ end
1016
+ end
850
1017
  end # Modem
851
1018
  end # Gsm
@@ -94,7 +94,11 @@ module Gsm
94
94
  "[type=#{@type}] [code=#{code}]"
95
95
  end
96
96
 
97
- alias :to_s :desc
97
+ # not the same as alias :to_s, :desc,
98
+ # because this works on subclasses
99
+ def to_s
100
+ desc
101
+ end
98
102
  end
99
103
 
100
104
  # TODO: what the hell is going on with
@@ -15,7 +15,31 @@ class Modem
15
15
  :warn => 2,
16
16
  :error => 1 }
17
17
 
18
+ def log_init
19
+ fn_port = File.basename(@port)
20
+ fn_time = Time.now.strftime("%Y-%m-%d.%H-%M-%S")
21
+
22
+ # (re-) open the full log file
23
+ filename = "rubygsm.#{fn_port}.#{fn_time}"
24
+ @log = File.new filename, "w"
25
+
26
+ # dump some useful information
27
+ # at the top, for debugging
28
+ log "RUBYGSM"
29
+ log " port: #{@port}"
30
+ log " timeout: #{@read_timeout}"
31
+ log " verbosity: #{@verbosity}"
32
+ log " started at: #{Time.now}"
33
+ log "===="
34
+ end
35
+
18
36
  def log(msg, level=:debug)
37
+
38
+ # abort if logging isn't
39
+ # enabled yet (or ever?)
40
+ return false if\
41
+ @log.nil?
42
+
19
43
  ind = " " * (@log_indents[Thread.current] or 0)
20
44
 
21
45
  # create a
@@ -21,8 +21,11 @@ module Gsm
21
21
  @received = Time.now
22
22
  end
23
23
 
24
- def to_a
25
- [@device, @sender, @sent, @text]
24
+ # Returns the sender of this message,
25
+ # so incoming and outgoing messages
26
+ # can be logged in the same way.
27
+ def number
28
+ sender
26
29
  end
27
30
  end
28
31
  end
@@ -26,5 +26,12 @@ module Gsm
26
26
  # more modifications
27
27
  freeze
28
28
  end
29
+
30
+ # Returns the recipient of this message,
31
+ # so incoming and outgoing messages
32
+ # can be logged in the same way.
33
+ def number
34
+ recipient
35
+ end
29
36
  end
30
37
  end
@@ -1,11 +1,11 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "rubygsm"
3
- s.version = "0.3.2"
4
- s.date = "2009-01-09"
3
+ s.version = "0.5.0"
4
+ s.date = "2009-03-12"
5
5
  s.summary = "Send and receive SMS with a GSM modem"
6
6
  s.email = "adam.mckaig@gmail.com"
7
7
  s.homepage = "http://github.com/adammck/rubygsm"
8
- s.authors = ["Adam Mckaig"]
8
+ s.authors = ["Adam Mckaig", "Jeff Rafter"]
9
9
  s.has_rdoc = true
10
10
 
11
11
  s.files = [
@@ -17,12 +17,14 @@ Gem::Specification.new do |s|
17
17
  "lib/rubygsm/log.rb",
18
18
  "lib/rubygsm/msg/incoming.rb",
19
19
  "lib/rubygsm/msg/outgoing.rb",
20
- "bin/gsm-modem-band"
20
+ "bin/gsm-modem-band",
21
+ "bin/gsm-app-monitor"
21
22
  ]
22
23
 
23
24
  s.executables = [
24
- "gsm-modem-band"
25
+ "gsm-modem-band",
26
+ "sms"
25
27
  ]
26
28
 
27
- # s.add_dependency("toholio-serialport", ["> 0.7.1"])
29
+ s.add_dependency("toholio-serialport", ["> 0.7.1"])
28
30
  end
metadata CHANGED
@@ -1,22 +1,33 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jeffrafter-rubygsm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Mckaig
8
+ - Jeff Rafter
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
12
 
12
- date: 2009-01-09 00:00:00 -08:00
13
+ date: 2009-03-12 00:00:00 -07:00
13
14
  default_executable:
14
- dependencies: []
15
-
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: toholio-serialport
18
+ type: :runtime
19
+ version_requirement:
20
+ version_requirements: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ">"
23
+ - !ruby/object:Gem::Version
24
+ version: 0.7.1
25
+ version:
16
26
  description:
17
27
  email: adam.mckaig@gmail.com
18
28
  executables:
19
29
  - gsm-modem-band
30
+ - sms
20
31
  extensions: []
21
32
 
22
33
  extra_rdoc_files: []
@@ -31,6 +42,7 @@ files:
31
42
  - lib/rubygsm/msg/incoming.rb
32
43
  - lib/rubygsm/msg/outgoing.rb
33
44
  - bin/gsm-modem-band
45
+ - bin/gsm-app-monitor
34
46
  has_rdoc: true
35
47
  homepage: http://github.com/adammck/rubygsm
36
48
  post_install_message: