Зависимости
Building
- c++17
- libdbus
- libjansson
- libhwhelper (1.4.0)
- contactless.h из libaqc_emv_ctls_l1
Runtime
- ofono
- connman
- udev
- libdbus
- libjansson
- libhwhelper
Building documentation
Для создания документации сначала установите зависимости (doxygen, doxyqml and dot):
sudo apt install doxygen graphviz doxyqml
Затем создайте документацию:
Документация будет создана в папке ./doc/html/
. Чтобы открыть документацию, откройте файл ./doc/html/pages.html
в браузере.
Терминология
Сервис - программа hw-keeper
Офоно - программа ofono
Коннман - программа connman
АЦП/ADC - Аналогово-цифровой преобразователь / Analog-to-digital converter
Пульсация (на GPIO) - попеременное включение и выключение GPIO с заданной длительностью и периодом.
Описание
Сервис создан для выполнения различных близких к железу задач, связанных со взаимодействием с модемом, контролем температуры, питания и т.д. Ниже будут подробно рассмотрены все возможности.
Задачи Сервиса:
- Выполнять настройку куба (timezone, логгирование)
- Выполнять настройку модема.
- Выполнять первичную активацию контекста. При первом запуске модема коннман не активирует контекст.
- Восстанавливать соединение, если связь была потеряна. Коннман не всегда может сам восстановить связь, и для этого необходимо переактивировать контекст либо перезагрузить модем.
- Мониторинг значений АЦП, с помощью которых определяется значения силы тока и напряжения источника питания, а также температура устройства, получаемая из термистра. Предоставляется DBus интерфейс.
- Pipe интерфейс для выполнения звукового сигнала по команде.
- Выполнение пульсаций по команде через DBus интерфейс.
Сервис по своему функционалу разделен на следующие логические модули:
- HwMonitor - мониторинг подключенных/отключенных устройств. Нужен для обнаружения модема.
- Modem - включение, настройка, выключение модема.
- CellularManager - менеджер мобильной связи. Активация контекста, контроль соединения.
- AdcReader - мониторинг значений АЦП и уведомление
- Beeper - ожидание команды по fifo для бипера
- PulsatorManager - менеджер пульсаций. Выполняет поступившую команду на запуск пульсации.
- DBusServer - предоставление DBus API.
Все модули выполняются в отдельном одноименном треде (кроме PulsatorManger).
Некоторые модули изменяют значения GPIO. Соответствие имени и пути до файла:
PB14: /sys/class/gpio/PB14/direction // направление
/sys/class/gpio/PB14/value // значение
Запись "PB14 -> 1" означает запись значения 1 в value файл. Перед этим в direction будет записано out.
Сервис также имеет функционал, не относящийся к какому-либо из перечисленных модулей. Они описаны в разделе "Прочие возможности".
Параметры
Документация по всем параметрам ведётся здесь: https://gitlab.aqsi.ru/pg-group/documentation/aqsi/-/blob/test/models/cube-settings-admin.md
Параметры берутся из общей конфигурации куба, из файла /mnt/data/aqsi-config.json
.
Сервис поддерживает сигнал SIGHUP
, при котором происходит обновление значений всех параметров из файла конфигурации, а также их применение.
В начале описания каждого модуля приведен список параметров, влияющих на функционал модуля, в формате
"ИмяПараметра": ДефолтноеЗначение // Комментарий
Если параметр обозначает какое-то время (таймаут), то он указывается в миллисекундах (например, ModemPowerCommandSwitchTimeout
). Если в конце имени параметра есть S
, то в секундах (например, RestartModemOnErrorTimeoutS
).
HwMonitor
Модуль с помощью libudev определяет, какое оборудование в кубе подключается и отключается. Реализация близка к той, что в офоно:
С помощью udev rules usb-модему с определенным VID/PID ставится метка OFONO_DRIVER
, в которой записывается название модема (gemalto, quectel). При подключении устройства, офоно и Сервис смотрят на эту метку и определяют, как дальше с ним работать. Сервис передает дальше управление модулю Modem.
Modem
Параметры:
"StartModemOnPoweron": 1 // Поведение при включении
"ModemStopStartTimeout": 3000 // Время между выключением и включением модема
"ModemPowerCommandSwitchTimeout": 800 // Время "зажима" кнопки включения модема
Модуль запускает и настраивает модем, найденный с помощью HwMonitor.
В зависимости от StartModemOnPoweron
различается поведение при старте:
0. Модем не включается
- Модем включается, если не обнаружен
- Модем перезапускается
Поддерживается 2 типа модема: gemalto, quectel. Если модем неизвестен, используется схема работы для quectel.
Управление модема происходит с помощью записи в GPIO:
- PB14 - подача питания. Для cube-a используется PB15
- PC10 - команда (кнопка) включения
Включение
- PB14 -> 1
- sleep 30 ms
- PC10 -> 1
- sleep
ModemPowerCommandSwitchTimeout
ms
- PC10 -> 0
Выключение (gemalto)
- AT^SMSO
- Ожидание выключения, не более 3 секунд
- PB14 -> 0
Выключение (quectel)
- PC10 -> 1
- sleep
ModemPowerCommandSwitchTimeout
ms
- PC10 -> 0
- sleep 30 ms
- PB14 -> 0
Перезапуск
- Выключение
- sleep
ModemStopStartTimeout
ms
- Включение
Настройка (gemalto)
После включения происходит настройка с помощью AT команд. Проверяются следующие параметры, если отличны, то устанавливаются:
- AT^SCFG="Gpio/mode/SYNC" - "std"
- AT^SCFG="Serial/Interface/Allocation" - "2"
- AT^SLED=2,10
- В случае cube-a дополнительно устанавливает AT^SCPIN=1,6,1,0 (для контроля ADC)
Настройка (quectel)
Не происходит
CellularManager
Параметры:
"PingServer": "yandex.ru" // Адрес для проверки соединения через ping
"PingServers": [] // Если не пустой, пингует указанный список адресов (PingServer в этом случае игнорируется)
// Пример списка: ["yandex.ru", "google.com", "ru.wikipedia.org"]
"PingTimeoutS": 60 // время ожидания ping
"PingDataSize": 0 // аргумент опции ping -s
"PingRetries": 3 // сколько пингов в рамках одной проверки соединения
"CellularManagerMode": 1 // Варианты: 0, 1, 2
"CellularAutoSetup": 0 // Режим автонастройки
"CellularAPN": "" // Настройка контекста AccessPointName
"CellularUsername": "" // Настройка контекста Username
"CellularPassword": "" // Настройка контекста Password
"CellularAuthMethod": "" // Настройка контекста AuthenticationMethod, варианты: "pap", "chap", "none". В случае иной строки не меняется
"CellularProtocol": "" // Настройка контекста Protocol, варианты: "ip", "ipv6", "dual". В случае иной строки не меняется
"CellularEnableRoaming": 1 // Роуминг включен. Нужен для M2M симкарт
"RestartModemOnErrorTimeoutS": 480 // 8 * 60, время ожидания появления связи после включения модема
"RestartModemOnErrorMaximumTimeoutS": 28800 // 3600 * 8, максимальное время ожидания
Модуль осуществляет активацию и переактивацию контекста при необходимости, так же по необходимости отправляет в Modem команду на перезапуск модема.
Данный функционал был написан как дополнение возможностей коннмана в плане управления контекстом, потому что существуют следующие сценарии, при которых connman не способен самостоятельно восстановить связь:
- Временно отошёл контакт симки
- Ухудшение качества связи, попытка активации, бесконечный ответ от офоно OperationInProgress при фактическом наличии связи
- Ухудшение качества связи, попытка активации, ошибка офоно OpeationFailed при фактическом наличии связи
Программно невозможно дифференцировать случаи, когда для восстановления связи достаточно переактивировать контекст, а когда нужно перезапускать модем, поэтому используется следующий алгоритм:
- Пропала связь
- Переактивация контекста
- Если связь не появилась в течение таймаута
RestartModemOnErrorTimeoutS
, перезапустить модем
- Таймаут ожидания появления связи увеличивается вдвое (но не более, чем
RestartModemOnErrorMaximumTimeoutS
)
Активация контекста
Основная задача, которую выполняет модуль - это активация контекста. Процесс реализован в виде конечного автомата. Переключение состояний в логе отображается как
Список состояний:
- Default - изначальное состояние
- Initializing - ожидание модема
- InitializingLong - ожидание модема без таймера на перезапуск модема
- FindingObjectPath - получение object_path от офоно
- CheckingSimPresenting - проверка наличия симкарты
- FindingContextPath - получение context_path (по признаку type "internet")
- SettingUpInternetSettings - выставление настроек роуминга, APN, Username, Password, AuthMethod, Protocol
- ActivatingContext - команда активации контекста
- CheckingConnection - проверка соединения через пинг
Диаграмма переходов состояний:

