вторник, 3 января 2017 г.

SciPro: продолжаем обрабатывать данные на Python

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

SciPro (Scientific Processing) - рабочее название проекта, о котором я уже писал здесь. За прошедшие годы был расширен набор методов, появилась наследственность, расширены функции импорта и пр. Однако, общий принцип принцип остался неизменным - это объектно-ориентированное представление данных. Вот эти объекты:
  • Spectrum - данные формата (x,y), где 'x' может быть задан в длинах волн или частотах, а 'y' в линейном или логарифмическом масштабе
  • Oscillogram - (x,y), где 'x' - время, 'y' - линейный масштаб
  • Field - поле, формат (x,y), где 'y' - массив комплексных чисел, задающийся в алгебраическом или экспоненциальном виде
  • FROGTrace - работа с исходными данными FROG, здесь 'x' - двумерный массив длин волн и временных отстроек, а 'y' - массив измеренных интансивностей (также двумерный).
Кроме методов обработки у объектов также переопределены операции сложения, умножения и пр., что позволяет реализовывать конвеерую обработку данных последовательным вызовом соответствующих методов. На текущий момент данный набор перекрывает большинство моих повседневных потребностей в оперативной обработке и анализе, и я планирую развивать его по мере необходимости и возможности.

пятница, 20 марта 2015 г.

Cross-platform GUI for an OceanOptics spectrometers

И  так, не прошло и года, а прошло целых 4 года...
Однако, есть отличный повод для записи - у меня наконец-то дошли руки, чтобы сделать по крайней мере одну полезную конфигурацию для khameleon-а. Конфигурация предназначена для работы со спектрометрами OceanOptics, поддерживаемыми свободным драйвером SeaBreeze.

Среди основных функций:
  • График спектра с измерениями и экспортом 
  • Установка времени экспозиции
  • "Динамическая экспозиция"
  • Корректировка шумового фона по показаниям с "тёмных пикселей" и/или по "сырому" измерению.
  • Усреднение получаемых спектров
  • Потоковая запись спектров в отдельные бинарные файлы
Найти всё это можно здесь. По ссылке есть исходники и готовые сборки для Windows 7 и Linux (обе для 32-х битных систем). Я надеюсь, что эта сборка может оказаться полезной для кого-либо, кроме меня, и буду благодарен за любую обратную связь :).

пятница, 4 ноября 2011 г.

Khameleon tutorial

И так, после того, как скачаны исходники, и даже прошла компиляция, хочется понять, для чего всё это нужно. В этой заметке я на примерах опишу различные варианты соединения модулей друг с другом, в чём, собственно, и заключается вся суть проекта. Но для начала несколько вводных слов.

Вся функциональность программы заложена в модулях. Другими словами, какие модули мы загрузим, то мы и получим - от простого терминала до программы, которая связывает вместе работу нескольких устройств используя данные с одного для управления другим (например).  Список загружаемых модулей и связи между ними записаны в конфигах. Для каждой конфигурации есть два рабочих конфига с окончаниями .cfg и cfg.user. В первом записана сама конфигурация: виджеты и действия, выносимые на главное окно, опции модулей, определяющие их режим работы (т.н. скрытые опции). Во втором содержатся опции, меняющиеся в процессе работы программы (выбранные директории, отмеченные галочки, параметры окна и пр.). Эти опции автоматически сохраняются при выходе из программы, и загружаются при старте.


Подсунуть конфиг можно разными способами:
1. Через параметр -c (./idpws -c <имя_файла>.cfg). 
2. Сделать симлинк <имя_файла>; на idpws и запускать с симлинка.
3. Запускать idpws без параметров, при этом в качестве .cfg.user будет использоваться khameleon.cfg, а в качестве системного - имя, указанное параметром cfgfile (не рекомендуется).


Теперь, собственно, про конфиги самого tutotial-а. Рассмотрим их в порядке возрастания сложности:


1.  echoterm.cfg
В файле записана конфигурация для соединения друг с другом двух модулей через конвеерный (stream) интерфейс. Рассмотрим значение каждой строчки.  


