jeffrafter-rubygsm 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +47 -0
- data/bin/gsm-modem-band +49 -0
- data/lib/rubygsm/core.rb +851 -0
- data/lib/rubygsm/errors.rb +128 -0
- data/lib/rubygsm/log.rb +60 -0
- data/lib/rubygsm/msg/incoming.rb +28 -0
- data/lib/rubygsm/msg/outgoing.rb +30 -0
- data/lib/rubygsm.rb +18 -0
- data/rubygsm.gemspec +28 -0
- metadata +69 -0
data/README.rdoc
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
RubyGSM is a Ruby library which uses ruby-serialport[http://github.com/toholio/ruby-serialport]
|
2
|
+
to provide a nifty interface to send and receive SMS messages via a GSM modem.
|
3
|
+
|
4
|
+
|
5
|
+
=== Sample Usage
|
6
|
+
|
7
|
+
class ReverseApp
|
8
|
+
def initialize(gsm)
|
9
|
+
gsm.receive(method(:incoming))
|
10
|
+
@gsm = gsm
|
11
|
+
end
|
12
|
+
|
13
|
+
def incoming(from, datetime, message)
|
14
|
+
@gsm.send(from, message.reverse)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
gsm = GsmModem.new
|
19
|
+
ReverseApp.new(gsm)
|
20
|
+
|
21
|
+
|
22
|
+
=== Installing
|
23
|
+
RubyGSM is distributed via GitHub[http://github.com/adammck/rubygsm], which you must
|
24
|
+
add as a gem source before installing:
|
25
|
+
|
26
|
+
$ sudo gem sources -a http://gems.github.com
|
27
|
+
|
28
|
+
The library depends upon ruby-serialport, which is currently maintained on GitHub by
|
29
|
+
Toholio[http://github.com/toholio/ruby-serialport]. This library is included as a Gem
|
30
|
+
dependancy, so to install both:
|
31
|
+
|
32
|
+
$ sudo gem install adammck-rubygsm
|
33
|
+
|
34
|
+
|
35
|
+
==== For Debian/Ubuntu users...
|
36
|
+
The ruby-serialport library is available in the APT repository as a binary package, which
|
37
|
+
is substatially less hassle to install than the Gem, for those who don't have a compiler
|
38
|
+
installed (I prefer not to, on production servers). To install the package via apt, and
|
39
|
+
ignore the Gem dependancy:
|
40
|
+
|
41
|
+
$ sudo apt-get install libserialport-ruby
|
42
|
+
$ sudo gem install --ignore-dependencies adammck-rubygsm
|
43
|
+
|
44
|
+
|
45
|
+
=== Devices Tested
|
46
|
+
* Multitech MTCBA
|
47
|
+
* Wavecom M1306B
|
data/bin/gsm-modem-band
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: noet
|
3
|
+
|
4
|
+
#require "rubygems"
|
5
|
+
#require "rubygsm"
|
6
|
+
dir = File.dirname(__FILE__)
|
7
|
+
require "#{dir}/../lib/rubygsm.rb"
|
8
|
+
|
9
|
+
|
10
|
+
begin
|
11
|
+
modem = Gsm::Modem.new
|
12
|
+
|
13
|
+
rescue Gsm::Modem::Error => err
|
14
|
+
puts "Initialization Error:"
|
15
|
+
puts " " + err.desc
|
16
|
+
exit
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
puts "Found modem on port:"
|
21
|
+
puts " " + modem.port
|
22
|
+
puts
|
23
|
+
|
24
|
+
|
25
|
+
puts "GSM bands supported:"
|
26
|
+
puts " " + modem.bands_available.join(", ")
|
27
|
+
puts
|
28
|
+
|
29
|
+
|
30
|
+
puts "Currently selected:"
|
31
|
+
puts " " + modem.band
|
32
|
+
puts
|
33
|
+
|
34
|
+
|
35
|
+
if ARGV[0]
|
36
|
+
new_band = ARGV[0].downcase
|
37
|
+
|
38
|
+
begin
|
39
|
+
modem.band = new_band
|
40
|
+
puts "Switched to: "
|
41
|
+
puts " " + modem.band
|
42
|
+
|
43
|
+
rescue StandardError => err
|
44
|
+
puts "Error switching band: "
|
45
|
+
puts " " + err
|
46
|
+
end
|
47
|
+
|
48
|
+
puts
|
49
|
+
end
|
data/lib/rubygsm/core.rb
ADDED
@@ -0,0 +1,851 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#:include:../../README.rdoc
|
3
|
+
#:title:Ruby GSM
|
4
|
+
#--
|
5
|
+
# vim: noet
|
6
|
+
#++
|
7
|
+
|
8
|
+
# standard library
|
9
|
+
require "timeout.rb"
|
10
|
+
require "date.rb"
|
11
|
+
|
12
|
+
# gems (we're using the ruby-serialport gem
|
13
|
+
# now, so we can depend upon it in our spec)
|
14
|
+
require "rubygems"
|
15
|
+
require "serialport"
|
16
|
+
|
17
|
+
module Gsm
|
18
|
+
class Modem
|
19
|
+
include Timeout
|
20
|
+
|
21
|
+
|
22
|
+
attr_accessor :verbosity, :read_timeout
|
23
|
+
attr_reader :device, :port
|
24
|
+
|
25
|
+
# call-seq:
|
26
|
+
# Gsm::Modem.new(port, verbosity=:warn)
|
27
|
+
#
|
28
|
+
# Create a new instance, to initialize and communicate exclusively with a
|
29
|
+
# single modem device via the _port_ (which is usually either /dev/ttyS0
|
30
|
+
# or /dev/ttyUSB0), and start logging to *rubygsm.log* in the chdir.
|
31
|
+
def initialize(port=:auto, verbosity=:warn, baud=9600, cmd_delay=0.1)
|
32
|
+
|
33
|
+
# if no port was specified, we'll attempt to iterate
|
34
|
+
# all of the serial ports that i've ever seen gsm
|
35
|
+
# modems mounted on. this is kind of shaky, and
|
36
|
+
# only works well with a single modem. for now,
|
37
|
+
# we'll try: ttyS0, ttyUSB0, ttyACM0, ttyS1...
|
38
|
+
if port == :auto
|
39
|
+
@device, @port = catch(:found) do
|
40
|
+
0.upto(8) do |n|
|
41
|
+
["ttyS", "ttyUSB", "ttyACM"].each do |prefix|
|
42
|
+
try_port = "/dev/#{prefix}#{n}"
|
43
|
+
|
44
|
+
begin
|
45
|
+
# serialport args: port, baud, data bits, stop bits, parity
|
46
|
+
device = SerialPort.new(try_port, baud, 8, 1, SerialPort::NONE)
|
47
|
+
throw :found, [device, try_port]
|
48
|
+
|
49
|
+
rescue ArgumentError, Errno::ENOENT
|
50
|
+
# do nothing, just continue to
|
51
|
+
# try the next port in order
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# tried all ports, nothing worked
|
57
|
+
raise AutoDetectError
|
58
|
+
end
|
59
|
+
|
60
|
+
else
|
61
|
+
@device = SerialPort.new(port, baud, 8, 1, SerialPort::NONE)
|
62
|
+
@port = port
|
63
|
+
end
|
64
|
+
|
65
|
+
@cmd_delay = cmd_delay
|
66
|
+
@verbosity = verbosity
|
67
|
+
@read_timeout = 10
|
68
|
+
@locked_to = false
|
69
|
+
|
70
|
+
# keep track of the depth which each
|
71
|
+
# thread is indented in the log
|
72
|
+
@log_indents = {}
|
73
|
+
@log_indents.default = 0
|
74
|
+
|
75
|
+
# to keep multi-part messages until
|
76
|
+
# the last part is delivered
|
77
|
+
@multipart = {}
|
78
|
+
|
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
|
85
|
+
|
86
|
+
# to store incoming messages
|
87
|
+
# until they're dealt with by
|
88
|
+
# someone else, like a commander
|
89
|
+
@incoming = []
|
90
|
+
|
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
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
|
104
|
+
INCOMING_FMT = "%y/%m/%d,%H:%M:%S%Z" #:nodoc:
|
105
|
+
|
106
|
+
def parse_incoming_timestamp(ts)
|
107
|
+
# extract the weirdo quarter-hour timezone,
|
108
|
+
# convert it into a regular hourly offset
|
109
|
+
ts.sub! /(\d+)$/ do |m|
|
110
|
+
sprintf("%02d", (m.to_i/4))
|
111
|
+
end
|
112
|
+
|
113
|
+
# parse the timestamp, and attempt to re-align
|
114
|
+
# it according to the timezone we extracted
|
115
|
+
DateTime.strptime(ts, INCOMING_FMT)
|
116
|
+
end
|
117
|
+
|
118
|
+
def parse_incoming_sms!(lines)
|
119
|
+
n = 0
|
120
|
+
|
121
|
+
# iterate the lines like it's 1984
|
122
|
+
# (because we're patching the array,
|
123
|
+
# which is hard work for iterators)
|
124
|
+
while n < lines.length
|
125
|
+
|
126
|
+
# not a CMT string? ignore it
|
127
|
+
unless lines && lines[n] && lines[n][0,5] == "+CMT:"
|
128
|
+
n += 1
|
129
|
+
next
|
130
|
+
end
|
131
|
+
|
132
|
+
# since this line IS a CMT string (an incomming
|
133
|
+
# SMS), parse it and store it to deal with later
|
134
|
+
unless m = lines[n].match(/^\+CMT: "(.+?)",.*?,"(.+?)".*?$/)
|
135
|
+
err = "Couldn't parse CMT data: #{buf}"
|
136
|
+
raise RuntimeError.new(err)
|
137
|
+
end
|
138
|
+
|
139
|
+
# extract the meta-info from the CMT line,
|
140
|
+
# and the message from the FOLLOWING line
|
141
|
+
from, timestamp = *m.captures
|
142
|
+
msg = lines[n+1].strip
|
143
|
+
|
144
|
+
# notify the network that we accepted
|
145
|
+
# the incoming message (for read receipt)
|
146
|
+
# BEFORE pushing it to the incoming queue
|
147
|
+
# (to avoid really ugly race condition if
|
148
|
+
# the message is grabbed from the queue
|
149
|
+
# and responded to quickly, before we get
|
150
|
+
# a chance to issue at+cnma)
|
151
|
+
begin
|
152
|
+
command "AT+CNMA"
|
153
|
+
|
154
|
+
# not terribly important if it
|
155
|
+
# fails, even though it shouldn't
|
156
|
+
rescue Gsm::Error
|
157
|
+
log "Receipt acknowledgement (CNMA) was rejected"
|
158
|
+
end
|
159
|
+
|
160
|
+
# we might abort if this is
|
161
|
+
catch :skip_processing do
|
162
|
+
|
163
|
+
# multi-part messages begin with ASCII char 130
|
164
|
+
if (msg[0] == 130) and (msg[1].chr == "@")
|
165
|
+
text = msg[7,999]
|
166
|
+
|
167
|
+
# ensure we have a place for the incoming
|
168
|
+
# message part to live as they are delivered
|
169
|
+
@multipart[from] = []\
|
170
|
+
unless @multipart.has_key?(from)
|
171
|
+
|
172
|
+
# append THIS PART
|
173
|
+
@multipart[from].push(text)
|
174
|
+
|
175
|
+
# add useless message to log
|
176
|
+
part = @multipart[from].length
|
177
|
+
log "Received part #{part} of message from: #{from}"
|
178
|
+
|
179
|
+
# abort if this is not the last part
|
180
|
+
throw :skip_processing\
|
181
|
+
unless (msg[5] == 173)
|
182
|
+
|
183
|
+
# last part, so switch out the received
|
184
|
+
# part with the whole message, to be processed
|
185
|
+
# below (the sender and timestamp are the same
|
186
|
+
# for all parts, so no change needed there)
|
187
|
+
msg = @multipart[from].join("")
|
188
|
+
@multipart.delete(from)
|
189
|
+
end
|
190
|
+
|
191
|
+
# just in case it wasn't already obvious...
|
192
|
+
log "Received message from #{from}: #{msg}"
|
193
|
+
|
194
|
+
# store the incoming data to be picked up
|
195
|
+
# from the attr_accessor as a tuple (this
|
196
|
+
# is kind of ghetto, and WILL change later)
|
197
|
+
sent = parse_incoming_timestamp(timestamp)
|
198
|
+
@incoming.push Gsm::Incoming.new(self, from, sent, msg)
|
199
|
+
end
|
200
|
+
|
201
|
+
# drop the two CMT lines (meta-info and message),
|
202
|
+
# and patch the index to hit the next unchecked
|
203
|
+
# line during the next iteration
|
204
|
+
lines.slice!(n,2)
|
205
|
+
n -= 1
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
# write a string to the modem immediately,
|
211
|
+
# without waiting for the lock
|
212
|
+
def write(str)
|
213
|
+
log "Write: #{str.inspect}", :traffic
|
214
|
+
|
215
|
+
begin
|
216
|
+
str.each_byte do |b|
|
217
|
+
@device.putc(b.chr)
|
218
|
+
end
|
219
|
+
|
220
|
+
# the device couldn't be written to,
|
221
|
+
# which probably means that it has
|
222
|
+
# crashed or been unplugged
|
223
|
+
rescue Errno::EIO
|
224
|
+
raise Gsm::WriteError
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
|
229
|
+
# read from the modem (blocking) until
|
230
|
+
# the term character is hit, and return
|
231
|
+
def read(term=nil)
|
232
|
+
term = "\r\n" if term==nil
|
233
|
+
term = [term] unless term.is_a? Array
|
234
|
+
buf = ""
|
235
|
+
|
236
|
+
# include the terminator in the traffic dump,
|
237
|
+
# if it's anything other than the default
|
238
|
+
#suffix = (term != ["\r\n"]) ? " (term=#{term.inspect})" : ""
|
239
|
+
#log_incr "Read" + suffix, :traffic
|
240
|
+
|
241
|
+
begin
|
242
|
+
timeout(@read_timeout) do
|
243
|
+
while true do
|
244
|
+
char = @device.getc
|
245
|
+
|
246
|
+
# die if we couldn't read
|
247
|
+
# (nil signifies an error)
|
248
|
+
raise Gsm::ReadError\
|
249
|
+
if char.nil?
|
250
|
+
|
251
|
+
# convert the character to ascii,
|
252
|
+
# and append it to the tmp buffer
|
253
|
+
buf << sprintf("%c", char)
|
254
|
+
|
255
|
+
# if a terminator was just received,
|
256
|
+
# then return the current buffer
|
257
|
+
term.each do |t|
|
258
|
+
len = t.length
|
259
|
+
if buf[-len, len] == t
|
260
|
+
log "Read: #{buf.inspect}", :traffic
|
261
|
+
return buf.strip
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# reading took too long, so intercept
|
268
|
+
# and raise a more specific exception
|
269
|
+
rescue Timeout::Error
|
270
|
+
log = "Read: Timed out", :warn
|
271
|
+
raise TimeoutError
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
|
276
|
+
# issue a single command, and wait for the response
|
277
|
+
def command(cmd, resp_term=nil, write_term="\r")
|
278
|
+
begin
|
279
|
+
out = ""
|
280
|
+
log_incr "Command: #{cmd}"
|
281
|
+
|
282
|
+
exclusive do
|
283
|
+
write(cmd + write_term)
|
284
|
+
out = wait(resp_term)
|
285
|
+
end
|
286
|
+
|
287
|
+
# some hardware (my motorola phone) adds extra CRLFs
|
288
|
+
# to some responses. i see no reason that we need them
|
289
|
+
out.delete ""
|
290
|
+
|
291
|
+
# for the time being, ignore any unsolicited
|
292
|
+
# status messages. i can't seem to figure out
|
293
|
+
# how to disable them (AT+WIND=0 doesn't work)
|
294
|
+
out.delete_if do |line|
|
295
|
+
(line[0,6] == "+WIND:") or
|
296
|
+
(line[0,6] == "+CREG:") or
|
297
|
+
(line[0,7] == "+CGREG:")
|
298
|
+
end
|
299
|
+
|
300
|
+
# parse out any incoming sms that were bundled
|
301
|
+
# with this data (to be fetched later by an app)
|
302
|
+
parse_incoming_sms!(out)
|
303
|
+
|
304
|
+
# log the modified output
|
305
|
+
log_decr "=#{out.inspect}"
|
306
|
+
|
307
|
+
# rest up for a bit (modems are
|
308
|
+
# slow, and get confused easily)
|
309
|
+
sleep(@cmd_delay)
|
310
|
+
return out
|
311
|
+
|
312
|
+
# if the 515 (please wait) error was thrown,
|
313
|
+
# then automatically re-try the command after
|
314
|
+
# a short delay. for others, propagate
|
315
|
+
rescue Error => err
|
316
|
+
log "Rescued: #{err.desc}"
|
317
|
+
|
318
|
+
if (err.type == "CMS") and (err.code == 515)
|
319
|
+
sleep 2
|
320
|
+
retry
|
321
|
+
end
|
322
|
+
|
323
|
+
log_decr
|
324
|
+
raise
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
|
329
|
+
def query(cmd)
|
330
|
+
log_incr "Query: #{cmd}"
|
331
|
+
out = command cmd
|
332
|
+
|
333
|
+
# only very simple responses are supported
|
334
|
+
# (on purpose!) here - [response, crlf, ok]
|
335
|
+
if (out.length==2) and (out[1]=="OK")
|
336
|
+
log_decr "=#{out[0].inspect}"
|
337
|
+
return out[0]
|
338
|
+
|
339
|
+
else
|
340
|
+
err = "Invalid response: #{out.inspect}"
|
341
|
+
raise RuntimeError.new(err)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
|
346
|
+
# just wait for a response, by reading
|
347
|
+
# until an OK or ERROR terminator is hit
|
348
|
+
def wait(term=nil)
|
349
|
+
buffer = []
|
350
|
+
log_incr "Waiting for response"
|
351
|
+
|
352
|
+
while true do
|
353
|
+
buf = read(term)
|
354
|
+
buffer.push(buf)
|
355
|
+
|
356
|
+
# some errors contain useful error codes,
|
357
|
+
# so raise a proper error with a description
|
358
|
+
if m = buf.match(/^\+(CM[ES]) ERROR: (\d+)$/)
|
359
|
+
log_then_decr "!! Raising Gsm::Error #{$1} #{$2}"
|
360
|
+
raise Error.new(*m.captures)
|
361
|
+
end
|
362
|
+
|
363
|
+
# some errors are not so useful :|
|
364
|
+
if buf == "ERROR"
|
365
|
+
log_then_decr "!! Raising Gsm::Error"
|
366
|
+
raise Error
|
367
|
+
end
|
368
|
+
|
369
|
+
# most commands return OK upon success, except
|
370
|
+
# for those which prompt for more data (CMGS)
|
371
|
+
if (buf=="OK") or (buf==">")
|
372
|
+
log_decr "=#{buffer.inspect}"
|
373
|
+
return buffer
|
374
|
+
end
|
375
|
+
|
376
|
+
# some commands DO NOT respond with OK,
|
377
|
+
# even when they're successful, so check
|
378
|
+
# for those exceptions manually
|
379
|
+
if m = buf.match(/^\+CPIN: (.+)$/)
|
380
|
+
log_decr "=#{buffer.inspect}"
|
381
|
+
return buffer
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
|
387
|
+
def exclusive &blk
|
388
|
+
old_lock = nil
|
389
|
+
|
390
|
+
begin
|
391
|
+
|
392
|
+
# prevent other threads from issuing
|
393
|
+
# commands TO THIS MODDEM while this
|
394
|
+
# block is working. this does not lock
|
395
|
+
# threads, just the gsm device
|
396
|
+
if @locked_to and (@locked_to != Thread.current)
|
397
|
+
log "Locked by #{@locked_to["name"]}, waiting..."
|
398
|
+
|
399
|
+
# wait for the modem to become available,
|
400
|
+
# so we can issue commands from threads
|
401
|
+
while @locked_to
|
402
|
+
sleep 0.05
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
# we got the lock!
|
407
|
+
old_lock = @locked_to
|
408
|
+
@locked_to = Thread.current
|
409
|
+
log_incr "Got lock"
|
410
|
+
|
411
|
+
# perform the command while
|
412
|
+
# we have exclusive access
|
413
|
+
# to the modem device
|
414
|
+
yield
|
415
|
+
|
416
|
+
|
417
|
+
# something went bang, which happens, but
|
418
|
+
# just pass it on (after unlocking...)
|
419
|
+
rescue Gsm::Error
|
420
|
+
raise
|
421
|
+
|
422
|
+
|
423
|
+
# no message, but always un-
|
424
|
+
# indent subsequent log messages
|
425
|
+
# and RELEASE THE LOCK
|
426
|
+
ensure
|
427
|
+
@locked_to = old_lock
|
428
|
+
Thread.pass
|
429
|
+
log_decr
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
|
434
|
+
|
435
|
+
|
436
|
+
public
|
437
|
+
|
438
|
+
|
439
|
+
# call-seq:
|
440
|
+
# hardware => hash
|
441
|
+
#
|
442
|
+
# Returns a hash of containing information about the physical
|
443
|
+
# modem. The contents of each value are entirely manufacturer
|
444
|
+
# dependant, and vary wildly between devices.
|
445
|
+
#
|
446
|
+
# modem.hardware => { :manufacturer => "Multitech".
|
447
|
+
# :model => "MTCBA-G-F4",
|
448
|
+
# :revision => "123456789",
|
449
|
+
# :serial => "ABCD" }
|
450
|
+
def hardware
|
451
|
+
return {
|
452
|
+
:manufacturer => query("AT+CGMI"),
|
453
|
+
:model => query("AT+CGMM"),
|
454
|
+
:revision => query("AT+CGMR"),
|
455
|
+
:serial => query("AT+CGSN") }
|
456
|
+
end
|
457
|
+
|
458
|
+
|
459
|
+
# The values accepted and returned by the AT+WMBS
|
460
|
+
# command, mapped to frequency bands, in MHz. Copied
|
461
|
+
# directly from the MultiTech AT command-set reference
|
462
|
+
Bands = {
|
463
|
+
0 => "850",
|
464
|
+
1 => "900",
|
465
|
+
2 => "1800",
|
466
|
+
3 => "1900",
|
467
|
+
4 => "850/1900",
|
468
|
+
5 => "900E/1800",
|
469
|
+
6 => "900E/1900"
|
470
|
+
}
|
471
|
+
|
472
|
+
# call-seq:
|
473
|
+
# bands_available => array
|
474
|
+
#
|
475
|
+
# Returns an array containing the bands supported by
|
476
|
+
# the modem.
|
477
|
+
def bands_available
|
478
|
+
data = query("AT+WMBS=?")
|
479
|
+
|
480
|
+
# wmbs data is returned as something like:
|
481
|
+
# +WMBS: (0,1,2,3,4,5,6),(0-1)
|
482
|
+
# +WMBS: (0,3,4),(0-1)
|
483
|
+
# extract the numbers with a regex, and
|
484
|
+
# iterate each to resolve it to a more
|
485
|
+
# readable description
|
486
|
+
if m = data.match(/^\+WMBS: \(([\d,]+)\),/)
|
487
|
+
return m.captures[0].split(",").collect do |index|
|
488
|
+
Bands[index.to_i]
|
489
|
+
end
|
490
|
+
|
491
|
+
else
|
492
|
+
# Todo: Recover from this exception
|
493
|
+
err = "Not WMBS data: #{data.inspect}"
|
494
|
+
raise RuntimeError.new(err)
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
# call-seq:
|
499
|
+
# band => string
|
500
|
+
#
|
501
|
+
# Returns a string containing the band
|
502
|
+
# currently selected for use by the modem.
|
503
|
+
def band
|
504
|
+
data = query("AT+WMBS?")
|
505
|
+
if m = data.match(/^\+WMBS: (\d+),/)
|
506
|
+
return Bands[m.captures[0].to_i]
|
507
|
+
|
508
|
+
else
|
509
|
+
# Todo: Recover from this exception
|
510
|
+
err = "Not WMBS data: #{data.inspect}"
|
511
|
+
raise RuntimeError.new(err)
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
BandAreas = {
|
516
|
+
:usa => 4,
|
517
|
+
:africa => 5,
|
518
|
+
:europe => 5,
|
519
|
+
:asia => 5,
|
520
|
+
:mideast => 5
|
521
|
+
}
|
522
|
+
|
523
|
+
# call-seq:
|
524
|
+
# band=(_numeric_band_) => string
|
525
|
+
#
|
526
|
+
# Sets the band currently selected for use
|
527
|
+
# by the modem, using either a literal band
|
528
|
+
# number (passed directly to the modem, see
|
529
|
+
# Gsm::Modem.Bands) or a named area from
|
530
|
+
# Gsm::Modem.BandAreas:
|
531
|
+
#
|
532
|
+
# m = Gsm::Modem.new
|
533
|
+
# m.band = :usa => "850/1900"
|
534
|
+
# m.band = :africa => "900E/1800"
|
535
|
+
# m.band = :monkey => ArgumentError
|
536
|
+
#
|
537
|
+
# (Note that as usual, the United States of
|
538
|
+
# America is wearing its ass backwards.)
|
539
|
+
#
|
540
|
+
# Raises ArgumentError if an unrecognized band was
|
541
|
+
# given, or raises Gsm::Error if the modem does
|
542
|
+
# not support the given band.
|
543
|
+
def band=(new_band)
|
544
|
+
|
545
|
+
# resolve named bands into numeric
|
546
|
+
# (mhz values first, then band areas)
|
547
|
+
unless new_band.is_a?(Numeric)
|
548
|
+
|
549
|
+
if Bands.has_value?(new_band.to_s)
|
550
|
+
new_band = Bands.index(new_band.to_s)
|
551
|
+
|
552
|
+
elsif BandAreas.has_key?(new_band.to_sym)
|
553
|
+
new_band = BandAreas[new_band.to_sym]
|
554
|
+
|
555
|
+
else
|
556
|
+
err = "Invalid band: #{new_band}"
|
557
|
+
raise ArgumentError.new(err)
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
# set the band right now (second wmbs
|
562
|
+
# argument is: 0=NEXT-BOOT, 1=NOW). if it
|
563
|
+
# fails, allow Gsm::Error to propagate
|
564
|
+
command("AT+WMBS=#{new_band},1")
|
565
|
+
end
|
566
|
+
|
567
|
+
# call-seq:
|
568
|
+
# pin_required? => true or false
|
569
|
+
#
|
570
|
+
# Returns true if the modem is waiting for a SIM PIN. Some SIM cards will refuse
|
571
|
+
# to work until the correct four-digit PIN is provided via the _use_pin_ method.
|
572
|
+
def pin_required?
|
573
|
+
not command("AT+CPIN?").include?("+CPIN: READY")
|
574
|
+
end
|
575
|
+
|
576
|
+
|
577
|
+
# call-seq:
|
578
|
+
# use_pin(pin) => true or false
|
579
|
+
#
|
580
|
+
# Provide a SIM PIN to the modem, and return true if it was accepted.
|
581
|
+
def use_pin(pin)
|
582
|
+
|
583
|
+
# if the sim is already ready,
|
584
|
+
# this method isn't necessary
|
585
|
+
if pin_required?
|
586
|
+
begin
|
587
|
+
command "AT+CPIN=#{pin}"
|
588
|
+
|
589
|
+
# if the command failed, then
|
590
|
+
# the pin was not accepted
|
591
|
+
rescue Gsm::Error
|
592
|
+
return false
|
593
|
+
end
|
594
|
+
end
|
595
|
+
|
596
|
+
# no error = SIM
|
597
|
+
# PIN accepted!
|
598
|
+
true
|
599
|
+
end
|
600
|
+
|
601
|
+
|
602
|
+
# call-seq:
|
603
|
+
# signal => fixnum or nil
|
604
|
+
#
|
605
|
+
# Returns an fixnum between 1 and 99, representing the current
|
606
|
+
# signal strength of the GSM network, or nil if we don't know.
|
607
|
+
def signal_strength
|
608
|
+
data = query("AT+CSQ")
|
609
|
+
if m = data.match(/^\+CSQ: (\d+),/)
|
610
|
+
|
611
|
+
# 99 represents "not known or not detectable",
|
612
|
+
# but we'll use nil for that, since it's a bit
|
613
|
+
# more ruby-ish to test for boolean equality
|
614
|
+
csq = m.captures[0].to_i
|
615
|
+
return (csq<99) ? csq : nil
|
616
|
+
|
617
|
+
else
|
618
|
+
# Todo: Recover from this exception
|
619
|
+
err = "Not CSQ data: #{data.inspect}"
|
620
|
+
raise RuntimeError.new(err)
|
621
|
+
end
|
622
|
+
end
|
623
|
+
|
624
|
+
|
625
|
+
# call-seq:
|
626
|
+
# wait_for_network
|
627
|
+
#
|
628
|
+
# Blocks until the signal strength indicates that the
|
629
|
+
# device is active on the GSM network. It's a good idea
|
630
|
+
# to call this before trying to send or receive anything.
|
631
|
+
def wait_for_network
|
632
|
+
|
633
|
+
# keep retrying until the
|
634
|
+
# network comes up (if ever)
|
635
|
+
until csq = signal_strength
|
636
|
+
sleep 1
|
637
|
+
end
|
638
|
+
|
639
|
+
# return the last
|
640
|
+
# signal strength
|
641
|
+
return csq
|
642
|
+
end
|
643
|
+
|
644
|
+
|
645
|
+
# call-seq:
|
646
|
+
# send_sms(message) => true or false
|
647
|
+
# send_sms(recipient, text) => true or false
|
648
|
+
#
|
649
|
+
# Sends an SMS message, and returns true if the network
|
650
|
+
# accepted it for delivery. We currently can't handle read
|
651
|
+
# receipts, so have no way of confirming delivery.
|
652
|
+
#
|
653
|
+
# Note: the recipient is passed directly to the modem, which
|
654
|
+
# in turn passes it straight to the SMSC (sms message center).
|
655
|
+
# for maximum compatibility, use phone numbers in international
|
656
|
+
# format, including the *plus* and *country code*.
|
657
|
+
def send_sms(*args)
|
658
|
+
|
659
|
+
# extract values from Outgoing object.
|
660
|
+
# for now, this does not offer anything
|
661
|
+
# in addition to the recipient/text pair,
|
662
|
+
# but provides an upgrade path for future
|
663
|
+
# features (like FLASH and VALIDITY TIME)
|
664
|
+
if args.length == 1\
|
665
|
+
and args[0].is_a? Gsm::Outgoing
|
666
|
+
to = args[0].recipient
|
667
|
+
msg = args[0].text
|
668
|
+
|
669
|
+
# the < v0.4 arguments. maybe
|
670
|
+
# deprecate this one day
|
671
|
+
elsif args.length == 2
|
672
|
+
to, msg = *args
|
673
|
+
|
674
|
+
else
|
675
|
+
raise ArgumentError,\
|
676
|
+
"The Gsm::Modem#send_sms method accepts" +\
|
677
|
+
"a single Gsm::Outgoing instance, " +\
|
678
|
+
"or recipient and text strings"
|
679
|
+
end
|
680
|
+
|
681
|
+
# the number must be in the international
|
682
|
+
# format for some SMSCs (notably, the one
|
683
|
+
# i'm on right now) so maybe add a PLUS
|
684
|
+
#to = "+#{to}" unless(to[0,1]=="+")
|
685
|
+
|
686
|
+
# 1..9 is a special number which does notm
|
687
|
+
# result in a real sms being sent (see inject.rb)
|
688
|
+
if to == "+123456789"
|
689
|
+
log "Not sending test message: #{msg}"
|
690
|
+
return false
|
691
|
+
end
|
692
|
+
|
693
|
+
# block the receiving thread while
|
694
|
+
# we're sending. it can take some time
|
695
|
+
exclusive do
|
696
|
+
log_incr "Sending SMS to #{to}: #{msg}"
|
697
|
+
|
698
|
+
# initiate the sms, and wait for either
|
699
|
+
# the text prompt or an error message
|
700
|
+
command "AT+CMGS=\"#{to}\"", ["\r\n", "> "]
|
701
|
+
|
702
|
+
begin
|
703
|
+
# send the sms, and wait until
|
704
|
+
# it is accepted or rejected
|
705
|
+
write "#{msg}#{26.chr}"
|
706
|
+
wait
|
707
|
+
|
708
|
+
# if something went wrong, we are
|
709
|
+
# be stuck in entry mode (which will
|
710
|
+
# result in someone getting a bunch
|
711
|
+
# of AT commands via sms!) so send
|
712
|
+
# an escpae, to... escape
|
713
|
+
rescue Exception, Timeout::Error => err
|
714
|
+
log "Rescued #{err.desc}"
|
715
|
+
return false
|
716
|
+
#write 27.chr
|
717
|
+
#wait
|
718
|
+
end
|
719
|
+
|
720
|
+
log_decr
|
721
|
+
end
|
722
|
+
|
723
|
+
# if no error was raised,
|
724
|
+
# then the message was sent
|
725
|
+
return true
|
726
|
+
end
|
727
|
+
|
728
|
+
|
729
|
+
# call-seq:
|
730
|
+
# receive(callback_method, interval=5, join_thread=false)
|
731
|
+
#
|
732
|
+
# Starts a new thread, which polls the device every _interval_
|
733
|
+
# seconds to capture incoming SMS and call _callback_method_
|
734
|
+
# for each.
|
735
|
+
#
|
736
|
+
# class Receiver
|
737
|
+
# def incoming(msg)
|
738
|
+
# puts "From #{msg.from} at #{msg.sent}:", msg.text
|
739
|
+
# end
|
740
|
+
# end
|
741
|
+
#
|
742
|
+
# # create the instances,
|
743
|
+
# # and start receiving
|
744
|
+
# rcv = Receiver.new
|
745
|
+
# m = Gsm::Modem.new "/dev/ttyS0"
|
746
|
+
# m.receive rcv.method :incoming
|
747
|
+
#
|
748
|
+
# # block until ctrl+c
|
749
|
+
# while(true) { sleep 2 }
|
750
|
+
#
|
751
|
+
# Note: New messages may arrive at any time, even if this method's
|
752
|
+
# receiver thread isn't waiting to process them. They are not lost,
|
753
|
+
# but cached in @incoming until this method is called.
|
754
|
+
def receive(callback, interval=5, join_thread=false)
|
755
|
+
@polled = 0
|
756
|
+
|
757
|
+
@thr = Thread.new do
|
758
|
+
Thread.current["name"] = "receiver"
|
759
|
+
|
760
|
+
# keep on receiving forever
|
761
|
+
while true
|
762
|
+
command "AT"
|
763
|
+
check_for_inbox_messages
|
764
|
+
|
765
|
+
# enable new message notification mode
|
766
|
+
# every ten intevals, in case the
|
767
|
+
# modem "forgets" (power cycle, etc)
|
768
|
+
if (@polled % 10) == 0
|
769
|
+
#COMPAT command "AT+CNMI=2,2,0,0,0"
|
770
|
+
end
|
771
|
+
|
772
|
+
# if there are any new incoming messages,
|
773
|
+
# iterate, and pass each to the receiver
|
774
|
+
# in the same format that they were built
|
775
|
+
# back in _parse_incoming_sms!_
|
776
|
+
unless @incoming.empty?
|
777
|
+
@incoming.each do |msg|
|
778
|
+
begin
|
779
|
+
callback.call(msg)
|
780
|
+
|
781
|
+
rescue StandardError => err
|
782
|
+
log "Error in callback: #{err}"
|
783
|
+
end
|
784
|
+
end
|
785
|
+
|
786
|
+
# we have dealt with all of the pending
|
787
|
+
# messages. todo: this is a ridiculous
|
788
|
+
# race condition, and i fail at ruby
|
789
|
+
@incoming.clear
|
790
|
+
end
|
791
|
+
|
792
|
+
# re-poll every
|
793
|
+
# five seconds
|
794
|
+
sleep(interval)
|
795
|
+
@polled += 1
|
796
|
+
end
|
797
|
+
end
|
798
|
+
|
799
|
+
# it's sometimes handy to run single-
|
800
|
+
# threaded (like debugging handsets)
|
801
|
+
@thr.join if join_thread
|
802
|
+
end
|
803
|
+
|
804
|
+
def select_default_mailbox
|
805
|
+
# Eventually we will select the first mailbox as the default
|
806
|
+
result = command("AT+CPMS=?")
|
807
|
+
boxes = result.first.scan(/\"(\w+)\"/).flatten #"
|
808
|
+
mailbox = boxes.first
|
809
|
+
command "AT+CPMS=\"#{mailbox}\""
|
810
|
+
mailbox
|
811
|
+
rescue
|
812
|
+
raise RuntimeError.new("Could not select the default mailbox")
|
813
|
+
nil
|
814
|
+
end
|
815
|
+
|
816
|
+
# Could accomplish this with a CMGL=\"REC UNREAD\" too
|
817
|
+
def check_for_inbox_messages
|
818
|
+
return unless select_default_mailbox
|
819
|
+
|
820
|
+
# Try to read the first message from the box (should this be 0?)
|
821
|
+
begin
|
822
|
+
out = command("AT+CMGR=1")
|
823
|
+
rescue
|
824
|
+
return
|
825
|
+
end
|
826
|
+
return if out.nil? || out.empty?
|
827
|
+
|
828
|
+
# Delete that message
|
829
|
+
command("AT+CMGD=1")
|
830
|
+
|
831
|
+
# Break that down
|
832
|
+
header = out.first
|
833
|
+
msg = out[1..out.size-2].join("\n")
|
834
|
+
validity = out.last
|
835
|
+
|
836
|
+
# Read the header
|
837
|
+
header = header.scan(/\"([^"]+)\"/).flatten #"
|
838
|
+
status = header[0]
|
839
|
+
from = header[1]
|
840
|
+
timestamp = header[2]
|
841
|
+
# Parsing this using the incoming format failed, it need %Y instead of %y
|
842
|
+
sent = DateTime.parse(timestamp)
|
843
|
+
|
844
|
+
# just in case it wasn't already obvious...
|
845
|
+
log "Received message from #{from}: #{msg}"
|
846
|
+
|
847
|
+
@incoming.push Gsm::Incoming.new(self, from, sent, msg)
|
848
|
+
end
|
849
|
+
|
850
|
+
end # Modem
|
851
|
+
end # Gsm
|
@@ -0,0 +1,128 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#:title:Ruby GSM Errors
|
3
|
+
#--
|
4
|
+
# vim: noet
|
5
|
+
#++
|
6
|
+
|
7
|
+
module Gsm
|
8
|
+
class Error < StandardError
|
9
|
+
ERRORS = {
|
10
|
+
"CME" => {
|
11
|
+
3 => "Operation not allowed",
|
12
|
+
4 => "Operation not supported",
|
13
|
+
5 => "PH-SIM PIN required (SIM lock)",
|
14
|
+
10 => "SIM not inserted",
|
15
|
+
11 => "SIM PIN required",
|
16
|
+
12 => "SIM PUK required",
|
17
|
+
13 => "SIM failure",
|
18
|
+
16 => "Incorrect password",
|
19
|
+
17 => "SIM PIN2 required",
|
20
|
+
18 => "SIM PUK2 required",
|
21
|
+
20 => "Memory full",
|
22
|
+
21 => "Invalid index",
|
23
|
+
22 => "Not found",
|
24
|
+
24 => "Text string too long",
|
25
|
+
26 => "Dial string too long",
|
26
|
+
27 => "Invalid characters in dial string",
|
27
|
+
30 => "No network service",
|
28
|
+
32 => "Network not allowed – emergency calls only",
|
29
|
+
40 => "Network personal PIN required (Network lock)",
|
30
|
+
103 => "Illegal MS (#3)",
|
31
|
+
106 => "Illegal ME (#6)",
|
32
|
+
107 => "GPRS services not allowed",
|
33
|
+
111 => "PLMN not allowed",
|
34
|
+
112 => "Location area not allowed",
|
35
|
+
113 => "Roaming not allowed in this area",
|
36
|
+
132 => "Service option not supported",
|
37
|
+
133 => "Requested service option not subscribed",
|
38
|
+
134 => "Service option temporarily out of order",
|
39
|
+
148 => "unspecified GPRS error",
|
40
|
+
149 => "PDP authentication failure",
|
41
|
+
150 => "Invalid mobile class"
|
42
|
+
},
|
43
|
+
|
44
|
+
# message service errors
|
45
|
+
"CMS" => {
|
46
|
+
301 => "SMS service of ME reserved",
|
47
|
+
302 => "Operation not allowed",
|
48
|
+
303 => "Operation not supported",
|
49
|
+
304 => "Invalid PDU mode parameter",
|
50
|
+
305 => "Invalid text mode parameter",
|
51
|
+
310 => "SIM not inserted",
|
52
|
+
311 => "SIM PIN required",
|
53
|
+
312 => "PH-SIM PIN required",
|
54
|
+
313 => "SIM failure",
|
55
|
+
316 => "SIM PUK required",
|
56
|
+
317 => "SIM PIN2 required",
|
57
|
+
318 => "SIM PUK2 required",
|
58
|
+
321 => "Invalid memory index",
|
59
|
+
322 => "SIM memory full",
|
60
|
+
330 => "SC address unknown",
|
61
|
+
340 => "No +CNMA acknowledgement expected",
|
62
|
+
|
63
|
+
# specific error result codes (also from +CMS ERROR)
|
64
|
+
500 => "Unknown error",
|
65
|
+
512 => "MM establishment failure (for SMS)",
|
66
|
+
513 => "Lower layer failure (for SMS)",
|
67
|
+
514 => "CP error (for SMS)",
|
68
|
+
515 => "Please wait, init or command processing in progress",
|
69
|
+
517 => "SIM Toolkit facility not supported",
|
70
|
+
518 => "SIM Toolkit indication not received",
|
71
|
+
519 => "Reset product to activate or change new echo cancellation algo",
|
72
|
+
520 => "Automatic abort about get PLMN list for an incomming call",
|
73
|
+
526 => "PIN deactivation forbidden with this SIM card",
|
74
|
+
527 => "Please wait, RR or MM is busy. Retry your selection later",
|
75
|
+
528 => "Location update failure. Emergency calls only",
|
76
|
+
529 => "PLMN selection failure. Emergency calls only",
|
77
|
+
531 => "SMS not send: the <da> is not in FDN phonebook, and FDN lock is enabled (for SMS)"
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
attr_reader :type, :code
|
82
|
+
def initialize(type=nil, code=nil)
|
83
|
+
@code = code.to_i
|
84
|
+
@type = type
|
85
|
+
end
|
86
|
+
|
87
|
+
def desc
|
88
|
+
# attempt to return something useful
|
89
|
+
return(ERRORS[@type][@code])\
|
90
|
+
if(@type and ERRORS[@type] and @code and ERRORS[@type][@code])
|
91
|
+
|
92
|
+
# fall back to something not-so useful
|
93
|
+
return "Unknown error (unrecognized command?) " +\
|
94
|
+
"[type=#{@type}] [code=#{code}]"
|
95
|
+
end
|
96
|
+
|
97
|
+
alias :to_s :desc
|
98
|
+
end
|
99
|
+
|
100
|
+
# TODO: what the hell is going on with
|
101
|
+
# all these "desc" methods? refactor this
|
102
|
+
|
103
|
+
class TimeoutError < Error #:nodoc:
|
104
|
+
def desc
|
105
|
+
"The command timed out"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class WriteError < Error #:nodoc:
|
110
|
+
def desc
|
111
|
+
"The modem couldn't be written to. It " +\
|
112
|
+
"may have crashed or been unplugged"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class ReadError < Error #:nodoc:
|
117
|
+
def desc
|
118
|
+
"The modem couldn't be read from. It " +\
|
119
|
+
"may have crashed or been unplugged"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class AutoDetectError < Error #:nodoc:
|
124
|
+
def desc
|
125
|
+
"No modem could be auto-detected."
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/lib/rubygsm/log.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: noet
|
3
|
+
|
4
|
+
module Gsm
|
5
|
+
class Modem
|
6
|
+
private
|
7
|
+
|
8
|
+
# Symbols accepted by the Gsm::Modem.new _verbosity_
|
9
|
+
# argument. Each level includes all of the levels
|
10
|
+
# below it (ie. :debug includes all :warn messages)
|
11
|
+
LOG_LEVELS = {
|
12
|
+
:file => 5,
|
13
|
+
:traffic => 4,
|
14
|
+
:debug => 3,
|
15
|
+
:warn => 2,
|
16
|
+
:error => 1 }
|
17
|
+
|
18
|
+
def log(msg, level=:debug)
|
19
|
+
ind = " " * (@log_indents[Thread.current] or 0)
|
20
|
+
|
21
|
+
# create a
|
22
|
+
thr = Thread.current["name"]
|
23
|
+
thr = (thr.nil?) ? "" : "[#{thr}] "
|
24
|
+
|
25
|
+
# dump (almost) everything to file
|
26
|
+
if LOG_LEVELS[level] >= LOG_LEVELS[:debug]\
|
27
|
+
or level == :file
|
28
|
+
|
29
|
+
@log.puts thr + ind + msg
|
30
|
+
@log.flush
|
31
|
+
end
|
32
|
+
|
33
|
+
# also print to the rolling
|
34
|
+
# screen log, if necessary
|
35
|
+
if LOG_LEVELS[@verbosity] >= LOG_LEVELS[level]
|
36
|
+
$stderr.puts thr + ind + msg
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# log a message, and increment future messages
|
41
|
+
# in this thread. useful for nesting logic
|
42
|
+
def log_incr(*args)
|
43
|
+
log(*args) unless args.empty?
|
44
|
+
@log_indents[Thread.current] += 1
|
45
|
+
end
|
46
|
+
|
47
|
+
# close the logical block, and (optionally) log
|
48
|
+
def log_decr(*args)
|
49
|
+
@log_indents[Thread.current] -= 1\
|
50
|
+
if @log_indents[Thread.current] > 0
|
51
|
+
log(*args) unless args.empty?
|
52
|
+
end
|
53
|
+
|
54
|
+
# the last message in a logical block
|
55
|
+
def log_then_decr(*args)
|
56
|
+
log(*args)
|
57
|
+
log_decr
|
58
|
+
end
|
59
|
+
end # Modem
|
60
|
+
end # Gsm
|
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: noet
|
3
|
+
|
4
|
+
module Gsm
|
5
|
+
class Incoming
|
6
|
+
attr_reader :device, :sender, :sent, :received, :text
|
7
|
+
|
8
|
+
def initialize(device, sender, sent, text)
|
9
|
+
|
10
|
+
# move all arguments into read-only
|
11
|
+
# attributes. ugly, but Struct only
|
12
|
+
# supports read/write attrs
|
13
|
+
@device = device
|
14
|
+
@sender = sender
|
15
|
+
@sent = sent
|
16
|
+
@text = text
|
17
|
+
|
18
|
+
# assume that the message was
|
19
|
+
# received right now, since we
|
20
|
+
# don't have an incoming buffer
|
21
|
+
@received = Time.now
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_a
|
25
|
+
[@device, @sender, @sent, @text]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: noet
|
3
|
+
|
4
|
+
module Gsm
|
5
|
+
class Outgoing
|
6
|
+
attr_accessor :recipient, :text
|
7
|
+
attr_reader :device, :sent
|
8
|
+
|
9
|
+
def initialize(device, recipient=nil, text=nil)
|
10
|
+
|
11
|
+
# check that the device is
|
12
|
+
#raise ArgumentError, "Invalid device"\
|
13
|
+
# unless device.respond_to?(:send_sms)
|
14
|
+
|
15
|
+
# init the instance vars
|
16
|
+
@recipient = recipient
|
17
|
+
@device = device
|
18
|
+
@text = text
|
19
|
+
end
|
20
|
+
|
21
|
+
def send!
|
22
|
+
@device.send_sms(self)
|
23
|
+
@sent = Time.now
|
24
|
+
|
25
|
+
# once sent, allow no
|
26
|
+
# more modifications
|
27
|
+
freeze
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/rubygsm.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: noet
|
3
|
+
|
4
|
+
dir = File.dirname(__FILE__)
|
5
|
+
require "#{dir}/rubygsm/core.rb"
|
6
|
+
require "#{dir}/rubygsm/errors.rb"
|
7
|
+
require "#{dir}/rubygsm/log.rb"
|
8
|
+
|
9
|
+
# messages are now passed around
|
10
|
+
# using objects, rather than flat
|
11
|
+
# arguments (from, time, msg, etc)
|
12
|
+
require "#{dir}/rubygsm/msg/incoming.rb"
|
13
|
+
require "#{dir}/rubygsm/msg/outgoing.rb"
|
14
|
+
|
15
|
+
# during development, it's important to EXPLODE
|
16
|
+
# as early as possible when something goes wrong
|
17
|
+
Thread.abort_on_exception = true
|
18
|
+
Thread.current["name"] = "main"
|
data/rubygsm.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "rubygsm"
|
3
|
+
s.version = "0.3.1"
|
4
|
+
s.date = "2009-01-09"
|
5
|
+
s.summary = "Send and receive SMS with a GSM modem"
|
6
|
+
s.email = "adam.mckaig@gmail.com"
|
7
|
+
s.homepage = "http://github.com/adammck/rubygsm"
|
8
|
+
s.authors = ["Adam Mckaig"]
|
9
|
+
s.has_rdoc = true
|
10
|
+
|
11
|
+
s.files = [
|
12
|
+
"rubygsm.gemspec",
|
13
|
+
"README.rdoc",
|
14
|
+
"lib/rubygsm.rb",
|
15
|
+
"lib/rubygsm/core.rb",
|
16
|
+
"lib/rubygsm/errors.rb",
|
17
|
+
"lib/rubygsm/log.rb",
|
18
|
+
"lib/rubygsm/msg/incoming.rb",
|
19
|
+
"lib/rubygsm/msg/outgoing.rb",
|
20
|
+
"bin/gsm-modem-band"
|
21
|
+
]
|
22
|
+
|
23
|
+
s.executables = [
|
24
|
+
"gsm-modem-band"
|
25
|
+
]
|
26
|
+
|
27
|
+
s.add_dependency("toholio-serialport", ["> 0.7.1"])
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jeffrafter-rubygsm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adam Mckaig
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-01-09 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: toholio-serialport
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.7.1
|
23
|
+
version:
|
24
|
+
description:
|
25
|
+
email: adam.mckaig@gmail.com
|
26
|
+
executables:
|
27
|
+
- gsm-modem-band
|
28
|
+
extensions: []
|
29
|
+
|
30
|
+
extra_rdoc_files: []
|
31
|
+
|
32
|
+
files:
|
33
|
+
- rubygsm.gemspec
|
34
|
+
- README.rdoc
|
35
|
+
- lib/rubygsm.rb
|
36
|
+
- lib/rubygsm/core.rb
|
37
|
+
- lib/rubygsm/errors.rb
|
38
|
+
- lib/rubygsm/log.rb
|
39
|
+
- lib/rubygsm/msg/incoming.rb
|
40
|
+
- lib/rubygsm/msg/outgoing.rb
|
41
|
+
- bin/gsm-modem-band
|
42
|
+
has_rdoc: true
|
43
|
+
homepage: http://github.com/adammck/rubygsm
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 1.2.0
|
65
|
+
signing_key:
|
66
|
+
specification_version: 2
|
67
|
+
summary: Send and receive SMS with a GSM modem
|
68
|
+
test_files: []
|
69
|
+
|