
Как не тратить время на велосипеды, а просто написать тесты на python и наслаждаться стабильно работающими тестовыми станциями на производстве электроники? Конечно, используя open source решение HardPy.
Разбор использования функций HardPy - открытого фреймворка для создания тестовых станций для производства электроники на Python на примере тестирования и прошивки отладочной платы Nucleo-F401.
Для чего это надо? Когда электронное устройство наконец разработано и успешно протестировано на соответствие всем требованиям, приходит пора запуска производства. При большом количестве функций, значительных партиях или просто высоких требованиях к качеству возникает задача автоматизации функционального тестирования и прошивки плат на производстве. Написание и отладка такого софта с нуля иногда отнимала у нас больше ресурсов, чем разработка самого изделия.
Разберём на простейшем примере, как с помощью HardPy сделать это быстро и надёжно. Задача - показать основные шаги разработки программного обеспечения для тестирования.
Требуемое железо, документация, полезные ссылки
Исходники этого проекта с подробными инструкциями можно посмотреть на github.
Статья написана для пакета hardpy версии 0.4.0 и для использования в Linux (Mint, Ubuntu). Под Windows HardPy тоже работает, но некоторые функции по запуску и адреса портов будут отличаться.
Для запуска примера нужна отладочная плата Nucleo-F401RE.
Если у вас нет платы Nucleo-F401RE - можно запустить другой пример, Examples GitHub, Examples documentation.
Тестовый стенд
Для того, чтобы построить тестовый стенд, нам надо ответить на несколько вопросов:
какое устройство мы тестируем - (DUT - Device under test, тестируемое устройство)
что мы хотим протестировать - список функций
как мы это будем тестировать - тестовый план
чем мы это тестируем - приборы, структурная схема стенда
DUT
В качестве тестируемого устройства выступает отладочная плата Nucleo-F401RE . Nucleo - аскетичные отладки, которые предлагают минимум функций по классной цене. Треть платы занимает программатор ST-LINK, мы будем считать его прибором, а не частью проверяемой платы.

Определяем объём тестирования
Какие функциональные блоки мы хотим проверить у этой платы:
Питание
Кнопка (1)
LED (1)
Цифровые входы-выходы, выведенные на разъёмы (2 шт.)
MCU
Для имитации реального мира привнесём имитатор дефекта на плате - jumper. Он позволит создать (дефекта нет) или разорвать соединение (дефект есть) между выводами микроконтроллера.
Мы тестируем маленькую партию, поэтому автоматизировать детектирование работы светодиода и активацию кнопки не будем, задействуем для этого глаза и руки тестировщика. Чтобы снизить влияние человеческого фактора, будем варьировать задания и проверять их исполнение человеком вместе с DUT.
Теперь, когда мы точно знаем, что мы тестируем и в каком объёме, можно приступать к разработке тест-плана.
Тестовый план
Проверить наличие приборов;
Проверить наличие прошивки для DUT;
Прошить DUT;
Прочитать серийный номер DUT;
Проверить наличие джампера;
Проверить светодиод, поморгать светодиодом несколько раз, спросить у пользователя количество морганий;
Проверить User Button, попросить пользователя нажать кнопку произвольное количество раз (вариация) на кнопку. Просим ввести это количество в форму.
Теперь мы знаем, что мы тестируем и как. Определимся с составом тестового стенда.
Вычислитель тестовой станции
Любой компьютер с Linux. На Windows пример запустить тоже можно,но не стоит.
Схема стенда
Зафиксируем состав стенда и все связи между компонентами на структурной схеме.

