Compare commits
No commits in common. "develop" and "master" have entirely different histories.
|
@ -16,4 +16,3 @@ spec/reports
|
|||
test/tmp
|
||||
test/version_tmp
|
||||
tmp
|
||||
*.s[a-w][a-z]
|
||||
|
|
47
README.md
47
README.md
|
@ -1,6 +1,6 @@
|
|||
# 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).
|
||||
|
||||
## Installation
|
||||
|
@ -20,28 +20,24 @@ Or install it yourself as:
|
|||
## 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
|
||||
# it can take the status in argument
|
||||
# :unread, :read, :unsent, :sent, :all
|
||||
modem.messages.each do |sms|
|
||||
@modem.messages.each do |sms|
|
||||
puts sms
|
||||
end
|
||||
|
||||
# method to send sms
|
||||
sms = Biju::Sms.new(phone_number: '+3312345678', message: 'hello world')
|
||||
modem.send(sms)
|
||||
sms = Biju::Sms.new(:phone_number => '+3312345678', :message => 'hello world')
|
||||
@modem.send(sms)
|
||||
|
||||
modem.close
|
||||
@modem.close
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
1. Write missing test for modem module.
|
||||
2. Write a documentation.
|
||||
3. Test with different kinds of modem and OS.
|
||||
4. Handle UDH (User Data Header) and SMS longer than 140 octets
|
||||
|
||||
## Contributing
|
||||
|
||||
|
@ -50,34 +46,3 @@ modem.close
|
|||
3. Commit your changes (`git commit -am 'Added some feature'`)
|
||||
4. Push to the branch (`git push origin my-new-feature`)
|
||||
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 '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.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 "parslet", "~> 1.5.0"
|
||||
gem.add_dependency "serialport", "1.0.4"
|
||||
end
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
require 'biju/version'
|
||||
require 'biju/modem'
|
||||
require 'biju/pdu'
|
||||
require 'biju/to_hayes'
|
||||
require 'biju/hayes'
|
||||
require 'biju/sms'
|
||||
require 'biju/parser'
|
||||
require "biju/modem"
|
||||
require "biju/sms"
|
||||
|
|
|
@ -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 'forwardable'
|
||||
require 'timeout'
|
||||
require_relative 'sms'
|
||||
|
||||
module Biju
|
||||
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.
|
||||
# @option options [String] :port The modem port to connect
|
||||
#
|
||||
# Biju::Modem.new('/dev/ttyUSB0')
|
||||
# Biju::Modem.new(:port => '/dev/ttyUSB0')
|
||||
#
|
||||
def initialize(port, options = {})
|
||||
@connection = SerialPort.new(port, DEFAULT_OPTIONS.merge!(options))
|
||||
def initialize(options={}, &block)
|
||||
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
|
||||
|
||||
def_delegators :connection, :close, :write
|
||||
|
||||
def flush
|
||||
wait(length: 0, timeout: 0)
|
||||
# Close the serial connection.
|
||||
def close
|
||||
@connection.close
|
||||
end
|
||||
|
||||
def wait(options = {})
|
||||
length = options[:length] || 0
|
||||
timeout = options[:timeout] || 10
|
||||
# Return an Array of Sms if there is messages nad return nil if not.
|
||||
def messages(which = "ALL")
|
||||
# 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 = ''
|
||||
Timeout.timeout(timeout) do
|
||||
while IO.select([connection], [], [], 0.25) || buffer.length < length
|
||||
buffer << connection.getc.chr
|
||||
while IO.select([@connection], [], [], 0.25)
|
||||
chr = @connection.getc.chr;
|
||||
buffer += chr
|
||||
end
|
||||
end
|
||||
|
||||
buffer
|
||||
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
|
||||
class Sms
|
||||
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
|
||||
attr_accessor :id, :phone_number, :datetime, :message
|
||||
|
||||
def initialize(params={})
|
||||
params.each do |attr, value|
|
||||
instance_variable_set(:"@#{attr}", value)
|
||||
self.public_send("#{attr}=", value)
|
||||
end if params
|
||||
end
|
||||
|
||||
def to_s
|
||||
"[#{id}] (#{phone_number}) #{datetime} '#{message}'"
|
||||
def datetime
|
||||
@datetime.sub(/(\d+)\D+(\d+)\D+(\d+),(\d*\D)(\d*\D)(\d+)(.*)/, '20\1-\2-\3 \4\5\6')
|
||||
end
|
||||
|
||||
def to_pdu
|
||||
Biju::PDU.encode(phone_number, message, type_of_address: type_of_address)
|
||||
def to_s
|
||||
"#{id} - #{phone_number} - #{datetime} - #{message}"
|
||||
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
|
||||
VERSION = '0.0.2'
|
||||
VERSION = "0.0.2"
|
||||
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 'biju'
|
||||
require_relative '../spec_helper'
|
||||
|
||||
describe Biju::Sms do
|
||||
subject do
|
||||
Biju::Sms.new(
|
||||
id: 1,
|
||||
phone_number: "144",
|
||||
datetime: DateTime.new(2011, 7, 28, 15, 34, 8, '-12'),
|
||||
message: "Some text here")
|
||||
end
|
||||
subject { Biju::Sms.new(:id => "1", :phone_number => "144", :datetime => "11/07/28,15:34:08-12", :message => "Some text here")}
|
||||
|
||||
its(:id) { should eq(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") }
|
||||
it { subject.id.must_equal "1" }
|
||||
|
||||
describe '::from_pdu' do
|
||||
subject do
|
||||
Biju::Sms.from_pdu(
|
||||
'07913396050066F3040B913366666666F600003190509095928004D4F29C0E')
|
||||
end
|
||||
it { subject.phone_number.must_equal "144" }
|
||||
|
||||
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
|
||||
it { subject.datetime.must_equal "2011-07-28 15:34:08" }
|
||||
|
||||
describe '#to_pdu' do
|
||||
subject do
|
||||
Biju::Sms.new(
|
||||
phone_number: '33666666666',
|
||||
type_of_address: :international,
|
||||
message: 'Test').to_pdu.upcase
|
||||
end
|
||||
it { subject.message.must_equal "Some text here" }
|
||||
|
||||
it { should eq('0001000B913366666666F6000004D4F29C0E') }
|
||||
end
|
||||
it { subject.to_s.must_equal "1 - 144 - 2011-07-28 15:34:08 - Some text here"}
|
||||
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
|
||||
# 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
|
||||
require 'minitest/autorun'
|
||||
require "./lib/biju"
|
Loading…
Reference in New Issue