Как не тратить время на велосипеды, а просто написать тесты на 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 - вызывает диалоговое на стороне панели оператора.
Собираем данные
Запись отчетов в базу данных
Для записи финальных отчетов нужно дополнить файл 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.
А где же онлайн сбор и анализ данных о тестировании и удалённое управление тестовым парком? Скоро всё будет, пишите, если хотите поучаствовать в тестировании ранних версий.