module SerialModem

Constants

DEBUG_LVL

Attributes

serial_sms[RW]
serial_sms_new[RW]
serial_ussd_new[RW]

Public Instance Methods

attached?() click to toggle source
# File lib/serial_modem.rb, line 469
def attached?
  @serial_sp != nil
end
check_presence() click to toggle source
# File lib/serial_modem.rb, line 340
def check_presence
  @serial_mutex.synchronize {
    @serial_tty.to_s.length > 0 and File.exists?(@serial_tty) and return
    System.exists?('lsusb') or return
    case lsusb = System.run_str('lsusb')
      when /19d2:fff1/
        log_msg :SerialModem, 'Found CDMA-modem with ttyUSB0-ttyUSB4'
        @serial_tty_error = '/dev/ttyUSB5'
        @serial_tty = '/dev/ttyUSB1'
        @ussd_add = ''
        @serial_eats_sms = true
      when /12d1:1506/, /12d1:14ac/, /12d1:1c05/, /12d1:1001/
        log_msg :SerialModem, 'Found 3G-modem with ttyUSB0-ttyUSB2'
        @serial_tty_error = '/dev/ttyUSB3'
        @serial_tty = '/dev/ttyUSB2'
        @ussd_add = (lsusb =~ /12d1:14ac/) ? ',15' : ''
        @serial_eats_sms = true
      when /airtel-modem/
        log_msg :SerialModem, 'Found 3G-modem with ttyUSB0-ttyUSB4'
        @serial_tty_error = '/dev/ttyUSB5'
        @serial_tty = '/dev/ttyUSB4'
        @ussd_add = ''
      else
        #puts caller.join("\n")
        @serial_tty = @serial_tty_error = nil
    end
    dputs(2) { "serial_tty is #{@serial_tty.inspect} and exists " +
        "#{File.exists?(@serial_tty.to_s)}" }
    if @serial_tty_error && File.exists?(@serial_tty_error)
      log_msg :SerialModem, 'resetting modem'
      reload_option
    end
  }
