jeffrafter-rubygsm 0.3.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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: