Эта статья о моем опыте сотрудничества с DeepSeek в разработке некоторых поделок на различных языках программирования. Раньше писал на этих языках, но без помощи ИИ.
Поделка 1:
Решил разработать систему авторизованного доступа в здание с использованием телефона.
Для этого собрал модем , состоящий из 3-х деталей: адаптер USB-UART, модуль SIM800L и корпус.
Получилось вот такое устройство:Внеш
Программу решил написать на Lua.
Для этого, не в первый раз, обращаюсь к DeepSeek(DS) с просьбой разработать скрипт, который будет выполнять следующие функции:
1) Подключаться через виртуальный com порт к модему;
2) Контролировать подключение к сети оператора связи;
3) Проверять уровень сигнала GSM;
4) При звонках:
4.1), определять номер звонящего;
4.2) записывать номер в файл регистрации и в таблицу;
5) При SMS:
5.1) Принимать SMS содержащие команды и параметры. Формат команды есть в тексте скрипта.
Далее процесс свелся к следующему.
DS пишет скрипт.
Я его запускаю и сообщаю ему результат.
Если есть ошибки, то DS объясняет их и исправляет.
Если у меня есть идея, как изменить или расширить функции кода, то сообщаю об этом DS.
Не всегда DS соглашается с моими идеями. В этом случае он объясняет, почему нет смысла в реализации этой идеи.
Если же нахожу его ошибки и указываю на них, то он соглашается и исправляет.
Когда DS исправляет ошибки, то он начинает улучшать и расширять скрипт. Порою это приводит к тому, что перестают работать те функции, которые работали раньше. В этом случае, предлагаю ему вернуться к предыдущему варианту.
В результате совместной аппаратно-программной разработке, получили работающее решение за 1 день.
Результирующий код:
-- GSM модем контроллер - КОМАНДЫ С БУКВЕННЫМИ ПАРАМЕТРАМИ
-- Формат: "КОМАНДА[БУКВА][ЗНАЧЕНИЕ]..."
-- Примеры:
-- "1" - команда 1 без параметров
-- "1t235" - команда 1 с параметром t=235
-- "2r1" - команда 2 с параметром r=1
-- "3t235h67" - команда 3 с параметрами t=235 и h=67
-- "1t235,2r1,3t235h67" - несколько команд через разделитель
os.execute("chcp 65001 > nul")
local port = nil
local buffer = ""
-- Таблицы для хранения данных
local calls = {} -- входящие звонки
local incoming_commands = {} -- входящие команды из SMS
local outgoing_messages = {} -- отправленные SMS
-- Функция задержки для Windows
function sleep(seconds)
local start = os.clock()
while os.clock() - start < seconds do
-- просто ждем
end
end
-- Основная функция отправки команд
function sendCommand(cmd, timeout)
timeout = timeout or 3
-- Очищаем входной буфер
local flush = ""
repeat
flush = port:read(1)
until not flush
-- Отправляем команду
port:write(cmd .. "rn")
port:flush()
-- Собираем ответ
local response = ""
local start = os.clock()
local lastDataTime = start
while os.clock() - start < timeout do
-- Пробуем прочитать символ
local char = port:read(1)
if char then
response = response .. char
lastDataTime = os.clock()
-- Для отладки (можно закомментировать)
-- print(string.format("0x%02X (%s)", char:byte(), char))
-- Проверяем окончание ответа
if response:find("OKrn") or
response:find("ERRORrn") or
response:find("NO CARRIERrn") or
response:find("RINGrn") then
break
end
else
-- Если нет данных и прошло больше 0.2 сек после последних данных
if #response > 0 and os.clock() - lastDataTime > 0.2 then
break
end
end
end
return response
end
-- Функция для отправки команды спящему модему
function sendToSleepingModem(cmd, timeout)
timeout = timeout or 3
-- ШАГ 1: Отправляем пустую строку или AT для пробуждения
port:write("rn") -- Просто перевод строки для пробуждения
port:flush()
-- Ждем 300 мс (используем нашу функцию sleep)
local wakeStart = os.clock()
while os.clock() - wakeStart < 0.3 do
-- просто ждем
end
-- ШАГ 2: Очищаем буфер от мусора пробуждения
local garbage = ""
local cleanStart = os.clock()
while os.clock() - cleanStart < 0.2 do
local char = port:read(1)
if char then
garbage = garbage .. char
else
break
end
end
if #garbage > 0 then
print("Пробуждение мусор:", garbage)
end
-- ШАГ 3: Отправляем реальную команду
return sendCommand(cmd, timeout)
end
-- Упрощенная версия для вашего случая
function wakeAndSend(cmd, timeout)
timeout = timeout or 3
-- Отправляем первую команду (она разбудит модем, но ответа не будет)
port:write("ATrn")
port:flush()
-- Ждем 500 мс на пробуждение
local t = os.clock()
while os.clock() - t < 0.5 do end
-- Теперь отправляем реальную команду
return sendCommand(cmd, timeout)
end
-- Открытие порта
local com = "com4"
function openPort()
for attempt = 1, 3 do
os.execute('mode ' .. com .. ': baud=115200 parity=n data=8 stop=1')
local portPath = "\\.\" .. com
port = io.open(portPath, "w+b")
if port then
port:setvbuf("no")
print("Порт открыт")
return true
end
print("Попытка " .. attempt .. " не удалась, ждем 2 сек...")
sleep(2000)
end
print("Ошибка: не удалось открыть порт")
return false
end
function sleep(ms)
local start = os.clock()
while os.clock() - start < ms / 1000 do end
end
function wrcmd(cmd)
port:write(cmd .. "rn") port:flush()
end
-- Отправка команды
function sendCommand(cmd, timeout)
timeout = timeout or 2
port:write(cmd .. "rn");-- port:flush()
local response = ""
local start = os.clock()
while os.clock() - start < timeout do
local char = port:read(1)
if char then
response = response .. char
if response:find("OKrn") or response:find("ERRORrn") then break end
end
end
return response
end
-- Очистка номера телефона
function cleanPhoneNumber(phone)
if not phone then return nil end
local cleaned = phone:gsub("[^%+%d]", "")
if cleaned:match("^8%d%d%d") then
cleaned = "+7" .. cleaned:sub(2)
end
if #cleaned >= 10 and #cleaned <= 15 then
return cleaned
end
return nil
end
-- Извлечение номера из строки CLIP
function extractPhoneFromCLIP(line)
local phone = line:match('CLIP: "([^"]+)"')
if phone then
return cleanPhoneNumber(phone)
end
return nil
end
-- ============================================
-- ПАРСИНГ КОМАНД С БУКВЕННЫМИ ПАРАМЕТРАМИ
-- ============================================
-- Парсинг одной команды с буквенными параметрами
function parseSingleCommand(cmd_str)
if not cmd_str or cmd_str == "" then return nil end
-- Ищем команду (первые цифры)
local cmd_num = cmd_str:match("^(%d+)")
if not cmd_num then return nil end
local cmd = tonumber(cmd_num)
local rest = cmd_str:sub(#cmd_num + 1)
local params = {}
-- Разбираем буквенные параметры
if rest and #rest > 0 then
-- Ищем все паттерны "буква + число"
for letter, value in rest:gmatch("([a-zA-Z])(%d+)") do
params[letter] = tonumber(value)
end
end
return {
cmd = cmd,
params = params,
raw = cmd_str
}
end
-- Разбор SMS с несколькими командами
function parseCommands(text)
if not text or text == "" then return {} end
local commands = {}
-- Разделяем по любым не-цифробуквенным символам
-- (запятые, пробелы, точки с запятой и т.д.)
for part in text:gmatch("[^,%s;:]+") do
if part ~= "" then
local cmd_data = parseSingleCommand(part)
if cmd_data then
table.insert(commands, cmd_data)
end
end
end
return commands
end
-- Вывод команды в читаемом виде
function dumpCommand(cmd_data)
local str = string.format("Команда %d", cmd_data.cmd)
if next(cmd_data.params) then
local params = {}
for letter, value in pairs(cmd_data.params) do
table.insert(params, string.format("%s=%d", letter, value))
end
str = str .. " [" .. table.concat(params, ", ") .. "]"
end
return str
end
-- Обработка нескольких команд
function processCommands(phone, commands)
print(string.format(" Получено %d команд", #commands))
local responses = {}
for idx, cmd_data in ipairs(commands) do
print(" " .. dumpCommand(cmd_data))
local response = nil
-- Команда 1: запрос статуса
if cmd_data.cmd == 1 then
response = "101"
-- Команда 2: управление реле
elseif cmd_data.cmd == 2 then
if cmd_data.params['r'] then
if cmd_data.params['r'] == 1 then
relayOn()
response = "102r1"
elseif cmd_data.params['r'] == 0 then
relayOff()
response = "102r0"
end
end
-- Команда 3: данные с датчиков
elseif cmd_data.cmd == 3 then
local temp = readTemperature()
local humidity = readHumidity()
local pressure = readPressure()
-- Формируем ответ с буквенными параметрами
response = "103"
if temp then response = response .. "t" .. temp end
if humidity then response = response .. "h" .. humidity end
if pressure then response = response .. "p" .. pressure end
-- Команда 4: установка пара��етров
elseif cmd_data.cmd == 4 then
if cmd_data.params['t'] then
setTimer(cmd_data.params['t'])
end
if cmd_data.params['d'] then
setDelay(cmd_data.params['d'])
end
response = "104"
if cmd_data.params['t'] then response = response .. "t" .. cmd_data.params['t'] end
if cmd_data.params['d'] then response = response .. "d" .. cmd_data.params['d'] end
-- Команда 5: мульти-запрос
elseif cmd_data.cmd == 5 then
local temp = readTemperature()
local status = getSystemStatus()
response = "105t" .. temp .. "s" .. status
end
if response then
table.insert(responses, response)
end
end
-- Отправляем все ответы одним SMS
if #responses > 0 then
local answer = table.concat(responses, ",")
sendSMS(phone, answer)
end
end
-- SMS ФУНКЦИИ
-- Отправка SMS
function sendSMS(phone, text)
phone = cleanPhoneNumber(phone)
if not phone then
print("Ошибка: неверный формат номера")
return false
end
print("Отправка SMS на " .. phone .. ": " .. text)
sendCommand("AT+CMGF=1", 1)
sleep(100)
local resp = sendCommand('AT+CMGS="' .. phone .. '"', 2)
if resp:find(">") then
port:write(text .. "26")
port:flush()
sleep(2000)
local final_resp = ""
local start = os.clock()
while os.clock() - start < 5 do
local char = port:read(1)
if char then
final_resp = final_resp .. char
if final_resp:find("OK") or final_resp:find("ERROR") then
break
end
end
end
if final_resp:find("OK") then
print("? SMS отправлена")
-- Сохраняем в историю
local msg = {
id = #outgoing_messages + 1,
phone = phone,
text = text,
date = os.date("%Y-%m-%d"),
time = os.date("%H:%M:%S"),
timestamp = os.time()
}
table.insert(outgoing_messages, msg)
-- Лог
local today = os.date("%Y-%m-%d")
local f = io.open("sent_sms_" .. today .. ".log", "a")
if f then
f:write(string.format("[%s] КОМУ: %sn", os.date("%H:%M:%S"), phone))
f:write(" ТЕКСТ: " .. text .. "n")
f:write(" " .. string.rep("-", 50) .. "n")
f:close()
end
saveToFile()
return true
else
print("Ошибка отправки: " .. final_resp)
return false
end
else
print("Ошибка: модем не ответил символом >")
return false
end
end
-- Чтение SMS
function readSMS(index)
local resp = sendCommand('AT+CMGR=' .. index, 3)
local lines = {}
for line in resp:gmatch("[^rn]+") do
table.insert(lines, line)
end
if #lines < 2 then
return nil, nil
end
local header = lines[1]
local phone = header:match('"+([%+%d]+)"')
local date = header:match(',,"([^"]+)"') or header:match('","([^"]+)"$')
local text = ""
for i = 2, #lines do
if lines[i] ~= "OK" and lines[i] ~= "" then
text = lines[i]
break
end
end
return phone, text, date
end
-- Удаление SMS
function deleteSMS(index)
sendCommand('AT+CMGD=' .. index, 1)
end
-- Функция проверки уровня сигнала
function getCSQ()
-- Отправляем команду несколько раз, пока не получим правильный ответ
for i = 1, 3 do
local resp = sendCommand("AT+CSQ", 2)
-- Проверяем, что это действительно ответ на CSQ
if resp:find("+CSQ:") then
local _, _, rssi = resp:find("+CSQ: (%d+),")
if rssi then
print("Уровень сигнала:" .. rssi)
return tonumber(rssi)
end
else
-- print("Попытка " .. i .. ": получен мусор, очищаем буфер...")
-- Очищаем буфер
local start = os.clock()
while os.clock() - start < 0.3 do
port:read(1)
end
end
end
print("Не удалось получить CSQ после 3 попыток")
return nil
end
-- Функция ожидания регистрации в сети
function waitForNetwork(maxWaitTime)
maxWaitTime = maxWaitTime or 30 -- Ждем до 30 секунд
print("Ожидание регистрации в сети (до " .. maxWaitTime .. " сек)...")
local startTime = os.clock()
while os.clock() - startTime < maxWaitTime do
local resp = sendCommand("AT+CREG?", 3)
-- Ищем паттерн +CREG: 0,1 (регистрация в домашней сети)
-- или +CREG: 0,5 (регистрация в роуминге)
if resp:find("+CREG: 0,[15]") then
print("Модем зарегистрирован в сети!")
-- Узнаем оператора
local opResp = sendCommand("AT+COPS?", 3)
local _, _, operator = opResp:find('"+COPS: %d+,%d+,"([^"]+)"')
if operator then
print("Оператор: " .. operator)
end
return true
end
-- Проверяем другие статусы
if resp:find("+CREG: 0,2") then
print(" Поиск сети...")
elseif resp:find("+CREG: 0,3") then
print(" Регистрация отклонена")
elseif resp:find("+CREG: 0,4") then
print(" Неизвестно")
end
sleep(2) -- Проверяем каждые 2 секунды
end
print("Таймаут ожидания сети")
return false
end
function getSystemStatus()
return 1 -- 1 = OK, 0 = Error
end
function relayOn()
print(" Реле ВКЛ")
-- Здесь код управления реле
end
function relayOff()
print(" Реле ВЫКЛ")
-- Здесь код управления реле
end
function readTemperature()
return 235 -- 23.5°C * 10
end
function readHumidity()
return 67 -- 67%
end
function readPressure()
return 1013 -- 1013 гПа
end
function setTimer(seconds)
print(" Таймер установлен на " .. seconds .. " сек")
end
function setDelay(seconds)
print(" Задержка установлена на " .. seconds .. " сек")
end
-- Сохранение звонка
function saveCall(phoneNumber)
local call = {
id = #calls + 1,
phone = phoneNumber,
date = os.date("%Y-%m-%d"),
time = os.date("%H:%M:%S"),
timestamp = os.time()
}
table.insert(calls, call)
local today = os.date("%Y-%m-%d")
local f = io.open("calls_" .. today .. ".log", "a")
if f then
f:write(string.format("[%s] ЗВОНОК с %sn", os.date("%H:%M:%S"), phoneNumber))
f:close()
end
print(" Звонок сохранен и сброшен")
saveToFile()
return call
end
-- Сохранение входящих команд
function saveIncomingCommands(phone, raw_text, commands)
local record = {
id = #incoming_commands + 1,
phone = phone,
raw = raw_text,
commands = commands,
date = os.date("%Y-%m-%d"),
time = os.date("%H:%M:%S"),
timestamp = os.time()
}
table.insert(incoming_commands, record)
local today = os.date("%Y-%m-%d")
local f = io.open("commands_" .. today .. ".log", "a")
if f then
f:write(string.format("[%s] ОТ: %sn", os.date("%H:%M:%S"), phone))
f:write(" ТЕКСТ: " .. raw_text .. "n")
f:write(" КОМАНДЫ:n")
for _, cmd in ipairs(commands) do
f:write(" " .. dumpCommand(cmd) .. "n")
end
f:write(" " .. string.rep("-", 50) .. "n")
f:close()
end
saveToFile()
return record
end
-- Сохранение в файл
function saveToFile()
local f = io.open("modem_data.lua", "w")
if f then
f:write("-- Данные модемаnn")
f:write("calls = {n")
for _, call in ipairs(calls) do
f:write(string.format(' {id=%d, phone="%s", date="%s", time="%s", timestamp=%d},n',
call.id, call.phone, call.date, call.time, call.timestamp))
end
f:write("}nn")
f:write("incoming_commands = {n")
for _, cmd in ipairs(incoming_commands) do
f:write(string.format(' {id=%d, phone="%s", raw="%s", date="%s", time="%s", timestamp=%d},n',
cmd.id, cmd.phone, cmd.raw:gsub('"', '\"'), cmd.date, cmd.time, cmd.timestamp))
end
f:write("}nn")
f:write("outgoing_messages = {n")
for _, msg in ipairs(outgoing_messages) do
local safeText = msg.text:gsub('"', '\"')
f:write(string.format(' {id=%d, phone="%s", text="%s", date="%s", time="%s", timestamp=%d},n',
msg.id, msg.phone, safeText, msg.date, msg.time, msg.timestamp))
end
f:write("}n")
f:close()
end
end
-- Загрузка из файла
function loadTables()
local f = io.open("modem_data.lua", "r")
if f then
f:close()
dofile("modem_data.lua")
calls = calls or {}
incoming_commands = incoming_commands or {}
outgoing_messages = outgoing_messages or {}
print(string.format("Загружено: звонков %d, команд %d, исх SMS %d",
#calls, #incoming_commands, #outgoing_messages))
else
print("Новый файл данных")
calls = {}
incoming_commands = {}
outgoing_messages = {}
end
end
-- ИНИЦИАЛИЗАЦИЯ
-- САМОЕ ПРОСТОЕ РЕШЕНИЕ: просто отправляем CSQ несколько раз
function getCSQ_simple()
-- print("Получение уровня сигнала...")
for i = 1, 3 do
local resp = sendCommand("AT+CSQ", 2)
-- Проверяем, содержит ли ответ "+CSQ:"
if resp and resp:find("+CSQ:") then
local _, _, rssi = resp:find("+CSQ: (%d+),")
if rssi then
print("Уровень сигнала: " .. rssi)
return tonumber(rssi)
end
-- else
-- Если пришел не CSQ, просто логируем и продолжаем
-- print("Попытка " .. i .. ": получили '" .. resp .. "', пробуем снова...")
end
-- Пауза между попытками
local wait = os.clock()
while os.clock() - wait < 0.3 do end
end
print("Не удалось получить уровень сигнала")
return nil
end
function isNetworkReady()
local resp = sendCommand("AT+CREG?", 2)
-- true если зарегистрирован (0,1 или 0,5)
return resp:find("0,1") or resp:find("0,5") ~= nil
end
function init()
if not openPort() then return false end
port:write("rn") -- Просто перевод строки для пробуждения
port:flush()
-- Ждем 300 мс (используем нашу функцию sleep)
local wakeStart = os.clock() while os.clock() - wakeStart < 0.3 do end -- просто ждем
local s=sendCommand("AT",1);
-- wrcmd("ATE0"); --sendCommand("ATE0",1) --эхо выкл
if s=="" then print("Модем не отвечает") return false end
print("Модем отвечает:"..s)
wrcmd("ATE0"); --sendCommand("ATE0",1) --эхо выкл
waitForNetwork()
-- Использование
local rssi = getCSQ_simple()
if rssi and rssi >= 10 then print("Сигнал достаточный для работы") end
wrcmd("AT+IFC=0,0"); sendCommand("AT+IFC=0,0", 1)
sendCommand("AT+CMGF=1", 1) -- Текстовый режим для SMS
sendCommand("AT+CMGD=1,4", 2) -- print("Очистка памяти SIM...")
sendCommand("AT+CLIP=1", 1) --аон вкл print("Настройка определителя номера...") — включает отправку номера вызывающего абонента. Теперь после каждого входящего звонка модем будет автоматически присылать строку с номером .
sendCommand("AT+CMEE=1", 1) --— включает развернутые сообщения об ошибках. Полезно для отладки на случай, если что-то пойдет не так.
sendCommand("AT+CNMI=2,1", 1)
sendCommand("AT+CSCLK=2",1) --авто сон
print("Модем готов к командам с буквенными параметрами")
return true
end
-- ============================================
-- ГЛАВНЫЙ ЦИКЛ
-- ============================================
function run()
print("n" .. string.rep("=", 60))
print("GSM КОНТРОЛЛЕР - КОМАНДЫ С БУКВЕННЫМИ ПАРАМЕТРАМИ")
print("Формат: КОМАНДА[БУКВА][ЗНАЧЕНИЕ]...")
print("Примеры:")
print(" 1 - команда 1 без параметров")
print(" 1t235 - команда 1 с параметром t=235")
print(" 2r1 - команда 2 с параметром r=1")
print(" 3t235h67 - команда 3 с t=235 и h=67")
print(" 1t235,2r1,3t235h67 - несколько команд")
print(string.rep("=", 60))
local lastStats = os.clock()
local lastPhone = ""
local lastPhoneTime = 0
while true do
local char = port:read(1)
if char then
buffer = buffer .. char
if char == "n" then
local line = buffer:gsub("r", ""):gsub("n", "")
buffer = ""
-- Обработка звонков
local phone = extractPhoneFromCLIP(line)
if phone then
local currentTime = os.time()
if phone == lastPhone and (currentTime - lastPhoneTime) < 21 then
-- дубликат
else
port:write("rn") port:flush() -- Просто перевод строки для пробуждения
-- Ждем 100 мс (используем нашу функцию sleep)
local wakeStart = os.clock() while os.clock() - wakeStart < 0.1 do end -- просто ждем
wrcmd("ATH");
print("!!! ВХОДЯЩИЙ ЗВОНОК от " .. phone)
saveCall(phone)
lastPhone = phone
lastPhoneTime = currentTime
end
end
-- Обработка SMS
local idx = line:match('CMTI: "[^"]+",(%d+)')
if idx then
local num = tonumber(idx)
print("!!! ВХОДЯЩЕЕ SMS в ячейке " .. num)
sleep(200)
local phone, text, date = readSMS(num)
if phone and text then
print(" От: " .. phone)
print(" Текст: " .. text)
-- Парсим команды
local commands = parseCommands(text)
if #commands > 0 then
print(string.format(" Найдено команд: %d", #commands))
saveIncomingCommands(phone, text, commands)
processCommands(phone, commands)
else
print(" Нет распознанных команд")
end
deleteSMS(num)
end
end
end
else
if os.clock() - lastStats > 60 then
local s=sendCommand("AT+CSQ",1); --уровень сигнала
print("уровень сигнала ",s);
print(string.format("Звонков: %d, Команд: %d, Отправлено: %d",
#calls, #incoming_commands, #outgoing_messages))
lastStats = os.clock()
end
sleep(10)
end
end
end
-- ЗАПУСК
loadTables()
if init() then
print("n" .. string.rep("-", 60))
print("ТЕКУЩАЯ СТАТИСТИКА")
print(" Звонков: " .. #calls)
print(" Команд: " .. #incoming_commands)
print(" Отправлено SMS: " .. #outgoing_messages)
print(string.rep("-", 60) .. "n")
local ok, err = pcall(run)
if not ok then
print("Ошибка: " .. tostring(err))
end
end
if port then port:close() end
print("nПрограмма завершена")
print("Всего звонков: " .. #calls)
print("Всего команд: " .. #incoming_commands)
print("Всего отправлено SMS: " .. #outgoing_messages)
saveToFile()
Поделка 2:
Идея в том, чтобы заменить в Поделке 1 ПК на микроконтроллер ESP32-C3. В результате сделать автономное устройство, которое осуществляет авторизованный доступ в здание путем приема звонков с телефона и идентификацию звонящего по белому списку.
При этом программу решил написать на С.
Спросил мнение DS:


Поделка находится в стадии отладки, поэтому фрагменты кода для примера.
main/gsm.h
#ifndef GSM_H
#define GSM_H
#include <stdint.h>
#include <stdbool.h>
// Настройки SIM800L
#define GSM_UART_PORT UART_NUM_1
#define GSM_TX_PIN 17
#define GSM_RX_PIN 16
#define GSM_BAUD_RATE 115200
#define GSM_PWR_PIN 4
#define GSM_RST_PIN 5
// Буферы
#define GSM_RX_BUF_SIZE 1024
#define GSM_TX_BUF_SIZE 512
#define GSM_CMD_TIMEOUT_MS 5000
#define GSM_SMS_BUFFER_SIZE 256
// Результаты операций
typedef enum {
GSM_OK = 0,
GSM_ERROR,
GSM_TIMEOUT,
GSM_NO_SIM,
GSM_NO_SIGNAL
} gsm_status_t;
// Структура для SMS
typedef struct {
char phone[20];
char message[GSM_SMS_BUFFER_SIZE];
char datetime[32];
int index;
} sms_data_t;
// Инициализация
void gsm_init(void);
gsm_status_t gsm_power_on(void);
gsm_status_t gsm_check_communication(void);
// SMS функции
gsm_status_t gsm_check_new_sms(int *new_index);
gsm_status_t gsm_read_sms(int index, sms_data_t *sms);
gsm_status_t gsm_delete_sms(int index);
gsm_status_t gsm_send_sms(const char *phone, const char *message);
// Утилиты
bool gsm_is_authorized(const char *phone);
void gsm_parse_command(const char *message, int *cmd, int *params, int *param_count);
#endif // GSM_H
main/gsm.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "gsm.h"
#include "relay.h"
#include "storage.h"
static const char *TAG = "GSM";
// Авторизованные номера (загружаются из NVS)
static char authorized_numbers[5][20] = {
"+79093436455", // Ваш номер
"", "", "", ""
};
// Буфер для ответов
static char response_buffer[1024];
// Отправка AT команды и получение ответа
static gsm_status_t gsm_send_command(const char *cmd, char *response, int timeout_ms) {
uart_write_bytes(GSM_UART_PORT, cmd, strlen(cmd));
uart_write_bytes(GSM_UART_PORT, "rn", 2);
int len = 0;
int total_len = 0;
TickType_t start_time = xTaskGetTickCount();
memset(response, 0, sizeof(response_buffer));
while ((xTaskGetTickCount() - start_time) < pdMS_TO_TICKS(timeout_ms)) {
len = uart_read_bytes(GSM_UART_PORT, (uint8_t*)response + total_len,
sizeof(response_buffer) - total_len - 1, pdMS_TO_TICKS(100));
if (len > 0) {
total_len += len;
if (strstr(response, "OKrn") || strstr(response, "ERRORrn")) {
break;
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}
return (total_len > 0) ? GSM_OK : GSM_TIMEOUT;
}
// Инициализация UART для GSM
void gsm_init(void) {
uart_config_t uart_config = {
.baud_rate = GSM_BAUD_RATE,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_APB,
};
uart_param_config(GSM_UART_PORT, &uart_config);
uart_set_pin(GSM_UART_PORT, GSM_TX_PIN, GSM_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
uart_driver_install(GSM_UART_PORT, GSM_RX_BUF_SIZE * 2, GSM_TX_BUF_SIZE * 2, 0, NULL, 0);
ESP_LOGI(TAG, "GSM UART initialized");
}
// Включение питания модема
gsm_status_t gsm_power_on(void) {
gpio_set_direction(GSM_PWR_PIN, GPIO_MODE_OUTPUT);
gpio_set_direction(GSM_RST_PIN, GPIO_MODE_OUTPUT);
// Сигнал PWRKEY для включения SIM800L
gpio_set_level(GSM_PWR_PIN, 1);
vTaskDelay(pdMS_TO_TICKS(1000));
gpio_set_level(GSM_PWR_PIN, 0);
vTaskDelay(pdMS_TO_TICKS(3000));
return GSM_OK;
}
// Проверка связи с модемом
gsm_status_t gsm_check_communication(void) {
char response[256];
for (int i = 0; i < 3; i++) {
if (gsm_send_command("AT", response, 2000) == GSM_OK) {
if (strstr(response, "OK")) {
ESP_LOGI(TAG, "GSM modem OK");
return GSM_OK;
}
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
ESP_LOGE(TAG, "GSM modem not responding");
return GSM_ERROR;
}
// Проверка нового SMS
gsm_status_t gsm_check_new_sms(int *new_index) {
char response[256];
*new_index = -1;
if (gsm_send_command("AT+CMGL="REC UNREAD",1", response, 5000) == GSM_OK) {
// Ищем +CMGL: index,"REC UNREAD"...
char *p = strstr(response, "+CMGL:");
if (p) {
p += 6;
while (*p == ' ') p++;
*new_index = atoi(p);
return GSM_OK;
}
}
return GSM_ERROR;
}
// Чтение SMS
gsm_status_t gsm_read_sms(int index, sms_data_t *sms) {
char cmd[32];
char response[512];
snprintf(cmd, sizeof(cmd), "AT+CMGR=%d", index);
if (gsm_send_command(cmd, response, 5000) != GSM_OK) {
return GSM_ERROR;
}
// Парсинг номера: +CMGR: "REC READ","+79093436455",,"24/03/08,11:32:39+04"
char *phone_start = strchr(response, '"');
if (!phone_start) return GSM_ERROR;
phone_start = strchr(phone_start + 1, '"');
if (!phone_start) return GSM_ERROR;
phone_start++;
char *phone_end = strchr(phone_start, '"');
if (!phone_end) return GSM_ERROR;
int len = phone_end - phone_start;
if (len > 0 && len < (int)sizeof(sms->phone) - 1) {
strncpy(sms->phone, phone_start, len);
sms->phone[len] = '';
}
// Ищем текст сообщения (после последней кавычки)
char *text_start = strrchr(response, '"');
if (text_start) {
text_start = strchr(text_start + 1, 'n');
if (text_start) {
text_start++;
char *text_end = strstr(text_start, "OK");
if (text_end) {
len = text_end - text_start;
if (len > 0 && len < (int)sizeof(sms->message) - 1) {
strncpy(sms->message, text_start, len);
sms->message[len] = '';
// Удаляем r
char *p;
while ((p = strchr(sms->message, 'r')) != NULL) {
*p = ' ';
}
}
}
}
}
sms->index = index;
return GSM_OK;
}
// Удаление SMS
gsm_status_t gsm_delete_sms(int index) {
char cmd[32];
char response[32];
snprintf(cmd, sizeof(cmd), "AT+CMGD=%d", index);
return gsm_send_command(cmd, response, 2000);
}
// Отправка SMS
gsm_status_t gsm_send_sms(const char *phone, const char *message) {
char cmd[64];
char response[64];
// Текстовый режим
gsm_send_command("AT+CMGF=1", response, 1000);
vTaskDelay(pdMS_TO_TICKS(100));
// Подготовка к отправке
snprintf(cmd, sizeof(cmd), "AT+CMGS="%s"", phone);
if (gsm_send_command(cmd, response, 5000) != GSM_OK) {
return GSM_ERROR;
}
// Отправка текста и Ctrl+Z
if (strstr(response, ">")) {
uart_write_bytes(GSM_UART_PORT, message, strlen(message));
uart_write_bytes(GSM_UART_PORT, "x1A", 1); // Ctrl+Z
vTaskDelay(pdMS_TO_TICKS(5000));
// Проверка ответа
memset(response, 0, sizeof(response));
int len = uart_read_bytes(GSM_UART_PORT, (uint8_t*)response, sizeof(response) - 1, pdMS_TO_TICKS(1000));
if (len > 0 && strstr(response, "OK")) {
ESP_LOGI(TAG, "SMS sent to %s", phone);
return GSM_OK;
}
}
return GSM_ERROR;
}
// Проверка авторизации
bool gsm_is_authorized(const char *phone) {
for (int i = 0; i < 5; i++) {
if (authorized_numbers[i][0] == '') continue;
// Сравниваем номера
if (strcmp(phone, authorized_numbers[i]) == 0) {
return true;
}
// Проверка формата 8 вместо +7
if (phone[0] == '8' && authorized_numbers[i][0] == '+') {
char phone_with_7[20];
snprintf(phone_with_7, sizeof(phone_with_7), "+7%s", phone + 1);
if (strcmp(phone_with_7, authorized_numbers[i]) == 0) {
return true;
}
}
}
return false;
}
// Парсинг команды из SMS (формат: "команда параметр")
void gsm_parse_command(const char *message, int *cmd, int *params, int *param_count) {
*cmd = 0;
*param_count = 0;
// Парсим первую цифру - команда
char *endptr;
*cmd = strtol(message, &endptr, 10);
// Если есть параметры
if (endptr && *endptr != '') {
char *p = (char*)endptr;
while (*p != '' && *param_count < 5) {
// Пропускаем не-цифры
while (*p != '' && (*p < '0' || *p > '9')) p++;
if (*p == '') break;
// Читаем параметр
params[*param_count] = strtol(p, &p, 10);
(*param_count)++;
}
}
}
main/main.c
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "gsm.h"
#include "relay.h"
#include "wifi_ap.h"
#include "web_server.h"
#include "storage.h"
static const char *TAG = "MAIN";
// Задача обработки GSM событий
void gsm_task(void *pvParameters) {
int last_sms_index = -1;
int check_count = 0;
while (1) {
// Проверка новых SMS каждые 2 секунды
vTaskDelay(pdMS_TO_TICKS(2000));
int new_index;
if (gsm_check_new_sms(&new_index) == GSM_OK && new_index != last_sms_index) {
if (new_index >= 0) {
ESP_LOGI(TAG, "New SMS at index %d", new_index);
sms_data_t sms;
if (gsm_read_sms(new_index, &sms) == GSM_OK) {
ESP_LOGI(TAG, "From: %s", sms.phone);
ESP_LOGI(TAG, "Msg: %s", sms.message);
// Проверка авторизации
if (gsm_is_authorized(sms.phone)) {
ESP_LOGI(TAG, "Authorized number, processing command");
// Парсинг команды
int cmd, params[5] = {0}, param_count = 0;
gsm_parse_command(sms.message, &cmd, params, ¶m_count);
// Обработка команд
if (cmd == 1) {
// STATUS
char status[128];
snprintf(status, sizeof(status),
"Relay status: 1=%s 2=%s 3=%s 4=%s",
relay_get_state(0) ? "ON" : "OFF",
relay_get_state(1) ? "ON" : "OFF",
relay_get_state(2) ? "ON" : "OFF",
relay_get_state(3) ? "ON" : "OFF");
gsm_send_sms(sms.phone, status);
} else if (cmd >= 11 && cmd <= 14) {
// RELAY control (11=relay1, 12=relay2, etc)
int relay = cmd - 11;
if (param_count > 0) {
relay_set(relay, params[0] == 1);
char response[32];
snprintf(response, sizeof(response), "Relay%d %s",
relay + 1, params[0] == 1 ? "ON" : "OFF");
gsm_send_sms(sms.phone, response);
}
} else if (cmd == 2) {
// ALL ON/OFF
if (param_count > 0) {
relay_all_set(params[0] == 1);
gsm_send_sms(sms.phone, params[0] == 1 ? "All ON" : "All OFF");
}
} else if (cmd == 3) {
// WiFi mode switch
if (wifi_get_mode() == WIFI_MODE_AP) {
wifi_set_mode(WIFI_MODE_STA);
gsm_send_sms(sms.phone, "WiFi mode: Client");
} else {
wifi_set_mode(WIFI_MODE_AP);
gsm_send_sms(sms.phone, "WiFi mode: AP");
}
wifi_ap_stop();
vTaskDelay(pdMS_TO_TICKS(1000));
wifi_ap_start();
} else {
gsm_send_sms(sms.phone, "Unknown command");
}
} else {
ESP_LOGW(TAG, "Unauthorized number: %s", sms.phone);
gsm_send_sms(sms.phone, "ERROR: Unauthorized");
}
// Удаляем прочитанное SMS
gsm_delete_sms(new_index);
}
last_sms_index = new_index;
}
}
// Периодическая проверка сигнала
check_count++;
if (check_count >= 30) { // ~1 минута
check_count = 0;
ESP_LOGI(TAG, "System running: Relays %d%d%d%d",
relay_get_state(0), relay_get_state(1),
relay_get_state(2), relay_get_state(3));
}
}
}
void app_main(void) {
ESP_LOGI(TAG, "GSM Relay Controller starting...");
// Инициализация хранилища
storage_init();
// Инициализация реле
relay_init();
// Инициализация GSM
gsm_init();
// Включение модема
if (gsm_power_on() != GSM_OK) {
ESP_LOGE(TAG, "Failed to power on GSM");
}
// Проверка связи
if (gsm_check_communication() != GSM_OK) {
ESP_LOGE(TAG, "GSM communication failed");
}
// Настройка WiFi AP
wifi_ap_init();
wifi_set_mode(WIFI_MODE_AP);
wifi_ap_start();
// Запуск веб-сервера
web_server_start();
// Создание задачи обработки GSM
xTaskCreate(gsm_task, "gsm_task", 4096, NULL, 5, NULL);
ESP_LOGI(TAG, "System ready");
}
Поделка 3:
Так как использую программу QUIK для торговли на фондовом рынке, то решил автоматизировать двухфакторную аутентификацию.
Для тех, кто не знает, что это такое, поясняю.
При запуске торгового терминала QUIK, необходимо сначала ввести пароль в соответствующее окно. После этого программа подключится к серверу брокера и запросит у вас код , который получите на телефон(смартфон) от брокера. Необходимо этот код вручную ввести в окно программы.
Так как у меня есть модем, который я сделал в поделки 1, то почему бы не использовать его для автоматизации старта QUIK.
Решил написать скрипт на AutoIt.
Предварительно узнать у DS о возможных вариантах решения.

Ответ DS:

Поделка фактически состоит из двух задач.
Первая задача связана с запуском QUIK и автоматическим вводам пароля и кода в соответствующие окна. При ее решении пароль и код разместил в файлах. Так как задача простая, то на ее решение ушло примерно 20 минут.
Результирующий скрипт:
; =======================================================================
; Скрипт автоматического ввода пароля и кода для QUIK
; автор nikolz & DS
; Версия: 3.0 (с поддержкой параметров и настроек)
; =======================================================================
; --- Настройки ---
$sQuikPath = "C:QUIKinfo.exe"
$sQuikParams = "" ; Сюда добавьте параметры из ярлыка, если есть
$sQuikWorkingDir = "C:QUIK" ; Рабочая папка (обычно там же, где info.exe)
$sPasswordFile = @ScriptDir & "password.txt"
$sCodeFile = @ScriptDir & "code.txt"
; --- Чтение пароля и кода ---
Local $sPassword = FileReadLine($sPasswordFile)
If @error Then
MsgBox(16, "Ошибка", "Не удалось прочитать файл пароля")
Exit
EndIf
Local $sCode = FileReadLine($sCodeFile)
If @error Then
MsgBox(16, "Ошибка", "Не удалось прочитать файл с кодом")
Exit
EndIf
; --- Запуск QUIK с рабочей папкой ---
; Это ключевой момент! QUIK должен быть запущен из правильной директории
; чтобы увидеть все сохраненные настройки
Run('"' & $sQuikPath & '" ' & $sQuikParams, $sQuikWorkingDir)
; --- Ждем появления окна пароля (с таймаутом) ---
; Окно может появиться не сразу, т.к. сначала грузятся настройки
Local $hFirstWindow = 0
Local $iMaxWait = 30 ; ждем до 30 секунд
For $i = 1 To $iMaxWait
$hFirstWindow = WinGetHandle("[CLASS:#32770]")
If $hFirstWindow Then
Local $sTitle = WinGetTitle($hFirstWindow)
If StringInStr($sTitle, "пароль") Or StringInStr($sTitle, "Идентификация") Then
ExitLoop
EndIf
EndIf
Sleep(1000)
Next
If Not $hFirstWindow Then
MsgBox(16, "Ошибка", "Окно пароля не появилось за " & $iMaxWait & " секунд")
Exit
EndIf
; --- Ввод пароля ---
WinActivate($hFirstWindow)
Sleep(500)
ControlSend($hFirstWindow, "", "[CLASS:Edit; INSTANCE:2]", $sPassword)
Sleep(300)
ControlSend($hFirstWindow, "", "", "{ENTER}")
; --- Ждем окно для кода ---
Sleep(2000) ; Даем время на обработку пароля
Local $hSecondWindow = WinWait("[CLASS:QuikGUI]", "", 20)
If Not $hSecondWindow Then
; Если не нашли по классу, ищем по заголовку
$hSecondWindow = WinWait("Введите код", "", 10)
EndIf
If $hSecondWindow Then
WinActivate($hSecondWindow)
Sleep(500)
ControlSend($hSecondWindow, "", "[CLASS:Edit; INSTANCE:1]", $sCode)
Sleep(300)
ControlSend($hSecondWindow, "", "", "{ENTER}")
MsgBox(64, "Успех", "Авторизация выполнена!")
Else
MsgBox(48, "Предупреждение", "Окно кода не найдено, но пароль введен")
EndIf
Exit
Вторая задача – прием кода из SMS от брокера
Решение этой задачи находится в стадии отладки, поэтому скрипт лишь для примера:
AutoIt-скрипт для работы с модемом
; =======================================================================
; Модуль получения кода с GSM-модема (AutoIt версия)
; Версия: 1.0
; Основан на ATCmd.au3 UDF
; =======================================================================
#include <ATCmd.au3>
#include <FileConstants.au3>
#include <MsgBoxConstants.au3>
#include <StringConstants.au3>
; --- Параметры командной строки ---
; Формат: script.exe "ComPort=COM3;BaudRate=9600;SenderNumber=+79...;Timeout=60" "output.txt"
If $CmdLine[0] < 2 Then
ConsoleWrite("Использование: " & @ScriptName & ' "параметры" "выходной_файл"' & @CRLF)
Exit 1
EndIf
Local $sSettings = $CmdLine[1]
Local $sOutputFile = $CmdLine[2]
Local $sLogFile = @ScriptDir & "modem_autoit.log"
; --- Очищаем лог при запуске ---
FileDelete($sLogFile)
LogWrite("=== Запуск AutoIt-модуля модема ===")
; --- Парсинг параметров ---
Local $aSettings = StringSplit($sSettings, ";")
Local $sComPort = "COM3"
Local $iBaudRate = 9600
Local $sSenderNumber = ""
Local $iTimeout = 60
Local $sSimPIN = ""
For $i = 1 To $aSettings[0]
Local $aPair = StringSplit($aSettings[$i], "=")
If $aPair[0] >= 2 Then
Switch $aPair[1]
Case "ComPort"
$sComPort = $aPair[2]
Case "BaudRate"
$iBaudRate = Number($aPair[2])
Case "SenderNumber"
$sSenderNumber = $aPair[2]
Case "Timeout"
$iTimeout = Number($aPair[2])
Case "SimPIN"
$sSimPIN = $aPair[2]
EndSwitch
EndIf
Next
LogWrite("Порт: " & $sComPort)
LogWrite("Скорость: " & $iBaudRate)
LogWrite("Номер отправителя: " & $sSenderNumber)
LogWrite("Таймаут: " & $iTimeout & " сек")
; --- Главная функция получения кода ---
Local $sCode = GetCodeFromModem()
Local $iResult = 1 ; 0 - успех, 1 - ошибка
If $sCode <> "" Then
; Сохраняем код в выходной файл
Local $hFile = FileOpen($sOutputFile, $FO_OVERWRITE)
If $hFile <> -1 Then
FileWrite($hFile, $sCode)
FileClose($hFile)
LogWrite("Код сохранен в файл: " & $sCode)
$iResult = 0
Else
LogWrite("ОШИБКА: Не удалось создать выходной файл")
EndIf
Else
LogWrite("ОШИБКА: Код не получен")
EndIf
LogWrite("=== Завершение работы ===")
Exit $iResult
; ==============================================================================
; ФУНКЦИИ
; ==============================================================================
; #FUNCTION# ===================================================================
; Name ..........: GetCodeFromModem
; Description ...: Получает код подтверждения из SMS с модема
; Syntax ........: GetCodeFromModem()
; Return values .: Успех - строка с кодом, Неудача - пустая строка
; ==============================================================================
Func GetCodeFromModem()
Local $sCode = ""
Local $hModem = 0
Local $iMaxAttempts = Int($iTimeout / 2) ; Проверяем каждые 2 секунды
LogWrite("Инициализация модема на порту " & $sComPort)
; Открываем соединение с модемом
$hModem = _ATCmd_Open($sComPort, $iBaudRate)
If @error Then
LogWrite("ОШИБКА: Не удалось открыть COM-порт " & $sComPort)
Return ""
EndIf
LogWrite("Соединение с модемом установлено")
; Проверяем наличие SIM-карты
If Not _ATCmd_IsSIMInserted($hModem) Then
LogWrite("ОШИБКА: SIM-карта не обнаружена")
_ATCmd_Close($hModem)
Return ""
EndIf
LogWrite("SIM-карта обнаружена")
; Проверяем, требуется ли ввод PIN-кода
If _ATCmd_IsPINRequired($hModem) Then
LogWrite("Требуется PIN-код SIM-карты")
If $sSimPIN = "" Then
LogWrite("ОШИБКА: PIN не указан в настройках")
_ATCmd_Close($hModem)
Return ""
EndIf
_ATCmd_SetPIN($hModem, $sSimPIN)
Sleep(2000)
LogWrite("PIN введен")
EndIf
; Проверяем регистрацию в сети
If Not _ATCmd_WaitForNetwork($hModem, 30) Then
LogWrite("ОШИБКА: Модем не зарегистрирован в сети")
_ATCmd_Close($hModem)
Return ""
EndIf
LogWrite("Модем зарегистрирован в сети")
; Получаем уровень сигнала для информации
Local $iCSQ = _ATCmd_GetCSQ($hModem)
If $iCSQ <> -1 Then
LogWrite("Уровень сигнала: " & $iCSQ)
EndIf
; Настраиваем текстовый режим для SMS
_ATCmd_SMS_SetTextMode($hModem)
; --- Цикл ожидания SMS ---
LogWrite("Ожидание SMS с кодом от " & $sSenderNumber & "...")
For $i = 1 To $iMaxAttempts
Sleep(2000)
LogWrite("Проверка SMS (попытка " & $i & " из " & $iMaxAttempts & ")")
; Получаем список всех SMS
Local $aSMSList = _ATCmd_SMS_ListMessages($hModem, "ALL")
If Not @error And IsArray($aSMSList) Then
For $j = 0 To UBound($aSMSList) - 1
; Формат: [0] - индекс, [1] - статус, [2] - отправитель, [3] - дата, [4] - текст
Local $sSender = $aSMSList[$j][2]
Local $sText = $aSMSList[$j][4]
Local $sIndex = $aSMSList[$j][0]
LogWrite(" Найдено SMS от: " & $sSender)
; Проверяем отправителя (ищем номер брокера)
If StringInStr($sSender, $sSenderNumber) Then
LogWrite(" Совпадение отправителя!")
$sCode = ExtractCodeFromText($sText)
If $sCode <> "" Then
LogWrite(" Код найден: " & $sCode)
; Удаляем прочитанное SMS
_ATCmd_SMS_DeleteMessage($hModem, $sIndex)
LogWrite(" SMS удалено")
ExitLoop 2
Else
LogWrite(" Внимание: код не найден в тексте: " & $sText)
EndIf
EndIf
Next
Else
LogWrite(" Новых SMS не найдено")
EndIf
Next
_ATCmd_Close($hModem)
LogWrite("Соединение с модемом закрыто")
Return $sCode
EndFunc
; #FUNCTION# ===================================================================
; Name ..........: ExtractCodeFromText
; Description ...: Извлекает цифровой код из текста SMS
; Syntax ........: ExtractCodeFromText($sText)
; Parameters ....: $sText - текст SMS
; Return values .: строка с кодом или пустая строка
; ==============================================================================
Func ExtractCodeFromText($sText)
; Ищем последовательность из 4-8 цифр
Local $aMatches = StringRegExp($sText, "(d{4,8})", $STR_REGEXPARRAYMATCH)
If Not @error And UBound($aMatches) > 0 Then
Local $sCode = $aMatches[0]
; Дополнительная проверка: код обычно не начинается с 0
If StringLeft($sCode, 1) <> "0" Then
Return $sCode
EndIf
EndIf
; Альтернативный поиск - если код в формате "код: 123456" или "code: 123456"
$aMatches = StringRegExp($sText, "(?i)(?:код|code|pass|пароль)[:s]*(d{4,8})", $STR_REGEXPARRAYMATCH)
If Not @error And UBound($aMatches) > 0 Then
Return $aMatches[0]
EndIf
Return ""
EndFunc
; #FUNCTION# ===================================================================
; Name ..........: LogWrite
; Description ...: Записывает сообщение в лог-файл
; ==============================================================================
Func LogWrite($sMessage)
FileWriteLine($sLogFile, @HOUR & ":" & @MIN & ":" & @SEC & " - " & $sMessage)
EndFunc
Автор: nikolz


