Термин звучит красиво, но его можно рассматривать просто как действие определения кода, который определяет код. В Ruby on Rails есть много таких примеров. Каждый раз, когда вы используете макрос, определяющий дополнительные методы, вы используете метапрограммирование.

Мы не просто собираемся использовать методы, определенные кем-то другим, чтобы облегчить нашу жизнь, мы собираемся написать эти методы сами. По сути, мы собираемся создать целый DSL (предметно-ориентированный язык) для моделирования Индикатора.

Что такое контакт GPIO?

Выводы GPIO специфичны для RaspberryPI и обозначают вывод «ввод-вывод общего назначения». Они физически находятся на устройстве и могут быть включены, выключены или иметь то, что мы назовем «нулевым» значением. При программировании на RaspberryPI мы можем использовать программное обеспечение для проверки значения вывода в любой момент времени, а также для изменения значения.

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

Таблица истинности индикатора

Во-первых, давайте рассмотрим краткую таблицу истинности, которая показывает, в каком состоянии будет находиться индикатор при настройке соответствующих контактов GPIO.

Indicator Name | State  | PIN 1 | PIN 2 | PIN 3 | PIN 4 | PIN 5 |
-----------------------------------------------------------------
Status         | OFF    |       |       |       |       |       |
               | BLUE   | ON    |       |       |       |       |
               | GREEN  | ON    | ON    |       |       |       |
Motor          | OFF    |       |       |       |       |       |
               | YELLOW |       |       | ON    |       |       |
               | RED    |       |       | ON    | ON    |       |
Power          | OFF    |       |       |       |       |       |
               | GREEN  |       |       |       |       | ON    |

В приведенной выше таблице у нас есть 3 разных индикатора. «Статус» может быть выключенным, синим или зеленым; «Мотор» может быть выключенным, желтым или красным; «Питание» может быть либо выключено, либо зеленым.

Общий дизайн программного обеспечения

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

Процесс монитора был создан для простого многократного планирования рабочего процесса Sidekiq, при этом рабочий процесс Sidekiq фактически проверяет желаемое состояние и манипулирует значением контактов GPIO, чтобы физический светодиод отражал это состояние.

Мы собираемся использовать Redis для хранения состояния каждого индикатора в любой момент времени, иметь службу для перевода индикатора в определенное состояние (например, запись в Redis), а затем рабочий процесс Sidekiq запросит у Redis текущее состояние. и настроить оборудование.

Простейшая попытка

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

В приведенном выше примере есть несколько вещей, на которые я хотел бы обратить внимание:

  • Я уже представил некоторую концепцию определения возможных состояний и того, как они выглядят, как методы уровня класса.
  • Мы предоставили начало метода для перехода в определенное состояние

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

indicator = StatusIndicator.new
indicator.transition!(StatusIndicator::CHARGED)

Мы сохраним этот интерфейс при рефакторинге и уточнении того, как мы определяем поведение нашего индикатора ниже.

Представляем суперкласс

Зная, что мне нужно написать больше индикаторов (в этом примере еще два, но в нашем реальном приложении было еще несколько), я уже знаю, что хочу поделиться как можно большим количеством логики, используя наследование.

Имея это в виду, я перепишу, чтобы ввести родительский класс и подкласс.

Наиболее естественный рефакторинг для введения базового класса выглядит примерно так:

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

Добавление DSL

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

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

Определение состояний

В приведенном выше примере большая часть волшебства происходит в методе «define» уровня класса, который принимает блок.

define do |router|
  router.on OFF, color: :off, blinks: false
end

В этом фрагменте мы представили простой способ определить, как должен выглядеть индикатор, когда он выключен. Это заменило методы класса «рендеринга» и «состояния», которые у нас были ранее.

Определение представления GPIO

Мы не стали кодировать способ сопоставления состояния с фактическими значениями GPIO в наших простейших примерах, но мы включили это сопоставление здесь.

define do |router|
  router.when :blue do
    enable_pin(GPIO_PIN_BLUE)
    disable_pin(GPIO_PIN_GREEN)
  end
end

Преимущества DSL

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

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

Реализация с помощью метапрограммирования

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

В этом родительском классе мы создаем несколько функций для использования каждым подклассом.

Во-первых, это определение метода «define»:

def self.define(&block)
  router = Router.new
  block.call(router)
  self.state_machine = router
end

Это просто предоставляет подклассу способ настроить экземпляр класса маршрутизатора, который хранится в родительском классе как атрибут класса, к которому можно будет получить доступ позже (называемый «state_machine»).

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

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

class Indicator::Router
  def initialize
    on(Indicator::OFF, color: :off, blink: false)
  end
end

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

class Indicator::Router
  def when(color, &block)
    renderings[color.to_s] = block
  end
end

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

class Indicator
  def render(status)
    if visually_off?(status)
      turn_off
      return
    end
    case status.color
    when 'red' then turn_red
    when 'green' then turn_green
    when 'yellow' then turn_yellow
    when 'blue' then turn_blue
    end
  end
end

Реализация «turn_red» или «turn_blue» определяется динамически (это метапрограммирование, верно?) путем нахождения блока, сохраненного ранее при вызове «когда» на маршрутизаторе, и выполнения этого блока.

class Indicator
  %w(off red green yellow blue).each do |state|
    define_method("turn_#{ state }") do
      render_proc = state_machine.renderings[state]
      raise "Unknown how to render color: #{ state } for LED: #{ self.class.name }" if render_proc.nil?
      instance_exec(&render_proc)
    end
  end
end

Собираем все вместе

Чтобы объединить все это, мы упростили создание экземпляра «Маршрутизатора», настройку маршрутизатора и сохранение его для каждого дочернего класса. Тогда при вызове «переход!» или «рендеринг» в дочернем классе, реализация просматривает определение state_machine от маршрутизатора, чтобы выполнить процедуру, или ищет детали из хэша для взаимодействия с реальным оборудованием.

Использование

Когда это определение будет завершено, как будет выглядеть индикатор из нашего процесса монитора? Супер просто:

Наш окончательный интерфейс для взаимодействия с нашими индикаторами сводится к следующему:

# in our project
indicator = StatusIndicator.new
indicator.charging! # calls transition! storing state in redis
# change to another state, watch the indicator update
indicator.charged!
# in separate monitor (background) process
every 1.second do
  renderer = RenderIndicatorStatus.new
  renderer.perform(indicator.class.name)
end

Тестирование с помощью RSpec

Тестирование с помощью RSpec также было легким и читабельным при таком подходе. Мы протестировали базовый класс Indicator, поскольку он взаимодействовал с другими существующими сервисами для управления аппаратными значениями (используя имитации). Чтобы проверить фактическое определение состояния и рендеринга, мы написали собственный сопоставитель, а также несколько общих примеров для удобочитаемого теста, например:

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

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

Мы будем рады услышать ваши отзывы, поэтому оставьте нам комментарий!