Раздел [connections] содержит описание соединений между модулями. Формат записи:
<номер_cоединения>="id_<модуль_от>;signal_<имя_сигнала>;id_<модуль_куда>;slot_<имя_слота>" 
Здесь мы фактически средствами Qt соединяем сигнал одного модуля со слотом другого. Предусмотрено по одному сигналу и одному слоту на модель типа stream: через "ImReady(QVariantList)" модуль передаёт обработанные данные дальше по цепочке, а через "StartProcessing(QVariantList)" принимает данные от другого модуля. Есть ещё дополнительный сигнал "myStatus(QString)", через который может передать какие-либо сообщения в главное окно программы, где для этого предусмотрен объект logger со слотом  append(QString), или timedAppend(QString), если требуется отобразить время прихода сообщения.
 

Далее раздел [widgets]. Здесь мы пишем какие виджеты из каких модулей следует выносить на главное окно. Формат следующий:
id_<модуль>\<параметр>=<значение>
 Параметры бывают следующими: wmain, woptions, где значение, это имя заголовка окна или заголовка блока, содержащего данный виджет; wmainobjname  должно быть уникальным для каждого модуля и используется при сохранении параметров окна; woptionsdialog (true или false) - следует ли отображать виджет опций в меню опций программы; ismainlayout (true) - применяется только к одному виджету wmain, и говорит о том, что он является главным, остальные - перемещаемые.



Идём далее, раздел [modules]. Именно в этом разделе мы указываем, какие именно модули необходимо загрузить, а также присваиваем им id. Параметр id должен начинаться с нуля, быть уникальным для каждого модуля, и возрастать без пропусков (т.е. после 3 не может следовать 5). Формат записи как и в предыдущей секции:
id_<модуль>\<параметр>=<значение>
 Отличие в том, что се параметры в данной секции начинаются с точки, что говорит о том, что они являются скрытыми параметрами модуля.  ".name" - имя загружаемого модуля, ".thread" - номер потока, в который необходимо поместить данный модуль. Кроме этих параметров каждый модуль может содержать любое число индивидуальных скрытых параметров. От обычных параметров их отличает то, что они задаются в файле конфигурации и не могут изменятся во время работы программы.  Они предназначены для того, чтобы задавать конкретный вариант поведения модуля в конкретной конфигурации.


Уф, с этим файлом разобрались. Дальше будет проще и интереснее одновременно.


2.  echotermfb.cfg
Пример соединения модуля, использующего потоковый (конвейерный) интерфейс, и модуля, работающего через пропагаторы. Здесь в пояснении нуждается секция [connections], а именно, 2-ое и 3-е соединения. Остальные секции повторяют прошлую конфигурацию.  
02="id_0;out;id_1;slot_StartProcessing(QVariantList)"
03="id_1;signal_ImReady(QVariantList);id_0;in"
Во втором соединении вместо указания сигнала для первого модуля используется имя пропагатора, out. Важно, что в конструкторе модуля в лист "hpg" должен быть довавлен элемент, с именем "out" и значением NULL (создание объекта пропагатора происходит в ядре во время соединения модулей). Также важно заметить, что для связки таких разнородных модулей можно использовать только пропагаторы без обратной связи, сигнал пропагатора "trySendList(QVariantList)", чтобы ловить пришедшие данные, и функцию "sendList(QVariantList)", чтобы отправить данные.


3.  echofbterm.cfg
Это также пример соединения разнородных модулей. Разность в конфиге с предыдущим примером минимальна, но заинтересовавшиеся могут найти немного нового в исходных кодах модулей.