Для тестирования DUT нам понадобится единственный прибор - программатор ST-LINKV2. По счастливой случайности он есть на нашей плате Nucleo, так что осталось просто подключить плату к компьютеру по USB и приготовить джампер
Программа тестовой станции
Пишем драйвера
Для работы программы с DUT и приборами нужны драйвера. У нас один прибор и один модуль DUT.
ST-LINK driver
К счастью, многое уже написано за нас, и можно просто использовать PyOCD для прошивки микроконтроллера STM32 с помощью ST-LINKV2.
DUT driver
Для нашего DUT никто готовых драйверов не написал, так что пишем сами.
Драйвер достаточно простой и подключается к устройству по серийному порту к DUT на скорости 115200 бод. Драйвер запрашивает у DUT серийный номер, статус наличия джампера и количество нажатий на кнопку.
from serial import Serial from struct import unpack class DutDriver(object): def __init__(self, port: str, baud: int) -> None: self.serial = self._connect(port, baud) self.SERIAL_NUMBER_STR_LEN = 11 self.SIMPLE_REPLY_LEN = 1 def write_bytes(self, data: str) -> None: self.serial.write(data.encode()) def read_bytes(self, size: int) -> bytes: return self.serial.read(size) def reqserial_num(self) -> str: self.write_bytes("0") reply = self.read_bytes(self.SERIAL_NUMBER_STR_LEN) return reply.decode("ascii")[:-1] def req_jumper_status(self) -> int: self.write_bytes("1") reply = self.read_bytes(self.SIMPLE_REPLY_LEN) return unpack("<b", reply)[0] def req_button_press(self) -> int: self.write_bytes("2") reply = self.read_bytes(self.SIMPLE_REPLY_LEN) return unpack("<b", reply)[0] def _connect(self, port: str, baud: int) -> Serial | None: try: return Serial(port=port, baudrate=baud) except IOError as exc: print(f"Error open port: {exc}") return None
Пишем тестовый план на pytest
pytest - открытый инструмент, который позволяет легко писать небольшие, читаемые тесты и может масштабироваться для поддержки сложного функционального тестирования приложений и библиотек. Подробнее про функционал pytest можно почитать в документации проекта
В процессе отладки кода можно удобно запускать его прямо в своём IDE, HardPy для этого не нужен.
Прошивка MCU
Напишем тест для прошивки DUT. Пример будет состоять из 2 файлов: conftest.py и test_1_fw.py. В файле conftest.py положим инициализацию драйвера DUT, а в test_1_fw.py будет лежать сам тест. Подробнее про conftest.py можно почитать в документации к pytest. Все файлы примера должны лежать в одной папке, у нас это будет папка tests.
conftest.py
В conftest.py создаем экземпляр драйвера, передаем ему порт и скорость в 115200 бод (задана в DUT). Порт на разных ПК может отличаться, поэтому может потребоваться редактирование этой переменной.
Для работы с портом надо добавиться в группу dialout. После этого надо перелогиниться в систему.
sudo usermod -aG dialout имя_пользователя
# conftest.py import pytest from dut_driver import DutDriver @pytest.fixture(scope="session") def device_under_test(): dut = DutDriver("/dev/ttyACM0", 115200) yield dut
test_1_fw.py
Тестирование в рамках файла test_1_fw.py заключается в проверке наличия прошивки, проверке подключения DUT к ПК, прошивке DUT. В папку с тестом надо положить скомпилированную прошивку DUT, dut-nucleo.hex, можно собрать её самостоятельно из исходников, либо воспользоваться уже скомпилированной версией.
# test_1_fw.py from glob import glob from pathlib import Path import pytest from pyocd.core.helpers import ConnectHelper from pyocd.flash.file_programmer import FileProgrammer def firmware_find(): fw_dir = Path(Path(__file__).parent.resolve() / "dut-nucleo.hex") return glob(str(fw_dir)) def test_fw_exist(): assert firmware_find() != [] def test_check_connection(device_under_test): assert device_under_test.serial is not None, "DUT not connected" def test_fw_flash(): fw = firmware_find()[0] hardpy.set_message(f"Flashing file {fw}...", "flash_status") with ConnectHelper.session_with_chosen_probe( target_override="stm32f401retx" ) as session: # Load firmware into device. FileProgrammer(session).program(fw)
Для работы тестов нам потребуется установить через pip несколько пакетов python.
pyserial==3.5 pytest==8.1.1 pyocd==0.36.0
Рекомендуется устанавливать их в виртуальное окружение venv или conda.
pyserialиспользуется для взаимодействия с DUT по серийному портуpyocdиспользуется для обновления прошивки DUT через встроенный ST-link.
Для корректной работы pyocd также потребуется установить пакет поддержки stm32f401retx.
Для этого нужно вызвать команды:
pyocd pack update pyocd pack install stm32f401retx
Можно запускать тесты из папки tests командой:
pytest .
Получаем сообщение об успешном тестировании:
collected 3 items test_1_fw.py ... [100%] ======= 3 passed in 7.27s =======
Таким образом, у нас получилось написать простой тест план, обновляющий прошивку DUT с помощью pytest.
В процессе разработки устройств можно использовать pytest для тестовых скриптов, которые будут проверять конкретные сценарии работы разрабатываемого устройства. В идеале это может быть встроено в процесс CI/CD, HIL.
Отлично, тесты запускаются, но для работы на производстве это не годится, как минимум нужна панель оператора и хранение результатов тестирования.
Добавляем HardPy
Теперь в тесты можно добавить HardPy, установив его через pip. (Для HardPy версии 0.4.0 версия python должна быть не ниже 3.10, а версия pytest не ниже 7)
pip install hardpy
Улучшаем наш файл test_1_fw.py:
# test_1_fw.py from glob import glob from pathlib import Path import hardpy import pytest from pyocd.core.helpers import ConnectHelper from pyocd.flash.file_programmer import FileProgrammer pytestmark = pytest.mark.module_name("Testing preparation") def firmware_find(): fw_dir = Path(Path(__file__).parent.resolve() / "dut-nucleo.hex") return glob(str(fw_dir)) @pytest.mark.case_name("Availability of firmware") def test_fw_exist(): assert firmware_find() != [] @pytest.mark.case_name("Check DUT connection") def test_check_connection(device_under_test): assert device_under_test.serial is not None, "DUT not connected" @pytest.mark.dependency("test_1_fw::test_check_connection") @pytest.mark.case_name("Flashing firmware") def test_fw_flash(): fw = firmware_find()[0] hardpy.set_message(f"Flashing file {fw}...", "flash_status") with ConnectHelper.session_with_chosen_probe( target_override="stm32f401retx" ) as session: # Load firmware into device. FileProgrammer(session).program(fw) hardpy.set_message(f"Successfully flashed file {fw}", "flash_status") assert True
В файле добавились:
Импорт пакета
hardpy;Сообщения оператору, через функцию set_message();
Понятные оператору названия тестов и группы тестов: case_name и module_name в терминологии hardpy.
Маркер зависимости dependency теста
test_fw_flashот тестаtest_check_connection. В случае, если тестtest_fw_flashпровалится, тестtest_check_connectionне будет запускаться.
На этом этапе добавляем в папку с тестами файл pytest.ini, который настроит лог и включит hardpy при запуске pytest через регистрацию плагина addopts = --hardpy-pt.
[pytest] log_cli = true log_cli_level = INFO log_cli_format = %(asctime)s [%(levelname)s] %(message)s log_cli_date_format = %H:%M:%S addopts = --hardpy-pt
Запускаем базу данных
Результаты тестирования надо сохранять. Для этого HardPy использует базу данных CouchDB. Важно, что версия CouchDB должна быть не ниже 3.2.
Создаем папку database и добавляем туда файл couchdb.ini, хранящий настройки базы.
[chttpd] enable_cors=true [cors] origins = * methods = GET, PUT, POST, HEAD, DELETE credentials = true headers = accept, authorization, content-type, origin, referer, x-csrf-token
Для запуска воспользуемся инструментом docker compose. Создадим файл docker-compose.yaml со следующим содержимым:
version: "3.8" services: couchserver: image: couchdb:3.3.2 ports: - "5984:5984" environment: COUCHDB_USER: dev COUCHDB_PASSWORD: dev volumes: - ./dbdata:/opt/couchdb/data - ./couchdb.ini:/opt/couchdb/etc/local.ini
И запустим базу через команду docker compose up.
Интерфейс базы данных доступен по адресу http://127.0.0.1:5984/_utils с логином и паролем, которые были указаны в docker-compose.yaml - dev/dev.
Запускаем панель оператора
Тесты написаны, база запущена, можно запускать панель оператора, для этого нужно выполнить команду:
hardpy-panel .
либо командой:
hardpy-panel <путь до папки tests>
если запуск происходит не из папки с тестами.
В итоге в терминале мы должны увидеть сообщение о том, что все 3 теста cколлекционированы.
configfile: pytest.ini plugins: hardpy-0.4.0, anyio-4.4.0 collected 3 items <Dir tests> <Module test_1_fw.py> <Function test_fw_exist> <Function test_check_connection> <Function test_fw_flash> ======================================================= 3 tests collected in 0.37s =======================================================
А дальше можно открыть панель оператора в браузере по адресу http://localhost:8000/

