jeffrafter-win32-sms 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION.yml +1 -1
- data/lib/win32/sms.rb +308 -0
- metadata +3 -2
- data/lib/win32_sms.rb +0 -1042
data/VERSION.yml
CHANGED
data/lib/win32/sms.rb
ADDED
@@ -0,0 +1,308 @@
|
|
1
|
+
require 'Win32API'
|
2
|
+
|
3
|
+
# http://www.rubyinside.com/cross-platform-ruby-serial-port-library-328.html
|
4
|
+
|
5
|
+
module Win32
|
6
|
+
|
7
|
+
GENERIC_READ = 0x80000000
|
8
|
+
GENERIC_WRITE = 0x40000000
|
9
|
+
OPEN_EXISTING = 0x00000003
|
10
|
+
FILE_FLAG_OVERLAPPED = 0x40000000
|
11
|
+
NULL = 0x00000000
|
12
|
+
EV_RXCHAR = 0x0001
|
13
|
+
ERROR_IO_PENDING = 997
|
14
|
+
ERROR_HANDLE_EOF = 38
|
15
|
+
DCB_SIZE = 80
|
16
|
+
|
17
|
+
class SmsError < RuntimeError; end
|
18
|
+
|
19
|
+
class Sms
|
20
|
+
|
21
|
+
attr_accessor :messages
|
22
|
+
|
23
|
+
def initialize(port)
|
24
|
+
@read_buffer = ""
|
25
|
+
|
26
|
+
@CreateFile = Win32API.new('Kernel32', 'CreateFile', 'PLLLLLL', 'L')
|
27
|
+
@CloseHandle = Win32API.new('Kernel32','CloseHandle', 'L', 'N')
|
28
|
+
@ReadFile = Win32API.new('Kernel32','ReadFile','LPLPP','I')
|
29
|
+
@WriteFile = Win32API.new('Kernel32','WriteFile','LPLPP','I')
|
30
|
+
@SetCommState = Win32API.new('Kernel32','SetCommState','LP','N')
|
31
|
+
@SetCommTimeouts = Win32API.new('Kernel32','SetCommTimeouts','LP','N')
|
32
|
+
@BuildCommDCB = Win32API.new('Kernel32','BuildCommDCB', 'PP', 'N')
|
33
|
+
@GetLastError = Win32API.new('Kernel32','GetLastError', 'V', 'N')
|
34
|
+
|
35
|
+
# Open the device non-overlapped
|
36
|
+
@device = create_file(port, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL)
|
37
|
+
|
38
|
+
# Set the speed
|
39
|
+
set_comm_state(9600, 8, 'N', 1)
|
40
|
+
|
41
|
+
# Need to be able to reset blocking
|
42
|
+
set_comm_timeouts(2, 1, 1, 0, 0)
|
43
|
+
|
44
|
+
# Keep multi-part messages until the last part is delivered
|
45
|
+
@multipart = {}
|
46
|
+
|
47
|
+
# Store incoming messages until they're dealt with by someone else
|
48
|
+
@messages = []
|
49
|
+
|
50
|
+
# Setup the command for texting and processing
|
51
|
+
connect
|
52
|
+
end
|
53
|
+
|
54
|
+
def finalize
|
55
|
+
close
|
56
|
+
end
|
57
|
+
|
58
|
+
def close
|
59
|
+
hResult = @CloseHandle.call(@device) if @device
|
60
|
+
raise "Could not close device (#{get_last_error})" if hResult == 0
|
61
|
+
@device = nil
|
62
|
+
end
|
63
|
+
|
64
|
+
def command(data, term = "\r\n")
|
65
|
+
write(data + "\r\n")
|
66
|
+
response = wait(term)
|
67
|
+
response.delete_if do |line|
|
68
|
+
(line[0,6] == "+WIND:") or
|
69
|
+
(line[0,6] == "+CREG:") or
|
70
|
+
(line[0,7] == "+CGREG:")
|
71
|
+
end
|
72
|
+
response
|
73
|
+
end
|
74
|
+
|
75
|
+
def query(data)
|
76
|
+
response = command(data)
|
77
|
+
return response[0] if response.size == 2 and response[1] == "OK"
|
78
|
+
raise "Invalid response: #{response.inspect}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def encodings?
|
82
|
+
command "AT+CSCS=?"
|
83
|
+
end
|
84
|
+
|
85
|
+
def encoding
|
86
|
+
@encoding
|
87
|
+
end
|
88
|
+
|
89
|
+
def encoding=(enc)
|
90
|
+
@encoding = enc
|
91
|
+
case enc
|
92
|
+
when :ascii
|
93
|
+
command "AT+CSCS=\"ASCII\""
|
94
|
+
when :utf8
|
95
|
+
command "AT+CSCS=\"UTF8\""
|
96
|
+
when :ucs2
|
97
|
+
command "AT+CSCS=\"UCS2\""
|
98
|
+
when :gsm
|
99
|
+
command "AT+CSCS=\"GSM\""
|
100
|
+
when :iso88591
|
101
|
+
command "AT+CSCS=\"8859-1\""
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def sms(number, text)
|
106
|
+
# initiate the sms, and wait for either the text prompt or an error
|
107
|
+
command "AT+CMGS=\"#{number}\"", ["\r\n", "> "]
|
108
|
+
begin
|
109
|
+
# send the sms, and wait until it is accepted or rejected
|
110
|
+
text = encode(text)
|
111
|
+
write "#{text}#{26.chr}"
|
112
|
+
response = wait
|
113
|
+
rescue
|
114
|
+
# Escape entry mode
|
115
|
+
write 27.chr
|
116
|
+
raise
|
117
|
+
end
|
118
|
+
response
|
119
|
+
end
|
120
|
+
|
121
|
+
def hardware
|
122
|
+
{:manufacturer => query("AT+CGMI"),
|
123
|
+
:model => query("AT+CGMM"),
|
124
|
+
:revision => query("AT+CGMR"),
|
125
|
+
:serial => query("AT+CGSN") }
|
126
|
+
end
|
127
|
+
|
128
|
+
def process
|
129
|
+
@messages = []
|
130
|
+
fetch_and_delete_stored_messages
|
131
|
+
end
|
132
|
+
|
133
|
+
protected
|
134
|
+
|
135
|
+
def wait(term = "\r\n")
|
136
|
+
response = []
|
137
|
+
loop do
|
138
|
+
buffer = read_until(term)
|
139
|
+
buffer.strip!
|
140
|
+
next if buffer.nil? || buffer.empty?
|
141
|
+
response << buffer
|
142
|
+
|
143
|
+
# Check for formatted error
|
144
|
+
if m = buffer.match(/^\+(CM[ES]) ERROR: (\d+)$/)
|
145
|
+
raise SmsError.new(buffer)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Check for unformatted error
|
149
|
+
if buffer == "ERROR"
|
150
|
+
raise SmsError
|
151
|
+
end
|
152
|
+
|
153
|
+
# Check for 'OK' or prompt
|
154
|
+
if (buffer == "OK") or (buffer == ">")
|
155
|
+
return response
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def parse_timestamp(data)
|
161
|
+
# Extract the weirdo quarter-hour timezone into a regular hourly offset
|
162
|
+
data.sub! /(\d+)$/ do |m| sprintf("%02d", (m.to_i/4)); end
|
163
|
+
DateTime.strptime(data, "%d/%m/%Y %H:%M:%S %z")
|
164
|
+
end
|
165
|
+
|
166
|
+
# Encodes the message using the set encoding or, if no encoding is specified
|
167
|
+
# returns the msg unchange
|
168
|
+
def encode(msg)
|
169
|
+
if (@encoding == :ascii)
|
170
|
+
# TODO, use lucky sneaks here
|
171
|
+
msg
|
172
|
+
elsif (@encoding == :utf8)
|
173
|
+
# Unpacking and repacking supposedly cleans out bad (non-UTF-8) stuff
|
174
|
+
utf8 = msg.unpack("U*");
|
175
|
+
packed = utf8.pack("U*");
|
176
|
+
packed
|
177
|
+
elsif (@encoding == :ucs2)
|
178
|
+
ucs2 = Iconv.iconv("UCS-2", "UTF-8", msg).first
|
179
|
+
ucs2 = ucs2.unpack("H*").join
|
180
|
+
ucs2
|
181
|
+
else
|
182
|
+
msg
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def select_default_mailbox
|
187
|
+
# Eventually we will select the first mailbox as the default
|
188
|
+
result = query("AT+CPMS=?")
|
189
|
+
boxes = result.scan(/\"(\w+)\"/).flatten #"
|
190
|
+
mailbox = boxes.first
|
191
|
+
command "AT+CPMS=\"#{mailbox}\""
|
192
|
+
mailbox
|
193
|
+
rescue
|
194
|
+
raise RuntimeError.new("Could not select the default mailbox")
|
195
|
+
nil
|
196
|
+
end
|
197
|
+
|
198
|
+
def fetch_and_delete_stored_messages
|
199
|
+
# If there is no way to select a default mailbox we can't continue
|
200
|
+
return unless select_default_mailbox
|
201
|
+
|
202
|
+
# Try to read the first message from the box
|
203
|
+
begin
|
204
|
+
response = command("AT+CMGR=1")
|
205
|
+
rescue
|
206
|
+
return
|
207
|
+
end
|
208
|
+
|
209
|
+
# Did we find any messages
|
210
|
+
return if response.nil? || response.empty?
|
211
|
+
|
212
|
+
# Delete that message
|
213
|
+
command("AT+CMGD=1")
|
214
|
+
|
215
|
+
# Break that down
|
216
|
+
header = response.first
|
217
|
+
text = response[1..response.size-2].join("\n")
|
218
|
+
validity = response.last
|
219
|
+
|
220
|
+
# Read the header
|
221
|
+
header = header.scan(/\"([^"]+)\"/).flatten #"
|
222
|
+
status = header[0]
|
223
|
+
from = header[1]
|
224
|
+
timestamp = header[2]
|
225
|
+
sent = DateTime.parse(timestamp)
|
226
|
+
|
227
|
+
# Just in case it wasn't already obvious
|
228
|
+
puts "Received message from #{from}: #{text}"
|
229
|
+
@messages << {:from => from,
|
230
|
+
:text => text,
|
231
|
+
:created_at => sent,
|
232
|
+
:processed_at => Time.now}
|
233
|
+
end
|
234
|
+
|
235
|
+
private
|
236
|
+
|
237
|
+
def get_last_error
|
238
|
+
@GetLastError.Call
|
239
|
+
end
|
240
|
+
|
241
|
+
def create_file(file_name, desired_access, shared_mode, security_attributes,
|
242
|
+
creation_distribution, flags_and_attributes, template_file)
|
243
|
+
hResult = @CreateFile.Call(file_name, desired_access, shared_mode,
|
244
|
+
security_attributes, creation_distribution, flags_and_attributes,
|
245
|
+
template_file)
|
246
|
+
raise "Could not open device (#{get_last_error})" if hResult < 1
|
247
|
+
hResult
|
248
|
+
end
|
249
|
+
|
250
|
+
def set_comm_state(baud, data, parity, stop)
|
251
|
+
args = "baud=#{baud} parity=#{parity} data=#{data} stop=#{stop}"
|
252
|
+
dcb = " "*80
|
253
|
+
hResult = @BuildCommDCB.Call(args, dcb)
|
254
|
+
raise "Could not build dcb structure (#{get_last_error})" if hResult == 0
|
255
|
+
hResult = @SetCommState.Call(@device, dcb)
|
256
|
+
raise "Could not set comm state (#{get_last_error})" if hResult == 0
|
257
|
+
end
|
258
|
+
|
259
|
+
def set_comm_timeouts(read_interval_timeout = 3, read_total_timeout_multiplier = 3,
|
260
|
+
read_total_timeout_constant = 2, write_total_timeout_multiplier = 3,
|
261
|
+
write_total_timeout_constant = 2)
|
262
|
+
hResult = @SetCommTimeouts.Call(@device, [read_interval_timeout,
|
263
|
+
read_total_timeout_multiplier, read_total_timeout_constant,
|
264
|
+
write_total_timeout_multiplier, write_total_timeout_constant].pack("L*"))
|
265
|
+
raise "Could not set comm timeouts (#{get_last_error})" if hResult == 0
|
266
|
+
end
|
267
|
+
|
268
|
+
def read
|
269
|
+
count = " "*4
|
270
|
+
buffer = " "*1024
|
271
|
+
hResult = @ReadFile.Call(@device, buffer, 1024, count, NULL)
|
272
|
+
raise "Could not read data (#{get_last_error})" if hResult == 0
|
273
|
+
count = count.unpack("L").first
|
274
|
+
@read_buffer << buffer[0..(count-1)]
|
275
|
+
hResult
|
276
|
+
end
|
277
|
+
|
278
|
+
def read_until(term)
|
279
|
+
term = [term].flatten
|
280
|
+
stop = nil
|
281
|
+
loop do
|
282
|
+
term.each {|t| stop = t and break if @read_buffer.index(t)}
|
283
|
+
break if stop
|
284
|
+
read
|
285
|
+
end
|
286
|
+
@read_buffer.slice!(0, @read_buffer.index(stop)+stop.size)
|
287
|
+
end
|
288
|
+
|
289
|
+
def write(data)
|
290
|
+
count = " "*4
|
291
|
+
hResult = @WriteFile.Call(@device, data, data.size, count, NULL)
|
292
|
+
count = count.unpack("L").first
|
293
|
+
raise "Could not write data (#{get_last_error})" if hResult == 0
|
294
|
+
# raise "Not enough bytes written" if count != data.size
|
295
|
+
end
|
296
|
+
|
297
|
+
def connect
|
298
|
+
# Echo off
|
299
|
+
command "ATE0" rescue nil
|
300
|
+
# Useful errors
|
301
|
+
command "AT+CMEE=1" rescue nil
|
302
|
+
# No notifications
|
303
|
+
command "AT+WIND=0" rescue nil
|
304
|
+
# Switch to text mode
|
305
|
+
command "AT+CMGF=1"
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jeffrafter-win32-sms
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeff Rafter
|
@@ -23,7 +23,8 @@ extra_rdoc_files: []
|
|
23
23
|
|
24
24
|
files:
|
25
25
|
- VERSION.yml
|
26
|
-
- lib/
|
26
|
+
- lib/win32
|
27
|
+
- lib/win32/sms.rb
|
27
28
|
- test/test_helper.rb
|
28
29
|
- test/win32_sms_test.rb
|
29
30
|
has_rdoc: false
|
data/lib/win32_sms.rb
DELETED
@@ -1,1042 +0,0 @@
|
|
1
|
-
require 'Win32API'
|
2
|
-
|
3
|
-
# http://www.rubyinside.com/cross-platform-ruby-serial-port-library-328.html
|
4
|
-
|
5
|
-
module Win32
|
6
|
-
|
7
|
-
GENERIC_READ = 0x80000000
|
8
|
-
GENERIC_WRITE = 0x40000000
|
9
|
-
OPEN_EXISTING = 0x00000003
|
10
|
-
FILE_FLAG_OVERLAPPED = 0x40000000
|
11
|
-
NULL = 0x00000000
|
12
|
-
EV_RXCHAR = 0x0001
|
13
|
-
ERROR_IO_PENDING = 997
|
14
|
-
ERROR_HANDLE_EOF = 38
|
15
|
-
DCB_SIZE = 80
|
16
|
-
|
17
|
-
class Sms
|
18
|
-
def initialize(port)
|
19
|
-
@read_buffer = ""
|
20
|
-
|
21
|
-
@CreateFile = Win32API.new('Kernel32', 'CreateFile', 'PLLLLLL', 'L')
|
22
|
-
@CloseHandle = Win32API.new('Kernel32','CloseHandle', 'L', 'N')
|
23
|
-
@ReadFile = Win32API.new('Kernel32','ReadFile','LPLPP','I')
|
24
|
-
@WriteFile = Win32API.new('Kernel32','WriteFile','LPLPP','I')
|
25
|
-
@SetCommState = Win32API.new('Kernel32','SetCommState','LP','N')
|
26
|
-
@SetCommTimeouts = Win32API.new('Kernel32','SetCommTimeouts','LP','N')
|
27
|
-
@BuildCommDCB = Win32API.new('Kernel32','BuildCommDCB', 'LP', 'N')
|
28
|
-
@GetLastError = Win32API.new('Kernel32','GetLastError', 'V', 'N')
|
29
|
-
|
30
|
-
# Open the device non-overlapped
|
31
|
-
@device = create_file(port, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL)
|
32
|
-
|
33
|
-
# Set the speed
|
34
|
-
set_comm_state(9600, 8, 'N', 1)
|
35
|
-
|
36
|
-
# Need to be able to reset blocking
|
37
|
-
set_comm_timeouts(2, 1, 1, 0, 0)
|
38
|
-
|
39
|
-
# Keep multi-part messages until the last part is delivered
|
40
|
-
@multipart = {}
|
41
|
-
|
42
|
-
# Store incoming messages until they're dealt with by someone else
|
43
|
-
@incoming = []
|
44
|
-
end
|
45
|
-
|
46
|
-
def finalize
|
47
|
-
hResult = @CloseHandle.call(@device) if @device
|
48
|
-
raise "Could not close device (#{get_last_error})" if hResult == 0
|
49
|
-
end
|
50
|
-
|
51
|
-
def command(data)
|
52
|
-
write(data)
|
53
|
-
response = read_until("\r\n")
|
54
|
-
response.strip!
|
55
|
-
# Clean up status messages
|
56
|
-
response.delete_if do |line|
|
57
|
-
(line[0,6] == "+WIND:") or
|
58
|
-
(line[0,6] == "+CREG:") or
|
59
|
-
(line[0,7] == "+CGREG:")
|
60
|
-
end
|
61
|
-
response
|
62
|
-
end
|
63
|
-
|
64
|
-
private
|
65
|
-
def get_last_error
|
66
|
-
@GetLastError.Call
|
67
|
-
end
|
68
|
-
|
69
|
-
def create_file(file_name, desired_access, shared_mode, security_attributes,
|
70
|
-
creation_distribution, flags_and_attributes, template_file)
|
71
|
-
hResult = @CreateFile.Call(file_name, desired_access, shared_mode,
|
72
|
-
security_attributes, creation_distribution, flags_and_attributes,
|
73
|
-
template_file)
|
74
|
-
raise "Could not open device (#{get_last_error})" if hResult == INVALID_HANDLE_VALUE
|
75
|
-
hResult
|
76
|
-
end
|
77
|
-
|
78
|
-
def set_comm_state(baud, data, parity, stop)
|
79
|
-
args = "baud=#{baud} parity=#{parity} data=#{data} stop=#{stop}"
|
80
|
-
dcb = nil
|
81
|
-
hResult = @BuildCommDCB.Call(args, dcb)
|
82
|
-
raise "Could not build dcb structure (#{get_last_error})" if hResult == 0
|
83
|
-
hResult = @SetCommState.Call(@device, dcb)
|
84
|
-
raise "Could not set comm state (#{get_last_error})" if hResult == 0
|
85
|
-
end
|
86
|
-
|
87
|
-
def set_comm_timeouts(
|
88
|
-
read_interval_timeout = 3,
|
89
|
-
read_total_timeout_multiplier = 3,
|
90
|
-
read_total_timeout_constant = 2,
|
91
|
-
write_total_timeout_multiplier = 3,
|
92
|
-
write_total_timeout_constant = 2)
|
93
|
-
hResult = @SetCommTimeouts.Call(@device, [read_interval_timeout,
|
94
|
-
read_total_timeout_multiplier,
|
95
|
-
read_total_timeout_constant,
|
96
|
-
write_total_timeout_multiplier,
|
97
|
-
write_total_timeout_constant].pack("L*"))
|
98
|
-
raise "Could not set comm timeouts (#{get_last_error})" if hResult == 0
|
99
|
-
end
|
100
|
-
|
101
|
-
def read
|
102
|
-
count = nil
|
103
|
-
buffer = nil
|
104
|
-
hResult = @ReadFile.Call(@device, buffer, 1024, count, NULL)
|
105
|
-
raise "Could not read data (#{get_last_error})" if hResult == 0
|
106
|
-
count = [count].unpack("L")
|
107
|
-
@read_buffer << buffer.slice[0..count-1]
|
108
|
-
hResult
|
109
|
-
end
|
110
|
-
|
111
|
-
def read_until(term)
|
112
|
-
loop do
|
113
|
-
break if @read_buffer.index(term)
|
114
|
-
read
|
115
|
-
end
|
116
|
-
@read_buffer.slice!(0, buf.index(term)+term.size)
|
117
|
-
end
|
118
|
-
|
119
|
-
def write(data)
|
120
|
-
count = nil
|
121
|
-
hResult = @WriteFile.Call(@device, data, data.size, count, NULL)
|
122
|
-
count = [count].unpack("L")
|
123
|
-
raise "Could not write data (#{get_last_error})" if hResult == 0
|
124
|
-
raise "Not enough bytes written" if count != data.size
|
125
|
-
end
|
126
|
-
|
127
|
-
def connect
|
128
|
-
# Echo off
|
129
|
-
command "ATE0" rescue nil
|
130
|
-
# Useful errors
|
131
|
-
command "AT+CMEE=1" rescue nil
|
132
|
-
# No notifications
|
133
|
-
command "AT+WIND=0" rescue nil
|
134
|
-
# Switch to text mode
|
135
|
-
command "AT+CMGF=1"
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
=begin
|
141
|
-
|
142
|
-
|
143
|
-
private
|
144
|
-
|
145
|
-
|
146
|
-
INCOMING_FMT = "%y/%m/%d,%H:%M:%S%Z" #:nodoc:
|
147
|
-
CMGL_STATUS = "REC UNREAD" #:nodoc:
|
148
|
-
|
149
|
-
|
150
|
-
def parse_incoming_timestamp(ts)
|
151
|
-
# extract the weirdo quarter-hour timezone,
|
152
|
-
# convert it into a regular hourly offset
|
153
|
-
ts.sub! /(\d+)$/ do |m|
|
154
|
-
sprintf("%02d", (m.to_i/4))
|
155
|
-
end
|
156
|
-
|
157
|
-
# parse the timestamp, and attempt to re-align
|
158
|
-
# it according to the timezone we extracted
|
159
|
-
DateTime.strptime(ts, INCOMING_FMT)
|
160
|
-
end
|
161
|
-
|
162
|
-
def parse_incoming_sms!(lines)
|
163
|
-
n = 0
|
164
|
-
|
165
|
-
# iterate the lines like it's 1984
|
166
|
-
# (because we're patching the array,
|
167
|
-
# which is hard work for iterators)
|
168
|
-
while n < lines.length
|
169
|
-
|
170
|
-
# not a CMT string? ignore it
|
171
|
-
unless lines && lines[n] && lines[n][0,5] == "+CMT:"
|
172
|
-
n += 1
|
173
|
-
next
|
174
|
-
end
|
175
|
-
|
176
|
-
# since this line IS a CMT string (an incoming
|
177
|
-
# SMS), parse it and store it to deal with later
|
178
|
-
unless m = lines[n].match(/^\+CMT: "(.+?)",.*?,"(.+?)".*?$/)
|
179
|
-
err = "Couldn't parse CMT data: #{lines[n]}"
|
180
|
-
raise RuntimeError.new(err)
|
181
|
-
end
|
182
|
-
|
183
|
-
# extract the meta-info from the CMT line,
|
184
|
-
# and the message from the FOLLOWING line
|
185
|
-
from, timestamp = *m.captures
|
186
|
-
msg_text = lines[n+1].strip
|
187
|
-
|
188
|
-
# notify the network that we accepted
|
189
|
-
# the incoming message (for read receipt)
|
190
|
-
# BEFORE pushing it to the incoming queue
|
191
|
-
# (to avoid really ugly race condition if
|
192
|
-
# the message is grabbed from the queue
|
193
|
-
# and responded to quickly, before we get
|
194
|
-
# a chance to issue at+cnma)
|
195
|
-
begin
|
196
|
-
command "AT+CNMA"
|
197
|
-
|
198
|
-
# not terribly important if it
|
199
|
-
# fails, even though it shouldn't
|
200
|
-
rescue Gsm::Error
|
201
|
-
log "Receipt acknowledgement (CNMA) was rejected"
|
202
|
-
end
|
203
|
-
|
204
|
-
# we might abort if this part of a
|
205
|
-
# multi-part message, but not the last
|
206
|
-
catch :skip_processing do
|
207
|
-
|
208
|
-
# multi-part messages begin with ASCII char 130
|
209
|
-
if (msg_text[0] == 130) and (msg_text[1].chr == "@")
|
210
|
-
text = msg_text[7,999]
|
211
|
-
|
212
|
-
# ensure we have a place for the incoming
|
213
|
-
# message part to live as they are delivered
|
214
|
-
@multipart[from] = []\
|
215
|
-
unless @multipart.has_key?(from)
|
216
|
-
|
217
|
-
# append THIS PART
|
218
|
-
@multipart[from].push(text)
|
219
|
-
|
220
|
-
# add useless message to log
|
221
|
-
part = @multipart[from].length
|
222
|
-
log "Received part #{part} of message from: #{from}"
|
223
|
-
|
224
|
-
# abort if this is not the last part
|
225
|
-
throw :skip_processing\
|
226
|
-
unless (msg_text[5] == 173)
|
227
|
-
|
228
|
-
# last part, so switch out the received
|
229
|
-
# part with the whole message, to be processed
|
230
|
-
# below (the sender and timestamp are the same
|
231
|
-
# for all parts, so no change needed there)
|
232
|
-
msg_text = @multipart[from].join("")
|
233
|
-
@multipart.delete(from)
|
234
|
-
end
|
235
|
-
|
236
|
-
# just in case it wasn't already obvious...
|
237
|
-
log "Received message from #{from}: #{msg_text.inspect}"
|
238
|
-
|
239
|
-
# store the incoming data to be picked up
|
240
|
-
# from the attr_accessor as a tuple (this
|
241
|
-
# is kind of ghetto, and WILL change later)
|
242
|
-
sent = parse_incoming_timestamp(timestamp)
|
243
|
-
msg = Gsm::Incoming.new(self, from, sent, msg_text)
|
244
|
-
@incoming.push(msg)
|
245
|
-
end
|
246
|
-
|
247
|
-
# drop the two CMT lines (meta-info and message),
|
248
|
-
# and patch the index to hit the next unchecked
|
249
|
-
# line during the next iteration
|
250
|
-
lines.slice!(n,2)
|
251
|
-
n -= 1
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
|
256
|
-
# write a string to the modem immediately,
|
257
|
-
# without waiting for the lock
|
258
|
-
def write(str)
|
259
|
-
log "Write: #{str.inspect}", :traffic
|
260
|
-
|
261
|
-
begin
|
262
|
-
str.each_byte do |b|
|
263
|
-
@device.putc(b.chr)
|
264
|
-
end
|
265
|
-
|
266
|
-
# the device couldn't be written to,
|
267
|
-
# which probably means that it has
|
268
|
-
# crashed or been unplugged
|
269
|
-
rescue Errno::EIO
|
270
|
-
raise Gsm::WriteError
|
271
|
-
end
|
272
|
-
end
|
273
|
-
|
274
|
-
|
275
|
-
# read from the modem (blocking) until
|
276
|
-
# the term character is hit, and return
|
277
|
-
def read(term=nil)
|
278
|
-
term = "\r\n" if term==nil
|
279
|
-
term = [term] unless term.is_a? Array
|
280
|
-
buf = ""
|
281
|
-
|
282
|
-
# include the terminator in the traffic dump,
|
283
|
-
# if it's anything other than the default
|
284
|
-
#suffix = (term != ["\r\n"]) ? " (term=#{term.inspect})" : ""
|
285
|
-
#log_incr "Read" + suffix, :traffic
|
286
|
-
|
287
|
-
begin
|
288
|
-
timeout(@read_timeout) do
|
289
|
-
while true do
|
290
|
-
char = @device.getc
|
291
|
-
|
292
|
-
# die if we couldn't read
|
293
|
-
# (nil signifies an error)
|
294
|
-
raise Gsm::ReadError\
|
295
|
-
if char.nil?
|
296
|
-
|
297
|
-
# convert the character to ascii,
|
298
|
-
# and append it to the tmp buffer
|
299
|
-
buf << sprintf("%c", char)
|
300
|
-
|
301
|
-
# if a terminator was just received,
|
302
|
-
# then return the current buffer
|
303
|
-
term.each do |t|
|
304
|
-
len = t.length
|
305
|
-
if buf[-len, len] == t
|
306
|
-
log "Read: #{buf.inspect}", :traffic
|
307
|
-
return buf.strip
|
308
|
-
end
|
309
|
-
end
|
310
|
-
end
|
311
|
-
end
|
312
|
-
|
313
|
-
# reading took too long, so intercept
|
314
|
-
# and raise a more specific exception
|
315
|
-
rescue Timeout::Error
|
316
|
-
log = "Read: Timed out", :warn
|
317
|
-
raise TimeoutError
|
318
|
-
end
|
319
|
-
end
|
320
|
-
|
321
|
-
|
322
|
-
# issue a single command, and wait for the response
|
323
|
-
def command(cmd, resp_term=nil, write_term="\r")
|
324
|
-
begin
|
325
|
-
out = ""
|
326
|
-
|
327
|
-
exclusive do
|
328
|
-
log_incr "Command: #{cmd}"
|
329
|
-
write(cmd + write_term)
|
330
|
-
out = wait(resp_term)
|
331
|
-
end
|
332
|
-
|
333
|
-
# some hardware (my motorola phone) adds extra CRLFs
|
334
|
-
# to some responses. i see no reason that we need them
|
335
|
-
out.delete ""
|
336
|
-
|
337
|
-
# for the time being, ignore any unsolicited
|
338
|
-
# status messages. i can't seem to figure out
|
339
|
-
# how to disable them (AT+WIND=0 doesn't work)
|
340
|
-
out.delete_if do |line|
|
341
|
-
(line[0,6] == "+WIND:") or
|
342
|
-
(line[0,6] == "+CREG:") or
|
343
|
-
(line[0,7] == "+CGREG:")
|
344
|
-
end
|
345
|
-
|
346
|
-
# parse out any incoming sms that were bundled
|
347
|
-
# with this data (to be fetched later by an app)
|
348
|
-
parse_incoming_sms!(out)
|
349
|
-
|
350
|
-
# log the modified output
|
351
|
-
log_decr "=#{out.inspect}"
|
352
|
-
|
353
|
-
# rest up for a bit (modems are
|
354
|
-
# slow, and get confused easily)
|
355
|
-
sleep(@cmd_delay)
|
356
|
-
return out
|
357
|
-
|
358
|
-
# if the 515 (please wait) error was thrown,
|
359
|
-
# then automatically re-try the command after
|
360
|
-
# a short delay. for others, propagate
|
361
|
-
rescue Error => err
|
362
|
-
log "Rescued (in #command): #{err.desc}"
|
363
|
-
|
364
|
-
if (err.type == "CMS") and (err.code == 515)
|
365
|
-
sleep 2
|
366
|
-
retry
|
367
|
-
end
|
368
|
-
|
369
|
-
log_decr
|
370
|
-
raise
|
371
|
-
end
|
372
|
-
end
|
373
|
-
|
374
|
-
|
375
|
-
# proxy a single command to #command, but catch any
|
376
|
-
# Gsm::Error exceptions that are raised, and return
|
377
|
-
# nil. This should be used to issue commands which
|
378
|
-
# aren't vital - of which there are VERY FEW.
|
379
|
-
def try_command(cmd, *args)
|
380
|
-
begin
|
381
|
-
log_incr "Trying Command: #{cmd}"
|
382
|
-
out = command(cmd, *args)
|
383
|
-
log_decr "=#{out}"
|
384
|
-
return out
|
385
|
-
|
386
|
-
rescue Error => err
|
387
|
-
log_then_decr "Rescued (in #try_command): #{err.desc}"
|
388
|
-
return nil
|
389
|
-
end
|
390
|
-
end
|
391
|
-
|
392
|
-
|
393
|
-
def query(cmd)
|
394
|
-
log_incr "Query: #{cmd}"
|
395
|
-
out = command cmd
|
396
|
-
|
397
|
-
# only very simple responses are supported
|
398
|
-
# (on purpose!) here - [response, crlf, ok]
|
399
|
-
if (out.length==2) and (out[1]=="OK")
|
400
|
-
log_decr "=#{out[0].inspect}"
|
401
|
-
return out[0]
|
402
|
-
|
403
|
-
else
|
404
|
-
err = "Invalid response: #{out.inspect}"
|
405
|
-
raise RuntimeError.new(err)
|
406
|
-
end
|
407
|
-
end
|
408
|
-
|
409
|
-
|
410
|
-
# just wait for a response, by reading
|
411
|
-
# until an OK or ERROR terminator is hit
|
412
|
-
def wait(term=nil)
|
413
|
-
buffer = []
|
414
|
-
log_incr "Waiting for response"
|
415
|
-
|
416
|
-
while true do
|
417
|
-
buf = read(term)
|
418
|
-
buffer.push(buf)
|
419
|
-
|
420
|
-
# some errors contain useful error codes,
|
421
|
-
# so raise a proper error with a description
|
422
|
-
if m = buf.match(/^\+(CM[ES]) ERROR: (\d+)$/)
|
423
|
-
log_then_decr "!! Raising Gsm::Error #{$1} #{$2}"
|
424
|
-
raise Error.new(*m.captures)
|
425
|
-
end
|
426
|
-
|
427
|
-
# some errors are not so useful :|
|
428
|
-
if buf == "ERROR"
|
429
|
-
log_then_decr "!! Raising Gsm::Error"
|
430
|
-
raise Error
|
431
|
-
end
|
432
|
-
|
433
|
-
# most commands return OK upon success, except
|
434
|
-
# for those which prompt for more data (CMGS)
|
435
|
-
if (buf=="OK") or (buf==">")
|
436
|
-
log_decr "=#{buffer.inspect}"
|
437
|
-
return buffer
|
438
|
-
end
|
439
|
-
|
440
|
-
# some commands DO NOT respond with OK,
|
441
|
-
# even when they're successful, so check
|
442
|
-
# for those exceptions manually
|
443
|
-
if m = buf.match(/^\+CPIN: (.+)$/)
|
444
|
-
log_decr "=#{buffer.inspect}"
|
445
|
-
return buffer
|
446
|
-
end
|
447
|
-
end
|
448
|
-
end
|
449
|
-
|
450
|
-
|
451
|
-
def exclusive &blk
|
452
|
-
old_lock = nil
|
453
|
-
|
454
|
-
begin
|
455
|
-
|
456
|
-
# prevent other threads from issuing
|
457
|
-
# commands TO THIS MODDEM while this
|
458
|
-
# block is working. this does not lock
|
459
|
-
# threads, just the gsm device
|
460
|
-
if @locked_to and (@locked_to != Thread.current)
|
461
|
-
log "Locked by #{@locked_to["name"]}, waiting..."
|
462
|
-
|
463
|
-
# wait for the modem to become available,
|
464
|
-
# so we can issue commands from threads
|
465
|
-
while @locked_to
|
466
|
-
sleep 0.05
|
467
|
-
end
|
468
|
-
end
|
469
|
-
|
470
|
-
# we got the lock!
|
471
|
-
old_lock = @locked_to
|
472
|
-
@locked_to = Thread.current
|
473
|
-
log_incr "Got lock"
|
474
|
-
|
475
|
-
# perform the command while
|
476
|
-
# we have exclusive access
|
477
|
-
# to the modem device
|
478
|
-
yield
|
479
|
-
|
480
|
-
|
481
|
-
# something went bang, which happens, but
|
482
|
-
# just pass it on (after unlocking...)
|
483
|
-
rescue Gsm::Error
|
484
|
-
raise
|
485
|
-
|
486
|
-
|
487
|
-
# no message, but always un-
|
488
|
-
# indent subsequent log messages
|
489
|
-
# and RELEASE THE LOCK
|
490
|
-
ensure
|
491
|
-
@locked_to = old_lock
|
492
|
-
Thread.pass
|
493
|
-
log_decr
|
494
|
-
end
|
495
|
-
end
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
public
|
501
|
-
|
502
|
-
|
503
|
-
# call-seq:
|
504
|
-
# hardware => hash
|
505
|
-
#
|
506
|
-
# Returns a hash of containing information about the physical
|
507
|
-
# modem. The contents of each value are entirely manufacturer
|
508
|
-
# dependant, and vary wildly between devices.
|
509
|
-
#
|
510
|
-
# modem.hardware => { :manufacturer => "Multitech".
|
511
|
-
# :model => "MTCBA-G-F4",
|
512
|
-
# :revision => "123456789",
|
513
|
-
# :serial => "ABCD" }
|
514
|
-
def hardware
|
515
|
-
return {
|
516
|
-
:manufacturer => query("AT+CGMI"),
|
517
|
-
:model => query("AT+CGMM"),
|
518
|
-
:revision => query("AT+CGMR"),
|
519
|
-
:serial => query("AT+CGSN") }
|
520
|
-
end
|
521
|
-
|
522
|
-
|
523
|
-
# The values accepted and returned by the AT+WMBS
|
524
|
-
# command, mapped to frequency bands, in MHz. Copied
|
525
|
-
# directly from the MultiTech AT command-set reference
|
526
|
-
Bands = {
|
527
|
-
0 => "850",
|
528
|
-
1 => "900",
|
529
|
-
2 => "1800",
|
530
|
-
3 => "1900",
|
531
|
-
4 => "850/1900",
|
532
|
-
5 => "900E/1800",
|
533
|
-
6 => "900E/1900"
|
534
|
-
}
|
535
|
-
|
536
|
-
# call-seq:
|
537
|
-
# bands_available => array
|
538
|
-
#
|
539
|
-
# Returns an array containing the bands supported by
|
540
|
-
# the modem.
|
541
|
-
def bands_available
|
542
|
-
data = query("AT+WMBS=?")
|
543
|
-
|
544
|
-
# wmbs data is returned as something like:
|
545
|
-
# +WMBS: (0,1,2,3,4,5,6),(0-1)
|
546
|
-
# +WMBS: (0,3,4),(0-1)
|
547
|
-
# extract the numbers with a regex, and
|
548
|
-
# iterate each to resolve it to a more
|
549
|
-
# readable description
|
550
|
-
if m = data.match(/^\+WMBS: \(([\d,]+)\),/)
|
551
|
-
return m.captures[0].split(",").collect do |index|
|
552
|
-
Bands[index.to_i]
|
553
|
-
end
|
554
|
-
|
555
|
-
else
|
556
|
-
# Todo: Recover from this exception
|
557
|
-
err = "Not WMBS data: #{data.inspect}"
|
558
|
-
raise RuntimeError.new(err)
|
559
|
-
end
|
560
|
-
end
|
561
|
-
|
562
|
-
# call-seq:
|
563
|
-
# band => string
|
564
|
-
#
|
565
|
-
# Returns a string containing the band
|
566
|
-
# currently selected for use by the modem.
|
567
|
-
def band
|
568
|
-
data = query("AT+WMBS?")
|
569
|
-
if m = data.match(/^\+WMBS: (\d+),/)
|
570
|
-
return Bands[m.captures[0].to_i]
|
571
|
-
|
572
|
-
else
|
573
|
-
# Todo: Recover from this exception
|
574
|
-
err = "Not WMBS data: #{data.inspect}"
|
575
|
-
raise RuntimeError.new(err)
|
576
|
-
end
|
577
|
-
end
|
578
|
-
|
579
|
-
BandAreas = {
|
580
|
-
:usa => 4,
|
581
|
-
:africa => 5,
|
582
|
-
:europe => 5,
|
583
|
-
:asia => 5,
|
584
|
-
:mideast => 5
|
585
|
-
}
|
586
|
-
|
587
|
-
# call-seq:
|
588
|
-
# band=(_numeric_band_) => string
|
589
|
-
#
|
590
|
-
# Sets the band currently selected for use
|
591
|
-
# by the modem, using either a literal band
|
592
|
-
# number (passed directly to the modem, see
|
593
|
-
# Gsm::Modem.Bands) or a named area from
|
594
|
-
# Gsm::Modem.BandAreas:
|
595
|
-
#
|
596
|
-
# m = Gsm::Modem.new
|
597
|
-
# m.band = :usa => "850/1900"
|
598
|
-
# m.band = :africa => "900E/1800"
|
599
|
-
# m.band = :monkey => ArgumentError
|
600
|
-
#
|
601
|
-
# (Note that as usual, the United States of
|
602
|
-
# America is wearing its ass backwards.)
|
603
|
-
#
|
604
|
-
# Raises ArgumentError if an unrecognized band was
|
605
|
-
# given, or raises Gsm::Error if the modem does
|
606
|
-
# not support the given band.
|
607
|
-
def band=(new_band)
|
608
|
-
|
609
|
-
# resolve named bands into numeric
|
610
|
-
# (mhz values first, then band areas)
|
611
|
-
unless new_band.is_a?(Numeric)
|
612
|
-
|
613
|
-
if Bands.has_value?(new_band.to_s)
|
614
|
-
new_band = Bands.index(new_band.to_s)
|
615
|
-
|
616
|
-
elsif BandAreas.has_key?(new_band.to_sym)
|
617
|
-
new_band = BandAreas[new_band.to_sym]
|
618
|
-
|
619
|
-
else
|
620
|
-
err = "Invalid band: #{new_band}"
|
621
|
-
raise ArgumentError.new(err)
|
622
|
-
end
|
623
|
-
end
|
624
|
-
|
625
|
-
# set the band right now (second wmbs
|
626
|
-
# argument is: 0=NEXT-BOOT, 1=NOW). if it
|
627
|
-
# fails, allow Gsm::Error to propagate
|
628
|
-
command("AT+WMBS=#{new_band},1")
|
629
|
-
end
|
630
|
-
|
631
|
-
# call-seq:
|
632
|
-
# pin_required? => true or false
|
633
|
-
#
|
634
|
-
# Returns true if the modem is waiting for a SIM PIN. Some SIM cards will refuse
|
635
|
-
# to work until the correct four-digit PIN is provided via the _use_pin_ method.
|
636
|
-
def pin_required?
|
637
|
-
not command("AT+CPIN?").include?("+CPIN: READY")
|
638
|
-
end
|
639
|
-
|
640
|
-
|
641
|
-
# call-seq:
|
642
|
-
# use_pin(pin) => true or false
|
643
|
-
#
|
644
|
-
# Provide a SIM PIN to the modem, and return true if it was accepted.
|
645
|
-
def use_pin(pin)
|
646
|
-
|
647
|
-
# if the sim is already ready,
|
648
|
-
# this method isn't necessary
|
649
|
-
if pin_required?
|
650
|
-
begin
|
651
|
-
command "AT+CPIN=#{pin}"
|
652
|
-
|
653
|
-
# if the command failed, then
|
654
|
-
# the pin was not accepted
|
655
|
-
rescue Gsm::Error
|
656
|
-
return false
|
657
|
-
end
|
658
|
-
end
|
659
|
-
|
660
|
-
# no error = SIM
|
661
|
-
# PIN accepted!
|
662
|
-
true
|
663
|
-
end
|
664
|
-
|
665
|
-
|
666
|
-
# call-seq:
|
667
|
-
# signal => fixnum or nil
|
668
|
-
#
|
669
|
-
# Returns an fixnum between 1 and 99, representing the current
|
670
|
-
# signal strength of the GSM network, or nil if we don't know.
|
671
|
-
def signal_strength
|
672
|
-
data = query("AT+CSQ")
|
673
|
-
if m = data.match(/^\+CSQ: (\d+),/)
|
674
|
-
|
675
|
-
# 99 represents "not known or not detectable",
|
676
|
-
# but we'll use nil for that, since it's a bit
|
677
|
-
# more ruby-ish to test for boolean equality
|
678
|
-
csq = m.captures[0].to_i
|
679
|
-
return (csq<99) ? csq : nil
|
680
|
-
|
681
|
-
else
|
682
|
-
# Todo: Recover from this exception
|
683
|
-
err = "Not CSQ data: #{data.inspect}"
|
684
|
-
raise RuntimeError.new(err)
|
685
|
-
end
|
686
|
-
end
|
687
|
-
|
688
|
-
|
689
|
-
# call-seq:
|
690
|
-
# wait_for_network
|
691
|
-
#
|
692
|
-
# Blocks until the signal strength indicates that the
|
693
|
-
# device is active on the GSM network. It's a good idea
|
694
|
-
# to call this before trying to send or receive anything.
|
695
|
-
def wait_for_network
|
696
|
-
|
697
|
-
# keep retrying until the
|
698
|
-
# network comes up (if ever)
|
699
|
-
until csq = signal_strength
|
700
|
-
sleep 1
|
701
|
-
end
|
702
|
-
|
703
|
-
# return the last
|
704
|
-
# signal strength
|
705
|
-
return csq
|
706
|
-
end
|
707
|
-
|
708
|
-
|
709
|
-
# call-seq:
|
710
|
-
# send_sms(message) => true or false
|
711
|
-
# send_sms(recipient, text) => true or false
|
712
|
-
#
|
713
|
-
# Sends an SMS message via _send_sms!_, but traps
|
714
|
-
# any exceptions raised, and returns false instead.
|
715
|
-
# Use this when you don't really care if the message
|
716
|
-
# was sent, which is... never.
|
717
|
-
def send_sms(*args)
|
718
|
-
begin
|
719
|
-
send_sms!(*args)
|
720
|
-
return true
|
721
|
-
|
722
|
-
# something went wrong
|
723
|
-
rescue Gsm::Error
|
724
|
-
return false
|
725
|
-
end
|
726
|
-
end
|
727
|
-
|
728
|
-
|
729
|
-
# call-seq:
|
730
|
-
# send_sms!(message) => true or raises Gsm::Error
|
731
|
-
# send_sms!(receipt, text) => true or raises Gsm::Error
|
732
|
-
#
|
733
|
-
# Sends an SMS message, and returns true if the network
|
734
|
-
# accepted it for delivery. We currently can't handle read
|
735
|
-
# receipts, so have no way of confirming delivery. If the
|
736
|
-
# device or network rejects the message, a Gsm::Error is
|
737
|
-
# raised containing (hopefully) information about what went
|
738
|
-
# wrong.
|
739
|
-
#
|
740
|
-
# Note: the recipient is passed directly to the modem, which
|
741
|
-
# in turn passes it straight to the SMSC (sms message center).
|
742
|
-
# For maximum compatibility, use phone numbers in international
|
743
|
-
# format, including the *plus* and *country code*.
|
744
|
-
def send_sms!(*args)
|
745
|
-
|
746
|
-
# extract values from Outgoing object.
|
747
|
-
# for now, this does not offer anything
|
748
|
-
# in addition to the recipient/text pair,
|
749
|
-
# but provides an upgrade path for future
|
750
|
-
# features (like FLASH and VALIDITY TIME)
|
751
|
-
if args.length == 1\
|
752
|
-
and args[0].is_a? Gsm::Outgoing
|
753
|
-
to = args[0].recipient
|
754
|
-
msg = args[0].text
|
755
|
-
|
756
|
-
# the < v0.4 arguments. maybe
|
757
|
-
# deprecate this one day
|
758
|
-
elsif args.length == 2
|
759
|
-
to, msg = *args
|
760
|
-
|
761
|
-
else
|
762
|
-
raise ArgumentError,\
|
763
|
-
"The Gsm::Modem#send_sms method accepts" +\
|
764
|
-
"a single Gsm::Outgoing instance, " +\
|
765
|
-
"or recipient and text strings"
|
766
|
-
end
|
767
|
-
|
768
|
-
# the number must be in the international
|
769
|
-
# format for some SMSCs (notably, the one
|
770
|
-
# i'm on right now) so maybe add a PLUS
|
771
|
-
#to = "+#{to}" unless(to[0,1]=="+")
|
772
|
-
|
773
|
-
# 1..9 is a special number which does notm
|
774
|
-
# result in a real sms being sent (see inject.rb)
|
775
|
-
if to == "+123456789"
|
776
|
-
log "Not sending test message: #{msg}"
|
777
|
-
return false
|
778
|
-
end
|
779
|
-
|
780
|
-
# block the receiving thread while
|
781
|
-
# we're sending. it can take some time
|
782
|
-
exclusive do
|
783
|
-
log_incr "Sending SMS to #{to}: #{msg}"
|
784
|
-
|
785
|
-
# initiate the sms, and wait for either
|
786
|
-
# the text prompt or an error message
|
787
|
-
command "AT+CMGS=\"#{to}\"", ["\r\n", "> "]
|
788
|
-
|
789
|
-
# encode the message using the setup encoding or the default
|
790
|
-
msg = encode(msg)
|
791
|
-
|
792
|
-
begin
|
793
|
-
# send the sms, and wait until
|
794
|
-
# it is accepted or rejected
|
795
|
-
write "#{msg}#{26.chr}"
|
796
|
-
wait
|
797
|
-
|
798
|
-
# if something went wrong, we are
|
799
|
-
# be stuck in entry mode (which will
|
800
|
-
# result in someone getting a bunch
|
801
|
-
# of AT commands via sms!) so send
|
802
|
-
# an escpae, to... escape
|
803
|
-
rescue Exception, Timeout::Error => err
|
804
|
-
log "Rescued #{err.desc}"
|
805
|
-
write 27.chr
|
806
|
-
|
807
|
-
# allow the error to propagate,
|
808
|
-
# so the application can catch
|
809
|
-
# it for more useful info
|
810
|
-
raise
|
811
|
-
|
812
|
-
ensure
|
813
|
-
log_decr
|
814
|
-
end
|
815
|
-
end
|
816
|
-
|
817
|
-
# if no error was raised,
|
818
|
-
# then the message was sent
|
819
|
-
return true
|
820
|
-
end
|
821
|
-
|
822
|
-
# Encodes the message using the set encoding or, if no encoding is specified
|
823
|
-
# returns the msg unchange
|
824
|
-
def encode(msg)
|
825
|
-
if (@encoding == :ascii)
|
826
|
-
# TODO, use lucky sneaks here
|
827
|
-
msg
|
828
|
-
elsif (@encoding == :utf8)
|
829
|
-
# Unpacking and repacking supposedly cleans out bad (non-UTF-8) stuff
|
830
|
-
utf8 = msg.unpack("U*");
|
831
|
-
packed = utf8.pack("U*");
|
832
|
-
packed
|
833
|
-
elsif (@encoding == :ucs2)
|
834
|
-
ucs2 = Iconv.iconv("UCS-2", "UTF-8", msg).first
|
835
|
-
ucs2 = ucs2.unpack("H*").join
|
836
|
-
ucs2
|
837
|
-
else
|
838
|
-
msg
|
839
|
-
end
|
840
|
-
end
|
841
|
-
|
842
|
-
# call-seq:
|
843
|
-
# receive(callback_method, interval=5, join_thread=false)
|
844
|
-
#
|
845
|
-
# Starts a new thread, which polls the device every _interval_
|
846
|
-
# seconds to capture incoming SMS and call _callback_method_
|
847
|
-
# for each, and polls the device's internal storage for incoming
|
848
|
-
# SMS that we weren't notified about (some modems don't support
|
849
|
-
# that).
|
850
|
-
#
|
851
|
-
# class Receiver
|
852
|
-
# def incoming(msg)
|
853
|
-
# puts "From #{msg.from} at #{msg.sent}:", msg.text
|
854
|
-
# end
|
855
|
-
# end
|
856
|
-
#
|
857
|
-
# # create the instances,
|
858
|
-
# # and start receiving
|
859
|
-
# rcv = Receiver.new
|
860
|
-
# m = Gsm::Modem.new "/dev/ttyS0"
|
861
|
-
# m.receive rcv.method :incoming
|
862
|
-
#
|
863
|
-
# # block until ctrl+c
|
864
|
-
# while(true) { sleep 2 }
|
865
|
-
#
|
866
|
-
# Note: New messages may arrive at any time, even if this method's
|
867
|
-
# receiver thread isn't waiting to process them. They are not lost,
|
868
|
-
# but cached in @incoming until this method is called.
|
869
|
-
def receive(callback, interval=5, join_thread=false)
|
870
|
-
@polled = 0
|
871
|
-
|
872
|
-
@thr = Thread.new do
|
873
|
-
Thread.current["name"] = "receiver"
|
874
|
-
|
875
|
-
# keep on receiving forever
|
876
|
-
while true
|
877
|
-
process(callback)
|
878
|
-
# re-poll every
|
879
|
-
# five seconds
|
880
|
-
sleep(interval)
|
881
|
-
@polled += 1
|
882
|
-
end
|
883
|
-
end
|
884
|
-
|
885
|
-
# it's sometimes handy to run single-
|
886
|
-
# threaded (like debugging handsets)
|
887
|
-
@thr.join if join_thread
|
888
|
-
end
|
889
|
-
|
890
|
-
def process(callback)
|
891
|
-
command "AT"
|
892
|
-
|
893
|
-
# check for messages in the default mailbox (wether read or not)
|
894
|
-
# read them and then delete them
|
895
|
-
fetch_and_delete_stored_messages
|
896
|
-
|
897
|
-
# if there are any new incoming messages,
|
898
|
-
# iterate, and pass each to the receiver
|
899
|
-
# in the same format that they were built
|
900
|
-
# back in _parse_incoming_sms!_
|
901
|
-
unless @incoming.empty?
|
902
|
-
@incoming.each do |msg|
|
903
|
-
begin
|
904
|
-
callback.call(msg)
|
905
|
-
|
906
|
-
rescue StandardError => err
|
907
|
-
log "Error in callback: #{err}"
|
908
|
-
end
|
909
|
-
end
|
910
|
-
|
911
|
-
# we have dealt with all of the pending
|
912
|
-
# messages. todo: this is a ridiculous
|
913
|
-
# race condition, and i fail at ruby
|
914
|
-
@incoming.clear
|
915
|
-
end
|
916
|
-
end
|
917
|
-
|
918
|
-
|
919
|
-
def encodings?
|
920
|
-
command "AT+CSCS=?"
|
921
|
-
end
|
922
|
-
|
923
|
-
def encoding
|
924
|
-
@encoding
|
925
|
-
end
|
926
|
-
|
927
|
-
def encoding=(enc)
|
928
|
-
@encoding = enc
|
929
|
-
case enc
|
930
|
-
when :ascii
|
931
|
-
command "AT+CSCS=\"ASCII\""
|
932
|
-
when :utf8
|
933
|
-
command "AT+CSCS=\"UTF8\""
|
934
|
-
when :ucs2
|
935
|
-
command "AT+CSCS=\"UCS2\""
|
936
|
-
when :gsm
|
937
|
-
command "AT+CSCS=\"GSM\""
|
938
|
-
when :iso88591
|
939
|
-
command "AT+CSCS=\"8859-1\""
|
940
|
-
end
|
941
|
-
end
|
942
|
-
|
943
|
-
def select_default_mailbox
|
944
|
-
# Eventually we will select the first mailbox as the default
|
945
|
-
result = command("AT+CPMS=?")
|
946
|
-
boxes = result.first.scan(/\"(\w+)\"/).flatten #"
|
947
|
-
mailbox = boxes.first
|
948
|
-
command "AT+CPMS=\"#{mailbox}\""
|
949
|
-
mailbox
|
950
|
-
rescue
|
951
|
-
raise RuntimeError.new("Could not select the default mailbox")
|
952
|
-
nil
|
953
|
-
end
|
954
|
-
|
955
|
-
def fetch_and_delete_stored_messages
|
956
|
-
# If there is no way to select a default mailbox we can't continue
|
957
|
-
return unless select_default_mailbox
|
958
|
-
|
959
|
-
# Try to read the first message from the box
|
960
|
-
begin
|
961
|
-
out = command("AT+CMGR=1")
|
962
|
-
rescue
|
963
|
-
return
|
964
|
-
end
|
965
|
-
return if out.nil? || out.empty?
|
966
|
-
|
967
|
-
# Delete that message
|
968
|
-
command("AT+CMGD=1")
|
969
|
-
|
970
|
-
# Break that down
|
971
|
-
header = out.first
|
972
|
-
msg = out[1..out.size-2].join("\n")
|
973
|
-
validity = out.last
|
974
|
-
|
975
|
-
# Read the header
|
976
|
-
header = header.scan(/\"([^"]+)\"/).flatten #"
|
977
|
-
status = header[0]
|
978
|
-
from = header[1]
|
979
|
-
timestamp = header[2]
|
980
|
-
# Parsing this using the incoming format failed, it need %Y instead of %y
|
981
|
-
sent = DateTime.parse(timestamp)
|
982
|
-
|
983
|
-
# just in case it wasn't already obvious...
|
984
|
-
log "Received message from #{from}: #{msg}"
|
985
|
-
|
986
|
-
@incoming.push Gsm::Incoming.new(self, from, sent, msg)
|
987
|
-
end
|
988
|
-
|
989
|
-
def fetch_unread_messages
|
990
|
-
|
991
|
-
# fetch all/unread (see constant) messages
|
992
|
-
lines = command('AT+CMGL="%s"' % CMGL_STATUS)
|
993
|
-
n = 0
|
994
|
-
|
995
|
-
# if the last line returned is OK
|
996
|
-
# (and it SHOULD BE), remove it
|
997
|
-
lines.pop if lines[-1] == "OK"
|
998
|
-
|
999
|
-
# keep on iterating the data we received,
|
1000
|
-
# until there's none left. if there were no
|
1001
|
-
# stored messages waiting, this done nothing!
|
1002
|
-
while n < lines.length
|
1003
|
-
|
1004
|
-
# attempt to parse the CMGL line (we're skipping
|
1005
|
-
# two lines at a time in this loop, so we will
|
1006
|
-
# always land at a CMGL line here) - they look like:
|
1007
|
-
# +CMGL: 0,"REC READ","+13364130840",,"09/03/04,21:59:31-20"
|
1008
|
-
unless m = lines[n].match(/^\+CMGL: (\d+),"(.+?)","(.+?)",*?,"(.+?)".*?$/)
|
1009
|
-
err = "Couldn't parse CMGL data: #{lines[n]}"
|
1010
|
-
raise RuntimeError.new(err)
|
1011
|
-
end
|
1012
|
-
|
1013
|
-
# find the index of the next
|
1014
|
-
# CMGL line, or the end
|
1015
|
-
nn = n+1
|
1016
|
-
nn += 1 until\
|
1017
|
-
nn >= lines.length ||\
|
1018
|
-
lines[nn][0,6] == "+CMGL:"
|
1019
|
-
|
1020
|
-
# extract the meta-info from the CMGL line, and the
|
1021
|
-
# message text from the lines between _n_ and _nn_
|
1022
|
-
index, status, from, timestamp = *m.captures
|
1023
|
-
msg_text = lines[(n+1)..(nn-1)].join("\n").strip
|
1024
|
-
|
1025
|
-
# log the incoming message
|
1026
|
-
log "Fetched stored message from #{from}: #{msg_text.inspect}"
|
1027
|
-
|
1028
|
-
# store the incoming data to be picked up
|
1029
|
-
# from the attr_accessor as a tuple (this
|
1030
|
-
# is kind of ghetto, and WILL change later)
|
1031
|
-
sent = parse_incoming_timestamp(timestamp)
|
1032
|
-
msg = Gsm::Incoming.new(self, from, sent, msg_text)
|
1033
|
-
@incoming.push(msg)
|
1034
|
-
|
1035
|
-
# skip over the messge line(s),
|
1036
|
-
# on to the next CMGL line
|
1037
|
-
n = nn
|
1038
|
-
end
|
1039
|
-
end
|
1040
|
-
end # Modem
|
1041
|
-
end # Gsm
|
1042
|
-
=end
|