4.  echofbtermfb.cfg
И наконец, пример взаимодействия двух модулей через пропагаторы. Здесь в секции [connections] последние две строчки заменены одной, но в конце появилась подпись "feedback":
02="id_0;interface;id_1;io;feedback"
Что здесь написано: пропагатор модуля id_0 соединить с пропагатором модуля с id_1, при этом первый пропагатор должен дождаться ответа от второго, и только после этого вернуть управления модулю с id_0. В конструкторе модуля такой пропагатор мы уже должны поместить в лист "hpgfb", и вместо функций "send(QByteArray)" и "sendList(QVariantList)" следует использовать read, write, readList и writeList, в зависимости от передаваемых данных. Единственное требование, оба модуля должны быть согласованы по типу данных. Т.е., фактически мы получаем возможность читать и записывать данные в модуль, и неважно, что он работает в другом потоке и живёт своей жизнью. Если заглянуть в код "ведомого" модуля (из которого читают), то там можно найти вызов функции readReturn writeReturn, readListReturn или writeListReturn. Если это обработка ведомого пропагатора, то необходимо вызывать одну из этих функций перед возвратом из обработчика. Какую именно - зависит от выполняемой операции и от типа данных. Данные, положенные в эти функции "вернуться" модулю, вызывающему соответствующие ответные функции из своего пропагатора.


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


воскресенье, 11 сентября 2011 г.

Khameleon is opened

Наконец-то случилось то, о чём было уже столько разговоров! Исходники khameleon теперь доступны на sourceforge.  Теперь можно качать и пробовать, брать за основу уже написанные модули и писать свои, пробовать адаптировать её к новым девайсам. С чего начать? После сборки проекта скопируйте файлы из bin/configs/echo в bin и укажите имя конфига программе при запуске (./idpws -c echoterm.cfg). Здесь записана простейшая конфигурация системы - эхо и терминал завязаны друг на друга. Текст из поля уходит в эхо, а оттуда возвращается в терминал. Для демонстрации оба модуля грузятся в различные потоки. Они связаны простейшим интерфейсом (Stream), аналогичным первоначальной конвейерной концепции. Однако сейчас это не единственная возможность, но об этом в другой заметке, где я подробнее опишу на примерах различные варианты взаимодействия модулей друг с другом.

воскресенье, 30 января 2011 г.

Khameleon Modular System - возрождение

Давненько я ничего не писал про столь важный проект, как модульная система хамелеон. Ровно год, одна неделя и один день, если быть точным. А дело, тем временем, совсем не стояло на месте. Об этих переменах я сегодня и расскажу.

Не буду повторять проблемы, о которых я писал в предыдущем посте, а перейду сразу к решениям.
1. Взамен концепции конвейера пришла концепция взаимодействия модулей через т.н. пропагаторы. Термин пришел из ядерной физики, где он означает "переносчик взаимодействия", или, буквально, от английского "propagate" - распространение. Возможно термин притянут за уши, но лучшего определения той сущности, которую я хотел создать, не нашлось. Это не труба (pipe), из которой к вам сыпется некое нечто, и в которое вы ссыпаете всё скопом. Это не очередь запросов (queue) и не сигнал, в терминологии Qt. Основная идея пропагатора состоит в том, чтобы обеспечить максимально-абстрактный интерфейс взаимодействия модулей друг с другом. Пропагаторы разделены на два подкласса, feedback и stream, т.е. те, что ожидают ответа от модуля и возвращают его (подобно фукциям read/write в блокирующем режиме), и те, посредством которых можно организовать "массовую рассылку" или поток (подобно функциям send/received). Внутри модуля создаётся список пропагаторов, через которые происходит взаимодействие, а ядро при загрузке создаёт сами объекты, и связывает их с другими модулями. Т.о. каждый пропагатор может являться точкой входа в модуль. В примеру, модуль построения графиков multiplot имеет пропагаторы plotxy и collectxy. Первый отвечает за рисование графика целиком, второй - за накопление и отрисовку потока данных. Взаимодействие с пропагатором внутри модуля выглядит как обычный вызов функции.