По кнопке Start запускаются тесты, результаты - в базе. В целом, это уже полноценная тестовая станция для производства, только тестовое покрытие пока слабое.
Увеличим объем тестов
Добавим 2 модуля test_2_base_functions.py и test_3_user_button.py. Содержание тестов разбирать не будем, за исключением моментов использования hardpy.
Проверка серийного номера и наличия перемычки
Считываем серийный номер, проверяем его.
Проверяем наличие перемычки на DUT.
# test_2_base_functions.py import pytest import hardpy from dut_driver import DutDriver pytestmark = pytest.mark.module_name("Base functions") pytestmark = [ pytest.mark.module_name("Base functions"), pytest.mark.dependency("test_1_fw::test_fw_flash"), ] @pytest.mark.case_name("DUT info") def test_serial_num(device_under_test: DutDriver): serial_number = device_under_test.reqserial_num() hardpy.set_dut_serial_number(serial_number) assert serial_number == "test_dut_1" info = { "name": serial_number, "batch": "batch_1", } hardpy.set_dut_info(info) @pytest.mark.case_name("LED") def test_jumper_closed(device_under_test: DutDriver): assert device_under_test.req_jumper_status() == 0
Используются 2 функции:
set_dut_serial_number - записывает в базу данных серийный номер DUT.
set_dut_info - записывает в базу данных словарь с любой информацией об устройстве. В нашем случае мы записываем ещё раз серийный номер и номер партии.
Проверка User button
Просим пользователя понажимать на User button, а потом ввести количество нажатий.
# test_3_user_button.py import pytest import hardpy from hardpy.pytest_hardpy.utils.dialog_box import ( DialogBoxWidget, DialogBoxWidgetType, DialogBox, ) from dut_driver import DutDriver pytestmark = [ pytest.mark.module_name("User interface"), pytest.mark.dependency("test_1_fw::test_fw_flash"), ] @pytest.mark.case_name("User button") def test_user_button(device_under_test: DutDriver): hardpy.set_message(f"Push the user button") keystroke = device_under_test.req_button_press() dbx = DialogBox( dialog_text=( f"Enter the number of times the button has " f"been pressed and press the Confirm button" ), title_bar="User button", widget=DialogBoxWidget(DialogBoxWidgetType.NUMERIC_INPUT), ) user_input = int(hardpy.run_dialog_box(dbx)) assert keystroke == user_input, ( f"The DUT counted {keystroke} keystrokes" f"and the user entered {user_input} keystrokes" )
Используется возможность вызова диалоговых окон:
Класс
DialogBox- описывает содержимое диалогового окна. В нашем случае это окно для ввода числовых значений.run_dialog_box - вызывает диалоговое на стороне панели оператора.

