adammck-rubygsm 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ # included in bin/gsm-app-reverse
7
+
8
+ class ReverseApp
9
+ def initialize(gsm)
10
+ gsm.receive(method(:incoming))
11
+ @gsm = gsm
12
+ end
13
+
14
+ def incoming(from, datetime, message)
15
+ @gsm.send(from, message.reverse)
16
+ end
17
+ end
18
+
19
+ gsm = GsmModem.new("/dev/ttyS0")
20
+ ReverseApp.new(gsm)
21
+
22
+
23
+ === Installing
24
+ RubyGSM is distributed via GitHub[http://github.com/adammck/rubygsm], which you must
25
+ add as a gem source before installing:
26
+
27
+ $ sudo gem sources -a http://gems.github.com
28
+
29
+ The library depends upon ruby-serialport, which is currently maintained on GitHub by
30
+ Toholio[http://github.com/toholio/ruby-serialport]. This library is included as a Gem
31
+ dependancy, so to install both:
32
+
33
+ $ sudo gem install adammck-rubygsm
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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: noet
3
+
4
+ require "rubygsm/core.rb"
5
+ require "rubygsm/errors.rb"
6
+ require "rubygsm/lib.rb"
@@ -0,0 +1,687 @@
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
+
18
+ class GsmModem
19
+ include Timeout
20
+
21
+
22
+ attr_accessor :verbosity, :read_timeout
23
+ attr_reader :device
24
+
25
+ # call-seq:
26
+ # GsmModem.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, verbosity=:warn, baud=9600, cmd_delay=0.1)
32
+
33
+ # port, baud, data bits, stop bits, parity
34
+ @device = SerialPort.new(port, baud, 8, 1, SerialPort::NONE)
35
+
36
+ @cmd_delay = cmd_delay
37
+ @verbosity = verbosity
38
+ @read_timeout = 10
39
+ @locked_to = false
40
+
41
+ # keep track of the depth which each
42
+ # thread is indented in the log
43
+ @log_indents = {}
44
+ @log_indents.default = 0
45
+
46
+ # to keep multi-part messages until
47
+ # the last part is delivered
48
+ @multipart = {}
49
+
50
+ # (re-) open the full log file
51
+ @log = File.new "rubygsm.log", "w"
52
+
53
+ # initialization message (yes, it's underlined)
54
+ msg = "RubyGSM Initialized at: #{Time.now}"
55
+ log msg + "\n" + ("=" * msg.length), :file
56
+
57
+ # to store incoming messages
58
+ # until they're dealt with by
59
+ # someone else, like a commander
60
+ @incoming = []
61
+
62
+ # initialize the modem
63
+ command "ATE0" # echo off
64
+ command "AT+CMEE=1" # useful errors
65
+ command "AT+WIND=0" # no notifications
66
+ command "AT+CMGF=1" # switch to text mode
67
+ end
68
+
69
+
70
+
71
+
72
+ private
73
+
74
+
75
+ INCOMING_FMT = "%y/%m/%d,%H:%M:%S%Z" #:nodoc:
76
+
77
+ def parse_incoming_timestamp(ts)
78
+ # extract the weirdo quarter-hour timezone,
79
+ # convert it into a regular hourly offset
80
+ ts.sub! /(\d+)$/ do |m|
81
+ sprintf("%02d", (m.to_i/4))
82
+ end
83
+
84
+ # parse the timestamp, and attempt to re-align
85
+ # it according to the timezone we extracted
86
+ DateTime.strptime(ts, INCOMING_FMT)
87
+ end
88
+
89
+ def parse_incoming_sms!(lines)
90
+ n = 0
91
+
92
+ # iterate the lines like it's 1984
93
+ # (because we're patching the array,
94
+ # which is hard work for iterators)
95
+ while n < lines.length
96
+
97
+ # not a CMT string? ignore it
98
+ unless lines && lines[n] && lines[n][0,5] == "+CMT:"
99
+ n += 1
100
+ next
101
+ end
102
+
103
+ # since this line IS a CMT string (an incomming
104
+ # SMS), parse it and store it to deal with later
105
+ unless m = lines[n].match(/^\+CMT: "(.+?)",.*?,"(.+?)".*?$/)
106
+ err = "Couldn't parse CMT data: #{buf}"
107
+ raise RuntimeError.new(err)
108
+ end
109
+
110
+ # extract the meta-info from the CMT line,
111
+ # and the message from the FOLLOWING line
112
+ from, timestamp = *m.captures
113
+ msg = lines[n+1].strip
114
+
115
+ # notify the network that we accepted
116
+ # the incoming message (for read receipt)
117
+ # BEFORE pushing it to the incoming queue
118
+ # (to avoid really ugly race condition)
119
+ command "AT+CNMA"
120
+
121
+ # we might abort if this is
122
+ catch :skip_processing do
123
+
124
+ # multi-part messages begin with ASCII char 130
125
+ if (msg[0] == 130) and (msg[1].chr == "@")
126
+ text = msg[7,999]
127
+
128
+ # ensure we have a place for the incoming
129
+ # message part to live as they are delivered
130
+ @multipart[from] = []\
131
+ unless @multipart.has_key?(from)
132
+
133
+ # append THIS PART
134
+ @multipart[from].push(text)
135
+
136
+ # add useless message to log
137
+ part = @multipart[from].length
138
+ log "Received part #{part} of message from: #{from}"
139
+
140
+ # abort if this is not the last part
141
+ throw :skip_processing\
142
+ unless (msg[5] == 173)
143
+
144
+ # last part, so switch out the received
145
+ # part with the whole message, to be processed
146
+ # below (the sender and timestamp are the same
147
+ # for all parts, so no change needed there)
148
+ msg = @multipart[from].join("")
149
+ @multipart.delete(from)
150
+ end
151
+
152
+ # just in case it wasn't already obvious...
153
+ log "Received message from #{from}: #{msg}"
154
+
155
+ # store the incoming data to be picked up
156
+ # from the attr_accessor as a tuple (this
157
+ # is kind of ghetto, and WILL change later)
158
+ dt = parse_incoming_timestamp(timestamp)
159
+ @incoming.push [from, dt, msg]
160
+ end
161
+
162
+ # drop the two CMT lines (meta-info and message),
163
+ # and patch the index to hit the next unchecked
164
+ # line during the next iteration
165
+ lines.slice!(n,2)
166
+ n -= 1
167
+ end
168
+ end
169
+
170
+
171
+ # write a string to the modem immediately,
172
+ # without waiting for the lock
173
+ def write(str)
174
+ log "Write: #{str.inspect}", :traffic
175
+
176
+ begin
177
+ str.each_byte do |b|
178
+ @device.putc(b.chr)
179
+ end
180
+
181
+ # the device couldn't be written to,
182
+ # which probably means that it has
183
+ # crashed or been unplugged
184
+ rescue Errno::EIO
185
+ raise GsmModem::WriteError
186
+ end
187
+ end
188
+
189
+
190
+ # read from the modem (blocking) until
191
+ # the term character is hit, and return
192
+ def read(term=nil)
193
+ term = "\r\n" if term==nil
194
+ term = [term] unless term.is_a? Array
195
+ buf = ""
196
+
197
+ # include the terminator in the traffic dump,
198
+ # if it's anything other than the default
199
+ #suffix = (term != ["\r\n"]) ? " (term=#{term.inspect})" : ""
200
+ #log_incr "Read" + suffix, :traffic
201
+
202
+ begin
203
+ timeout(@read_timeout) do
204
+ while true do
205
+ char = @device.getc
206
+
207
+ # die if we couldn't read
208
+ # (nil signifies an error)
209
+ raise GsmModem::ReadError\
210
+ if char.nil?
211
+
212
+ # convert the character to ascii,
213
+ # and append it to the tmp buffer
214
+ buf << sprintf("%c", char)
215
+
216
+ # if a terminator was just received,
217
+ # then return the current buffer
218
+ term.each do |t|
219
+ len = t.length
220
+ if buf[-len, len] == t
221
+ log "Read: #{buf.inspect}", :traffic
222
+ return buf.strip
223
+ end
224
+ end
225
+ end
226
+ end
227
+
228
+ # reading took too long, so intercept
229
+ # and raise a more specific exception
230
+ rescue Timeout::Error
231
+ log = "Read: Timed out", :warn
232
+ raise TimeoutError
233
+ end
234
+ end
235
+
236
+
237
+ # issue a single command, and wait for the response
238
+ def command(cmd, resp_term=nil, write_term="\r")
239
+ begin
240
+ out = ""
241
+ log_incr "Command: #{cmd}"
242
+
243
+ exclusive do
244
+ write(cmd + write_term)
245
+ out = wait(resp_term)
246
+ end
247
+
248
+ # some hardware (my motorola phone) adds extra CRLFs
249
+ # to some responses. i see no reason that we need them
250
+ out.delete ""
251
+
252
+ # for the time being, ignore any unsolicited
253
+ # status messages. i can't seem to figure out
254
+ # how to disable them (AT+WIND=0 doesn't work)
255
+ out.delete_if do |line|
256
+ (line[0,6] == "+WIND:") or
257
+ (line[0,6] == "+CREG:") or
258
+ (line[0,7] == "+CGREG:")
259
+ end
260
+
261
+ # parse out any incoming sms that were bundled
262
+ # with this data (to be fetched later by an app)
263
+ parse_incoming_sms!(out)
264
+
265
+ # log the modified output
266
+ log_decr "=#{out.inspect}"
267
+
268
+ # rest up for a bit (modems are
269
+ # slow, and get confused easily)
270
+ sleep(@cmd_delay)
271
+ return out
272
+
273
+ # if the 515 (please wait) error was thrown,
274
+ # then automatically re-try the command after
275
+ # a short delay. for others, propagate
276
+ rescue Error => err
277
+ log "Rescued: #{err.desc}"
278
+
279
+ if (err.type == "CMS") and (err.code == 515)
280
+ sleep 2
281
+ retry
282
+ end
283
+
284
+ log_decr
285
+ raise
286
+ end
287
+ end
288
+
289
+
290
+ def query(cmd)
291
+ log_incr "Query: #{cmd}"
292
+ out = command cmd
293
+
294
+ # only very simple responses are supported
295
+ # (on purpose!) here - [response, crlf, ok]
296
+ if (out.length==2) and (out[1]=="OK")
297
+ log_decr "=#{out[0].inspect}"
298
+ return out[0]
299
+
300
+ else
301
+ err = "Invalid response: #{out.inspect}"
302
+ raise RuntimeError.new(err)
303
+ end
304
+ end
305
+
306
+
307
+ # just wait for a response, by reading
308
+ # until an OK or ERROR terminator is hit
309
+ def wait(term=nil)
310
+ buffer = []
311
+ log_incr "Waiting for response"
312
+
313
+ while true do
314
+ buf = read(term)
315
+ buffer.push(buf)
316
+
317
+ # some errors contain useful error codes,
318
+ # so raise a proper error with a description
319
+ if m = buf.match(/^\+(CM[ES]) ERROR: (\d+)$/)
320
+ log_then_decr "!! Raising GsmModem::Error #{$1} #{$2}"
321
+ raise Error.new(*m.captures)
322
+ end
323
+
324
+ # some errors are not so useful :|
325
+ if buf == "ERROR"
326
+ log_then_decr "!! Raising GsmModem::Error"
327
+ raise Error
328
+ end
329
+
330
+ # most commands return OK upon success, except
331
+ # for those which prompt for more data (CMGS)
332
+ if (buf=="OK") or (buf==">")
333
+ log_decr "=#{buffer.inspect}"
334
+ return buffer
335
+ end
336
+
337
+ # some commands DO NOT respond with OK,
338
+ # even when they're successful, so check
339
+ # for those exceptions manually
340
+ if m = buf.match(/^\+CPIN: (.+)$/)
341
+ log_decr "=#{buffer.inspect}"
342
+ return buffer
343
+ end
344
+ end
345
+ end
346
+
347
+
348
+ def exclusive &blk
349
+ old_lock = nil
350
+
351
+ begin
352
+
353
+ # prevent other threads from issuing
354
+ # commands while this block is working
355
+ if @locked_to and (@locked_to != Thread.current)
356
+ log "Locked by #{@locked_to["name"]}, waiting..."
357
+
358
+ # wait for the modem to become available,
359
+ # so we can issue commands from threads
360
+ while @locked_to
361
+ sleep 0.05
362
+ end
363
+ end
364
+
365
+ # we got the lock!
366
+ old_lock = @locked_to
367
+ @locked_to = Thread.current
368
+ log_incr "Got lock"
369
+
370
+ # perform the command while
371
+ # we have exclusive access
372
+ # to the modem device
373
+ yield
374
+
375
+
376
+ # something went bang, which happens, but
377
+ # just pass it on (after unlocking...)
378
+ rescue GsmModem::Error
379
+ raise
380
+
381
+
382
+ # no message, but always un-
383
+ # indent subsequent log messages
384
+ # and RELEASE THE LOCK
385
+ ensure
386
+ @locked_to = old_lock
387
+ Thread.pass
388
+ log_decr
389
+ end
390
+ end
391
+
392
+
393
+
394
+
395
+ public
396
+
397
+
398
+ # call-seq:
399
+ # hardware => hash
400
+ #
401
+ # Returns a hash of containing information about the physical
402
+ # modem. The contents of each value are entirely manufacturer
403
+ # dependant, and vary wildly between devices.
404
+ #
405
+ # modem.hardware => { :manufacturer => "Multitech".
406
+ # :model => "MTCBA-G-F4",
407
+ # :revision => "123456789",
408
+ # :serial => "ABCD" }
409
+ def hardware
410
+ return {
411
+ :manufacturer => query("AT+CGMI"),
412
+ :model => query("AT+CGMM"),
413
+ :revision => query("AT+CGMR"),
414
+ :serial => query("AT+CGSN") }
415
+ end
416
+
417
+
418
+ # The values accepted and returned by the AT+WMBS
419
+ # command, mapped to frequency bands, in MHz. Copied
420
+ # directly from the MultiTech AT command-set reference
421
+ Bands = {
422
+ "0" => "850",
423
+ "1" => "900",
424
+ "2" => "1800",
425
+ "3" => "1900",
426
+ "4" => "850/1900",
427
+ "5" => "900E/1800",
428
+ "6" => "900E/1900"
429
+ }
430
+
431
+ # call-seq:
432
+ # compatible_bands => array
433
+ #
434
+ # Returns an array containing the bands supported by
435
+ # the modem.
436
+ def compatible_bands
437
+ data = query("AT+WMBS=?")
438
+
439
+ # wmbs data is returned as something like:
440
+ # +WMBS: (0,1,2,3,4,5,6),(0-1)
441
+ # +WMBS: (0,3,4),(0-1)
442
+ # extract the numbers with a regex, and
443
+ # iterate each to resolve it to a more
444
+ # readable description
445
+ if m = data.match(/^\+WMBS: \(([\d,]+)\),/)
446
+ return m.captures[0].split(",").collect do |index|
447
+ Bands[index]
448
+ end
449
+
450
+ else
451
+ # Todo: Recover from this exception
452
+ err = "Not WMBS data: #{data.inspect}"
453
+ raise RuntimeError.new(err)
454
+ end
455
+ end
456
+
457
+ # call-seq:
458
+ # band => string
459
+ #
460
+ # Returns a string containing the band
461
+ # currently selected for use by the modem.
462
+ def band
463
+ data = query("AT+WMBS?")
464
+ if m = data.match(/^\+WMBS: (\d+),/)
465
+ return Bands[m.captures[0]]
466
+
467
+ else
468
+ # Todo: Recover from this exception
469
+ err = "Not WMBS data: #{data.inspect}"
470
+ raise RuntimeError.new(err)
471
+ end
472
+ end
473
+
474
+
475
+ # call-seq:
476
+ # pin_required? => true or false
477
+ #
478
+ # Returns true if the modem is waiting for a SIM PIN. Some SIM cards will refuse
479
+ # to work until the correct four-digit PIN is provided via the _use_pin_ method.
480
+ def pin_required?
481
+ not command("AT+CPIN?").include?("+CPIN: READY")
482
+ end
483
+
484
+
485
+ # call-seq:
486
+ # use_pin(pin) => true or false
487
+ #
488
+ # Provide a SIM PIN to the modem, and return true if it was accepted.
489
+ def use_pin(pin)
490
+
491
+ # if the sim is already ready,
492
+ # this method isn't necessary
493
+ if pin_required?
494
+ begin
495
+ command "AT+CPIN=#{pin}"
496
+
497
+ # if the command failed, then
498
+ # the pin was not accepted
499
+ rescue GsmModem::Error
500
+ return false
501
+ end
502
+ end
503
+
504
+ # no error = SIM
505
+ # PIN accepted!
506
+ true
507
+ end
508
+
509
+
510
+ # call-seq:
511
+ # signal => fixnum or nil
512
+ #
513
+ # Returns an fixnum between 1 and 99, representing the current
514
+ # signal strength of the GSM network, or nil if we don't know.
515
+ def signal_strength
516
+ data = query("AT+CSQ")
517
+ if m = data.match(/^\+CSQ: (\d+),/)
518
+
519
+ # 99 represents "not known or not detectable",
520
+ # but we'll use nil for that, since it's a bit
521
+ # more ruby-ish to test for boolean equality
522
+ csq = m.captures[0].to_i
523
+ return (csq<99) ? csq : nil
524
+
525
+ else
526
+ # Todo: Recover from this exception
527
+ err = "Not CSQ data: #{data.inspect}"
528
+ raise RuntimeError.new(err)
529
+ end
530
+ end
531
+
532
+
533
+ # call-seq:
534
+ # wait_for_network
535
+ #
536
+ # Blocks until the signal strength indicates that the
537
+ # device is active on the GSM network. It's a good idea
538
+ # to call this before trying to send or receive anything.
539
+ def wait_for_network
540
+
541
+ # keep retrying until the
542
+ # network comes up (if ever)
543
+ until csq = signal_strength
544
+ sleep 1
545
+ end
546
+
547
+ # return the last
548
+ # signal strength
549
+ return csq
550
+ end
551
+
552
+
553
+ # call-seq:
554
+ # send(recipient, message) => true or false
555
+ #
556
+ # Sends an SMS message, and returns true if the network
557
+ # accepted it for delivery. We currently can't handle read
558
+ # receipts, so have no way of confirming delivery.
559
+ #
560
+ # Note: the recipient is passed directly to the modem, which
561
+ # in turn passes it straight to the SMSC (sms message center).
562
+ # for maximum compatibility, use phone numbers in international
563
+ # format, including the *plus* and *country code*.
564
+ def send(to, msg)
565
+
566
+ # the number must be in the international
567
+ # format for some SMSCs (notably, the one
568
+ # i'm on right now) so maybe add a PLUS
569
+ #to = "+#{to}" unless(to[0,1]=="+")
570
+
571
+ # 1..9 is a special number which does not
572
+ # result in a real sms being sent (see inject.rb)
573
+ if to == "+123456789"
574
+ log "Not sending test message: #{msg}"
575
+ return false
576
+ end
577
+
578
+ # block the receiving thread while
579
+ # we're sending. it can take some time
580
+ exclusive do
581
+ log_incr "Sending SMS to #{to}: #{msg}"
582
+
583
+ # initiate the sms, and wait for either
584
+ # the text prompt or an error message
585
+ command "AT+CMGS=\"#{to}\"", ["\r\n", "> "]
586
+
587
+ begin
588
+ # send the sms, and wait until
589
+ # it is accepted or rejected
590
+ write "#{msg}#{26.chr}"
591
+ wait
592
+
593
+ # if something went wrong, we are
594
+ # be stuck in entry mode (which will
595
+ # result in someone getting a bunch
596
+ # of AT commands via sms!) so send
597
+ # an escpae, to... escape
598
+ rescue Exception, Timeout::Error => err
599
+ log "Rescued #{err.desc}"
600
+ return false
601
+ #write 27.chr
602
+ #wait
603
+ end
604
+
605
+ log_decr
606
+ end
607
+
608
+ # if no error was raised,
609
+ # then the message was sent
610
+ return true
611
+ end
612
+
613
+
614
+ # call-seq:
615
+ # receive(callback_method, interval=5, join_thread=false)
616
+ #
617
+ # Starts a new thread, which polls the device every _interval_
618
+ # seconds to capture incoming SMS and call _callback_method_
619
+ # for each.
620
+ #
621
+ # class Receiver
622
+ # def incoming(caller, datetime, message)
623
+ # puts "From #{caller} at #{datetime}:", message
624
+ # end
625
+ # end
626
+ #
627
+ # # create the instances,
628
+ # # and start receiving
629
+ # rcv = Receiver.new
630
+ # m = GsmModem.new "/dev/ttyS0"
631
+ # m.receive inst.method :incoming
632
+ #
633
+ # # block until ctrl+c
634
+ # while(true) { sleep 2 }
635
+ #
636
+ # Note: New messages may arrive at any time, even if this method's
637
+ # receiver thread isn't waiting to process them. They are not lost,
638
+ # but cached in @incoming until this method is called.
639
+ def receive(callback, interval=5, join_thread=false)
640
+ @polled = 0
641
+
642
+ @thr = Thread.new do
643
+ Thread.current["name"] = "receiver"
644
+
645
+ # keep on receiving forever
646
+ while true
647
+ command "AT"
648
+
649
+ # enable new message notification mode
650
+ # every ten intevals, in case the
651
+ # modem "forgets" (power cycle, etc)
652
+ if (@polled % 10) == 0
653
+ command "AT+CNMI=2,2,0,0,0"
654
+ end
655
+
656
+ # if there are any new incoming messages,
657
+ # iterate, and pass each to the receiver
658
+ # in the same format that they were built
659
+ # back in _parse_incoming_sms!_
660
+ unless @incoming.empty?
661
+ @incoming.each do |inc|
662
+ begin
663
+ callback.call *inc
664
+
665
+ rescue StandardError => err
666
+ log "Error in callback: #{err}"
667
+ end
668
+ end
669
+
670
+ # we have dealt with all of the pending
671
+ # messages. todo: this is a ridiculous
672
+ # race condition, and i fail at ruby
673
+ @incoming.clear
674
+ end
675
+
676
+ # re-poll every
677
+ # five seconds
678
+ sleep(interval)
679
+ @polled += 1
680
+ end
681
+ end
682
+
683
+ # it's sometimes handy to run single-
684
+ # threaded (like debugging handsets)
685
+ @thr.join if join_thread
686
+ end
687
+ end
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env ruby
2
+ #:title:Ruby GSM Errors
3
+ #--
4
+ # vim: noet
5
+ #++
6
+
7
+ class GsmModem
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
+ end
97
+
98
+ class TimeoutError < Error #:nodoc:
99
+ def desc
100
+ return "The command timed out"
101
+ end
102
+ end
103
+
104
+ class WriteError < Error #:nodoc:
105
+ def desc
106
+ "The modem couldn't be written to. It " +\
107
+ "may have crashed or been unplugged"
108
+ end
109
+ end
110
+
111
+ class ReadError < Error #:nodoc:
112
+ def desc
113
+ "The modem couldn't be read from. It " +\
114
+ "may have crashed or been unplugged"
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: noet
3
+
4
+ class GsmModem
5
+ private
6
+
7
+ # Symbols accepted by the GsmModem.new _verbosity_
8
+ # argument. Each level includes all of the levels
9
+ # below it (ie. :debug includes all :warn messages)
10
+ LOG_LEVELS = {
11
+ :file => 5,
12
+ :traffic => 4,
13
+ :debug => 3,
14
+ :warn => 2,
15
+ :error => 1 }
16
+
17
+ def log(msg, level=:debug)
18
+ ind = " " * (@log_indents[Thread.current] or 0)
19
+
20
+ # create a
21
+ thr = Thread.current["name"]
22
+ thr = (thr.nil?) ? "" : "[#{thr}] "
23
+
24
+ # dump (almost) everything to file
25
+ if LOG_LEVELS[level] >= LOG_LEVELS[:debug]\
26
+ or level == :file
27
+
28
+ @log.puts thr + ind + msg
29
+ @log.flush
30
+ end
31
+
32
+ # also print to the rolling
33
+ # screen log, if necessary
34
+ if LOG_LEVELS[@verbosity] >= LOG_LEVELS[level]
35
+ $stderr.puts thr + ind + msg
36
+ end
37
+ end
38
+
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
@@ -0,0 +1,21 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "rubygsm"
3
+ s.version = "0.2"
4
+ s.date = "2008-12-18"
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
+ ]
19
+
20
+ s.add_dependency("toholio-serialport", ["> 0.7.1"])
21
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: adammck-rubygsm
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.2"
5
+ platform: ruby
6
+ authors:
7
+ - Adam Mckaig
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-12-18 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
+
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
+ has_rdoc: true
40
+ homepage: http://github.com/adammck/rubygsm
41
+ post_install_message:
42
+ rdoc_options: []
43
+
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ requirements: []
59
+
60
+ rubyforge_project:
61
+ rubygems_version: 1.2.0
62
+ signing_key:
63
+ specification_version: 2
64
+ summary: Send and receive SMS with a GSM modem
65
+ test_files: []
66
+