jeffrafter-rubygsm 0.3.1
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/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
|
+
|