Управление освещением
    Презентации
    Технические решения на LogicMachine
    Визуализация
      evika.ru    Технические решения на LogicMachine    Управление устройствами KNX/EIB и получение от них сообщений посредством SMS

    Управление устройствами KNX/EIB и получение от них сообщений посредством SMS

    Подсоедините USB GSM адаптер к LogicMachine

    В данном примере использовался модем Huawei E173, который активируется автоматически сразу после подсоединения к любому USB-порту LogicMachine. Список доступных модемов указан здесь >>

    В библиотеку пользовательских скриптов необходимо добавить специальные функции вместе с PIN кодом и «белым» списком телефонных номеров, на которые будет производиться отправка и получение SMS-сообщений.

    Шаг 1 – Создать новую библиотеку user.sms в Скрипты — > Пользовательские библиотеки (Scripting -> User libraries):

    AT = {
      -- 7-bit alphabet
      alphabet = {
        64, 163, 36, 165, 232, 233, 249, 236, 242, 199, 10, 216, 248,
        13, 197, 229, 10, 95, 10, 10, 10, 10, 10, 10, 10, 10, 10, 38,
        198, 230, 223, 201, 32, 33, 34, 35, 164, 37, 38, 39, 40, 41,
        42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
        57, 58, 59, 60, 61, 62, 63, 161, 65, 66, 67, 68, 69, 70, 71,
        72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86,
        87, 88, 89, 90, 196, 214, 209, 220, 167, 191, 97, 98, 99, 100,
        101, 102, 103,  104, 105, 106, 107, 108, 109, 110, 111, 112,
        113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 228, 246,
        241, 252, 224
      },
      parsepdu = function(pdu)
        local data, len, msg, data, sender, offset, ntype, timestamp
     
        msg = {}
     
        -- offset from service center number
        offset = tonumber(pdu:sub(1, 2), 16) * 2
     
        -- sender number length
        len = tonumber(pdu:sub(offset + 5, offset + 6), 16)
        len = math.ceil(len / 2) * 2
     
        -- sender number type
        ntype = tonumber(pdu:sub(offset + 7, offset + 8), 16)
        ntype = bit.band(bit.rshift(ntype, 4), 0x07)
     
        -- raw sender number
        sender = pdu:sub(offset + 9, offset + len + 8)
     
        -- decode sender number
        msg.sender = AT.decodesender(sender, ntype)
     
        -- timestamp
        offset = offset + len + 13
        timestamp = pdu:sub(offset, offset + 13)
        timestamp = AT.decodeswapped(timestamp)
     
        msg.timestamp = AT.decodetime(timestamp)
     
        -- message
        len = tonumber(pdu:sub(offset + 14, offset + 15), 16)
        data = pdu:sub(offset + 16)
        msg.data = AT.decode7bit(data, len)
     
        return msg
      end,
      -- decode sender address depending on source type
      decodesender = function(sender, ntype)
        if ntype == 5 then
          return AT.decode7bit(sender)
        else
          return AT.decodeswapped(sender)
        end
      end,
      -- decode time in sms pdu
      decodetime = function(timestamp)
        local offset, year, time
     
        offset = tonumber(timestamp:sub(13, 14)) or 0
        offset = offset * 15 * 60
     
        year = tonumber(timestamp:sub(1, 2))
     
        time = os.time({
          year = year < 70 and (2000 + year) or (1900 + year),
          month = tonumber(timestamp:sub(3, 4)),
          day = tonumber(timestamp:sub(5, 6)),
          hour = tonumber(timestamp:sub(7, 8)),
          min = tonumber(timestamp:sub(9, 10)),
          sec = tonumber(timestamp:sub(11, 12))
        }) or os.time()
     
        return time
      end,
      -- convert swapped number to normal
      decodeswapped = function(data)
        local i, nr, len, buf
        buf = {}
     
        -- real byte length
        len = math.floor(data:len() / 2)
        -- read 2 bytes at once
        for i = 1, len do
          -- convert low byte to number
          nr = tonumber(data:sub(i * 2, i * 2))
          if nr then
            table.insert(buf, tostring(nr))
          end
     
          -- convert high byte to number
          nr = tonumber(data:sub(i * 2 - 1, i * 2 - 1))
          if nr then
            table.insert(buf, tostring(nr))
          end
        end
     
        return table.concat(buf)
      end,
      -- convert from 7 bit char to 8 bit
      from7bit = function(c)
        if c < 128 then
          return string.char(AT.alphabet[ c + 1 ])
        else
          return ' '
        end
      end,
      -- converts from 7 bit to 8 bit
      decode7bit = function(data, len)
        local i, o, byte, prev, curr, mask, buf, res
     
        -- convert to binary string
        data = lmcore.hextostr(data, true)
     
        -- init vars
        o = 0
        prev = 0
        buf = {}
     
        for i = 1, data:len() do
          byte = data:byte(i, i)
     
          -- get 7 bit data
          mask = bit.lshift(1, 7 - o) - 1
     
          -- get current chunk
          curr = bit.band(byte, mask)
          curr = bit.lshift(curr, o)
          curr = bit.bor(curr, prev)
     
          -- save bit chunk
          prev = bit.rshift(byte, 7 - o)
     
          -- add to buffer
          table.insert(buf, AT.from7bit(curr))
     
          -- every 7th step prev will have a full char
          if o == 6 then
            table.insert(buf, AT.from7bit(prev))
            prev = 0
          end
     
          o = (o + 1) % 7
        end
     
        -- catch last char in buffer
        if prev > 0 then
          table.insert(buf, AT.from7bit(prev))
        end
     
        -- flatten buffer
        res = table.concat(buf)
        if len then
          res = res:sub(1, len)
        end
     
        return res
      end
    }
     
    function AT:init(port)
      require('serial')
     
      local n, err
     
      n = setmetatable({}, { __index = AT })
      -- open serial connection
      n.port, err = serial.open(port)
     
      -- port open error
      if err then
        return nil, err
      end
     
      -- create empty read buffer
      n.buffer = {}
      return n
    end
     
    -- read single line from port
    function AT:read(timeout)
      local char, err, timeout, deftimeout, line
     
      -- default timeout is 1 second, converted to 0.1 sec ticks
      timeout = tonumber(timeout) or 1
      timeout = timeout * 10
     
      deftimeout = timeout
     
      -- read until got one line or timeout occured
      while timeout > 0 do
        -- read 1 char
        char, err = self.port:read(1, 0.1)
     
        -- got data
        if char then
          -- got LF, end of line
          if char == '\n' then
            -- convert to string and empty buffer
            line = table.concat(self.buffer)
            self.buffer = {}
            line = line:trim()
     
            -- return only lines with data
            if #line > 0 then
              return line
            -- reset timeout
            else
              timeout = deftimeout
            end
          -- ignore CR
          elseif char ~= '\r' then
            table.insert(self.buffer, char)
          end
        -- read timeout
        elseif err == 'timeout' then
          timeout = timeout - 1
        -- other error
        else
          break
        end
      end
     
      print('error', err)
      return nil, err
    end
     
    -- blocking read until cmd is received
    function AT:readuntil(cmd, timeout)
      local line, err
      timeout = timeout or 5
     
      while timeout > 0 do
        line, err = self:read()
     
        -- read line ok
        if line then
          if line == cmd or line == 'COMMAND NOT SUPPORT' or line:match('ERROR') then
            return line
          else
            timeout = timeout - 1
            err = 'invalid line'
          end
        -- timeout
        elseif err == 'timeout' then
          timeout = timeout - 1
        -- other error
        else
          break
        end
      end
     
      return nil, err
    end
     
    -- send command to terminal
    function AT:send(cmd)
      local res, err = self.port:write(cmd .. '\r\n')
     
      -- write ok, get local echo
      if res then
        res, err = self:readuntil(cmd)
        self:read()
      end
     
      return res, err
    end
     
    -- main handler
    function AT:run()
      local res, err, cmd, pos, sms
     
      res, err = self:read()
     
      -- check for incoming command
      if type(res) ~= 'string' or res:sub(1, 1) ~= '+' then
        return
      end
     
      pos = res:find(':', 1, true)
     
      if not pos then
        return
      end
     
      -- get command type
      cmd = res:sub(2, pos - 1)
     
      -- check only for incoming sms
      if cmd ~= 'CMTI' then
        return
      end
     
      -- read from sim
      sms = self:incsms(res)
     
      -- sms seems to be valid, pass to handler if specified
      if sms and self.smshandler then
        self.smshandler(sms)
      end
    end
     
    -- incoming sms handler
    function AT:incsms(res)
      local chunks, index, sms
     
      -- get message index from result
      chunks = res:split(',')
      if #chunks == 2 then
        -- get index and read from it
        index = tonumber(chunks[ 2 ])
        sms = self:readsms(index)
        -- delete sms from store
        self:deletesms(index)
      end
     
      return sms
    end
     
    -- delete sms at index
    function AT:deletesms(index)
      local cmd, res
     
      -- send delete request
      cmd = 'AT+CMGD=' .. index
      res = self:send(cmd)
     
      return res
    end
     
    -- read sms at index
    function AT:readsms(index)
      local cmd, res, sms
     
      -- send read request
      cmd = 'AT+CMGR=' .. index
      res = self:send(cmd)
     
      -- no message at then index
      if res == 'OK' then
        return nil, 'not found'
      end
     
      -- read sms pdu and try decoding
      sms = self:read()
      res, sms = pcall(AT.parsepdu, sms)
     
      -- decode failed
      if not res then
        return nil, sms
      end
     
      -- wait for ok from modem
      self:readuntil('OK')
     
      return sms
    end
     
    function AT:sendsms(number, message)
      local cmd, res
     
      -- switch to text mode
      self:send('AT+CMGF=1')
     
      -- set number
      cmd = string.format('AT+CMGS="%s"', number)
      res = self:send(cmd)
     
      -- number seems to be valid
      if res ~= 'ERROR' then
        -- message and CTRL+Z
        self.port:write(message .. string.char(0x1A))
        res = self:readuntil('OK')
      end
     
      -- switch back to pdu mode
      self:send('AT+CMGF=0')
     
      return res
    end
     
    -- set sms handler
    function AT:setsmshandler(fn)
      if type(fn) == 'function' then
        self.smshandler = fn
      end
    end
     
    table.contains = function(t, v)
      for _, i in pairs(t) do
        if i == v then
          return true
        end
      end
    end
     
    function sendsms(number, message)
      require('socket')
      client = socket.udp()
      client:sendto(number .. ' ' .. message, '127.0.0.1', 12535)
    end

    Шаг 2 – Добавить следующий код в Скрипты – > Скрипт запуска системы (Scripting -> Start-up (unit) script) (скрипты, выполняемые при загрузке Logic Machine)>

    os.execute('echo 1 > /sys/bus/platform/devices/ci_hdrc.0/force_full_speed')
    os.execute('echo 1 > /sys/bus/platform/devices/ci_hdrc.1/force_full_speed')
    os.execute('usbreset /dev/bus/usb/001/001')

    Шаг 3 – Добавить в резидентный скрипт (интервал запуска 0) подключить библио:

    Необходимые скрипты:

    -- init
    if not numbers then
      require('user.sms')
      require('json')
      require('socket')
     
      -- allowed numbers, SMS from other numbers will be ignored
      numbers = { '12345678' }
     
      -- port number depends on modem model
      comport = 'ttyUSB1'
     
      -- if SIM PIN is enabled, uncomment the line below and replace 0000 with SIM PIN
      -- pincode = '0000'
     
      -- command parser
      parser = function(cmd, sender)
        local find, pos, name, mode, offset, value, dvalue, obj, message
     
        cmd = cmd:trim()
        mode = cmd:sub(1, 1):upper()
     
        -- invalid request
        if mode ~= 'W' and mode ~= 'R' then
          return
        end
     
        cmd = cmd:sub(3):trim()
     
        -- parse object name/address
        find = cmd:sub(1, 1) == '"' and '"' or ' '
        offset = find == '"' and 1 or 0
     
        -- pad with space when in read mode
        if mode == 'R' and find == ' ' then
          cmd = cmd .. ' '
        end
     
        -- find object name
        pos = cmd:find(find, 1 + offset, true)
     
        -- name end not found, stop
        if not pos then
          return
        end
     
        -- get name part
        name = cmd:sub(1 + offset, pos - offset):trim()
     
        if mode == 'W' then
          value = cmd:sub(pos + offset):trim()
     
          if #value > 0 then
            -- try decoding value
            dvalue = json.pdecode(value)
            if dvalue ~= nil then
              value = dvalue
            end
     
            -- send to bus
            grp.write(name, value)
          end
        -- read request
        else
          obj = grp.find(name)
     
          -- object not known
          if not obj then
            return
          end
     
          -- send read request and wait for an update
          obj:read()
          os.sleep(1)
     
          -- read new value
          value = grp.getvalue(name)
     
          -- got no value
          if value == nil then
            return
          end
     
          -- add object name if specified
          if obj.name then
            name = string.format('%s (%s)', obj.name, obj.address)
          end
     
          message = string.format('Value of %s is %s', name, json.encode(value))
          modem:sendsms('+' .. sender, message)
        end
      end
     
      -- incoming sms handler
      handler = function(sms)
        alert('incoming sms: [%s] %s', tostring(sms.sender), tostring(sms.data))
     
        -- sms from known number, call parser
        if table.contains(numbers, sms.sender) then
          parser(sms.data, sms.sender)
        end
      end
     
      -- check local udp server for messages to send
      udphandler = function(server)
        -- check for local sms to send
        local msg = server:receive()
        -- got no message
        if not msg then
          return
        end
     
        -- split into number and message
        local sep = msg:find(' ')
        if not sep then
          return
        end
     
        alert('sending sms: ' .. msg)
        modem:sendsms(msg:sub(1, sep - 1), msg:sub(sep + 1))
      end
    end
     
    -- handle data from modem
    if modem then
      modem:run()
      udphandler(server)
    -- modem init
    else
      alert('SMS handler init')
      -- wait for usb reset after reboot
      os.sleep(15)
     
      -- open serial port
      modem = AT:init('/dev/' .. comport)
     
      -- init ok
      if modem then
        -- set sms handler
        modem:setsmshandler(handler)
     
        -- send pin if set
        if pincode then
          modem:send('AT+CPIN=' .. pincode)
          modem:read()
        end
     
        -- set to pdu mode
        modem:send('AT+CMGF=0')
        -- enable sms notifications
        modem:send('AT+CNMI=1,1,0,0,0')
        -- fixup encoding
        modem:send('AT+CSCS="GSM"')
     
        -- local udp server for sending sms
        server = socket.udp()
        server:setsockname('127.0.0.1', 12535)
        server:settimeout(0.1)
     
        alert('SMS handler started')
      -- init failed
      else
        alert('SMS USB init failed')
      end
    end

    Порт необходимо поменять на тот, под которым определился модем. Увидеть это можно в System configuration — > Status -> System status.

    Примеры:
    Отправка смс с Logic Machine на телефон:

    sendsms('1234567890','test sms')

    Отправка смс с телефона на Logic Machine:
    Синтаксис команд
    Запись на шину:

    • W ALIAS VALUE

    Чтение с шины:

    • R ALIAS
    • По запросу скрипт посылает SMS сообщение, которое содержит текущее значение выбранного объекта

    В качестве ALIAS могут быть использованы такие параметры, как:

    • Групповой адрес (например, 1/1/1)
    • Имя (например, Obj1). Если имя содержит пробелы тогда его следует поместить двойные кавычки (например, “Room Temperature”)

    Примечание!

    • Имя объекта и его тип задаются на вкладке Logic Machine -> Объекты (LogicMachine -> Objects), иначе скрипт не сможет прочитать или записать в объект
    • Поддерживаются только ASCII символы (русский язык не поддерживается)

    Примеры:
    Установка логической переменной (посылаем SMS чтобы включить свет на кухне):

    • W 1/1/1 true

    Установка scale переменной (посылаем SMS, чтобы задать уровень яркости красного цвета LED светильника, равный 67%):

    • W LED1Red 67

    Установка температуры (floating point) (посылаем SMS для установки уровня температуры в гостиной, равного 22.5 градусов):

    • W “Room Setpoint” 22.5

    Получение значения (посылаем SMS для чтения значения из охранной панели по адресу 2/1/1):

    • R 2/1/1



    Copyright
    © Embedded Systems Rus
    2017. All Rights Reserved