ГлавнаяАкадемияДатчики и входы: нормализация сигналов → Работа с кнопками и выключателями: короткое, длинное, двойное нажатие

Работа с кнопками и выключателями: короткое, длинное, двойное нажатие

Урок 3 · Датчики и входы: нормализация сигналов · 30 мин · theory

Введение: Дребезг контактов и его устранение

В предыдущих уроках (COURSE-04-M02-L01, L02) мы рассмотрели, что кнопка или выключатель по своей сути является устройством типа «сухой контакт». При нажатии или отпускании кнопки происходит физическое замыкание или размыкание двух металлических пластин. Однако этот процесс не является идеальным и мгновенным.

На микроскопическом уровне, в момент замыкания, контакты многократно соударяются друг с другом, прежде чем зафиксироваться в стабильном положении. Этот эффект, похожий на дрожание или вибрацию, называется дребезгом контактов (contact bounce). Каждый такой микро-отскок генерирует электрический импульс, и для чувствительной цифровой логики контроллера одно физическое нажатие выглядит как очень быстрая серия из десятков событий «включено-выключено».

Визуализация процесса дребезга: вместо одного чистого переключения из 0 в 1, система видит серию ложных импульсов.

> ⚠️ Внимание: Без программного устранения дребезга одна и та же кнопка может генерировать десятки ложных событий, что приведет к непредсказуемой работе автоматики и повышенной нагрузке на контроллер. Например, сценарий «включить свет по нажатию» может сработать несколько раз, в результате чего свет будет хаотично мигать.

Основной метод борьбы с этим явлением — программная фильтрация, или debounce. Идея заключается в том, чтобы после первого зарегистрированного изменения состояния (нажатия) игнорировать все последующие изменения в течение короткого промежутка времени (например, 50 миллисекунд).