2. Многопоточность. Так получилось, что реализация многопоточности оказалась тесно связанной с реализацией идеи о пропагаторах. Просто по тому, что сейчас feedback пропагатор реализован таким образом, что связывать им можно только модули, находящиеся в разных потоках, иначе будет deadlock.
Как это работает? При загрузке модуль может попросить (через параметр в конфиге) для своей работы отдельный поток. Ядро стартует новый тред со своим eventloop-ом, и помещает туда модуль, вызывая для него moveToThread(). Поскольку работа пропагаторов крутиться вокруг qt-ной концепции сигнал-слотов, то все вызовы между модулями из разных потоков полностью безопасны.

Казалось бы, зачем так всё усложнять, если требуется "просто" связать два модуля и сделать между ними практически синхронный обмен данными? На этот вопрос я подробно напишу в отдельной заметке. Несомненно, задачу можно было бы решить и в рамках одного потока, и без надуманных пропагаторов, и, возможно, более красиво. Однако, в свой подход я закладываю некоторые плюшки, реализация которых другими способами (имхо) будет уже совсем не тривиальной. Ну например, раздать нескольким модулям задания по обработке, запустить при этом чтение их третьего, после дождаться завершения обработки от первых, и выполнить какие либо действия по результатам их работы. и всё это в виде линейного кода.

На данный момент могу сказать, что с введением многопоточности и концепции пропагаторов жить стало заметно легче, и уже в ближайшее время я выложу кратенький обзор нескольких практически применимых творений. Это будет простая оболочка для снятия данных с измерителя мощности ThorLabs, и ModBus Debugger - приложение для отладки устройств по протоколу modbus (постоянный мониторинг регистров, флагов, запись в регистры).

понедельник, 24 января 2011 г.

ftd2xx library on wine

Сегодня рассмотрим решение проблемы запуска windows-приложений, написанных для различных девайсов и использующих библиотеку ftd2xx (libftd2xx) для связи с ними, под wine. Вначале я кратко расскажу про мотивацию к такой экзотике, потом о принципе реализации и граблях, на которые удалось наступить, и в конце дам ссылочку на патч.

И так, исходные данные, автокоррелятор от Avesta project, подключающийся к ПК по USB, и программа Irtac для работы с прибором. В приборе USB аппаратно реализовано на микросхеме ftdi, что можно увидеть по логам при подключении устройства (dmesg или /var/log/messages). Программа Irtac собрана под windows с использованием коммерческой (закрытой) версии библиотеки ftd2xx. Реализации под linux нет и ждать не приходится, а перегружаться в винду ради одного прибора очень не хочется, тем более, что реально он часто нужен не один. Есть вариант с использованием виртуальной машины. Неопенсорсный virtualbox умеет прокидывать USB в систему, и, например, поляриметр от thorlabs так заставить работать удалось. Ситуацию осложняет то, что такой трюк оборачивается существенными тормозами в работе виртуалки, и Irtac тупо отваливался по таймауту. Под wine сама программа хоть и запускается, но работать с прибором не хочет. Оно и понятно, если я ей подсуну windows-версию библиотеки, то она понятия не имеет как работать с USB  в linux, а linux-версию программа, собранная под винду, по ясным причинам подцепить не сможет. И так, выход один, надо разобраться, как wine реализует свои библиотеки, и написать обёртку (fake) для linux-версии libftd2xx.
Немного рутины - необходимо достать исходники установленного у вас wine, распаковать их, потом наложить прилагаемый патчик, запустить configure и make, а после скопировать ftd2xx.dll.so и ftd2xx.dll.fake в /usr/lib/wine/ftd2xx.dll.so и /usr/lib/wine/fakedlls/ftd2xx.dll (или куда там установлен штатный wine) соответственно. Как получился этот патч? Сначала всё казалось проще пареной репы: у нас есть две библиотеки с идентичными функциями, значит, нам даже толком не надо ничего писать, а просто "связать" вызов функции из программы с той же функцией в нативной библиотеке. Сказано - сделано. Но было бы наивно предполагать, что Irtac сходу заработает, тем более что мы не знаем функции, которые он использует. Для тестирования я взял примеры из libftd2xx1.0.2 и собрал их как под linux, так и под винду (через mingw). Тут количество используемых функций и порядок их вывода строго определён, и можно отследить на каком месте что у нас порушилось. На первом этапе я никак не мог понять, как сделать этот самый вызов функций с одним и тем же названием и метался от простой сборки wine-библиотеки статиком с libftd2xx.a до написания обёртки с другим именем, из которой бы цеплялась нативная библиотека, а сама обёртка использовалась бы нашей fake-либой, и тем самым, мы избежали бы конфликта имён. Однако, найти правильный путь помогла wine-библиотека glu32. Здесь реализовано фактически именно то, что нам необходимо. А вся изюминка кроется в том, что заголовки функций для windows- и для linux-версии ftd2xx немного различаются. Это обстоятельство учтено в ftd2xx.h, и правильные заголовки определяются при сборке define-ами. Нам же необходимо подставить define-ы, которые использовались бы при сборке под виндой (чтобы прога могла найти функции), хотя фактически, собираем мы всё это дело под пингвином. Однако, после этого прозрения вылезла ещё одна проблема, и заключается она вот в чём. Функции, которые мы хотим отдать wine-у на экспорт, перечисляются в ftd2xx.spec, где первый параметр - некий номер функции... Я не понимаю, что это за номер, но на практике, этот номер как-то связан с тем, где именно находится данная конкретная функция. И если мы правильно пишем обёртку, но вместо номера ставим "@", что в соответствии с http://linux.die.net/man/1/winebuild означает автоматическое присвоение номера, то не факт, что этот номер совпадёт с реальным. И при попутке запустить программу получим что-то типа:
wine: Call from 0x7ef918f0 to unimplemented function FTD2XX.dll.32, aborting
wine: Unimplemented function FTD2XX.dll.32 called at address 0x7ef918f0 (thread 0009), starting debugger...
Но это и ключ к разгадке! Если мы знаем, какую именно функцию дергает приложение, а это можно установить по исходникам, то данный вывод означает, что эта функция должна находится по номеру 32, но её там почему-то нет. Ставим этот номер вместо собаки в ftd2xx.spec, и двигаемся дальше...

