Compare commits
51 Commits
Author | SHA1 | Date |
---|---|---|
Guillaume Dott | 932acd1046 | |
Guillaume DOTT | c842211e75 | |
Guillaume DOTT | 52c9a13ca0 | |
Guillaume DOTT | e34a881ab7 | |
Guillaume DOTT | e1e7be5d98 | |
Guillaume DOTT | 6767b99b4a | |
Guillaume DOTT | 926d7ec544 | |
Guillaume DOTT | 3656fd6d82 | |
Guillaume DOTT | 77105b180a | |
Guillaume DOTT | 9475bdfb5c | |
Guillaume DOTT | a3a9c43d8b | |
Guillaume DOTT | 4e8463e0dc | |
Guillaume DOTT | a0757a9c69 | |
Guillaume DOTT | 001cbacfca | |
Guillaume DOTT | be53921802 | |
Guillaume DOTT | 550a2b4d3d | |
Guillaume DOTT | aabb2027b0 | |
Guillaume DOTT | 7281b8f82d | |
Guillaume DOTT | 2f4f1f81f3 | |
Guillaume DOTT | ff6dc617dc | |
Guillaume DOTT | f4218e1025 | |
Guillaume DOTT | f049777a00 | |
Guillaume DOTT | e790ac07c4 | |
Guillaume DOTT | 276f0b0093 | |
Guillaume DOTT | ea3106cfc2 | |
Guillaume DOTT | d1103a0772 | |
Guillaume DOTT | 2de794042d | |
Guillaume DOTT | 1c3521586a | |
Guillaume DOTT | dbd2f09ec2 | |
Guillaume DOTT | 885ac26cd1 | |
Guillaume DOTT | 3c677b7d29 | |
Guillaume DOTT | b4f49569bb | |
Guillaume DOTT | c0a3fe8ef0 | |
Guillaume DOTT | 955654365a | |
Guillaume DOTT | 45de11cb3b | |
Guillaume DOTT | cf655b2b3c | |
Guillaume DOTT | 84fbedc46b | |
Guillaume DOTT | 1446be3c14 | |
Guillaume DOTT | be87792382 | |
Guillaume DOTT | 548e5cd4cd | |
Guillaume DOTT | a001604ee7 | |
Guillaume DOTT | 4ca3d259c1 | |
Guillaume DOTT | 75509ac23a | |
Guillaume DOTT | 34224a072c | |
Guillaume DOTT | 67571dd12e | |
Guillaume DOTT | b1568bbf24 | |
Guillaume DOTT | 404f7d8290 | |
Guillaume DOTT | dcbafff75b | |
Thomas Kienlen | b17822b4eb | |
Thomas Kienlen | c087eb3973 | |
Rodrigo Pinto | 898d8d73a9 |
|
@ -16,3 +16,4 @@ spec/reports
|
||||||
test/tmp
|
test/tmp
|
||||||
test/version_tmp
|
test/version_tmp
|
||||||
tmp
|
tmp
|
||||||
|
*.s[a-w][a-z]
|
||||||
|
|
47
README.md
47
README.md
|
@ -1,6 +1,6 @@
|
||||||
# Biju
|
# Biju
|
||||||
|
|
||||||
[WIP] Biju is an easy way to mount a GSM modem to send, to receive and to delete messages through a ruby interface.
|
Biju is an easy way to mount a GSM modem to send, to receive and to delete messages through a ruby interface.
|
||||||
This is project is based on this [code snippet](http://dzone.com/snippets/send-and-receive-sms-text).
|
This is project is based on this [code snippet](http://dzone.com/snippets/send-and-receive-sms-text).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
@ -20,24 +20,28 @@ Or install it yourself as:
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
@modem = Biju::Modem.new(:port => "/dev/tty.HUAWEIMobile-Modem")
|
modem = Biju::Hayes.new('/dev/tty.HUAWEIMobile-Modem', pin: '0000')
|
||||||
|
|
||||||
# method to list all messages
|
# method to list all messages
|
||||||
@modem.messages.each do |sms|
|
# it can take the status in argument
|
||||||
|
# :unread, :read, :unsent, :sent, :all
|
||||||
|
modem.messages.each do |sms|
|
||||||
puts sms
|
puts sms
|
||||||
end
|
end
|
||||||
|
|
||||||
# method to send sms
|
# method to send sms
|
||||||
sms = Biju::Sms.new(:phone_number => '+3312345678', :message => 'hello world')
|
sms = Biju::Sms.new(phone_number: '+3312345678', message: 'hello world')
|
||||||
@modem.send(sms)
|
modem.send(sms)
|
||||||
|
|
||||||
@modem.close
|
modem.close
|
||||||
```
|
```
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
1. Write missing test for modem module.
|
1. Write missing test for modem module.
|
||||||
2. Write a documentation.
|
2. Write a documentation.
|
||||||
3. Test with different kinds of modem and OS.
|
3. Test with different kinds of modem and OS.
|
||||||
|
4. Handle UDH (User Data Header) and SMS longer than 140 octets
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
@ -46,3 +50,34 @@ sms = Biju::Sms.new(:phone_number => '+3312345678', :message => 'hello world')
|
||||||
3. Commit your changes (`git commit -am 'Added some feature'`)
|
3. Commit your changes (`git commit -am 'Added some feature'`)
|
||||||
4. Push to the branch (`git push origin my-new-feature`)
|
4. Push to the branch (`git push origin my-new-feature`)
|
||||||
5. Create new Pull Request
|
5. Create new Pull Request
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
http://en.wikipedia.org/wiki/GSM_03.40
|
||||||
|
http://www.etsi.org/deliver/etsi_gts/04/0408/05.00.00_60/gsmts_0408v050000p.pdf
|
||||||
|
http://www.etsi.org/deliver/etsi_ts/101000_101099/101032/05.01.00_60/ts_101032v050100p.pdf
|
||||||
|
http://en.wikipedia.org/wiki/Short_message_service_center
|
||||||
|
http://en.wikipedia.org/wiki/AT_command
|
||||||
|
http://subnets.ru/saved/sms_pdu_format.html
|
||||||
|
http://jazi.staff.ugm.ac.id/Mobile%20and%20Wireless%20Documents/SMS_PDU-mode.PDF
|
||||||
|
http://www.sendsms.cn/download/SMS_PDU-mode.PDF
|
||||||
|
http://www.sendsms.cn/download/wavecom/PDU%B6%CC%D0%C5%CF%A2/SMS_PDU-mode.PDF
|
||||||
|
http://www.gsm-modem.de/sms-pdu-mode.html
|
||||||
|
http://www.developershome.com/sms/cmgrCommand3.asp
|
||||||
|
http://www.developershome.com/sms/cmgsCommand4.asp
|
||||||
|
http://en.wikipedia.org/wiki/Concatenated_SMS
|
||||||
|
|
||||||
|
# Encoding
|
||||||
|
http://en.wikipedia.org/wiki/GSM_03.38
|
||||||
|
http://www.3gpp.org/ftp/Specs/html-info/0338.htm
|
||||||
|
http://www.codeproject.com/Tips/470755/Encoding-Decoding-7-bit-User-Data-for-SMS-PDU-PDU
|
||||||
|
https://github.com/bitcoder/ruby_ucp/wiki/SMS-Alphabets
|
||||||
|
|
||||||
|
# AT Commands
|
||||||
|
http://en.wikipedia.org/wiki/Hayes_command_set
|
||||||
|
https://www.sparkfun.com/datasheets/Cellular%20Modules/ADH8066-AT-Commands-v1.6.pdf
|
||||||
|
http://www.coster.eu/costerit/teleges/doc/gsm822w.pdf
|
||||||
|
http://www.developershome.com/sms/cmglCommand.asp
|
||||||
|
http://www.zoomtel.com/documentation/dial_up/100498D.pdf
|
||||||
|
|
||||||
|
AT+CLAC > list supported commands
|
||||||
|
|
|
@ -1,9 +1 @@
|
||||||
#!/usr/bin/env rake
|
|
||||||
require "bundler/gem_tasks"
|
require "bundler/gem_tasks"
|
||||||
require 'rake/testtask'
|
|
||||||
|
|
||||||
Rake::TestTask.new do |t|
|
|
||||||
t.libs.push "lib"
|
|
||||||
t.test_files = FileList['spec/**/*_spec.rb']
|
|
||||||
t.verbose = true
|
|
||||||
end
|
|
|
@ -15,7 +15,8 @@ Gem::Specification.new do |gem|
|
||||||
gem.require_paths = ["lib"]
|
gem.require_paths = ["lib"]
|
||||||
gem.version = Biju::VERSION
|
gem.version = Biju::VERSION
|
||||||
|
|
||||||
gem.add_development_dependency "minitest", "3.0.0"
|
gem.add_development_dependency "rspec", "~> 2.14.0"
|
||||||
|
|
||||||
gem.add_dependency "serialport", "1.0.4"
|
gem.add_dependency "serialport", "~> 1.1.0"
|
||||||
|
gem.add_dependency "parslet", "~> 1.5.0"
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
require 'biju/version'
|
require 'biju/version'
|
||||||
require "biju/modem"
|
require 'biju/modem'
|
||||||
require "biju/sms"
|
require 'biju/pdu'
|
||||||
|
require 'biju/to_hayes'
|
||||||
|
require 'biju/hayes'
|
||||||
|
require 'biju/sms'
|
||||||
|
require 'biju/parser'
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
module Biju
|
||||||
|
module AT
|
||||||
|
# Message Equipement Failure
|
||||||
|
class CmeError < Error
|
||||||
|
ERRORS = {
|
||||||
|
0 => 'Phone failure',
|
||||||
|
1 => 'No connection to phone',
|
||||||
|
2 => 'Phone adapter link reserved',
|
||||||
|
3 => 'Operation not allowed',
|
||||||
|
4 => 'Operation not supported',
|
||||||
|
5 => 'PH_SIM PIN required',
|
||||||
|
6 => 'PH_FSIM PIN required',
|
||||||
|
7 => 'PH_FSIM PUK required',
|
||||||
|
10 => 'SIM not inserted',
|
||||||
|
11 => 'SIM PIN required',
|
||||||
|
12 => 'SIM PUK required',
|
||||||
|
13 => 'SIM failure',
|
||||||
|
14 => 'SIM busy',
|
||||||
|
15 => 'SIM wrong',
|
||||||
|
16 => 'Incorrect password',
|
||||||
|
17 => 'SIM PIN2 required',
|
||||||
|
18 => 'SIM PUK2 required',
|
||||||
|
20 => 'Memory full',
|
||||||
|
21 => 'Invalid index',
|
||||||
|
22 => 'Not found',
|
||||||
|
23 => 'Memory failure',
|
||||||
|
24 => 'Text string too long',
|
||||||
|
25 => 'Invalid characters in text string',
|
||||||
|
26 => 'Dial string too long',
|
||||||
|
27 => 'Invalid characters in dial string',
|
||||||
|
30 => 'No network service',
|
||||||
|
31 => 'Network timeout',
|
||||||
|
32 => 'Network not allowed, emergency calls only',
|
||||||
|
40 => 'Network personalization PIN required',
|
||||||
|
41 => 'Network personalization PUK required',
|
||||||
|
42 => 'Network subset personalization PIN required',
|
||||||
|
43 => 'Network subset personalization PUK required',
|
||||||
|
44 => 'Service provider personalization PIN required',
|
||||||
|
45 => 'Service provider personalization PUK required',
|
||||||
|
46 => 'Corporate personalization PIN required',
|
||||||
|
47 => 'Corporate personalization PUK required',
|
||||||
|
48 => 'PH-SIM PUK required',
|
||||||
|
100 => 'Unknown error',
|
||||||
|
103 => 'Illegal MS',
|
||||||
|
106 => 'Illegal ME',
|
||||||
|
107 => 'GPRS services not allowed',
|
||||||
|
111 => 'PLMN not allowed',
|
||||||
|
112 => 'Location area not allowed',
|
||||||
|
113 => 'Roaming not allowed in this location area',
|
||||||
|
126 => 'Operation temporary not allowed',
|
||||||
|
132 => 'Service operation not supported',
|
||||||
|
133 => 'Requested service option not subscribed',
|
||||||
|
134 => 'Service option temporary out of order',
|
||||||
|
148 => 'Unspecified GPRS error',
|
||||||
|
149 => 'PDP authentication failure',
|
||||||
|
150 => 'Invalid mobile class',
|
||||||
|
256 => 'Operation temporarily not allowed',
|
||||||
|
257 => 'Call barred',
|
||||||
|
258 => 'Phone is busy',
|
||||||
|
259 => 'User abort',
|
||||||
|
260 => 'Invalid dial string',
|
||||||
|
261 => 'SS not executed',
|
||||||
|
262 => 'SIM Blocked',
|
||||||
|
263 => 'Invalid block',
|
||||||
|
772 => 'SIM powered down',
|
||||||
|
}
|
||||||
|
|
||||||
|
def initialize(id)
|
||||||
|
super(id, 100)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,99 @@
|
||||||
|
module Biju
|
||||||
|
module AT
|
||||||
|
# Message Service Failure
|
||||||
|
class CmsError < Error
|
||||||
|
ERRORS = {
|
||||||
|
1 => 'Unassigned number',
|
||||||
|
8 => 'Operator determined barring',
|
||||||
|
10 => 'Call bared',
|
||||||
|
21 => 'Short message transfer rejected',
|
||||||
|
27 => 'Destination out of service',
|
||||||
|
28 => 'Unindentified subscriber',
|
||||||
|
29 => 'Facility rejected',
|
||||||
|
30 => 'Unknown subscriber',
|
||||||
|
38 => 'Network out of order',
|
||||||
|
41 => 'Temporary failure',
|
||||||
|
42 => 'Congestion',
|
||||||
|
47 => 'Recources unavailable',
|
||||||
|
50 => 'Requested facility not subscribed',
|
||||||
|
69 => 'Requested facility not implemented',
|
||||||
|
81 => 'Invalid short message transfer reference value',
|
||||||
|
95 => 'Invalid message unspecified',
|
||||||
|
96 => 'Invalid mandatory information',
|
||||||
|
97 => 'Message type non existent or not implemented',
|
||||||
|
98 => 'Message not compatible with short message protocol',
|
||||||
|
99 => 'Information element non-existent or not implemente',
|
||||||
|
111 => 'Protocol error, unspecified',
|
||||||
|
127 => 'Internetworking , unspecified',
|
||||||
|
128 => 'Telematic internetworking not supported',
|
||||||
|
129 => 'Short message type 0 not supported',
|
||||||
|
130 => 'Cannot replace short message',
|
||||||
|
143 => 'Unspecified TP-PID error',
|
||||||
|
144 => 'Data code scheme not supported',
|
||||||
|
145 => 'Message class not supported',
|
||||||
|
159 => 'Unspecified TP-DCS error',
|
||||||
|
160 => 'Command cannot be actioned',
|
||||||
|
161 => 'Command unsupported',
|
||||||
|
175 => 'Unspecified TP-Command error',
|
||||||
|
176 => 'TPDU not supported',
|
||||||
|
192 => 'SC busy',
|
||||||
|
193 => 'No SC subscription',
|
||||||
|
194 => 'SC System failure',
|
||||||
|
195 => 'Invalid SME address',
|
||||||
|
196 => 'Destination SME barred',
|
||||||
|
197 => 'SM Rejected-Duplicate SM',
|
||||||
|
198 => 'TP-VPF not supported',
|
||||||
|
199 => 'TP-VP not supported',
|
||||||
|
208 => 'D0 SIM SMS Storage full',
|
||||||
|
209 => 'No SMS Storage capability in SIM',
|
||||||
|
210 => 'Error in MS',
|
||||||
|
211 => 'Memory capacity exceeded',
|
||||||
|
212 => 'Sim application toolkit busy',
|
||||||
|
213 => 'SIM data download error',
|
||||||
|
255 => 'Unspecified error cause',
|
||||||
|
300 => 'ME Failure',
|
||||||
|
301 => 'SMS service of ME reserved',
|
||||||
|
302 => 'Operation not allowed',
|
||||||
|
303 => 'Operation not supported',
|
||||||
|
304 => 'Invalid PDU mode parameter',
|
||||||
|
305 => 'Invalid Text mode parameter',
|
||||||
|
310 => 'SIM not inserted',
|
||||||
|
311 => 'SIM PIN required',
|
||||||
|
312 => 'PH-SIM PIN required',
|
||||||
|
313 => 'SIM failure',
|
||||||
|
314 => 'SIM busy',
|
||||||
|
315 => 'SIM wrong',
|
||||||
|
316 => 'SIM PUK required',
|
||||||
|
317 => 'SIM PIN2 required',
|
||||||
|
318 => 'SIM PUK2 required',
|
||||||
|
320 => 'Memory failure',
|
||||||
|
321 => 'Invalid memory index',
|
||||||
|
322 => 'Memory full',
|
||||||
|
330 => 'SMSC address unknown',
|
||||||
|
331 => 'No network service',
|
||||||
|
332 => 'Network timeout',
|
||||||
|
340 => 'No +CNMA expected',
|
||||||
|
500 => 'Unknown error',
|
||||||
|
512 => 'User abort',
|
||||||
|
513 => 'Unable to store',
|
||||||
|
514 => 'Invalid Status',
|
||||||
|
515 => 'Device busy or Invalid Character in string',
|
||||||
|
516 => 'Invalid length',
|
||||||
|
517 => 'Invalid character in PDU',
|
||||||
|
518 => 'Invalid parameter',
|
||||||
|
519 => 'Invalid length or character',
|
||||||
|
520 => 'Invalid character in text',
|
||||||
|
521 => 'Timer expired',
|
||||||
|
522 => 'Operation temporary not allowed',
|
||||||
|
532 => 'SIM not ready',
|
||||||
|
534 => 'Cell Broadcast error unknown',
|
||||||
|
535 => 'Protocol stack busy',
|
||||||
|
538 => 'Invalid parameter',
|
||||||
|
}
|
||||||
|
|
||||||
|
def initialize(id)
|
||||||
|
super(id, 500)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,17 @@
|
||||||
|
module Biju
|
||||||
|
module AT
|
||||||
|
class Error < ::Exception
|
||||||
|
ERRORS = {
|
||||||
|
1 => 'Unknown error',
|
||||||
|
}
|
||||||
|
|
||||||
|
def initialize(id, default = 1)
|
||||||
|
@error_id = (self.class::ERRORS.has_key?(id) ? id : default)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
"#{self.class::ERRORS[@error_id]} (#{@error_id})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,159 @@
|
||||||
|
require 'biju/at/error'
|
||||||
|
require 'biju/at/cms_error'
|
||||||
|
require 'biju/at/cme_error'
|
||||||
|
|
||||||
|
module Biju
|
||||||
|
class Hayes
|
||||||
|
attr_reader :modem
|
||||||
|
|
||||||
|
MESSAGE_STATUS = {
|
||||||
|
unread: [0, 'REC UNREAD'],
|
||||||
|
read: [1, 'REC UNREAD'],
|
||||||
|
unsent: [2, ''],
|
||||||
|
sent: [3, ''],
|
||||||
|
all: [4, 'ALL'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def initialize(port, options = {})
|
||||||
|
pin = options.delete(:pin) || '0000'
|
||||||
|
@modem = Modem.new(port, options)
|
||||||
|
|
||||||
|
attention
|
||||||
|
unlock_pin pin
|
||||||
|
|
||||||
|
text_mode(false)
|
||||||
|
extended_error
|
||||||
|
end
|
||||||
|
|
||||||
|
def close
|
||||||
|
modem.close
|
||||||
|
end
|
||||||
|
|
||||||
|
def at_command(cmd = nil, *args, &block)
|
||||||
|
command = ['AT', cmd].compact.join
|
||||||
|
command_args = args.compact.to_hayes
|
||||||
|
|
||||||
|
full_command = [command, (command_args.empty? ? nil : command_args)]
|
||||||
|
.compact.join('=') + "\r\n"
|
||||||
|
|
||||||
|
modem.flush
|
||||||
|
modem.write(full_command)
|
||||||
|
answer = hayes_to_obj(modem.wait(length: full_command.length))
|
||||||
|
|
||||||
|
return block.call(answer) if block_given?
|
||||||
|
answer
|
||||||
|
end
|
||||||
|
|
||||||
|
def attention
|
||||||
|
at_command[:status]
|
||||||
|
end
|
||||||
|
|
||||||
|
def init_modem
|
||||||
|
at_command('Z')[:status]
|
||||||
|
end
|
||||||
|
|
||||||
|
def phone_numbers
|
||||||
|
result = at_command('+CNUM')
|
||||||
|
return [] unless result.has_key?(:phone_numbers)
|
||||||
|
|
||||||
|
result[:phone_numbers].map do |number|
|
||||||
|
{
|
||||||
|
number: number[:array][1].gsub(/[^0-9]/, ''),
|
||||||
|
type_of_address: PDU::TypeOfAddress.new(number[:array][2]).to_sym
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def text_mode(enabled = true)
|
||||||
|
at_command('+CMGF', enabled)[:status]
|
||||||
|
end
|
||||||
|
|
||||||
|
def text_mode?(force = false)
|
||||||
|
@text_mode = at_command('+CMGF?')[:result] if @text_mode.nil? || force
|
||||||
|
@text_mode
|
||||||
|
end
|
||||||
|
|
||||||
|
def extended_error(enabled = true)
|
||||||
|
at_command('+CMEE', enabled)[:status]
|
||||||
|
end
|
||||||
|
|
||||||
|
def prefered_storage(pms = nil)
|
||||||
|
result = at_command('+CPMS', pms)
|
||||||
|
return result[:array] if result[:cmd] == '+CPMS'
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def pin_status
|
||||||
|
at_command('+CPIN?')[:result]
|
||||||
|
end
|
||||||
|
|
||||||
|
def unlock_pin(pin)
|
||||||
|
at_command('+CPIN', pin)[:status] if pin_status == 'SIM PIN'
|
||||||
|
end
|
||||||
|
|
||||||
|
def messages!(which = :all)
|
||||||
|
messages(which, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def messages(which = :all, exceptions = false)
|
||||||
|
which = MESSAGE_STATUS[which][text_mode? ? 1 : 0] if which.is_a?(Symbol)
|
||||||
|
|
||||||
|
sms = at_command('+CMGL', which)
|
||||||
|
|
||||||
|
return [] unless sms.has_key?(:sms)
|
||||||
|
sms[:sms].map do |msg|
|
||||||
|
begin
|
||||||
|
Biju::Sms.from_pdu(msg[:message].chomp, msg[:infos][0])
|
||||||
|
rescue Biju::PDU::Errors::PDUError => e
|
||||||
|
malformed = Biju::PDU::Errors::MalformedSms.new(msg[:message].chomp, msg[:infos][0], e)
|
||||||
|
if exceptions
|
||||||
|
raise malformed
|
||||||
|
else
|
||||||
|
malformed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Delete a sms message by id.
|
||||||
|
# @param [Fixnum] Id of sms message on modem.
|
||||||
|
def delete(id)
|
||||||
|
id = [id] if id.kind_of?(Fixnum)
|
||||||
|
return unless id.kind_of?(Enumerable)
|
||||||
|
|
||||||
|
res = true
|
||||||
|
id.each { |i| res &= at_command('+CMGD', i)[:status] }
|
||||||
|
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
def send(sms, options = {})
|
||||||
|
result = at_command('+CMGS', (sms.to_pdu.length - 2) / 2)
|
||||||
|
|
||||||
|
if result[:prompt]
|
||||||
|
modem.write("#{sms.to_pdu}#{26.chr}")
|
||||||
|
res = ''
|
||||||
|
loop do
|
||||||
|
res = modem.wait(length: 8)
|
||||||
|
break unless res.match(/\A[0-9A-Fa-f]+\r\n\z/)
|
||||||
|
end
|
||||||
|
hayes_to_obj(res.lstrip)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def hayes_to_obj(str)
|
||||||
|
res = ATTransform.new.apply(ATParser.new.parse(str))
|
||||||
|
|
||||||
|
case res[:cmd]
|
||||||
|
when '+CMS ERROR'
|
||||||
|
raise AT::CmsError.new(res[:result])
|
||||||
|
when '+CME ERROR'
|
||||||
|
raise AT::CmeError.new(res[:result])
|
||||||
|
end
|
||||||
|
|
||||||
|
res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,86 +1,42 @@
|
||||||
require 'serialport'
|
require 'serialport'
|
||||||
require_relative 'sms'
|
require 'forwardable'
|
||||||
|
require 'timeout'
|
||||||
|
|
||||||
module Biju
|
module Biju
|
||||||
class Modem
|
class Modem
|
||||||
|
extend Forwardable
|
||||||
|
|
||||||
|
DEFAULT_OPTIONS = { baud: 9600, data_bits: 8,
|
||||||
|
stop_bits: 1, parity: SerialPort::NONE }
|
||||||
|
|
||||||
|
attr_reader :connection
|
||||||
|
|
||||||
# @param [Hash] Options to serial connection.
|
# @param [Hash] Options to serial connection.
|
||||||
# @option options [String] :port The modem port to connect
|
# @option options [String] :port The modem port to connect
|
||||||
#
|
#
|
||||||
# Biju::Modem.new(:port => '/dev/ttyUSB0')
|
# Biju::Modem.new('/dev/ttyUSB0')
|
||||||
#
|
#
|
||||||
def initialize(options={}, &block)
|
def initialize(port, options = {})
|
||||||
raise Exception.new("Port is required") unless options[:port]
|
@connection = SerialPort.new(port, DEFAULT_OPTIONS.merge!(options))
|
||||||
pin = options.delete(:pin)
|
|
||||||
@connection = connection(options)
|
|
||||||
cmd("AT")
|
|
||||||
# initialize modem
|
|
||||||
cmd("ATZ")
|
|
||||||
# unlock pin code
|
|
||||||
cmd("AT+CPIN=\"#{pin}\"") if pin
|
|
||||||
# set SMS text mode
|
|
||||||
cmd("AT+CMGF=1")
|
|
||||||
# set extended error reports
|
|
||||||
cmd('AT+CMEE=1')
|
|
||||||
#instance_eval &block if block_given?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Close the serial connection.
|
def_delegators :connection, :close, :write
|
||||||
def close
|
|
||||||
@connection.close
|
def flush
|
||||||
|
wait(length: 0, timeout: 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Return an Array of Sms if there is messages nad return nil if not.
|
def wait(options = {})
|
||||||
def messages(which = "ALL")
|
length = options[:length] || 0
|
||||||
# read message from all storage in the mobile phone (sim+mem)
|
timeout = options[:timeout] || 10
|
||||||
cmd('AT+CPMS="MT"')
|
|
||||||
# get message list
|
|
||||||
sms = cmd('AT+CMGL="%s"' % which )
|
|
||||||
# collect messages
|
|
||||||
msgs = sms.scan(/\+CMGL\:\s*?(\d+)\,.*?\,\"(.+?)\"\,.*?\,\"(.+?)\".*?\n(.*)/)
|
|
||||||
return nil unless msgs
|
|
||||||
msgs.collect!{ |msg| Biju::Sms.new(:id => msg[0], :phone_number => msg[1], :datetime => msg[2], :message => msg[3].chomp) }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Delete a sms message by id.
|
|
||||||
# @param [Fixnum] Id of sms message on modem.
|
|
||||||
def delete(id)
|
|
||||||
cmd("AT+CMGD=#{id}")
|
|
||||||
end
|
|
||||||
|
|
||||||
def send(sms, options = {})
|
|
||||||
# initiate the sms, and wait for either
|
|
||||||
# the text prompt or an error message
|
|
||||||
cmd("AT+CMGS=\"#{sms.phone_number}\"")
|
|
||||||
|
|
||||||
# send the sms, and wait until
|
|
||||||
# it is accepted or rejected
|
|
||||||
cmd("#{sms.message}#{26.chr}")
|
|
||||||
# ... check reception
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def connection(options)
|
|
||||||
port = options.delete(:port)
|
|
||||||
SerialPort.new(port, default_options.merge!(options))
|
|
||||||
end
|
|
||||||
|
|
||||||
def default_options
|
|
||||||
{ :baud => 9600, :data_bits => 8, :stop_bits => 1, :parity => SerialPort::NONE }
|
|
||||||
end
|
|
||||||
|
|
||||||
def cmd(cmd)
|
|
||||||
@connection.write(cmd + "\r")
|
|
||||||
wait_str = wait
|
|
||||||
#p "#{cmd} --> #{wait_str}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def wait
|
|
||||||
buffer = ''
|
buffer = ''
|
||||||
while IO.select([@connection], [], [], 0.25)
|
Timeout.timeout(timeout) do
|
||||||
chr = @connection.getc.chr;
|
while IO.select([connection], [], [], 0.25) || buffer.length < length
|
||||||
buffer += chr
|
buffer << connection.getc.chr
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
buffer
|
buffer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
require 'parslet'
|
||||||
|
require 'date'
|
||||||
|
|
||||||
|
module Biju
|
||||||
|
class ATParser < Parslet::Parser
|
||||||
|
root :at_string
|
||||||
|
|
||||||
|
rule(:at_string) { request | response }
|
||||||
|
|
||||||
|
# REQUEST
|
||||||
|
rule(:request) do
|
||||||
|
str('+++') | str('A/') | (prefix >> (cr.absent? >> lf.absent? >> any).repeat(0)) >>
|
||||||
|
(cr >> crlf >> response).maybe
|
||||||
|
end
|
||||||
|
rule(:prefix) { str('AT') | str('at') }
|
||||||
|
|
||||||
|
# RESPONSE
|
||||||
|
rule(:response) { ((command.maybe >> status) | merror) >> crlf | prompt }
|
||||||
|
rule(:prompt) { str('> ').as(:prompt) }
|
||||||
|
rule(:command) { mgl | num | pin | mgf | mgs | generic_response }
|
||||||
|
|
||||||
|
rule(:merror) do
|
||||||
|
(str('+CME ERROR') | str('+CMS ERROR')).as(:cmd) >> str(': ') >>
|
||||||
|
int.as(:result)
|
||||||
|
end
|
||||||
|
|
||||||
|
rule(:mgl) do
|
||||||
|
(str('+CMGL').as(:cmd) >> str(': ') >> infos >> crlf >> message >> crlf)
|
||||||
|
.repeat(1).as(:sms) >> crlf
|
||||||
|
end
|
||||||
|
rule(:num) do
|
||||||
|
(str('+CNUM').as(:cmd) >> str(': ') >> array >> crlf)
|
||||||
|
.repeat(1).as(:phone_numbers) >> crlf >> crlf
|
||||||
|
end
|
||||||
|
rule(:mgf) do
|
||||||
|
str('+CMGF').as(:cmd) >> str(': ') >> boolean.as(:result) >> crlf >> crlf
|
||||||
|
end
|
||||||
|
rule(:pin) do
|
||||||
|
str('+CPIN').as(:cmd) >> str(': ') >> eol.as(:result) >> crlf >> crlf
|
||||||
|
end
|
||||||
|
rule(:mgs) do
|
||||||
|
str('+CMGS').as(:cmd) >> str(': ') >> int.as(:result) >> crlf >> crlf
|
||||||
|
end
|
||||||
|
rule(:generic_response) do
|
||||||
|
match('[^:]').repeat(1).as(:cmd) >> str(': ') >> array >>
|
||||||
|
crlf >> crlf
|
||||||
|
end
|
||||||
|
|
||||||
|
rule(:array) do
|
||||||
|
(data >> (comma >> data).repeat).as(:array)
|
||||||
|
end
|
||||||
|
rule(:data) { (str('(') >> array >> str(')')) | info }
|
||||||
|
rule(:infos) { (info >> (comma >> info).repeat).as(:infos) }
|
||||||
|
rule(:info) { datetime | string | int | empty_string }
|
||||||
|
rule(:message) { match('[0-9A-Fa-f]').repeat(1).as(:message) }
|
||||||
|
|
||||||
|
# MISC
|
||||||
|
rule(:status) { (ok | error).as(:status) }
|
||||||
|
rule(:ok) { str('OK').as(:ok) }
|
||||||
|
rule(:error) { str('ERROR').as(:error) }
|
||||||
|
|
||||||
|
rule(:cr) { str("\r") }
|
||||||
|
rule(:lf) { str("\n") }
|
||||||
|
rule(:crlf) { cr >> lf }
|
||||||
|
rule(:comma) { str(',') }
|
||||||
|
rule(:quote) { str('"') }
|
||||||
|
|
||||||
|
rule(:empty_string) { str('').as(:empty_string) }
|
||||||
|
rule(:string) { quote >> match('[^\"]').repeat.as(:string) >> quote }
|
||||||
|
rule(:int) { match('[0-9]').repeat(1).as(:int) }
|
||||||
|
rule(:boolean) { match('[01]').as(:boolean) }
|
||||||
|
rule(:eol) { (crlf.absent? >> any).repeat.as(:string) }
|
||||||
|
|
||||||
|
rule(:datetime) { quote >> (date >> str(',') >> time).as(:datetime) >> quote }
|
||||||
|
rule(:date) do
|
||||||
|
(match('[0-9]').repeat(2) >> str('/')).repeat(2) >> match('[0-9]').repeat(2)
|
||||||
|
end
|
||||||
|
rule(:time) do
|
||||||
|
(match('[0-9]').repeat(2) >> str(':')).repeat(2) >> match('[0-9]').repeat(2) >>
|
||||||
|
match('[-+]') >> match('[0-9]').repeat(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class ATTransform < Parslet::Transform
|
||||||
|
rule(prompt: simple(:prompt)) { { prompt: true } }
|
||||||
|
rule(cmd: simple(:cmd), infos: subtree(:infos), message: simple(:message)) do
|
||||||
|
{ cmd: cmd.to_s, infos: infos, message: message.to_s }
|
||||||
|
end
|
||||||
|
rule(cmd: simple(:cmd), array: subtree(:array)) do
|
||||||
|
{ cmd: cmd.to_s, array: array }
|
||||||
|
end
|
||||||
|
rule(cmd: simple(:cmd), result: simple(:result)) do
|
||||||
|
{ cmd: cmd.to_s, result: result }
|
||||||
|
end
|
||||||
|
|
||||||
|
rule(empty_string: simple(:empty_string)) { '' }
|
||||||
|
rule(int: simple(:int)) { int.to_i }
|
||||||
|
rule(boolean: simple(:boolean)) { boolean.to_i > 0 }
|
||||||
|
rule(string: simple(:string)) { string.to_s }
|
||||||
|
rule(datetime: simple(:datetime)) do
|
||||||
|
DateTime.strptime(datetime.to_s, '%y/%m/%d,%T%Z')
|
||||||
|
end
|
||||||
|
rule(array: subtree(:array)) { array }
|
||||||
|
|
||||||
|
rule(status: simple(:status)) { { status: status } }
|
||||||
|
rule(ok: simple(:ok)) { true }
|
||||||
|
rule(error: simple(:error)) { false }
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,84 @@
|
||||||
|
require 'biju/pdu/encoding/gsm7bit'
|
||||||
|
require 'biju/pdu/encoding/ucs2'
|
||||||
|
|
||||||
|
require 'biju/pdu/user_data'
|
||||||
|
require 'biju/pdu/data_coding_scheme'
|
||||||
|
|
||||||
|
require 'biju/pdu/first_octet'
|
||||||
|
require 'biju/pdu/timestamp'
|
||||||
|
require 'biju/pdu/phone_number'
|
||||||
|
require 'biju/pdu/type_of_address'
|
||||||
|
|
||||||
|
require 'biju/pdu/errors'
|
||||||
|
|
||||||
|
module Biju
|
||||||
|
module PDU
|
||||||
|
def self.encode(phone_number, message, options = {})
|
||||||
|
type_of_address = options[:type_of_address] || :international
|
||||||
|
|
||||||
|
phone_number = PhoneNumber.encode(phone_number)
|
||||||
|
user_data = UserData.encode(message)
|
||||||
|
first_octet = FirstOctet.new.message_type_indicator!(:sms_submit)
|
||||||
|
|
||||||
|
[
|
||||||
|
# Length of SMSC information
|
||||||
|
# 0 means the SMSC stored in the phone should be used
|
||||||
|
'00',
|
||||||
|
# First octet
|
||||||
|
'%02x' % first_octet.binary,
|
||||||
|
# TP-Message-Reference
|
||||||
|
'00',
|
||||||
|
'%02x' % phone_number.length,
|
||||||
|
'%02x' % phone_number.type_of_address.hex,
|
||||||
|
phone_number.number,
|
||||||
|
# TP-PID: Protocol identifier
|
||||||
|
'00',
|
||||||
|
'%02x' % user_data.encoding.hex,
|
||||||
|
'%02x' % user_data.length,
|
||||||
|
user_data.message
|
||||||
|
].join
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.decode(string)
|
||||||
|
octets = string.scan(/../)
|
||||||
|
|
||||||
|
smsc_length = octets.shift.hex
|
||||||
|
smsc_number = octets.shift(smsc_length)
|
||||||
|
|
||||||
|
first_octet = FirstOctet.new(octets.shift.hex)
|
||||||
|
|
||||||
|
address_length = octets.shift.hex
|
||||||
|
address_type = octets.shift.hex
|
||||||
|
sender_number = PhoneNumber.new(
|
||||||
|
octets.shift(
|
||||||
|
(address_length.odd? ? address_length.succ : address_length) / 2).join,
|
||||||
|
type_of_address: address_type)
|
||||||
|
|
||||||
|
protocol_identifier = octets.shift
|
||||||
|
data_coding_scheme = octets.shift
|
||||||
|
timestamp = Timestamp.new(octets.shift(7).join).to_datetime
|
||||||
|
user_data_length = octets.shift.hex
|
||||||
|
|
||||||
|
user_data = UserData.new(message: octets.join,
|
||||||
|
encoding: data_coding_scheme,
|
||||||
|
length: user_data_length)
|
||||||
|
|
||||||
|
{
|
||||||
|
smsc_length: smsc_length,
|
||||||
|
smsc_number: smsc_number,
|
||||||
|
|
||||||
|
first_octet: first_octet,
|
||||||
|
|
||||||
|
address_length: address_length,
|
||||||
|
address_type: address_type,
|
||||||
|
sender_number: sender_number,
|
||||||
|
|
||||||
|
protocol_identifier: protocol_identifier,
|
||||||
|
data_coding_scheme: data_coding_scheme,
|
||||||
|
timestamp: timestamp,
|
||||||
|
user_data_length: user_data_length,
|
||||||
|
user_data: user_data
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,42 @@
|
||||||
|
module Biju
|
||||||
|
module PDU
|
||||||
|
class DataCodingScheme
|
||||||
|
DATA_CODING_SCHEME = {
|
||||||
|
gsm7bit: 0,
|
||||||
|
gsm8bit: 4,
|
||||||
|
ucs2: 8,
|
||||||
|
reserved: 12,
|
||||||
|
}
|
||||||
|
|
||||||
|
def self.autodetect(message)
|
||||||
|
message.chars.each do |char|
|
||||||
|
return :ucs2 unless Encoding::GSM7Bit::BASIC_7BIT_CHARACTER_SET.include?(char) ||
|
||||||
|
Encoding::GSM7Bit::BASIC_7BIT_CHARACTER_SET_EXTENSION.has_value?(char)
|
||||||
|
end
|
||||||
|
|
||||||
|
:gsm7bit
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(dcs, options = {})
|
||||||
|
unless dcs.is_a?(Symbol)
|
||||||
|
dcs = dcs.hex if dcs.is_a?(String)
|
||||||
|
if dcs & 0b11000000 == 0
|
||||||
|
dcs = DATA_CODING_SCHEME.key(dcs & 0b00001100)
|
||||||
|
else
|
||||||
|
raise Biju::PDU::Errors::DataCodingSchemeNotSupported.new(dcs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@dcs = dcs
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_sym
|
||||||
|
@dcs
|
||||||
|
end
|
||||||
|
|
||||||
|
def hex
|
||||||
|
DATA_CODING_SCHEME[@dcs]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,116 @@
|
||||||
|
# encoding: UTF-8
|
||||||
|
|
||||||
|
module Biju
|
||||||
|
module PDU
|
||||||
|
module Encoding
|
||||||
|
class GSM7Bit
|
||||||
|
BASIC_7BIT_CHARACTER_SET = [
|
||||||
|
'@', '£', '$', '¥', 'è', 'é', 'ù', 'ì', 'ò', 'Ç', "\n", 'Ø', 'ø', "\r", 'Å', 'å',
|
||||||
|
"\u0394", '_', "\u03a6", "\u0393", "\u039b", "\u03a9", "\u03a0","\u03a8", "\u03a3", "\u0398", "\u039e", "\e", 'Æ', 'æ', 'ß', 'É',
|
||||||
|
' ', '!', '"', '#', '¤', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/',
|
||||||
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?',
|
||||||
|
'¡', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
|
||||||
|
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'Ä', 'Ö', 'Ñ', 'Ü', '§',
|
||||||
|
'¿', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
|
||||||
|
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ä', 'ö', 'ñ', 'ü', 'à'
|
||||||
|
]
|
||||||
|
|
||||||
|
BASIC_7BIT_CHARACTER_SET_EXTENSION = {
|
||||||
|
0x0A => "\n",
|
||||||
|
0x0D => '',
|
||||||
|
0x14 => '^',
|
||||||
|
0x1B => '',
|
||||||
|
0x28 => '{',
|
||||||
|
0x29 => '}',
|
||||||
|
0x2F => '\\',
|
||||||
|
0x3C => '[',
|
||||||
|
0x3D => '~',
|
||||||
|
0x3E => ']',
|
||||||
|
0x40 => '|',
|
||||||
|
0x65 => '€',
|
||||||
|
}
|
||||||
|
|
||||||
|
def self.decode(string, options = {})
|
||||||
|
length = options[:length] || 0
|
||||||
|
|
||||||
|
res = ''
|
||||||
|
next_char = 0
|
||||||
|
current_length = 0
|
||||||
|
|
||||||
|
string.scan(/../).map(&:hex).each_with_index do |octet, i|
|
||||||
|
index = i % 7
|
||||||
|
# Only keep the bits for the current character and
|
||||||
|
# add relevant bits from the previous octet
|
||||||
|
# to get the full septet and decode the current character
|
||||||
|
current = ((octet & (2**(7 - index) - 1)) << index) | next_char
|
||||||
|
|
||||||
|
res = add_char(res, current)
|
||||||
|
current_length += 1
|
||||||
|
|
||||||
|
# Break when the number of septet is reached
|
||||||
|
# to prevent to add a last @ when there is 7 septets.
|
||||||
|
# The last octet will have one more septet to ignore.
|
||||||
|
break if length > 0 && current_length >= length
|
||||||
|
|
||||||
|
# Get the relevant bits for the next character
|
||||||
|
next_char = octet >> (7 - index)
|
||||||
|
# When index is 6, next_char contains a full septet
|
||||||
|
if index == 6
|
||||||
|
res = add_char(res, next_char)
|
||||||
|
current_length += 1
|
||||||
|
next_char = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
res
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.encode(string)
|
||||||
|
res = ''
|
||||||
|
length = 0
|
||||||
|
|
||||||
|
string.chars.each do |char|
|
||||||
|
# Look for the current character in basic character set and
|
||||||
|
# extension and concatenate the reversed septets to get
|
||||||
|
# full octets
|
||||||
|
if get_septet(char)
|
||||||
|
res << get_septet(char).reverse
|
||||||
|
length += 1
|
||||||
|
elsif get_septet(char, escape: true)
|
||||||
|
res << get_septet("\e").reverse
|
||||||
|
res << get_septet(char, escape: true).reverse
|
||||||
|
length += 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# Add necessary bits to get a full octet
|
||||||
|
res << ('0' * (8 - (res.length % 8))) unless res.length % 8 == 0
|
||||||
|
|
||||||
|
[
|
||||||
|
# Group by octet, reverse them and print them in hex
|
||||||
|
res.scan(/.{8}/).map { |octet| '%02x' % octet.reverse.to_i(2) }.join,
|
||||||
|
length: length,
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def self.add_char(string, char)
|
||||||
|
if string[-1] == "\e"
|
||||||
|
string.chop << BASIC_7BIT_CHARACTER_SET_EXTENSION[char]
|
||||||
|
else
|
||||||
|
string << BASIC_7BIT_CHARACTER_SET[char]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.get_septet(char, options = {})
|
||||||
|
escape = options[:escape] || false
|
||||||
|
|
||||||
|
char = (!escape ? BASIC_7BIT_CHARACTER_SET.index(char) : BASIC_7BIT_CHARACTER_SET_EXTENSION.key(char))
|
||||||
|
|
||||||
|
return nil unless char
|
||||||
|
'%07b' % char
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,23 @@
|
||||||
|
module Biju
|
||||||
|
module PDU
|
||||||
|
module Encoding
|
||||||
|
class UCS2
|
||||||
|
def self.decode(string, options = {})
|
||||||
|
length = options[:length] || 0
|
||||||
|
|
||||||
|
string.scan(/.{4}/).map { |char| char.hex.chr('UCS-2BE') }.join
|
||||||
|
.encode('UTF-8', 'UCS-2BE')
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.encode(string)
|
||||||
|
[
|
||||||
|
string.encode('UCS-2BE').chars.map do |char|
|
||||||
|
'%04x' % char.ord
|
||||||
|
end.join,
|
||||||
|
length: string.length * 2,
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,38 @@
|
||||||
|
module Biju
|
||||||
|
module PDU
|
||||||
|
module Errors
|
||||||
|
class PDUError < ::StandardError
|
||||||
|
end
|
||||||
|
|
||||||
|
class MalformedSms < PDUError
|
||||||
|
attr_reader :original_exception
|
||||||
|
attr_reader :pdu, :id
|
||||||
|
|
||||||
|
def initialize(pdu, id, original_exception = nil)
|
||||||
|
@id = id
|
||||||
|
@pdu = pdu
|
||||||
|
@original_exception = original_exception
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
"This SMS can not be parsed: #{pdu} (#{original_exception.class}: #{original_exception})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class DataCodingSchemeNotSupported < PDUError
|
||||||
|
attr_reader :data_coding_scheme
|
||||||
|
|
||||||
|
def initialize(dcs = nil)
|
||||||
|
@data_coding_scheme = dcs
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
"This data coding scheme (0b#{data_coding_scheme.to_s(2)}) is not supported"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class EncodingNotSupported < PDUError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,68 @@
|
||||||
|
module Biju
|
||||||
|
module PDU
|
||||||
|
class FirstOctet
|
||||||
|
FIRST_OCTET = {
|
||||||
|
reply_path: 0b10000000,
|
||||||
|
user_data_header: 0b01000000,
|
||||||
|
status_report_request: 0b00100000,
|
||||||
|
validity_period_format: 0b00011000,
|
||||||
|
reject_duplicates: 0b00000100,
|
||||||
|
message_type_indicator: 0b00000011,
|
||||||
|
}
|
||||||
|
|
||||||
|
MESSAGE_TYPE_INDICATOR = {
|
||||||
|
sms_deliver: 0b00000000,
|
||||||
|
sms_submit: 0b00000001,
|
||||||
|
sms_status: 0b00000010,
|
||||||
|
reserved: 0b00000011,
|
||||||
|
}
|
||||||
|
|
||||||
|
VALIDITY_PERIOD_FORMAT = {
|
||||||
|
not_present: 0b00000000,
|
||||||
|
reserved: 0b00001000,
|
||||||
|
relative: 0b00010000,
|
||||||
|
absolute: 0b00011000,
|
||||||
|
}
|
||||||
|
|
||||||
|
attr_accessor :binary
|
||||||
|
|
||||||
|
def initialize(first_octet = 0)
|
||||||
|
self.binary = first_octet
|
||||||
|
end
|
||||||
|
|
||||||
|
def get(field)
|
||||||
|
binary & FIRST_OCTET[field]
|
||||||
|
end
|
||||||
|
|
||||||
|
[:reply_path, :user_data_header, :status_report_request,
|
||||||
|
:reject_duplicates].each do |sym|
|
||||||
|
define_method :"#{sym}?" do
|
||||||
|
get(sym) > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
define_method :"#{sym}!" do |value = true|
|
||||||
|
if value
|
||||||
|
self.binary |= FIRST_OCTET[sym]
|
||||||
|
else
|
||||||
|
self.binary &= (FIRST_OCTET[sym] ^ 0b11111111)
|
||||||
|
end
|
||||||
|
self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
[:message_type_indicator, :validity_period_format].each do |sym|
|
||||||
|
define_method sym do
|
||||||
|
self.class.const_get(sym.upcase).key(get(sym))
|
||||||
|
end
|
||||||
|
|
||||||
|
define_method :"#{sym}!" do |value|
|
||||||
|
hash = self.class.const_get(sym.upcase)
|
||||||
|
|
||||||
|
self.binary = ((binary & (FIRST_OCTET[sym] ^ 0b11111111)) |
|
||||||
|
hash[value]) unless hash[value].nil?
|
||||||
|
self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,33 @@
|
||||||
|
module Biju
|
||||||
|
module PDU
|
||||||
|
class PhoneNumber
|
||||||
|
attr_accessor :type_of_address, :number
|
||||||
|
|
||||||
|
def self.encode(number, options = {})
|
||||||
|
type_of_address = options[:type_of_address] || :international
|
||||||
|
|
||||||
|
number = number + 'F' if number.length.odd?
|
||||||
|
new(
|
||||||
|
number.scan(/../).map(&:reverse).join,
|
||||||
|
type_of_address: type_of_address
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(number, options = {})
|
||||||
|
type_of_address = options[:type_of_address] || :international
|
||||||
|
|
||||||
|
self.number = number
|
||||||
|
self.type_of_address = TypeOfAddress.new(type_of_address)
|
||||||
|
end
|
||||||
|
|
||||||
|
def decode
|
||||||
|
number.scan(/../).map(&:reverse).join.chomp('F')
|
||||||
|
end
|
||||||
|
|
||||||
|
def length
|
||||||
|
# If the last character is 0xF, remove this one from length
|
||||||
|
number.length - (number[-2].hex == 15 ? 1 : 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,34 @@
|
||||||
|
require 'date'
|
||||||
|
|
||||||
|
module Biju
|
||||||
|
module PDU
|
||||||
|
class Timestamp
|
||||||
|
attr_reader :timestamp
|
||||||
|
|
||||||
|
def initialize(timestamp)
|
||||||
|
@timestamp = timestamp
|
||||||
|
end
|
||||||
|
|
||||||
|
def timezone
|
||||||
|
# The last 2 digits of the timestamp are for the timezone
|
||||||
|
timezone = timestamp[-2, 2].reverse.hex
|
||||||
|
|
||||||
|
# The MSB define the plus-minus sign. 0 for +, 1 for -
|
||||||
|
sign = (timezone >> 7 == 0 ? '+' : '-')
|
||||||
|
|
||||||
|
# The following 3 bits represent tens digit
|
||||||
|
# and the last 4 bits are for the units digit
|
||||||
|
tens_digit = ((timezone & 0b01110000) >> 4)
|
||||||
|
units_digit = (timezone & 0b00001111)
|
||||||
|
|
||||||
|
# Timezone is in quarters of an hour
|
||||||
|
sign << '%02d' % ((tens_digit * 10 + units_digit) / 4)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_datetime
|
||||||
|
DateTime.strptime(
|
||||||
|
"#{timestamp[0..-3].reverse}#{timezone}", '%S%M%H%d%m%y%Z')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,30 @@
|
||||||
|
module Biju
|
||||||
|
module PDU
|
||||||
|
class TypeOfAddress
|
||||||
|
TYPE_OF_ADDRESS = {
|
||||||
|
unknown: 0b10000001,
|
||||||
|
international: 0b10010001,
|
||||||
|
national: 0b10100001,
|
||||||
|
reserved: 0b11110001,
|
||||||
|
}
|
||||||
|
|
||||||
|
def initialize(type_of_address, options = {})
|
||||||
|
type_of_address = :international if type_of_address.nil?
|
||||||
|
|
||||||
|
unless type_of_address.is_a?(Symbol)
|
||||||
|
type_of_address = type_of_address.hex if type_of_address.is_a?(String)
|
||||||
|
type_of_address = TYPE_OF_ADDRESS.key(type_of_address)
|
||||||
|
end
|
||||||
|
@type_of_address = type_of_address
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_sym
|
||||||
|
@type_of_address
|
||||||
|
end
|
||||||
|
|
||||||
|
def hex
|
||||||
|
TYPE_OF_ADDRESS[@type_of_address]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,34 @@
|
||||||
|
module Biju
|
||||||
|
module PDU
|
||||||
|
class UserData
|
||||||
|
ENCODING = {
|
||||||
|
gsm7bit: Encoding::GSM7Bit,
|
||||||
|
ucs2: Encoding::UCS2,
|
||||||
|
}
|
||||||
|
|
||||||
|
attr_accessor :encoding, :message, :length, :user_data_header
|
||||||
|
|
||||||
|
def self.encode(message, options = {})
|
||||||
|
encoding = options[:encoding] || DataCodingScheme.autodetect(message)
|
||||||
|
|
||||||
|
raise ArgumentError, 'Unknown encoding' unless ENCODING.has_key?(encoding)
|
||||||
|
res = ENCODING[encoding].encode(message)
|
||||||
|
|
||||||
|
new(message: res[0], length: res[1][:length], encoding: encoding)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(options = {})
|
||||||
|
self.encoding = DataCodingScheme.new(options[:encoding] || :gsm7bit)
|
||||||
|
self.message = options[:message] || ''
|
||||||
|
|
||||||
|
self.length = options[:length] || 0
|
||||||
|
|
||||||
|
self.user_data_header = options[:user_data_header] || false
|
||||||
|
end
|
||||||
|
|
||||||
|
def decode
|
||||||
|
ENCODING[encoding.to_sym].decode(message, length: length)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,19 +1,30 @@
|
||||||
|
require 'date'
|
||||||
|
|
||||||
module Biju
|
module Biju
|
||||||
class Sms
|
class Sms
|
||||||
attr_accessor :id, :phone_number, :datetime, :message
|
attr_reader :id, :phone_number, :type_of_address, :message, :datetime
|
||||||
|
|
||||||
|
def self.from_pdu(string, id = nil)
|
||||||
|
sms_infos = PDU.decode(string)
|
||||||
|
new(id: id,
|
||||||
|
phone_number: sms_infos[:sender_number].decode,
|
||||||
|
type_of_address: sms_infos[:sender_number].type_of_address.to_sym,
|
||||||
|
datetime: sms_infos[:timestamp],
|
||||||
|
message: sms_infos[:user_data].decode)
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(params = {})
|
def initialize(params = {})
|
||||||
params.each do |attr, value|
|
params.each do |attr, value|
|
||||||
self.public_send("#{attr}=", value)
|
instance_variable_set(:"@#{attr}", value)
|
||||||
end if params
|
end if params
|
||||||
end
|
end
|
||||||
|
|
||||||
def datetime
|
def to_s
|
||||||
@datetime.sub(/(\d+)\D+(\d+)\D+(\d+),(\d*\D)(\d*\D)(\d+)(.*)/, '20\1-\2-\3 \4\5\6')
|
"[#{id}] (#{phone_number}) #{datetime} '#{message}'"
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_s
|
def to_pdu
|
||||||
"#{id} - #{phone_number} - #{datetime} - #{message}"
|
Biju::PDU.encode(phone_number, message, type_of_address: type_of_address)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
class Object
|
||||||
|
def to_hayes; "\"#{to_s}\""; end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Fixnum
|
||||||
|
def to_hayes; to_s; end
|
||||||
|
end
|
||||||
|
|
||||||
|
class TrueClass
|
||||||
|
def to_hayes; '1'; end
|
||||||
|
end
|
||||||
|
|
||||||
|
class FalseClass
|
||||||
|
def to_hayes; '0'; end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Array
|
||||||
|
def to_hayes; map(&:to_hayes).join(','); end
|
||||||
|
end
|
|
@ -1,3 +1,3 @@
|
||||||
module Biju
|
module Biju
|
||||||
VERSION = "0.0.2"
|
VERSION = '0.0.2'
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
require_relative '../spec_helper'
|
|
||||||
|
|
||||||
# TODO: Fix missing tests SOON
|
|
||||||
describe Biju::Modem do
|
|
||||||
describe ".new" do
|
|
||||||
it "should raise an Exception without port option" do
|
|
||||||
lambda { Biju::Modem.new }.must_raise Exception
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'biju/parser'
|
||||||
|
|
||||||
|
describe Biju::ATParser do
|
||||||
|
context "status" do
|
||||||
|
it "returns ok status" do
|
||||||
|
result = Biju::ATTransform.new.apply(
|
||||||
|
Biju::ATParser.new.parse("AT\r\r\nOK\r\n"))
|
||||||
|
expect(result).to include(status: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns error status" do
|
||||||
|
result = Biju::ATTransform.new.apply(
|
||||||
|
Biju::ATParser.new.parse("AT\r\r\nERROR\r\n"))
|
||||||
|
expect(result).to include(status: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "errors" do
|
||||||
|
it "parses CMS ERROR" do
|
||||||
|
result = Biju::ATTransform.new.apply(
|
||||||
|
Biju::ATParser.new.parse("AT\r\r\n+CMS ERROR: 500\r\n"))
|
||||||
|
expect(result[:cmd]).to eq('+CMS ERROR')
|
||||||
|
expect(result[:result]).to eq(500)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "parses CME ERROR" do
|
||||||
|
result = Biju::ATTransform.new.apply(
|
||||||
|
Biju::ATParser.new.parse("AT\r\r\n+CME ERROR: 100\r\n"))
|
||||||
|
expect(result[:cmd]).to eq('+CME ERROR')
|
||||||
|
expect(result[:result]).to eq(100)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "response" do
|
||||||
|
it "parses generic response" do
|
||||||
|
resp = "AT+COPS\r\r\n+COPS: 1,\"two\",(3,4)\r\n\r\nOK\r\n"
|
||||||
|
|
||||||
|
result = Biju::ATTransform.new.apply(
|
||||||
|
Biju::ATParser.new.parse(resp))
|
||||||
|
|
||||||
|
expect(result[:cmd]).to eq('+COPS')
|
||||||
|
expect(result[:array]).to have(3).fields
|
||||||
|
|
||||||
|
expect(result[:array]).to include(1)
|
||||||
|
expect(result[:array]).to include('two')
|
||||||
|
expect(result[:array]).to include([3,4])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "parses cmgs prompt" do
|
||||||
|
mgs = "AT+CMGS=18\r\r\n> "
|
||||||
|
|
||||||
|
result = Biju::ATTransform.new.apply(
|
||||||
|
Biju::ATParser.new.parse(mgs))
|
||||||
|
|
||||||
|
expect(result).to include(prompt: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "parses messages list" do
|
||||||
|
messages = "AT+CMGL=1\r\r\n" <<
|
||||||
|
"+CMGL: 0,1,,23\r\n" <<
|
||||||
|
"07913396050066F3040B91336789\r\n" <<
|
||||||
|
"+CMGL: 3,1,,74\r\n" <<
|
||||||
|
"BD60B917ACC68AC17431982E066BC5642205F3C95400\r\n" <<
|
||||||
|
"+CMGL: 4,1,,20\r\n" <<
|
||||||
|
"07913396050066F3040B913364446864\r\n" <<
|
||||||
|
"\r\n" <<
|
||||||
|
"OK\r\n"
|
||||||
|
result = Biju::ATTransform.new.apply(
|
||||||
|
Biju::ATParser.new.parse(messages))
|
||||||
|
|
||||||
|
expect(result).to include(status: true)
|
||||||
|
expect(result[:sms]).to have(3).messages
|
||||||
|
expect(result[:sms][0][:message]).to eq('07913396050066F3040B91336789')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "gets phone numbers" do
|
||||||
|
pms = "AT+CNUM\r\r\n+CNUM: \"M\",\"+33666666666\",145\r\n\r\n\r\nOK\r\n"
|
||||||
|
|
||||||
|
result = Biju::ATTransform.new.apply(
|
||||||
|
Biju::ATParser.new.parse(pms))
|
||||||
|
|
||||||
|
expect(result[:phone_numbers][0][:cmd]).to eq('+CNUM')
|
||||||
|
expect(result[:phone_numbers]).to have(1).phone_number
|
||||||
|
expect(result[:phone_numbers][0][:array][1]).to eq('+33666666666')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "gets messages storage" do
|
||||||
|
pms = "AT+CPMS=?\r\r\n+CPMS: ((\"SM\",\"BM\",\"SR\"),(\"SM\"))\r\n\r\nOK\r\n"
|
||||||
|
|
||||||
|
result = Biju::ATTransform.new.apply(
|
||||||
|
Biju::ATParser.new.parse(pms))
|
||||||
|
|
||||||
|
expect(result[:cmd]).to eq('+CPMS')
|
||||||
|
expect(result[:array]).to have(2).storage
|
||||||
|
expect(result[:array][0]).to have(3).storage
|
||||||
|
end
|
||||||
|
|
||||||
|
it "gets specified message storage infos" do
|
||||||
|
pms = "AT+CPMS=\"MT\"\r\r\n+CPMS: 23,23,7,100,7,100\r\n\r\nOK\r\n"
|
||||||
|
|
||||||
|
result = Biju::ATTransform.new.apply(
|
||||||
|
Biju::ATParser.new.parse(pms))
|
||||||
|
|
||||||
|
expect(result).to include(status: true)
|
||||||
|
expect(result[:array]).to have(6).storage
|
||||||
|
expect(result[:array]).to eq([23, 23, 7, 100, 7, 100])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "gets pin status" do
|
||||||
|
pin = "AT+CPIN?\r\r\n+CPIN: READY\r\n\r\nOK\r\n"
|
||||||
|
|
||||||
|
result = Biju::ATTransform.new.apply(
|
||||||
|
Biju::ATParser.new.parse(pin))
|
||||||
|
|
||||||
|
expect(result).to include(status: true)
|
||||||
|
expect(result[:result]).to eq("READY")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "parses +CMGF? response" do
|
||||||
|
mgf = "AT+CMGF?\r\r\n+CMGF: 0\r\n\r\nOK\r\n"
|
||||||
|
|
||||||
|
result = Biju::ATTransform.new.apply(
|
||||||
|
Biju::ATParser.new.parse(mgf))
|
||||||
|
|
||||||
|
expect(result).to include(status: true)
|
||||||
|
expect(result[:result]).to be_false
|
||||||
|
end
|
||||||
|
|
||||||
|
it "parses message sent response" do
|
||||||
|
mgs = "+CMGS: 163\r\n\r\nOK\r\n"
|
||||||
|
|
||||||
|
result = Biju::ATTransform.new.apply(
|
||||||
|
Biju::ATParser.new.parse(mgs))
|
||||||
|
|
||||||
|
expect(result).to include(status: true)
|
||||||
|
expect(result[:result]).to eq(163)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises ParseFailed exception" do
|
||||||
|
expect { Biju::ATParser.new.parse('Ha') }.to raise_error(Parslet::ParseFailed)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,30 @@
|
||||||
|
# encoding: UTF-8
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'biju/pdu'
|
||||||
|
|
||||||
|
describe Biju::PDU::DataCodingScheme do
|
||||||
|
describe '::autodetect' do
|
||||||
|
it "autodetects gsm7bit encoding" do
|
||||||
|
[
|
||||||
|
"Test",
|
||||||
|
"Ç$",
|
||||||
|
"[teßt}",
|
||||||
|
].each do |string|
|
||||||
|
expect(Biju::PDU::DataCodingScheme.autodetect(string)).to eq(:gsm7bit)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "autodetects ucs2 encoding" do
|
||||||
|
[
|
||||||
|
"ç",
|
||||||
|
"âmazing",
|
||||||
|
].each do |string|
|
||||||
|
expect(Biju::PDU::DataCodingScheme.autodetect(string)).to eq(:ucs2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { Biju::PDU::DataCodingScheme.new(:gsm7bit) }
|
||||||
|
its(:to_sym) { should eq(:gsm7bit) }
|
||||||
|
its(:hex) { should eq(0) }
|
||||||
|
end
|
|
@ -0,0 +1,47 @@
|
||||||
|
# encoding: UTF-8
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'biju/pdu/encoding/gsm7bit'
|
||||||
|
|
||||||
|
describe Biju::PDU::Encoding::GSM7Bit do
|
||||||
|
describe '::decode' do
|
||||||
|
it "decodes string" do
|
||||||
|
expect(Biju::PDU::Encoding::GSM7Bit.decode('D4F29C0E', length: 4)).to eq('Test')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "decodes character from extension set" do
|
||||||
|
expect(Biju::PDU::Encoding::GSM7Bit.decode('9B32', length: 2)).to eq('€')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "decodes character with a length of 7" do
|
||||||
|
expect(Biju::PDU::Encoding::GSM7Bit.decode('E170381C0E8701', length: 7)).to eq('a' * 7)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '::encode' do
|
||||||
|
it "encodes string" do
|
||||||
|
expect(Biju::PDU::Encoding::GSM7Bit.encode('Test').first.upcase).to eq('D4F29C0E')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "encodes character from extension set" do
|
||||||
|
expect(Biju::PDU::Encoding::GSM7Bit.encode('€').first.upcase).to eq('9B32')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "encodes character with a length of 7" do
|
||||||
|
expect(Biju::PDU::Encoding::GSM7Bit.encode('a' * 7).first.upcase).to eq('E170381C0E8701')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "gives same text after encoding and decoding" do
|
||||||
|
strings = [
|
||||||
|
'My first TEST',
|
||||||
|
'{More complicated]',
|
||||||
|
'And on€ More~',
|
||||||
|
'a' * 7,
|
||||||
|
]
|
||||||
|
|
||||||
|
strings.each do |string|
|
||||||
|
expect(Biju::PDU::Encoding::GSM7Bit.decode(
|
||||||
|
*Biju::PDU::Encoding::GSM7Bit.encode(string))).to eq(string)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,31 @@
|
||||||
|
# encoding: UTF-8
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'biju/pdu/encoding/ucs2'
|
||||||
|
|
||||||
|
describe Biju::PDU::Encoding::UCS2 do
|
||||||
|
describe '::decode' do
|
||||||
|
it "decodes string" do
|
||||||
|
expect(Biju::PDU::Encoding::UCS2.decode('00C700E700E200E300E500E4016B00F80153', length: 4)).to eq('Ççâãåäūøœ')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '::encode' do
|
||||||
|
it "encodes string" do
|
||||||
|
expect(Biju::PDU::Encoding::UCS2.encode('Ççâãåäūøœ').first.upcase).to eq('00C700E700E200E300E500E4016B00F80153')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "gives same text after encoding and decoding" do
|
||||||
|
strings = [
|
||||||
|
'My first TEST',
|
||||||
|
'{More çomplicated]',
|
||||||
|
'And on€ More~',
|
||||||
|
'þß®',
|
||||||
|
]
|
||||||
|
|
||||||
|
strings.each do |string|
|
||||||
|
expect(Biju::PDU::Encoding::UCS2.decode(
|
||||||
|
*Biju::PDU::Encoding::UCS2.encode(string))).to eq(string)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,12 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'biju/pdu'
|
||||||
|
|
||||||
|
describe Biju::PDU::FirstOctet do
|
||||||
|
its(:reply_path?) { should be_false }
|
||||||
|
its(:user_data_header?) { should be_false }
|
||||||
|
its(:status_report_request?) { should be_false }
|
||||||
|
its(:reject_duplicates?) { should be_false }
|
||||||
|
|
||||||
|
its(:message_type_indicator) { should eq(:sms_deliver) }
|
||||||
|
its(:validity_period_format) { should eq(:not_present) }
|
||||||
|
end
|
|
@ -0,0 +1,21 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'biju/pdu'
|
||||||
|
|
||||||
|
describe Biju::PDU::PhoneNumber do
|
||||||
|
describe '::encode' do
|
||||||
|
subject { Biju::PDU::PhoneNumber.encode('33123456789') }
|
||||||
|
its(:number) { should eq('3321436587F9') }
|
||||||
|
end
|
||||||
|
|
||||||
|
context "odd length" do
|
||||||
|
subject { Biju::PDU::PhoneNumber.new('3321436587F9') }
|
||||||
|
its(:decode) { should eq('33123456789') }
|
||||||
|
its(:length) { should eq(11) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context "even length" do
|
||||||
|
subject { Biju::PDU::PhoneNumber.new('3321436587') }
|
||||||
|
its(:decode) { should eq('3312345678') }
|
||||||
|
its(:length) { should eq(10) }
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'biju/pdu'
|
||||||
|
|
||||||
|
describe Biju::PDU::Timestamp do
|
||||||
|
subject { Biju::PDU::Timestamp.new('31900141039580') }
|
||||||
|
|
||||||
|
its(:timezone) { should eq('+02') }
|
||||||
|
its(:to_datetime) { should eq(DateTime.new(2013, 9, 10, 14, 30, 59, '+02')) }
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'biju/pdu'
|
||||||
|
|
||||||
|
describe Biju::PDU::TypeOfAddress do
|
||||||
|
subject { Biju::PDU::TypeOfAddress.new(:international) }
|
||||||
|
|
||||||
|
its(:to_sym) { should eq(:international) }
|
||||||
|
its(:hex) { should eq(145) }
|
||||||
|
end
|
|
@ -0,0 +1,12 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'biju/pdu'
|
||||||
|
|
||||||
|
describe Biju::PDU::UserData do
|
||||||
|
subject(:message) { 'Test' }
|
||||||
|
subject(:encoded) { Biju::PDU::Encoding::GSM7Bit.encode(message) }
|
||||||
|
subject { Biju::PDU::UserData.encode(message, encoding: :gsm7bit) }
|
||||||
|
|
||||||
|
its(:message) { should eq(encoded[0]) }
|
||||||
|
its(:length) { should eq(encoded[1][:length]) }
|
||||||
|
its(:decode) { should eq(message) }
|
||||||
|
end
|
|
@ -1,15 +1,40 @@
|
||||||
require_relative '../spec_helper'
|
require 'spec_helper'
|
||||||
|
require 'biju'
|
||||||
|
|
||||||
describe Biju::Sms do
|
describe Biju::Sms do
|
||||||
subject { Biju::Sms.new(:id => "1", :phone_number => "144", :datetime => "11/07/28,15:34:08-12", :message => "Some text here")}
|
subject do
|
||||||
|
Biju::Sms.new(
|
||||||
it { subject.id.must_equal "1" }
|
id: 1,
|
||||||
|
phone_number: "144",
|
||||||
it { subject.phone_number.must_equal "144" }
|
datetime: DateTime.new(2011, 7, 28, 15, 34, 8, '-12'),
|
||||||
|
message: "Some text here")
|
||||||
it { subject.datetime.must_equal "2011-07-28 15:34:08" }
|
end
|
||||||
|
|
||||||
it { subject.message.must_equal "Some text here" }
|
its(:id) { should eq(1) }
|
||||||
|
its(:phone_number) { should eq("144") }
|
||||||
it { subject.to_s.must_equal "1 - 144 - 2011-07-28 15:34:08 - Some text here"}
|
its(:datetime) { should eq(DateTime.new(2011, 7, 28, 15, 34, 8, '-12')) }
|
||||||
|
its(:message) { should eq("Some text here") }
|
||||||
|
|
||||||
|
describe '::from_pdu' do
|
||||||
|
subject do
|
||||||
|
Biju::Sms.from_pdu(
|
||||||
|
'07913396050066F3040B913366666666F600003190509095928004D4F29C0E')
|
||||||
|
end
|
||||||
|
|
||||||
|
its(:datetime) { should eq(DateTime.new(2013, 9, 5, 9, 59, 29, '+02')) }
|
||||||
|
its(:message) { should eq('Test') }
|
||||||
|
its(:phone_number) { should eq('33666666666') }
|
||||||
|
its(:type_of_address) { should eq(:international) }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#to_pdu' do
|
||||||
|
subject do
|
||||||
|
Biju::Sms.new(
|
||||||
|
phone_number: '33666666666',
|
||||||
|
type_of_address: :international,
|
||||||
|
message: 'Test').to_pdu.upcase
|
||||||
|
end
|
||||||
|
|
||||||
|
it { should eq('0001000B913366666666F6000004D4F29C0E') }
|
||||||
|
end
|
||||||
end
|
end
|
|
@ -0,0 +1,10 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'biju/to_hayes'
|
||||||
|
|
||||||
|
describe "blah blah" do
|
||||||
|
it { expect(5.to_hayes).to eq('5') }
|
||||||
|
it { expect(true.to_hayes).to eq('1') }
|
||||||
|
it { expect(false.to_hayes).to eq('0') }
|
||||||
|
it { expect("test".to_hayes).to eq('"test"') }
|
||||||
|
it { expect([1, 2].to_hayes).to eq('1,2') }
|
||||||
|
end
|
|
@ -1,2 +1,17 @@
|
||||||
require 'minitest/autorun'
|
# This file was generated by the `rspec --init` command. Conventionally, all
|
||||||
require "./lib/biju"
|
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
||||||
|
# Require this file using `require "spec_helper"` to ensure that it is only
|
||||||
|
# loaded once.
|
||||||
|
#
|
||||||
|
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
||||||
|
RSpec.configure do |config|
|
||||||
|
config.treat_symbols_as_metadata_keys_with_true_values = true
|
||||||
|
config.run_all_when_everything_filtered = true
|
||||||
|
config.filter_run :focus
|
||||||
|
|
||||||
|
# Run specs in random order to surface order dependencies. If you find an
|
||||||
|
# order dependency and want to debug it, you can fix the order by providing
|
||||||
|
# the seed, which is printed after each run.
|
||||||
|
# --seed 1234
|
||||||
|
config.order = 'random'
|
||||||
|
end
|
||||||
|
|
Loading…
Reference in New Issue