Compare commits
No commits in common. "develop" and "master" have entirely different histories.
|
@ -16,4 +16,3 @@ 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
|
||||||
|
|
||||||
Biju is an easy way to mount a GSM modem to send, to receive and to delete messages through a ruby interface.
|
[WIP] 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,28 +20,24 @@ Or install it yourself as:
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
modem = Biju::Hayes.new('/dev/tty.HUAWEIMobile-Modem', pin: '0000')
|
@modem = Biju::Modem.new(:port => "/dev/tty.HUAWEIMobile-Modem")
|
||||||
|
|
||||||
# method to list all messages
|
# method to list all messages
|
||||||
# it can take the status in argument
|
@modem.messages.each do |sms|
|
||||||
# :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
|
||||||
|
|
||||||
|
@ -50,34 +46,3 @@ modem.close
|
||||||
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 +1,9 @@
|
||||||
|
#!/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,8 +15,7 @@ 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 "rspec", "~> 2.14.0"
|
gem.add_development_dependency "minitest", "3.0.0"
|
||||||
|
|
||||||
gem.add_dependency "serialport", "~> 1.1.0"
|
gem.add_dependency "serialport", "1.0.4"
|
||||||
gem.add_dependency "parslet", "~> 1.5.0"
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
require 'biju/version'
|
require 'biju/version'
|
||||||
require 'biju/modem'
|
require "biju/modem"
|
||||||
require 'biju/pdu'
|
require "biju/sms"
|
||||||
require 'biju/to_hayes'
|
|
||||||
require 'biju/hayes'
|
|
||||||
require 'biju/sms'
|
|
||||||
require 'biju/parser'
|
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
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
|
|
|
@ -1,99 +0,0 @@
|
||||||
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
|
|
|
@ -1,17 +0,0 @@
|
||||||
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
|
|
|
@ -1,159 +0,0 @@
|
||||||
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,42 +1,86 @@
|
||||||
require 'serialport'
|
require 'serialport'
|
||||||
require 'forwardable'
|
require_relative 'sms'
|
||||||
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('/dev/ttyUSB0')
|
# Biju::Modem.new(:port => '/dev/ttyUSB0')
|
||||||
#
|
#
|
||||||
def initialize(port, options = {})
|
def initialize(options={}, &block)
|
||||||
@connection = SerialPort.new(port, DEFAULT_OPTIONS.merge!(options))
|
raise Exception.new("Port is required") unless options[:port]
|
||||||
|
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
|
||||||
|
|
||||||
def_delegators :connection, :close, :write
|
# Close the serial connection.
|
||||||
|
def close
|
||||||
def flush
|
@connection.close
|
||||||
wait(length: 0, timeout: 0)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait(options = {})
|
# Return an Array of Sms if there is messages nad return nil if not.
|
||||||
length = options[:length] || 0
|
def messages(which = "ALL")
|
||||||
timeout = options[:timeout] || 10
|
# read message from all storage in the mobile phone (sim+mem)
|
||||||
|
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 = ''
|
||||||
Timeout.timeout(timeout) do
|
while IO.select([@connection], [], [], 0.25)
|
||||||
while IO.select([connection], [], [], 0.25) || buffer.length < length
|
chr = @connection.getc.chr;
|
||||||
buffer << connection.getc.chr
|
buffer += chr
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
buffer
|
buffer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,109 +0,0 @@
|
||||||
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
|
|
|
@ -1,84 +0,0 @@
|
||||||
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
|
|
|
@ -1,42 +0,0 @@
|
||||||
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
|
|
|
@ -1,116 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,23 +0,0 @@
|
||||||
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
|
|
|
@ -1,38 +0,0 @@
|
||||||
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
|
|
|
@ -1,68 +0,0 @@
|
||||||
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
|
|
|
@ -1,33 +0,0 @@
|
||||||
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
|
|
|
@ -1,34 +0,0 @@
|
||||||
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
|
|
|
@ -1,30 +0,0 @@
|
||||||
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
|
|
|
@ -1,34 +0,0 @@
|
||||||
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,30 +1,19 @@
|
||||||
require 'date'
|
|
||||||
|
|
||||||
module Biju
|
module Biju
|
||||||
class Sms
|
class Sms
|
||||||
attr_reader :id, :phone_number, :type_of_address, :message, :datetime
|
attr_accessor :id, :phone_number, :datetime, :message
|
||||||
|
|
||||||
def self.from_pdu(string, id = nil)
|
def initialize(params={})
|
||||||
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 = {})
|
|
||||||
params.each do |attr, value|
|
params.each do |attr, value|
|
||||||
instance_variable_set(:"@#{attr}", value)
|
self.public_send("#{attr}=", value)
|
||||||
end if params
|
end if params
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_s
|
def datetime
|
||||||
"[#{id}] (#{phone_number}) #{datetime} '#{message}'"
|
@datetime.sub(/(\d+)\D+(\d+)\D+(\d+),(\d*\D)(\d*\D)(\d+)(.*)/, '20\1-\2-\3 \4\5\6')
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_pdu
|
def to_s
|
||||||
Biju::PDU.encode(phone_number, message, type_of_address: type_of_address)
|
"#{id} - #{phone_number} - #{datetime} - #{message}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
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
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
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
|
|
@ -1,144 +0,0 @@
|
||||||
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
|
|
|
@ -1,30 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,47 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,31 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,12 +0,0 @@
|
||||||
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
|
|
|
@ -1,21 +0,0 @@
|
||||||
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
|
|
|
@ -1,9 +0,0 @@
|
||||||
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
|
|
|
@ -1,9 +0,0 @@
|
||||||
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
|
|
|
@ -1,12 +0,0 @@
|
||||||
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,40 +1,15 @@
|
||||||
require 'spec_helper'
|
require_relative '../spec_helper'
|
||||||
require 'biju'
|
|
||||||
|
|
||||||
describe Biju::Sms do
|
describe Biju::Sms do
|
||||||
subject do
|
subject { Biju::Sms.new(:id => "1", :phone_number => "144", :datetime => "11/07/28,15:34:08-12", :message => "Some text here")}
|
||||||
Biju::Sms.new(
|
|
||||||
id: 1,
|
|
||||||
phone_number: "144",
|
|
||||||
datetime: DateTime.new(2011, 7, 28, 15, 34, 8, '-12'),
|
|
||||||
message: "Some text here")
|
|
||||||
end
|
|
||||||
|
|
||||||
its(:id) { should eq(1) }
|
it { subject.id.must_equal "1" }
|
||||||
its(:phone_number) { should eq("144") }
|
|
||||||
its(:datetime) { should eq(DateTime.new(2011, 7, 28, 15, 34, 8, '-12')) }
|
|
||||||
its(:message) { should eq("Some text here") }
|
|
||||||
|
|
||||||
describe '::from_pdu' do
|
it { subject.phone_number.must_equal "144" }
|
||||||
subject do
|
|
||||||
Biju::Sms.from_pdu(
|
|
||||||
'07913396050066F3040B913366666666F600003190509095928004D4F29C0E')
|
|
||||||
end
|
|
||||||
|
|
||||||
its(:datetime) { should eq(DateTime.new(2013, 9, 5, 9, 59, 29, '+02')) }
|
it { subject.datetime.must_equal "2011-07-28 15:34:08" }
|
||||||
its(:message) { should eq('Test') }
|
|
||||||
its(:phone_number) { should eq('33666666666') }
|
|
||||||
its(:type_of_address) { should eq(:international) }
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#to_pdu' do
|
it { subject.message.must_equal "Some text here" }
|
||||||
subject do
|
|
||||||
Biju::Sms.new(
|
|
||||||
phone_number: '33666666666',
|
|
||||||
type_of_address: :international,
|
|
||||||
message: 'Test').to_pdu.upcase
|
|
||||||
end
|
|
||||||
|
|
||||||
it { should eq('0001000B913366666666F6000004D4F29C0E') }
|
it { subject.to_s.must_equal "1 - 144 - 2011-07-28 15:34:08 - Some text here"}
|
||||||
end
|
end
|
||||||
end
|
|
|
@ -1,10 +0,0 @@
|
||||||
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,17 +1,2 @@
|
||||||
# This file was generated by the `rspec --init` command. Conventionally, all
|
require 'minitest/autorun'
|
||||||
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
require "./lib/biju"
|
||||||
# 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