В результате, я написал обёртки для всех функций из ftd2xx.h кроме тех, где есть префикс W32 (просто я не знаю как уложить их параметры в концепцию wine). Получилось 49 штук. Из них я подобрал номера к 16-ти. Для этой процедуры я использовал примеры  BitMode, EEPROM/read, LargeRead, Simple и Timeouts. Вывод данных программ был идентичен про запуске в linux, и при запуске под wine-ом. В качестве "прибора" использовалась заглушка в виде микросхемы FT232RL с замкнутыми Rx/Tx и питанием от USB-порта. В ближайшее время буду пробовать на реальном железе.
Использование данного материала, а также поправки и исправления неточностей только приветствуются.
Патчик можно найти тут.

upd: Оказывается, кое-что всё-таки "уже украдено до нас". Так, вот тут автор ещё в 2005 году сделал вызов основных функций через libusb. Кроме того, он приводит spec, в котором уже указаны адреса для всех функций.. Что-то мне подсказывает, что их узнать можно гораздо проще, чем я описал выше...

upd2: Поразительно, но мне действительно удалось заставить эту прогу корректно работать с прибором! Однако, обнаружилось пара нюансов:
1. Значения Timeout-ов и LatencyTimer пришлось увеличить в 10 раз возможно, хватило бы и пары, но взял с запасом, и оно попёрло)
2. Если функция FT_ListDevices используется с флагом FT_LIST_BY_INDEX, то перед вызовом нативной функции стоит вызвать FT_CreateDeviceInfoList, т.к. без этого всё или виснет или crash-ится.
Обновлённый патчик со всеми этими изменениями, а также с некоторым количеством отладочной информации для wine-1.2_rc7 можно найти тут.

понедельник, 26 июля 2010 г.

MacrOS: Simply RTOS on Macroses

