Compare commits

...

51 Commits

Author SHA1 Message Date
Guillaume Dott 932acd1046 Add resources in README 2017-04-13 10:41:36 +02:00
Guillaume DOTT c842211e75 Add SMS ID to MalformedSms error 2014-02-10 11:15:13 +01:00
Guillaume DOTT 52c9a13ca0 Return the exception without raising it when getting messages 2014-02-10 11:05:07 +01:00
Guillaume DOTT e34a881ab7 Add custom errors for PDU decoding 2014-02-10 11:05:00 +01:00
Guillaume DOTT e1e7be5d98 Add compatibility with ruby 1.9.3 2013-11-06 17:26:26 +01:00
Guillaume DOTT 6767b99b4a Add TODO note for UDH and SMS longer than 140 octets 2013-10-08 15:02:46 +02:00
Guillaume DOTT 926d7ec544 Add count on repeat when at least one occurence is required 2013-10-03 11:14:28 +02:00
Guillaume DOTT 3656fd6d82 Allow for an id or an enumerable for Biju::Hayes#delete 2013-10-03 11:11:21 +02:00
Guillaume DOTT 77105b180a Ignore PDU message lines in response when sending SMS
Tested with TP-LINK MA180.
It returns the PDU message when sending a message with a length greater
than 56 (i don't really know why for now). These lines are ignored and
only the last one with the status is parsed.
2013-10-03 11:07:45 +02:00
Guillaume DOTT 9475bdfb5c Flush serialport output before every AT command 2013-10-03 11:05:35 +02:00
Guillaume DOTT a3a9c43d8b Clean Biju::PDU.decode method 2013-10-01 15:08:29 +02:00
Guillaume DOTT 4e8463e0dc Add class for PDU 'first octet' field 2013-10-01 15:04:19 +02:00
Guillaume DOTT a0757a9c69 Add generic rule for responses in parser 2013-10-01 15:01:27 +02:00
Guillaume DOTT 001cbacfca Do not use ATZ command on initialization
ATZ can reset settings like SMSC number on some 3G keys.
2013-10-01 14:54:41 +02:00
Guillaume DOTT be53921802 Modify README and add comments for some cryptic methods 2013-09-12 10:37:50 +02:00
Guillaume DOTT 550a2b4d3d Add support for +CPIN? and use SIM status before entering PIN 2013-09-12 10:37:13 +02:00
Guillaume DOTT aabb2027b0 Add timeout to raise exception when Modem#wait is stuck 2013-09-12 10:37:13 +02:00
Guillaume DOTT 7281b8f82d Add support for +CNUM to get SIM phone numbers 2013-09-12 10:37:13 +02:00
Guillaume DOTT 2f4f1f81f3 Modify Hayes#messages to always return an array 2013-09-12 10:37:13 +02:00
Guillaume DOTT ff6dc617dc Remove support for string datetime in SMS 2013-09-12 10:37:13 +02:00
Guillaume DOTT f4218e1025 Add timestamp class to convert from PDU format to DateTime 2013-09-12 10:37:13 +02:00
Guillaume DOTT f049777a00 Remove mutation of argument string 2013-09-12 10:37:13 +02:00
Guillaume DOTT e790ac07c4 Raise a CmsError or CmeError when AT responds with error 2013-09-12 10:37:13 +02:00
Guillaume DOTT 276f0b0093 Improve Hayes#send to check for prompt and wait for answer 2013-09-12 10:37:13 +02:00
Guillaume DOTT ea3106cfc2 Add minimum length for Modem#wait 2013-09-12 10:37:12 +02:00
Guillaume DOTT d1103a0772 Add spec for new PDU classes 2013-09-12 10:37:12 +02:00
Guillaume DOTT 2de794042d Move GSM7Bit and UCS2 into Biju::PDU::Encoding submodule 2013-09-12 10:37:12 +02:00
Guillaume DOTT 1c3521586a Move PDU fields methods into their own classes 2013-09-12 10:37:12 +02:00
Guillaume DOTT dbd2f09ec2 Return length in encode method for gsm7bit and ucs2 2013-09-12 10:37:12 +02:00
Guillaume DOTT 885ac26cd1 Accept prompt in parser 2013-09-12 10:37:12 +02:00
Guillaume DOTT 3c677b7d29 Disable text mode and generate Sms objects in PDU mode
Text mode is only useful for debug purposes.
To parse answers, PDU mode is required (multiline messages, ...).
2013-09-12 10:37:12 +02:00
Guillaume DOTT b4f49569bb Add +CMGF? support and text_mode? method 2013-09-12 10:37:12 +02:00
Guillaume DOTT c0a3fe8ef0 Add request before response in parser 2013-09-12 10:37:12 +02:00
Guillaume DOTT 955654365a Add UCS-2 encoding / decoding for SMS user data 2013-09-12 10:37:12 +02:00
Guillaume DOTT 45de11cb3b Add tests for GSM7Bit 2013-09-12 10:37:12 +02:00
Guillaume DOTT cf655b2b3c Add length parameter to GSM7Bit decode 2013-09-12 10:37:12 +02:00
Guillaume DOTT 84fbedc46b Modify parser for PDU mode 2013-09-12 10:37:12 +02:00
Guillaume DOTT 1446be3c14 Remove useless self from to_hayes methods 2013-09-12 10:37:12 +02:00
Guillaume DOTT be87792382 Add timezone to DateTime format 2013-09-12 10:37:12 +02:00
Guillaume DOTT 548e5cd4cd Use Forwardable in Biju::Modem 2013-09-12 10:37:11 +02:00
Guillaume DOTT a001604ee7 Add PDU mode support and GSM 7Bit encoding / decoding 2013-09-12 10:37:11 +02:00
Guillaume DOTT 4ca3d259c1 Remove useless file 2013-09-12 10:37:11 +02:00
Guillaume DOTT 75509ac23a Add tests using Rspec 2013-09-12 10:37:11 +02:00
Guillaume DOTT 34224a072c Add swap files to gitignore 2013-09-12 10:37:11 +02:00
Guillaume DOTT 67571dd12e Move AT commands in Hayes class and use Modem to send to SerialPort 2013-09-12 10:37:11 +02:00
Guillaume DOTT b1568bbf24 Add to_hayes method to some basic classes 2013-09-12 10:37:11 +02:00
Guillaume DOTT 404f7d8290 Use DateTime object for date in Sms class 2013-09-12 10:37:11 +02:00
Guillaume DOTT dcbafff75b Add AT parser 2013-09-12 10:37:11 +02:00
Thomas Kienlen b17822b4eb at regex 2013-09-12 10:37:11 +02:00
Thomas Kienlen c087eb3973 hayes begins 2013-09-12 10:37:11 +02:00
Rodrigo Pinto 898d8d73a9 Merge pull request #1 from kmmndr/master
added more methods
2012-08-21 14:24:54 -07:00
38 changed files with 1448 additions and 113 deletions

1
.gitignore vendored
View File

@ -16,3 +16,4 @@ spec/reports
test/tmp
test/version_tmp
tmp
*.s[a-w][a-z]

2
.rspec 100644
View File

@ -0,0 +1,2 @@
--color
--format progress

View File

@ -1,6 +1,6 @@
# Biju
[WIP] Biju is an easy way to mount a GSM modem to send, to receive and to delete messages through a ruby interface.
Biju is an easy way to mount a GSM modem to send, to receive and to delete messages through a ruby interface.
This is project is based on this [code snippet](http://dzone.com/snippets/send-and-receive-sms-text).
## Installation
@ -20,24 +20,28 @@ Or install it yourself as:
## Usage
```
@modem = Biju::Modem.new(:port => "/dev/tty.HUAWEIMobile-Modem")
modem = Biju::Hayes.new('/dev/tty.HUAWEIMobile-Modem', pin: '0000')
# method to list all messages
@modem.messages.each do |sms|
# it can take the status in argument
# :unread, :read, :unsent, :sent, :all
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
@ -46,3 +50,34 @@ sms = Biju::Sms.new(:phone_number => '+3312345678', :message => 'hello world')
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

8
Rakefile 100644 → 100755
View File

@ -1,9 +1 @@
#!/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

View File

@ -15,7 +15,8 @@ Gem::Specification.new do |gem|
gem.require_paths = ["lib"]
gem.version = Biju::VERSION
gem.add_development_dependency "minitest", "3.0.0"
gem.add_development_dependency "rspec", "~> 2.14.0"
gem.add_dependency "serialport", "1.0.4"
gem.add_dependency "serialport", "~> 1.1.0"
gem.add_dependency "parslet", "~> 1.5.0"
end

View File

@ -1,3 +1,7 @@
require 'biju/version'
require "biju/modem"
require "biju/sms"
require 'biju/modem'
require 'biju/pdu'
require 'biju/to_hayes'
require 'biju/hayes'
require 'biju/sms'
require 'biju/parser'

View File

@ -0,0 +1,73 @@
module Biju
module AT
# Message Equipement Failure
class CmeError < Error
ERRORS = {
0 => 'Phone failure',
1 => 'No connection to phone',
2 => 'Phone adapter link reserved',
3 => 'Operation not allowed',
4 => 'Operation not supported',
5 => 'PH_SIM PIN required',
6 => 'PH_FSIM PIN required',
7 => 'PH_FSIM PUK required',
10 => 'SIM not inserted',
11 => 'SIM PIN required',
12 => 'SIM PUK required',
13 => 'SIM failure',
14 => 'SIM busy',
15 => 'SIM wrong',
16 => 'Incorrect password',
17 => 'SIM PIN2 required',
18 => 'SIM PUK2 required',
20 => 'Memory full',
21 => 'Invalid index',
22 => 'Not found',
23 => 'Memory failure',
24 => 'Text string too long',
25 => 'Invalid characters in text string',
26 => 'Dial string too long',
27 => 'Invalid characters in dial string',
30 => 'No network service',
31 => 'Network timeout',
32 => 'Network not allowed, emergency calls only',
40 => 'Network personalization PIN required',
41 => 'Network personalization PUK required',
42 => 'Network subset personalization PIN required',
43 => 'Network subset personalization PUK required',
44 => 'Service provider personalization PIN required',
45 => 'Service provider personalization PUK required',
46 => 'Corporate personalization PIN required',
47 => 'Corporate personalization PUK required',
48 => 'PH-SIM PUK required',
100 => 'Unknown error',
103 => 'Illegal MS',
106 => 'Illegal ME',
107 => 'GPRS services not allowed',
111 => 'PLMN not allowed',
112 => 'Location area not allowed',
113 => 'Roaming not allowed in this location area',
126 => 'Operation temporary not allowed',
132 => 'Service operation not supported',
133 => 'Requested service option not subscribed',
134 => 'Service option temporary out of order',
148 => 'Unspecified GPRS error',
149 => 'PDP authentication failure',
150 => 'Invalid mobile class',
256 => 'Operation temporarily not allowed',
257 => 'Call barred',
258 => 'Phone is busy',
259 => 'User abort',
260 => 'Invalid dial string',
261 => 'SS not executed',
262 => 'SIM Blocked',
263 => 'Invalid block',
772 => 'SIM powered down',
}
def initialize(id)
super(id, 100)
end
end
end
end

View File

@ -0,0 +1,99 @@
module Biju
module AT
# Message Service Failure
class CmsError < Error
ERRORS = {
1 => 'Unassigned number',
8 => 'Operator determined barring',
10 => 'Call bared',
21 => 'Short message transfer rejected',
27 => 'Destination out of service',
28 => 'Unindentified subscriber',
29 => 'Facility rejected',
30 => 'Unknown subscriber',
38 => 'Network out of order',
41 => 'Temporary failure',
42 => 'Congestion',
47 => 'Recources unavailable',
50 => 'Requested facility not subscribed',
69 => 'Requested facility not implemented',
81 => 'Invalid short message transfer reference value',
95 => 'Invalid message unspecified',
96 => 'Invalid mandatory information',
97 => 'Message type non existent or not implemented',
98 => 'Message not compatible with short message protocol',
99 => 'Information element non-existent or not implemente',
111 => 'Protocol error, unspecified',
127 => 'Internetworking , unspecified',
128 => 'Telematic internetworking not supported',
129 => 'Short message type 0 not supported',
130 => 'Cannot replace short message',
143 => 'Unspecified TP-PID error',
144 => 'Data code scheme not supported',
145 => 'Message class not supported',
159 => 'Unspecified TP-DCS error',
160 => 'Command cannot be actioned',
161 => 'Command unsupported',
175 => 'Unspecified TP-Command error',
176 => 'TPDU not supported',
192 => 'SC busy',
193 => 'No SC subscription',
194 => 'SC System failure',
195 => 'Invalid SME address',
196 => 'Destination SME barred',
197 => 'SM Rejected-Duplicate SM',
198 => 'TP-VPF not supported',
199 => 'TP-VP not supported',
208 => 'D0 SIM SMS Storage full',
209 => 'No SMS Storage capability in SIM',
210 => 'Error in MS',
211 => 'Memory capacity exceeded',
212 => 'Sim application toolkit busy',
213 => 'SIM data download error',
255 => 'Unspecified error cause',
300 => 'ME Failure',
301 => 'SMS service of ME reserved',
302 => 'Operation not allowed',
303 => 'Operation not supported',
304 => 'Invalid PDU mode parameter',
305 => 'Invalid Text mode parameter',
310 => 'SIM not inserted',
311 => 'SIM PIN required',
312 => 'PH-SIM PIN required',
313 => 'SIM failure',
314 => 'SIM busy',
315 => 'SIM wrong',
316 => 'SIM PUK required',
317 => 'SIM PIN2 required',
318 => 'SIM PUK2 required',
320 => 'Memory failure',
321 => 'Invalid memory index',
322 => 'Memory full',
330 => 'SMSC address unknown',
331 => 'No network service',
332 => 'Network timeout',
340 => 'No +CNMA expected',
500 => 'Unknown error',
512 => 'User abort',
513 => 'Unable to store',
514 => 'Invalid Status',
515 => 'Device busy or Invalid Character in string',
516 => 'Invalid length',
517 => 'Invalid character in PDU',
518 => 'Invalid parameter',
519 => 'Invalid length or character',
520 => 'Invalid character in text',
521 => 'Timer expired',
522 => 'Operation temporary not allowed',
532 => 'SIM not ready',
534 => 'Cell Broadcast error unknown',
535 => 'Protocol stack busy',
538 => 'Invalid parameter',
}
def initialize(id)
super(id, 500)
end
end
end
end

View File

@ -0,0 +1,17 @@
module Biju
module AT
class Error < ::Exception
ERRORS = {
1 => 'Unknown error',
}
def initialize(id, default = 1)
@error_id = (self.class::ERRORS.has_key?(id) ? id : default)
end
def to_s
"#{self.class::ERRORS[@error_id]} (#{@error_id})"
end
end
end
end

159
lib/biju/hayes.rb 100644
View File

@ -0,0 +1,159 @@
require 'biju/at/error'
require 'biju/at/cms_error'
require 'biju/at/cme_error'
module Biju
class Hayes
attr_reader :modem
MESSAGE_STATUS = {
unread: [0, 'REC UNREAD'],
read: [1, 'REC UNREAD'],
unsent: [2, ''],
sent: [3, ''],
all: [4, 'ALL'],
}
def initialize(port, options = {})
pin = options.delete(:pin) || '0000'
@modem = Modem.new(port, options)
attention
unlock_pin pin
text_mode(false)
extended_error
end
def close
modem.close
end
def at_command(cmd = nil, *args, &block)
command = ['AT', cmd].compact.join
command_args = args.compact.to_hayes
full_command = [command, (command_args.empty? ? nil : command_args)]
.compact.join('=') + "\r\n"
modem.flush
modem.write(full_command)
answer = hayes_to_obj(modem.wait(length: full_command.length))
return block.call(answer) if block_given?
answer
end
def attention
at_command[:status]
end
def init_modem
at_command('Z')[:status]
end
def phone_numbers
result = at_command('+CNUM')
return [] unless result.has_key?(:phone_numbers)
result[:phone_numbers].map do |number|
{
number: number[:array][1].gsub(/[^0-9]/, ''),
type_of_address: PDU::TypeOfAddress.new(number[:array][2]).to_sym
}
end
end
def text_mode(enabled = true)
at_command('+CMGF', enabled)[:status]
end
def text_mode?(force = false)
@text_mode = at_command('+CMGF?')[:result] if @text_mode.nil? || force
@text_mode
end
def extended_error(enabled = true)
at_command('+CMEE', enabled)[:status]
end
def prefered_storage(pms = nil)
result = at_command('+CPMS', pms)
return result[:array] if result[:cmd] == '+CPMS'
nil
end
def pin_status
at_command('+CPIN?')[:result]
end
def unlock_pin(pin)
at_command('+CPIN', pin)[:status] if pin_status == 'SIM PIN'
end
def messages!(which = :all)
messages(which, true)
end
def messages(which = :all, exceptions = false)
which = MESSAGE_STATUS[which][text_mode? ? 1 : 0] if which.is_a?(Symbol)
sms = at_command('+CMGL', which)
return [] unless sms.has_key?(:sms)
sms[:sms].map do |msg|
begin
Biju::Sms.from_pdu(msg[:message].chomp, msg[:infos][0])
rescue Biju::PDU::Errors::PDUError => e
malformed = Biju::PDU::Errors::MalformedSms.new(msg[:message].chomp, msg[:infos][0], e)
if exceptions
raise malformed
else
malformed
end
end
end
end
# Delete a sms message by id.
# @param [Fixnum] Id of sms message on modem.
def delete(id)
id = [id] if id.kind_of?(Fixnum)
return unless id.kind_of?(Enumerable)
res = true
id.each { |i| res &= at_command('+CMGD', i)[:status] }
res
end
def send(sms, options = {})
result = at_command('+CMGS', (sms.to_pdu.length - 2) / 2)
if result[:prompt]
modem.write("#{sms.to_pdu}#{26.chr}")
res = ''
loop do
res = modem.wait(length: 8)
break unless res.match(/\A[0-9A-Fa-f]+\r\n\z/)
end
hayes_to_obj(res.lstrip)
end
end
private
def hayes_to_obj(str)
res = ATTransform.new.apply(ATParser.new.parse(str))
case res[:cmd]
when '+CMS ERROR'
raise AT::CmsError.new(res[:result])
when '+CME ERROR'
raise AT::CmeError.new(res[:result])
end
res
end
end
end

View File

@ -1,86 +1,42 @@
require 'serialport'
require_relative 'sms'
require 'forwardable'
require 'timeout'
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(:port => '/dev/ttyUSB0')
# Biju::Modem.new('/dev/ttyUSB0')
#
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?
def initialize(port, options = {})
@connection = SerialPort.new(port, DEFAULT_OPTIONS.merge!(options))
end
# Close the serial connection.
def close
@connection.close
def_delegators :connection, :close, :write
def flush
wait(length: 0, timeout: 0)
end
# 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
def wait(options = {})
length = options[:length] || 0
timeout = options[:timeout] || 10
# 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 = ''
while IO.select([@connection], [], [], 0.25)
chr = @connection.getc.chr;
buffer += chr
Timeout.timeout(timeout) do
while IO.select([connection], [], [], 0.25) || buffer.length < length
buffer << connection.getc.chr
end
end
buffer
end
end

109
lib/biju/parser.rb 100644
View File

@ -0,0 +1,109 @@
require 'parslet'
require 'date'
module Biju
class ATParser < Parslet::Parser
root :at_string
rule(:at_string) { request | response }
# REQUEST
rule(:request) do
str('+++') | str('A/') | (prefix >> (cr.absent? >> lf.absent? >> any).repeat(0)) >>
(cr >> crlf >> response).maybe
end
rule(:prefix) { str('AT') | str('at') }
# RESPONSE
rule(:response) { ((command.maybe >> status) | merror) >> crlf | prompt }
rule(:prompt) { str('> ').as(:prompt) }
rule(:command) { mgl | num | pin | mgf | mgs | generic_response }
rule(:merror) do
(str('+CME ERROR') | str('+CMS ERROR')).as(:cmd) >> str(': ') >>
int.as(:result)
end
rule(:mgl) do
(str('+CMGL').as(:cmd) >> str(': ') >> infos >> crlf >> message >> crlf)
.repeat(1).as(:sms) >> crlf
end
rule(:num) do
(str('+CNUM').as(:cmd) >> str(': ') >> array >> crlf)
.repeat(1).as(:phone_numbers) >> crlf >> crlf
end
rule(:mgf) do
str('+CMGF').as(:cmd) >> str(': ') >> boolean.as(:result) >> crlf >> crlf
end
rule(:pin) do
str('+CPIN').as(:cmd) >> str(': ') >> eol.as(:result) >> crlf >> crlf
end
rule(:mgs) do
str('+CMGS').as(:cmd) >> str(': ') >> int.as(:result) >> crlf >> crlf
end
rule(:generic_response) do
match('[^:]').repeat(1).as(:cmd) >> str(': ') >> array >>
crlf >> crlf
end
rule(:array) do
(data >> (comma >> data).repeat).as(:array)
end
rule(:data) { (str('(') >> array >> str(')')) | info }
rule(:infos) { (info >> (comma >> info).repeat).as(:infos) }
rule(:info) { datetime | string | int | empty_string }
rule(:message) { match('[0-9A-Fa-f]').repeat(1).as(:message) }
# MISC
rule(:status) { (ok | error).as(:status) }
rule(:ok) { str('OK').as(:ok) }
rule(:error) { str('ERROR').as(:error) }
rule(:cr) { str("\r") }
rule(:lf) { str("\n") }
rule(:crlf) { cr >> lf }
rule(:comma) { str(',') }
rule(:quote) { str('"') }
rule(:empty_string) { str('').as(:empty_string) }
rule(:string) { quote >> match('[^\"]').repeat.as(:string) >> quote }
rule(:int) { match('[0-9]').repeat(1).as(:int) }
rule(:boolean) { match('[01]').as(:boolean) }
rule(:eol) { (crlf.absent? >> any).repeat.as(:string) }
rule(:datetime) { quote >> (date >> str(',') >> time).as(:datetime) >> quote }
rule(:date) do
(match('[0-9]').repeat(2) >> str('/')).repeat(2) >> match('[0-9]').repeat(2)
end
rule(:time) do
(match('[0-9]').repeat(2) >> str(':')).repeat(2) >> match('[0-9]').repeat(2) >>
match('[-+]') >> match('[0-9]').repeat(2)
end
end
class ATTransform < Parslet::Transform
rule(prompt: simple(:prompt)) { { prompt: true } }
rule(cmd: simple(:cmd), infos: subtree(:infos), message: simple(:message)) do
{ cmd: cmd.to_s, infos: infos, message: message.to_s }
end
rule(cmd: simple(:cmd), array: subtree(:array)) do
{ cmd: cmd.to_s, array: array }
end
rule(cmd: simple(:cmd), result: simple(:result)) do
{ cmd: cmd.to_s, result: result }
end
rule(empty_string: simple(:empty_string)) { '' }
rule(int: simple(:int)) { int.to_i }
rule(boolean: simple(:boolean)) { boolean.to_i > 0 }
rule(string: simple(:string)) { string.to_s }
rule(datetime: simple(:datetime)) do
DateTime.strptime(datetime.to_s, '%y/%m/%d,%T%Z')
end
rule(array: subtree(:array)) { array }
rule(status: simple(:status)) { { status: status } }
rule(ok: simple(:ok)) { true }
rule(error: simple(:error)) { false }
end
end

84
lib/biju/pdu.rb 100644
View File

@ -0,0 +1,84 @@
require 'biju/pdu/encoding/gsm7bit'
require 'biju/pdu/encoding/ucs2'
require 'biju/pdu/user_data'
require 'biju/pdu/data_coding_scheme'
require 'biju/pdu/first_octet'
require 'biju/pdu/timestamp'
require 'biju/pdu/phone_number'
require 'biju/pdu/type_of_address'
require 'biju/pdu/errors'
module Biju
module PDU
def self.encode(phone_number, message, options = {})
type_of_address = options[:type_of_address] || :international
phone_number = PhoneNumber.encode(phone_number)
user_data = UserData.encode(message)
first_octet = FirstOctet.new.message_type_indicator!(:sms_submit)
[
# Length of SMSC information
# 0 means the SMSC stored in the phone should be used
'00',
# First octet
'%02x' % first_octet.binary,
# TP-Message-Reference
'00',
'%02x' % phone_number.length,
'%02x' % phone_number.type_of_address.hex,
phone_number.number,
# TP-PID: Protocol identifier
'00',
'%02x' % user_data.encoding.hex,
'%02x' % user_data.length,
user_data.message
].join
end
def self.decode(string)
octets = string.scan(/../)
smsc_length = octets.shift.hex
smsc_number = octets.shift(smsc_length)
first_octet = FirstOctet.new(octets.shift.hex)
address_length = octets.shift.hex
address_type = octets.shift.hex
sender_number = PhoneNumber.new(
octets.shift(
(address_length.odd? ? address_length.succ : address_length) / 2).join,
type_of_address: address_type)
protocol_identifier = octets.shift
data_coding_scheme = octets.shift
timestamp = Timestamp.new(octets.shift(7).join).to_datetime
user_data_length = octets.shift.hex
user_data = UserData.new(message: octets.join,
encoding: data_coding_scheme,
length: user_data_length)
{
smsc_length: smsc_length,
smsc_number: smsc_number,
first_octet: first_octet,
address_length: address_length,
address_type: address_type,
sender_number: sender_number,
protocol_identifier: protocol_identifier,
data_coding_scheme: data_coding_scheme,
timestamp: timestamp,
user_data_length: user_data_length,
user_data: user_data
}
end
end
end

View File

@ -0,0 +1,42 @@
module Biju
module PDU
class DataCodingScheme
DATA_CODING_SCHEME = {
gsm7bit: 0,
gsm8bit: 4,
ucs2: 8,
reserved: 12,
}
def self.autodetect(message)
message.chars.each do |char|
return :ucs2 unless Encoding::GSM7Bit::BASIC_7BIT_CHARACTER_SET.include?(char) ||
Encoding::GSM7Bit::BASIC_7BIT_CHARACTER_SET_EXTENSION.has_value?(char)
end
:gsm7bit
end
def initialize(dcs, options = {})
unless dcs.is_a?(Symbol)
dcs = dcs.hex if dcs.is_a?(String)
if dcs & 0b11000000 == 0
dcs = DATA_CODING_SCHEME.key(dcs & 0b00001100)
else
raise Biju::PDU::Errors::DataCodingSchemeNotSupported.new(dcs)
end
end
@dcs = dcs
end
def to_sym
@dcs
end
def hex
DATA_CODING_SCHEME[@dcs]
end
end
end
end

View File

@ -0,0 +1,116 @@
# encoding: UTF-8
module Biju
module PDU
module Encoding
class GSM7Bit
BASIC_7BIT_CHARACTER_SET = [
'@', '£', '$', '¥', 'è', 'é', 'ù', 'ì', 'ò', 'Ç', "\n", 'Ø', 'ø', "\r", 'Å', 'å',
"\u0394", '_', "\u03a6", "\u0393", "\u039b", "\u03a9", "\u03a0","\u03a8", "\u03a3", "\u0398", "\u039e", "\e", 'Æ', 'æ', 'ß', 'É',
' ', '!', '"', '#', '¤', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?',
'¡', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'Ä', 'Ö', 'Ñ', 'Ü', '§',
'¿', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ä', 'ö', 'ñ', 'ü', 'à'
]
BASIC_7BIT_CHARACTER_SET_EXTENSION = {
0x0A => "\n",
0x0D => '',
0x14 => '^',
0x1B => '',
0x28 => '{',
0x29 => '}',
0x2F => '\\',
0x3C => '[',
0x3D => '~',
0x3E => ']',
0x40 => '|',
0x65 => '€',
}
def self.decode(string, options = {})
length = options[:length] || 0
res = ''
next_char = 0
current_length = 0
string.scan(/../).map(&:hex).each_with_index do |octet, i|
index = i % 7
# Only keep the bits for the current character and
# add relevant bits from the previous octet
# to get the full septet and decode the current character
current = ((octet & (2**(7 - index) - 1)) << index) | next_char
res = add_char(res, current)
current_length += 1
# Break when the number of septet is reached
# to prevent to add a last @ when there is 7 septets.
# The last octet will have one more septet to ignore.
break if length > 0 && current_length >= length
# Get the relevant bits for the next character
next_char = octet >> (7 - index)
# When index is 6, next_char contains a full septet
if index == 6
res = add_char(res, next_char)
current_length += 1
next_char = 0
end
end
res
end
def self.encode(string)
res = ''
length = 0
string.chars.each do |char|
# Look for the current character in basic character set and
# extension and concatenate the reversed septets to get
# full octets
if get_septet(char)
res << get_septet(char).reverse
length += 1
elsif get_septet(char, escape: true)
res << get_septet("\e").reverse
res << get_septet(char, escape: true).reverse
length += 2
end
end
# Add necessary bits to get a full octet
res << ('0' * (8 - (res.length % 8))) unless res.length % 8 == 0
[
# Group by octet, reverse them and print them in hex
res.scan(/.{8}/).map { |octet| '%02x' % octet.reverse.to_i(2) }.join,
length: length,
]
end
private
def self.add_char(string, char)
if string[-1] == "\e"
string.chop << BASIC_7BIT_CHARACTER_SET_EXTENSION[char]
else
string << BASIC_7BIT_CHARACTER_SET[char]
end
end
def self.get_septet(char, options = {})
escape = options[:escape] || false
char = (!escape ? BASIC_7BIT_CHARACTER_SET.index(char) : BASIC_7BIT_CHARACTER_SET_EXTENSION.key(char))
return nil unless char
'%07b' % char
end
end
end
end
end

View File

@ -0,0 +1,23 @@
module Biju
module PDU
module Encoding
class UCS2
def self.decode(string, options = {})
length = options[:length] || 0
string.scan(/.{4}/).map { |char| char.hex.chr('UCS-2BE') }.join
.encode('UTF-8', 'UCS-2BE')
end
def self.encode(string)
[
string.encode('UCS-2BE').chars.map do |char|
'%04x' % char.ord
end.join,
length: string.length * 2,
]
end
end
end
end
end

View File

@ -0,0 +1,38 @@
module Biju
module PDU
module Errors
class PDUError < ::StandardError
end
class MalformedSms < PDUError
attr_reader :original_exception
attr_reader :pdu, :id
def initialize(pdu, id, original_exception = nil)
@id = id
@pdu = pdu
@original_exception = original_exception
end
def to_s
"This SMS can not be parsed: #{pdu} (#{original_exception.class}: #{original_exception})"
end
end
class DataCodingSchemeNotSupported < PDUError
attr_reader :data_coding_scheme
def initialize(dcs = nil)
@data_coding_scheme = dcs
end
def to_s
"This data coding scheme (0b#{data_coding_scheme.to_s(2)}) is not supported"
end
end
class EncodingNotSupported < PDUError
end
end
end
end

View File

@ -0,0 +1,68 @@
module Biju
module PDU
class FirstOctet
FIRST_OCTET = {
reply_path: 0b10000000,
user_data_header: 0b01000000,
status_report_request: 0b00100000,
validity_period_format: 0b00011000,
reject_duplicates: 0b00000100,
message_type_indicator: 0b00000011,
}
MESSAGE_TYPE_INDICATOR = {
sms_deliver: 0b00000000,
sms_submit: 0b00000001,
sms_status: 0b00000010,
reserved: 0b00000011,
}
VALIDITY_PERIOD_FORMAT = {
not_present: 0b00000000,
reserved: 0b00001000,
relative: 0b00010000,
absolute: 0b00011000,
}
attr_accessor :binary
def initialize(first_octet = 0)
self.binary = first_octet
end
def get(field)
binary & FIRST_OCTET[field]
end
[:reply_path, :user_data_header, :status_report_request,
:reject_duplicates].each do |sym|
define_method :"#{sym}?" do
get(sym) > 0
end
define_method :"#{sym}!" do |value = true|
if value
self.binary |= FIRST_OCTET[sym]
else
self.binary &= (FIRST_OCTET[sym] ^ 0b11111111)
end
self
end
end
[:message_type_indicator, :validity_period_format].each do |sym|
define_method sym do
self.class.const_get(sym.upcase).key(get(sym))
end
define_method :"#{sym}!" do |value|
hash = self.class.const_get(sym.upcase)
self.binary = ((binary & (FIRST_OCTET[sym] ^ 0b11111111)) |
hash[value]) unless hash[value].nil?
self
end
end
end
end
end

View File

@ -0,0 +1,33 @@
module Biju
module PDU
class PhoneNumber
attr_accessor :type_of_address, :number
def self.encode(number, options = {})
type_of_address = options[:type_of_address] || :international
number = number + 'F' if number.length.odd?
new(
number.scan(/../).map(&:reverse).join,
type_of_address: type_of_address
)
end
def initialize(number, options = {})
type_of_address = options[:type_of_address] || :international
self.number = number
self.type_of_address = TypeOfAddress.new(type_of_address)
end
def decode
number.scan(/../).map(&:reverse).join.chomp('F')
end
def length
# If the last character is 0xF, remove this one from length
number.length - (number[-2].hex == 15 ? 1 : 0)
end
end
end
end

View File

@ -0,0 +1,34 @@
require 'date'
module Biju
module PDU
class Timestamp
attr_reader :timestamp
def initialize(timestamp)
@timestamp = timestamp
end
def timezone
# The last 2 digits of the timestamp are for the timezone
timezone = timestamp[-2, 2].reverse.hex
# The MSB define the plus-minus sign. 0 for +, 1 for -
sign = (timezone >> 7 == 0 ? '+' : '-')
# The following 3 bits represent tens digit
# and the last 4 bits are for the units digit
tens_digit = ((timezone & 0b01110000) >> 4)
units_digit = (timezone & 0b00001111)
# Timezone is in quarters of an hour
sign << '%02d' % ((tens_digit * 10 + units_digit) / 4)
end
def to_datetime
DateTime.strptime(
"#{timestamp[0..-3].reverse}#{timezone}", '%S%M%H%d%m%y%Z')
end
end
end
end

View File

@ -0,0 +1,30 @@
module Biju
module PDU
class TypeOfAddress
TYPE_OF_ADDRESS = {
unknown: 0b10000001,
international: 0b10010001,
national: 0b10100001,
reserved: 0b11110001,
}
def initialize(type_of_address, options = {})
type_of_address = :international if type_of_address.nil?
unless type_of_address.is_a?(Symbol)
type_of_address = type_of_address.hex if type_of_address.is_a?(String)
type_of_address = TYPE_OF_ADDRESS.key(type_of_address)
end
@type_of_address = type_of_address
end
def to_sym
@type_of_address
end
def hex
TYPE_OF_ADDRESS[@type_of_address]
end
end
end
end

View File

@ -0,0 +1,34 @@
module Biju
module PDU
class UserData
ENCODING = {
gsm7bit: Encoding::GSM7Bit,
ucs2: Encoding::UCS2,
}
attr_accessor :encoding, :message, :length, :user_data_header
def self.encode(message, options = {})
encoding = options[:encoding] || DataCodingScheme.autodetect(message)
raise ArgumentError, 'Unknown encoding' unless ENCODING.has_key?(encoding)
res = ENCODING[encoding].encode(message)
new(message: res[0], length: res[1][:length], encoding: encoding)
end
def initialize(options = {})
self.encoding = DataCodingScheme.new(options[:encoding] || :gsm7bit)
self.message = options[:message] || ''
self.length = options[:length] || 0
self.user_data_header = options[:user_data_header] || false
end
def decode
ENCODING[encoding.to_sym].decode(message, length: length)
end
end
end
end

View File

@ -1,19 +1,30 @@
require 'date'
module Biju
class Sms
attr_accessor :id, :phone_number, :datetime, :message
attr_reader :id, :phone_number, :type_of_address, :message, :datetime
def initialize(params={})
def self.from_pdu(string, id = nil)
sms_infos = PDU.decode(string)
new(id: id,
phone_number: sms_infos[:sender_number].decode,
type_of_address: sms_infos[:sender_number].type_of_address.to_sym,
datetime: sms_infos[:timestamp],
message: sms_infos[:user_data].decode)
end
def initialize(params = {})
params.each do |attr, value|
self.public_send("#{attr}=", value)
instance_variable_set(:"@#{attr}", value)
end if params
end
def datetime
@datetime.sub(/(\d+)\D+(\d+)\D+(\d+),(\d*\D)(\d*\D)(\d+)(.*)/, '20\1-\2-\3 \4\5\6')
def to_s
"[#{id}] (#{phone_number}) #{datetime} '#{message}'"
end
def to_s
"#{id} - #{phone_number} - #{datetime} - #{message}"
def to_pdu
Biju::PDU.encode(phone_number, message, type_of_address: type_of_address)
end
end
end

View File

@ -0,0 +1,19 @@
class Object
def to_hayes; "\"#{to_s}\""; end
end
class Fixnum
def to_hayes; to_s; end
end
class TrueClass
def to_hayes; '1'; end
end
class FalseClass
def to_hayes; '0'; end
end
class Array
def to_hayes; map(&:to_hayes).join(','); end
end

View File

@ -1,3 +1,3 @@
module Biju
VERSION = "0.0.2"
VERSION = '0.0.2'
end

View File

@ -1,10 +0,0 @@
require_relative '../spec_helper'
# TODO: Fix missing tests SOON
describe Biju::Modem do
describe ".new" do
it "should raise an Exception without port option" do
lambda { Biju::Modem.new }.must_raise Exception
end
end
end

View File

@ -0,0 +1,144 @@
require 'spec_helper'
require 'biju/parser'
describe Biju::ATParser do
context "status" do
it "returns ok status" do
result = Biju::ATTransform.new.apply(
Biju::ATParser.new.parse("AT\r\r\nOK\r\n"))
expect(result).to include(status: true)
end
it "returns error status" do
result = Biju::ATTransform.new.apply(
Biju::ATParser.new.parse("AT\r\r\nERROR\r\n"))
expect(result).to include(status: false)
end
end
context "errors" do
it "parses CMS ERROR" do
result = Biju::ATTransform.new.apply(
Biju::ATParser.new.parse("AT\r\r\n+CMS ERROR: 500\r\n"))
expect(result[:cmd]).to eq('+CMS ERROR')
expect(result[:result]).to eq(500)
end
it "parses CME ERROR" do
result = Biju::ATTransform.new.apply(
Biju::ATParser.new.parse("AT\r\r\n+CME ERROR: 100\r\n"))
expect(result[:cmd]).to eq('+CME ERROR')
expect(result[:result]).to eq(100)
end
end
context "response" do
it "parses generic response" do
resp = "AT+COPS\r\r\n+COPS: 1,\"two\",(3,4)\r\n\r\nOK\r\n"
result = Biju::ATTransform.new.apply(
Biju::ATParser.new.parse(resp))
expect(result[:cmd]).to eq('+COPS')
expect(result[:array]).to have(3).fields
expect(result[:array]).to include(1)
expect(result[:array]).to include('two')
expect(result[:array]).to include([3,4])
end
it "parses cmgs prompt" do
mgs = "AT+CMGS=18\r\r\n> "
result = Biju::ATTransform.new.apply(
Biju::ATParser.new.parse(mgs))
expect(result).to include(prompt: true)
end
it "parses messages list" do
messages = "AT+CMGL=1\r\r\n" <<
"+CMGL: 0,1,,23\r\n" <<
"07913396050066F3040B91336789\r\n" <<
"+CMGL: 3,1,,74\r\n" <<
"BD60B917ACC68AC17431982E066BC5642205F3C95400\r\n" <<
"+CMGL: 4,1,,20\r\n" <<
"07913396050066F3040B913364446864\r\n" <<
"\r\n" <<
"OK\r\n"
result = Biju::ATTransform.new.apply(
Biju::ATParser.new.parse(messages))
expect(result).to include(status: true)
expect(result[:sms]).to have(3).messages
expect(result[:sms][0][:message]).to eq('07913396050066F3040B91336789')
end
it "gets phone numbers" do
pms = "AT+CNUM\r\r\n+CNUM: \"M\",\"+33666666666\",145\r\n\r\n\r\nOK\r\n"
result = Biju::ATTransform.new.apply(
Biju::ATParser.new.parse(pms))
expect(result[:phone_numbers][0][:cmd]).to eq('+CNUM')
expect(result[:phone_numbers]).to have(1).phone_number
expect(result[:phone_numbers][0][:array][1]).to eq('+33666666666')
end
it "gets messages storage" do
pms = "AT+CPMS=?\r\r\n+CPMS: ((\"SM\",\"BM\",\"SR\"),(\"SM\"))\r\n\r\nOK\r\n"
result = Biju::ATTransform.new.apply(
Biju::ATParser.new.parse(pms))
expect(result[:cmd]).to eq('+CPMS')
expect(result[:array]).to have(2).storage
expect(result[:array][0]).to have(3).storage
end
it "gets specified message storage infos" do
pms = "AT+CPMS=\"MT\"\r\r\n+CPMS: 23,23,7,100,7,100\r\n\r\nOK\r\n"
result = Biju::ATTransform.new.apply(
Biju::ATParser.new.parse(pms))
expect(result).to include(status: true)
expect(result[:array]).to have(6).storage
expect(result[:array]).to eq([23, 23, 7, 100, 7, 100])
end
it "gets pin status" do
pin = "AT+CPIN?\r\r\n+CPIN: READY\r\n\r\nOK\r\n"
result = Biju::ATTransform.new.apply(
Biju::ATParser.new.parse(pin))
expect(result).to include(status: true)
expect(result[:result]).to eq("READY")
end
it "parses +CMGF? response" do
mgf = "AT+CMGF?\r\r\n+CMGF: 0\r\n\r\nOK\r\n"
result = Biju::ATTransform.new.apply(
Biju::ATParser.new.parse(mgf))
expect(result).to include(status: true)
expect(result[:result]).to be_false
end
it "parses message sent response" do
mgs = "+CMGS: 163\r\n\r\nOK\r\n"
result = Biju::ATTransform.new.apply(
Biju::ATParser.new.parse(mgs))
expect(result).to include(status: true)
expect(result[:result]).to eq(163)
end
end
it "raises ParseFailed exception" do
expect { Biju::ATParser.new.parse('Ha') }.to raise_error(Parslet::ParseFailed)
end
end

View File

@ -0,0 +1,30 @@
# encoding: UTF-8
require 'spec_helper'
require 'biju/pdu'
describe Biju::PDU::DataCodingScheme do
describe '::autodetect' do
it "autodetects gsm7bit encoding" do
[
"Test",
"Ç$",
"[teßt}",
].each do |string|
expect(Biju::PDU::DataCodingScheme.autodetect(string)).to eq(:gsm7bit)
end
end
it "autodetects ucs2 encoding" do
[
"ç",
"âmazing",
].each do |string|
expect(Biju::PDU::DataCodingScheme.autodetect(string)).to eq(:ucs2)
end
end
end
subject { Biju::PDU::DataCodingScheme.new(:gsm7bit) }
its(:to_sym) { should eq(:gsm7bit) }
its(:hex) { should eq(0) }
end

View File

@ -0,0 +1,47 @@
# encoding: UTF-8
require 'spec_helper'
require 'biju/pdu/encoding/gsm7bit'
describe Biju::PDU::Encoding::GSM7Bit do
describe '::decode' do
it "decodes string" do
expect(Biju::PDU::Encoding::GSM7Bit.decode('D4F29C0E', length: 4)).to eq('Test')
end
it "decodes character from extension set" do
expect(Biju::PDU::Encoding::GSM7Bit.decode('9B32', length: 2)).to eq('€')
end
it "decodes character with a length of 7" do
expect(Biju::PDU::Encoding::GSM7Bit.decode('E170381C0E8701', length: 7)).to eq('a' * 7)
end
end
describe '::encode' do
it "encodes string" do
expect(Biju::PDU::Encoding::GSM7Bit.encode('Test').first.upcase).to eq('D4F29C0E')
end
it "encodes character from extension set" do
expect(Biju::PDU::Encoding::GSM7Bit.encode('€').first.upcase).to eq('9B32')
end
it "encodes character with a length of 7" do
expect(Biju::PDU::Encoding::GSM7Bit.encode('a' * 7).first.upcase).to eq('E170381C0E8701')
end
end
it "gives same text after encoding and decoding" do
strings = [
'My first TEST',
'{More complicated]',
'And on€ More~',
'a' * 7,
]
strings.each do |string|
expect(Biju::PDU::Encoding::GSM7Bit.decode(
*Biju::PDU::Encoding::GSM7Bit.encode(string))).to eq(string)
end
end
end

View File

@ -0,0 +1,31 @@
# encoding: UTF-8
require 'spec_helper'
require 'biju/pdu/encoding/ucs2'
describe Biju::PDU::Encoding::UCS2 do
describe '::decode' do
it "decodes string" do
expect(Biju::PDU::Encoding::UCS2.decode('00C700E700E200E300E500E4016B00F80153', length: 4)).to eq('Ççâãåäūøœ')
end
end
describe '::encode' do
it "encodes string" do
expect(Biju::PDU::Encoding::UCS2.encode('Ççâãåäūøœ').first.upcase).to eq('00C700E700E200E300E500E4016B00F80153')
end
end
it "gives same text after encoding and decoding" do
strings = [
'My first TEST',
'{More çomplicated]',
'And on€ More~',
'þß®',
]
strings.each do |string|
expect(Biju::PDU::Encoding::UCS2.decode(
*Biju::PDU::Encoding::UCS2.encode(string))).to eq(string)
end
end
end

View File

@ -0,0 +1,12 @@
require 'spec_helper'
require 'biju/pdu'
describe Biju::PDU::FirstOctet do
its(:reply_path?) { should be_false }
its(:user_data_header?) { should be_false }
its(:status_report_request?) { should be_false }
its(:reject_duplicates?) { should be_false }
its(:message_type_indicator) { should eq(:sms_deliver) }
its(:validity_period_format) { should eq(:not_present) }
end

View File

@ -0,0 +1,21 @@
require 'spec_helper'
require 'biju/pdu'
describe Biju::PDU::PhoneNumber do
describe '::encode' do
subject { Biju::PDU::PhoneNumber.encode('33123456789') }
its(:number) { should eq('3321436587F9') }
end
context "odd length" do
subject { Biju::PDU::PhoneNumber.new('3321436587F9') }
its(:decode) { should eq('33123456789') }
its(:length) { should eq(11) }
end
context "even length" do
subject { Biju::PDU::PhoneNumber.new('3321436587') }
its(:decode) { should eq('3312345678') }
its(:length) { should eq(10) }
end
end

View File

@ -0,0 +1,9 @@
require 'spec_helper'
require 'biju/pdu'
describe Biju::PDU::Timestamp do
subject { Biju::PDU::Timestamp.new('31900141039580') }
its(:timezone) { should eq('+02') }
its(:to_datetime) { should eq(DateTime.new(2013, 9, 10, 14, 30, 59, '+02')) }
end

View File

@ -0,0 +1,9 @@
require 'spec_helper'
require 'biju/pdu'
describe Biju::PDU::TypeOfAddress do
subject { Biju::PDU::TypeOfAddress.new(:international) }
its(:to_sym) { should eq(:international) }
its(:hex) { should eq(145) }
end

View File

@ -0,0 +1,12 @@
require 'spec_helper'
require 'biju/pdu'
describe Biju::PDU::UserData do
subject(:message) { 'Test' }
subject(:encoded) { Biju::PDU::Encoding::GSM7Bit.encode(message) }
subject { Biju::PDU::UserData.encode(message, encoding: :gsm7bit) }
its(:message) { should eq(encoded[0]) }
its(:length) { should eq(encoded[1][:length]) }
its(:decode) { should eq(message) }
end

View File

@ -1,15 +1,40 @@
require_relative '../spec_helper'
require 'spec_helper'
require 'biju'
describe Biju::Sms do
subject { Biju::Sms.new(:id => "1", :phone_number => "144", :datetime => "11/07/28,15:34:08-12", :message => "Some text here")}
subject do
Biju::Sms.new(
id: 1,
phone_number: "144",
datetime: DateTime.new(2011, 7, 28, 15, 34, 8, '-12'),
message: "Some text here")
end
it { subject.id.must_equal "1" }
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.phone_number.must_equal "144" }
describe '::from_pdu' do
subject do
Biju::Sms.from_pdu(
'07913396050066F3040B913366666666F600003190509095928004D4F29C0E')
end
it { subject.datetime.must_equal "2011-07-28 15:34:08" }
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.message.must_equal "Some text here" }
describe '#to_pdu' do
subject do
Biju::Sms.new(
phone_number: '33666666666',
type_of_address: :international,
message: 'Test').to_pdu.upcase
end
it { subject.to_s.must_equal "1 - 144 - 2011-07-28 15:34:08 - Some text here"}
end
it { should eq('0001000B913366666666F6000004D4F29C0E') }
end
end

View File

@ -0,0 +1,10 @@
require 'spec_helper'
require 'biju/to_hayes'
describe "blah blah" do
it { expect(5.to_hayes).to eq('5') }
it { expect(true.to_hayes).to eq('1') }
it { expect(false.to_hayes).to eq('0') }
it { expect("test".to_hayes).to eq('"test"') }
it { expect([1, 2].to_hayes).to eq('1,2') }
end

View File

@ -1,2 +1,17 @@
require 'minitest/autorun'
require "./lib/biju"
# 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