HardPy operator dialog
Собираем данные
Запись отчетов в базу данных
Для записи финальных отчетов нужно дополнить файл conftest.py действиями по окончании тестирования.
Во время тестирования отчет всегда пишется в CouchDB, в базу runstore, но мы добавим сохранение всех отчетов по окончании тестирования в базу report.
Отчет хранится в формате json ��окумента согласно схеме, описанной в документации.
Сама база доступна по адресу http://127.0.0.1:5984/_utils/#
# conftest.py import pytest from hardpy import ( CouchdbLoader, CouchdbConfig, get_current_report, ) from dut_driver import DutDriver @pytest.fixture(scope="session") def device_under_test(): dut = DutDriver("/dev/ttyACM0", 115200) yield dut def finish_executing(): report = get_current_report() if report: loader = CouchdbLoader(CouchdbConfig()) loader.load(report) @pytest.fixture(scope="session", autouse=True) def fill_actions_after_test(post_run_functions: list): post_run_functions.append(finish_executing) yield
В файл были добавлены:
Функция
fill_actions_after_test, которая заполняет список post_run_functions функций, которые нужно выполнить по окончании тестирования.Функция
finish_executing- описывает действия по считыванию актуального отчета изrunstoreи записи его в базуreport.
Отчёт о тестировании в базе данных
Панель оператора
Как это всё видит оператор стенда:



Заключение
Сегодня мы легко и просто сделали тестовую станцию, написав только минимум кода, который является специфичным для DUT. Стенд готов к производству.
Спасибо соавтору @ilya_alexandrov !
P.S.
А где же онлайн сбор и анализ данных о тестировании и удалённое управление тестовым парком? Скоро всё будет, пишите, если хотите поучаствовать в тестировании ранних версий.
