Перейти к публикации
iT4iT.CLUB

Kitsum

Пользователи
  • Публикации

    424
  • Зарегистрирован

  • Посещение

  • Дней в лидерах

    234

Сообщения, опубликованные пользователем Kitsum


  1. Всем доброго времени суток.

    Как всегда начнем с предыстории. Необходимо было реализовать видеонаблюдение за помещением, естественно с нулевым бюджетом, а как иначе. В наличие имелся сервер с Linux на борту и домашняя usb web камера. За основу был взят программный пакет Motion. В принципе ничего сложного в настройке нет, правим пару строк в конфиге и вуаля. Писать было ни о чем. 

    На днях обновил Ubuntu до версии 16.04.1 LTS и заодно, решено было обновить часть пакетов, в том числе Motion, и переделать систему видеонаблюдения. От usb камеры давно было решено отказаться, и ей на смену пришла дешевая IP камера (поддерживающая MJPEG поток) купленная на распродаже.

    cam1.JPGcam2.JPG

    Отразим все на "бумаге"

    Имеется:

    • Сервер Linux Ubuntu 16.04.1 LTS
    • Пакет Motion 3.2.12
    • IP камера с поддержкой MJPEG

    Требуется:

    • Писать только видео (никаких фото и таймлапсов)
    • Писать только по движению
    • Оповещать об обнаружении движения
    • Транслировать поток в реальном времени с сервера

    В Ubuntu установка пакетов проста до безобразия

    apt-get update && apt-get install motion

    По завершении установки переходим в каталог с файлом конфигурации

    cd /etc/motion
    nano motion.conf

    Сразу разберемся с порядком действий т.к понимая, что сейчас будем делать, мы сможем настроить эту программу вне зависимости от её версии и версии операционной системы на которой она установлена.

    Сам Motion может работать одновременно с несколькими потоками (камерами для обывателя), неважно будь то IP камеры, USB камеры или иные устройства захвата видео. Да хоть все вперемешку. Самое главное, что если мы используем IP камеры, то они должны поддерживать MJPEG поток. Никакого RTSP (Real Time Streaming Protocol) Motion не поддерживает.

    Файл конфигурации motion.conf представляет из себя набор основных настроек программы и настройки для первого потока. Все последующие потоки можно конфигурировать в файлах thread1.conf, thread2.conf ... thread9.conf (по умолчанию они отсутствуют) и подключать в конце файла motion.conf

    Лично я предпочитаю основные настройки и настройки по умолчанию хранить в основном файле конфигурации, а настройки самих камер вынести во внешние файлы thread, по одному файлу на каждую камеру. Таким образом, не будет никакой путаницы если например камера №2 выйдет из строя и её понадобится заменить на не типичную камеру.

    Основные настройки представлены в двух секциях

    ############################################################
    # Daemon
    ############################################################
    
    # Запуск в фоновом режиме (default: off)
    daemon on
    
    # Файл для хранения идентификатора процесса (default: not defined)
    process_id_file /var/run/motion/motion.pid
    
    ############################################################
    # Basic Setup Mode
    ############################################################
    
    # Запускать в Setup-Mode, отключает режим демона. (default: off)
    setup_mode off
    
    # Путь до файла с логами. (default: not defined)
    logfile /var/log/motion/motion.log
    
    # Уровель лог сообщений [1..9] (EMR, ALR, CRT, ERR, WRN, NTC, INF, DBG, ALL). (default: 6 / NTC)
    log_level 6
    
    # Фильтр лог сообщений по типу (COR, STR, ENC, NET, DBL, EVT, TRK, VID, ALL). (default: ALL)
    log_type all
    

    Тут нас интересует только запуск программы в фоновом режиме, остальные настройки можно оставить без изменения.

    Далее идет секция настройки первого потока

    ###########################################################
    # Capture device options
    ############################################################
    
    # Videodevice to be used for capturing  (default /dev/video0)
    # for FreeBSD default is /dev/bktr0
    ;videodevice /dev/video0
    
    ...

    Закомментируем все настройки в этой секции, в этом файле они нам не нужны.

    Далее большинство настроек мы оставим без изменения, а поправим только некоторые. Эти настройки будут общие для всех потоков.

    Отключаем создание снимков при обнаружении движения. Можно указать один из режимов (first, best, center) и тогда мы будем получать по одному снимку на одно видео. Это может пригодиться в том случае, если вы собираетесь организовывать WEB доступ к архиву видеозаписей.

    ############################################################
    # Image File Output
    ############################################################
    
    # Output 'normal' pictures when motion is detected (default: on)
    # Valid values: on, off, first, best, center
    # When set to 'first', only the first picture of an event is saved.
    # Picture with most motion of an event is saved when set to 'best'.
    # Picture with motion nearest center of picture is saved when set to 'center'.
    # Can be used as preview shot for the corresponding movie.
    output_pictures off
    

    Отключаем стриминг по умолчанию

    ############################################################
    # Live Stream Server
    ############################################################
    
    # The mini-http server listens to this port for requests (default: 0 = disabled)
    stream_port 0
    

    Отключаем встроенный web сервер

    ############################################################
    # HTTP Based Control
    ############################################################
    
    # TCP/IP port for the http server to listen on (default: 0 = disabled)
    webcontrol_port 0

    В самом конце основного файла конфигурации motion.conf имеется секция для подключения дополнительных потоков. По умолчанию файл motion.conf соответствовал файлу thread0.conf, но мы это доблестно исправили выше. Теперь нам необходимо включить в файл конфигурации файлы с настройками для работы с нашими камерами. Мы рассмотрим на примере конфигурации одной камеры, раскомментируем thread1.conf

    ##############################################################
    # Thread config files - One for each camera.
    # Except if only one camera - You only need this config file.
    # If you have more than one camera you MUST define one thread
    # config file for each camera in addition to this config file.
    ##############################################################
    
    # Remember: If you have more than one camera you must have one
    # thread file for each camera. E.g. 2 cameras requires 3 files:
    # This motion.conf file AND thread1.conf and thread2.conf.
    # Only put the options that are unique to each camera in the
    # thread config files.
    thread /etc/motion/thread1.conf
    ; thread /etc/motion/thread2.conf
    ; thread /etc/motion/thread3.conf
    ; thread /etc/motion/thread4.conf
    

     

    Создадим файл thread1.conf

    cd /etc/motion && touch thread1.conf && nano thread1.conf
    

    Первым делом вам необходимо узнать:

    • адрес MJPEG потока вашей камеры
    • разрешение камеры (обычно для дешевых камер это 640х480)
    • частоту кадров (у дешевых камер обычно не более 20)
    # Разрешение картинки
    width 640
    height 480
    
    # Частота кадров
    framerate 20
    

    Укажем настройки для доступа к видео потоку. В моем случае камеры находятся в отдельной физической сети, и доступ к потоку по логину и паролю не требуется (только для доступа к настройкам камер).

    # Сетевой адрес для захвата изображения с камеры (mjpeg поток)
    netcam_url http://10.10.1.101/mjpeg.cgi
    
    # Логин и пароль к видео потоку (only if required). Default: not defined
    ; netcam_userpass admin:password

    Укажем подпись для камеры, это поможет быстро идентификации её местоположение (можно использовать символ перевода каретки)

    # Подпись к камере (название камеры)
    text_left CAMERA 1\nSH-21 STORAGE
    

    Укажем путь до места хранения видео фрагментов (лучше создать каталог заранее)

    # Путь до места сохранения видео\фото
    target_dir /media/share/motion/cam1
    

    Включаем детектор движения

    # Количество изменившихся пикселей для триггера обнаружения движения
    threshold 2000
    

    У меня камера стоит под потолком, рядом со стеной с большим количеством окон. Ну и естественно окна смотрят на солнечную сторону, а это значит, что открывается прекрасный вид на быстро плывущие облака отбрасывающие тень на здание, а во время летней грозы помещение может освещаться завораживающими вспышка молний. И это все можно будет посмотреть на видео, если не добавить следующие параметры. Можно поэкспериментировать и добиться приемлемого результата для вас

    # Не реагировать на резкое изменение яркости (0 - отключено, 0-100% от общего числа пикселей)
    lightswitch 60
    
    # Минимальное число кадров, в которых фиксируется движение для взведения триггера тревоги
    minimum_motion_frames 5

    Добавляем мертвую зону. Она позволит не создавать кучу мусора в папке с видео

    # Количество секунд (мертвая зона) после окончания движения, в течении которых отключен детектор
    event_gap 10

    Для наглядности можно добавить рамку вокруг движущегося объекта. Также рекомендую включить эти параметры на время, пока проходит настройка и обкатка системы

    # Рисуем рамку вокруг движущегося объекта
    locate_motion_mode on
    
    # Стиль рамки вокруг движущегося объекта
    locate_motion_style redbox
    

    Также для настройки я использую отображение количество изменившихся пикселей. Это очень помогает отрегулировать уровень тревоги т.к камера может быть на разном ударении от наблюдаемой зоны и следовательно чем дальше, тем меньше пикселей будет занимать движущийся вдали объект

    # Показывать количество изменившихся пикселей (используем для настройки threshold)
    text_changes on
    

    Включаем возможность стримить с сервера все происходящее на данной камере

    # Настройки потока вещания (порт\качество\частота кадров\доступ только с 172.0.0.1\ограничение потока)
    stream_port 8081
    stream_quality 70
    stream_maxrate 20
    stream_localhost off
    stream_limit 0

    Можно (и нужно, если установлен stream_localhost off) требовать авторизацию при доступе к потоку с этой камеры из вне

    # Требовать авторизацию для доступа к потоку вещания
    stream_auth_method 1
    stream_authentication admin:12345678

    В принципе этого уже достаточно для организации работы системы и на этом этапе можно сохранить файл. Но в моем случае требуется организовать внешнюю систему оповещения. Благо Motion умеет вызывать внешние программы и передавать им различные параметры. 

    Так как первая камера у меня захватывает не все помещение, а только ту его часть, где находится входная дверь, то мне куда более важно быть в курсе, когда движение началось именно в той части комнаты. И соблюдать режим тишины, когда объект движется от камеры.

    При вызове внешнего скрипта ему можно передать следующие параметры

    Скрытый текст
    
    %Y = year
    %m = month
    %d = date
    %H = hour
    %M = minute
    %S = second,
    
    %v = event
    %q = frame number
    %t = thread (camera) number
    %D = changed pixels
    %N = noise level
    
    %i and %J = width and height of motion area
    %K and %L = X and Y coordinates of motion center
    
    %C = value defined by text_event
    %f = filename with full path
    %n = number indicating filetype
    
    # Both %f and %n are only defined for on_picture_save,
    # on_movie_start and on_movie_end
    # Quotation marks round string are allowed.

     

    Добавим строку вызова внешнего исполняемого файла с передачей ему параметрами - номер камеры и координаты центра начала движения

    # При обнаружении движения запускать внешний скрипт
    on_event_start /etc/motion/zone_alarm.sh %t %K %L

    Содержимое zone_alarm.sh

    #!/bin/bash
    
    CAM="$1"
    X="$2"
    Y="$3"
    
    if (("$CAM" == "1"))
    then
            if (("$X" < "320" && "$Y" < "240"))
            then
                    beep -f 4000 -l 200 -n -f 4000 -l 200 -n -f 4000 -l 200 -n -f 4000 -l 200 -n -f 4000 -l 200
            fi
    fi

    В данном случае происходит звуковое оповещение на самом сервере с помощью утилиты beep, по умолчанию её может и не быть

    apt-get install beep

    Также можно сигнализировать об обнаружении движения на удаленную станцию или на мобильное устройство. Очень актуально если камера наблюдает за помещением, в котором не бывает и не должно бывать людей. Например, склад.

    Необходимо разрешить Motion работать в режиме демона, для этого правим файл /etc/default/motion

    # set to 'yes' to enable the motion daemon
    start_motion_daemon=yes

    Проверяем работу

    /etc/init.d/motion start

    Что мы имеем на выходе

    cam6.pngcam7.png

    cam4.JPG

    Все прекрасно работает, но остается вопрос переполнения жесткого диска. Чтобы этот счастливый момент никогда не настал, стоит автоматизировать удаление устаревших видеозаписей. В этом нам поможет планировщик CRON.

    crontab -e

    И добавим запись, которая будет выполняться ежедневно в 23:30 и искать файлы формата .avi в указанном нами каталоге и если найденный файл существует более 14 суток, то он удаляется. Естественно если камера стоит в проходном месте, то стоит уменьшить время жизни видео.

    30 23 * * * find /media/share/motion/ -name '*.avi' -mtime +14 -exec rm {} \;

    На этом настройка завершена.

    PS: в файле motion.conf можно найти еще очень много интересных параметров!

    • Like 1

  2. @svchekalin Могу на вскидку предложить внести изменения в скетч

    1. Заменить строку запроса к SQL серверу

    const char QUERY_S[] = "SELECT type FROM adatum.rc522 WHERE uid = %s;";

    на

    const char QUERY_S[] = "SELECT type, description FROM adatum.rc522 WHERE uid = %s;";

     

    2. Найти и заменить кусок кода

    if((row = cur.get_next_row()) != NULL) typeKey = String(row->values[0]); else typeKey = "0";
        if(typeKey == F("1") or typeKey == F("2")) {

    на

    if((row = cur.get_next_row()) != NULL) typeKey = String(row->values[0]); else typeKey = "0";
        Serial.println(String(row->values[1]));
        if(typeKey == F("1") or typeKey == F("2")) {

     

    • Like 2

  3. Данную тему не поднимал только ленивый. По существу все просто и очень подробно описано в первоисточнике https://github.com/esp8266/Arduino

    • Скачиваем Arduino IDE с официального сайта
    • Запускаем Arduino и открываем окно настроек (Файл -> Настройки)
    • В поле "Дополнительные ссылки для Менеджера плат:" указываем адрес 
    • http://arduino.esp8266.com/stable/package_esp8266com_index.json
    • Открываем менеджер плат (Инструменты -> Список плат -> Менеджер плат) и в качестве фильтра поиска указываем "esp8266". И жмем кнопку установить.

    В списке поддерживаемых плат появится ESP8266 в различных вариациях. Все необходимые файлы для поддержки работы с этим микроконтроллером и дополнительные библиотеки с примерами их работы будут закачаны в каталог %APPDATA%\Arduino15\

    Скрытый текст

    esp8266_arduino_ide_install_1.png

    Скрытый текст

    esp8266_arduino_ide_install_2.png

    Подключение ESP8266 для загрузки скетча

    В сети множество схем для подключения всех разновидностей ESP, от ESP-01 до ESP-12, но нужно понимать, что как бы они не назывались и не выглядели, собраны они на одном микроконтроллере ESP8266EX, следовательно, подключаются они все одинаково за исключением того момента, что если у ESP не выведена та или иная нога (например, ESP-01), то и искать, и подключать её не требуется. Достаточно знать, как подключить ESP-12 и Вы сможете завести и прошить любой тип ESP.

    Предлагаю использовать схему подключения с автоматическим сбросом и переводом микроконтроллера в режим загрузки программы. Главное, что понадобится - USB-TTL конвертер с выведенными ногами DTR и RTS. Я использовал конвертер на базе FT232RL.

    Скрытый текст

    FT232RL-FTDI-USB-to-TTL-Serial-Adapter-Module-for-Arduino-Mini-Port-3-3V-5V-.jpg

    ESP8266_ESP07_ESP12.png

    Если вы собираетесь использовать deepSleep(), то необходимо обязательно подтянуть GPIO-16 к RESET через резистор 470 Ом.

    И так, что тут к чему

    • RTS - Запрос на передачу (Request to Send)
    • DTR - Готовность приемника данных (Data Terminal Ready)

    Во время загрузки скетча транзистор T1 откроется и подтянет GPIO-0 к земле, в это время конденсатор C1 перезарядится и тем самым нога RESET кратковременно окажется подтянута к земле, что приведет к перезагрузке микроконтроллера и загрузке программы. Также благодаря конденсатору C1 мы будем перезапускать микроконтроллер каждый раз, при открытии монитора порта (как при использовании обычной Arduino UNO и т.п).

    Подведем итоги

    Лично мое мнение, что на базе ESP8266 стоит брать только ESP-07 или ESP-12 с переходной платой под отверстия 2,54мм, чтобы можно было установить модуль в макетную плату. Таким образом, за туже цену мы получаем микроконтроллер с максимальным количеством выведенных для нас ног и возможностью быстрого монтажа для экспериментов с разными поделками.

    esp8266_riser_2.jpg

    Скрытый текст

    esp8266_riser.jpg

     

    • Like 1

  4. Данный пост является шпаргалкой из собранных в сети различных материалов связанных с распиновкой (pinout) распространенных микроконтроллеров и плат на их основе.

    Микроконтроллеры

    ESP8266EX

    Скрытый текст

    esp8266_pinout.png

    ESP32 (R1)

    Скрытый текст

    esp32chip-Rev1_0.png

    Платы

    ESP-01

    Скрытый текст

    esp8266_esp01_pinout.png

    ESP-07

    Данная плата практически полностью идентична ESP-12X, разница только в наличие керамической антенны и разъема IPX для подключения внешней антенны.

    ESP-12 (E/F)

    Скрытый текст

    esp8266_esp12e_pinout.png

    NodeMCU

    Скрытый текст

    esp8266_devkit-01.png

    ESP32 Board

    Скрытый текст

    ESP32_Pinout_a1.png

    ESP32-WROOM-32 (DevKitC)

    Скрытый текст

    myESP32 DevKitC pinout.png

     

    • Like 3

  5. @Oksymoron К сожалению нет клавиатуры используемой в этом проекте, но уверен, что решим эту задачу и без физической реализации.

    Начальный скетч мне сильно не нравится, в силу "копипаста", но будем плясать от него. Логика программы очень проста - при нажатии "*" происходит вызов функции checkPassword() в которой вызывается password.evaluate() для проверки введенного кода с клавиатуры. И тут кроется ошибка, по завершению проверки, автор должен был очистить буфер с паролем, но это не происходит. Соответственно введенный пароль остается в памяти и если он был верен, то злоумышленник может открыть замок просто нажав клавишу "*".

    За сброс пароля отвечает функция password.reset() реализованная в библиотеки Password.h и нам лишь останется внести изменение в функцию checkPassword(), добавив в её конце очистку буфера.

    Скрытый текст
    
    //* is to validate password   короче чтобы войти
    //# is to reset password attempt Ну чтоб сделать сброс
    
    //внимаение ! если выдает ошибки качаем все библиотеки что указанны ниже !
    
    
    #include <SPI.h> // скачайте эту библиотеку !
    #include <MFRC522.h> // скачайте эту библиотеку !
    #include <Password.h> // скачайте эту библиотеку !
    #include <Keypad.h> // скачайте эту библиотеку !
    #define SS_PIN 10
    #define RST_PIN 9
    
    MFRC522 mfrc522(SS_PIN, RST_PIN);  // объект MFRC522C / reate MFRC522 instance.
    unsigned long uidDec, uidDecTemp;
    
    
    int door =8;
    int key = A5;
    int green = A1;
    int red = A2;
    
    Password password = Password( "DDCBC97" );
    
    const byte ROWS = 4; // Four rows
    const byte COLS = 4; //  columns
    // Define the Keymap
    char keys[ROWS][COLS] = {
    {'1','2','3','A'},
    {'4','5','6','B'},
    {'7','8','9','C'},
    {'*','0','#','D'}
    };
    
    byte rowPins[ROWS] = { 0,A0,7,6 };// Connect keypad ROW0, ROW1, ROW2 and ROW3 to these Arduino pins.
    byte colPins[COLS] = { 5,4,3,2, };// Connect keypad COL0, COL1 and COL2 to these Arduino pins.
    
    // Create the Keypad
    Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS );
    
    void setup()
    {
        
        pinMode (door,OUTPUT);
        pinMode (key,INPUT);
        pinMode (green,OUTPUT);
        pinMode (red,OUTPUT);
        Serial.begin(9600);
        
        SPI.begin();  //  инициализация SPI / Init SPI bus.
        mfrc522.PCD_Init();     // инициализация MFRC522 / Init MFRC522 card.
    
        keypad.addEventListener(keypadEvent); //add an event listener for this keypad
    
        Serial.println("");
        Serial.println("");
        Serial.println(" ======================== ");
    
        Serial.println(" Waiting for card... ");
    }
    
    void loop()
    {
        keypad.getKey();
        if (digitalRead(key) == HIGH)
        {
            digitalWrite(door,HIGH);
            Serial.println("Open ==>");
            digitalWrite(green, HIGH);
            delay(100);
            digitalWrite(red, HIGH);
            delay(100);
            digitalWrite(door,LOW);
            delay(100);
            digitalWrite(green, LOW);
            delay(100);
            digitalWrite(green, HIGH);
            delay(50);
            digitalWrite(red, HIGH);
            delay(50);
            digitalWrite(door,LOW);
            delay(50);
            digitalWrite(green, LOW);
            digitalWrite(green, HIGH);
            delay(50);
            digitalWrite(red, HIGH);
            delay(50);
            digitalWrite(door,LOW);
            delay(50);
            digitalWrite(green, LOW);
            delay(50);
            digitalWrite(green, HIGH);
            delay(50);
            digitalWrite(red, HIGH);
            delay(50);
            digitalWrite(green, LOW);
            delay(50);
            digitalWrite(red,LOW);
            digitalWrite(door,LOW);
        }
        // Поиск новой карточки / Look for new cards.
        if ( ! mfrc522.PICC_IsNewCardPresent()) {
            return;
        }
    
        // Выбор карточки / Select one of the cards.
        if ( ! mfrc522.PICC_ReadCardSerial()) {
            return;
        }
    
        uidDec = 0;
    
        // Выдача серийного номера карточки "UID".
        for (byte i = 0; i < mfrc522.uid.size; i++)
        {
            uidDecTemp = mfrc522.uid.uidByte[i];
            uidDec = uidDec*256+uidDecTemp;
        }
        Serial.println("Card UID: ");
        Serial.println(uidDec);
    
        
        if (uidDec == 384627541) // если "UID" номер совпал.
        {
            // включим светодиод.
            digitalWrite(door,HIGH);
            // Печатаем в Serial монитор .
            Serial.println("Open ==>");
        }
        // также и с другими номерами карт.
        else if (uidDec == 959884118)
        {
            Serial.println("Hi Sergey");
        }
        else if (uidDec == 695670870)
        {
            Serial.println("Hi Peter");
        }
        else  // если "UID" номер карты не совпал.
        {
            Serial.println(" unknown card");
            delay(10000);
        }
    
        Serial.println("=====================================");
    
        delay(3000); // пауза 3 секунды.
        digitalWrite(door,LOW);
    
        // и выключим все светодиоды.
    }
    
    //take care of some special events
    void keypadEvent(KeypadEvent eKey){
        switch (keypad.getState()){
            case PRESSED:
                Serial.print("Pressed: ");
                Serial.println(eKey);
                digitalWrite(green,HIGH);
                digitalWrite(red,HIGH);
                delay(50);
                digitalWrite(green,LOW);
                digitalWrite(red,LOW);
    
                switch (eKey){
                    case '*': checkPassword(); break;
                    case '#': password.reset(); break;
                    default: password.append(eKey);
                }
        }
    }
    
    void checkPassword(){
        if (password.evaluate()){
            Serial.println("Success");
            digitalWrite(door,HIGH); // открываем дверь
            digitalWrite(green, HIGH); //включаем выключаем светодиоды =) моргаем короче
            delay(200);
            digitalWrite(green,LOW);
            delay(200);
            digitalWrite(green, HIGH);
            delay(200);
            digitalWrite(green,LOW);
            delay(200);
            digitalWrite(green, HIGH);
            delay(200);
            digitalWrite(green,LOW);
            delay(200);
            digitalWrite(green, HIGH);
            delay(200);
            digitalWrite(green,LOW);
            delay(200);
            digitalWrite(green, HIGH);
            delay(200);
            digitalWrite(green,LOW);
            delay(200);
            digitalWrite(green, HIGH);
            delay(200);
            digitalWrite(green,LOW);
            delay(200);
            digitalWrite(green, HIGH);
            delay(200);
            digitalWrite(green,LOW);
            delay(200);
            digitalWrite(green, HIGH);
            delay(200);
            digitalWrite(green,LOW);
            delay(200);
            digitalWrite(green, HIGH);
            delay(200);
            digitalWrite(green,LOW);
            delay(200);
            digitalWrite(green, HIGH);
            delay(200);
            digitalWrite(green,LOW);
            delay(200);
            Serial.println("Open ==>");
            digitalWrite(door,LOW);
        }else{
            Serial.println("Wrong");
            digitalWrite(red, HIGH); // моргаем красным светодиодом (долго, а потомучто нефиг взламывать мой замок ! это затруднит взлом )
            delay(400);
            digitalWrite(red,LOW);
            delay(400);
            digitalWrite(red, HIGH);
            delay(400);
            digitalWrite(red,LOW);
            delay(400);
            digitalWrite(red, HIGH);
            delay(400);
            digitalWrite(red,LOW);
            delay(400);
            digitalWrite(red, HIGH);
            delay(400);
            digitalWrite(red,LOW);
            delay(400);
            digitalWrite(red, HIGH);
            delay(400);
            digitalWrite(red,LOW);
            delay(400);
        }
        password.reset();
    }

     

     


  6. @Oksymoron на сколько я понял, после ввода пароля необходимо нажать "*" для того, чтобы замок проверил его и если пароль верный, то соответственно нас впустят, но если после этого нажать еще раз "*", то замок откроется повторно. От этого надо избавиться, я так понимаю? И что имеется в виду под сбросом ардуино, сбросить буфер с паролем или перегрузить контроллер?


  7. Всем доброго времени суток, сегодня поговорим о esp8266 и парсинге погоды с интернета. Это довольно интересная альтернатива метеостанции описанной в параллельной теме и думаю, что первый пост стоит посвятить базовой программе - своего рода фундаменту, на базе которого можно производить дальнейшие модификации от подключения дисплея до прикручивания MQTT протокола для системы умного дома.

    О самом проекте OpenWeatherMap много написано в сети, но я познакомился с ним недавно, как и с микроконтроллером esp8266 который мы будем программировать в доработанной среде Arduino IDE. Но перед тем как начать, необходимо посетить сайт, с которого мы будем вытаскивать данные об окружающей среде.

    И так, OpenWeatherMap предоставляет нам удобный и бесплатный API. На момент публикации статьи (05.2016) мы можем:

    • Производить не более 60 запросов в минуту
    • Не более 50000 запросов в день
    • Два типа прогноза (на 5 и на 16 дней)
    • Доступность сервиса 95%
    • Интервал обновление данных < 2 часов

    Отлично, нас все устраивает. Регистрируемся и в личном кабинете переходим в раздел API Keys. По умолчанию нам уже дадут один ключ, можно воспользоваться им, а можно создать новый и обозвать его более понятным именем. Это довольно актуально, если планируется использовать несколько устройств, да и вообще должен быть порядок везде.

    esp-owm1.png

    Без данного ключа получить доступ к API невозможно.

    Осталось определиться с интересующей нас местностью. API гласит, что есть несколько вариантов:

    • По названию города и коду страны
    • По уникальному идентификатору города
    • По географическим координатам
    • По почтовому индексу

    Мне показалось удобным использовать географические координаты. При этом сервер сам найдет ближайший населенный пункт, а по сути, ближайшую метеостанцию и предоставит данные с неё.

    esp-owm2.png

    Данные по необходимым для Вас координатам можно взять прямо с сайта, воспользовавшись любезно предоставленной картой. При выборе населенного пункта долгота и широта будут указаны в описании.

    Список всех доступных населенных пунктов можно найти тут http://openweathermap.org/help/city_list.txt

    Теперь переходим к самому интересному - скетч. В начале поста я оставил ссылку на ресурс, подробно описывающий все процедуры необходимые, чтобы завести контроллер и использовать Arduino IDE для прошивки. Но я пошел еще более простым путем и приобрел ESP-12E с обвесом под NodeMCU, это позволило прошивать контроллер без нажатия кнопок RESET и FLASH.

    esp-owm3.jpg

    Получать данные от сервера можно в двух форматах, XML и JSON. Последний мне показался более предпочтительным и в связи с этим нам понадобится библиотека ArduinoJson

    Программа

    Скрытый текст
    
    #include <ESP8266WiFi.h>
    #include <ESP8266HTTPClient.h>
    #include <ArduinoJson.h>
    
    const char* ssid     = "WiFi AP name";
    const char* password = "WiFi AP password";
    const String server  = "api.openweathermap.org";
    const String lat     = "51.31";
    const String lon     = "37.89";
    const String appid   = "b1f6b0a1ea9e10feb04eb3a2de2ad2b9";
    const String url     = "http://" + server + "/data/2.5/weather?lat=" + lat + "&lon=" + lon + "&units=metric&appid=" + appid;
    
    unsigned long lastConnectionTime = 0;
    unsigned long postingInterval = 0;
    
    String httpData;
    
    struct weather_structure {
      unsigned int id;
      const char* main;
      const char* icon;
      const char* descript;
      float temp;
      float pressure;
      byte  humidity;
      float speed;
      float deg;
    };
    weather_structure weather;
    
    void setup() {
      Serial.begin(115200);
      Serial.print("\nConnecting to ");
      Serial.println(ssid);
    
      WiFi.begin(ssid, password);
    
      while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
      }
    
      Serial.println("\nWiFi connected\nIP address: ");
      Serial.println(WiFi.localIP());
      Serial.println();
    }
    
    void loop() {
      if (WiFi.status() == WL_CONNECTED) {
        if (millis() < lastConnectionTime) lastConnectionTime = 0;
        if (millis() - lastConnectionTime > postingInterval or lastConnectionTime == 0) {
          if (httpRequest() and parseData()) {
            Serial.println("\nWeather");
            Serial.printf("id: %d\n", weather.id);
            Serial.printf("main: %s\n", weather.main);
            Serial.printf("description: %s\n", weather.descript);
            Serial.printf("icon: %s\n", weather.icon);
            Serial.printf("temp: %d celsius\n", round(weather.temp));
            Serial.printf("humidity: %d %\n", round(weather.humidity));
            Serial.printf("pressure: %d hPa or %d mmHg\n", round(weather.pressure), round(weather.pressure * 0.75));
            Serial.printf("wind's speed: %d\n", round(weather.speed));
            Serial.printf("wind's direction: %d\n", round(weather.deg));
            Serial.println();
          }
        }
      }
    }
    
    bool httpRequest() {
      HTTPClient client;
      bool find = false;
      //client.setTimeout(1000);
      Serial.print("Connecting ");
      client.begin(url);
      int httpCode = client.GET();
    
      if (httpCode > 0) {
        Serial.printf("successfully, code: %d\n", httpCode);
        if (httpCode == HTTP_CODE_OK) {
          httpData = client.getString();
          if (httpData.indexOf(F("\"main\":{\"temp\":")) > -1) {
            lastConnectionTime = millis();
            find = true;
          }
          else Serial.println("Failed, json string is not found");
        }
      }
      else Serial.printf("failed, error: %s\n", client.errorToString(httpCode).c_str());
    
      postingInterval = find ? 600L * 1000L : 60L * 1000L;
      client.end();
    
      return find;
    }
    
    bool parseData() {
      Serial.println(httpData);
    
      DynamicJsonBuffer jsonBuffer;
      JsonObject& root = jsonBuffer.parseObject(httpData);
    
      if (!root.success()) {
        Serial.println("Json parsing failed!");
        return false;
      }
    
      weather.id       = root["weather"][0]["id"];
      weather.main     = root["weather"][0]["main"];
      weather.descript = root["weather"][0]["description"];
      weather.icon     = root["weather"][0]["icon"];
      weather.temp     = root["main"]["temp"];
      weather.humidity = root["main"]["humidity"];
      weather.pressure = root["main"]["pressure"];
      weather.speed    = root["wind"]["speed"];
      weather.deg      = root["wind"]["deg"];
    
      httpData = "";
      return true;
    }

     

    Все, что нам нужно редактировать, это начальные переменные (ключ указан рабочий, но это все временно)

    const char* ssid     = "WiFi AP name";
    const char* password = "WiFi AP password";
    const String server  = "api.openweathermap.org";
    const String lat     = "51.31";
    const String lon     = "37.89";
    const String appid   = "b1f6b0a1ea9e10feb04eb3a2de2ad2b9";

    По умолчанию программа производит запрос информации с сервера каждые 10 минут, но если что-то пойдет не так и информация не будет получена, то интервал уменьшится до 1 минуты, а при ближайшем удачном подключении вернется обратно.

    И так, на данный момент мы собираем:

    • weather.id - Идентификатор погодных условий (будет полезен, см. отсылку с weather.icon)
    • weather.main - Группа метеорологических параметров
    • weather.descript - Описание погодных условий в группе
    • weather.icon - Идентификатор иконки (подробнее ознакомиться с ними можно тут)
    • weather.temp - Текущая температура в градусах Цельсия
    • weather.humidity - Текущая влажность в %
    • weather.pressure - Текущее давление (по умолчанию сервер отдает её в hPa, но при умножении на 0,75 мы получаем mmHg)
    • weather.speed - Скорость ветра метры/сек.
    • weather.deg - Направление ветра в градусах

    Выглядит это следующим образом

    Connecting to Lenovo S90
    ......
    WiFi connected
    IP address: 
    10.10.1.243
    
    Connecting successfully, code: 200
    {"coord":{"lon":37.84,"lat":51.3},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"base":"cmc stations","main":{"temp":18.24,"pressure":996.52,"humidity":92,"temp_min":18.24,"temp_max":18.24,"sea_level":1021.77,"grnd_level":996.52},"wind":{"speed":2.01,"deg":30.5008},"clouds":{"all":48},"dt":1464076525,"sys":{"message":0.0025,"country":"RU","sunrise":1464053115,"sunset":1464110798},"id":487928,"name":"Staryy Oskol","cod":200}
    
    
    Weather
    id: 802
    main: Clouds
    description: scattered clouds
    icon: 03d
    temp: 18 celsius
    humidity: 92 %
    pressure: 997 hPa or 747 mmHg
    wind's speed: 2
    wind's direction: 31

    Как оказалось, ничего сложного в реализации задуманного не было, но местами пришлось повозиться. Думаю, что это будет хорошей отправной точкой для дальнейшего развития проекта.

    • Like 1

  8. @sergon63tlt Уверен, что можно.

    Допустим, что мы говорим о весьма распространённом OBD2. Думаю, что вполне можно обойтись без CAN SHIELD, если воспользоваться Bluetooth адаптером на базе ELM327 и подключаться к нему с помощью CH-06 или подобного модуля. Это минимизирует затраты и избавит от лишних проводов, да и поможет избежать неприятностей если со стороны контроллера что-то замкнули или т.п

    По сути, мы получаем контроллер, подключенный по UART к автомобильной сети и теперь мы сами стали частью системы. Осталось разобраться в протоколе передачи данных. Как гласит теория, бортовой компьютер знает все и вся, а сами данные держит у себя под определенными PID. Отправляем номер PID, а в ответ получаем его значение, которое преобразуем в читаемый вид путем некоторых вычислений. Основную часть можно узнать в wikipedia.

    Например, отправим коды:

    • 0104 - Расчетная нагрузка на двигатель от 0 до 100%
    • 0105 - Температура охлаждающей жидкости двигателя от -40 до 215 градусов Цельсия
    • 010A - Давление топлива от 0 до 765 кПа
    • 010C - Число оборотов двигателя в минуту
    • 010D - Скорость автомобиля
    • 010F - Температура воздуха во впускном коллекторе от -40 до 215 градусов Цельсия
    • 015E - Удельный расход топлива от 0 до 3212,75 литров в час

    Это лишь малая часть информации, взятая из таблицы Mode 01. Большинство значений рассчитываются по формулам, приведенным в этой же таблице.

    Кстати, имеется очень интересная библиотека ArduinoOBD и сопутствующие ей статьи:

    DSC04445.jpgDSC04442.jpg

    Скрытый текст

    DSC04126.jpg

     

     

     

    • Like 1

  9. В 04.05.2016 в 20:15, sergon63tlt сказал:

    Затея интересная! Жаль у меня приборы на CAN шине работают:(а заводить в приборку доп провода не хочется...

    Все собирается прямо в приборке и без разборки и пайки не обойтись никак. Но если жалко только ради подсветки браться за инструмент, то можно воспользоваться CAN BUS SHIELD для Arduino или вариантом подешевле в виде MCP2515 и собирать всю информацию об автомобиле.

    CAN SHIELD

    Arduino_Uno_with_CAN-bus_shield.JPG

    Скрытый текст

    143301_pinout.gif

    http://www.seeedstudio.com/wiki/CAN-BUS_Shield

    MCP2515

    MCP2515_CAN.jpg

    OBDCAN-Arduino-SPI-Communication-Bus-Illustration-Diagram.jpg

    Библиотеки


  10. @Alex13 Если мы говорим о подсветке приборной панели, то ничего сложного тут нет. Самое главное - найти в проводке или на самой панели сигнал, отвечающий за обороты двигателя. По сути, импульсы представляют из себя ШИМ, нам нужно посчитать количество импульсов в секунду, это и будут обороты двигателя. Возможно, будет какой-то множитель (2) и при 1000 об/мин будем иметь 2000 импульсов/мин, что соответствует примерно 33 импульса/сек. Отсюда можно плясать с анимацией.

    В случае с RGB лентой изменять цвет можно с помощью аналогичного ШИМ сигнала от самого микроконтроллера.


  11. На базе светодиодов WS2812 можно баловаться подсветкой приборной панели в зависимости от скорости автомобиля или оборотов двигателя. Аналогичным образом, можно соорудить "ангельские глазки" которые будет иметь базовый цвет белый, а при включении поворота проигрывать желтым цветом анимацию. Но это уже более кропотливая работа в плане эстетики.

    Некоторые народные умельцы собирают автомобильные компьютеры, при этом показания считывают с диагностического разъема.


  12. 13 минуты назад, sergon63tlt сказал:

    Вот теперь " то, что доктор прописал":) Если не секрет какие были изменения?

    @sergon63tlt отключены лишние подтягивающие резисторы встроенные в микроконтроллер и исправил некоторые косяки которые связанные с внедрением функции lock. Хотя на домашнем компьютере версия замка @Alex13 была уже с этими исправлениями... Это еще раз доказывает, что коллективно проект развивается намного быстрее и качественнее.

    • Like 1

  13. 1 час назад, sergon63tlt сказал:

    реле закрытия повело себя как и описано в мониторе СОМ, а вот реле открытия включилось одновременно с реле закрытия и не отключается до поднесения любого из прописанных ключей

    Обновил программу из этого поста. Пробуйте залить её как есть, не внося никаких изменений. Посмотрим что будет в Serial. И подтяните пины 8 и 4 земле.


  14. Да, изначально скетч писался для электромагнитного замка, но управление нагрузкой это всего лишь пару строчек кода, которые легко переписываются. Львиная доля программы - обработка RFID меток и соответствующая реакция на них.

    Для того чтобы было понятно, что мы пытаемся изменить, предлагаю с начало разобраться в том, что происходит при подаче питания с пинами управления реле.

    1. В самом начале программы смотрим содержимое первого байта EEPROM микроконтроллера. Там должно быть число, указывающее на количество ключей.
      byte keys_count = EEPROM.read();
    2. В функции Setup происходит инициализация пинов и на них выставляется высокий логический уровень, опять же все ради Китайских реле, это обесточит их.
      // Реле
      pinMode(PIN_RELAY_1, OUTPUT);
      digitalWrite(PIN_RELAY_1, HIGH);
      pinMode(PIN_RELAY_2, OUTPUT);
      digitalWrite(PIN_RELAY_2, HIGH);
    3. Следующий ключевой момент - принятие решения, открыть замок при отсутствие ключей в памяти и сообщить об этом в Serial или же запереть замок и встать на охрану.
      if(keys_count >  and keys_count < 255) {
        keysRead();
        lock(true);
      }
      else {
        keys_count = ;
        Serial.println(F("The master key is not in memory. The first presentation to the key will be the master!\n"));
        lock(false);
      }
    4. Серьезным отличием от электромагнитного замка является наличие функции void lock(bool lock); именно она манипулирует пинами и переключает реле с паузой в 0.4 сек. Заодно фиксирует последнее состояние замка, своего рода программный концевик на двери.
      void lock(bool lock) {
        digitalWrite(lock ? PIN_RELAY_1 : PIN_RELAY_2, LOW);
        delay(400);
        digitalWrite(lock ? PIN_RELAY_1 : PIN_RELAY_2, HIGH);
        lockStat = lock;
      }

    До момента, описанного в п.3 реле находились в покое и не производили никаких манипуляций с соленоидом. Функция lock, кроме возложенных на неё обязанностей, гарантирует нам, что любое реле будет в работе ровно 0.4 секунды, а затем вернется в свое обычное состояние. Следовательно, если ключ будет отсутствовать в памяти микроконтроллера то по условию в п.3 произойдет вызов lock(false); и это приведет к появлению логического нуля на 6 пине на 0.4 секунды, после вернется логическая единица. Если в EEPROM будет находиться хотя бы один ключ, то произойдет вызов lock(true); и аналогичные манипуляции произойдут с пином 7 - замок закроется.

    Предлагаю проделать следующее: 

    1. Просмотреть вывод в Serial начиная с момента подачи питания, возможно там будет подсказка.
    2. Модернизировать функцию lock и выводить в Serial все, что происходит с реле
      void lock(bool lock) {
        digitalWrite(lock ? PIN_RELAY_1 : PIN_RELAY_2, LOW);
        Serial.println(lock ? "RELAY CLOSE (PIN 6) ACTIVATED" : "RELAY OPEN (PIN 7) ACTIVATED");
        delay(400);
        digitalWrite(lock ? PIN_RELAY_1 : PIN_RELAY_2, HIGH);
        Serial.println(lock ? "RELAY CLOSE (PIN 6) DEACTIVATED" : "RELAY OPEN (PIN 7) DEACTIVATED");
        lockStat = lock;
      }
    3. Можно, но не обязательно, пару фотографий как это выглядит (собрано).

     

    22 часа назад, sergon63tlt сказал:

    еще заметил одну особенность: при случайном замыкании  контактов питания считывателя( +3.3в и общий) питание восстанавливаться, а вот сам считыватель теряется до полного сброса питания.

    Это вполне ожидаемое поведение т.к после подачи питания, микроконтроллер MFRC522 скорее всего ожидает запись в соответствующие регистры. Уверен, что этим занимается PCD_Init().

    • Like 1

  15. Совершенно верно. Изначально подразумевалось использование Китайских реле в качестве управления нагрузкой, а для их работы необходим инвертированный сигнал - HIGH для закрытия и LOW для открытия реле. Естественно, что если выкинуть реле из схемы, то необходимо произвести изменения в коде, на которые указал @Alex13


  16. @Alex13

    Кнопка открытия двери и кнопка сброса памяти

    // Кнопка сброса памяти
    pinMode(PIN_RESET,INPUT_PULLUP);
    key_reset.attach(PIN_RESET);
    key_reset.interval(5);
      
    // Кнопка открытия двери
    pinMode(PIN_OPEN,INPUT_PULLUP);
    key_open.attach(PIN_OPEN);
    key_open.interval(5);

    Сейчас силы брошены на другой проект. Позже опять вернусь к замку.


  17. @Alex13

    Ошибки будут всегда, все мы люди и нам свойственно ошибаться.

    Ищем кусок кода в функции Setup отвечающий за инициализацию пина под кнопку сброса памяти

    // Кнопка сброса памяти
    pinMode(PIN_RESET,INPUT_PULLUP);
    digitalWrite(PIN_RESET, HIGH);
    key_reset.attach(PIN_RESET);
    key_reset.interval(5);

    Необходимо удалить строку

    digitalWrite(PIN_RESET, HIGH);

    Данная строка задействовала внутренний подтягивающий резистор микроконтроллера на 20kOm, но по моей невнимательности, на этом пине он не нужен т.к подтягивает пин к питанию а не к земле. Хотя работает и с ним т.к номинал довольно высок.

    Дополнительный резистор на 4.7kOm не нужен т.к на мой взгляд не дает результата, но натолкнул на мысль об удалении вышеупомянутой строки в коде. Номиналом резистора, подтягивающего пин к питанию можно играться т.к он по сути нужен только для подстраховки, все заведется и без него. Думаю, что стоит проверить уровень напряжения, поступающего на 4-ый пин, возможно его недостаточно для формирования логической единицы, хотя эта же линия используется для формирования единицы 2-ом пине (открытие двери).

    В любом случае, все это догадки, но одну ошибку в коде исправляют. Нужно больше информации для дальнейшего поиска.


  18. @Alex13

    Я пересмотрю все листинги программ, может что-то найду. Но все должно работать, по крайней мере работает на нескольких контроллерах в данный момент. 

    Сама очистка памяти начнется только при длительном удержании кнопки.


  19. Тема является своего рода заметкой по мониторингу источников бесперебойного питания фирмы ippon. Заметкой именно потому, что шаблон был написан уже давно, а статья так и не появилась на свет и как обычно бывает, все постепенно начинает забываться, а наработанная информация растворяться в бытие. Дабы не потерять последние крошки выкидываем оставшиеся наработки. Постепенно тема будет переписана и дополнена.

    ippon1.jpg

    Скрытый текст

    ippon2.jpgippon3.jpg

    Самым удобным способом мониторинга является, уже знакомый нам по теме с ИБП APC, протокол SNMP. Для подключения устройства в локальную сеть необходимо дополнить его сетевым адаптером UPS Ippon Smart Winner 1500/2000/3000.

    Сама карточка поддерживает следующие протоколы:

    • HTTP
    • HTTPS
    • IPv4
    • IPv6
    • NTP
    • SMTP
    • SNMP v1
    • SNMP v2c
    • SNMP v3
    • SSH V1
    • SSH V2
    • SSL
    • TCP/IP
    • Telnet

    ippon4.jpg

    Скрытый текст

    ippon5.jpg

    ippon6.jpg

    ippon7.jpg

    На диске, идущем в комплекте, имеется весь необходимый софт и документация. Также, если немного покопаться, можно найти MIB файл со всеми поддерживаемыми OID. На всякий случай мы прикрепим к посту MIB файл на базе которого был создан текущий шаблон.

    Перед тем как перейти к шаблону хочу отметить, что он содержит макрос {$SNMP_COMMUNITY} значение которого необходимо изменить на используемое вами Community.

    ippon_community.jpg

    Шаблон позволяет производить мониторинг следующих параметров:

    1. The measured output real complex power in VA
    2. Величина отрицательного напряжения аккумуляторной батареи
    3. Величина положительного напряжения аккумуляторной батареи
    4. Версия прошивки Network Management Card
    5. Версия прошивки микропроцессора
    6. Время оставшееся до истощения заряда батареи
    7. Время прошедшее с момента перехода на работу от батареи
    8. Входная мощность
    9. Входное напряжение
    10. Входной ток
    11. Выходная мощность
    12. Выходная нагрузка ИБП в процентах от номинальной мощности
    13. Выходное напряжение
    14. Выходное напряжение
    15. Выходной ток
    16. Код неисправности ИБП из таблицы Q6
    17. Код предупреждения ИБП из таблицы Q6
    18. Максимальная разрешенная выходная нагрузка
    19. Максимальная разрешенная рабочая температура
    20. Модель ИБП
    21. Название производителя
    22. Описание напитанной нагрузки (устанавливается администратором)
    23. Описание устройства (устанавливается администратором)
    24. Серийный номер
    25. Статус батареи
    26. Текущее состояние ИБП
    27. Температура батареи
    28. Температура внутри ИБП
    29. Уровень заряда батареи
    30. Частота тока на входе
    31. Частота тока на выходе

    Данных параметров должно быть достаточно для создание базовых проверок и триггеров. Мы добавили несколько для основных тревог.

    ippon_triggers.jpg

    И пару базовых графиков.

    ippon_g1.png

    ippon_g2.png

    Данный шаблон вполне подойдет для создания на его основе Ваших решений. Мы будем рады увидеть Ваши наработки или помочь Вам в реализации, каких либо идей по мониторингу этих источников бесперебойного питания.

    EPPC.MIB.zip

    templates_ups_ippon.xml.zip

    • Like 2

  20. @svchekalin

    Подключение RFID ридера RC522 к Arduino Mega 2560

    mega2560_rfid-rc522.png

    С программой все практически без изменений. Используем всю туже библиотеку MFRC522.h, и только изменим в настройках два пина - RST и SS.

    #include <SPI.h>
    #include <MFRC522.h>
    
    #define RST_PIN         5
    #define SS_PIN          53
    
    MFRC522 mfrc522(SS_PIN, RST_PIN);

     


  21. 36 минут назад, Alex13 сказал:

    Но ещё вопросик, можно сделать так, чтоб пин A3 активировался, только когда замок закрыт?

    // Автоматическое отключение сигнализации
    if(alarmTimer != 0) {
      if(millis()/1000 - alarmTimer > 60) {
        alarmTimer = 0;
        digitalWrite(PIN_ALARM_1M, invert ? HIGH : LOW);
        Serial.println(F("Automatic shutdown of the first signaling channel.\n"));
      }
    }
    else {
      if(digitalRead(PIN_ALARM_FORCED) and lockStat) {
        alarmTimer = millis()/1000;
        digitalWrite(PIN_ALARM_1M, invert ? LOW : HIGH);
        digitalWrite(PIN_ALARM_LONG, invert ? LOW : HIGH);
      }
    }

     

×
×
  • Создать...