Пишем бесплатный Gerber-вьювер с открытым исходным кодом под Android с нуля

Привет! Меня зову Сергей Велеско, я Android разработчик в настоящее время и инженер-конструктор печатных плат в прошлой жизни. В этой статье я расскажу, как мне удалось применить знания, полученные в прошлой профессии, и написать простое Android приложение для просмотра Gerber-файлов.

Немного предыстории

Идея написать просмотрщик/конвертер возникала у меня еще в 2018 году, когда на прошлой работе на шестерых конструкторов была единственная лицензия на софтину для чтения герберов с очень неудобным экспортом в растровое изображение. Экспорт фотошаблонов в растр был необходим для документации и требовал неприличное количество ручных операций и времени. С этим нужно было что-то делать. И нужно было делать что-то с выгоранием от основной работы. В общем, было решено писать десктопный конвертер gerber -> bmp/png на Qt, который бы быстро (в многопоточном режиме) и за один раз конвертил все слои платы в изображения, обрезанные по размеру платы, и имеющие осмысленные названия файлов.

Пришлось вспоминать С++ практически с нуля (за 6 лет после универа без практики забылись даже те примитивные навыки программирования, которые были), сидя вечерами с учебником и выполняя упражнения. Потом знакомство с Qt и изучение спецификации Gerber. За пол года приложение было написано, и успешно использовалось по назначению.

Мне так зашел процесс, что я стал всерьез интересоваться разработкой, и где-то через год Qt привел меня в андроид. Да так удачно привёл, что я сменил профессию конструктора на андроид-разработчика. На момент публикации чуть больше полугода профессионально занимаюсь нативной разработкой под андроид на kotlin. Тут еще на одну статью тянет, но вернемся к главной теме.

Зачем gerber viewer под Android?

Во-первых решил отдать дань уважения печатным платам, так сказать, попрощаться достойно со всем, что связано с проектированием электроники. Во-вторых “обкатать” некоторые актуальные технологии в андроиде. В третьих - в маркете я нашел всего два аналогичных приложения, одно из которых платное, второе - с рекламой. Посчитал это возмутительным. Ближе к делу…

Требования

Как должно выглядеть приложение? Максимально просто. Два экрана. Первый - со списком открытых файлов, второй - с изображением содержимого открытых файлов.

Что должно делать приложение?

1. Открывать гербер файлы

2. Отображать список открытых файлов

3. Каждый элемент списка должен содержать имя файла, переключатель видимости и цвет слоя на экране с изображением.

4. Элемент должен удаляться из списка по свайпу.5. Парсинг и вообще вся обработка должна производиться в отдельном потоке.

6. Изображение должно иметь зум, панораму и управление ими с помощью привычных жестов.

Архитектура проекта

MVVM, потому что 1) подходит под задачу 2) имеет поддержку от гугл в виде architecture components. Также я решил делать проект многомодульным, т.к. сразу можно было выделить относительно независимые части приложения, выполняющие свою функцию. + в планах было выделить в отдельную kotlin библиотеку парсер. Ниже приведена примерная схема приложения с модулями и зависимостями.

Кратко по каждому модулю:

app - главный модуль приложения с экранами, вью моделями и репозиторием.

File Reader - модуль, который читает файлы и загружает их в оперативную память. На выходе список строк.

Syntax Parser - парсит строки из гербера. На выходе список команд.

Graphics Processor - обрабатывает команды. На выходе список графических объектов.

Logger - служебный модуль - обертка над Timber.

Многопоточность

Логично было бы выполнять парсинг файлов и генерацию графических объектов в фоне, начиная сразу после добавления файла в список, показывая какой-нибудь лоадинг. Сначала я посматривал в сторону Rx Java, которая предлагала относительно удобный способ работы с многопоточностью и плюшки в виде производительности при использовании rx источников/подписчиков вместо больших коллекций с командами гербера и графическими объектами. Но учитывая, что Rx теряет популярность, поднадоела за время учебы и используется на работе, было решено использовать корутины, с которыми у меня до этого не было опыта.

UI

Долго не сомневался при выборе, Single Activity (звучит громко, учитывая всего 2 экрана в приложении ;-) ), экраны на Fragment. Compose показался слишком экспериментальным, отпугнул потенциально большими ресурсами на его параллельное освоение.

DI

Koin. Потому что простой и было интересно попробовать что-то кроме dagger. На работе пользуемся Hilt, но даже он показался чересчур сложным для такого простого проекта.

Рисование