В Node-RED это можно реализовать двумя способами:

  • Использование узла `delay`. Этот встроенный узел имеет режим «Ограничение частоты» (`rate limit`). Если настроить его на пропуск только одного сообщения в заданный интервал (например, 50 мс) и отбрасывание промежуточных, он эффективно отфильтрует весь «шум» дребезга.
  • Использование специализированного узла `node-red-contrib-debounce`. Этот узел предоставляет более гибкие настройки, но требует отдельной установки и выполняет ту же базовую функцию.
  • Для большинства задач достаточно встроенного узла `delay`, который мы и будем использовать в наших примерах.

    ---

    Практика: Детектирование короткого нажатия

    После того как мы отфильтровали дребезг, мы получаем чистое событие нажатия (`true`) и отпускания (`false`). Теперь наша задача — научиться отличать простое короткое нажатие от других действий.

    Базовая цепочка узлов для предварительной обработки сигнала от кнопки выглядит так:

    [MQTT In] -> [delay (debounce)] -> [rbe] -> [Дальнейшая логика]
    

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

    Простейший способ зафиксировать такое событие — использовать узел `trigger`.

    Логика с использованием узла `trigger`

  • Цепочка: `... -> rbe -> trigger -> Function -> ...`
  • На узел `trigger` поступают сообщения `true` (кнопка нажата) и `false` (кнопка отпущена).
  • Настроим `trigger` следующим образом:
  • * `Send`: `msg.payload` (будет отправлять `false`).

    * `then wait for`: `500 ms`.

    * `then send`: `{"click": "single"}` (строка или JSON).

    * When message arrives: `If msg.payload is false...` -> `send the 'wait' message`. `If msg.payload is not false...` -> `do nothing`.

    Такая конфигурация работает по следующему принципу:

    Хотя этот метод работает, он не очень гибок и не позволяет легко добавить логику длинных или двойных нажатий. Более профессиональный подход — использовать узел `Function` для управления состоянием.

    Логика с использованием узла `Function`

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

    // Входящее сообщение msg.payload может быть true (нажато) или false (отпущено)
    
    

    // Получаем временную метку последнего нажатия из контекста узла

    let press_time = context.get('press_time') || 0;

    if (msg.payload === true) {

    // Кнопка нажата. Сохраняем время нажатия в контексте.

    context.set('press_time', Date.now());

    // На этом этапе мы не отправляем никаких сообщений, т.к. еще не знаем,

    // будет ли это короткое или длинное нажатие.

    return null;

    }

    else if (msg.payload === false && press_time !== 0) {

    // Кнопка отпущена. Проверяем, было ли перед этим зафиксировано нажатие.

    // Рассчитываем длительность удержания кнопки

    const duration = Date.now() - press_time;

    // Сбрасываем время нажатия, чтобы избежать повторной обработки

    context.set('press_time', 0);

    // Порог для длинного нажатия - 800 мс.

    // Все, что короче, считаем коротким нажатием.

    if (duration < 800) {

    msg.payload = {

    "event": "click",

    "type": "single"

    };

    node.status({fill:"blue", shape:"dot", text:"Single click"});

    return msg;

    }

    // Если нажатие было длиннее, мы просто игнорируем его на этом этапе.

    // Логика длинного нажатия будет рассмотрена далее.

    }

    return null; // Во всех остальных случаях ничего не делаем.

    Этот подход гораздо более управляемый и является основой для реализации более сложных сценариев.

    ---

    Практика: Реализация длинного нажатия (Hold)

    Длинное нажатие (или удержание) — это событие, которое генерируется, когда пользователь удерживает кнопку нажатой дольше определенного порога (например, 1 секунда). Это часто используется для управления диммированием света, регулировки громкости или вызова специальных функций.

    > 💡 Подсказка: Для управления диммируемыми светильниками, событие `long_press` можно использовать для запуска плавного изменения яркости, а `release` (отпускание кнопки после длинного нажатия) — для остановки диммирования.

    Для реализации этой логики мы будем использовать встроенные в узел `Function` таймеры: `setTimeout` для запуска отложенной задачи и `clearTimeout` для ее отмены.

    Архитектура потока и логика

    Мы модифицируем наш предыдущий узел `Function`, чтобы он мог определять и короткие, и длинные нажатия.

    Логика:
  • Когда приходит сообщение `msg.payload = true` (кнопка нажата), мы запускаем таймер (`setTimeout`). Устанавливаем его, например, на 800 мс. ID этого таймера мы сохраняем в контексте узла.
  • Возможны два исхода:
  • Сценарий А (короткое нажатие): Приходит сообщение `msg.payload = false` (кнопка отпущена) до того*, как таймер сработал. В этом случае мы отменяем таймер с помощью `clearTimeout` и отправляем событие `single_click`.

    * Сценарий Б (длинное нажатие): Пользователь продолжает удерживать кнопку, и таймер на 800 мс успевает сработать. Функция, привязанная к таймеру, выполняется и отправляет событие `long_press_start`. Чтобы система знала, что длинное нажатие уже зафиксировано, мы устанавливаем флаг в контексте.

  • Когда после долгого нажатия приходит `msg.payload = false`, мы проверяем флаг. Если он установлен, мы отправляем событие `long_press_stop` и сбрасываем флаг.
  • Пример кода для узла `Function`

    // Получаем таймер и флаги из контекста
    

    let long_press_timer = context.get('long_press_timer');

    let is_long_press = context.get('is_long_press') || false;

    if (msg.payload === true) {

    // Кнопка нажата

    // Устанавливаем флаг, что длинное нажатие еще не произошло

    context.set('is_long_press', false);

    // Запускаем таймер, который сработает через 800 мс

    long_press_timer = setTimeout(() => {

    // Таймер сработал - это длинное нажатие

    // Устанавливаем флаг

    context.set('is_long_press', true);

    // Отправляем сообщение о начале длинного нажатия

    node.send({

    "topic": msg.topic, // Сохраняем исходный topic для идентификации кнопки

    "payload": {

    "event": "hold",

    "type": "start"

    }

    });

    node.status({fill:"green", shape:"dot", text:"Long Press Start"});

    }, 800); // Порог длинного нажатия

    // Сохраняем ID таймера в контекст, чтобы его можно было отменить

    context.set('long_press_timer', long_press_timer);

    } else {

    // Кнопка отпущена (msg.payload === false)

    // Отменяем таймер, который был запущен при нажатии.

    // Если он уже сработал, clearTimeout ничего не сделает.

    clearTimeout(long_press_timer);

    if (is_long_press) {

    // Если флаг is_long_press установлен, значит, кнопка была отпущена

    // ПОСЛЕ того, как было зафиксировано длинное нажатие.

    node.send({

    "topic": msg.topic,

    "payload": {

    "event": "hold",

    "type": "stop"

    }

    });

    node.status({fill:"grey", shape:"dot", text:"Long Press Stop"});

    } else {

    // Если флаг is_long_press не установлен, значит, кнопка была отпущена

    // ДО того, как сработал таймер. Это было короткое нажатие.

    node.send({

    "topic": msg.topic,

    "payload": {

    "event": "click",

    "type": "single"

    }

    });

    node.status({fill:"blue", shape:"dot", text:"Single Click"});

    }

    // Сбрасываем флаг в любом случае

    context.set('is_long_press', false);

    }

    // Мы отправляем сообщения асинхронно через node.send(),

    // поэтому основной поток можно завершить.

    return null;

    ---

    Практика: Двойное и множественное нажатие

    Двойное нажатие (double click) — это два коротких нажатия, выполненных в течение заданного временного окна (например, 400 мс). Этот тип события позволяет назначить дополнительную функцию на ту же кнопку, например, включить свет на 100% яркости.

    Реализация этого механизма требует более сложного управления состоянием. Нам нужно не только отслеживать нажатия, но и считать их количество в коротком промежутке времени. Ключевую роль здесь играет контекст (`flow context` или `node context`) для хранения счетчика и временных меток.

    Логика двойного нажатия

  • Мы получаем событие `single_click` от нашей предыдущей логики.
  • При получении первого `single_click`:
  • * Мы не отправляем событие немедленно.

    * Вместо этого мы увеличиваем счетчик нажатий в контексте (`clicks = 1`).

    * Запускаем таймер (например, на 400 мс).

  • Возможны два исхода:
  • * Сценарий А (второе нажатие): В течение этих 400 мс приходит еще одно событие `single_click`. Мы увеличиваем счетчик (`clicks = 2`), отменяем предыдущий таймер и снова его запускаем.

    * Сценарий Б (таймаут): Таймер на 400 мс срабатывает. Это означает, что в заданном окне больше не было нажатий. Мы проверяем значение счетчика:

    * Если `clicks = 1`, отправляем итоговое событие `single_click`.

    * Если `clicks = 2`, отправляем итоговое событие `double_click`.

    * После отправки сбрасываем счетчик в 0.

    Пример реализации на узле `Function`

    Этот узел должен стоять после узла, определяющего короткие нажатия.

    // Этот узел принимает на вход только события короткого нажатия,
    

    // например, msg.payload = {"event": "click", "type": "single"}

    // Получаем счетчик кликов и таймер из контекста

    let clicks = context.get('clicks') || 0;

    let dispatch_timer = context.get('dispatch_timer');

    // Увеличиваем счетчик при каждом входящем сообщении

    clicks++;

    context.set('clicks', clicks);

    // Если уже был запущен таймер отправки, отменяем его,

    // так как пришло новое нажатие в серии.

    clearTimeout(dispatch_timer);

    // Запускаем новый таймер. Он сработает через 400мс,

    // если больше не будет нажатий.

    dispatch_timer = setTimeout(() => {

    // Таймер сработал. Определяем, сколько нажатий было в серии.

    const final_clicks = context.get('clicks');

    let event_type = null;

    if (final_clicks === 1) {

    event_type = "single";

    } else if (final_clicks === 2) {

    event_type = "double";

    } else if (final_clicks >= 3) {

    event_type = "triple"; // Можно расширить и на тройные нажатия

    }

    if (event_type) {

    node.send({

    "topic": msg.topic,

    "payload": {

    "event": "click",

    "type": event_type

    }

    });

    node.status({fill:"purple", shape:"dot", text: event_type + " click"});

    }

    // Сбрасываем счетчик для следующей серии

    context.set('clicks', 0);

    }, 400); // Временное окно для двойного/тройного нажатия

    // Сохраняем ID таймера в контекст

    context.set('dispatch_timer', dispatch_timer);

    return null; // Сообщение будет отправлено асинхронно

    ---

    Сборка итоговой схемы: универсальный обработчик кнопок

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

    🔗 Связанный материал: Полученные в этом уроке события (`single_click`, `double_click`, `hold_start`) можно использовать для вызова комплексных сценариев, как описано в Уроке COURSE-06-M02-L03 "Реализация комплексных сценариев автоматизации".

    Финальная цепочка узлов

                                                +-> (Выход 1: single click) 
    

    |

    [MQTT In] -> [delay (debounce)] -> [Универсальный обработчик] -> [Switch] --+-> (Выход 2: double click)

    (Subflow или Function) (по payload.type)

    |

    +-> (Выход 3: hold_start)

    |

    +-> (Выход 4: hold_stop)

  • `MQTT In`: Получает состояние кнопки (`true`/`false`).
  • `delay`: Устраняет дребезг контактов (50 мс).
  • Универсальный обработчик: Комбинированный `Function`-узел или субпоток, реализующий всю логику short/long/double press.
  • `Switch`: Маршрутизирует итоговые события на разные ветки потока в зависимости от значения в `msg.payload.type`. Это позволяет легко привязать разные действия к разным типам нажатий.
  • Рекомендации по настройке таймеров

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

    | Параметр | Рекомендуемое значение | Описание |

    | ------------------------- | ---------------------- | ------------------------------------------------------------------------------- |

    | Debounce time | 50 мс | Время фильтрации дребезга. Должно быть больше длительности дребезга. |

    | Long press threshold | 800 мс - 1000 мс | Время удержания для срабатывания длинного нажатия. |

    | Double-press window | 400 мс - 500 мс | Максимальный интервал между двумя нажатиями, чтобы они считались двойным. |

    Преимущества стандартизации

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

    Этот подход является краеугольным камнем при построении надежных и интуитивно понятных систем управления на платформе HI.