Pull to refresh

Hardpy. Nucleo-f401 example — автоматизируем тестирование электроники на производстве на Python

Level of difficultyMedium
Reading time10 min
Views3.1K

Как не тратить время на велосипеды, а просто написать тесты на python и наслаждаться стабильно работающими тестовыми станциями на производстве электроники? Конечно, используя open source решение HardPy.

Разбор использования функций HardPy - открытого фреймворка для создания тестовых станций для производства электроники на Python на примере тестирования и прошивки отладочной платы Nucleo-F401.

Для чего это надо? Когда электронное устройство наконец разработано и успешно протестировано на соответствие всем требованиям, приходит пора запуска производства. При большом количестве функций, значительных партиях или просто высоких требованиях к качеству возникает задача автоматизации функционального тестирования и прошивки плат на производстве. Написание и отладка такого софта с нуля иногда отнимала у нас больше ресурсов, чем разработка самого изделия.

Разберём на простейшем примере, как с помощью HardPy сделать это быстро и надёжно. Задача - показать основные шаги разработки программного обеспечения для тестирования.

Требуемое железо, документация, полезные ссылки

Исходники этого проекта с подробными инструкциями можно посмотреть на github.

  • HardPy GitHub

  • HardPy Documentation

  • Telegram

  • Статья написана для пакета 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, мы будем считать его прибором, а не частью проверяемой платы.

Nucleo-F401RE functions
Nucleo-F401RE functions

Определяем объём тестирования

Какие функциональные блоки мы хотим проверить у этой платы:

  • Питание

  • Кнопка (1)

  • LED (1)

  • Цифровые входы-выходы, выведенные на разъёмы (2 шт.)

  • MCU

Для имитации реального мира привнесём имитатор дефекта на плате - jumper. Он позволит создать (дефекта нет) или разорвать соединение (дефект есть) между выводами микроконтроллера.

Мы тестируем маленькую партию, поэтому автоматизировать детектирование работы светодиода и активацию кнопки не будем, задействуем для этого глаза и руки тестировщика. Чтобы снизить влияние человеческого фактора, будем варьировать задания и проверять их исполнение человеком вместе с DUT.

Теперь, когда мы точно знаем, что мы тестируем и в каком объёме, можно приступать к разработке тест-плана.

Тестовый план

  1. Проверить наличие приборов;

  2. Проверить наличие прошивки для DUT;

  3. Прошить DUT;

  4. Прочитать серийный номер DUT;

  5. Проверить наличие джампера;

  6. Проверить светодиод, поморгать светодиодом несколько раз, спросить у пользователя количество морганий;

  7. Проверить User Button, попросить пользователя нажать кнопку произвольное количество раз (вариация) на кнопку. Просим ввести это количество в форму.

Теперь мы знаем, что мы тестируем и как. Определимся с составом тестового стенда.

Вычислитель тестовой станции

Любой компьютер с Linux. На Windows пример запустить тоже можно,но не стоит.

Схема стенда

Зафиксируем состав стенда и все связи между компонентами на структурной схеме.

Stand structure
Stand structure

Для тестирования 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/

HardPy operator panel
HardPy operator panel

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

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 12: ↑11 and ↓1+14
Comments14

Articles