Если во время действия внутри состояния произошла ошибка, действие повторяется через 3 секунды.
Default
В этом состоянии модуль находится изначально. Поведение различается в зависимости от CellularManagerMode
:
0. Модуль отключен, находится в режиме ожидания, пока параметр не примет другое значение.
- Происходит первичная активация контекста, после чего идёт проверка соединения без указания интерфейса. Таким образом, если есть подключение по ethernet, то даже если нет связи по модему, он не будет переактивироваться/перезапускаться.
- Как в п.1, только проверка соединения идет по интерфейсу ppp0. Даже если есть связь по ethernet, модем будет переактивироваться/перезапускаться.
Initializing / InitializingLong
Ожидание модуля Modem до момента, когда модем будет включен и настроен (инициализирован).
FindingObjectPath
Получение object_path модема (здесь и далее все команды выполняются через офоно)
Команда: org.ofono.Manager.GetModems
CheckingSimPresenting
Проверка наличия симкарты. Если симкарты нет, остаёмся на этом состоянии.
Команда: org.ofono.SimManager.GetProperties
Проверяется Present
property.
FindingContextPath
Получение информации о контексте, в частности path.
Команда: org.ofono.ConnectionManager.GetContexts
Ищется контекст с ConnectionContext property Type
= "internet".
Помимо пути из контекста получаем настройки:
Команда: org.ofono.ConnectionContext.GetProperties
Properties:
AccessPointName
Username
Password
AuthenticationMethod
Protocol
SettingUpInternetSettings
Поведение различно, в зависимости от CellularAutoSetup
:
0. Параметры выставляются исходя из параметров CellularAPN
, CellularUsername
, CellularPassword
. Если в них пустая строка, то ничего не меняется.
- APN/User/Pass выставляются исходя из таблицы в Приложении 1. В этом случае вышеуказанные параметры не используются.
Сначала выставляется CellularEnableRoaming
с помощью команды:
org.ofono.ConnectionManager.SetProperty(RoamingAllowed)
После чего выставляются остальные настройки в порядке, указанном в списке "Properties" в FindingContextPath.
Команда: org.ofono.ConnectionContext.SetProperty
Перед каждым вызовом ConnectionContext.SetProperty
происходит отключение cellular technology на уровне коннмана, т.к. иначе настройка не применится. Для этого используется команда net.connman.Technology.SetProperty , property Powered
-> false. После завершения ConnectionContext.SetProperty
, Powered
снова выставляется в true.
ActivatingContext
Команда: org.ofono.ConnectionContext.SetProperty
Active
-> true
CheckingConnection
После того, как контекст активирован, происходит проверка соединения с помощью команды ping.
Адрес пинга берется из PingServer
либо PingServers
.
PingServer
- строка, например "yandex.ru"
. Пинг идет PingRetries
раз до "yandex.ru"
PingServers
- массив строк, например ["google.com", "yandex.ru"]
. Пинг идёт PingRetries
раз сначала до "google.com", потом PingRetries
раз до "yandex.ru". Если PingServers
не пустой, то PingServer
игнорируется.
Как только пройдёт хотя бы 1 пинг, проверка считается успешной, связь есть.
Итоговая команда зависит от CellularManagerMode
:
- ``` ping -c1 -s <PingDataSize> <Address from PingServer(s)> ping -c1 -I ppp0 -s <PingDataSize> <Address from PingServer(s)>
Если по результатам проверки связь отсутствует, происходит отключение контекста (команда та же, что и при включении, только с параметров false). После этого происходит переход в состояние Initializing.
#### Механизм перезапуска модема
В модуле присутствует таймер перезапуска модема, далее - Таймер. У таймера есть Интервал, который изначально равен `RestartModemOnErrorTimeoutS`. Интервал не может превышать `RestartModemOnErrorMaximumTimeoutS`.
Таймер:
* Запускается в состоянии Default
* Сбрасывается в состоянии InitializingLong
* Сбрасывается в состоянии ConnectionChecking в случае успешной проверки связи
* Не учитывается при срабатывании в случае, если отсутствует симка
При срабатывании таймера, в каком бы состоянии не находились, происходит перезапуск модема и переход в состояние Initializing. Так же при этом Интервал увеличивается вдвое.
### AdcReader
Параметры:
"AdcReadTimeout": 500 // задержка после чтения данных из АЦП "AdcErrorReportTimeout": 5000 // Период отправки сигнала о том, что значения выходят за допустимые пределы "TemperatureCheckTimeout": 3000 // Период проверки температуры "TemperatureMax": 60 // Верхняя граница допустимой температуры "TemperatureMin": -20 // Нижняя граница допустимой температуры "PsuCurrentMax": 4 // Верхняя граница допустимой силы тока "PsuVoltageMin": 7 // Нижняя граница допустимого напряжения "PsuVoltageMax": 62 // Верхняя граница допустимого напряжения Параметры (скрытые, только для разработчиков):
"PsuCurrentAmpVolt": 3.3333333333 // 5A : 1.5V - ампер-вольтная характеристика
// cube-a "PsuCurrentDivider": 0.244 // Делитель тока "PsuVoltageDivider": 0.031 // Делитель напряжения
// cube-b "PsuCurrentDivider": 1.3 "PsuVoltageDivider": 0.031
// cube-c (and others) "PsuCurrentDivider": 1.3 "PsuVoltageDivider": 0.04
Мониторинг АЦП. Значения берутся из файлов:
/sys/bus/iio/devices/iio:device0/in_voltage_scale /sys/bus/iio/devices/iio:device0/in_voltaged_raw
В зависимости от ревизии куба отличается схема подключения входов АЦП:
* cube-a: на входе АЦП внешний мультиплексор, который переключил каналы измерения тока и напряжения. Переключение происходит с помощью GPIO модема. Выход АЦП - 5 канал (`in_voltage5_raw`)
* cube-b: как в cube-a, но переключение происходит с помощью GPIO 503 куба.
* cube-c и выше: вход измерения силы тока подключен к 5 выходу АЦП (`in_voltage5_raw`) , вход измерения напряжения подключен к 6 выходу АЦП (`in_voltage6_raw`).
Термистор доступен на 4 (`in_voltage4_raw`) выходе АЦП. Температура берется исходя из таблицы соответствия сопротивления и температуры, которая предварительно рассчитывается для конкретного термистра (в нашем случае ERT-J0ET473J).
Формула расчета для таблицы зависимости сопротивлений от температуры
R = EXP(B*((1/(x + 273.15))-(1/298.15))), where B = 4450
Модуль занимается мониторингом значений тока, напряжения, температуры. Формулы рассчетов:
value = <in_voltaged_raw> * <in_voltage_scale> / 1000 I = value / <PsuVoltageDivider> * <PsuCurrentAmpVolt> U = value / <PsuVoltageDivider>
Более подробная информация находится в Приложении 2.
Модуль постоянно считывает значения с АЦП с задержкой в `AdcReadTimeout`. Температура считывается с периодом `TemperatureCheckTimeout`. Значения проверяются на соответствие диапазонам, указанным с помощью параметров `TemperatureMax`, `TemperatureMin`, `PsuVoltageMax`, `PsuVoltageMin`, `PsuCurrentMax`. Если значения выходят за диапазон, то отправляется DBus сигнал `OutOfRange`. Подробнее в разделе DBus API.
### Beeper
Параметры:
"BeeperTimeout": 0 // Задержка после обработки команды
Модуль слушает fifo файл (pipe) по пути [EMVL1_PIPE_NAME_BEEPER](https://gitlab.aqsi.ru/aqc_embedded/libaqc_emv_ctls_l1/-/blob/master/contactless.h#L10) (на момент написания это "/tmp/aqc.emv.l1.ctls.beeper")
Если пришел байт [EMVL1_BEEP_OK](https://gitlab.aqsi.ru/aqc_embedded/libaqc_emv_ctls_l1/-/blob/master/contactless.h#L29) (0x00), пищит 1 раз 500 мс.
Если пришел байт [EMVL1_BEEP_FAIL](https://gitlab.aqsi.ru/aqc_embedded/libaqc_emv_ctls_l1/-/blob/master/contactless.h#L30) (0x01), пищит 2 раза по 200 мс с перерывом в 200 мс.
Если пришло несколько байт, выполняется последний. После выполнения команды выполняется задержка в `BeeperTimeout` мс.
### PulsatorManager
Параметры:
"OutputPulseDuration": 500 // длительность нахождения в верхнем состоянии, в микросекундах "OutputPulsePeriod": 1000 // период пульса, в микросекундах "OutputPulsePolarity": 1 // состояние пина в режиме ожидания (0/1)
Вызов DBus метода `GeneratePulse(pin, count)` запускает пульсацию на определенном GPIO. Под каждый пульс создается отдельный тред с именем `Pulse <pin>`, где `<pin> `- номер GPIO.
Если запустить пульсацию на пине, где уже пульсация идет, то она продлится на
заданное количество тиков.
Параметры пульсации настраиваются с помощью `OutputPulseDuration`, `OutputPulsePeriod`, `OutputPulsePolarity`
### DBus API
Примеры использования из шелла в Приложении 3.
Service org.hwkeeper Interface org.hwkeeper Object path /
Methods double GetTemperature()
Возвращает температуру устройства в °C, .1 точность
double GetPSUVoltage()
Возвращает значение напряжения на источнике питания в вольтах.
double GetPSUCurrent()
Возвращает значение силы тока на источнике питания в амперах.
void UpdateConfig()
Перезагружает конфигурационный файл.
Deprecated, используйте SIGHUP.
void GpioWrite(int32 n, boolean val)
Записывает значение в GPIO n.
Deprecated, используйте библиотеку hwhelper
boolean GpioRead(int32 n)
Считывает значение GPIO n.
Deprecated, используйте библиотеку hwhelper
void GpioDirection(int32 n, boolean val)
Изменяет значение direction GPIO n (false - input, true - output)
Deprecated, используйте библиотеку hwhelper
void GeneratePulse(int32 n, int32 count)
Запускает пульсацию на GPIO n в размере count тиков.
void ModemTurnOn()
Включает модем
void ModemTurnOff()
Выключает модем
void ModemRestart()
Перезапускает модем
Service org.hwkeeper Interface org.hwkeeper Object path /cellular
Methods boolean GetOnline()
true, если есть связь согласно логике CellularManager
(прошел ping до PingServer/PingServers)
Signals OnlineChanged(boolean val)
Отправляется при изменении online-статуса CellularManager.
Service org.hwkeeper Interface org.hwkeeper Object path /adc/temperature, /adc/psu_current, /adc/psu_voltage
Signals OutOfRange(double value)
Значение температуры, напряжения или тока за пределами допустимого
### Прочие возможности
#### Режим отладки
Параметр:
"HwKeeperDebug": 1 // 0 - выключено (default), 1 - включено
В этом режиме приложение пишет больше сообщений в лог, чем обычно, что может помочь при отладке.
#### Настройка часового пояса
Параметр:
"Timezone": 3 // от -12 до 14
Создается ссылка в `/mnt/data/localtime`, указывающая на `/usr/share/zoneinfo/Etc/GMT<inverted-sign><value>`, где `<inverted-sign>` это инвертированный знак `Timezone`, а `<value>` - абсолютное значение `Timezone`.
Например, если `Timezone` = 3, то создается ссылка на `/usr/share/zoneinfo/Etc/GMT-3`
#### Настройка параметров логгирования
Параметры:
"LogLocation": "/media/sdcard"
"LogSize": 10485760 // 10 Mb "ElasticsearchConfigPreset": "" // test, prod, disable
`ElasticsearchConfigPreset`- пресет настроек для elasticsearch.
* "" (пустая строка) - оставляет все по-умолчанию (по-умолчаню логи не шлются)
* "disable" - отправка логов отключена
* "prod" - включена отправка на продовый сервер (https://elk-cube.aqsi.ru)
* "test" - включена отправка на тестовый сервер (https://elk-cube-test.aqsi.ru)
Реализовано через управление таргета ссылки `/etc/rsyslog.d/elasticsearch.conf`
`LogLocation` - корень местоположения логов. Внутри указанной директории создается поддиректория "syslog", внутри которой - файлы `syslog.log` и `syslog.1.log`, каждый размером не более, чем `LogSize`/2.
* "/var/log" - логи хранятся в оперативной памяти, стираются при перезагрузке.
* "/mnt/data" - логи хранятся в постоянной памяти, при перезагрузке не стираются, но тратят ресурс флеш-памяти.
* "/media/sdcard" - логи хранятся на microSD-карте, если она присутствует.
Если в строке указано иное, либо если указанный путь не смонтирован в системе, используется "/var/log".
`LogSize` - размер лог-файла (в байтах). Запись идёт в файл `<LogLocation>/syslog/syslog.log`. По достижению половины значения файл переименовывается с заменой в `syslog.1.log`, создается новый `syslog.log`
## Приложения
### Приложение 1
Значения APN/User/Pass в случае, если `CellularAuthMethod` = 1:
Оператор APN Username Password
MTS internet.mts.ru mts mts Beeline internet.beeline.ru beeline beeline Megafon internet gdata gdata Tele2 internet.tele2.ru - - Yota internet.yota - -
### Приложение 2
Информация, связанная с АЦП:
https://ru.mouser.com/datasheet/2/315/AUA0000C11-1100844.pdf
https://datasheet.octopart.com/ERT-J0ET473J-Panasonic-datasheet-78377388.pdf
http://ww1.microchip.com/downloads/en/Appnotes/AN_3250-How-to-use-SAMA5D2-ADC-under-Linux-00003250a.pdf

Для ревизии RevA:
PD23 - ADCIN4 - NTC Thermistor 47k (ERT-J0ET473J with Pull_UP 47k 3V3) PD24 - ADCIN5 - VIN (57V max 0.031 divider) CURRENT_OUT (divider 0.244)(1.5V@5A) WM_GPIO7 (PWM) - ADCIN5_SLCT - управление мультиплексором на входе ADCIN5. При 0 к ADCIN5 подключен сигнал линейно пропорциональный току потребления от главного источника питания 5В. При 1 к ADCIN5 подключен сигнал линейно пропорциональный напряжению на входе устройства. PD25 - ADCIN6 - WM_EP (WM Analog audio) PD30 - ADCIN11 - AUX_35 (30V max 0.099 divider)
Для ревизии RevB & CubeT RevA:
PD23 - ADCIN4 - NTC Thermistor 47k (ERT-J0ET473J with Pull_UP 47k 3V3) PD24 - ADCIN5 - VIN (57V max 0.031 divider) CURRENT_OUT (divider 1.3)(1.5V@5A) gpio503 - ADCIN5_SLCT - управление мультиплексором на входе ADCIN5. При 0 к ADCIN5 подключен сигнал линейно пропорциональный току потребления от главного источника питания 5В. При 1 к ADCIN5 подключен сигнал линейно пропорциональный напряжению на входе устройства. PD25 - ADCIN6 - WM_EP (WM Analog audio) PD30 - ADCIN11 - AUX_35 (30V max 0.099 divider)
Для ревизии RevC & RevD & CubeT RevB:
PD23 - ADCIN4 - NTC Thermistor 47k (ERT-J0ET473J with Pull_UP 47k 3V3) PD24 - ADCIN5 - CURRENT_OUT (divider 1.3)(1.5V@5A) PD25 - ADCIN6 - VIN (57V max 0.04 divider) PD30 - ADCIN11 - AUX_35 (30V max 0.099 divider)
### Приложение 3
Примеры команд из шела для использования DBus API:
Мониторить изменение свойства PropertyChanged на интерфейсе org.ofono.ConnectionContext:
dbus-monitor –system interface='org.ofono.ConnectionContext',member='PropertyChanged'
Получить ток, напряжение, температуру:
dbus-send –system –print-reply –dest=org.hwkeeper / org.hwkeeper.GetPSUCurrent dbus-send –system –print-reply –dest=org.hwkeeper / org.hwkeeper.GetPSUVoltage dbus-send –system –print-reply –dest=org.hwkeeper / org.hwkeeper.GetTemperature
Включение, выключение, перезапуск модема:
dbus-send –system –print-reply –dest=org.hwkeeper / org.hwkeeper.ModemTurnOn dbus-send –system –print-reply –dest=org.hwkeeper / org.hwkeeper.ModemTurnOff dbus-send –system –print-reply –dest=org.hwkeeper / org.hwkeeper.ModemRestart
echo -ne "\x00" > "/tmp/aqc.emv.l1.ctls.beeper" echo -ne "\x01" > "/tmp/aqc.emv.l1.ctls.beeper" ```