end
get_operator() click to toggle source
# File lib/serial_modem.rb, line 272
def get_operator
  modem_send_array([['AT+COPS=3,0', 'OK'],
                    ['AT+COPS?', 'OK']])
  (1..6).each {
    if @serial_codes.has_key? 'COPS'
      return '' if @serial_codes['COPS'] == '0'
      @serial_eats_sms and modem_send('AT+CNMI=0,0,0,0,0', 'OK')
      op = @serial_codes['COPS'].scan(/".*?"|[^",]\s*|,,/)[2].gsub(/"/, '')
      dputs(2) { "Found operator-string #{op}" }
      return op
    end
    sleep 0.5
  }
  return ''
end
init_modem() click to toggle source
# File lib/serial_modem.rb, line 299
def init_modem
  %w( ATZ
  AT+CNMI=0,0,0,0,0
  AT+CPMS="SM","SM","SM"
  AT+CFUN=1
  AT+CMGF=1 ).each { |at| modem_send(at, 'OK') }
  @serial_eats_sms and modem_send('AT+CNMI=0,0,0,0,0', 'OK')
  set_connection_type '3g'
end
interpret_serial_reply() click to toggle source
# File lib/serial_modem.rb, line 67
def interpret_serial_reply
  ret = []
  @serial_replies.each { |s| dputs(3) { s } }
  while m = @serial_replies.shift
    @serial_debug and dputs_func
    dputs(3) { "Reply: #{m}" }
    next if (m == '' || m =~ /^\^/)
    ret.push m
    if m =~ /\+[\w]{4}: /
      code, msg = m[1..4], m[7..-1]
      dputs(3) { "found code #{code.inspect} - #{msg.inspect}" }
      @serial_codes[code] = msg
      case code
        when /CMGL/
          # Typical input from the modem:
          # "0,\"REC UNREAD\",\"+23599836457\",,\"15/04/08,17:12:21+04\""
          # Output desired:
          # ["0", "REC UNREAD", "+23599836457", "", "15/04/08,17:12:21+04"]
          id, flag, number, unknown, date =
              msg.scan(/"(.*?)"|([^",]+)\s*|,,/m).collect { |a, b| a.to_s + b.to_s }
          msg = []
          # Read all lines up to an empty line or a +CMGL: which indicates
          # a new message
          while @serial_replies[0] &&
              @serial_replies[0] != '' &&
              !(@serial_replies[0] =~ /^\+CMGL:/)
            msg.push @serial_replies.shift
          end
          # If we finish with an empty line, delete it and the 'OK' that follows
          if @serial_replies[0] == ''
            @serial_replies.shift(2)
          end
          ret.push msg.join("\n")
          sms_new(id, flag, number, date, ret.last, unknown)
        when /CUSD/
          if pdu = msg.match(/.*\"(.*)\".*/)
            ussd_received(pdu_to_ussd(pdu[1]))
          elsif msg == '2'
            #log_msg :serialmodem, 'Closed USSD.'
            #ussd_received('')
            #ussd_close
          else
            log_msg :serialmodem, "Unknown: CUSD - #{msg}"
          end
        when /CMTI/
          if msg =~ /^.ME.,/
            dputs(3) { "I think I got a new message: #{msg}" }
            @serial_sms_autoscan_last = Time.now - @serial_sms_autoscan
          else
            log_msg :serialmodem, "Unknown: CMTI - #{msg}"
          end
        # Probably a message or so - '+CMTI: "ME",0' is a new message
      end
    end
  end
end
kill() click to toggle source
# File lib/serial_modem.rb, line 452
def kill
  #dputs_func
  #puts "Killed by \n" + caller.join("\n")
  if @serial_thread
    if @serial_thread.alive?
      dputs(3) { 'Killing thread' }
      @serial_thread.kill
      dputs(3) { 'Joining thread' }
      @serial_thread.join
      dputs(3) { 'Thread joined' }
    end
  end
  @serial_sp and @serial_sp.close
  dputs(1) { 'SerialModem killed' }
  @serial_sp = nil
end
modem_send(str, reply = true, lock: true) click to toggle source
# File lib/serial_modem.rb, line 133
def modem_send(str, reply = true, lock: true)
  return unless @serial_sp
  @serial_debug and dputs_func
  dputs(3) { "Sending string #{str} to modem" }
  lock and @serial_mutex.lock
  begin
    @serial_sp.write("#{str}\r\n")
  rescue Errno::EIO => e
    log_msg :SerialModem, "Couldn't write to device"
    kill
    return
  rescue Errno::ENODEV => e
    log_msg :SerialModem, 'Device is not here anymore'
    kill
    return
  end
  read_reply(reply, lock: false)
  lock and @serial_mutex.unlock
end
modem_send_array(cmds) click to toggle source
# File lib/serial_modem.rb, line 153
def modem_send_array(cmds)
  @serial_mutex.synchronize {
    cmds.each { |str, reply=true|
      modem_send(str, reply, lock: false)
    }
  }
end
pdu_to_ussd(str) click to toggle source
# File lib/serial_modem.rb, line 174
def pdu_to_ussd(str)
  [str].pack('H*').unpack('b*').join.scan(/.{7}/).
      map { |s| [s+'0'].pack('b*') }.join
end
read_reply(wait = nil, lock: true) click to toggle source
# File lib/serial_modem.rb, line 44
def read_reply(wait = nil, lock: true)
  @serial_debug and dputs_func
  raise IOError.new('NoModemHere') unless @serial_sp
  begin
    lock and @serial_mutex.lock
    while !@serial_sp.eof? || wait
      begin
        @serial_replies.push rep = @serial_sp.readline.chomp
        break if rep == wait
      rescue EOFError => e
        dputs(4) { 'Waited for string, but got nothing' }
        break
      end
    end
    lock and @serial_mutex.unlock

    ret = interpret_serial_reply
  rescue IOError => e
    raise e
  end
  ret
end
reload_option() click to toggle source
# File lib/serial_modem.rb, line 440
def reload_option
  @serial_sp and @serial_sp.close
  @serial_sp = nil
  dputs(1) { 'Trying to reload modem-driver - killing and reloading' }
  %w(chat ppp).each { |pro|
    System.run_str("killall -9 #{pro}")
  }
  %w(rmmod modprobe).each { |cmd|
    System.run_str("#{cmd} option")
  }
end
save_modem() click to toggle source
# File lib/serial_modem.rb, line 165
def save_modem
  modem_send('AT^U2DIAG=0', 'OK')
end
set_connection_type(net, modem = :e303) click to toggle source
# File lib/serial_modem.rb, line 288
def set_connection_type(net, modem = :e303)
  # According to https://wiki.archlinux.org/index.php/3G_and_GPRS_modems_with_pppd
  cmds = {e303: {c3go: '14,2,3FFFFFFF,0,2', c3g: '2,2,3FFFFFFF,0,2',
                 c2go: '13,1,3FFFFFFF,0,2', c2g: '2,1,3FFFFFFF,0,2'}}
  modem_send "AT^SYSCFG=#{cmds[modem]["c#{net}".to_sym]}", 'OK'
end
setup_modem(dev = nil) click to toggle source
# File lib/serial_modem.rb, line 13
def setup_modem(dev = nil)
  # @serial_debug = true
  @serial_debug = false
  @serial_tty = @serial_tty_error = @serial_sp = nil
  @serial_replies = []
  @serial_codes = {}
  @serial_sms = {}
  # TODO: once serialmodem == class, change this into Observer
  @serial_sms_new = []
  @serial_sms_new_list = []
  @serial_sms_autoscan = 20
  @serial_sms_autoscan_last = Time.now
  @serial_ussd = []
  @serial_ussd_last = Time.now
  @serial_ussd_timeout = 30
  @serial_ussd_results = []
  @serial_ussd_results_max = 100
  @serial_ussd_sent = 0
  @serial_ussd_sent_max = 5
  @serial_ussd_send_next = false
  # TODO: once serialmodem == class, change this into Observer
  @serial_ussd_new = []
  @serial_ussd_new_list = []
  @serial_mutex = Mutex.new
  # Some Huawei-modems eat SMS once they send a +CMTI-message - this
  # turns off the CMTI-messages which slows down incoming SMS detection
  @serial_eats_sms = false
  setup_tty
  dp "tty is #{@serial_tty}"
end
setup_tty() click to toggle source
# File lib/serial_modem.rb, line 309
def setup_tty
  check_presence

  @serial_mutex.synchronize {
    if !@serial_sp && @serial_tty
      if File.exists? @serial_tty
        dputs(2) { 'setting up SerialPort' }
        @serial_sp = SerialPort.new(@serial_tty, 115200)
        @serial_sp.read_timeout = 500
      end
    elsif @serial_sp &&
        (!@serial_tty||(@serial_tty && !File.exists?(@serial_tty)))
      dputs(2) { 'disconnecting modem' }
      kill
    end
  }
  if @serial_sp
    dputs(2) { 'initialising modem' }
    init_modem
    start_serial_thread
    if !@serial_sp
      dputs(2) { 'Lost serial-connection while initialising - killing and reloading' }
      kill
      reload_option
      return false
    end
    dputs(2) { 'finished connecting' }
  end
  return @serial_sp != nil
end
sms_delete(number) click to toggle source
# File lib/serial_modem.rb, line 263
def sms_delete(number)
  dputs(3) { "Asking to delete #{number} from #{@serial_sms.inspect}" }
  if @serial_sms.has_key? number.to_s
    dputs(3) { "Deleting #{number}" }
    modem_send("AT+CMGD=#{number}", 'OK')
    @serial_sms.delete number.to_s
  end
end
sms_new(id, flag, number, date, msg, unknown = nil) click to toggle source
# File lib/serial_modem.rb, line 124
def sms_new(id, flag, number, date, msg, unknown = nil)
  sms = {flag: flag, number: number, unknown: unknown, date: date,
         msg: msg, id: id}
  @serial_sms[id.to_s] = sms
  log_msg :SerialModem, "New SMS: #{sms.inspect}"
  @serial_sms_new_list.push(sms)
  sms
end
sms_scan(force = false) click to toggle source
# File lib/serial_modem.rb, line 251
def sms_scan(force = false)
  if force || (@serial_sms_autoscan > 0 &&
      Time.now - @serial_sms_autoscan_last > @serial_sms_autoscan)
    dputs(3) { 'Auto-scanning sms' }
    @serial_sms_autoscan_last = Time.now
    req = [['AT+CMGF=1', 'OK'],
           ['AT+CMGL="ALL"', 'OK']]
    @serial_eats_sms and req.push(['AT+CNMI=0,0,0,0,0', 'OK'])
    modem_send_array(req)
  end
end
sms_send(number, msg) click to toggle source
# File lib/serial_modem.rb, line 244
def sms_send(number, msg)
  log_msg :SerialModem, "Sending SMS --#{msg.inspect}-- to --#{number.inspect}--"
  modem_send_array([['AT+CMGF=1', 'OK'],
                    ["AT+CMGS=\"#{number}\""],
                    ["#{msg}\x1a", 'OK']])
end
start_serial_thread() click to toggle source
# File lib/serial_modem.rb, line 375
def start_serial_thread
  @serial_thread = Thread.new {
    # dputs_func
    dputs(2) { 'Thread started' }
    while @serial_sp do
      begin
        dputs(3) { 'Reading out modem' }
        read_reply

        dputs(4) { (Time.now - @serial_ussd_last).to_s }
        if @serial_ussd_send_next
          @serial_ussd_send_next = false
          ussd_send_now
        elsif ((Time.now - @serial_ussd_last > @serial_ussd_timeout) &&
            (@serial_ussd.length > 0)) || @serial_ussd_send_next
          if (@serial_ussd_sent += 1) <= @serial_ussd_sent_max
            log_msg :SerialModem, "Re-sending #{@serial_ussd.first} for #{@serial_ussd_sent}"
            ussd_send_now
          else
            log_msg :SerialModem, "Discarding #{@serial_ussd.first}"
            @serial_ussd.shift
            @serial_ussd_sent = 0
          end
        end

        sms_scan

        # Check for any new sms and call attached methods
        while (sms = @serial_sms_new_list.shift) do
          rescue_all do
            if sms._flag =~ /unread/i
              @serial_sms_new.each { |s|
                s.call(sms)
              }
            else
              dputs(2) { "Already read sms: #{sms}" }
            end
            sms_delete(sms._id)
          end
        end

        # Check for any new ussds and call attached methods
        while (ussd = @serial_ussd_new_list.shift) do
          code, str = ussd
          @serial_ussd_new.each { |s|
            s.call(code, str)
          }
        end

        sleep 0.5
      rescue IOError
        log_msg :SerialModem, 'IOError - killing modem'
        kill
        return
      rescue Exception => e
        dputs(0) { "#{e.inspect}" }
        dputs(0) { "#{e.to_s}" }
        e.backtrace.each { |l| dputs(0) { l } }
      end
      dputs(5) { 'Finished' }
    end
    dputs(0) { '@serial_sp disappeared - quitting' }
  }
end
traffic_statistics() click to toggle source
# File lib/serial_modem.rb, line 295
def traffic_statistics

end
ussd_close() click to toggle source
# File lib/serial_modem.rb, line 194
def ussd_close
  modem_send("AT+CUSD=2#{@ussd_add}", 'OK')
  @serial_ussd.length > 0 and ussd_send_now
end
ussd_fetch(str) click to toggle source
# File lib/serial_modem.rb, line 237
def ussd_fetch(str)
  return nil unless @serial_ussd_results
  dputs(3) { "Fetching str #{str} - #{@serial_ussd_results.inspect}" }
  res = @serial_ussd_results.reverse.find { |u| u._code == str }
  res ? res._result : nil
end
ussd_received(str) click to toggle source
# File lib/serial_modem.rb, line 231
def ussd_received(str)
  code = ussd_store_result(str)
  dputs(2) { "Got result for #{code}: -#{str}-" }
  @serial_ussd_new_list.push([code, str])
end
ussd_send(str) click to toggle source
# File lib/serial_modem.rb, line 199
def ussd_send(str)
  # dputs_func
  if str.class == String
    dputs(3) { "Sending ussd-code #{str}" }
    @serial_ussd.push str
    @serial_ussd.length == 1 and ussd_send_now
  elsif str.class == Array
    dputs(3) { "Sending menu-command #{str}" }
    @serial_ussd.concat str
    @serial_ussd.push nil
    @serial_ussd.length == str.length + 1 and ussd_send_now
  end
  @serial_ussd_sent = 0
end
ussd_send_now() click to toggle source
# File lib/serial_modem.rb, line 179
def ussd_send_now
  return unless @serial_ussd.length > 0
  str_send = @serial_ussd.first
  @serial_ussd_last = Time.now
  if str_send
    #log_msg :SerialModem, "Sending ussd-string #{str_send} with add of #{@ussd_add} "+
    #"and queue #{@serial_ussd}"
    modem_send("AT+CUSD=1,\"#{ussd_to_pdu(str_send)}\"#{@ussd_add}", 'OK')
  else
    dputs(2) { 'Sending ussd-close' }
    @serial_ussd.shift
    ussd_close
  end
end
ussd_store_result(str) click to toggle source
# File lib/serial_modem.rb, line 214
def ussd_store_result(str)
  if @serial_ussd.length > 0
    code = @serial_ussd.shift
    dputs(2) { "Got USSD-reply for #{code}: #{str}" }
    @serial_ussd_results.push(time: Time.now.strftime('%H:%M'),
                              code: code, result: str)
    @serial_ussd_results.shift([0, @serial_ussd_results.length -
                                     @serial_ussd_results_max].max)
    @serial_ussd_send_next = true
    @serial_ussd_sent = 0
    code
  else
    #log_msg :serialmodem, "Got unasked code #{str}"
    'unknown'
  end
end
ussd_to_pdu(str) click to toggle source
# File lib/serial_modem.rb, line 169
def ussd_to_pdu(str)
  str.unpack('b*').join.scan(/.{8}/).map { |s| s[0..6] }.join.
      scan(/.{1,8}/).map { |s| [s].pack('b*').unpack('H*')[0].upcase }.join
end