Kitabı oku: «Обратные вызовы в C++», sayfa 11
6.1.2. Сценарий функционирования
Базовый сценарий функционирования модуля следующий.
Основным компонентом, поставляющим информацию, являются датчики. Они могут производить измерения трех типов: текущее, сглаженное и производное. Для идентификации датчикам присваиваются уникальные номера.
Перед началом работы производится настройка, т. е. определяется состав датчиков, с которых будут сниматься показания. Настройка не статическая, она может изменяться в процессе работы.
В любой момент приложение может запросить показания датчиков как в синхронном, так и в асинхронном режиме. Показания возвращаются только для функционирующих датчиков, в приложении должна иметься возможность проверить их работоспособность.
Коммуникация с датчиками осуществляется через протокол USB либо Ethernet путем пересылки / получения команд в соответствии с заданным протоколом.
В процессе работы модуль должен отслеживать и уведомлять приложение о том, что некоторые показатели превышают заданные пороговое значение. Состав измеряемых значений и их предельные величины настраиваются приложением.
В соответствии с описанием структура системы может быть представлена следующим образом (Рис. 26).
Рис. 26. Структурная схема
Приложение через интерфейс обращается к функциям модуля. В зависимости от вызываемой функции интерфейс обращается к соответствующим компонентам и возвращает результат.
Компонент «Асинхронный вызов» предназначен для выполнения асинхронных вызовов. «Наблюдатель» предназначен для отслеживания пороговых значений. «Контейнер» хранит список датчиков. Компонент «Датчик» через компонент «драйвер» обращается к аппаратному обеспечению.
6.1.3. Декомпозиция системы
Итак, в соответствии методологией объектно-ориентированного анализа необходимо определить состав классов и связи между ними, отражающие предметную область. Нам будут необходимы следующие классы:
• класс для работы с датчиком;
• контейнер для хранения указанных классов;
• драйвер, обеспечивающий низкоуровневое взаимодействие с аппаратурой;
• очередь для выполнения асинхронных запросов;
• класс для отслеживания пороговых значений;
• интерфейсный класс, который будет взаимодействовать с приложением для вызовов соответствующих функций модуля.
Обобщенная диаграмма классов модуля представлена на Рис. 2735.
Рис. 27. Обобщенная диаграмма классов
Класс ISensorControl объявляет интерфейс модуля, класс SensorControl реализует указанный интерфейс. SensorControl содержит классы Observer (отслеживает пороговые значения), CommandQueue (очередь комманд для асинхронных запросов), SensorContainer (реализует контейнер для хранения классов для работы с датчиком).
Интерфейс для работы с датчиками объявлен в классе ISensor, обощенная реализация интерфейса осуществляется в классе SensorAbstract. Указанный класс хранит указатель на IDriver, который используется для получения значений датчиков. В классе IDriver объявляется интерфейс для взаимодействия с аппаратурой.
6.2. Реализация классов
6.2.1. Общие определения
В Листинг 86 представлены общие объявления типов.
Листинг 86. Общие объявления типов (SensorDef.h)
namespace sensor
{
class ISensor;
class IDriver;
using SensorNumber = unsigned int; // (1)
using SensorValue = double; // (2)
using CheckAlertTimeout = unsigned int; // (3)
enum class SensorType : uint32_t // (4)
{
Spot = 0,
Smooth = 1,
Derivative = 2,
};
enum class DriverType : uint32_t // (5)
{
Simulation = 0,
Usb = 1,
Ethernet = 2
};
enum class AlertRule : uint32_t // (6)
{
More = 0,
Less = 1
};
using SensorPointer = std::shared_ptr<ISensor>; // (7)
using DriverPointer = std::shared_ptr<IDriver>; // (8)
using SensorValueCallback = std::function<void(SensorNumber, SensorValue)>; // (9)
using SensorAlertCallback = std::function<CheckAlertTimeout(SensorNumber, SensorValue)>; // (10)
}; //namespace sensor
В строке 1 объявлен тип для номера датчика, в строке 2 объявлен тип значения, возвращаемого датчиком. В строке 3 объявлен тип значения интервала опроса датчиков для сигнализации пороговых значений.
В строке 4 объявлены идентификаторы типов датчиков, в строке 5 объявлены идентификаторы драйверов. В строке 6 объявлены идентификаторы правил для задания пороговых значений (сигнализация превышения или опускания ниже заданного значения).
В строке 7 объявлен тип для хранения указателей классов датчиков, в строке 8 – тип для хранения указателей классов драйверов. В строке 9 объявлен тип обратного вызова, в который передается значение датчика, в строке 10 – тип обратного вызова, в который передается значение датчика в случае срабатывания сигнализации порогового значения.
6.2.2. Обработка ошибок
В процессе работы любой программы могут ситуации, приводящие к ошибкам. Причины ошибок могут быть самыми различными: неправильные действия пользователя, некорректная работа ПО, сбои в работе оборудования и т. п. Таким образом, возникает необходимость реализации подсистемы обработки ошибок, которая осуществляет восстановление работоспособности компонента после возникновения ошибочной ситуации и уведомление об этом пользователя.
В общем случае существуют две модели обработки ошибок: анализ кодов возврата и использование исключений. Несмотря на то, что использование исключений в последнее время подвергается серьезной критике, вплоть до того, что в новых языках программирования от них избавляются, в C++ указанный механизм остается востребованным, и мы также им воспользуемся. Объявления для формирования исключений представлены в Листинг 87.
Листинг 87. Исключения для обработки ошибок (Errors.h)
namespace sensor
{
enum class SensorError: uint32_t // (1)
{
NoError = 0,
NotInitialized = 1,
UnknownSensorType = 2,
UnknownSensorNumber = 3,
SensorIsNotOperable = 4,
DriverIsNotSet = 5,
InvalidArgument = 6,
NotSupportedOperation = 7,
InitDriverError = 8
};
class sensor_exception : public std::exception // (2)
{
public:
sensor_exception(SensorError error);
SensorError code() const;
virtual const char* what() const;
static void throw_exception(SensorError error); // (3)
private:
SensorError code_;
};
}; //namespace sensor
В строке 1 объявлены коды возможных ошибок, в строке 2 объявлен класс исключений. Если при выполнении где-то в коде возникает ошибка, то в этом месте нужно вызвать метод, объявленный в строке 3. Указанный метод выбросит исключение с соответствующим кодом.
6.2.3. Драйвер
Драйвер предназначен для взаимодействия с аппаратным обеспечением. Класс, представляющий обобщенный интерфейс для работы с драйвером, приведен в Листинг 88.
Листинг 88. Интерфейс для работы с драйвером (DriverInterface.h)
namespace sensor
{
class IDriver
{
public:
virtual void initialize() = 0; // (1)
virtual void activate(SensorNumber number) = 0; // (2)
virtual bool isOperable(SensorNumber number) = 0; // (3)
virtual SensorValue readSpot(SensorNumber number) = 0; // (4)
virtual SensorValue readSmooth(SensorNumber number) = 0; // (5)
virtual SensorValue readDerivative(SensorNumber number) = 0; // (6)
virtual ~IDriver() = default;
static DriverPointer createDriver(DriverType type); // (7)
};
}; //namespace sensor
В строке 1 объявлен метод для инициализации драйвера. В строке 2 объявлен метод для активации датчика. В строке 3 объявлен метод, возвращающий признак работоспособности датчика. В строках 4, 5 и 6 объявлены методы для чтения соответственно текущих, сглаженных и производных значений. Метод в строке 7 представляет собой фабрику классов, в котором происходит создание класса соответствующего типа.
От общего интерфейса наследуются классы, реализующие драйверы различных типов. В нашей системе реализованы три типа драйверов: драйвер для работы с шиной USB; драйвер для работы через сеть Ethernet; имитируемый драйвер. Диаграмма классов изображена на Рис. 28.
Рис. 28. Диаграмма классов, реализующих драйверы
Драйверы для работы с физическими устройствами формируют команды, посылают их через соответствующие протоколы и возвращают результаты. Реализацию этих драйверов мы рассматривать не будем, поскольку работа с hardware – это отдельная тема, для изучения которой требуется не одна книга. Для нас представляет интерес реализация имитируемого драйвера.
Очевидно, что имитируемый драйвер должен возвращать заранее заданные значения. Самое простое решение, лежащее на поверхности, заключается в том, чтобы хранить эти значения в глобальных или статических переменных и возвращать их в соответствующих методах. Однако в этом случае имитация будет очень примитивной: для всех датчиков будет возвращаться одно и то же значение. Можно хранить свое возвращаемое значение в каждом классе датчика, что больше похоже на работу в реальной системе, но это также не лишено недостатков: мы не можем моделировать изменения показателей в динамике. Лучшим решением было бы предоставить возможность пользователю возвратить значение в момент запроса, для чего нам, конечно же, понадобится обратный вызов. Обратный вызов будет использоваться по схеме «Запрос данных» (см. п. 1.2.1).
Итак, для реализации интерфейса имитируемого драйвера нам понадобятся дополнительные методы и определения (см. Листинг 89).
Листинг 89. Имитируемый драйвер (DriverImpl.h)
class DriverSimulation : public IDriver
{
public:
enum ReadType { READ_SPOT = 0, READ_SMOOTH = 1, READ_DERIVATIVE = 2 }; // (1)
using OnReadValue = std::function<SensorValue(SensorNumber, ReadType)>; // (2)
using OnOperable = std::function<bool(SensorNumber)>; // (3)
void initialize() override;
void activate(SensorNumber number) override;
bool isOperable(SensorNumber number) override;
void setDefaultValue(SensorValue value); // (4)
void setDefaultOperable(bool isOperable); // (5)
void setReadValue(OnReadValue value); // (6)
void setOperable(OnOperable operable); // (7)
SensorValue readSpot(SensorNumber number) override; // (8)
SensorValue readSmooth(SensorNumber number) override; // (9)
SensorValue readDerivative(SensorNumber number) override; // (10)
static IDriver* create();
protected:
DriverSimulation();
private:
OnReadValue getValue_; // (11)
OnOperable getOperable_; // (12)
SensorValue defaultValue_ = 0; // (13)
bool defaultOperable_ = true; // (14)
};
В строке 1 объявляется перечисление для указания используемого метода чтения показателей. В строке 2 и 3 объявляются типы для обратных вызовов. Переменные соответствующих типов для хранения вызовов объявлены в строках 11 и 12. Настройка вызовов производится в методах 6 и 7. Кроме того, объявляются переменные для хранения значений по умолчанию (строки 13 и 14), эти переменные настраиваются в методах 4 и 5.
Реализацию чтения показателей продемонстрируем на примере получения текущего значения датчика (Листинг 90).
Листинг 90. Чтение текущего значения датчика в имитируемом драйвере (DriverImpl.cpp)
SensorValue DriverSimulation::readSpot(SensorNumber number)
{
if (getValue_) // (1)
{
return getValue_(number, READ_SPOT); // (2)
}
else
{
return defaultValue_; // (3)
}
}
В строке 1 проверяется, настроен ли обратный вызов. Если настроен, то через него запрашивается значение для соответствующего датчика. Информацией вызова здесь является номер датчика и метод чтения показателей (строка 2). Если обратный вызов не настроен, то возвращается значение по умолчанию (строка 3).
6.2.4. Датчик
Обобщенный интерфейсный класс для работы с датчиком приведен в Листинг 91.
Листинг 91. Интерфейсный класс для роботы с датчиком (SensorInterface.h)
namespace sensor
{
class ISensor
{
public:
virtual void setDriver(DriverPointer driverPointer) = 0; // (1)
virtual DriverPointer getDriver() = 0; // (2)
virtual double getValue() = 0; // (3)
virtual bool isOperable() = 0; // (4)
virtual ~ISensor() = default;
static SensorPointer createSensor(SensorType type, SensorNumber number, DriverPointer driverPointer); // (5)
};
}; //namespace sensor
В строке 1 объявлен метод для настройки драйвера, с которым будет работать датчик. Получить используемый драйвер можно с помощью метода 2. В строках 3 и 4 объявлены методы для получения текущего значения датчика и определения его работоспособности. В строке 5 объявлен метод для создания экземпляра класса соответствующего типа.
В соответствии с требованиями нам необходимо реализовать датчики, которые бы возвращали текущие, сглаженные и производные значения показателей. Для каждого способа реализован отдельный класс; диаграмма классов изображена на Рис. 29.
Рис. 29. Диаграмма классов, реализующих управление датчиками
Как видно из диаграммы, при вызове метода для получения значения датчик обращается к драйверу, вызывая соответствующие методы. В зависимости от настроенного драйвера будут возвращаться реальные или имитируемые значения.
6.2.5. Контейнер
Контейнер предназначен для хранения экземпляров классов для управления датчиками. Объявление класса приведено в Листинг 92.
Листинг 92. Объявление контейнера (SensorContainer.h)
namespace sensor
{
class ISensor;
class SensorContainer
{
public:
void addSensor(SensorNumber number, SensorPointer sensor); // (1)
void deleteSensor(SensorNumber number); // (2)
SensorPointer checkSensorExist(SensorNumber number); // (3)
SensorPointer findSensor(SensorNumber number); // (4)
template<typename CallbackIterate>
void forEachSensor(CallbackIterate&& callback) // (5)
{
for (auto item : container_) // (6)
{
callback(item.first, item.second);
}
}
private:
std::map<SensorNumber, SensorPointer> container_; // (7)
};
};
Хранилище объектов реализовано в виде двоичного дерева (строка 7). Ключом здесь выступает номер датчика, содержимым является указатель на класс управления датчиком. Методы для добавления и удаления указателей объявлены в строках 1 и 2.
Метод в строке 3 возвращает указатель на объект класса, если последний с заданным номером содержится в хранилище, в противном случае возвращается нулевой указатель. Метод в строке 4 возвращает указатель на объект класса для соответствующего номера; если объект отсутствует, то генерируется исключение.
Метод 5 предназначен для итерации по всем хранимым объектам. Здесь используется обратный синхронный вызов (см. п. 1.4.1) по схеме «перебор элементов» (см. п. 1.2.3). Реализация осуществляет перебор всех элементов хранилища, для каждого элемента выполняется соответствующий вызов. Метод реализован в виде шаблона, что позволяет его использование для различных типов объектов. Входным параметром метода выступает объект вызова, объявленный как ссылка на r-value. Такое объявление позволяет передавать выражения или временные копии объектов.
6.2.6. Асинхронные запросы
Для реализации асинхронных запросов объявляется очередь, в которую помещаются все поступающие запросы. Обработка очереди происходит в отдельном потоке. Поток извлекает очередной запрос и для него выполняет обратный вызов. Объявление класса для выполнения асинхронных вызовов приведено в Листинг 93.
Листинг 93. Класс для выполнения асинхронных вызовов (CommandQueue.h)
class CommandQueue
{
public:
void start(); // (1)
void stop(); // (2)
void addCommand(SensorNumber number, SensorPointer pointer, SensorValueCallback callback); // (3)
private:
struct Command // (4)
{
SensorNumber number;
SensorPointer pointer;
SensorValueCallback callback;
};
std::queue<Command> commandQueue_ ; // (5)
std::condition_variable conditional_; // (6)
std::mutex mutex_; // (7)
std::thread queueThread_; // (8)
bool exit_; // (9)
void readCommand(); // (10)
};
В строке 4 объявлена структура, в которой будут храниться данные для выполнения вызова: номер датчика, указатель на класс датчика и объект вызова. В строке 5 объявлен контейнер, который будет хранить указанные структуры. В строках 6 и 7 объявлены переменные для синхронизации операций записи/чтения очереди, в строке 8 объявлен класс для запуска потока обработки очереди, в строке 9 объявлен индикатор для завершения работы потока.
В строке 1 объявлен метод, который запускает поток обработки очереди, в строке 2 объявлен метод для остановки этого потока. Метод, объявленный в строке 3, добавляет переданные данные в очередь путем создания экземпляра структуры 4 и размещения ее в контейнере 5.
Обработка очереди реализована в методе, объявленном в строке 10. Поток обработки очереди вызывает этот метод, который, в свою очередь, ожидает поступления записей и обрабатывает их. Реализация приведена в Листинг 95.
Листинг 94. Обработка очереди запросов (CommandQueue.cpp)
void CommandQueue::readCommand()
{
while (!exit_) // (1)
{
std::unique_lock<std::mutex> lock(mutex_); // (2)
conditional_.wait(lock, [this]() {return commandQueue_.size() > 0 || exit_ == true; }); // (3)
while (commandQueue_.size() > 0 && exit_ == false) // (4)
{
Command cmd = commandQueue_.front(); // (5)
commandQueue_.pop(); // (6)
lock.unlock(); // (7)
cmd.callback(cmd.number, cmd.pointer->getValue()); // (8)
lock.lock(); // (9)
}
}
}
Пока не установлен индикатор завершения (устанавливается в методе stop), выполняется цикл 1. Вначале блокируется мьютекс 2 (это необходимо для корректной работы условной переменной), затем осуществляется ожидание условной переменной 3. Когда метод addCommand сформировал новую запись и добавил ее в контейнер, он инициирует срабатывание условной переменной, и поток выполнения переходит к циклу 4 (мьютекс при этом оказывается заблокирован). Этот цикл работает, пока очередь не опустеет либо будет установлен индикатор выхода.
В строке 5 из контейнера извлекается очередная запись, в строке 6 эта запись удаляется из контейнера. В строке 7 снимается блокировка мьютекса, что позволяет добавлять в контейнер новые записи, пока идет обработка очередной команды. В строке 8 осуществляется обратный вызов, в строке 9 мьютекс блокируется вновь, и далее повторяется цикл 4.
6.2.7. Наблюдатель
Объявление класса наблюдателя приведено в Листинг 95.
Листинг 95. Наблюдатель – класс для отслеживания пороговых значений (Observer.h)
class Observer
{
public:
void start(); // (1)
void stop(); // (2)
void addAlert(SensorNumber number, SensorPointer pointer, SensorAlertCallback callback, SensorValue alertValue, AlertRule alertRule, CheckAlertTimeout checkTimeoutSeс); // (3)
void deleteAlert(SensorNumber number); // (4)
private:
struct Alert // (5)
{
Alert() {}
Alert(SensorAlertCallback callback, SensorValue alertValue, AlertRule alertRule, SensorPointer sensor, CheckAlertTimeout checkTimeout):
callback(callback), alertValue(alertValue), alertRule(alertRule), sensor(sensor), checkTimeout(checkTimeout), currentTimeout(0)
{
}
SensorAlertCallback callback;
SensorValue alertValue;
AlertRule alertRule;
SensorPointer sensor;
CheckAlertTimeout checkTimeout;
CheckAlertTimeout currentTimeout;
};
std::map<SensorNumber, Alert> containerAlert; // (6)
std::thread pollThread_; // (7)
bool exit_; // (8)
std::mutex mutex_; // (9)
void poll(); // (10)
};
В строке 1 объявлен метод для запуска процесса отслеживания пороговых значений, в строке 2 – метод для останова. Метод в строке 3 добавляет датчик для отслеживания, метод 4 – удаляет.
В строке 5 объявлена структура, в которой хранятся данные, необходимые для отслеживания показаний датчика. В строке 6 объявлен контейнер для хранения указанных структур; метод addAlert добавляет запись в контейнер, метод deleteAlert удаляет ее. В строке 7 объявлен класс для запуска потока для отслеживания, в строке 8 объявлен индикатор выхода, в строке 9 объявлен мьютекс для синхронизации.
Отслеживание показаний реализовано в методе, объявленном в строке 10. Поток отслеживания вызывает этот метод, который циклически опрашивает назначенные датчики и в случае превышения пороговых значений осуществляет обратный вызов. Реализация приведена в Листинг 96.
Листинг 96. Отслеживание пороговых значений
void Observer::poll()
{
using namespace std::chrono_literals;
while (!exit_) // (1)
{
std::this_thread::sleep_for(1s); // (2)
std::lock_guard<std::mutex> lock(mutex_); // (3)
for (auto& item : containerAlert) // (4)
{
Alert& alert = item.second;
alert.currentTimeout++; // (5)
if (alert.checkTimeout != 0 && alert.currentTimeout >= alert.checkTimeout) // (6)
{
bool triggerAlert = false;
if (alert.alertRule == AlertRule::More) // (7)
{
triggerAlert = alert.sensor->getValue() > alert.alertValue;
}
else // (8)
{
triggerAlert = alert.sensor->getValue() < alert.alertValue;
}
if (triggerAlert) // (9)
{
alert.checkTimeout = alert.callback(item.first, alert.alertValue); // (10)
}
alert.currentTimeout = 0; // (11)
}
}
}
}
В строке 1 объявлен цикл опроса, который выполняется, пока не выставлен индикатор завершения (выставляется в методе stop). В строке 2 поток засыпает на 1 секунду, т. е. интервал опроса равен 1 секунде. В строке 3 блокируется мьютекс, чтобы избежать коллизий добавления/удаления элементов в контейнере.
В строке 4 осуществляется опрос элементов, хранящихся в контейнере. Текущее время опроса в строке 5 увеличивается на единицу. Если уведомление разрешено, о чем говорит ненулевое значение timeout, и время последнего опроса превысило назначенное время (строка 6), то тогда проверяется, имелось ли превышение пороговых значений в соответствии с назначенными правилами (строки 6, 7). Если превышение зафиксировано (строка 9), то осуществляется обратный вызов (строка 10). Этот вызов возвращает следующий интервал опроса, после чего текущее время сбрасывается (строка 11).