View и Canvas. Смотрел в сторону SurfaceView но после непродолжительных экспериментов решил, что и производительности обычной View должно хватить. Естественно с учетом того, что на канве должны отрисовываться полностью готовые объекты, чтобы ничего не создавалось в методе onDraw.

Сборка

В качестве эксперимента перевел все билд скрипты на Kotlin. Для удобства управления версиями запилил builsSrc.

Тестирование

JUnit для тестов. По плану должно быть много юнит-тестов :).

Реализация

Модуль File Reader. Самый простой платформенно зависимый модуль. Все, что он делает - читает файл в список строк.

Модуль Syntax Parser. Gerber-файл представляет собой текстовый файл, описывающий поток gerber команд. Более подробно про Gerber вот тут. Как правило одна команда занимает одну строку. Поэтому было решено читать файл в список строк, и дальше работать с ним, используя регулярные выражения для парсинга конкретных команд. Учитывая относительно большое количество команд, хотелось сделать их последующую обработке в потоке удобной, т.е. без огромного условного оператора, а что бы команды сами могли позаботиться о своей обработке - тут полиморфизм и ничего нового.

Я завел вот такой интерфейс для команд:

interface GerberCommand { val lineNumber: Int fun perform(processor: CommandProcessor) }

Каждая команда реализует этот интерфейс, предоставляя номер строки в файле (необходимо для информативных сообщений о синтаксических ошибках) и реализацию единственного метода perform, в котором команда сообщает некоему CommandProcessor, что она делает.

Использование абстракций позволило выполнить модуль простой kotlin-библиотекой, без зависимостей на платформу. (спасибо дяде Бобу)

В целом работа над парсером была не сильно сложной, довольно рутинной. И таки осложнялась тотальным несоблюдением спецификации Gerber разработчиками САПР (не буду показывать пальцем). Самой интересным этапом в разработке парсера оказалась реализация парсинга макро шаблонов - таких параметризованных моделей, с определениями кастомных графических примитивов и переменными с выражениями внутри.

Вот пример:

%AMVB_RCRECTANGLE* $3=$3X2* 21,1,$1-$3,$2,0,0,0* 21,1,$1,$2-$3,0,0,0* $1=$1/2* $2=$2/2* $3=$3/2* $1=$1-$3* $2=$2-$3* 1,1,$3X2,0-$1,0-$2* 1,1,$3X2,0-$1,$2* 1,1,$3X2,$1,$2* 1,1,$3X2,$1,0-$2* %

Тут пришлось вспомнить реализованный когда-то на С++ парсер математических выражений, основанный на переводе в постфиксную форму и алгоритме сортировочной станции.

Модуль GraphicsProcessor. Тут уже все просто. Нужно, чтобы кто-то реализовал интерфейсы парсера, которые описывают все действия гербер команд, направленных в основном на создание потока графических объектов или служебных объектов, меняющих настройки рисования.

Интерфейс для графических объектов:

interface GraphicsObject { fun draw(canvas: Canvas, penConfig: PenConfig) }

По ходу выполнения программы эти объекты дойдут до самого метода onDraw() в View. Они либо рисуют на канве графический примитив (прямая, дуга), либо добавляют готовый Path (все flash операции, контуры в регионах реализованы с помощью Path), либо меняют настройки канвы (поворот, начало координат) или пера (цвет, размер, способ заливки).

App модуль. Все остальное осталось в app модуле, т.е. весь UI, View Model, Repository и DI.

В Ui все стандартно. На экране со списком - RecyclerView с DiffUtils. На экране с графикой - кастомная View, в которую сеттится набор графических объектов для отрисовки. Все данные экраны получают из вью моделей, которые в свою очередь берут данные из центрального репозитория. Контракт репозитория:

interface GerberRepository { val gerbers: List suspend fun addItem(fileUri: Uri, fileName: String): GerberResult fun removeItem(id: String) fun changeItemVisibility(id: String, visibility: Boolean) }

Cхема взаимодействия экранов, view model’ей и репозитория:

Что в итоге

После нескольких месяцев работы вечерами и по выходным проект был завершен.

Часть функционала по спецификации Gerber осталась не реализована, т.к. он либо не влияет на изображение, либо я не встречал его в реальных файлах за 9 лет работы конструктором и пока отказался от реализации (а время немного поджимало, т.к. перспектива выгореть из-за пет проекта на самом старте профессии - ну такое :) ). В репозитории я отметил все эти моменты в виде небольшой roadmap в readme.

Большое спасибо тем, кто дочитал статью до конца, с радостью отвечу на вопросы в комментариях. Приветствую конструктивную критику. До встречи!

Ссылки на репозиторий и маркет:

0
Комментарии
-3 комментариев
Раскрывать всегда