Compare commits

..

No commits in common. "develop" and "master" have entirely different histories.

38 changed files with 113 additions and 1448 deletions

1
.gitignore vendored
View File

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

2
.rspec
View File

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

View File

@ -1,6 +1,6 @@
# Biju # Biju
Biju is an easy way to mount a GSM modem to send, to receive and to delete messages through a ruby interface. [WIP] Biju is an easy way to mount a GSM modem to send, to receive and to delete messages through a ruby interface.
This is project is based on this [code snippet](http://dzone.com/snippets/send-and-receive-sms-text). This is project is based on this [code snippet](http://dzone.com/snippets/send-and-receive-sms-text).
## Installation ## Installation
@ -20,28 +20,24 @@ Or install it yourself as:
## Usage ## Usage
``` ```
modem = Biju::Hayes.new('/dev/tty.HUAWEIMobile-Modem', pin: '0000') @modem = Biju::Modem.new(:port => "/dev/tty.HUAWEIMobile-Modem")
# method to list all messages # method to list all messages
# it can take the status in argument @modem.messages.each do |sms|
# :unread, :read, :unsent, :sent, :all
modem.messages.each do |sms|
puts sms puts sms
end end
# method to send sms # method to send sms
sms = Biju::Sms.new(phone_number: '+3312345678', message: 'hello world') sms = Biju::Sms.new(:phone_number => '+3312345678', :message => 'hello world')
modem.send(sms) @modem.send(sms)
modem.close @modem.close
``` ```
## TODO ## TODO
1. Write missing test for modem module. 1. Write missing test for modem module.
2. Write a documentation. 2. Write a documentation.
3. Test with different kinds of modem and OS. 3. Test with different kinds of modem and OS.
4. Handle UDH (User Data Header) and SMS longer than 140 octets
## Contributing ## Contributing
@ -50,34 +46,3 @@ modem.close
3. Commit your changes (`git commit -am 'Added some feature'`) 3. Commit your changes (`git commit -am 'Added some feature'`)
4. Push to the branch (`git push origin my-new-feature`) 4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request 5. Create new Pull Request
## Resources
http://en.wikipedia.org/wiki/GSM_03.40
http://www.etsi.org/deliver/etsi_gts/04/0408/05.00.00_60/gsmts_0408v050000p.pdf
http://www.etsi.org/deliver/etsi_ts/101000_101099/101032/05.01.00_60/ts_101032v050100p.pdf
http://en.wikipedia.org/wiki/Short_message_service_center
http://en.wikipedia.org/wiki/AT_command
http://subnets.ru/saved/sms_pdu_format.html
http://jazi.staff.ugm.ac.id/Mobile%20and%20Wireless%20Documents/SMS_PDU-mode.PDF
http://www.sendsms.cn/download/SMS_PDU-mode.PDF
http://www.sendsms.cn/download/wavecom/PDU%B6%CC%D0%C5%CF%A2/SMS_PDU-mode.PDF
http://www.gsm-modem.de/sms-pdu-mode.html
http://www.developershome.com/sms/cmgrCommand3.asp
http://www.developershome.com/sms/cmgsCommand4.asp
http://en.wikipedia.org/wiki/Concatenated_SMS
# Encoding
http://en.wikipedia.org/wiki/GSM_03.38
http://www.3gpp.org/ftp/Specs/html-info/0338.htm
http://www.codeproject.com/Tips/470755/Encoding-Decoding-7-bit-User-Data-for-SMS-PDU-PDU
https://github.com/bitcoder/ruby_ucp/wiki/SMS-Alphabets
# AT Commands
http://en.wikipedia.org/wiki/Hayes_command_set
https://www.sparkfun.com/datasheets/Cellular%20Modules/ADH8066-AT-Commands-v1.6.pdf
http://www.coster.eu/costerit/teleges/doc/gsm822w.pdf
http://www.developershome.com/sms/cmglCommand.asp
http://www.zoomtel.com/documentation/dial_up/100498D.pdf
AT+CLAC > list supported commands

8
Rakefile 100755 → 100644
View File

@ -1 +1,9 @@
#!/usr/bin/env rake
require "bundler/gem_tasks" require "bundler/gem_tasks"
require 'rake/testtask'
Rake::TestTask.new do |t|
t.libs.push "lib"
t.test_files = FileList['spec/**/*_spec.rb']
t.verbose = true
end

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,42 +1,86 @@
require 'serialport' require 'serialport'
require 'forwardable' require_relative 'sms'
require 'timeout'
module Biju module Biju
class Modem class Modem
extend Forwardable
DEFAULT_OPTIONS = { baud: 9600, data_bits: 8,
stop_bits: 1, parity: SerialPort::NONE }
attr_reader :connection
# @param [Hash] Options to serial connection. # @param [Hash] Options to serial connection.
# @option options [String] :port The modem port to connect # @option options [String] :port The modem port to connect
# #
# Biju::Modem.new('/dev/ttyUSB0') # Biju::Modem.new(:port => '/dev/ttyUSB0')
# #
def initialize(port, options = {}) def initialize(options={}, &block)
@connection = SerialPort.new(port, DEFAULT_OPTIONS.merge!(options)) raise Exception.new("Port is required") unless options[:port]
pin = options.delete(:pin)
@connection = connection(options)
cmd("AT")
# initialize modem
cmd("ATZ")
# unlock pin code
cmd("AT+CPIN=\"#{pin}\"") if pin
# set SMS text mode
cmd("AT+CMGF=1")
# set extended error reports
cmd('AT+CMEE=1')
#instance_eval &block if block_given?
end end
def_delegators :connection, :close, :write # Close the serial connection.
def close
def flush @connection.close
wait(length: 0, timeout: 0)
end end
def wait(options = {}) # Return an Array of Sms if there is messages nad return nil if not.
length = options[:length] || 0 def messages(which = "ALL")
timeout = options[:timeout] || 10 # read message from all storage in the mobile phone (sim+mem)
cmd('AT+CPMS="MT"')
# get message list
sms = cmd('AT+CMGL="%s"' % which )
# collect messages
msgs = sms.scan(/\+CMGL\:\s*?(\d+)\,.*?\,\"(.+?)\"\,.*?\,\"(.+?)\".*?\n(.*)/)
return nil unless msgs
msgs.collect!{ |msg| Biju::Sms.new(:id => msg[0], :phone_number => msg[1], :datetime => msg[2], :message => msg[3].chomp) }
end
# Delete a sms message by id.
# @param [Fixnum] Id of sms message on modem.
def delete(id)
cmd("AT+CMGD=#{id}")
end
def send(sms, options = {})
# initiate the sms, and wait for either
# the text prompt or an error message
cmd("AT+CMGS=\"#{sms.phone_number}\"")
# send the sms, and wait until
# it is accepted or rejected
cmd("#{sms.message}#{26.chr}")
# ... check reception
end
private
def connection(options)
port = options.delete(:port)
SerialPort.new(port, default_options.merge!(options))
end
def default_options
{ :baud => 9600, :data_bits => 8, :stop_bits => 1, :parity => SerialPort::NONE }
end
def cmd(cmd)
@connection.write(cmd + "\r")
wait_str = wait
#p "#{cmd} --> #{wait_str}"
end
def wait
buffer = '' buffer = ''
Timeout.timeout(timeout) do while IO.select([@connection], [], [], 0.25)
while IO.select([connection], [], [], 0.25) || buffer.length < length chr = @connection.getc.chr;
buffer << connection.getc.chr buffer += chr
end end
end
buffer buffer
end end
end end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

@ -1,17 +1,2 @@
# This file was generated by the `rspec --init` command. Conventionally, all require 'minitest/autorun'
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. require "./lib/biju"
# Require this file using `require "spec_helper"` to ensure that it is only
# loaded once.
#
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
RSpec.configure do |config|
config.treat_symbols_as_metadata_keys_with_true_values = true
config.run_all_when_everything_filtered = true
config.filter_run :focus
# Run specs in random order to surface order dependencies. If you find an
# order dependency and want to debug it, you can fix the order by providing
# the seed, which is printed after each run.
# --seed 1234
config.order = 'random'
end