Как не тратить время на велосипеды, а просто написать тесты на 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.
А где же онлайн сбор и анализ данных о тестировании и удалённое управление тестовым парком? Скоро всё будет, пишите, если хотите поучаствовать в тестировании ранних версий.
engin
Ребята, такой подход к тестированию конкретной отладочной платы рулит, в том случае когда производство ее на потоке рассчитано как минимум на 3 - 5 лет без каких либо изменений в ее процессорной архитектуре. Опять же такую "станцию" никак нельзя строить на опенсорсе, или сразу с себя снимать ответственность перед клиентом за ее стабильность и функционал. Обычно производители такие тесты хранят как зеницу ока, т.к. это часть их конкуренции в условиях агрессивного рынка.
Что касается среды исполнения тест бенча, применительно к фикстуре испытуемой платы (разработчики платы обязаны в процессе ее разводки размещать тест поинты по по всем точкам, подлежащим контролю, с целью выявления отклонений от рабочих параметров).
В статье есть ряд тестов, которые перенасыщают алгоритм теста.
Так к примеру:
Если прибор это встроенный в отладочную плату модуль, то это не прибор, а четкое понятие к примеру схема шлюза, интерфейсного преобразователя, драйвер и т.п. и если он отвечает за обмен данными с внешним ПК, то успешная прошивка (перезапись) это часть одного теста, при том, что предшествующие тесты питаний в определенных тест поинтах прошли успешно.
Далее переход к следующим тестам согласно сценария тет бенча.
Я не стал разбирать всю логику алгоритма в приведеном примере, т.к. что захотели, то и протестировали, Самый важный аспект во всех тест бенчах - минимализм с охватом всей процессорной архитектуры с конечным результатом того, что имеем на GPIO, при условии что обратная связь отладочной карты так же тестируется по сценарию - счет/триггер/индикация события.
Ну и самый важный момент - станция должна нести на себе среду визуального построения тестов, которые можно быстро, без привлечения кодеров (программистов - разрабов) отдать в личное пользование клиенту (его сервисному персоналу).
juramehanik
Никак нельзя строить на опенсурсе - шта? Питон нельзя использовать? Он тоже опенсурсный сам по себе если что.
engin
Строить (писать) тестбенч можно на всем, на чем Вы умеете, но он должен иметь достаточную защиту несанкционированного доступа, в т.ч. и среды разработки таких тестов, к слову тесты могут быть финальными с функциями автоматизированной юстировки ряда параметров по результатам промежуточного сбора данных и т.п.
Все прочие опенсорсные хотелки применимы исключительно с целью дать волю народному творчеству в таком развлечении, но кто будет такие карты покупать в свои ответственные прототипы или проекты.... это грабли, где никто не несет ответсвенность за последствия.
juramehanik
Никто никому ничего не должен, пока мы не определимся что мы делаем, для кого мы делаем и по каким требованиям. В статье про это не было ничего сказано.
engin
И далее речь идет об использования функций HardPy - открытого фреймворка .
Так из статьи и не понял, что в фреимворке открыто, его логическое ядро или его инструменты для работы с исходниками (тестбенчи). Если доступ к ядру, то это то о чем я говорю, все остальное круги по воде, если Вы предлагаете в открытых ресурсах людям строить эти загрузочные файлы для проведения тестов на свой страх и риск что в результате не спалите порты по причине ошибки написания тб.