31.01.2022, 16:05
(31.01.2022, 15:30)admin Wrote: This script has nothing to do with the SMS sending via modem it simply sends message to the resident script. Post your full resident SMS script and a screenshot from System config > Status > System status > Serial ports.
resident sms
Code:
-- init
if not numbers then
require('user.sms')
require('json')
require('socket')
-- allowed numbers, SMS from other numbers will be ignored
numbers = { '3332958126' }
-- port number depends on modem model
comport = 'ttyACM3'
doreset = false -- set to true if USB reset is required before starting any communication
-- 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
if modem:run() then
udphandler(server)
else
--alert('SMS handler lost connection')
modem:reinit()
end
-- modem init
else
--alert('SMS handler init')
-- open serial port
modem = AT:init('/dev/' .. comport, doreset)
-- 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"')
-- delete all saved messages
modem:send('AT+CMGD=1,4')
-- 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
-- init
if not numbers then
require('user.sms')
require('json')
require('socket')
-- allowed numbers, SMS from other numbers will be ignored
numbers = { '3332958126' }
-- port number depends on modem model
comport = 'ttyACM3'
doreset = false -- set to true if USB reset is required before starting any communication
-- 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
if modem:run() then
udphandler(server)
else
--alert('SMS handler lost connection')
modem:reinit()
end
-- modem init
else
--alert('SMS handler init')
-- open serial port
modem = AT:init('/dev/' .. comport, doreset)
-- 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"')
-- delete all saved messages
modem:send('AT+CMGD=1,4')
-- 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
Code:
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(dev, reset)
require('serial')
local t = setmetatable({}, { __index = AT })
t.dev = dev
while true do
if t:reinit(30, reset) then
break
else
if reset then
t:reset()
else
return nil
end
end
end
return t
end
function AT:open(timeout)
local port
while true do
if io.exists(self.dev) then
port = serial.open(self.dev)
if port then
break
end
end
if timeout then
timeout = timeout - 1
if timeout <= 0 then
return false
end
end
os.sleep(1)
end
self.port = port
self.buffer = {}
return true
end
function AT:reset()
--alert('modem reset')
os.execute('usbreset /dev/bus/usb/001/001')
end
function AT:reinit(timeout, reset)
local res
if reset then
self:reset()
end
res = self:open(timeout)
self.buffer = {}
return res
end
function AT:close()
if self.port then
self.port:close()
self.port = nil
end
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()
if err then
return err == 'timeout'
end
-- check for incoming command
if res:sub(1, 1) ~= '+' then
return true
end
pos = res:find(':', 1, true)
if not pos then
return true
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
return true
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