Горячая линия Embedded System Rus:8-800-775-06-34 (звонок по России бесплатный)

LM5_N
LM-Wall_N
DALI_N
Vita_N

Управление устройствами 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')
    require('json')
    local packet = {}
    packet.type = 'text'
    packet.message = message
    packet.number = number
    packet = json.encode(packet)
    client = socket.udp()
    client:sendto(packet, '127.0.0.1', 12535)
end
function LuaToUCS2(text)
    local newTable = {}
    for i = 1, text:len() do
        local code = text:byte(i)
        -- Остальные символы
        if code < 128 then
            -- Первый символ всегда 00 или 0
            table.insert(newTable, string.format('%02X', 0))
            table.insert(newTable, string.format('%02X', code))
            -- Русские символы
        elseif code == 208 or code == 209 then
            local nextChar = text:byte(i + 1)
            if nextChar then
                -- Первый символ всегда 04 или 4
                table.insert(newTable, string.format('%02X', 4))
                -- ё
                if code == 209 and nextChar == 145 then
                    table.insert(newTable, 51)
                    -- Ё
                elseif code == 208 and nextChar == 129 then
                    table.insert(newTable, string.format('%02X', 1))
                    -- А-Я
                elseif nextChar >= 144 and nextChar <= 191 then
                    table.insert(newTable,
                                 string.format('%X', 0x10 + nextChar - 144))
                    -- а-я
                elseif nextChar >= 128 and nextChar <= 143 then
                    table.insert(newTable,
                                 string.format('%X', 0x40 + nextChar - 128))
                end
            end
        end
    end
    return newTable
end
function pduStringGen(number, text)
    -- Номер
    number = number:sub(2, -1) .. 'F'
    local pduString = '0011000B91'
    for i = 1, number:len() - 1, 2 do
        pduString = pduString .. number:sub(i + 1, i + 1) .. number:sub(i, i)
    end
    local mesLen = #(text:gsub('[\128-\191]','')) * 2
    if mesLen > 140 then mesLen = 140 end
    --mesLen = math.floor(mesLen / 2 - 1)
    -- Вообще последнее - "время действия" сообщения, но я не знаю, какое значение указывать
    pduString = pduString .. '00' .. '08' .. '00'
    .. string.format('%02X', mesLen)
    -- Преобразование текста сообщения в ucs2
    UCS2_text = LuaToUCS2(text)
    for index, value in ipairs(UCS2_text) do
        pduString = pduString .. value
        if index > 140 then
            break
        end
    end
    return pduString
end
function AT:sendsmspdu(number, message)
    local cmd, res
    pduString = pduStringGen(number, message)
    cmd = 'AT+CMGS=' .. pduString:len() / 2 - 1
    self:send(cmd)
    self:send('')
    self.port:write(pduString .. string.char(0x1a))
    return
end
function sendsmspdu(number, message)
    require('socket')
    require('json')
    local packet = {}
    packet.type = 'pdu'
    packet.message = message
    packet.number = number
    packet = json.encode(packet)
    client = socket.udp()
    client:sendto(packet, '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
        -- decode json
        local msg = json.pdecode(msg)
        if not msg then return end
        if msg.type == 'text' then
            log('sending sms: ', msg)
            modem:sendsms(msg.number, msg.message)
        elseif msg.type == 'pdu' then
            log('sendins sms: ', msg)
            modem:sendsmspdu(msg.number, msg.message)
        end
    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') -- отправляется до 160 символов

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

sendsmspdu('+79991234567','тест смс') -- отправляется до 70 символов

Отправка смс с телефона на 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