При всей бесперспективности повторного изобретательства велосипедов порой от этого праздного занятия просто невозможно удержаться. И вот сейчас, зачем нужна ещё одна пародия на RTOS, когда их и так полно, и тут есть несколько соображений. Во-первых, как ни крути, а своя рубашка ближе к телу. И во-вторых, после некоторого опыта писания прошивок под наши приборы я осознал, что для полного счастья мне необходимо совсем не много. Это немного и является отличительной особенностью этого произведения.
1. Таймеры. Программные, с характерной длительностью в 10-тки миллисекунд. Неблокирующие ожидания таймаутов. Wait-ы внутри задачи.
2. Кооперативная многозадачность. В топку приоритеты, вытеснения, жесткое распределение времени. Специфика такая, что прошивка состоит из множества небольших задач, которые не несут сложных расчётов, а большую часть времени ждут срабатывания какого-нить флага или исключения, или запускаются периодически, чтобы собрать данные.
3. Быстрое определение необходимости выполнения задачи, и переход на следующую в противном случае. Переключение контекста - дорогостоящая операция, поэтому хотелось бы понять, чего ждёт задача и проверить это условие не входя в контекст задачи.
Вот, пожалуй, три вещи необходимые для счастья.
Про ОС как таковую тут можно говорить вообще с большой натяжкой. По сути нам необходим лишь планировщик, который бы в цикле перебирал все задачки. Прототипом был планировщик из какого-то примера с www.atmel.com. Уж очень мне нравится фирма, и даташиты толковые, и контроллеры функциональные, производительные и не обременённые ненужными сложностями, и примеры есть, и gcc умеет компилить под avr. Да, изначально MacrOS написана под AVR архитектуру, но для переноса на другой контроллер требуется лишь переписать код для миллисекундного таймера, т.к. всё остальное написано на C. Это в теории. на практике надо переписывать всё, что касается прерываний, а это usart, spi и т.д. Но вернёмся к планировщику. К сожалению, он не умел управлять контекстом, а наличие такой фичи сильно облегчает написание каждой отдельной задачи.
Спасение я нашел в функции setjmp. Но её использование весьма нетривиально, и именно тогда родилась идея, "а почему  бы не обернуть ей вызов в макрос?" Сказано - сделано. Заводим на каждую задачку структуру, типа task, в которой храним контекст и ещё немного инфы, о которой скажу позже. Пишем макрос MOS_Scheduler(), который сохраняет контекст текущей задачи и делает longjmp в контекст планировщика. Всё просто! Планировщик, получив управление, переходит к следующей задаче, сохраняет свой контекст и делает longjmp в следующую task-у, и так по кругу. Но это слишком медленно, учитывая что часто надо проверить лишь состояние флага. Делаем финт ушами, в структуру задачи добавляем ссылку на фнкцию checker и пару переменных, которые будем передавать этой функции как параметры. Одна - указатель, вторая - значение которое надо проверить. После в планировщике вместо перехода в задачу сначала делаем вызов checker-а, а по его результатам определяем, требуется ли переход в задачу. И тут Остапа понесло: можно сделать checker-ы разного типа, которые будут проверять налицие флага, отсутствие, делать anr или or со значением в указателе. В общем, практически всё, что может понадобится для отслеживания нажатия на кнопку или отслаживания аппаратного флага.
Теперь ввести таймеры не составит большого труда. Ставим условие, что проверка на таймаут должна быть быстрой, а старт можно сделать и дольше. Создаём массив с таймерами. Заводим глобальный счётчик, который инктементируется от аппаратного таймера. При старте высчитываем время, когда таймер закончится, и сбрасываем флаг таймаута. В прерывании от аппаратного таймера пробегаемся по всему массиву, и выставляем флаги для тех таймеров, чьи значения меньше текущего. Значения этих флагов позже проверятся в задаче, которая ждёт таймаута.
На этом пока всё, минимально-необходимое окружение есть. Надеюсь, что это окружение будет полезно не только мне, и велосипед приобретёт статус ноу-хау. Исходники имеются и будут выложены, как я приведу их в божеский вид. Нетерпеливым просьба писать на мыло.